use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use tempfile::TempDir;
fn setup_test_project() -> TempDir {
let temp = TempDir::new().expect("Failed to create temp dir");
let base = temp.path();
let dsl_content = r#"
invariant: balance_conservation
description: "Total balance must be conserved across transactions"
forall tx in transactions:
sum(tx.inputs) == sum(tx.outputs) + tx.fee
"#;
fs::write(base.join("invariants.invar"), dsl_content).expect("Failed to write invariants file");
temp
}
#[test]
fn test_cli_help_output() {
let mut cmd = Command::cargo_bin("truent").expect("Failed to find binary");
cmd.arg("--help");
cmd.assert()
.success()
.stdout(predicate::str::contains("Truent"));
}
#[test]
fn test_cli_version_output() {
let mut cmd = Command::cargo_bin("truent").expect("Failed to find binary");
cmd.arg("--version");
cmd.assert().success();
}
#[test]
fn test_cli_init_creates_project() {
let temp = TempDir::new().expect("Failed to create temp dir");
let project_path = temp.path().join("new_project");
let mut cmd = Command::cargo_bin("truent").expect("Failed to find binary");
cmd.arg("init").arg(&project_path);
cmd.assert().success();
assert!(
project_path.exists(),
"init should create project directory"
);
}
#[test]
fn test_cli_missing_file_exits_with_error() {
let mut cmd = Command::cargo_bin("truent").expect("Failed to find binary");
cmd.arg("build")
.arg("--source")
.arg("/nonexistent/file.rs")
.arg("--chain")
.arg("solana")
.arg("--output")
.arg("/tmp/out");
cmd.assert().failure();
}
#[test]
fn test_cli_invalid_chain_exits_with_error() {
let temp = setup_test_project();
let mut cmd = Command::cargo_bin("truent").expect("Failed to find binary");
cmd.arg("build")
.arg("--source")
.arg(temp.path().join("test.rs"))
.arg("--chain")
.arg("invalid_chain")
.arg("--output")
.arg(temp.path().join("output"));
cmd.assert().failure();
}
#[test]
fn test_cli_verbose_flag_produces_output() {
let _temp = setup_test_project();
let mut cmd = Command::cargo_bin("truent").expect("Failed to find binary");
cmd.arg("--verbose").arg("doctor");
cmd.assert().success();
}
#[test]
fn test_cli_log_level_flag() {
let mut cmd = Command::cargo_bin("truent").expect("Found to find binary");
cmd.arg("--verbose").arg("doctor");
cmd.assert().success();
}
#[test]
fn test_cli_invalid_subcommand() {
let mut cmd = Command::cargo_bin("truent").expect("Failed to find binary");
cmd.arg("nonexistent_command");
cmd.assert().failure();
}
mod exit_codes {
use super::*;
#[test]
fn test_exit_code_success_is_zero() {
let mut cmd = Command::cargo_bin("truent").expect("Failed to find binary");
cmd.arg("--help");
let output = cmd.output().expect("Failed to execute");
assert_eq!(output.status.code(), Some(0), "Success should exit with 0");
}
#[test]
fn test_exit_code_error_is_nonzero() {
let mut cmd = Command::cargo_bin("truent").expect("Failed to find binary");
cmd.arg("nonexistent_command");
let output = cmd.output().expect("Failed to execute");
assert_ne!(
output.status.code(),
Some(0),
"Error should exit with non-zero"
);
}
}
mod output_formats {
use super::*;
#[test]
fn test_json_output_is_valid() {
let temp = setup_test_project();
let mut cmd = Command::cargo_bin("truent").expect("Failed to find binary");
cmd.arg("report")
.arg("--input")
.arg(temp.path().join("test_report.json"))
.arg("--format")
.arg("json");
let output = cmd.output().expect("Failed to execute");
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("{") || stdout.is_empty());
}
}
#[test]
fn test_markdown_output() {
let temp = setup_test_project();
let mut cmd = Command::cargo_bin("truent").expect("Failed to find binary");
cmd.arg("report")
.arg("--input")
.arg(temp.path().join("test_report.json"))
.arg("--format")
.arg("markdown");
let output = cmd.output().expect("Failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(!stdout.starts_with("{") || output.status.success());
}
}
mod configuration {
use super::*;
#[test]
fn test_config_file_loading() {
let temp = TempDir::new().expect("Failed to create temp dir");
let config_path = temp.path().join("invar.toml");
let config_content = r#"
[project]
name = "test-project"
version = "0.1.0"
[invariants]
chains = ["solana", "evm"]
"#;
fs::write(&config_path, config_content).expect("Failed to write config");
assert!(config_path.exists());
}
}
mod determinism {
use super::*;
#[test]
fn test_same_input_same_output() {
let _temp = setup_test_project();
let mut cmd1 = Command::cargo_bin("truent").expect("Failed to find binary");
cmd1.arg("list");
let output1 = cmd1.output().expect("Failed to execute");
let mut cmd2 = Command::cargo_bin("truent").expect("Failed to find binary");
cmd2.arg("list");
let output2 = cmd2.output().expect("Failed to execute");
assert_eq!(output1.stdout, output2.stdout, "CLI must be deterministic");
assert_eq!(
output1.stderr, output2.stderr,
"Errors must be deterministic"
);
}
}
mod detection {
use super::*;
fn fixture_path(name: &str) -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
#[test]
fn test_check_detects_vulnerable_evm_fixture() {
let mut cmd = Command::cargo_bin("truent").expect("Failed to find binary");
cmd.arg("check")
.arg(fixture_path("test_vulnerable_evm.sol"))
.arg("--chain")
.arg("evm")
.arg("--format")
.arg("json");
let output = cmd.output().expect("Failed to execute");
assert_eq!(
output.status.code(),
Some(0),
"unproven leads must not fail the run by default"
);
let mut gated = Command::cargo_bin("truent").expect("binary exists");
gated
.arg("check")
.arg(fixture_path("test_vulnerable_evm.sol"))
.arg("--chain")
.arg("evm")
.arg("--format")
.arg("json")
.arg("--fail-on-leads");
assert_eq!(
gated.output().expect("Failed to execute").status.code(),
Some(1),
"--fail-on-leads must gate on unproven results"
);
let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value =
serde_json::from_str(&stdout).expect("check --format json must emit valid JSON");
let violation_count = json["summary"]["violations"]
.as_u64()
.expect("summary.violations must be a number");
assert!(
violation_count > 0,
"Expected at least one violation in the known-vulnerable EVM fixture, found 0"
);
}
#[test]
fn test_check_detects_vulnerable_solana_fixture() {
let mut cmd = Command::cargo_bin("truent").expect("Failed to find binary");
cmd.arg("check")
.arg(fixture_path("test_vulnerable_solana.rs"))
.arg("--chain")
.arg("solana")
.arg("--format")
.arg("json");
let output = cmd.output().expect("Failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value =
serde_json::from_str(&stdout).expect("check --format json must emit valid JSON");
let violation_count = json["summary"]["violations"]
.as_u64()
.expect("summary.violations must be a number");
assert!(
violation_count > 0,
"Expected at least one violation in the known-vulnerable Solana fixture, found 0"
);
}
#[test]
fn test_check_detects_vulnerable_move_fixture() {
let mut cmd = Command::cargo_bin("truent").expect("Failed to find binary");
cmd.arg("check")
.arg(fixture_path("test_vulnerable_move.move"))
.arg("--chain")
.arg("move")
.arg("--format")
.arg("json");
let output = cmd.output().expect("Failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value =
serde_json::from_str(&stdout).expect("check --format json must emit valid JSON");
let violation_count = json["summary"]["violations"]
.as_u64()
.expect("summary.violations must be a number");
assert!(
violation_count > 0,
"Expected at least one violation in the known-vulnerable Move fixture, found 0"
);
}
#[test]
fn test_shared_ir_rule_fires_for_solana_and_move() {
for (fixture, chain) in [
("test_vulnerable_solana.rs", "solana"),
("test_vulnerable_move.move", "move"),
] {
let mut cmd = Command::cargo_bin("truent").expect("Failed to find binary");
cmd.arg("check")
.arg(fixture_path(fixture))
.arg("--chain")
.arg(chain)
.arg("--format")
.arg("json");
let output = cmd.output().expect("Failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value =
serde_json::from_str(&stdout).expect("check --format json must emit valid JSON");
let violations = json["violations"]
.as_array()
.expect("violations must be an array");
assert!(
violations
.iter()
.any(|v| v["invariant_id"] == "unauthorized_privileged_mutation"),
"Expected the shared-IR rule to fire for {chain} fixture {fixture}, but it didn't. Violations: {violations:#?}"
);
}
}
#[test]
fn test_scan_respects_severity_and_fail_on_filters() {
let mut cmd = Command::cargo_bin("truent").expect("Failed to find binary");
cmd.arg("scan")
.arg(fixture_path("test_vulnerable_evm.sol"))
.arg("--chain")
.arg("evm")
.arg("--output")
.arg("json")
.arg("--severity")
.arg("critical")
.arg("--fail-on")
.arg("critical");
let output = cmd.output().expect("Failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value =
serde_json::from_str(&stdout).expect("scan --output json must emit valid JSON");
let violations = json["violations"]
.as_array()
.expect("violations must be an array");
assert!(
!violations.is_empty(),
"Expected at least one critical violation in the vulnerable EVM fixture"
);
assert!(
violations.iter().all(|v| v["severity"] == "critical"),
"--severity critical must filter out all non-critical violations"
);
assert_eq!(
output.status.code(),
Some(0),
"critical *leads* are reported but do not fail the run"
);
}
#[test]
fn test_fuzz_runs_against_real_detectors_without_crashing() {
let mut cmd = Command::cargo_bin("truent").expect("Failed to find binary");
cmd.arg("fuzz")
.arg(fixture_path("test_vulnerable_evm.sol"))
.arg("--chain")
.arg("evm")
.arg("--iterations")
.arg("50")
.arg("--depth")
.arg("4")
.arg("--seed")
.arg("7");
let output = cmd.output().expect("Failed to execute");
assert_eq!(
output.status.code(),
Some(0),
"fuzz must exit 0 when no crashes are found"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Ran 50 iterations"),
"expected fuzz to report the iteration count, got: {stderr}"
);
assert!(
stderr.contains("No crashes found"),
"expected fuzz to report no crashes on the known-parseable fixture, got: {stderr}"
);
}
}