use cleanlib_client::transport;
use wiremock::matchers::{method, path_regex, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn empty_audit_response() -> serde_json::Value {
serde_json::json!({ "records": [] })
}
fn audit_response_with_entry(decision: &str, ecosystem: &str) -> serde_json::Value {
serde_json::json!({
"records": [{
"request_id": "req-cleanlib-375",
"request_at": "2026-07-01T00:00:00Z",
"ecosystem": ecosystem,
"package_name": "cors",
"package_version": "2.8.5",
"policy_decision": decision,
"reasoning": "audit synthetic"
}]
})
}
#[tokio::test]
async fn cleanlib_375_since_flag_reaches_wire_as_query_param() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path_regex(r"^/v1/audit"))
.and(query_param("since", "2026-06-01T00:00:00Z"))
.respond_with(ResponseTemplate::new(200).set_body_json(empty_audit_response()))
.mount(&server)
.await;
let client = transport::Client::new(&server.uri(), Some("test-bearer".into()))
.expect("client construct");
client
.audit(Some("2026-06-01T00:00:00Z"), None, None)
.await
.expect("since-only audit must reach the mock with the query param");
}
#[tokio::test]
async fn cleanlib_375_decision_flag_reaches_wire_as_query_param() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path_regex(r"^/v1/audit"))
.and(query_param("decision", "DENY"))
.respond_with(
ResponseTemplate::new(200).set_body_json(audit_response_with_entry("DENY", "npm")),
)
.mount(&server)
.await;
let client = transport::Client::new(&server.uri(), Some("test-bearer".into()))
.expect("client construct");
let resp = client
.audit(None, Some("DENY"), None)
.await
.expect("decision-only audit must reach the mock with the query param");
assert_eq!(resp.records.len(), 1);
assert_eq!(resp.records[0].policy_decision, "DENY");
}
#[tokio::test]
async fn cleanlib_375_since_and_decision_together_both_reach_wire() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path_regex(r"^/v1/audit"))
.and(query_param("since", "2026-06-01T00:00:00Z"))
.and(query_param("decision", "WARN"))
.respond_with(
ResponseTemplate::new(200).set_body_json(audit_response_with_entry("WARN", "pypi")),
)
.mount(&server)
.await;
let client = transport::Client::new(&server.uri(), Some("test-bearer".into()))
.expect("client construct");
let resp = client
.audit(Some("2026-06-01T00:00:00Z"), Some("WARN"), None)
.await
.expect("combined since+decision audit must carry BOTH query params");
assert_eq!(resp.records.len(), 1);
assert_eq!(resp.records[0].policy_decision, "WARN");
}
#[tokio::test]
async fn cleanlib_375_all_three_filters_together_reach_wire() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path_regex(r"^/v1/audit"))
.and(query_param("since", "2026-06-01T00:00:00Z"))
.and(query_param("decision", "ALLOW"))
.and(query_param("ecosystem", "npm"))
.respond_with(
ResponseTemplate::new(200).set_body_json(audit_response_with_entry("ALLOW", "npm")),
)
.mount(&server)
.await;
let client = transport::Client::new(&server.uri(), Some("test-bearer".into()))
.expect("client construct");
let resp = client
.audit(Some("2026-06-01T00:00:00Z"), Some("ALLOW"), Some("npm"))
.await
.expect("since+decision+ecosystem audit must carry ALL query params");
assert_eq!(resp.records[0].ecosystem, "npm");
}
#[tokio::test]
async fn cleanlib_375_none_filters_omit_query_params() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path_regex(r"^/v1/audit"))
.and(query_param("since", "any-value"))
.respond_with(ResponseTemplate::new(500).set_body_string("must not fire"))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path_regex(r"^/v1/audit"))
.respond_with(ResponseTemplate::new(200).set_body_json(empty_audit_response()))
.mount(&server)
.await;
let client = transport::Client::new(&server.uri(), Some("test-bearer".into()))
.expect("client construct");
client
.audit(None, None, None)
.await
.expect("None-filter audit must succeed via the fallback matcher");
}
fn cleanlib_bin() -> std::path::PathBuf {
std::path::PathBuf::from(std::env!("CARGO_BIN_EXE_cleanlib"))
}
#[tokio::test]
async fn cleanlib_375_binary_threads_since_and_decision_to_query_params() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path_regex(r"^/v1/audit"))
.and(query_param("since", "2026-06-01T00:00:00Z"))
.and(query_param("decision", "DENY"))
.respond_with(
ResponseTemplate::new(200).set_body_json(audit_response_with_entry("DENY", "npm")),
)
.mount(&server)
.await;
let dir = tempfile::tempdir().expect("tempdir");
let out = std::process::Command::new(cleanlib_bin())
.env("CLEANLIBRARY_ENDPOINT", server.uri())
.env("CLEANLIBRARY_API_KEY", "test-bearer")
.env("HOME", dir.path())
.args([
"audit",
"--since",
"2026-06-01T00:00:00Z",
"--decision",
"DENY",
])
.output()
.expect("invoke cleanlib binary");
assert!(
out.status.success(),
"audit --since --decision must exit 0; stderr: {}\nstdout: {}",
String::from_utf8_lossy(&out.stderr),
String::from_utf8_lossy(&out.stdout),
);
}