From 03559f81eacab84bb74a60b7266f02c6b4cfd830 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 20 Jun 2026 21:35:24 -0400 Subject: [PATCH] test(cli/org): org document add/edit/purge round-trips + attachment staging + grant denial --- crates/relicario-cli/tests/org_items.rs | 129 ++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/crates/relicario-cli/tests/org_items.rs b/crates/relicario-cli/tests/org_items.rs index d98883b..5c7307c 100644 --- a/crates/relicario-cli/tests/org_items.rs +++ b/crates/relicario-cli/tests/org_items.rs @@ -456,3 +456,132 @@ fn org_edit_key_replaces_material_and_reveals_with_show() { assert!(shown.contains("NEW-MATERIAL-bbbb"), "replaced material not revealed with --show: {shown}"); assert!(!shown.contains("OLD-MATERIAL"), "old material still present after replace: {shown}"); } + +// --- v0.8.1 org Document tests ----------------------------------------------- + +/// `git status --porcelain` output for the org repo (trimmed). Empty-of-`attachments/` +/// proves every attachment add/remove was staged into the signed commit. +fn git_porcelain(repo: &str) -> String { + let out = std::process::Command::new("git") + .args(["-C", repo, "status", "--porcelain"]) + .output() + .unwrap(); + String::from_utf8_lossy(&out.stdout).trim().to_string() +} + +#[test] +fn org_add_document_stores_collection_scoped_attachment() { + let f = OrgFixture::new(); + f.create_collection_and_grant("eng"); + let srcdir = TempDir::new().unwrap(); + let src = srcdir.path().join("note.txt"); + std::fs::write(&src, b"secret memo").unwrap(); + + let out = f.run(&["org", "add", "document", "--collection", "eng", + "--title", "Q3 Memo", "--file", src.to_str().unwrap()]); + assert!(out.status.success(), "add doc: {}", String::from_utf8_lossy(&out.stderr)); + + // Encrypted blob at attachments/eng//.enc (3 segments). + let att_eng = f.vault_path().join("attachments").join("eng"); + assert!(att_eng.exists(), "attachment dir missing"); + let item_dirs: Vec<_> = std::fs::read_dir(&att_eng).unwrap().map(|e| e.unwrap().path()).collect(); + assert_eq!(item_dirs.len(), 1, "expected exactly one item attachment dir"); + let blobs: Vec<_> = std::fs::read_dir(&item_dirs[0]).unwrap().map(|e| e.unwrap().path()).collect(); + assert_eq!(blobs.len(), 1, "expected exactly one attachment blob"); + assert_eq!(blobs[0].extension().and_then(|e| e.to_str()), Some("enc"), "blob must be .enc"); + + let got = f.run(&["org", "get", "Q3 Memo"]); + let stdout = String::from_utf8_lossy(&got.stdout); + assert!(stdout.contains("Filename: note.txt"), "get missing filename: {stdout}"); + // Staging proof: nothing attachment-related left uncommitted. + assert!(!git_porcelain(f.vault_str()).contains("attachments/"), "unstaged attachment after add"); +} + +#[test] +fn org_purge_document_removes_attachment_dir() { + let f = OrgFixture::new(); + f.create_collection_and_grant("eng"); + let srcdir = TempDir::new().unwrap(); + let src = srcdir.path().join("d.bin"); + std::fs::write(&src, b"bytes").unwrap(); + assert!(f.run(&["org", "add", "document", "--collection", "eng", + "--title", "Doc", "--file", src.to_str().unwrap()]).status.success()); + + let att_eng = f.vault_path().join("attachments").join("eng"); + assert!(std::fs::read_dir(&att_eng).unwrap().next().is_some(), "attachment must exist after add"); + + assert!(f.run(&["org", "rm", "Doc"]).status.success(), "rm"); + let out = f.run(&["org", "purge", "Doc"]); + assert!(out.status.success(), "purge: {}", String::from_utf8_lossy(&out.stderr)); + + let empty = !att_eng.exists() || std::fs::read_dir(&att_eng).unwrap().next().is_none(); + assert!(empty, "attachment dir should be gone after purge"); + assert!(!git_porcelain(f.vault_str()).contains("attachments/"), "unstaged attachment removal after purge"); +} + +#[test] +fn org_edit_document_replaces_attachment_and_stages_cleanly() { + let f = OrgFixture::new(); + f.create_collection_and_grant("eng"); + let srcdir = TempDir::new().unwrap(); + let a = srcdir.path().join("a.txt"); + std::fs::write(&a, b"version A").unwrap(); + assert!(f.run(&["org", "add", "document", "--collection", "eng", + "--title", "Spec", "--file", a.to_str().unwrap()]).status.success()); + + let b = srcdir.path().join("b.md"); + std::fs::write(&b, b"version B has different content").unwrap(); + let out = f.run(&["org", "edit", "Spec", "--file", b.to_str().unwrap()]); + assert!(out.status.success(), "edit --file: {}", String::from_utf8_lossy(&out.stderr)); + + let got = String::from_utf8_lossy(&f.run(&["org", "get", "Spec"]).stdout).to_string(); + assert!(got.contains("Filename: b.md"), "get should show new filename: {got}"); + assert!(!got.contains("a.txt"), "old filename should be gone: {got}"); + + // Old blob replaced, not accumulated: exactly one blob remains. + let att_eng = f.vault_path().join("attachments").join("eng"); + let item_dirs: Vec<_> = std::fs::read_dir(&att_eng).unwrap().map(|e| e.unwrap().path()).collect(); + assert_eq!(item_dirs.len(), 1, "one item attachment dir"); + let blobs = std::fs::read_dir(&item_dirs[0]).unwrap().count(); + assert_eq!(blobs, 1, "old blob must be replaced, not accumulated"); + + // The key staging proof: no orphaned old blob / unstaged new blob. + assert!(!git_porcelain(f.vault_str()).contains("attachments/"), + "edit-replace left attachment changes unstaged (incomplete git add)"); +} + +#[test] +fn org_edit_file_on_non_document_is_rejected() { + let f = OrgFixture::new(); + f.create_collection_and_grant("eng"); + assert!(f.run(&["org", "add", "login", "--collection", "eng", + "--title", "Site", "--password", "p"]).status.success()); + let srcdir = TempDir::new().unwrap(); + let x = srcdir.path().join("x.txt"); + std::fs::write(&x, b"nope").unwrap(); + + let out = f.run(&["org", "edit", "Site", "--file", x.to_str().unwrap()]); + assert!(!out.status.success(), "--file on a Login must be rejected"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(stderr.contains("--file is only valid"), "unexpected error: {stderr}"); +} + +#[test] +fn org_add_document_into_ungranted_collection_is_denied() { + let f = OrgFixture::new(); + // Collection exists but the owner is NOT granted. + assert!(f.run(&["org", "create-collection", "secret", "--name", "Secret"]).status.success(), + "create-collection"); + let srcdir = TempDir::new().unwrap(); + let src = srcdir.path().join("f.txt"); + std::fs::write(&src, b"data").unwrap(); + + let out = f.run(&["org", "add", "document", "--collection", "secret", + "--title", "X", "--file", src.to_str().unwrap()]); + assert!(!out.status.success(), "ungranted document add must fail"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(stderr.contains("access denied") || stderr.contains("grant"), "unexpected error: {stderr}"); + // Grant is enforced before any attachment is written. + assert!(!f.vault_path().join("attachments").join("secret").exists(), + "no attachment dir should exist on a denied add"); +}