test(cli/org): org document add/edit/purge round-trips + attachment staging + grant denial
This commit is contained in:
@@ -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/<item-id>/<att-id>.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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user