#![allow(clippy::unwrap_used, clippy::panic)]
use std::process::Command;
fn cli() -> Command {
Command::new(env!("CARGO_BIN_EXE_fraiseql-cli"))
}
fn fixture(name: &str) -> String {
format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"))
}
#[test]
fn analyze_on_valid_schema_exits_zero() {
let out = cli().args(["analyze", &fixture("empty_schema.json")]).output().unwrap();
assert!(
out.status.success(),
"analyze on valid schema must exit 0; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn analyze_on_missing_file_exits_nonzero() {
let out = cli().args(["analyze", "does_not_exist.json"]).output().unwrap();
assert!(!out.status.success(), "analyze on missing file must exit non-zero");
}
#[test]
fn analyze_on_invalid_json_exits_nonzero() {
let dir = tempfile::tempdir().unwrap();
let bad_file = dir.path().join("bad.json");
std::fs::write(&bad_file, b"not valid json at all {{{").unwrap();
let out = cli().args(["analyze", bad_file.to_str().unwrap()]).output().unwrap();
assert!(!out.status.success(), "analyze on invalid JSON must exit non-zero");
}
#[test]
fn analyze_json_output_is_valid_json() {
let out = cli()
.args(["analyze", &fixture("empty_schema.json"), "--json"])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("analyze --json output must be valid JSON: {e}\ngot: {stdout}"));
assert!(parsed.is_object(), "JSON output must be an object");
}
#[test]
fn analyze_json_output_contains_recommendations() {
let out = cli()
.args(["analyze", &fixture("empty_schema.json"), "--json"])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let data = parsed.get("data").unwrap_or(&parsed);
assert!(
data.get("recommendations").and_then(serde_json::Value::as_array).is_some(),
"JSON output must contain a `recommendations` array; got: {parsed}"
);
}
#[test]
fn analyze_recommendations_match_the_published_contract() {
let out = cli()
.args(["analyze", &fixture("empty_schema.json"), "--json"])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let data = parsed.get("data").unwrap_or(&parsed);
let recs = data["recommendations"].as_array().expect("recommendations must be an array");
assert!(!recs.is_empty(), "an analysed schema must produce findings");
for rec in recs {
for key in ["category", "severity", "message", "suggestion"] {
assert!(rec.get(key).is_some(), "recommendation missing `{key}`: {rec}");
}
}
}
#[test]
fn analyze_json_output_has_health_score() {
let out = cli()
.args(["analyze", &fixture("empty_schema.json"), "--json"])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let data = parsed.get("data").unwrap_or(&parsed);
let summary = data.get("summary").expect("JSON output must contain `summary`");
assert!(
summary.get("health_score").is_some(),
"summary must contain `health_score`; got: {summary}"
);
}
#[test]
fn analyze_reports_an_unconfigured_schema_honestly() {
let out = cli()
.args(["analyze", &fixture("empty_schema.json"), "--json"])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let data = parsed.get("data").unwrap_or(&parsed);
assert!(
stdout.contains("security"),
"the report must still cover security; output: {stdout}"
);
assert!(
!stdout.contains("Rate limiting configured and active"),
"an empty schema must not be told its rate limiting is active; output: {stdout}"
);
let score = data["summary"]["health_score"].as_u64().expect("health_score");
assert!(score < 100, "an empty schema cannot be in perfect health, got {score}");
}
#[test]
fn analyze_output_depends_on_the_schema_it_is_given() {
let empty = cli()
.args(["analyze", &fixture("empty_schema.json"), "--json"])
.output()
.unwrap();
let minimal = cli()
.args([
"analyze",
&fixture("analyze_compiled_schema.json"),
"--json",
])
.output()
.unwrap();
assert!(empty.status.success() && minimal.status.success());
let strip = |out: &std::process::Output| {
let text = String::from_utf8_lossy(&out.stdout).to_string();
let mut v: serde_json::Value = serde_json::from_str(&text).unwrap();
if let Some(data) = v.get_mut("data").and_then(serde_json::Value::as_object_mut) {
data.remove("schema_file");
}
v
};
assert_ne!(
strip(&empty),
strip(&minimal),
"analyze must report on the schema it was given, not a constant"
);
}
#[test]
fn analyze_exit_code_zero_on_success() {
let out = cli()
.args(["analyze", &fixture("analyze_compiled_schema.json")])
.output()
.unwrap();
let code = out.status.code().unwrap_or(-1);
assert_eq!(code, 0, "analyze on valid schema must exit with code 0, got {code}");
}
#[test]
fn analyze_exit_code_one_on_file_not_found() {
let out = cli().args(["analyze", "missing_file.json"]).output().unwrap();
let code = out.status.code().unwrap_or(-1);
assert_eq!(code, 1, "file-not-found must exit with code 1, got {code}");
}