//! Integration tests of the property maintenance commands: renaming a key //! in every note, the conflicts, the dry run, the values of a key and the //! keys of the vault. mod common; use common::{on_disk, seed, temp_vault}; use notescraft_engine::{Error, Registry, Session, register_core}; use serde_json::{Value, json}; /// A registry with every core command. fn registry() -> Registry { let mut reg = Registry::new(); register_core(&mut reg).expect("registry"); reg } /// The code of a command failure, when the outcome is one. fn failure_code(outcome: &Result) -> Option { match outcome { Err(Error::Command { source, .. }) => Some(source.code.clone()), _ => None, } } /// The paths of a report array, empty when it is not there. fn paths(report: &Value, name: &str) -> Vec { report.get(name).and_then(Value::as_array).map(|rows| rows.iter().filter_map(|row| row.as_str().map(str::to_owned)).collect()).unwrap_or_default() } /// A key is renamed in every note that holds it; a note that already holds /// the new key is a conflict and is left alone. #[test] fn a_key_is_renamed_in_every_note() { let vault = temp_vault("prop-rename"); seed( vault.path(), &[ ("a.md", "---\nstatus: open\n---\nbody a\n"), ("b.md", "---\nstatus: closed\n---\nbody b\n"), ("c.md", "---\nstatus: open\n---\nbody c\n"), ("d.md", "---\nstatus: open\n---\nbody d\n"), ("e.md", "---\nstatus: open\nstate: yes\n---\nbody e\n"), ("f.md", "no frontmatter here\n"), ], ); let mut session = Session::open(vault.path()).expect("session"); let reg = registry(); let report = reg.run("properties.rename_all", &mut session, &json!({ "from": "status", "to": "state" })).expect("rename"); assert_eq!(report.get("renamed"), Some(&json!(4)), "{report}"); assert_eq!(paths(&report, "notes"), vec![String::from("a.md"), String::from("b.md"), String::from("c.md"), String::from("d.md")], "{report}"); assert_eq!(paths(&report, "conflicts"), vec![String::from("e.md")], "{report}"); assert_eq!(on_disk(vault.path(), "a.md"), "---\nstate: open\n---\nbody a\n"); assert_eq!(on_disk(vault.path(), "e.md"), "---\nstatus: open\nstate: yes\n---\nbody e\n", "the conflict is left alone"); assert_eq!(on_disk(vault.path(), "f.md"), "no frontmatter here\n", "a note without the key is left alone"); } /// The body of a note survives the rename, byte for byte. #[test] fn the_body_of_a_note_is_untouched() { let vault = temp_vault("prop-body"); let body = "# Title\n\nA rule ---\n\nstatus: not a key\n"; seed(vault.path(), &[("a.md", &format!("---\nstatus: open\n---\n{body}"))]); let mut session = Session::open(vault.path()).expect("session"); let reg = registry(); reg.run("properties.rename_all", &mut session, &json!({ "from": "status", "to": "state" })).expect("rename"); assert_eq!(on_disk(vault.path(), "a.md"), format!("---\nstate: open\n---\n{body}")); } /// A dry run reports what would change and writes nothing at all. #[test] fn a_dry_run_changes_no_file() { let vault = temp_vault("prop-dry-run"); seed(vault.path(), &[("a.md", "---\nstatus: open\n---\nbody\n")]); let mut session = Session::open(vault.path()).expect("session"); let reg = registry(); let before = on_disk(vault.path(), "a.md"); let report = reg.run("properties.rename_all", &mut session, &json!({ "from": "status", "to": "state", "dry_run": true })).expect("dry run"); assert_eq!(report.get("renamed"), Some(&json!(1)), "{report}"); assert_eq!(report.get("dry_run"), Some(&json!(true)), "{report}"); assert_eq!(on_disk(vault.path(), "a.md"), before, "the dry run wrote nothing"); reg.run("properties.rename_all", &mut session, &json!({ "from": "status", "to": "state" })).expect("rename"); assert_eq!(on_disk(vault.path(), "a.md"), "---\nstate: open\n---\nbody\n"); } /// The values of a key are counted, and the items of a list count one by /// one. #[test] fn the_values_of_a_key_are_counted() { let vault = temp_vault("prop-values"); seed( vault.path(), &[ ("a.md", "---\nlabels: [work, home]\nstatus: open\n---\n"), ("b.md", "---\nlabels: [work]\nstatus: closed\n---\n"), ("c.md", "---\nlabels:\n - work\n - away\nstatus: open\n---\n"), ], ); let mut session = Session::open(vault.path()).expect("session"); let reg = registry(); let report = reg.run("properties.values", &mut session, &json!({ "key": "labels" })).expect("values"); assert_eq!(report.get("type"), Some(&json!("list")), "{report}"); assert_eq!(report.get("total_notes"), Some(&json!(3)), "{report}"); assert_eq!( report.get("values"), Some(&json!([ { "value": "work", "count": 3 }, { "value": "away", "count": 1 }, { "value": "home", "count": 1 }, ])), "{report}" ); let scalars = reg.run("properties.values", &mut session, &json!({ "key": "status" })).expect("values"); assert_eq!(scalars.get("type"), Some(&json!("text")), "{scalars}"); assert_eq!(scalars.get("values"), Some(&json!([{ "value": "open", "count": 2 }, { "value": "closed", "count": 1 }])), "{scalars}"); let limited = reg.run("properties.values", &mut session, &json!({ "key": "labels", "limit": 1 })).expect("values"); assert_eq!(limited.get("values"), Some(&json!([{ "value": "work", "count": 3 }])), "{limited}"); } /// Every key of the vault is listed, the most used first. #[test] fn the_keys_of_the_vault_are_sorted_by_count() { let vault = temp_vault("prop-keys"); seed(vault.path(), &[("a.md", "---\nstatus: open\nlabels: [work]\n---\n"), ("b.md", "---\nstatus: closed\n---\n"), ("c.md", "---\nstatus: open\n---\n")]); let mut session = Session::open(vault.path()).expect("session"); let reg = registry(); let report = reg.run("properties.keys", &mut session, &json!({})).expect("keys"); assert_eq!( report.get("keys"), Some(&json!([ { "key": "status", "type": "text", "count": 3 }, { "key": "labels", "type": "list", "count": 1 }, ])), "{report}" ); } /// A property name may hold spaces. #[test] fn a_key_with_spaces_works() { let vault = temp_vault("prop-spaces"); seed(vault.path(), &[("a.md", "---\nmy key: value\n---\nbody\n")]); let mut session = Session::open(vault.path()).expect("session"); let reg = registry(); let report = reg.run("properties.rename_all", &mut session, &json!({ "from": "my key", "to": "other key" })).expect("rename"); assert_eq!(report.get("renamed"), Some(&json!(1)), "{report}"); assert_eq!(on_disk(vault.path(), "a.md"), "---\nother key: value\n---\nbody\n"); } /// A name that cannot be a frontmatter key is refused, on both sides of the /// rename. #[test] fn an_invalid_key_is_refused() { let vault = temp_vault("prop-invalid"); seed(vault.path(), &[("a.md", "---\nstatus: open\n---\n")]); let mut session = Session::open(vault.path()).expect("session"); let reg = registry(); for key in ["", "a:b", "a\nb"] { let from = reg.run("properties.rename_all", &mut session, &json!({ "from": key, "to": "state" })); assert_eq!(failure_code(&from), Some(String::from("invalid_params")), "`{key}` must be refused"); let to = reg.run("properties.rename_all", &mut session, &json!({ "from": "status", "to": key })); assert_eq!(failure_code(&to), Some(String::from("invalid_params")), "`{key}` must be refused"); let values = reg.run("properties.values", &mut session, &json!({ "key": key })); assert_eq!(failure_code(&values), Some(String::from("invalid_params")), "`{key}` must be refused"); } assert_eq!(on_disk(vault.path(), "a.md"), "---\nstatus: open\n---\n", "nothing was written"); } /// `.obsidian/types.json` is read-only: the report says the old key had a /// type there and the file is untouched. #[test] fn the_types_file_is_read_only() { let vault = temp_vault("prop-types"); seed(vault.path(), &[("a.md", "---\nstatus: open\n---\n"), (".obsidian/types.json", "{\"types\":{\"status\":\"text\"}}")]); let mut session = Session::open(vault.path()).expect("session"); let reg = registry(); let report = reg.run("properties.rename_all", &mut session, &json!({ "from": "status", "to": "state" })).expect("rename"); assert_eq!(report.get("types_json_note"), Some(&json!(true)), "{report}"); assert_eq!(on_disk(vault.path(), ".obsidian/types.json"), "{\"types\":{\"status\":\"text\"}}", "the types file is never edited"); } /// A note open in the editor gets one undoable edit, the disk waits. #[test] fn an_open_buffer_gets_one_undoable_edit() { let vault = temp_vault("prop-buffer"); seed(vault.path(), &[("a.md", "---\nstatus: open\n---\nbody\n")]); let mut session = Session::open(vault.path()).expect("session"); session.open_buffer("a.md").expect("buffer"); let reg = registry(); reg.run("properties.rename_all", &mut session, &json!({ "from": "status", "to": "state" })).expect("rename"); let buffer = session.buffer("a.md").expect("open buffer"); assert_eq!(buffer.text(), "---\nstate: open\n---\nbody\n"); assert!(buffer.is_dirty(), "the buffer edit marks the note dirty"); assert!(buffer.can_undo(), "one undo step covers the rewrite"); assert_eq!(on_disk(vault.path(), "a.md"), "---\nstatus: open\n---\nbody\n", "the disk still holds what was saved last"); let edit = session.buffer_mut("a.md").expect("buffer"); edit.undo().expect("undo"); assert_eq!(edit.text(), "---\nstatus: open\n---\nbody\n", "undo takes the rename back"); } /// A read-only session refuses the write and keeps the two reads. #[test] fn a_read_only_session_refuses_the_write_only() { let vault = temp_vault("prop-read-only"); seed(vault.path(), &[("a.md", "---\nstatus: open\n---\n")]); let mut session = Session::open_read_only(vault.path()).expect("read-only session"); let reg = registry(); assert!(!reg.is_enabled("properties.rename_all", &session)); assert!(reg.is_enabled("properties.values", &session)); assert!(reg.is_enabled("properties.keys", &session)); assert!(matches!(reg.run("properties.rename_all", &mut session, &json!({ "from": "status", "to": "state" })), Err(Error::Disabled(_)))); assert!(reg.run("properties.values", &mut session, &json!({ "key": "status" })).is_ok()); assert!(reg.run("properties.keys", &mut session, &json!({})).is_ok()); assert_eq!(on_disk(vault.path(), "a.md"), "---\nstatus: open\n---\n"); }