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()); }