use clap::Parser;
use forge_guard::cli::{Cli, Commands, SecurityAction};
fn assert_shared_defaults(
chain: &str,
project: &std::path::Path,
json: bool,
markdown: bool,
strict: bool,
offline: bool,
production: bool,
report: bool,
parallelism: usize,
) {
assert_eq!(chain, "ethereum", "default chain should be ethereum");
assert_eq!(
project.to_string_lossy(),
".",
"default project should be ."
);
assert!(!json, "json should default to false");
assert!(!markdown, "markdown should default to false");
assert!(!strict, "strict should default to false");
assert!(!offline, "offline should default to false");
assert!(!production, "production should default to false");
assert!(!report, "report should default to false");
assert_eq!(parallelism, 4, "parallelism should default to 4");
}
fn parse_shared(
cli: &Cli,
) -> (
&str,
&std::path::Path,
bool,
bool,
bool,
bool,
bool,
bool,
usize,
) {
let args = match &cli.command {
Commands::Audit(a) => &a.shared,
Commands::Deploy(a) => &a.shared,
Commands::DeploySafe(a) => &a.shared,
Commands::Fuzz(a) => &a.shared,
Commands::Invariant(a) => &a.shared,
Commands::Simulate(a) => &a.shared,
Commands::Gas(a) => &a.shared,
Commands::Report(a) => &a.shared,
Commands::Verify(a) => &a.shared,
Commands::Doctor(a) => &a.shared,
Commands::Watch(a) => &a.shared,
Commands::Ci(a) => &a.shared,
Commands::Benchmark(a) => &a.shared,
Commands::Scan(a) => &a.shared,
Commands::UpgradeCheck(a) => &a.shared,
Commands::Plugins(a) => &a.shared,
Commands::Chain(a) => &a.shared,
Commands::Security(a) => &a.shared,
};
(
&args.chain,
&args.project,
args.json,
args.markdown,
args.strict,
args.offline,
args.production,
args.report,
args.parallelism,
)
}
#[test]
fn test_cli_parse_audit() {
let cli = Cli::try_parse_from(&["forge-guard", "audit"]).unwrap();
assert!(matches!(cli.command, Commands::Audit(_)));
}
#[test]
fn test_cli_parse_audit_with_flags() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"audit",
"--full",
"--quick",
"--summary",
"--ai",
"--ai-provider",
"claude",
"--ai-model",
"claude-5-opus-20260701",
"--ai-api-key",
"sk-test",
"--ollama-endpoint",
"http://localhost:11434",
"--ai-full",
"--exploit",
"--gas",
"--all-chains",
"--sources",
"src,contracts",
"--exclude",
"test,mock",
"--chain",
"polygon",
"--project",
"/tmp/test",
"--json",
"--strict",
"--offline",
"--production",
"--report",
"--parallelism",
"8",
])
.unwrap();
match cli.command {
Commands::Audit(args) => {
assert!(args.full);
assert!(args.quick);
assert!(args.summary);
assert!(args.ai);
assert_eq!(args.ai_provider, "claude");
assert_eq!(args.ai_model, "claude-5-opus-20260701");
assert_eq!(args.ai_api_key, Some("sk-test".into()));
assert_eq!(args.ollama_endpoint, Some("http://localhost:11434".into()));
assert!(args.ai_full);
assert!(args.exploit);
assert!(args.gas);
assert!(args.all_chains);
assert_eq!(args.sources, "src,contracts");
assert_eq!(args.exclude, Some("test,mock".into()));
assert_eq!(args.shared.chain, "polygon");
assert!(args.shared.json);
assert!(args.shared.strict);
assert!(args.shared.offline);
assert!(args.shared.production);
assert!(args.shared.report);
assert_eq!(args.shared.parallelism, 8);
}
_ => panic!("Expected Audit command"),
}
}
#[test]
fn test_cli_parse_audit_defaults() {
let cli = Cli::try_parse_from(&["forge-guard", "audit"]).unwrap();
match &cli.command {
Commands::Audit(args) => {
assert!(!args.full);
assert!(!args.quick);
assert!(!args.summary);
assert!(!args.ai);
assert_eq!(args.ai_provider, "openai");
assert_eq!(args.ai_model, "gpt-5");
assert!(args.ai_api_key.is_none());
assert!(args.ollama_endpoint.is_none());
assert!(!args.ai_full);
assert!(!args.exploit);
assert!(!args.gas);
assert!(!args.all_chains);
assert_eq!(args.sources, "src");
assert!(args.exclude.is_none());
let (c, p, j, m, s, o, pr, r, pl) = parse_shared(&cli);
assert_shared_defaults(c, p, j, m, s, o, pr, r, pl);
}
_ => panic!("Expected Audit command"),
}
}
#[test]
fn test_cli_parse_deploy() {
let cli = Cli::try_parse_from(&["forge-guard", "deploy"]).unwrap();
assert!(matches!(cli.command, Commands::Deploy(_)));
}
#[test]
fn test_cli_parse_deploy_with_contract() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"deploy",
"MyContract",
"--force",
"--args",
"0x1234,100",
"--salt",
"0xabcd",
"--verify",
"--chain",
"polygon",
])
.unwrap();
match cli.command {
Commands::Deploy(args) => {
assert_eq!(args.contract, Some("MyContract".into()));
assert!(args.force);
assert_eq!(args.args, Some("0x1234,100".into()));
assert_eq!(args.salt, Some("0xabcd".into()));
assert!(args.verify);
assert_eq!(args.shared.chain, "polygon");
}
_ => panic!("Expected Deploy command"),
}
}
#[test]
fn test_cli_parse_deploy_defaults() {
let cli = Cli::try_parse_from(&["forge-guard", "deploy"]).unwrap();
match &cli.command {
Commands::Deploy(args) => {
assert!(args.contract.is_none());
assert!(!args.force);
assert!(args.args.is_none());
assert!(args.salt.is_none());
assert!(!args.verify);
let (c, p, j, m, s, o, pr, r, pl) = parse_shared(&cli);
assert_shared_defaults(c, p, j, m, s, o, pr, r, pl);
}
_ => panic!("Expected Deploy command"),
}
}
#[test]
fn test_cli_parse_deploy_safe() {
let cli = Cli::try_parse_from(&["forge-guard", "deploy-safe"]).unwrap();
assert!(matches!(cli.command, Commands::DeploySafe(_)));
}
#[test]
fn test_cli_parse_deploy_safe_with_contract() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"deploy-safe",
"MyContract",
"--args",
"0x1234",
"--verify",
])
.unwrap();
match cli.command {
Commands::DeploySafe(args) => {
assert_eq!(args.contract, Some("MyContract".into()));
assert_eq!(args.args, Some("0x1234".into()));
assert!(args.verify);
}
_ => panic!("Expected DeploySafe command"),
}
}
#[test]
fn test_cli_parse_fuzz() {
let cli = Cli::try_parse_from(&["forge-guard", "fuzz"]).unwrap();
assert!(matches!(cli.command, Commands::Fuzz(_)));
}
#[test]
fn test_cli_parse_fuzz_with_flags() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"fuzz",
"MyTest",
"--runs",
"50000",
"--seed",
"42",
"--test",
"test_deposit",
])
.unwrap();
match cli.command {
Commands::Fuzz(args) => {
assert_eq!(args.contract, Some("MyTest".into()));
assert_eq!(args.runs, 50000);
assert_eq!(args.seed, Some(42));
assert_eq!(args.test, Some("test_deposit".into()));
}
_ => panic!("Expected Fuzz command"),
}
}
#[test]
fn test_cli_parse_fuzz_defaults() {
let cli = Cli::try_parse_from(&["forge-guard", "fuzz"]).unwrap();
match cli.command {
Commands::Fuzz(args) => {
assert!(args.contract.is_none());
assert_eq!(args.runs, 10000);
assert!(args.seed.is_none());
assert!(args.test.is_none());
}
_ => panic!("Expected Fuzz command"),
}
}
#[test]
fn test_cli_parse_invariant() {
let cli = Cli::try_parse_from(&["forge-guard", "invariant"]).unwrap();
assert!(matches!(cli.command, Commands::Invariant(_)));
}
#[test]
fn test_cli_parse_invariant_with_flags() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"invariant",
"MyInvariant",
"--runs",
"5000",
"--depth",
"200",
"--fail-on-revert",
])
.unwrap();
match cli.command {
Commands::Invariant(args) => {
assert_eq!(args.contract, Some("MyInvariant".into()));
assert_eq!(args.runs, 5000);
assert_eq!(args.depth, 200);
assert!(args.fail_on_revert);
}
_ => panic!("Expected Invariant command"),
}
}
#[test]
fn test_cli_parse_simulate() {
let cli = Cli::try_parse_from(&["forge-guard", "simulate"]).unwrap();
assert!(matches!(cli.command, Commands::Simulate(_)));
}
#[test]
fn test_cli_parse_simulate_with_flags() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"simulate",
"MyContract",
"--blocks",
"500",
"--deployer",
"0x1234",
"--mev",
])
.unwrap();
match cli.command {
Commands::Simulate(args) => {
assert_eq!(args.contract, Some("MyContract".into()));
assert_eq!(args.blocks, 500);
assert_eq!(args.deployer, Some("0x1234".into()));
assert!(args.mev);
}
_ => panic!("Expected Simulate command"),
}
}
#[test]
fn test_cli_parse_gas() {
let cli = Cli::try_parse_from(&["forge-guard", "gas"]).unwrap();
assert!(matches!(cli.command, Commands::Gas(_)));
}
#[test]
fn test_cli_parse_gas_with_flags() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"gas",
"MyContract",
"--diff",
"previous.json",
"--all",
"--warn-threshold",
"100000",
])
.unwrap();
match cli.command {
Commands::Gas(args) => {
assert_eq!(args.contract, Some("MyContract".into()));
assert_eq!(args.diff, Some("previous.json".into()));
assert!(args.all);
assert_eq!(args.warn_threshold, 100000);
}
_ => panic!("Expected Gas command"),
}
}
#[test]
fn test_cli_parse_report() {
let cli = Cli::try_parse_from(&["forge-guard", "report"]).unwrap();
assert!(matches!(cli.command, Commands::Report(_)));
}
#[test]
fn test_cli_parse_report_with_flags() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"report",
"result.json",
"--format",
"json",
"--output",
"report.json",
"--exploit-paths",
"--summary",
])
.unwrap();
match cli.command {
Commands::Report(args) => {
assert_eq!(args.input, Some(std::path::PathBuf::from("result.json")));
assert_eq!(args.format, "json");
assert_eq!(args.output, Some(std::path::PathBuf::from("report.json")));
assert!(args.exploit_paths);
assert!(args.summary);
}
_ => panic!("Expected Report command"),
}
}
#[test]
fn test_cli_parse_verify() {
let cli = Cli::try_parse_from(&["forge-guard", "verify"]).unwrap();
assert!(matches!(cli.command, Commands::Verify(_)));
}
#[test]
fn test_cli_parse_verify_with_flags() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"verify",
"0x1234",
"MyContract",
"--api-key",
"test-key",
"--constructor-args",
"0xabcdef",
"--all",
])
.unwrap();
match cli.command {
Commands::Verify(args) => {
assert_eq!(args.address, Some("0x1234".into()));
assert_eq!(args.name, Some("MyContract".into()));
assert_eq!(args.api_key, Some("test-key".into()));
assert_eq!(args.constructor_args, Some("0xabcdef".into()));
assert!(args.all);
}
_ => panic!("Expected Verify command"),
}
}
#[test]
fn test_cli_parse_doctor() {
let cli = Cli::try_parse_from(&["forge-guard", "doctor"]).unwrap();
assert!(matches!(cli.command, Commands::Doctor(_)));
}
#[test]
fn test_cli_parse_doctor_with_flags() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"doctor",
"--fix",
"--verbose",
"--check",
"foundry",
])
.unwrap();
match cli.command {
Commands::Doctor(args) => {
assert!(args.fix);
assert!(args.verbose);
assert_eq!(args.check, Some("foundry".into()));
}
_ => panic!("Expected Doctor command"),
}
}
#[test]
fn test_cli_parse_watch() {
let cli = Cli::try_parse_from(&["forge-guard", "watch"]).unwrap();
assert!(matches!(cli.command, Commands::Watch(_)));
}
#[test]
fn test_cli_parse_watch_with_flags() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"watch",
"--dirs",
"src,contracts",
"--debounce-ms",
"1000",
"--exclude",
"test",
"--full",
])
.unwrap();
match cli.command {
Commands::Watch(args) => {
assert_eq!(args.dirs, "src,contracts");
assert_eq!(args.debounce_ms, 1000);
assert_eq!(args.exclude, Some("test".into()));
assert!(args.full);
}
_ => panic!("Expected Watch command"),
}
}
#[test]
fn test_cli_parse_ci() {
let cli = Cli::try_parse_from(&["forge-guard", "ci"]).unwrap();
assert!(matches!(cli.command, Commands::Ci(_)));
}
#[test]
fn test_cli_parse_ci_with_flags() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"ci",
"--platform",
"gitlab",
"--output",
".gitlab-ci",
"--include-deploy",
"--overwrite",
])
.unwrap();
match cli.command {
Commands::Ci(args) => {
assert_eq!(args.platform, "gitlab");
assert_eq!(args.output, std::path::PathBuf::from(".gitlab-ci"));
assert!(args.include_deploy);
assert!(args.overwrite);
}
_ => panic!("Expected Ci command"),
}
}
#[test]
fn test_cli_parse_ci_defaults() {
let cli = Cli::try_parse_from(&["forge-guard", "ci"]).unwrap();
match &cli.command {
Commands::Ci(args) => {
assert_eq!(args.platform, "github");
assert_eq!(args.output, std::path::PathBuf::from(".github/workflows"));
assert!(!args.include_deploy);
assert!(!args.overwrite);
let (c, p, j, m, s, o, pr, r, pl) = parse_shared(&cli);
assert_shared_defaults(c, p, j, m, s, o, pr, r, pl);
}
_ => panic!("Expected Ci command"),
}
}
#[test]
fn test_cli_parse_benchmark() {
let cli = Cli::try_parse_from(&["forge-guard", "benchmark"]).unwrap();
assert!(matches!(cli.command, Commands::Benchmark(_)));
}
#[test]
fn test_cli_parse_benchmark_with_flags() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"benchmark",
"--iterations",
"20",
"--warmup",
"5",
"--compare",
"baseline.json",
"--save",
"results.json",
"--module",
"pattern_matching",
])
.unwrap();
match cli.command {
Commands::Benchmark(args) => {
assert_eq!(args.iterations, 20);
assert_eq!(args.warmup, 5);
assert_eq!(
args.compare,
Some(std::path::PathBuf::from("baseline.json"))
);
assert_eq!(args.save, Some(std::path::PathBuf::from("results.json")));
assert_eq!(args.module, Some("pattern_matching".into()));
}
_ => panic!("Expected Benchmark command"),
}
}
#[test]
fn test_cli_parse_scan() {
let cli = Cli::try_parse_from(&["forge-guard", "scan"]).unwrap();
assert!(matches!(cli.command, Commands::Scan(_)));
}
#[test]
fn test_cli_parse_scan_with_flags() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"scan",
"--depth",
"2",
"--update",
"--vulnerable-only",
"--fail-fast",
])
.unwrap();
match cli.command {
Commands::Scan(args) => {
assert_eq!(args.depth, 2);
assert!(args.update);
assert!(args.vulnerable_only);
assert!(args.fail_fast);
}
_ => panic!("Expected Scan command"),
}
}
#[test]
fn test_cli_parse_upgrade_check() {
let cli = Cli::try_parse_from(&["forge-guard", "upgrade-check"]).unwrap();
assert!(matches!(cli.command, Commands::UpgradeCheck(_)));
}
#[test]
fn test_cli_parse_upgrade_check_with_flags() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"upgrade-check",
"0xproxy",
"0ximpl",
"--all",
"--storage-collision",
"--uups",
])
.unwrap();
match cli.command {
Commands::UpgradeCheck(args) => {
assert_eq!(args.proxy, Some("0xproxy".into()));
assert_eq!(args.implementation, Some("0ximpl".into()));
assert!(args.all);
assert!(args.storage_collision);
assert!(args.uups);
}
_ => panic!("Expected UpgradeCheck command"),
}
}
#[test]
fn test_cli_parse_plugins_list() {
let cli = Cli::try_parse_from(&["forge-guard", "plugins", "list"]).unwrap();
assert!(matches!(cli.command, Commands::Plugins(_)));
}
#[test]
fn test_cli_parse_plugins_install() {
let cli = Cli::try_parse_from(&["forge-guard", "plugins", "install", "my-plugin"]).unwrap();
match cli.command {
Commands::Plugins(args) => {
assert!(args.action.is_some());
}
_ => panic!("Expected Plugins command"),
}
}
#[test]
fn test_cli_parse_plugins_install_with_source() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"plugins",
"install",
"my-plugin",
"https://github.com/user/plugin.git",
])
.unwrap();
assert!(matches!(cli.command, Commands::Plugins(_)));
}
#[test]
fn test_cli_parse_plugins_remove() {
let cli = Cli::try_parse_from(&["forge-guard", "plugins", "remove", "my-plugin"]).unwrap();
assert!(matches!(cli.command, Commands::Plugins(_)));
}
#[test]
fn test_cli_parse_plugins_enable_disable() {
let cli_enable =
Cli::try_parse_from(&["forge-guard", "plugins", "enable", "my-plugin"]).unwrap();
assert!(matches!(cli_enable.command, Commands::Plugins(_)));
let cli_disable =
Cli::try_parse_from(&["forge-guard", "plugins", "disable", "my-plugin"]).unwrap();
assert!(matches!(cli_disable.command, Commands::Plugins(_)));
}
#[test]
fn test_cli_parse_plugins_new() {
let cli = Cli::try_parse_from(&["forge-guard", "plugins", "new", "my-awesome-plugin"]).unwrap();
assert!(matches!(cli.command, Commands::Plugins(_)));
}
#[test]
fn test_cli_parse_chain_list() {
let cli = Cli::try_parse_from(&["forge-guard", "chain", "list"]).unwrap();
assert!(matches!(cli.command, Commands::Chain(_)));
}
#[test]
fn test_cli_parse_chain_info() {
let cli = Cli::try_parse_from(&["forge-guard", "chain", "info", "polygon"]).unwrap();
assert!(matches!(cli.command, Commands::Chain(_)));
}
#[test]
fn test_cli_parse_chain_add() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"chain",
"add",
"my-chain",
"https://rpc.my-chain.io",
"99999",
])
.unwrap();
assert!(matches!(cli.command, Commands::Chain(_)));
}
#[test]
fn test_cli_parse_chain_remove() {
let cli = Cli::try_parse_from(&["forge-guard", "chain", "remove", "my-chain"]).unwrap();
assert!(matches!(cli.command, Commands::Chain(_)));
}
#[test]
fn test_cli_parse_chain_test() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"chain",
"test",
"polygon",
"https://polygon-rpc.com",
])
.unwrap();
assert!(matches!(cli.command, Commands::Chain(_)));
}
#[test]
fn test_cli_parse_security_config() {
let cli = Cli::try_parse_from(&["forge-guard", "security", "config"]).unwrap();
assert!(matches!(cli.command, Commands::Security(_)));
}
#[test]
fn test_cli_parse_security_threshold() {
let cli = Cli::try_parse_from(&["forge-guard", "security", "threshold", "85"]).unwrap();
assert!(matches!(cli.command, Commands::Security(_)));
}
#[test]
fn test_cli_parse_security_enable() {
let cli = Cli::try_parse_from(&["forge-guard", "security", "enable", "Reentrancy"]).unwrap();
assert!(matches!(cli.command, Commands::Security(_)));
}
#[test]
fn test_cli_parse_security_list() {
let cli = Cli::try_parse_from(&["forge-guard", "security", "list"]).unwrap();
assert!(matches!(cli.command, Commands::Security(_)));
}
#[test]
fn test_cli_parse_security_info() {
let cli = Cli::try_parse_from(&["forge-guard", "security", "info", "Reentrancy"]).unwrap();
assert!(matches!(cli.command, Commands::Security(_)));
}
#[test]
fn test_shared_flags_on_audit() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"audit",
"--chain",
"polygon",
"--project",
"/tmp/test",
"--json",
"--markdown",
"--strict",
"--offline",
"--production",
"--report",
"--parallelism",
"16",
])
.unwrap();
match cli.command {
Commands::Audit(args) => {
assert_eq!(args.shared.chain, "polygon");
assert_eq!(args.shared.project, std::path::PathBuf::from("/tmp/test"));
assert!(args.shared.json);
assert!(args.shared.markdown);
assert!(args.shared.strict);
assert!(args.shared.offline);
assert!(args.shared.production);
assert!(args.shared.report);
assert_eq!(args.shared.parallelism, 16);
}
_ => panic!("Expected Audit command"),
}
}
#[test]
fn test_shared_flags_on_doctor() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"doctor",
"--chain",
"optimism",
"--offline",
"--json",
])
.unwrap();
match cli.command {
Commands::Doctor(args) => {
assert_eq!(args.shared.chain, "optimism");
assert!(args.shared.offline);
assert!(args.shared.json);
}
_ => panic!("Expected Doctor command"),
}
}
#[test]
fn test_shared_flags_on_ci() {
let cli = Cli::try_parse_from(&[
"forge-guard",
"ci",
"--strict",
"--production",
"--parallelism",
"2",
])
.unwrap();
match cli.command {
Commands::Ci(args) => {
assert!(args.shared.strict);
assert!(args.shared.production);
assert_eq!(args.shared.parallelism, 2);
}
_ => panic!("Expected Ci command"),
}
}
#[test]
fn test_cli_parse_invalid_subcommand() {
let result = Cli::try_parse_from(&["forge-guard", "nonexistent"]);
assert!(result.is_err(), "Expected error for invalid subcommand");
}
#[test]
fn test_cli_parse_invalid_flag() {
let result = Cli::try_parse_from(&["forge-guard", "audit", "--nonexistent-flag"]);
assert!(result.is_err(), "Expected error for invalid flag");
}
#[test]
fn test_cli_parse_invalid_parallelism_value() {
let result = Cli::try_parse_from(&["forge-guard", "audit", "--parallelism", "not-a-number"]);
assert!(
result.is_err(),
"Expected error for non-numeric parallelism"
);
}
#[test]
fn test_cli_parse_missing_argument() {
let result = Cli::try_parse_from(&["forge-guard", "plugins", "install"]);
assert!(result.is_err(), "Expected error for missing plugin name");
}
#[test]
fn test_cli_parse_no_subcommand() {
let result = Cli::try_parse_from(&["forge-guard"]);
assert!(result.is_err(), "Expected error when no subcommand given");
}
#[test]
fn test_run_chain_list() {
let cli = Cli::try_parse_from(&["forge-guard", "chain", "list"]).unwrap();
let result = cli.run();
assert!(
result.is_ok(),
"chain list should succeed: {:?}",
result.err()
);
}
#[test]
fn test_run_security_list() {
let cli = Cli::try_parse_from(&["forge-guard", "security", "list"]).unwrap();
let result = cli.run();
assert!(
result.is_ok(),
"security list should succeed: {:?}",
result.err()
);
}
#[test]
fn test_run_security_config() {
let cli = Cli::try_parse_from(&["forge-guard", "security", "config"]).unwrap();
let result = cli.run();
assert!(
result.is_ok(),
"security config should succeed: {:?}",
result.err()
);
}
#[test]
fn test_run_security_threshold() {
let cli = Cli::try_parse_from(&["forge-guard", "security", "threshold", "75"]).unwrap();
let result = cli.run();
assert!(
result.is_ok(),
"security threshold should succeed: {:?}",
result.err()
);
}
#[test]
fn test_run_security_enable() {
let cli = Cli::try_parse_from(&["forge-guard", "security", "enable", "Reentrancy"]).unwrap();
let result = cli.run();
assert!(
result.is_ok(),
"security enable should succeed: {:?}",
result.err()
);
}
#[test]
fn test_run_security_disable() {
let cli = Cli::try_parse_from(&["forge-guard", "security", "disable", "Reentrancy"]).unwrap();
let result = cli.run();
assert!(
result.is_ok(),
"security disable should succeed: {:?}",
result.err()
);
}
#[test]
fn test_run_chain_info() {
let cli = Cli::try_parse_from(&["forge-guard", "chain", "info", "polygon"]).unwrap();
let result = cli.run();
assert!(
result.is_ok(),
"chain info should succeed: {:?}",
result.err()
);
}
#[test]
fn test_run_security_info() {
let cli = Cli::try_parse_from(&["forge-guard", "security", "info", "Reentrancy"]).unwrap();
let result = cli.run();
assert!(
result.is_ok(),
"security info should succeed: {:?}",
result.err()
);
}
#[test]
fn test_security_action_variants() {
let config = Cli::try_parse_from(&["forge-guard", "security", "config"]).unwrap();
assert!(matches!(config.command, Commands::Security(_)));
let threshold = Cli::try_parse_from(&["forge-guard", "security", "threshold", "90"]).unwrap();
let _action = SecurityAction::Threshold { score: 90 };
assert!(matches!(threshold.command, Commands::Security(_)));
let enable =
Cli::try_parse_from(&["forge-guard", "security", "enable", "tx.origin Usage"]).unwrap();
let _action2 = SecurityAction::Enable {
check: "tx.origin Usage".into(),
};
assert!(matches!(enable.command, Commands::Security(_)));
let disable =
Cli::try_parse_from(&["forge-guard", "security", "disable", "Reentrancy"]).unwrap();
assert!(matches!(disable.command, Commands::Security(_)));
let list = Cli::try_parse_from(&["forge-guard", "security", "list"]).unwrap();
assert!(matches!(list.command, Commands::Security(_)));
let info = Cli::try_parse_from(&["forge-guard", "security", "info", "Reentrancy"]).unwrap();
assert!(matches!(info.command, Commands::Security(_)));
}
#[test]
fn test_plugin_action_variants() {
let r = Cli::try_parse_from(&["forge-guard", "plugins", "list"]);
assert!(r.is_ok());
let r = Cli::try_parse_from(&["forge-guard", "plugins", "install", "p"]);
assert!(r.is_ok());
let r = Cli::try_parse_from(&[
"forge-guard",
"plugins",
"install",
"p",
"https://github.com/x/y.git",
]);
assert!(r.is_ok());
let r = Cli::try_parse_from(&["forge-guard", "plugins", "remove", "p"]);
assert!(r.is_ok());
let r = Cli::try_parse_from(&["forge-guard", "plugins", "enable", "p"]);
assert!(r.is_ok());
let r = Cli::try_parse_from(&["forge-guard", "plugins", "disable", "p"]);
assert!(r.is_ok());
let r = Cli::try_parse_from(&["forge-guard", "plugins", "new", "my-plugin"]);
assert!(r.is_ok());
}
#[test]
fn test_chain_action_variants() {
let r = Cli::try_parse_from(&["forge-guard", "chain", "list"]);
assert!(r.is_ok());
let r = Cli::try_parse_from(&["forge-guard", "chain", "info", "ethereum"]);
assert!(r.is_ok());
let r = Cli::try_parse_from(&["forge-guard", "chain", "add", "c", "http://rpc", "1"]);
assert!(r.is_ok());
let r = Cli::try_parse_from(&["forge-guard", "chain", "remove", "c"]);
assert!(r.is_ok());
let r = Cli::try_parse_from(&["forge-guard", "chain", "test", "c", "http://rpc"]);
assert!(r.is_ok());
}
#[test]
fn test_cli_run_returns_result_ok_for_valid_commands() {
let cmds = vec![
vec!["forge-guard", "security", "list"],
vec!["forge-guard", "security", "config"],
vec!["forge-guard", "chain", "list"],
vec!["forge-guard", "chain", "info", "ethereum"],
];
for args in cmds {
let cli = Cli::try_parse_from(&args).unwrap();
let result = cli.run();
assert!(
result.is_ok(),
"Command '{:?}' should succeed: {:?}",
args,
result.err()
);
}
}
const CLEAN_CONTRACT: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Simple {
uint256 public value;
address public owner;
event ValueChanged(address indexed sender, uint256 newValue);
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
constructor() {
owner = msg.sender;
}
function set(uint256 newValue) external onlyOwner {
value = newValue;
emit ValueChanged(msg.sender, newValue);
}
function get() external view returns (uint256) {
return value;
}
}
"#;
const VULNERABLE_CONTRACT: &str = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Vulnerable {
mapping(address => uint256) public balances;
address public owner;
constructor() {
owner = msg.sender;
}
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] -= amount;
}
function setAdmin(address newAdmin) external {
admin = newAdmin;
}
function kill() external {
selfdestruct(payable(msg.sender));
}
function transfer(address to, uint256 amount) external {
require(tx.origin == owner);
balances[to] += amount;
}
receive() external payable {}
}
"#;
fn create_temp_project(contracts: &[(&str, &str)]) -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::tempdir().expect("Failed to create temp dir");
let src_dir = dir.path().join("test-contracts").join("secure").join("src");
std::fs::create_dir_all(&src_dir).expect("Failed to create src dir");
for (filename, content) in contracts {
let file_path = src_dir.join(filename);
std::fs::write(&file_path, content)
.unwrap_or_else(|e| panic!("Failed to write {}: {}", filename, e));
}
let path = dir.path().to_path_buf();
(dir, path)
}
fn run_audit(project: &std::path::Path, extra_args: &[&str]) -> Result<(), anyhow::Error> {
let mut args = vec!["forge-guard", "audit", "--offline"];
args.push("--project");
args.push(project.to_str().unwrap());
args.extend_from_slice(extra_args);
let cli = Cli::try_parse_from(&args).expect("Failed to parse CLI args");
cli.run()
}
#[test]
fn test_audit_clean_contract_succeeds() {
let (_dir, project) = create_temp_project(&[("Simple.sol", CLEAN_CONTRACT)]);
let result = run_audit(&project, &[]);
assert!(
result.is_ok(),
"Audit of clean contract should succeed: {:?}",
result.err()
);
}
#[test]
fn test_audit_clean_contract_quick_mode() {
let (_dir, project) = create_temp_project(&[("Simple.sol", CLEAN_CONTRACT)]);
let result = run_audit(&project, &["--quick"]);
assert!(
result.is_ok(),
"Quick audit of clean contract should succeed: {:?}",
result.err()
);
}
#[test]
fn test_audit_clean_contract_json_output() {
let (_dir, project) = create_temp_project(&[("Simple.sol", CLEAN_CONTRACT)]);
let result = run_audit(&project, &["--json"]);
assert!(
result.is_ok(),
"Audit with --json should succeed: {:?}",
result.err()
);
}
#[test]
fn test_audit_clean_contract_strict_mode_passes() {
let (_dir, project) = create_temp_project(&[("Simple.sol", CLEAN_CONTRACT)]);
let result = run_audit(&project, &["--strict"]);
let _ = result;
}
#[test]
fn test_audit_vulnerable_contract_detects_issues() {
let (_dir, project) = create_temp_project(&[("Vulnerable.sol", VULNERABLE_CONTRACT)]);
let result = run_audit(&project, &[]);
assert!(
result.is_ok(),
"Audit of vulnerable contract should succeed (reports issues): {:?}",
result.err()
);
}
#[test]
fn test_audit_vulnerable_contract_strict_fails() {
let (_dir, project) = create_temp_project(&[("Vulnerable.sol", VULNERABLE_CONTRACT)]);
let result = run_audit(&project, &["--strict"]);
assert!(
result.is_err(),
"Strict audit should fail on vulnerable contract with findings"
);
}
#[test]
fn test_audit_vulnerable_contract_with_exploit() {
let (_dir, project) = create_temp_project(&[("Vulnerable.sol", VULNERABLE_CONTRACT)]);
let result = run_audit(&project, &["--exploit"]);
assert!(
result.is_ok(),
"Audit with exploit analysis should succeed: {:?}",
result.err()
);
}
#[test]
fn test_audit_vulnerable_contract_with_gas() {
let (_dir, project) = create_temp_project(&[("Vulnerable.sol", VULNERABLE_CONTRACT)]);
let result = run_audit(&project, &["--gas"]);
assert!(
result.is_ok(),
"Audit with gas analysis should succeed: {:?}",
result.err()
);
}
#[test]
fn test_audit_vulnerable_contract_json_output() {
let (_dir, project) = create_temp_project(&[("Vulnerable.sol", VULNERABLE_CONTRACT)]);
let result = run_audit(&project, &["--json"]);
assert!(
result.is_ok(),
"Audit with --json on vulnerable contract should succeed: {:?}",
result.err()
);
}
#[test]
fn test_audit_empty_directory_fails() {
let (_dir, project) = create_temp_project(&[]);
let result = run_audit(&project, &[]);
assert!(result.is_err(), "Audit with empty project should fail");
let err = format!("{:#}", result.unwrap_err());
assert!(
err.contains("No Solidity source files found"),
"Error should mention no source files: {}",
err
);
}
#[test]
fn test_audit_multiple_contracts() {
let (_dir, project) = create_temp_project(&[
("Simple.sol", CLEAN_CONTRACT),
("Vulnerable.sol", VULNERABLE_CONTRACT),
]);
let result = run_audit(&project, &[]);
assert!(
result.is_ok(),
"Audit of multiple contracts should succeed: {:?}",
result.err()
);
}
#[test]
fn test_audit_with_report_output() {
let (_dir, project) = create_temp_project(&[("Simple.sol", CLEAN_CONTRACT)]);
let result = run_audit(&project, &["--report"]);
assert!(
result.is_ok(),
"Audit with --report should succeed: {:?}",
result.err()
);
}
#[test]
fn test_audit_different_chain() {
let (_dir, project) = create_temp_project(&[("Simple.sol", CLEAN_CONTRACT)]);
let result = run_audit(&project, &["--chain", "polygon"]);
assert!(
result.is_ok(),
"Audit with different chain should succeed: {:?}",
result.err()
);
}
#[test]
fn test_audit_with_exclude_pattern() {
let (_dir, project) = create_temp_project(&[
("Simple.sol", CLEAN_CONTRACT),
("Vulnerable.sol", VULNERABLE_CONTRACT),
]);
let result = run_audit(&project, &["--exclude", "Vulnerable"]);
assert!(
result.is_ok(),
"Audit with exclude should succeed: {:?}",
result.err()
);
}
#[test]
fn test_audit_custom_sources() {
let (_dir, project) = create_temp_project(&[("Simple.sol", CLEAN_CONTRACT)]);
let result = run_audit(&project, &["--sources", "src"]);
assert!(
result.is_ok(),
"Audit with explicit sources should succeed: {:?}",
result.err()
);
}
#[test]
fn test_audit_non_standard_sources_dir() {
let dir = tempfile::tempdir().expect("Failed to create temp dir");
let default_src = dir.path().join("test-contracts").join("secure").join("src");
std::fs::create_dir_all(&default_src).expect("Failed to create dir");
std::fs::write(default_src.join("Simple.sol"), CLEAN_CONTRACT)
.expect("Failed to write contract");
let contracts_dir = dir.path().join("contracts");
std::fs::create_dir_all(&contracts_dir).expect("Failed to create dir");
std::fs::write(contracts_dir.join("Other.sol"), CLEAN_CONTRACT)
.expect("Failed to write contract");
let result = run_audit(dir.path(), &["--quick"]);
assert!(
result.is_ok(),
"Audit should succeed with files in configured src_dirs: {:?}",
result.err()
);
}
#[test]
fn test_deploy_vulnerable_contract_blocked() {
let (_dir, project) = create_temp_project(&[("Vulnerable.sol", VULNERABLE_CONTRACT)]);
let args = vec![
"forge-guard",
"deploy",
"Vulnerable",
"--project",
project.to_str().unwrap(),
"--offline",
];
let cli = Cli::try_parse_from(&args).expect("Failed to parse CLI args");
let result = cli.run();
assert!(result.is_err(), "Deploy of vulnerable contract should err");
let err_msg = format!("{:#}", result.unwrap_err());
assert!(
err_msg.contains("blocked")
|| err_msg.contains("forge")
|| err_msg.contains("Failed")
|| err_msg.contains("Deploy failed"),
"Error should mention blocked or forge: {:#}",
err_msg
);
}
#[test]
fn test_deploy_clean_contract_outcome() {
let (_dir, project) = create_temp_project(&[("Simple.sol", CLEAN_CONTRACT)]);
let args = vec![
"forge-guard",
"deploy",
"Simple",
"--project",
project.to_str().unwrap(),
"--offline",
];
let cli = Cli::try_parse_from(&args).expect("Failed to parse CLI args");
let result = cli.run();
if let Err(e) = &result {
let msg = format!("{:#}", e);
assert!(
msg.contains("forge") || msg.contains("Failed") || msg.contains("blocked"),
"If deploy fails, should mention forge or blocked: {:#}",
msg
);
}
}
#[test]
fn test_deploy_force_bypass() {
let (_dir, project) = create_temp_project(&[("Simple.sol", CLEAN_CONTRACT)]);
let args = vec![
"forge-guard",
"deploy",
"Simple",
"--project",
project.to_str().unwrap(),
"--offline",
"--force",
];
let cli = Cli::try_parse_from(&args).expect("Failed to parse CLI args");
let result = cli.run();
if let Err(e) = &result {
let msg = format!("{:#}", e);
assert!(
msg.contains("forge") || msg.contains("Failed"),
"With --force, error should be forge-related: {:#}",
msg
);
}
}
#[test]
fn test_deploy_safe_blocks_vulnerable() {
let (_dir, project) = create_temp_project(&[("Vulnerable.sol", VULNERABLE_CONTRACT)]);
let args = vec![
"forge-guard",
"deploy-safe",
"Vulnerable",
"--project",
project.to_str().unwrap(),
"--offline",
];
let cli = Cli::try_parse_from(&args).expect("Failed to parse CLI args");
let result = cli.run();
assert!(result.is_err(), "deploy-safe should block vulnerable");
let msg = format!("{:#}", result.unwrap_err());
assert!(
msg.contains("blocked") || msg.contains("Security"),
"Error should mention blocked or security: {:#}",
msg
);
}
#[test]
fn test_deploy_empty_project_fails() {
let (_dir, project) = create_temp_project(&[]);
let args = vec![
"forge-guard",
"deploy",
"--project",
project.to_str().unwrap(),
"--offline",
];
let cli = Cli::try_parse_from(&args).expect("Failed to parse CLI args");
let result = cli.run();
assert!(result.is_err(), "Deploy with no contracts should fail");
}