crtx-ledger 0.1.1

Append-only event log, hash chain, trace assembly, and audit records.
Documentation
//! Unit-test surface for `build_validation::discover_embedded_snapshot`.
//!
//! The actual build script (`build.rs`) runs on every compile and can
//! only be observed via its `panic!` messages, which is awkward to
//! assert against. We instead `#[path]`-include the same
//! `build_validation.rs` the build script uses and exercise its public
//! `discover_embedded_snapshot` against synthetic directories.
//!
//! Covered cases:
//!
//! 1. **Happy path** — exactly one regular-file
//!    `sigstore_trusted_root_YYYY-MM-DD.json` is accepted.
//! 2. **Symlink refusal** (Red Team v2 F2-S1, `#[cfg(unix)]`) — a
//!    symlinked candidate is rejected with `DiscoveryError::Symlink`
//!    even when the link points at a valid JSON payload.
//! 3. **Invalid calendar date** (Code Review v2 F3) —
//!    `sigstore_trusted_root_2026-02-30.json` is rejected with
//!    `DiscoveryError::InvalidCalendarDate`.
//! 4. **Multiple candidates** — two valid-looking snapshots fail with
//!    `DiscoveryError::MultipleCandidates`.
//! 5. **Zero candidates** — an empty directory fails with
//!    `DiscoveryError::NoCandidates`.

#[path = "../build_validation.rs"]
mod build_validation;

use std::fs;
use std::path::Path;

use build_validation::{discover_embedded_snapshot, DiscoveryError};

fn write_stub_snapshot(dir: &Path, filename: &str) {
    fs::write(dir.join(filename), b"{\"stub\":true}\n")
        .expect("write stub snapshot for build_validation test");
}

#[test]
fn accepts_unique_valid_regular_file() {
    let tmp = tempfile::tempdir().expect("tempdir");
    write_stub_snapshot(tmp.path(), "sigstore_trusted_root_2026-05-12.json");
    // Distractor that does NOT match the prefix — must be ignored.
    write_stub_snapshot(tmp.path(), "README.md");

    let discovery = discover_embedded_snapshot(tmp.path()).expect("happy path");
    assert_eq!(discovery.snapshot.date, "2026-05-12");
    assert_eq!(
        discovery.snapshot.filename,
        "sigstore_trusted_root_2026-05-12.json"
    );
    assert_eq!(
        discovery.matched_filenames,
        vec!["sigstore_trusted_root_2026-05-12.json".to_string()]
    );
}

#[cfg(unix)]
#[test]
fn refuses_symlinked_candidate() {
    use std::os::unix::fs::symlink;

    let tmp = tempfile::tempdir().expect("tempdir");
    // Real payload lives outside the scanned directory — exactly the
    // attacker shape: a checkout-time link planted next to the embedded
    // JSON that the build script would otherwise resolve.
    let outside = tmp.path().join("attacker.json");
    fs::write(&outside, b"{\"attacker\":true}\n").expect("write attacker payload");

    let scan_dir = tmp.path().join("embedded");
    fs::create_dir(&scan_dir).expect("create scan dir");
    symlink(
        &outside,
        scan_dir.join("sigstore_trusted_root_2026-05-12.json"),
    )
    .expect("create symlink candidate");

    let err = discover_embedded_snapshot(&scan_dir).expect_err("symlink must be refused");
    match err {
        DiscoveryError::Symlink { path } => {
            assert!(
                path.ends_with("sigstore_trusted_root_2026-05-12.json"),
                "symlink error must name the offending candidate path; got {}",
                path.display()
            );
        }
        other => panic!("expected Symlink error, got {other:?}"),
    }
}

#[cfg(windows)]
#[test]
fn refuses_symlinked_candidate_windows() {
    // Windows requires either Developer Mode or admin to create
    // symlinks. When that capability is unavailable in CI/sandbox we
    // skip rather than fail — the symlink invariant is the same shape
    // as the Unix case which already exercises the code path.
    let tmp = tempfile::tempdir().expect("tempdir");
    let outside = tmp.path().join("attacker.json");
    fs::write(&outside, b"{\"attacker\":true}\n").expect("write attacker payload");

    let scan_dir = tmp.path().join("embedded");
    fs::create_dir(&scan_dir).expect("create scan dir");
    let link_path = scan_dir.join("sigstore_trusted_root_2026-05-12.json");
    if std::os::windows::fs::symlink_file(&outside, &link_path).is_err() {
        eprintln!(
            "skipping refuses_symlinked_candidate_windows: cannot create symlinks \
             on this host (needs Developer Mode or admin)"
        );
        return;
    }

    let err = discover_embedded_snapshot(&scan_dir).expect_err("symlink must be refused");
    assert!(
        matches!(err, DiscoveryError::Symlink { .. }),
        "expected Symlink error, got {err:?}"
    );
}

#[test]
fn refuses_calendar_invalid_date() {
    let tmp = tempfile::tempdir().expect("tempdir");
    // Pattern matches and is lexically `YYYY-MM-DD` shaped, but Feb 30
    // does not exist on any year — the runtime
    // `NaiveDate::parse_from_str` inside `TrustRootStalenessAnchor`
    // would otherwise blow up the first time a freshness check ran.
    write_stub_snapshot(tmp.path(), "sigstore_trusted_root_2026-02-30.json");

    let err = discover_embedded_snapshot(tmp.path()).expect_err("invalid date must be refused");
    match err {
        DiscoveryError::InvalidCalendarDate {
            filename,
            date_segment,
            ..
        } => {
            assert_eq!(filename, "sigstore_trusted_root_2026-02-30.json");
            assert_eq!(date_segment, "2026-02-30");
        }
        other => panic!("expected InvalidCalendarDate error, got {other:?}"),
    }
}

#[test]
fn refuses_calendar_invalid_month_thirteen() {
    let tmp = tempfile::tempdir().expect("tempdir");
    write_stub_snapshot(tmp.path(), "sigstore_trusted_root_2026-13-01.json");

    let err =
        discover_embedded_snapshot(tmp.path()).expect_err("month 13 must be refused as invalid");
    assert!(
        matches!(err, DiscoveryError::InvalidCalendarDate { .. }),
        "expected InvalidCalendarDate, got {err:?}"
    );
}

#[test]
fn refuses_multiple_candidates() {
    let tmp = tempfile::tempdir().expect("tempdir");
    write_stub_snapshot(tmp.path(), "sigstore_trusted_root_2026-05-12.json");
    write_stub_snapshot(tmp.path(), "sigstore_trusted_root_2026-08-12.json");

    let err =
        discover_embedded_snapshot(tmp.path()).expect_err("two valid candidates must be refused");
    match err {
        DiscoveryError::MultipleCandidates { filenames, .. } => {
            assert_eq!(filenames.len(), 2);
            assert!(filenames
                .iter()
                .any(|f| f == "sigstore_trusted_root_2026-05-12.json"));
            assert!(filenames
                .iter()
                .any(|f| f == "sigstore_trusted_root_2026-08-12.json"));
        }
        other => panic!("expected MultipleCandidates, got {other:?}"),
    }
}

#[test]
fn refuses_zero_candidates() {
    let tmp = tempfile::tempdir().expect("tempdir");
    // Only an unrelated file — no candidate at all.
    write_stub_snapshot(tmp.path(), "unrelated.json");

    let err = discover_embedded_snapshot(tmp.path()).expect_err("empty must be refused");
    assert!(
        matches!(err, DiscoveryError::NoCandidates { .. }),
        "expected NoCandidates, got {err:?}"
    );
}

#[test]
fn error_messages_are_actionable() {
    // The build script surfaces `DiscoveryError` strings verbatim in
    // its panic; check the user-visible text mentions both the
    // specific failure mode and the canonical filename shape so a
    // build-time failure leads the operator to the fix.
    let tmp = tempfile::tempdir().expect("tempdir");
    write_stub_snapshot(tmp.path(), "sigstore_trusted_root_2026-02-30.json");
    let err = discover_embedded_snapshot(tmp.path()).expect_err("must error");
    let rendered = format!("{err}");
    assert!(
        rendered.contains("sigstore_trusted_root_2026-02-30.json"),
        "error must name the offending filename: {rendered}"
    );
    assert!(
        rendered.contains("calendar-invalid"),
        "error must explain that the date is calendar-invalid: {rendered}"
    );

    #[cfg(unix)]
    {
        use std::os::unix::fs::symlink;
        let scan_dir = tmp.path().join("symlink_msg");
        fs::create_dir(&scan_dir).expect("create dir");
        let target = tmp.path().join("real.json");
        fs::write(&target, b"{}").unwrap();
        symlink(
            &target,
            scan_dir.join("sigstore_trusted_root_2026-05-12.json"),
        )
        .unwrap();
        let err = discover_embedded_snapshot(&scan_dir).expect_err("must error");
        let rendered = format!("{err}");
        assert!(
            rendered.contains("Red Team v2 F2-S1"),
            "symlink error must cite the originating finding for grep-ability: {rendered}"
        );
    }
}