7082b3cec7
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
157 lines
8.2 KiB
Rust
157 lines
8.2 KiB
Rust
//! Integration tests of the session: opening, note operations, rename with link
|
|
//! rewriting, buffers and external changes.
|
|
|
|
mod common;
|
|
|
|
use common::{linked_vault, on_disk, seed, temp_vault};
|
|
use notescraft_engine::{Error, Session};
|
|
|
|
/// Twenty notes and one attachment land in the index, and nothing is reported.
|
|
#[test]
|
|
fn open_indexes_every_note_and_attachment() {
|
|
let vault = linked_vault("open", 20);
|
|
seed(vault.path(), &[("img/pic.png", "not really a png"), ("data/notes.txt", "text")]);
|
|
let session = Session::open(vault.path()).expect("open the vault");
|
|
|
|
assert_eq!(session.index().len(), 20);
|
|
assert_eq!(session.index().files(), vec!["data/notes.txt", "img/pic.png"]);
|
|
assert_eq!(session.index().notes().len(), 20);
|
|
assert_eq!(session.index().unresolved().len(), 0, "the ring of links resolves");
|
|
assert_eq!(session.diagnostics(), &[] as &[String]);
|
|
assert!(!session.is_read_only());
|
|
assert_eq!(session.vault().root(), std::fs::canonicalize(vault.path()).expect("canonical root"));
|
|
}
|
|
|
|
/// A file that cannot be read is skipped with a diagnostic, never an error.
|
|
#[test]
|
|
fn a_broken_file_is_a_diagnostic_not_a_failure() {
|
|
let vault = linked_vault("diagnostic", 3);
|
|
seed(vault.path(), &[("bad.md", "")]);
|
|
// A note that is not valid UTF-8: reading it fails, the open must not.
|
|
std::fs::write(vault.path().join("broken.md"), [0xff, 0xfe, 0x00]).expect("write invalid utf-8");
|
|
let session = Session::open(vault.path()).expect("open the vault");
|
|
|
|
assert_eq!(session.index().len(), 4, "the three ring notes and bad.md are indexed");
|
|
assert!(!session.index().notes().contains(&"broken.md"));
|
|
assert_eq!(session.diagnostics().len(), 1, "{:?}", session.diagnostics());
|
|
assert!(session.diagnostics()[0].starts_with("broken.md: "), "{:?}", session.diagnostics());
|
|
}
|
|
|
|
/// A read-only session indexes exactly like a writable one and refuses writes.
|
|
#[test]
|
|
fn a_read_only_session_refuses_every_write() {
|
|
let vault = linked_vault("read-only", 2);
|
|
let mut session = Session::open_read_only(vault.path()).expect("open read-only");
|
|
|
|
assert!(session.is_read_only());
|
|
assert_eq!(session.index().len(), 2);
|
|
assert!(session.read_note("note-00.md").is_ok(), "reading still works");
|
|
assert_eq!(session.read_note("note-00.md").expect("read"), "# note-00\n\n[[note-01]] #work/tag\n");
|
|
|
|
session.open_buffer("note-00.md").expect("read-only sessions can still open a buffer");
|
|
|
|
for error in [
|
|
session.write_note("note-00.md", "x").expect_err("write"),
|
|
session.create_note("new.md", "x").expect_err("create"),
|
|
session.delete_note("note-00.md").expect_err("delete"),
|
|
session.rename_note("note-00.md", "other.md", true).expect_err("rename"),
|
|
session.save_buffer("note-00.md").expect_err("save"),
|
|
] {
|
|
assert_eq!(error, Error::ReadOnly, "{error:?}");
|
|
}
|
|
assert!(session.index().note("new.md").is_none(), "nothing was created");
|
|
assert!(!vault.path().join("new.md").exists(), "no file was created");
|
|
}
|
|
|
|
/// Create, write and delete keep the vault, the index and the buffers in step.
|
|
#[test]
|
|
fn notes_can_be_created_written_and_deleted() {
|
|
let vault = temp_vault("crud");
|
|
let mut session = Session::open(vault.path()).expect("open");
|
|
|
|
session.create_note("sub/One.md", "# One\n[[Two]]\n").expect("create");
|
|
assert_eq!(session.index().len(), 1);
|
|
assert_eq!(on_disk(vault.path(), "sub/One.md"), "# One\n[[Two]]\n");
|
|
|
|
assert_eq!(session.create_note("sub/One.md", "again"), Err(Error::Exists(String::from("sub/One.md"))));
|
|
assert_eq!(session.create_note("sub/notes.txt", "nope"), Err(Error::InvalidPath(String::from("`sub/notes.txt` is not a note: it must end in `.md`"))));
|
|
|
|
assert!(session.write_note("sub/One.md", "# One\n[[Two]] #new\n").expect("write"), "the bytes changed");
|
|
assert!(!session.write_note("sub/One.md", "# One\n[[Two]] #new\n").expect("write again"), "an unchanged write saves nothing");
|
|
assert_eq!(session.index().note("sub/One.md").map(|note| note.tags.clone()), Some(vec![String::from("new")]));
|
|
|
|
session.delete_note("sub/One.md").expect("delete");
|
|
assert_eq!(session.index().len(), 0);
|
|
assert!(!vault.path().join("sub/One.md").exists());
|
|
assert!(vault.path().join(".notescraft/trash").exists(), "the file went to the vault trash");
|
|
}
|
|
|
|
/// Every link form keeps its alias and subpath, and still resolves afterwards.
|
|
#[test]
|
|
fn rename_rewrites_every_inbound_link_form() {
|
|
let vault = temp_vault("rename");
|
|
let source = "[[Alpha]] [[Alpha|shown]] [[Alpha#Head]] ![[Alpha]] [[folder/Alpha]] [[/folder/Alpha]] [md](Alpha.md#Head)\n";
|
|
seed(vault.path(), &[("folder/Alpha.md", "# Head\n\nbody\n"), ("hub.md", source), ("lonely.md", "no links\n")]);
|
|
let mut session = Session::open(vault.path()).expect("open");
|
|
assert_eq!(session.index().backlinks("folder/Alpha.md").len(), 7);
|
|
|
|
let report = session.rename_note("folder/Alpha.md", "beta/Beta.md", true).expect("rename");
|
|
assert_eq!(report.rewritten, vec![String::from("hub.md")]);
|
|
assert_eq!(on_disk(vault.path(), "hub.md"), "[[Beta]] [[Beta|shown]] [[Beta#Head]] ![[Beta]] [[beta/Beta]] [[/beta/Beta]] [md](beta/Beta.md#Head)\n");
|
|
assert_eq!(on_disk(vault.path(), "beta/Beta.md"), "# Head\n\nbody\n", "the content moved untouched");
|
|
assert!(!vault.path().join("folder/Alpha.md").exists());
|
|
assert_eq!(on_disk(vault.path(), "lonely.md"), "no links\n", "a note without links is not rewritten");
|
|
assert_eq!(session.index().len(), 3);
|
|
assert_eq!(session.index().backlinks("beta/Beta.md").len(), 7);
|
|
assert!(session.index().unresolved().is_empty(), "{:?}", session.index().unresolved());
|
|
}
|
|
|
|
/// With `update_links` off the file moves and the links are left broken.
|
|
#[test]
|
|
fn rename_without_link_updates_leaves_the_links_alone() {
|
|
let vault = temp_vault("rename-plain");
|
|
seed(vault.path(), &[("Alpha.md", "body\n"), ("hub.md", "[[Alpha]]\n")]);
|
|
let mut session = Session::open(vault.path()).expect("open");
|
|
|
|
let report = session.rename_note("Alpha.md", "Beta.md", false).expect("rename");
|
|
assert_eq!(report.rewritten, Vec::<String>::new());
|
|
assert_eq!(on_disk(vault.path(), "hub.md"), "[[Alpha]]\n");
|
|
assert_eq!(session.index().unresolved().len(), 1);
|
|
}
|
|
|
|
/// One note that cannot be rewritten aborts the whole rename: nothing moves.
|
|
#[test]
|
|
fn rename_is_all_or_nothing() {
|
|
let vault = temp_vault("rename-atomic");
|
|
seed(vault.path(), &[("Alpha.md", "body\n"), ("hub.md", "[[Alpha]]\n"), ("gone.md", "[[Alpha]]\n"), ("late.md", "[[Alpha]]\n")]);
|
|
let mut session = Session::open(vault.path()).expect("open");
|
|
// The index still knows `gone.md`; its file is already deleted.
|
|
std::fs::remove_file(vault.path().join("gone.md")).expect("delete behind the index");
|
|
|
|
let error = session.rename_note("Alpha.md", "Beta.md", true).expect_err("the rename must abort");
|
|
let Error::RenameIncomplete { failures } = error else { panic!("expected RenameIncomplete, got {error:?}") };
|
|
assert_eq!(failures.len(), 1, "{failures:?}");
|
|
assert!(failures[0].starts_with("gone.md: "));
|
|
|
|
assert!(vault.path().join("Alpha.md").exists(), "the note did not move");
|
|
assert!(!vault.path().join("Beta.md").exists());
|
|
assert_eq!(on_disk(vault.path(), "hub.md"), "[[Alpha]]\n", "no link was rewritten");
|
|
assert_eq!(on_disk(vault.path(), "late.md"), "[[Alpha]]\n");
|
|
assert_eq!(session.index().len(), 4, "the index still holds all four notes");
|
|
}
|
|
|
|
/// Rename refuses a taken target, an unknown source and a non-note path.
|
|
#[test]
|
|
fn rename_validates_its_paths() {
|
|
let vault = temp_vault("rename-bad");
|
|
seed(vault.path(), &[("Alpha.md", "body\n"), ("Beta.md", "body\n")]);
|
|
let mut session = Session::open(vault.path()).expect("open");
|
|
|
|
assert_eq!(session.rename_note("Alpha.md", "Beta.md", true), Err(Error::Exists(String::from("Beta.md"))));
|
|
assert!(matches!(session.rename_note("Nope.md", "Beta.md", false), Err(Error::NotFound(_))));
|
|
assert!(matches!(session.rename_note("Alpha.md", "notes.txt", false), Err(Error::InvalidPath(_))));
|
|
assert!(matches!(session.rename_note("Alpha.md", "../out.md", false), Err(Error::InvalidPath(_))));
|
|
assert_eq!(session.rename_note("Alpha.md", "Alpha.md", true).expect("same path").rewritten, Vec::<String>::new());
|
|
assert!(vault.path().join("Alpha.md").exists());
|
|
}
|