allow-policy-legacy 0.1.9

Legacy policy adapters for cargo-allow migrations.
Documentation
use super::*;
use crate::test_support::*;
use std::fs;
use std::path::Path;

#[test]
fn migrates_legacy_policy_directory_to_one_config() {
    let dir = fixture_dir();
    fs::write(
        dir.join("process-allowlist.toml"),
        process_policy_fixture_text(),
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("process fixture write: {err}")));
    fs::write(
        dir.join("network-allowlist.toml"),
        network_policy_fixture_text(),
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("network fixture write: {err}")));

    let cfg = load_legacy_policy_dir(&dir)
        .unwrap_or_else(|err| std::panic::panic_any(format!("policy directory migrates: {err}")));

    assert_eq!(cfg.policy, "cargo-allow");
    assert_eq!(cfg.owner.as_deref(), Some("EffortlessMetrics"));
    assert_eq!(cfg.allow.len(), 4);
    assert!(
        cfg.allow
            .iter()
            .any(|entry| entry.family.as_deref() == Some("process_spawn"))
    );
    assert!(
        cfg.allow
            .iter()
            .any(|entry| entry.family.as_deref() == Some("network_destination"))
    );
}

#[test]
fn policy_directory_includes_lint_and_unsafe_legacy_policies() {
    let dir = fixture_dir();
    fs::write(
        dir.join("clippy-exceptions.toml"),
        clippy_policy_fixture_text(),
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("clippy fixture write: {err}")));
    fs::write(
        dir.join("unsafe-allowlist.toml"),
        unsafe_policy_fixture_text(),
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("unsafe fixture write: {err}")));

    let cfg = load_legacy_policy_dir(&dir)
        .unwrap_or_else(|err| std::panic::panic_any(format!("policy directory migrates: {err}")));

    assert_eq!(cfg.policy, "cargo-allow");
    assert!(
        cfg.allow
            .iter()
            .any(|entry| entry.kind == allow_core::FindingKind::LintException
                && entry.selector.lint.as_deref() == Some("clippy::unwrap_used")),
        "repo-policy migration should include legacy lint exceptions"
    );
    assert!(
        cfg.allow
            .iter()
            .any(|entry| entry.kind == allow_core::FindingKind::Unsafe
                && entry.family.as_deref() == Some("unsafe_block")
                && entry
                    .evidence
                    .iter()
                    .any(|item| item.starts_with("unsafe-review:"))),
        "repo-policy migration should preserve reviewed unsafe evidence"
    );
    assert!(
        cfg.allow
            .iter()
            .any(|entry| entry.kind == allow_core::FindingKind::Unsafe
                && entry.family.as_deref() == Some("unsafe_fn")
                && entry.classification == "baseline_debt"
                && entry
                    .evidence
                    .iter()
                    .any(|item| item.contains("TODO: add unsafe-review"))),
        "repo-policy migration should keep unsafe entries without evidence visibly temporary"
    );
}

#[test]
fn policy_directory_includes_no_panic_legacy_policies() {
    let dir = fixture_dir();
    fs::write(
        dir.join("no-panic-baseline.toml"),
        no_panic_baseline_fixture_text(),
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("baseline fixture write: {err}")));
    fs::write(
        dir.join("no-panic-allowlist.toml"),
        no_panic_allowlist_fixture_text(),
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("allowlist fixture write: {err}")));

    let cfg = load_legacy_policy_dir(&dir)
        .unwrap_or_else(|err| std::panic::panic_any(format!("policy directory migrates: {err}")));

    assert_eq!(cfg.policy, "cargo-allow");
    assert!(
        cfg.allow
            .iter()
            .any(|entry| entry.id == "panic-baseline-0001"
                && entry.kind == allow_core::FindingKind::Panic
                && entry.family.as_deref() == Some("unwrap")
                && entry.classification == "baseline_debt"
                && entry.occurrence_limit == Some(2)
                && entry.evidence.iter().any(|item| item == "baseline_count:2")),
        "repo-policy migration should preserve no-panic baseline occurrence limits"
    );
    assert!(
        cfg.allow.iter().any(|entry| entry.id == "no-panic-unwrap"
            && entry.kind == allow_core::FindingKind::Panic
            && entry.family.as_deref() == Some("unwrap")
            && entry.classification == "reviewed_panic_exception"
            && entry.selector.ast_kind.as_deref() == Some("method_call")
            && entry.selector.callee.as_deref() == Some("unwrap")
            && entry.selector.container.as_deref() == Some("load")),
        "repo-policy migration should preserve reviewed no-panic structural selectors"
    );
}

#[test]
fn policy_directory_includes_file_workflow_and_dependency_policies() {
    let dir = fixture_dir();
    fs::write(
        dir.join("generated-allowlist.toml"),
        generated_policy_fixture_text(),
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("generated fixture write: {err}")));
    fs::write(
        dir.join("executable-allowlist.toml"),
        executable_policy_fixture_text(),
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("executable fixture write: {err}")));
    fs::write(
        dir.join("workflow-allowlist.toml"),
        workflow_policy_fixture_text(),
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("workflow fixture write: {err}")));
    fs::write(
        dir.join("dependency-surface-allowlist.toml"),
        dependency_policy_fixture_text(),
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("dependency fixture write: {err}")));

    let cfg = load_legacy_policy_dir(&dir)
        .unwrap_or_else(|err| std::panic::panic_any(format!("policy directory migrates: {err}")));

    assert_eq!(cfg.policy, "cargo-allow");
    assert_eq!(cfg.allow.len(), 7);
    assert!(
        cfg.allow
            .iter()
            .any(|entry| entry.kind == allow_core::FindingKind::GeneratedCode
                && entry.family.as_deref() == Some("generated_code")
                && entry.path.as_deref() == Some(Path::new("policy/no-panic-baseline.toml"))),
        "repo-policy migration should include generated-code legacy policy entries"
    );
    assert!(
        cfg.allow.iter().any(
            |entry| entry.kind == allow_core::FindingKind::PolicyException
                && entry.family.as_deref() == Some("executable_file")
                && entry.path.as_deref() == Some(Path::new("scripts/package-proof.sh"))
                && entry.selector.target_fingerprint.as_deref() == Some("git-mode:100755")
        ),
        "repo-policy migration should include executable-file legacy policy entries"
    );
    assert!(
        cfg.allow.iter().any(
            |entry| entry.kind == allow_core::FindingKind::PolicyException
                && entry.family.as_deref() == Some("github_workflow")
                && entry.path.as_deref() == Some(Path::new(".github/workflows/ci.yml"))
        ),
        "repo-policy migration should include workflow-file legacy policy entries"
    );
    assert!(
        cfg.allow.iter().any(
            |entry| entry.kind == allow_core::FindingKind::PolicyException
                && entry.family.as_deref() == Some("workflow_external_action")
                && entry.selector.target_fingerprint.as_deref()
                    == Some("action:actions/checkout@v6.0.2")
        ),
        "repo-policy migration should include workflow external-action entries"
    );
    assert!(
        cfg.allow.iter().any(
            |entry| entry.kind == allow_core::FindingKind::PolicyException
                && entry.family.as_deref() == Some("dependency_surface")
                && entry.path.as_deref() == Some(Path::new("Cargo.toml"))
                && entry
                    .evidence
                    .iter()
                    .any(|item| item == "dep_count_at_baseline:22")
        ),
        "repo-policy migration should include dependency-surface legacy policy entries"
    );
}

#[test]
fn policy_directory_can_expand_non_rust_globs_with_findings() {
    let dir = fixture_dir();
    fs::write(dir.join("non-rust-allowlist.toml"), policy_fixture_text())
        .unwrap_or_else(|err| std::panic::panic_any(format!("non-rust fixture write: {err}")));
    let findings = vec![finding(".github/workflows/ci.yml", "tracked_file")];

    let cfg =
        load_legacy_policy_dir_with_non_rust_findings(&dir, &findings).unwrap_or_else(|err| {
            std::panic::panic_any(format!("policy directory with findings migrates: {err}"))
        });

    assert_eq!(cfg.allow.len(), 1);
    let entry = cfg
        .allow
        .first()
        .unwrap_or_else(|| std::panic::panic_any("expected expanded non-rust entry"));
    assert_eq!(entry.id, "non-rust-github-workflows--0001");
    assert_eq!(
        entry.path.as_deref(),
        Some(Path::new(".github/workflows/ci.yml"))
    );
    assert_eq!(entry.links, vec!["legacy-policy:non-rust-github-workflows"]);
}

#[test]
fn legacy_policy_directory_requires_supported_files() {
    let dir = fixture_dir();
    let err = load_legacy_policy_dir(&dir).expect_err("empty policy directory should not migrate");
    assert!(
        err.to_string()
            .contains("contains no supported legacy policy files")
    );
}