mod common; use common::TestVault; #[test] fn attach_list_extract_round_trip() { let v = TestVault::init(); v.run(&["add", "login", "--title", "thing", "--username", "u", "--password", "p"]); let payload_path = v.path().join("payload.txt"); std::fs::write(&payload_path, b"attached-bytes").unwrap(); let attach = v.run(&["attach", "thing", payload_path.to_str().unwrap()]); assert!(attach.status.success(), "attach failed: {:?}", attach); let list = v.run(&["attachments", "thing"]); let stdout = String::from_utf8(list.stdout).unwrap(); assert!(stdout.contains("payload.txt"), "missing payload: {stdout}"); let aid = stdout.lines() .find(|l| l.contains("payload.txt")) .and_then(|l| l.split_whitespace().next()) .expect("aid token"); let out_path = v.path().join("extracted.txt"); let ex = v.run(&["extract", "thing", aid, "--out", out_path.to_str().unwrap()]); assert!(ex.status.success(), "extract failed: {:?}", ex); assert_eq!(std::fs::read(out_path).unwrap(), b"attached-bytes"); } #[test] fn detach_removes_attachment_and_blob() { let v = TestVault::init(); v.run(&["add", "login", "--title", "thing", "--username", "u", "--password", "p"]); let payload_path = v.path().join("payload.txt"); std::fs::write(&payload_path, b"attached-bytes").unwrap(); let attach = v.run(&["attach", "thing", payload_path.to_str().unwrap()]); assert!(attach.status.success()); let list = v.run(&["attachments", "thing"]); let stdout = String::from_utf8(list.stdout).unwrap(); let aid = stdout.lines() .find(|l| l.contains("payload.txt")) .and_then(|l| l.split_whitespace().next()) .expect("aid token") .to_string(); // Detach removes the attachment from the item AND deletes the blob. let out = v.run(&["detach", "thing", &aid]); assert!( out.status.success(), "detach failed:\nstdout: {}\nstderr: {}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr), ); // Item no longer lists the attachment. let list2 = v.run(&["attachments", "thing"]); let stdout2 = String::from_utf8(list2.stdout).unwrap(); assert!( !stdout2.contains("payload.txt"), "attachment still listed after detach: {stdout2}" ); // Encrypted blob file is gone. let blob_path = v.path() .join("attachments") .join(stdout.lines().nth(1).is_some().then_some("").unwrap_or("")); let item_attach_dir = std::fs::read_dir(v.path().join("attachments")) .unwrap().next().unwrap().unwrap().path(); let blob = item_attach_dir.join(format!("{aid}.enc")); assert!(!blob.exists(), "blob still on disk: {}", blob.display()); let _ = blob_path; // keep the variable to avoid an unused warning } #[test] fn detach_refuses_unknown_aid() { let v = TestVault::init(); v.run(&["add", "login", "--title", "thing", "--username", "u", "--password", "p"]); let out = v.run(&["detach", "thing", "deadbeef"]); assert!(!out.status.success(), "expected failure: {:?}", out); assert!( String::from_utf8_lossy(&out.stderr).to_lowercase().contains("no attachment"), "expected 'no attachment' error in stderr" ); } #[test] fn attach_rejects_over_cap() { let v = TestVault::init(); v.run(&["add", "login", "--title", "thing", "--username", "u", "--password", "p"]); v.run(&["settings", "attachment-cap", "--per-attachment-max-bytes", "10"]); let big = v.path().join("big.bin"); std::fs::write(&big, vec![0u8; 100]).unwrap(); let out = v.run(&["attach", "thing", big.to_str().unwrap()]); assert!(!out.status.success(), "expected failure; got {:?}", out); assert!(String::from_utf8(out.stderr).unwrap().to_lowercase().contains("attachment")); }