use assert_cmd::Command;
use std::io::Write;
use tempfile::TempDir;
fn sdkt() -> Command {
Command::cargo_bin("sdkt").unwrap()
}
fn write_fixture(dir: &TempDir, name: &str, content: &str) -> std::path::PathBuf {
let p = dir.path().join(name);
let mut f = std::fs::File::create(&p).unwrap();
f.write_all(content.as_bytes()).unwrap();
p
}
fn sarif_schema() -> serde_json::Value {
let schema_bytes = include_bytes!("fixtures/sarif-2.1.0.json");
serde_json::from_slice(schema_bytes).expect("fixture is valid JSON")
}
fn assert_valid_sarif(doc: &serde_json::Value) {
let schema = sarif_schema();
let validator = jsonschema::validator_for(&schema).expect("schema compiles");
if let Err(e) = validator.validate(doc) {
panic!("SARIF document failed schema validation: {e}");
}
}
#[test]
fn sarif_output_has_version_and_schema_fields() {
let dir = TempDir::new().unwrap();
let path = write_fixture(&dir, "bad.rs", "pub fn mint(to: Address) { }\n");
let out = sdkt()
.args(["audit", path.to_str().unwrap(), "--format", "sarif"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).expect("sarif output is valid JSON");
assert_eq!(v["version"], "2.1.0", "SARIF version must be 2.1.0");
assert!(
v["$schema"].as_str().unwrap_or("").contains("sarif"),
"$schema URI must reference sarif"
);
assert!(v["runs"].is_array(), "runs array present");
assert_eq!(v["runs"].as_array().unwrap().len(), 1, "exactly one run");
}
#[test]
fn sarif_output_has_tool_driver() {
let dir = TempDir::new().unwrap();
let path = write_fixture(
&dir,
"ok.rs",
"pub fn balance(who: Address) -> u32 { require_auth(); 0 }\n",
);
let out = sdkt()
.args(["audit", path.to_str().unwrap(), "--format", "sarif"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
let driver = &v["runs"][0]["tool"]["driver"];
assert_eq!(driver["name"], "sdkt", "tool name is sdkt");
assert!(driver["version"].is_string(), "tool version is a string");
assert!(
driver["informationUri"].is_string(),
"informationUri is a string"
);
}
#[test]
fn sarif_each_finding_becomes_a_result() {
let dir = TempDir::new().unwrap();
let path = write_fixture(
&dir,
"bad.rs",
"pub fn initialize(admin: Address) { }\npub fn mint(to: Address) { }\n",
);
let out = sdkt()
.args(["audit", path.to_str().unwrap(), "--format", "sarif"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
let results = v["runs"][0]["results"].as_array().expect("results array");
assert!(
!results.is_empty(),
"source with issues must produce results"
);
for result in results {
assert!(result["ruleId"].is_string(), "ruleId present");
assert!(result["level"].is_string(), "level present");
assert!(
result["message"]["text"].is_string(),
"message.text present"
);
assert!(result["locations"].is_array(), "locations array present");
}
}
#[test]
fn sarif_critical_severity_maps_to_error_level() {
let dir = TempDir::new().unwrap();
let path = write_fixture(&dir, "bad.rs", "pub fn mint(to: Address) { }\n");
let out = sdkt()
.args(["audit", path.to_str().unwrap(), "--format", "sarif"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
let results = v["runs"][0]["results"].as_array().unwrap();
let auth001 = results.iter().find(|r| r["ruleId"] == "AUTH-001");
let result = auth001.expect("AUTH-001 finding present");
assert_eq!(result["level"], "error", "Critical maps to SARIF error");
}
#[test]
fn sarif_warning_severity_maps_to_warning_level() {
let dir = TempDir::new().unwrap();
let path = write_fixture(
&dir,
"move.rs",
"pub fn do_swap(x: i128) { foo(x); bar(x); }\n",
);
let out = sdkt()
.args(["audit", path.to_str().unwrap(), "--format", "sarif"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
let results = v["runs"][0]["results"].as_array().unwrap();
let move001 = results.iter().find(|r| r["ruleId"] == "MOVE-001");
let result = move001.expect("MOVE-001 (warning) finding present");
assert_eq!(
result["level"], "warning",
"Warning severity maps to SARIF warning"
);
}
#[test]
fn sarif_includes_division_before_multiplication_finding() {
let dir = TempDir::new().unwrap();
let path = write_fixture(
&dir,
"arithmetic.rs",
"pub fn quote(amount: i128, bps: i128) -> i128 { amount / 10_000 * bps }\n",
);
let out = sdkt()
.args(["audit", path.to_str().unwrap(), "--format", "sarif"])
.assert()
.success()
.get_output()
.stdout
.clone();
let doc: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_valid_sarif(&doc);
let results = doc["runs"][0]["results"].as_array().unwrap();
let finding = results
.iter()
.find(|finding| finding["ruleId"] == "MATH-001")
.expect("MATH-001 should be rendered in SARIF");
assert_eq!(finding["level"], "warning");
assert!(finding["message"]["text"]
.as_str()
.unwrap()
.contains("divides before multiplying"));
}
#[test]
fn sarif_artifact_uri_contains_source_file_path() {
let dir = TempDir::new().unwrap();
let path = write_fixture(&dir, "mycontract.rs", "pub fn mint(to: Address) { }\n");
let path_str = path.to_str().unwrap();
let out = sdkt()
.args(["audit", path_str, "--format", "sarif"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
let results = v["runs"][0]["results"].as_array().unwrap();
assert!(!results.is_empty(), "need at least one result to check");
for result in results {
let uri = result["locations"][0]["physicalLocation"]["artifactLocation"]["uri"]
.as_str()
.expect("uri is a string");
assert!(
uri.contains("mycontract.rs"),
"artifact URI should contain the source filename; got: {uri}"
);
}
}
#[test]
fn sarif_multiple_files_preserve_individual_artifact_uris() {
let dir = TempDir::new().unwrap();
let a = write_fixture(&dir, "a.rs", "pub fn mint_token(to: Address) { }\n");
let b = write_fixture(
&dir,
"b.rs",
"pub fn transfer(from: Address, to: Address, amount: i128) { }\n",
);
let out = sdkt()
.args([
"audit",
a.to_str().unwrap(),
b.to_str().unwrap(),
"--format",
"sarif",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
let results = v["runs"][0]["results"].as_array().unwrap();
assert!(
!results.is_empty(),
"multi-file sarif must include findings"
);
let auth001 = results
.iter()
.find(|result| result["ruleId"] == "AUTH-001")
.expect("AUTH-001 result should be present");
let auth004 = results
.iter()
.find(|result| result["ruleId"] == "AUTH-004")
.expect("AUTH-004 result should be present");
let auth001_uri = auth001["locations"][0]["physicalLocation"]["artifactLocation"]["uri"]
.as_str()
.unwrap();
let auth004_uri = auth004["locations"][0]["physicalLocation"]["artifactLocation"]["uri"]
.as_str()
.unwrap();
assert!(
auth001_uri.contains("a.rs"),
"AUTH-001 should be attributed to a.rs; got: {auth001_uri}"
);
assert!(
auth004_uri.contains("b.rs"),
"AUTH-004 should be attributed to b.rs; got: {auth004_uri}"
);
}
#[test]
fn sarif_rule_ids_appear_in_rules_section() {
let dir = TempDir::new().unwrap();
let path = write_fixture(
&dir,
"bad.rs",
"pub fn initialize(admin: Address) { }\npub fn mint(to: Address) { }\n",
);
let out = sdkt()
.args(["audit", path.to_str().unwrap(), "--format", "sarif"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
let rules = v["runs"][0]["tool"]["driver"]["rules"]
.as_array()
.expect("rules array");
assert!(!rules.is_empty(), "rules section is non-empty");
let results = v["runs"][0]["results"].as_array().unwrap();
let rule_ids_in_rules: Vec<&str> = rules.iter().filter_map(|r| r["id"].as_str()).collect();
for result in results {
let rid = result["ruleId"].as_str().unwrap();
assert!(
rule_ids_in_rules.contains(&rid),
"ruleId '{rid}' in results must have an entry in rules section"
);
}
}
#[test]
fn sarif_empty_findings_produces_valid_document_with_zero_results() {
let dir = TempDir::new().unwrap();
let path = write_fixture(
&dir,
"ok.rs",
"pub fn balance(who: Address) -> u32 { require_auth(); 0 }\n",
);
let out = sdkt()
.args(["audit", path.to_str().unwrap(), "--format", "sarif"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
let results = v["runs"][0]["results"].as_array().expect("results array");
assert!(
results.is_empty(),
"clean source produces zero SARIF results"
);
let rules = v["runs"][0]["tool"]["driver"]["rules"].as_array().unwrap();
assert!(rules.is_empty(), "no rules section for zero findings");
}
#[test]
fn sarif_output_validates_against_sarif_2_1_0_schema_with_findings() {
let dir = TempDir::new().unwrap();
let path = write_fixture(
&dir,
"bad.rs",
"pub fn initialize(admin: Address) { }\npub fn mint(to: Address) { }\n",
);
let out = sdkt()
.args(["audit", path.to_str().unwrap(), "--format", "sarif"])
.assert()
.success()
.get_output()
.stdout
.clone();
let doc: serde_json::Value = serde_json::from_slice(&out).expect("valid JSON");
assert_valid_sarif(&doc);
}
#[test]
fn sarif_output_validates_against_sarif_2_1_0_schema_empty_findings() {
let dir = TempDir::new().unwrap();
let path = write_fixture(
&dir,
"ok.rs",
"pub fn balance(who: Address) -> u32 { require_auth(); 0 }\n",
);
let out = sdkt()
.args(["audit", path.to_str().unwrap(), "--format", "sarif"])
.assert()
.success()
.get_output()
.stdout
.clone();
let doc: serde_json::Value = serde_json::from_slice(&out).expect("valid JSON");
assert_valid_sarif(&doc);
}
#[test]
fn json_format_still_produces_findings_and_summary_fields() {
let dir = TempDir::new().unwrap();
let path = write_fixture(&dir, "bad.rs", "pub fn initialize(admin: Address) { }\n");
let out = sdkt()
.args(["audit", path.to_str().unwrap(), "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).expect("valid JSON");
assert!(
v.get("findings").is_some(),
"json output must have findings"
);
assert!(v.get("summary").is_some(), "json output must have summary");
assert!(v.get("runs").is_none(), "json output must not be SARIF");
}
#[test]
fn pretty_format_still_produces_text_report() {
let dir = TempDir::new().unwrap();
let path = write_fixture(&dir, "bad.rs", "pub fn mint(to: Address) { }\n");
sdkt()
.args(["audit", path.to_str().unwrap(), "--format", "pretty"])
.assert()
.success()
.stdout(predicates::str::contains("Static Analysis Report"))
.stdout(predicates::str::contains("AUTH-001"));
}
#[test]
fn invalid_format_exits_nonzero() {
let dir = TempDir::new().unwrap();
let path = write_fixture(
&dir,
"ok.rs",
"pub fn balance(who: Address) -> u32 { require_auth(); 0 }\n",
);
sdkt()
.args(["audit", path.to_str().unwrap(), "--format", "xml"])
.assert()
.failure();
}
#[test]
fn list_rules_with_sarif_format_exits_nonzero_with_error() {
sdkt()
.args(["audit", "--list-rules", "--format", "sarif"])
.assert()
.failure()
.stderr(predicates::str::contains("--format sarif"));
}
#[test]
fn sarif_uribaseid_present_for_relative_path() {
let rel_name = "_sarif_test_relative_fixture.rs";
let cwd = std::env::current_dir().unwrap();
let abs = cwd.join(rel_name);
std::fs::write(&abs, "pub fn mint(to: Address) { }\n").unwrap();
let out = sdkt()
.args(["audit", rel_name, "--format", "sarif"])
.assert()
.success()
.get_output()
.stdout
.clone();
let _ = std::fs::remove_file(&abs);
let v: serde_json::Value = serde_json::from_slice(&out).unwrap();
let results = v["runs"][0]["results"].as_array().unwrap();
if !results.is_empty() {
let uri_base =
&results[0]["locations"][0]["physicalLocation"]["artifactLocation"]["uriBaseId"];
assert_eq!(
uri_base, "%SRCROOT%",
"relative path must have %SRCROOT% uriBaseId"
);
}
}