201 lines
9.4 KiB
Rust
201 lines
9.4 KiB
Rust
//! Integration tests of the settings document: the `craft-settings` store
|
|
//! round trip, recovery of a damaged file, newer schema files, every repair
|
|
//! (SPEC 3, 4.1, 6 item 14). The commands are covered in
|
|
//! [`settings_commands`](super); both run against a throw-away `config.toml`
|
|
//! folder the session points at, so nothing here touches a real config or a
|
|
//! real vault.
|
|
|
|
use notescraft_engine::{AppSettings, Error, SCHEMA_VERSION};
|
|
|
|
use common::{TempVault, temp_config};
|
|
|
|
mod common;
|
|
|
|
/// A temp vault with its own settings folder to load and save against.
|
|
struct Fix {
|
|
config: TempVault,
|
|
}
|
|
|
|
impl Fix {
|
|
#[must_use]
|
|
fn new(name: &str) -> Self {
|
|
Self { config: temp_config(name) }
|
|
}
|
|
}
|
|
|
|
/// The `config.toml` inside the settings folder.
|
|
fn config_file(fix: &Fix) -> std::path::PathBuf {
|
|
fix.config.path().join("config.toml")
|
|
}
|
|
|
|
/// (settings, notes) from the settings folder of `fix`.
|
|
fn load_from(config: &TempVault) -> (AppSettings, Vec<String>) {
|
|
notescraft_engine::load_at(config.path())
|
|
}
|
|
|
|
/// Defaults survive the TOML round trip with no notes at all.
|
|
#[test]
|
|
fn defaults_round_trip() {
|
|
let fix = Fix::new("round-trip");
|
|
let before = AppSettings::default();
|
|
notescraft_engine::save_at(fix.config.path(), &before).expect("save the defaults");
|
|
assert!(config_file(&fix).exists(), "the store writes the file even for defaults");
|
|
|
|
let (settings, notes) = load_from(&fix.config);
|
|
assert_eq!(settings, before);
|
|
assert_eq!(notes, &[] as &[String], "{notes:?}");
|
|
}
|
|
|
|
/// A damaged `config.toml` is moved aside, defaults are in use and the note
|
|
/// says where the bytes went.
|
|
#[test]
|
|
fn a_corrupt_file_is_recovered_with_a_note() {
|
|
let fix = Fix::new("corrupt");
|
|
let broken = "this is = not toml ][";
|
|
std::fs::write(config_file(&fix), broken).expect("write the broken file");
|
|
|
|
let (settings, notes) = load_from(&fix.config);
|
|
assert_eq!(settings, AppSettings::default());
|
|
assert_eq!(notes.len(), 1, "{notes:?}");
|
|
assert!(notes[0].starts_with("the damaged config.toml was kept at "), "{}", notes[0]);
|
|
assert!(notes[0].contains("and the defaults are in use"), "{}", notes[0]);
|
|
|
|
let kept: Vec<_> = std::fs::read_dir(fix.config.path())
|
|
.unwrap_or_else(|error| panic!("read the config dir: {error}"))
|
|
.filter_map(Result::ok)
|
|
.map(|entry| entry.file_name().to_string_lossy().into_owned())
|
|
.collect();
|
|
assert!(kept.iter().any(|name| name.starts_with("config.broken-")), "kept = {kept:?}, the corrupt bytes are kept");
|
|
}
|
|
|
|
/// A new `schema_version` file from a newer build is read (its known sections
|
|
/// are in use) but never overwritten.
|
|
#[test]
|
|
fn a_newer_schema_file_is_read_but_not_saved() {
|
|
let fix = Fix::new("newer");
|
|
std::fs::write(config_file(&fix), b"schema_version = 2\n\n[session]\ndaily_folder = \"Notes\"\n\n[hotkeys]\n\"note.read\" = \"Mod-r\"\n")
|
|
.expect("write the newer file");
|
|
|
|
let (settings, notes) = load_from(&fix.config);
|
|
assert_eq!(settings.session.daily_folder, "Notes", "known keys are still usable");
|
|
assert_eq!(settings.hotkeys.get("note.read").map(String::as_str), Some("Mod-r"));
|
|
let joined = notes.join("\n");
|
|
assert!(joined.contains("config.toml is read-only: config.toml has schema version 2"), "{joined}");
|
|
|
|
let refused = notescraft_engine::save_at(fix.config.path(), &AppSettings::default());
|
|
assert!(matches!(refused, Err(Error::Settings(ref message)) if message.contains("read-only")), "{refused:?}");
|
|
let text = std::fs::read_to_string(config_file(&fix)).unwrap_or_default();
|
|
assert!(text.starts_with("schema_version = 2"), "the newer file is untouched: {text}");
|
|
}
|
|
|
|
/// A file one version older runs the (empty) migration hook per step and gets
|
|
/// the migration note.
|
|
#[test]
|
|
fn an_older_schema_file_migrates() {
|
|
let fix = Fix::new("migrate");
|
|
std::fs::write(config_file(&fix), b"schema_version = 0\n\n[session]\ntheme = \"latte\"\n").expect("write the old file");
|
|
|
|
let (settings, notes) = load_from(&fix.config);
|
|
assert_eq!(settings.session.theme, "latte");
|
|
assert!(notes.iter().any(|note| note.contains("config.toml was migrated from schema version 0")), "{notes:?}");
|
|
// The migrated document is written back at the current version, too.
|
|
notescraft_engine::save_at(fix.config.path(), &settings).expect("the migrated file can be saved over");
|
|
let text = std::fs::read_to_string(config_file(&fix)).unwrap_or_default();
|
|
assert!(text.contains(&format!("schema_version = {SCHEMA_VERSION}")), "{text}");
|
|
}
|
|
|
|
/// A section that parses as TOML but does not fit the document is replaced by
|
|
/// its defaults and reported, while the other sections stay.
|
|
#[test]
|
|
fn a_broken_section_is_reset_with_a_note() {
|
|
let fix = Fix::new("broken-section");
|
|
std::fs::write(config_file(&fix), b"schema_version = 1\n\nhotkeys = \"oops\"\n\n[session]\ntheme = \"latte\"\n").expect("write");
|
|
|
|
let (settings, notes) = load_from(&fix.config);
|
|
assert_eq!(settings.hotkeys, std::collections::BTreeMap::new(), "the broken section is at its defaults");
|
|
assert_eq!(settings.session.theme, "latte", "the other sections survive");
|
|
assert!(notes.iter().any(|note| note.starts_with("the `hotkeys` section was replaced by its defaults")), "{notes:?}");
|
|
}
|
|
|
|
/// A hand-written file with bad values is repaired on load, the notes say
|
|
/// what was repaired, and the next save stores the repaired document.
|
|
#[test]
|
|
fn bad_values_in_a_file_are_repaired_with_notes() {
|
|
let fix = Fix::new("repairs");
|
|
let big = "x".repeat(notescraft_engine::MAX_UI_BYTES * 2);
|
|
let document = format!(
|
|
"schema_version = 1\n\nui = {{ theme = \"latte\", blob = \"{big}\" }}\n\nrecent_vaults = [\"b\", \"b\", \"\", \"a\"]\n\n[session]\ndaily_folder = \"../up\"\n\n[hotkeys]\n\"bad key\" = \"Mod-r\"\n\"ok\" = \"not ok!!\"\n"
|
|
);
|
|
std::fs::write(config_file(&fix), document).expect("write the file with repairs");
|
|
|
|
let (settings, notes) = load_from(&fix.config);
|
|
let joined = notes.join("\n");
|
|
assert!(joined.contains("session.daily_folder"), "{joined}");
|
|
assert!(joined.contains("ui: the opaque ui state is dropped"), "{joined}");
|
|
assert!(joined.contains("recent_vaults: kept"), "{joined}");
|
|
assert!(joined.contains("hotkeys: dropped"), "{joined}");
|
|
assert_eq!(settings.session.daily_folder, "Daily");
|
|
assert_eq!(settings.ui, AppSettings::default().ui);
|
|
assert_eq!(settings.recent_vaults, ["b", "a"].iter().map(|s| (*s).to_owned()).collect::<Vec<String>>());
|
|
assert_eq!(settings.hotkeys, std::collections::BTreeMap::new());
|
|
|
|
notescraft_engine::save_at(fix.config.path(), &settings).expect("save");
|
|
let (again, again_notes) = load_from(&fix.config);
|
|
assert_eq!(again, settings, "the repaired values stay over a save");
|
|
// The raw keys craft-settings preserves (hand edits survive a merge) are
|
|
// still in the file, so they are repaired again, with the same notes.
|
|
let wanted: Vec<&String> = again_notes.iter().filter(|note| note.starts_with("ui:") || note.starts_with("hotkeys:")).collect();
|
|
assert_eq!(wanted.len(), 2, "{again_notes:?}");
|
|
}
|
|
|
|
/// The recents list keeps its order through the file round trip.
|
|
#[test]
|
|
fn recents_keep_their_order_through_the_file() {
|
|
let fix = Fix::new("recents");
|
|
let settings = AppSettings { recent_vaults: ["zed", "old", "new"].iter().map(|s| (*s).to_owned()).collect(), ..AppSettings::default() };
|
|
notescraft_engine::save_at(fix.config.path(), &settings).expect("save");
|
|
|
|
let (loaded, notes) = load_from(&fix.config);
|
|
assert_eq!(notes, &[] as &[String], "{notes:?}");
|
|
assert_eq!(loaded.recent_vaults, settings.recent_vaults, "newest first, in order");
|
|
}
|
|
|
|
/// The attachment cap lives in the nested `session.files` group and is set and
|
|
/// repaired like every other session setting (SPEC 9).
|
|
#[test]
|
|
fn the_attachment_cap_is_a_session_files_setting() {
|
|
use notescraft_engine::{FilesConfig, Session, SessionConfig};
|
|
|
|
let vault = temp_config("attach-cap-setting");
|
|
let mut session = Session::open(vault.path()).expect("open");
|
|
session.use_settings_dir(temp_config("attach-cap-cfg").path());
|
|
assert_eq!(session.config().files.max_attachment_mib, 64, "the default");
|
|
|
|
let mut registry = notescraft_engine::Registry::new();
|
|
notescraft_engine::register_core(&mut registry).expect("register");
|
|
let set = |session: &mut Session, key: &str, value: i64| {
|
|
let params = serde_json::json!({ "key": key, "value": value });
|
|
registry.run("settings.set", session, ¶ms)
|
|
};
|
|
|
|
let ok = set(&mut session, "session.files.max_attachment_mib", 128);
|
|
assert!(ok.is_ok(), "{ok:?}");
|
|
assert_eq!(session.config().files.max_attachment_mib, 128, "applied to the live session");
|
|
|
|
// 0 is refused at the config boundary...
|
|
let config = SessionConfig { files: FilesConfig { max_attachment_mib: 0 }, ..SessionConfig::default() };
|
|
assert!(config.validate().is_err(), "0 is refused");
|
|
|
|
// ...and repaired to the default with a note through the settings document.
|
|
let repaired = set(&mut session, "session.files.max_attachment_mib", 0);
|
|
assert!(repaired.is_ok(), "{repaired:?}");
|
|
assert_eq!(session.config().files.max_attachment_mib, 64, "repaired to the default");
|
|
let notes = repaired
|
|
.unwrap_or_default()
|
|
.get("notes")
|
|
.and_then(serde_json::Value::as_array)
|
|
.map(|notes| notes.iter().filter_map(|note| note.as_str()).collect::<Vec<&str>>().join("; "))
|
|
.unwrap_or_default();
|
|
assert!(notes.contains("session.files.max_attachment_mib"), "{notes}");
|
|
}
|