use std::fs;
use std::path::{Path, PathBuf};
use secunit_capture::canonical::Envelope;
use secunit_capture::time::set_fixed_time_for_tests;
use serde_json::{json, Value};
fn fixture_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.join("testdata/fixtures/captures")
.canonicalize()
.expect("locate testdata/fixtures/captures")
}
fn read_json(p: &Path) -> Value {
let text = fs::read_to_string(p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()));
serde_json::from_str(&text).unwrap_or_else(|e| panic!("parse {}: {e}", p.display()))
}
fn assert_byte_identical_and_schema_valid(env_a: &Envelope, env_b: &Envelope) {
let a = env_a.to_canonical_json().unwrap();
let b = env_b.to_canonical_json().unwrap();
assert_eq!(
a, b,
"two captures of the same fixture must be byte-identical"
);
let errs = secunit_capture::schema::validate(env_a).expect("schema lookup");
assert!(errs.is_empty(), "schema mismatch: {errs:?}");
}
#[cfg(feature = "deps")]
mod deps_fixtures {
use super::*;
use secunit_capture::deps::cmd::testing::CannedRunner;
use secunit_capture::deps::cmd::CmdOutput;
fn pip_audit_runner(stdout: String) -> CannedRunner {
let r = CannedRunner::new();
r.register(
"pip-audit",
&["--format=json", "--strict", "-r", "requirements.txt"],
CmdOutput {
stdout,
stderr: String::new(),
exit_code: 1,
},
);
r
}
fn pip_audit_project_dir() -> tempfile::TempDir {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("requirements.txt"), "# placeholder\n").unwrap();
d
}
fn pnpm_runner(stdout: String) -> CannedRunner {
let r = CannedRunner::new();
r.register(
"pnpm",
&["audit", "--json"],
CmdOutput {
stdout,
stderr: String::new(),
exit_code: 1,
},
);
r
}
#[test]
fn pip_audit_fixture_roundtrips() {
let _g = set_fixed_time_for_tests("2026-05-01T00:00:00Z");
let fx = fixture_root().join("deps/pip-audit/sample.json");
let stdout = fs::read_to_string(&fx).unwrap();
let runner = pip_audit_runner(stdout);
let d = pip_audit_project_dir();
let a = secunit_capture::deps::pip_audit::capture_with(d.path(), &runner).unwrap();
let b = secunit_capture::deps::pip_audit::capture_with(d.path(), &runner).unwrap();
assert_byte_identical_and_schema_valid(&a, &b);
}
#[test]
fn pnpm_audit_fixture_roundtrips() {
let _g = set_fixed_time_for_tests("2026-05-01T00:00:00Z");
let fx = fixture_root().join("deps/pnpm-audit/sample.json");
let stdout = fs::read_to_string(&fx).unwrap();
let runner = pnpm_runner(stdout);
let a =
secunit_capture::deps::pnpm_audit::capture_with(Path::new("/tmp"), &runner).unwrap();
let b =
secunit_capture::deps::pnpm_audit::capture_with(Path::new("/tmp"), &runner).unwrap();
assert_byte_identical_and_schema_valid(&a, &b);
}
#[test]
fn cargo_audit_fixture_roundtrips() {
let _g = set_fixed_time_for_tests("2026-05-01T00:00:00Z");
let fx = fixture_root().join("deps/cargo-audit/synthetic-report.json");
let mut raw_a = read_json(&fx);
let mut raw_b = read_json(&fx);
let result_a = secunit_capture::deps::cargo_audit::canonicalize_report(&mut raw_a);
let result_b = secunit_capture::deps::cargo_audit::canonicalize_report(&mut raw_b);
let args = json!({"lockfile": "/x", "db_path": null});
let env_a = Envelope::new("deps.cargo-audit", "1", args.clone(), result_a);
let env_b = Envelope::new("deps.cargo-audit", "1", args, result_b);
assert_byte_identical_and_schema_valid(&env_a, &env_b);
}
#[tokio::test]
async fn osv_query_fixture_roundtrips() {
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
let _g = set_fixed_time_for_tests("2026-05-01T00:00:00Z");
let fx = fixture_root().join("deps/osv-query/requests-2.30.json");
let body = read_json(&fx);
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/query"))
.respond_with(ResponseTemplate::new(200).set_body_json(body))
.mount(&server)
.await;
let args = secunit_capture::deps::osv_query::OsvArgs {
ecosystem: "PyPI",
package: "requests",
version: "2.30.0",
};
let a = secunit_capture::deps::osv_query::capture_with_base(&server.uri(), args)
.await
.unwrap();
let args2 = secunit_capture::deps::osv_query::OsvArgs {
ecosystem: "PyPI",
package: "requests",
version: "2.30.0",
};
let b = secunit_capture::deps::osv_query::capture_with_base(&server.uri(), args2)
.await
.unwrap();
assert_byte_identical_and_schema_valid(&a, &b);
}
}
#[cfg(feature = "github")]
mod github_fixtures {
use super::*;
use secunit_capture::github::GhClient;
use wiremock::matchers::{method, path, query_param, query_param_is_missing};
use wiremock::{Mock, MockServer, ResponseTemplate};
async fn empty_page_2(server: &MockServer, route: &str) {
Mock::given(method("GET"))
.and(path(route))
.and(query_param("page", "2"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
.mount(server)
.await;
}
#[tokio::test]
async fn dependabot_alerts_fixture_roundtrips() {
let _g = set_fixed_time_for_tests("2026-05-01T00:00:00Z");
let fx = fixture_root().join("github/dependabot-alerts/page-1.json");
let body = read_json(&fx);
let server = MockServer::start().await;
let next_url = format!(
"{}/repos/o/r/dependabot/alerts?per_page=100&after=CUR1",
server.uri()
);
Mock::given(method("GET"))
.and(path("/repos/o/r/dependabot/alerts"))
.and(query_param_is_missing("after"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("Link", format!(r#"<{next_url}>; rel="next""#))
.set_body_json(body),
)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/repos/o/r/dependabot/alerts"))
.and(query_param("after", "CUR1"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
.mount(&server)
.await;
let c = GhClient::with_base_uri(&server.uri(), Some("ghp_x")).unwrap();
let a = secunit_capture::github::dependabot_alerts::capture(&c, "o", "r", None)
.await
.unwrap();
let b = secunit_capture::github::dependabot_alerts::capture(&c, "o", "r", None)
.await
.unwrap();
assert_byte_identical_and_schema_valid(&a, &b);
}
#[tokio::test]
async fn branch_protection_fixture_roundtrips() {
let _g = set_fixed_time_for_tests("2026-05-01T00:00:00Z");
let fx = fixture_root().join("github/branch-protection/main.json");
let body = read_json(&fx);
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/repos/o/r/branches/main/protection"))
.respond_with(ResponseTemplate::new(200).set_body_json(body))
.mount(&server)
.await;
let c = GhClient::with_base_uri(&server.uri(), Some("ghp_x")).unwrap();
let a = secunit_capture::github::branch_protection::capture(&c, "o", "r", "main")
.await
.unwrap();
let b = secunit_capture::github::branch_protection::capture(&c, "o", "r", "main")
.await
.unwrap();
assert_byte_identical_and_schema_valid(&a, &b);
}
#[tokio::test]
async fn org_members_fixture_roundtrips() {
let _g = set_fixed_time_for_tests("2026-05-01T00:00:00Z");
let fx = fixture_root().join("github/org-members/page-1.json");
let body = read_json(&fx);
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/orgs/acme/members"))
.and(query_param("page", "1"))
.respond_with(ResponseTemplate::new(200).set_body_json(body))
.mount(&server)
.await;
empty_page_2(&server, "/orgs/acme/members").await;
let c = GhClient::with_base_uri(&server.uri(), Some("ghp_x")).unwrap();
let a = secunit_capture::github::org_members::capture(&c, "acme")
.await
.unwrap();
let b = secunit_capture::github::org_members::capture(&c, "acme")
.await
.unwrap();
assert_byte_identical_and_schema_valid(&a, &b);
}
#[tokio::test]
async fn audit_log_fixture_roundtrips() {
let _g = set_fixed_time_for_tests("2026-05-01T00:00:00Z");
let fx = fixture_root().join("github/audit-log/page-1.json");
let body = read_json(&fx);
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/orgs/acme/audit-log"))
.and(query_param("page", "1"))
.respond_with(ResponseTemplate::new(200).set_body_json(body))
.mount(&server)
.await;
empty_page_2(&server, "/orgs/acme/audit-log").await;
let c = GhClient::with_base_uri(&server.uri(), Some("ghp_x")).unwrap();
let a = secunit_capture::github::audit_log::capture(&c, "acme", "2026-04-01T00:00:00Z")
.await
.unwrap();
let b = secunit_capture::github::audit_log::capture(&c, "acme", "2026-04-01T00:00:00Z")
.await
.unwrap();
assert_byte_identical_and_schema_valid(&a, &b);
}
#[tokio::test]
async fn codeql_alerts_fixture_roundtrips() {
let _g = set_fixed_time_for_tests("2026-05-01T00:00:00Z");
let fx = fixture_root().join("github/codeql-alerts/page-1.json");
let body = read_json(&fx);
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/repos/o/r/code-scanning/alerts"))
.and(query_param("page", "1"))
.respond_with(ResponseTemplate::new(200).set_body_json(body))
.mount(&server)
.await;
empty_page_2(&server, "/repos/o/r/code-scanning/alerts").await;
let c = GhClient::with_base_uri(&server.uri(), Some("ghp_x")).unwrap();
let a = secunit_capture::github::codeql_alerts::capture(&c, "o", "r")
.await
.unwrap();
let b = secunit_capture::github::codeql_alerts::capture(&c, "o", "r")
.await
.unwrap();
assert_byte_identical_and_schema_valid(&a, &b);
}
}