use serde_json::{json, Value};
use std::process::Output;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn run_json(server_uri: &str, args: &[&str]) -> Output {
std::process::Command::new(env!("CARGO_BIN_EXE_anvil"))
.arg("--json")
.args(args)
.env("ANVIL_SERVER_URL", server_uri)
.env("ANVIL_TOKEN", "test-token")
.output()
.expect("run anvil")
}
fn stdout_json(out: &Output) -> Value {
serde_json::from_slice(&out.stdout).unwrap_or_else(|e| {
panic!(
"stdout was not valid JSON: {e}\n--- stdout ---\n{}\n--- stderr ---\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
)
})
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn list_requirements_bare_read() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/test-org/test-repo/requirements"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"requirements": [{"requirement_id": "REQ-A-001", "title": "T"}]
})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&["requirement", "list", "--repo", "test-org/test-repo"],
);
assert!(out.status.success());
let v = stdout_json(&out);
assert!(v["requirements"].is_array(), "got: {v}");
assert!(v["items"].is_null(), "should not be wrapped in items: {v}");
assert_eq!(v["requirements"][0]["requirement_id"], "REQ-A-001");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn list_requirements_empty_is_clean_json() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/test-org/test-repo/requirements"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"requirements": []})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&["requirement", "list", "--repo", "test-org/test-repo"],
);
assert!(out.status.success());
let v = stdout_json(&out);
assert_eq!(
v["requirements"].as_array().map(|a| a.len()),
Some(0),
"got: {v}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn list_standards_bare_read() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/test-org/standards"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"standards": [{"requirement_id": "STD-GDPR-017", "title": "Erasure"}]
})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"list",
"--kind",
"standard",
"--organization",
"test-org",
],
);
assert!(out.status.success());
let v = stdout_json(&out);
assert_eq!(
v["standards"][0]["requirement_id"], "STD-GDPR-017",
"got: {v}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn list_all_merges_both_halves() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/test-org/test-repo/requirements"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"requirements": [{"requirement_id": "REQ-A-001", "title": "R"}]
})))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/api/v1/test-org/standards"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"standards": [{"requirement_id": "STD-B-002", "title": "S"}]
})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"list",
"--kind",
"all",
"--repo",
"test-org/test-repo",
"--organization",
"test-org",
],
);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = stdout_json(&out);
assert!(v["requirements"].is_array(), "got: {v}");
assert!(v["standards"].is_array(), "got: {v}");
assert_eq!(v["requirements"][0]["requirement_id"], "REQ-A-001");
assert_eq!(v["standards"][0]["requirement_id"], "STD-B-002");
assert!(v["requirements"][0].is_object() && v["requirements"]["requirements"].is_null());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn list_all_missing_org_errors() {
let server = MockServer::start().await;
let out = run_json(
&server.uri(),
&[
"requirement",
"list",
"--kind",
"all",
"--repo",
"test-org/test-repo",
],
);
assert!(!out.status.success(), "expected nonzero exit");
let v = stdout_json(&out);
assert_eq!(v["ok"], false, "got: {v}");
assert!(v["error"].is_string(), "got: {v}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn view_requirement_bare_read() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/test-org/test-repo/requirements/REQ-A-001"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"id": "uuid-1", "requirement_id": "REQ-A-001", "title": "T",
"category": "security", "status": "active", "version": "3"
})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"view",
"REQ-A-001",
"--repo",
"test-org/test-repo",
],
);
assert!(out.status.success());
let v = stdout_json(&out);
assert_eq!(v["requirement_id"], "REQ-A-001", "got: {v}");
assert!(
v["ok"].is_null(),
"read must be bare, not an ok-envelope: {v}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn view_requirement_echoes_unexpected_shape() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/test-org/test-repo/requirements/REQ-A-002"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"id": "uuid-2", "requirement_id": "REQ-A-002", "title": "T",
"version": 2
})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"view",
"REQ-A-002",
"--repo",
"test-org/test-repo",
],
);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = stdout_json(&out);
assert_eq!(v["requirement_id"], "REQ-A-002", "got: {v}");
assert_eq!(v["version"], 2, "numeric version echoed verbatim: {v}");
assert!(
v["ok"].is_null(),
"read must be bare, not an ok-envelope: {v}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn view_standard_bare_read_defensive() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/test-org/standards/STD-GDPR-017"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"id": "uuid-s", "requirement_id": "STD-GDPR-017", "title": "Erasure",
"mandatory": true, "version": 7
})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"view",
"STD-GDPR-017",
"--organization",
"test-org",
],
);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = stdout_json(&out);
assert_eq!(v["requirement_id"], "STD-GDPR-017", "got: {v}");
assert_eq!(v["version"], 7, "numeric version echoed verbatim: {v}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn view_unknown_prefix_errors() {
let server = MockServer::start().await;
let out = run_json(
&server.uri(),
&[
"requirement",
"view",
"FOO-1",
"--repo",
"test-org/test-repo",
],
);
assert!(!out.status.success());
let v = stdout_json(&out);
assert_eq!(v["ok"], false, "got: {v}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn view_requirement_404_error_envelope() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/test-org/test-repo/requirements/REQ-A-404"))
.respond_with(ResponseTemplate::new(404).set_body_json(json!({"error": "not found"})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"view",
"REQ-A-404",
"--repo",
"test-org/test-repo",
],
);
assert!(!out.status.success());
let v = stdout_json(&out);
assert_eq!(v["ok"], false, "got: {v}");
assert!(v["error"].is_string(), "got: {v}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn create_requirement_no_double_nest() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/api/v1/test-org/test-repo/requirements"))
.respond_with(ResponseTemplate::new(201).set_body_json(json!({
"id": "uuid-c", "requirement_id": "REQ-A-010", "title": "New"
})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"create",
"--requirement-id",
"REQ-A-010",
"--title",
"New",
"--repo",
"test-org/test-repo",
],
);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = stdout_json(&out);
assert_eq!(v["ok"], true, "got: {v}");
assert_eq!(v["requirement"]["requirement_id"], "REQ-A-010", "got: {v}");
assert!(
v["requirement"]["requirement"].is_null(),
"double nest: {v}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn create_standard_no_double_nest() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/api/v1/test-org/standards"))
.respond_with(ResponseTemplate::new(201).set_body_json(json!({
"id": "uuid-s2", "requirement_id": "STD-X-001", "title": "Std"
})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"create",
"--requirement-id",
"STD-X-001",
"--title",
"Std",
"--organization",
"test-org",
],
);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = stdout_json(&out);
assert_eq!(v["standard"]["requirement_id"], "STD-X-001", "got: {v}");
assert!(v["standard"]["standard"].is_null(), "double nest: {v}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn create_requirement_422_error_envelope() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/api/v1/test-org/test-repo/requirements"))
.respond_with(ResponseTemplate::new(422).set_body_json(json!({"error": "duplicate"})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"create",
"--requirement-id",
"REQ-A-011",
"--title",
"Dup",
"--repo",
"test-org/test-repo",
],
);
assert!(!out.status.success(), "must not mask a 422 as success");
let v = stdout_json(&out);
assert_eq!(v["ok"], false, "got: {v}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn update_requirement_no_double_nest() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/api/v1/test-org/test-repo/requirements/REQ-A-010"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"id": "uuid-c", "requirement_id": "REQ-A-010", "title": "Renamed"
})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"update",
"REQ-A-010",
"--title",
"Renamed",
"--repo",
"test-org/test-repo",
],
);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = stdout_json(&out);
assert_eq!(v["requirement"]["title"], "Renamed", "got: {v}");
assert!(
v["requirement"]["requirement"].is_null(),
"double nest: {v}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn update_standard_no_double_nest() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path("/api/v1/test-org/standards/STD-X-001"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"id": "uuid-s2", "requirement_id": "STD-X-001", "title": "Std2"
})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"update",
"STD-X-001",
"--title",
"Std2",
"--organization",
"test-org",
],
);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = stdout_json(&out);
assert_eq!(v["standard"]["requirement_id"], "STD-X-001", "got: {v}");
assert!(v["standard"]["standard"].is_null(), "double nest: {v}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn delete_requirement_envelope() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/api/v1/test-org/test-repo/requirements/REQ-A-010"))
.respond_with(ResponseTemplate::new(204))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"delete",
"REQ-A-010",
"--repo",
"test-org/test-repo",
],
);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = stdout_json(&out);
assert_eq!(v["ok"], true, "got: {v}");
assert_eq!(v["deleted"], "REQ-A-010", "got: {v}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn delete_standard_envelope() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/api/v1/test-org/standards/STD-X-001"))
.respond_with(ResponseTemplate::new(204))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"delete",
"STD-X-001",
"--organization",
"test-org",
],
);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = stdout_json(&out);
assert_eq!(v["deleted"], "STD-X-001", "got: {v}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn link_test_no_double_nest() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path(
"/api/v1/test-org/test-repo/requirements/REQ-A-010/links",
))
.respond_with(ResponseTemplate::new(201).set_body_json(json!({
"test_name": "logintest", "test_link_id": "tl-1"
})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"link",
"REQ-A-010",
"--test",
"logintest",
"--repo",
"test-org/test-repo",
],
);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = stdout_json(&out);
assert_eq!(v["ok"], true, "got: {v}");
assert_eq!(v["link"]["test_name"], "logintest", "got: {v}");
assert!(v["link"]["link"].is_null(), "double nest: {v}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn link_standard_id_rejected() {
let server = MockServer::start().await;
let out = run_json(
&server.uri(),
&[
"requirement",
"link",
"STD-X-001",
"--test",
"t",
"--repo",
"test-org/test-repo",
],
);
assert!(!out.status.success());
let v = stdout_json(&out);
assert_eq!(v["ok"], false, "got: {v}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn unlink_test_envelope() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path(
"/api/v1/test-org/test-repo/requirements/REQ-A-010/links/logintest",
))
.respond_with(ResponseTemplate::new(204))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"unlink",
"REQ-A-010",
"--test",
"logintest",
"--repo",
"test-org/test-repo",
],
);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = stdout_json(&out);
assert_eq!(v["unlinked"]["requirement"], "REQ-A-010", "got: {v}");
assert_eq!(v["unlinked"]["test"], "logintest", "got: {v}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn applicability_list_bare_read() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/test-org/standards/STD-X-001/applicabilities"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"repositories": [{"slug": "app", "name": "App", "visibility": "private"}]
})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"applicability",
"list",
"STD-X-001",
"--organization",
"test-org",
],
);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = stdout_json(&out);
assert_eq!(v["repositories"][0]["slug"], "app", "got: {v}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn applicability_list_empty_clean() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/test-org/standards/STD-X-001/applicabilities"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"repositories": []})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"applicability",
"list",
"STD-X-001",
"--organization",
"test-org",
],
);
assert!(out.status.success());
let v = stdout_json(&out);
assert_eq!(
v["repositories"].as_array().map(|a| a.len()),
Some(0),
"got: {v}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn applicability_add_envelope() {
let server = MockServer::start().await;
Mock::given(method("PUT"))
.and(path(
"/api/v1/test-org/standards/STD-X-001/applicabilities/app",
))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"applicability",
"add",
"STD-X-001",
"--organization",
"test-org",
"--repo",
"app",
],
);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = stdout_json(&out);
assert_eq!(v["applicability"]["opted_in"], true, "got: {v}");
assert_eq!(v["applicability"]["repo"], "app", "got: {v}");
assert!(
v["applicability"]["applicability"].is_null(),
"double nest: {v}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn applicability_remove_envelope() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path(
"/api/v1/test-org/standards/STD-X-001/applicabilities/app",
))
.respond_with(ResponseTemplate::new(204))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"applicability",
"remove",
"STD-X-001",
"--organization",
"test-org",
"--repo",
"app",
],
);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = stdout_json(&out);
assert_eq!(v["applicability"]["opted_in"], false, "got: {v}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn matrix_requirements_bare_read() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/test-org/test-repo/requirements/matrix"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"matrix": [{"requirement": {"requirement_id": "REQ-A-001", "title": "T"},
"coverage_status": "covered", "tests": []}]
})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&["requirement", "matrix", "--repo", "test-org/test-repo"],
);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = stdout_json(&out);
assert_eq!(
v["matrix"][0]["requirement"]["requirement_id"], "REQ-A-001",
"got: {v}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn matrix_standards_empty_clean() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/test-org/standards/matrix"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"matrix": []})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"matrix",
"--kind",
"standard",
"--organization",
"test-org",
],
);
assert!(out.status.success());
let v = stdout_json(&out);
assert_eq!(v["matrix"].as_array().map(|a| a.len()), Some(0), "got: {v}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn status_gate_fails_but_emits_json() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/test-org/test-repo/requirements/matrix"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"matrix": [
{"requirement": {"requirement_id": "REQ-A-001", "title": "Covered"},
"coverage_status": "covered"},
{"requirement": {"requirement_id": "REQ-A-002", "title": "Uncovered"},
"coverage_status": "uncovered"}
]
})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&["requirement", "status", "--repo", "test-org/test-repo"],
);
assert!(!out.status.success(), "uncovered must exit nonzero");
let v = stdout_json(&out);
assert_eq!(v["passed"], false, "got: {v}");
assert_eq!(v["counts"]["uncovered"], 1, "got: {v}");
assert_eq!(v["failing"][0]["requirement_id"], "REQ-A-002", "got: {v}");
assert!(
v["ok"].is_null(),
"gate payload must not be an ok-envelope: {v}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn status_gate_passes() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/test-org/test-repo/requirements/matrix"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"matrix": [{"requirement": {"requirement_id": "REQ-A-001", "title": "C"},
"coverage_status": "covered"}]
})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&["requirement", "status", "--repo", "test-org/test-repo"],
);
assert!(out.status.success());
let v = stdout_json(&out);
assert_eq!(v["passed"], true, "got: {v}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn status_strict_partial_fails_but_emits_json() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/test-org/test-repo/requirements/matrix"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"matrix": [{"requirement": {"requirement_id": "REQ-A-003", "title": "P"},
"coverage_status": "partial"}]
})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"status",
"--strict",
"--repo",
"test-org/test-repo",
],
);
assert!(!out.status.success(), "strict + partial must exit nonzero");
let v = stdout_json(&out);
assert_eq!(v["passed"], false, "got: {v}");
assert_eq!(v["counts"]["partial"], 1, "got: {v}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn status_empty_matrix_passes_clean() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/test-org/test-repo/requirements/matrix"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"matrix": []})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&["requirement", "status", "--repo", "test-org/test-repo"],
);
assert!(out.status.success());
let v = stdout_json(&out);
assert_eq!(v["passed"], true, "got: {v}");
assert_eq!(v["counts"]["total"], 0, "got: {v}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn strict_standards_gate_fails_but_emits_json() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/test-org/standards/strict"))
.respond_with(ResponseTemplate::new(422).set_body_json(json!({
"status": "uncovered",
"uncovered": [
{"standard": {"requirement_id": "STD-X-001", "title": "S"},
"repo": {"slug": "app"}, "status": "uncovered"}
]
})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"status",
"--strict-standards",
"--organization",
"test-org",
],
);
assert!(
!out.status.success(),
"uncovered mandatory standards must exit nonzero"
);
let v = stdout_json(&out);
assert_eq!(v["passed"], false, "got: {v}");
assert_eq!(
v["uncovered"][0]["standard"]["requirement_id"], "STD-X-001",
"got: {v}"
);
assert!(
v["ok"].is_null(),
"gate payload must not be an ok-envelope: {v}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn strict_standards_gate_passes() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/test-org/standards/strict"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"status": "ok"})))
.mount(&server)
.await;
let out = run_json(
&server.uri(),
&[
"requirement",
"status",
"--strict-standards",
"--organization",
"test-org",
],
);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = stdout_json(&out);
assert_eq!(v["passed"], true, "got: {v}");
assert_eq!(
v["uncovered"].as_array().map(|a| a.len()),
Some(0),
"got: {v}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn import_apply_bare_read() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/api/v1/test-org/test-repo/requirements/import"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"applied": {"created": 2, "updated": 1, "unchanged": 0}
})))
.mount(&server)
.await;
let dir = std::env::var("TMPDIR").unwrap_or_else(|_| "/tmp".into());
let f = format!("{dir}/adversarial_import_{}.yml", std::process::id());
std::fs::write(&f, "- requirement_id: REQ-A-001\n title: T\n").unwrap();
let out = run_json(
&server.uri(),
&["requirement", "import", &f, "--repo", "test-org/test-repo"],
);
let _ = std::fs::remove_file(&f);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = stdout_json(&out);
assert_eq!(v["applied"]["created"], 2, "got: {v}");
assert!(v["ok"].is_null(), "import read must be bare: {v}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn import_dry_run_bare_read() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/api/v1/test-org/test-repo/requirements/import"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"preview": {"create": [{"requirement_id": "REQ-A-002"}],
"update": [], "unchanged": [], "errors": []}
})))
.mount(&server)
.await;
let dir = std::env::var("TMPDIR").unwrap_or_else(|_| "/tmp".into());
let f = format!("{dir}/adversarial_import_dry_{}.yml", std::process::id());
std::fs::write(&f, "- requirement_id: REQ-A-002\n title: T\n").unwrap();
let out = run_json(
&server.uri(),
&[
"requirement",
"import",
&f,
"--dry-run",
"--repo",
"test-org/test-repo",
],
);
let _ = std::fs::remove_file(&f);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = stdout_json(&out);
assert_eq!(
v["preview"]["create"][0]["requirement_id"], "REQ-A-002",
"got: {v}"
);
}