cargo-allow 0.1.9

Source-tree exception ledger and policy scanner for Rust repositories.
use super::*;
use allow_core::{AllowConfig, AllowEntry, FindingKind, Lifecycle, Selector};
use allow_policy::{EvidenceReferenceCategory, EvidenceReferenceStatus};
use std::collections::BTreeSet;
use std::fs;
use std::path::PathBuf;

fn fixture_dir() -> PathBuf {
    let stamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|duration| duration.as_nanos())
        .unwrap_or(0);
    std::env::temp_dir().join(format!(
        "cargo-allow-evidence-inventory-{}-{stamp}",
        std::process::id()
    ))
}

fn test_entry(id: &str, evidence: Vec<&str>, links: Vec<&str>) -> AllowEntry {
    AllowEntry {
        id: id.to_string(),
        kind: FindingKind::PolicyException,
        family: Some("network_destination".to_string()),
        path: Some(PathBuf::from("src/lib.rs")),
        glob: None,
        owner: "security".to_string(),
        classification: "reviewed".to_string(),
        reason: "Network exception is reviewed.".to_string(),
        evidence: evidence.into_iter().map(str::to_string).collect(),
        links: links.into_iter().map(str::to_string).collect(),
        occurrence_limit: Some(1),
        lifecycle: Lifecycle::empty(),
        selector: Selector {
            ast_kind: Some("function".to_string()),
            ..Selector::default()
        },
        last_seen: None,
    }
}

#[test]
fn evidence_reference_diagnostics_for_source_tree_downgrades_present_file_outside_inventory() {
    let root = fixture_dir();
    fs::create_dir_all(root.join("docs"))
        .unwrap_or_else(|err| std::panic::panic_any(format!("fixture docs dir: {err}")));
    fs::write(root.join("docs/untracked.md"), "review notes")
        .unwrap_or_else(|err| std::panic::panic_any(format!("fixture evidence file: {err}")));
    let entry = test_entry("allow-network", vec!["doc:docs/untracked.md"], vec![]);
    let source_tree_files = BTreeSet::new();

    let diagnostics =
        evidence_reference_diagnostics_for_source_tree(&root, &entry, Some(&source_tree_files));

    assert_eq!(diagnostics.len(), 1);
    let diagnostic = diagnostics
        .first()
        .unwrap_or_else(|| std::panic::panic_any("expected one evidence diagnostic"));
    assert_eq!(diagnostic.raw, "doc:docs/untracked.md");
    assert_eq!(diagnostic.status, EvidenceReferenceStatus::LocalFileMissing);
    assert_eq!(diagnostic.category, EvidenceReferenceCategory::Missing);
    assert_eq!(
        diagnostic.message,
        DEFAULT_SOURCE_TREE_INVENTORY_EVIDENCE_MESSAGE
    );
    assert_eq!(
        diagnostic.target.as_deref(),
        Some(Path::new("docs/untracked.md"))
    );
    fs::remove_dir_all(&root)
        .unwrap_or_else(|err| std::panic::panic_any(format!("remove fixture dir: {err}")));
}

#[test]
fn evidence_reference_diagnostics_for_source_tree_preserves_inventory_members() {
    let root = fixture_dir();
    fs::create_dir_all(root.join("docs"))
        .unwrap_or_else(|err| std::panic::panic_any(format!("fixture docs dir: {err}")));
    fs::write(root.join("docs/tracked.md"), "review notes")
        .unwrap_or_else(|err| std::panic::panic_any(format!("fixture evidence file: {err}")));
    let entry = test_entry("allow-network", vec!["doc:docs/tracked.md"], vec![]);
    let mut source_tree_files = BTreeSet::new();
    source_tree_files.insert("docs/tracked.md".to_string());

    let diagnostics =
        evidence_reference_diagnostics_for_source_tree(&root, &entry, Some(&source_tree_files));

    assert_eq!(diagnostics.len(), 1);
    let diagnostic = diagnostics
        .first()
        .unwrap_or_else(|| std::panic::panic_any("expected one evidence diagnostic"));
    assert_eq!(diagnostic.status, EvidenceReferenceStatus::LocalFilePresent);
    assert_eq!(diagnostic.category, EvidenceReferenceCategory::Present);
    assert_eq!(diagnostic.message, "local evidence file exists");
    fs::remove_dir_all(&root)
        .unwrap_or_else(|err| std::panic::panic_any(format!("remove fixture dir: {err}")));
}

#[test]
fn evidence_reference_diagnostics_for_source_tree_skips_inventory_when_unavailable() {
    let root = fixture_dir();
    fs::create_dir_all(root.join("docs"))
        .unwrap_or_else(|err| std::panic::panic_any(format!("fixture docs dir: {err}")));
    fs::write(root.join("docs/local.md"), "review notes")
        .unwrap_or_else(|err| std::panic::panic_any(format!("fixture evidence file: {err}")));
    let entry = test_entry("allow-network", vec!["doc:docs/local.md"], vec![]);

    let diagnostics = evidence_reference_diagnostics_for_source_tree(&root, &entry, None);

    assert_eq!(diagnostics.len(), 1);
    let diagnostic = diagnostics
        .first()
        .unwrap_or_else(|| std::panic::panic_any("expected one evidence diagnostic"));
    assert_eq!(diagnostic.status, EvidenceReferenceStatus::LocalFilePresent);
    assert_eq!(diagnostic.category, EvidenceReferenceCategory::Present);
    fs::remove_dir_all(&root)
        .unwrap_or_else(|err| std::panic::panic_any(format!("remove fixture dir: {err}")));
}

#[test]
fn policy_reference_diagnostics_for_source_tree_applies_inventory_to_links() {
    let root = fixture_dir();
    fs::create_dir_all(root.join("docs"))
        .unwrap_or_else(|err| std::panic::panic_any(format!("fixture docs dir: {err}")));
    fs::write(root.join("docs/trace.md"), "traceability")
        .unwrap_or_else(|err| std::panic::panic_any(format!("fixture link file: {err}")));
    let entry = test_entry("allow-network", vec![], vec!["doc:docs/trace.md"]);
    let source_tree_files = BTreeSet::new();

    let references =
        policy_reference_diagnostics_for_source_tree(&root, &entry, Some(&source_tree_files));

    assert_eq!(references.len(), 1);
    let reference = references
        .first()
        .unwrap_or_else(|| std::panic::panic_any("expected one policy reference"));
    assert_eq!(reference.source, ReferenceSource::Link);
    assert_eq!(
        reference.diagnostic.status,
        EvidenceReferenceStatus::LocalFileMissing
    );
    assert_eq!(
        reference.diagnostic.message,
        DEFAULT_SOURCE_TREE_INVENTORY_EVIDENCE_MESSAGE
    );
    fs::remove_dir_all(&root)
        .unwrap_or_else(|err| std::panic::panic_any(format!("remove fixture dir: {err}")));
}

#[test]
fn validate_evidence_references_for_source_tree_returns_ok_when_inventory_clean() {
    let root = fixture_dir();
    fs::create_dir_all(root.join("docs"))
        .unwrap_or_else(|err| std::panic::panic_any(format!("fixture docs dir: {err}")));
    fs::write(root.join("docs/tracked.md"), "review notes")
        .unwrap_or_else(|err| std::panic::panic_any(format!("fixture evidence file: {err}")));
    let mut cfg = AllowConfig::empty();
    cfg.allow.push(test_entry(
        "allow-network",
        vec!["doc:docs/tracked.md"],
        vec![],
    ));
    let mut source_tree_files = BTreeSet::new();
    source_tree_files.insert("docs/tracked.md".to_string());

    let result =
        validate_evidence_references_for_source_tree(&root, &cfg, Some(&source_tree_files));

    assert!(result.is_ok());
    fs::remove_dir_all(&root)
        .unwrap_or_else(|err| std::panic::panic_any(format!("remove fixture dir: {err}")));
}

#[test]
fn validate_evidence_references_for_source_tree_returns_err_for_missing_local_files() {
    let root = fixture_dir();
    fs::create_dir_all(&root)
        .unwrap_or_else(|err| std::panic::panic_any(format!("fixture root dir: {err}")));
    let mut cfg = AllowConfig::empty();
    cfg.allow.push(test_entry(
        "allow-network",
        vec!["doc:docs/missing.md"],
        vec![],
    ));
    let source_tree_files = BTreeSet::new();

    let err = validate_evidence_references_for_source_tree(&root, &cfg, Some(&source_tree_files))
        .expect_err("missing local evidence should fail validation");

    let message = err.to_string();
    assert!(message.contains("allow-network"));
    assert!(message.contains("doc:docs/missing.md"));
    assert!(message.contains("local evidence file is missing"));
    fs::remove_dir_all(&root)
        .unwrap_or_else(|err| std::panic::panic_any(format!("remove fixture dir: {err}")));
}

#[test]
fn validate_evidence_references_for_source_tree_returns_err_for_outside_inventory_evidence() {
    let root = fixture_dir();
    fs::create_dir_all(root.join("docs"))
        .unwrap_or_else(|err| std::panic::panic_any(format!("fixture docs dir: {err}")));
    fs::write(root.join("docs/untracked.md"), "review notes")
        .unwrap_or_else(|err| std::panic::panic_any(format!("fixture evidence file: {err}")));
    let mut cfg = AllowConfig::empty();
    cfg.allow.push(test_entry(
        "allow-network",
        vec!["doc:docs/untracked.md"],
        vec![],
    ));
    let source_tree_files = BTreeSet::new();

    let err = validate_evidence_references_for_source_tree(&root, &cfg, Some(&source_tree_files))
        .expect_err("untracked local evidence should fail default inventory validation");

    let message = err.to_string();
    assert!(message.contains("allow-network"));
    assert!(message.contains("doc:docs/untracked.md"));
    assert!(message.contains(DEFAULT_SOURCE_TREE_INVENTORY_EVIDENCE_MESSAGE));
    fs::remove_dir_all(&root)
        .unwrap_or_else(|err| std::panic::panic_any(format!("remove fixture dir: {err}")));
}