mod harness;
use harness::{MAGIC, TestRepo};
const EXPOSED: i32 = 5;
const SECRET: &[u8] = b"hunter2\n";
fn report(output: &std::process::Output) -> String {
String::from_utf8_lossy(&output.stdout).into_owned()
}
fn staged_blob(repo: &TestRepo, path: &str) -> Vec<u8> {
repo.git_ok(["cat-file", "blob", &format!(":{path}")])
.stdout
}
#[test]
fn a_secret_committed_before_its_pattern_is_found_fixed_forward_and_still_reported() {
let repo = TestRepo::init();
repo.init_xcrypt();
repo.write_xcrypt_config("# nothing declared yet\n");
repo.xcrypt_ok(["sync"]);
repo.write_file("secrets/db.env", SECRET);
repo.write_file("README.md", b"# ordinary project\n");
repo.commit_all("before anyone declared anything");
assert_eq!(
repo.blob_bytes("secrets/db.env"),
SECRET,
"the premise is a plaintext blob; without it this test proves nothing"
);
std::thread::sleep(std::time::Duration::from_millis(1100));
repo.git_ok(["update-index", "--refresh"]);
repo.write_xcrypt_config("secrets/\n");
repo.xcrypt_ok(["sync"]);
let found = repo.xcrypt(["status"]);
let text = report(&found);
assert_eq!(
found.status.code(),
Some(EXPOSED),
"a plaintext secret in history must fail the gate:\n{text}"
);
assert!(
text.contains("leaked in history"),
"the finding must say the history is where it is:\n{text}"
);
assert!(
text.contains("secrets/db.env"),
"the report must name the path:\n{text}"
);
assert!(
text.contains("in the clear:"),
"the file is still plain text in the index and in HEAD, and that is a \
separate finding from the history — the two need different repairs:\n{text}"
);
let rotate = text
.find("ROTATE THE SECRET")
.unwrap_or_else(|| panic!("the report never says to rotate:\n{text}"));
let rewrite = text
.find("git filter-repo")
.unwrap_or_else(|| panic!("the report never says how to rewrite:\n{text}"));
assert!(
rotate < rewrite,
"rewriting was offered before rotation, which reads as a fix:\n{text}"
);
assert!(
text.contains("does NOT undo this"),
"a rewrite must not be allowed to read as a fix:\n{text}"
);
repo.git_ok(["add", "-A"]);
assert_eq!(
staged_blob(&repo, "secrets/db.env"),
SECRET,
"git re-read the file anyway, so this run is inside the racy-clean \
window and proves nothing about `--fix`"
);
let fixed = repo.xcrypt(["status", "--fix"]);
let fixed_text = report(&fixed);
assert_eq!(
fixed.status.code(),
Some(EXPOSED),
"`--fix` cannot clear a finding it did not fix:\n{fixed_text}"
);
assert!(
staged_blob(&repo, "secrets/db.env").starts_with(MAGIC),
"`--fix` reported success over an index still pointing at the plaintext"
);
repo.assert_worktree_eq("secrets/db.env", SECRET);
repo.git_ok(["commit", "-q", "-m", "declare the secret"]);
assert!(
repo.blob_is_encrypted("secrets/db.env"),
"the commit after `--fix` still stored the secret in the clear"
);
assert!(
!repo.blob_is_encrypted("README.md"),
"an undeclared file must stay readable"
);
repo.assert_status_clean();
let still = repo.xcrypt(["status"]);
let still_text = report(&still);
assert_eq!(
still.status.code(),
Some(EXPOSED),
"the plaintext blob is still in history, so the gate must stay red — a \
green gate here would tell the user the secret is safe:\n{still_text}"
);
assert!(
still_text.contains("leaked in history"),
"the finding must survive `--fix`:\n{still_text}"
);
assert!(
still_text.contains("secrets/db.env"),
"the report must still name the path:\n{still_text}"
);
let old = repo.git_ok(["show", "HEAD~1:secrets/db.env"]).stdout;
assert_eq!(
old, SECRET,
"history no longer holds the plaintext, so the finding above would be \
wrong rather than right"
);
assert!(
!repo.blob_bytes("secrets/db.env").starts_with(SECRET),
"the current blob is the plaintext"
);
assert!(repo.blob_bytes("secrets/db.env").starts_with(MAGIC));
}
#[test]
fn a_secret_left_on_a_branch_nobody_has_checked_out_is_still_found() {
let repo = TestRepo::init();
repo.init_xcrypt();
repo.write_xcrypt_config("# nothing declared yet\n");
repo.xcrypt_ok(["sync"]);
repo.write_file("README.md", b"# ordinary project\n");
repo.commit_all("an ordinary start");
repo.git_ok(["checkout", "-q", "-b", "feature"]);
repo.write_file("secrets/db.env", SECRET);
repo.commit_all("wire the database up");
assert_eq!(
repo.blob_bytes("secrets/db.env"),
SECRET,
"the premise is a plaintext blob on the branch"
);
repo.git_ok(["checkout", "-q", "main"]);
assert!(
!repo.path().join("secrets/db.env").exists(),
"the premise is that nothing in the working tree shows this any more"
);
repo.write_xcrypt_config("secrets/\n");
repo.xcrypt_ok(["sync"]);
repo.commit_all("declare the secrets directory");
let output = repo.xcrypt(["status"]);
let text = report(&output);
assert_eq!(
output.status.code(),
Some(EXPOSED),
"a plaintext secret on another branch must fail the gate — it is pushed \
with that branch and it is in every clone:\n{text}"
);
assert!(
text.contains("leaked in history"),
"the finding must say where it is:\n{text}"
);
assert!(
text.contains("secrets/db.env"),
"the report must name the path:\n{text}"
);
}
#[test]
fn an_uncommitted_bootstrap_names_the_clones_exposure_not_this_checkouts() {
let repo = TestRepo::init();
repo.init_xcrypt();
repo.write_xcrypt_config("secrets/\n");
repo.xcrypt_ok(["sync"]);
repo.write_file("secrets/db.env", SECRET);
repo.git_ok(["add", "secrets/db.env"]);
repo.git_ok(["commit", "-q", "-m", "the secret, without its bootstrap"]);
assert!(
repo.blob_bytes("secrets/db.env").starts_with(MAGIC),
"the premise is a correctly filtering checkout; without it this test \
proves nothing"
);
let output = repo.xcrypt(["status"]);
let text = report(&output);
assert_eq!(
output.status.code(),
Some(2),
"an uncommitted bootstrap is a setup gap, and the gate must say so:\n{text}"
);
assert!(
!text.contains("stores it in the clear"),
"this repository filters correctly, and the headline says it does not:\n{text}"
);
assert!(
text.contains("no clone gets them"),
"the real exposure — the clone's — goes unnamed:\n{text}"
);
assert!(
text.contains("git add"),
"the one remedy that works, a commit, is not offered:\n{text}"
);
assert!(
!text.contains("git-xcrypt init"),
"`init` does not commit anything, so offering it here is a loop that \
changes nothing:\n{text}"
);
}