""" Tests for Stage 70 — bundle CLI. python -m delivery.bundle create DIR [--out PATH] python -m delivery.bundle verify ZIP Recipient who got a bundle.zip in an email can verify in one shell command. Exit-code contract: 0 — create succeeded OR verify passed (manifest intact) 1 — verify failed (sha256 mismatch on at least one file) 2 — bad args / file not found / not a zip """ import json import zipfile from delivery.bundle import _cli, bundle_pilot_output def _populate(tmp_path): src = tmp_path / "pilot_out" src.mkdir() (src / "report.html").write_text("

R

", encoding="utf-8") (src / "queue.html").write_text("

Q

", encoding="utf-8") return src def _run(capsys, *args): capsys.readouterr() rc = _cli(list(args)) out, err = capsys.readouterr() body = json.loads(out) if out.strip() else None err_body = json.loads(err) if err.strip() else None return rc, body, err_body # --- create ---------------------------------------------------------- def test_create_emits_bundle_path_and_writes_zip(tmp_path, capsys): src = _populate(tmp_path) rc, body, _ = _run(capsys, "create", str(src)) assert rc == 0 assert "bundle_path" in body zip_path = tmp_path / "pilot_out.zip" assert zip_path.exists() def test_create_with_explicit_out(tmp_path, capsys): src = _populate(tmp_path) out = tmp_path / "custom.zip" rc, body, _ = _run(capsys, "create", str(src), "--out", str(out)) assert rc == 0 assert body["bundle_path"] == str(out.resolve()) assert out.exists() def test_create_missing_dir_exits_2(tmp_path, capsys): rc, _, err = _run(capsys, "create", str(tmp_path / "nope")) assert rc == 2 assert "does not exist" in err["error"] # --- verify ---------------------------------------------------------- def test_verify_intact_bundle_exits_0(tmp_path, capsys): src = _populate(tmp_path) zip_path = bundle_pilot_output(src) rc, body, _ = _run(capsys, "verify", str(zip_path)) assert rc == 0 assert body["ok"] is True assert body["n_mismatches"] == 0 def test_verify_tampered_bundle_exits_1(tmp_path, capsys): """The recipient's smoking gun — if anyone modified a file in the bundle, verify returns exit 1 + ok=false + the mismatching paths in the JSON. Cron-friendly: pipe to ``jq .ok`` and gate on it.""" src = _populate(tmp_path) src_zip = bundle_pilot_output(src) tampered = tmp_path / "tampered.zip" with zipfile.ZipFile(src_zip, "r") as zin, \ zipfile.ZipFile(tampered, "w", zipfile.ZIP_DEFLATED) as zout: for name in zin.namelist(): data = zin.read(name) if name == "report.html": data = b"

EVIL

" zout.writestr(name, data) rc, body, _ = _run(capsys, "verify", str(tampered)) assert rc == 1 assert body["ok"] is False assert body["n_mismatches"] == 1 assert body["mismatches"][0]["path"] == "report.html" def test_verify_missing_file_exits_2(tmp_path, capsys): rc, _, err = _run(capsys, "verify", str(tmp_path / "no_such.zip")) assert rc == 2 assert "error" in err def test_verify_non_zip_exits_2(tmp_path, capsys): """A path that exists but isn't a zip — exit 2 with a clear 'not a valid zip' message. Operator pointing at the wrong file sees the right error, not a Python traceback.""" bad = tmp_path / "not_a_zip.txt" bad.write_text("plain text not a zip", encoding="utf-8") rc, _, err = _run(capsys, "verify", str(bad)) assert rc == 2 assert "not a valid zip" in err["error"]