132 lines
4.6 KiB
Rust
132 lines
4.6 KiB
Rust
use host::MetadataStore;
|
|
|
|
#[test]
|
|
fn missing_reads_do_not_create_state() {
|
|
let temporary = tempfile::tempdir().unwrap();
|
|
let directory = temporary.path().join("state");
|
|
let store = MetadataStore::new(&directory).unwrap();
|
|
assert_eq!(store.get("missing").unwrap(), None);
|
|
assert!(!directory.exists());
|
|
}
|
|
|
|
#[test]
|
|
fn utf8_values_survive_reopening_and_same_key_replacement() {
|
|
let temporary = tempfile::tempdir().unwrap();
|
|
let directory = temporary.path().join("日本語");
|
|
let store = MetadataStore::new(&directory).unwrap();
|
|
store.put("host.version", "0.1.0").unwrap();
|
|
store.put("project.note", "日本語・中文・😀").unwrap();
|
|
let reopened = MetadataStore::new(&directory).unwrap();
|
|
assert_eq!(
|
|
reopened.get("project.note").unwrap().as_deref(),
|
|
Some("日本語・中文・😀")
|
|
);
|
|
reopened.put("host.version", "0.2.0").unwrap();
|
|
assert_eq!(store.get("host.version").unwrap().as_deref(), Some("0.2.0"));
|
|
assert_eq!(
|
|
store.get("project.note").unwrap().as_deref(),
|
|
Some("日本語・中文・😀")
|
|
);
|
|
let record: serde_json::Value =
|
|
serde_json::from_slice(&std::fs::read(directory.join("project.note.json")).unwrap())
|
|
.unwrap();
|
|
assert_eq!(record["format_version"], 1);
|
|
assert_eq!(record["key"], "project.note");
|
|
}
|
|
|
|
#[test]
|
|
fn keys_cannot_escape_the_state_directory() {
|
|
let temporary = tempfile::tempdir().unwrap();
|
|
let directory = temporary.path().join("state");
|
|
let store = MetadataStore::new(&directory).unwrap();
|
|
for key in [
|
|
"",
|
|
"../outside",
|
|
"/outside",
|
|
"nested/key",
|
|
"nested\\key",
|
|
".hidden",
|
|
"日本語",
|
|
] {
|
|
assert!(store.put(key, "invalid").is_err(), "{key}");
|
|
assert!(store.get(key).is_err(), "{key}");
|
|
}
|
|
assert!(store.put(&"x".repeat(129), "invalid").is_err());
|
|
assert!(!directory.exists());
|
|
}
|
|
|
|
#[test]
|
|
fn corrupt_or_mismatched_records_fail_without_being_rewritten() {
|
|
let temporary = tempfile::tempdir().unwrap();
|
|
let store = MetadataStore::new(temporary.path()).unwrap();
|
|
let path = temporary.path().join("host.version.json");
|
|
for bytes in [
|
|
b"not-json".as_slice(),
|
|
b"\xff".as_slice(),
|
|
br#"{"format_version":2,"key":"host.version","value":"old"}"#.as_slice(),
|
|
br#"{"format_version":1,"key":"wrong","value":"old"}"#.as_slice(),
|
|
] {
|
|
std::fs::write(&path, bytes).unwrap();
|
|
assert!(store.get("host.version").is_err());
|
|
assert_eq!(std::fs::read(&path).unwrap(), bytes);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn concurrent_replacements_expose_complete_records_and_preserve_other_keys() {
|
|
let temporary = tempfile::tempdir().unwrap();
|
|
let store = MetadataStore::new(temporary.path()).unwrap();
|
|
store.put("independent", "keep").unwrap();
|
|
let a = "a".repeat(8192);
|
|
let b = "b".repeat(8192);
|
|
store.put("shared", &a).unwrap();
|
|
std::thread::scope(|scope| {
|
|
for value in [&a, &b] {
|
|
let store = &store;
|
|
scope.spawn(move || {
|
|
for _ in 0..8 {
|
|
store.put("shared", value).unwrap();
|
|
}
|
|
});
|
|
}
|
|
for _ in 0..64 {
|
|
let value = store.get("shared").unwrap().unwrap();
|
|
assert!(value == a || value == b);
|
|
}
|
|
});
|
|
assert_eq!(store.get("independent").unwrap().as_deref(), Some("keep"));
|
|
assert!(std::fs::read_dir(temporary.path()).unwrap().all(|entry| {
|
|
!entry
|
|
.unwrap()
|
|
.file_name()
|
|
.to_string_lossy()
|
|
.ends_with(".tmp")
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_import_is_repeatable_and_preserves_newer_file_values() {
|
|
let temporary = tempfile::tempdir().unwrap();
|
|
let store = MetadataStore::new(temporary.path()).unwrap();
|
|
store.put("host.version", "new").unwrap();
|
|
let input =
|
|
br#"[{"key":"host.version","value":"old"},{"key":"project.note","value":"legacy"}]"#;
|
|
assert_eq!(store.import_missing(input.as_slice()).unwrap(), 1);
|
|
assert_eq!(store.import_missing(input.as_slice()).unwrap(), 0);
|
|
assert_eq!(store.get("host.version").unwrap().as_deref(), Some("new"));
|
|
assert_eq!(
|
|
store.get("project.note").unwrap().as_deref(),
|
|
Some("legacy")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_import_keys_are_rejected_before_any_record_is_written() {
|
|
let temporary = tempfile::tempdir().unwrap();
|
|
let directory = temporary.path().join("state");
|
|
let store = MetadataStore::new(&directory).unwrap();
|
|
let input = br#"[{"key":"valid","value":"keep"},{"key":"../outside","value":"invalid"}]"#;
|
|
assert!(store.import_missing(input.as_slice()).is_err());
|
|
assert!(!directory.exists());
|
|
}
|