cargo-allow 0.1.11

Source-tree exception ledger and policy scanner for Rust repositories.
use std::path::Path;

use crate::*;

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cli::ColorChoice;
    use clap::CommandFactory;
    use clap::Parser;

    #[test]
    fn normalized_args_accepts_cargo_subcommand_prefix() {
        let normalized = normalized_args(argv(vec!["cargo-allow", "allow", "audit"]));
        let expected = argv(vec!["cargo-allow", "audit"]);
        assert_eq!(normalized, expected);
    }

    #[test]
    fn normalized_args_accepts_root_flag_before_cargo_subcommand_prefix() {
        let normalized = normalized_args(argv(vec![
            "cargo-allow",
            "--color=always",
            "allow",
            "audit",
        ]));
        let expected = argv(vec!["cargo-allow", "--color=always", "audit"]);
        assert_eq!(normalized, expected);
    }

    #[test]
    fn normalized_args_strips_allow_shim_when_flags_follow_before_subcommand() {
        let normalized =
            normalized_args(argv(vec!["cargo-allow", "allow", "--color=never", "check"]));
        let expected = argv(vec!["cargo-allow", "--color=never", "check"]);
        assert_eq!(normalized, expected);
    }

    #[test]
    fn normalized_args_does_not_strip_bare_allow_without_following_subcommand() {
        // Keeps `allow` available as a future subcommand name. Cargo plugin
        // invocations with only the shim token stay free for that evolution.
        let normalized = normalized_args(argv(vec!["cargo-allow", "allow"]));
        let expected = argv(vec!["cargo-allow", "allow"]);
        assert_eq!(normalized, expected);
    }

    #[test]
    fn normalized_args_strips_allow_shim_before_root_version_flag() {
        // `cargo allow --version` becomes `cargo-allow allow --version`.
        let normalized = normalized_args(argv(vec!["cargo-allow", "allow", "--version"]));
        let expected = argv(vec!["cargo-allow", "--version"]);
        assert_eq!(normalized, expected);
    }

    #[test]
    fn normalized_args_does_not_strip_allow_when_followed_by_unknown_command() {
        let normalized = normalized_args(argv(vec!["cargo-allow", "allow", "future-cmd"]));
        let expected = argv(vec!["cargo-allow", "allow", "future-cmd"]);
        assert_eq!(normalized, expected);
    }

    #[test]
    fn normalized_args_does_not_strip_allow_after_real_subcommand() {
        let normalized = normalized_args(argv(vec!["cargo-allow", "add", "allow"]));
        let expected = argv(vec!["cargo-allow", "add", "allow"]);
        assert_eq!(normalized, expected);
    }

    #[test]
    fn known_subcommands_do_not_reserve_allow_name() {
        assert!(!CargoAllowCommand::SUBCOMMANDS.contains(&"allow"));
    }

    #[test]
    fn clap_parses_color_before_cargo_subcommand_prefix() {
        let parsed = CargoAllowCli::try_parse_from(normalized_args(argv(vec![
            "cargo-allow",
            "--color=always",
            "allow",
            "add",
            "--kind",
            "panic",
            "--path",
            "src/lib.rs",
            "--owner",
            "runtime",
            "--reason",
            "migration smoke",
        ])))
        .unwrap_or_else(|err| {
            std::panic::panic_any(format!("CLI should parse root color before allow: {err}"))
        });

        assert_eq!(parsed.color, ColorChoice::Always);
        assert!(matches!(parsed.command, Some(CargoAllowCommand::Add(_))));
    }

    #[test]
    fn clap_exposes_package_version() {
        let mut command = CargoAllowCli::command();

        assert_eq!(
            command.get_version(),
            Some(env!("CARGO_PKG_VERSION")),
            "root CLI should expose the package version"
        );
        assert!(
            command.render_help().to_string().contains("-V, --version"),
            "root help should include the standard version flag"
        );
    }

    #[test]
    fn clap_parses_markdown_alias() {
        let parsed =
            CargoAllowCli::try_parse_from(argv(vec!["cargo-allow", "check", "--format", "md"]))
                .unwrap_or_else(|err| std::panic::panic_any(format!("CLI should parse: {err}")));

        assert!(matches!(
            parsed.command,
            Some(CargoAllowCommand::Check(check::CheckArgs {
                format: OutputFormat::Markdown,
                ..
            }))
        ));
    }

    #[test]
    fn clap_requires_diff_base() {
        let parsed = CargoAllowCli::try_parse_from(argv(vec!["cargo-allow", "diff"]));

        assert!(parsed.is_err());
    }

    #[test]
    fn clap_parses_source_tree_root_for_check() {
        let parsed = CargoAllowCli::try_parse_from(argv(vec![
            "cargo-allow",
            "check",
            "--root",
            "fixtures/source-snapshot",
        ]))
        .unwrap_or_else(|err| std::panic::panic_any(format!("CLI should parse --root: {err}")));

        assert!(matches!(
            parsed.command,
            Some(CargoAllowCommand::Check(check::CheckArgs {
                root: RootArgs { root: Some(root) },
                ..
            })) if root == Path::new("fixtures/source-snapshot")
        ));
    }

    #[test]
    fn clap_leaves_check_mode_unset_when_not_provided() {
        let parsed = CargoAllowCli::try_parse_from(argv(vec!["cargo-allow", "check"]))
            .unwrap_or_else(|err| std::panic::panic_any(format!("CLI should parse check: {err}")));

        assert!(matches!(
            parsed.command,
            Some(CargoAllowCommand::Check(check::CheckArgs {
                mode: None,
                ..
            }))
        ));
    }

    #[test]
    fn clap_parses_explicit_check_mode() {
        let parsed =
            CargoAllowCli::try_parse_from(argv(vec!["cargo-allow", "check", "--mode", "strict"]))
                .unwrap_or_else(|err| {
                    std::panic::panic_any(format!("CLI should parse --mode strict: {err}"))
                });

        assert!(matches!(
            parsed.command,
            Some(CargoAllowCommand::Check(check::CheckArgs {
                mode: Some(mode),
                ..
            })) if mode == "strict"
        ));
    }

    #[test]
    fn clap_parses_repeatable_check_deny_statuses() {
        let parsed = CargoAllowCli::try_parse_from(argv(vec![
            "cargo-allow",
            "check",
            "--deny",
            "review_due",
            "--deny",
            "stale",
        ]))
        .unwrap_or_else(|err| std::panic::panic_any(format!("CLI should parse --deny: {err}")));

        assert!(matches!(
            parsed.command,
            Some(CargoAllowCommand::Check(check::CheckArgs {
                deny,
                ..
            })) if deny == vec!["review_due".to_string(), "stale".to_string()]
        ));
    }

    #[test]
    fn clap_rejects_unknown_kind_filters_for_report_commands() {
        for args in [
            vec!["cargo-allow", "audit", "--kind", "unsfae"],
            vec!["cargo-allow", "check", "--kind", "unsfae"],
            vec![
                "cargo-allow",
                "diff",
                "--base",
                "origin/main",
                "--kind",
                "unsfae",
            ],
            vec!["cargo-allow", "propose", "--kind", "unsfae"],
        ] {
            let err = CargoAllowCli::try_parse_from(argv(args.clone()))
                .expect_err("unknown kind should fail closed");
            assert!(
                err.to_string().contains("unknown kind"),
                "unexpected parse error for {args:?}: {err}"
            );
        }
    }

    #[test]
    fn check_help_describes_policy_default_as_source_tree_gate_mode() {
        let mut command = CargoAllowCli::command();
        let help = command.render_help().to_string();

        assert!(help.contains("Source exception ledger for source trees"));

        let Some(check) = command.find_subcommand_mut("check") else {
            std::panic::panic_any("check subcommand should exist");
        };
        let help = check.render_help().to_string();

        assert!(help.contains("policy-configured source-tree gate mode"));
        assert!(help.contains("--deny"));
        assert!(help.contains("occurrence_headroom"));
        assert!(!help.contains("workspace.default_mode"));
    }

    #[test]
    fn diff_help_describes_kind_filter_as_source_and_policy_posture() {
        let mut command = CargoAllowCli::command();
        let Some(diff) = command.find_subcommand_mut("diff") else {
            std::panic::panic_any("diff subcommand should exist");
        };
        let help = diff.render_help().to_string();

        assert!(help.contains("Filter source findings and allow-entry policy changes by kind"));
    }

    #[test]
    fn diff_help_describes_base_and_head_as_posture_revisions() {
        let mut command = CargoAllowCli::command();
        let Some(diff) = command.find_subcommand_mut("diff") else {
            std::panic::panic_any("diff subcommand should exist");
        };
        let help = diff.render_help().to_string();

        assert!(help.contains("Base Git revision; resolves to an exact commit before comparison"));
        assert!(
            help.contains(
                "Optional head Git revision; defaults to committed HEAD and resolves first"
            )
        );
    }

    fn argv(items: Vec<&str>) -> Vec<String> {
        items.into_iter().map(String::from).collect()
    }
}