Files
swagger-cli/tests/golden_test.rs

322 lines
11 KiB
Rust

mod helpers;
use serde_json::Value;
/// Recursively mask dynamic fields (timestamps, durations, versions) for stable comparison.
fn normalize_value(value: &mut Value) {
match value {
Value::Object(map) => {
for (key, val) in map.iter_mut() {
match key.as_str() {
"duration_ms" if val.is_number() => *val = Value::Number(0.into()),
"tool_version" if val.is_string() => *val = Value::String("MASKED".into()),
"cached_at" | "fetched_at" | "last_accessed" if val.is_string() => {
*val = Value::String("MASKED_TIMESTAMP".into())
}
_ => normalize_value(val),
}
}
}
Value::Array(arr) => {
for item in arr.iter_mut() {
normalize_value(item);
}
}
_ => {}
}
}
/// Normalize robot JSON for golden comparison: mask all dynamic fields.
fn normalize_robot(mut json: Value) -> Value {
normalize_value(&mut json);
json
}
/// Assert that a robot success envelope has the correct structural invariants.
fn assert_robot_success_structure(json: &Value, expected_command: &str) {
// "ok" must be bool true
assert_eq!(
json.get("ok").and_then(|v| v.as_bool()),
Some(true),
"ok field must be true"
);
// "data" must be an object
assert!(
json.get("data").is_some_and(|v| v.is_object()),
"data field must be an object"
);
// "meta" must be an object with required fields
let meta = json.get("meta").expect("meta field is required");
assert!(meta.is_object(), "meta must be an object");
// meta.schema_version must be number == 1
let sv = meta
.get("schema_version")
.expect("meta.schema_version required");
assert!(sv.is_number(), "meta.schema_version must be a number");
assert_eq!(sv.as_u64(), Some(1), "meta.schema_version must equal 1");
// meta.tool_version must be a string
let tv = meta
.get("tool_version")
.expect("meta.tool_version required");
assert!(tv.is_string(), "meta.tool_version must be a string");
// meta.command must be a string matching expected
let cmd = meta.get("command").expect("meta.command required");
assert!(cmd.is_string(), "meta.command must be a string");
assert_eq!(
cmd.as_str().unwrap(),
expected_command,
"meta.command must match the command run"
);
// meta.duration_ms must be a number
let dur = meta.get("duration_ms").expect("meta.duration_ms required");
assert!(dur.is_number(), "meta.duration_ms must be a number");
}
/// Assert that a robot error envelope has the correct structural invariants.
fn assert_robot_error_structure(json: &Value) {
// "ok" must be bool false
assert_eq!(
json.get("ok").and_then(|v| v.as_bool()),
Some(false),
"ok field must be false on error"
);
// "data" should be absent or null
let data = json.get("data");
assert!(
data.is_none() || data.unwrap().is_null(),
"data field must be null or absent on error"
);
// "error" must be an object with code and message
let error = json.get("error").expect("error field is required");
assert!(error.is_object(), "error must be an object");
assert!(
error.get("code").is_some_and(|v| v.is_string()),
"error.code must be a string"
);
assert!(
error.get("message").is_some_and(|v| v.is_string()),
"error.message must be a string"
);
// "meta" structural invariants (same as success)
let meta = json.get("meta").expect("meta field is required");
assert!(meta.is_object(), "meta must be an object");
assert!(
meta.get("schema_version").is_some_and(|v| v.is_number()),
"meta.schema_version must be a number"
);
assert!(
meta.get("tool_version").is_some_and(|v| v.is_string()),
"meta.tool_version must be a string"
);
assert!(
meta.get("command").is_some_and(|v| v.is_string()),
"meta.command must be a string"
);
assert!(
meta.get("duration_ms").is_some_and(|v| v.is_number()),
"meta.duration_ms must be a number"
);
}
/// Load a golden snapshot file, parse as JSON.
fn load_golden(name: &str) -> Value {
let path = helpers::fixture_path(&format!("golden/{name}"));
let content = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read golden file {}: {e}", path.display()));
serde_json::from_str(&content)
.unwrap_or_else(|e| panic!("failed to parse golden file {}: {e}", path.display()))
}
/// Write a golden snapshot file if it does not exist (first-run bootstrap).
fn write_golden_if_missing(name: &str, value: &Value) {
let path = helpers::fixture_path(&format!("golden/{name}"));
if !path.exists() {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("failed to create golden dir");
}
let pretty =
serde_json::to_string_pretty(value).expect("failed to serialize golden snapshot");
std::fs::write(&path, pretty).expect("failed to write golden snapshot");
}
}
// ---------------------------------------------------------------------------
// Golden structure tests
// ---------------------------------------------------------------------------
#[test]
fn test_golden_list_robot_structure() {
let env = helpers::TestEnv::new();
helpers::fetch_fixture(&env, "petstore.json", "petstore");
let a = helpers::run_cmd(&env, &["list", "petstore", "--robot"]).success();
let json = helpers::parse_robot_json(&a.get_output().stdout);
assert_robot_success_structure(&json, "list");
// Snapshot comparison
let normalized = normalize_robot(json);
write_golden_if_missing("list.json", &normalized);
let golden = load_golden("list.json");
assert_eq!(
normalized, golden,
"list robot output does not match golden snapshot"
);
}
#[test]
fn test_golden_show_robot_structure() {
let env = helpers::TestEnv::new();
helpers::fetch_fixture(&env, "petstore.json", "petstore");
let a = helpers::run_cmd(
&env,
&["show", "petstore", "/pets", "--method", "GET", "--robot"],
)
.success();
let json = helpers::parse_robot_json(&a.get_output().stdout);
assert_robot_success_structure(&json, "show");
let normalized = normalize_robot(json);
write_golden_if_missing("show.json", &normalized);
let golden = load_golden("show.json");
assert_eq!(
normalized, golden,
"show robot output does not match golden snapshot"
);
}
#[test]
fn test_golden_search_robot_structure() {
let env = helpers::TestEnv::new();
helpers::fetch_fixture(&env, "petstore.json", "petstore");
let a = helpers::run_cmd(&env, &["search", "petstore", "pet", "--robot"]).success();
let json = helpers::parse_robot_json(&a.get_output().stdout);
assert_robot_success_structure(&json, "search");
let normalized = normalize_robot(json);
write_golden_if_missing("search.json", &normalized);
let golden = load_golden("search.json");
assert_eq!(
normalized, golden,
"search robot output does not match golden snapshot"
);
}
#[test]
fn test_golden_schemas_list_robot_structure() {
let env = helpers::TestEnv::new();
helpers::fetch_fixture(&env, "petstore.json", "petstore");
let a = helpers::run_cmd(&env, &["schemas", "petstore", "--robot"]).success();
let json = helpers::parse_robot_json(&a.get_output().stdout);
assert_robot_success_structure(&json, "schemas");
let normalized = normalize_robot(json);
write_golden_if_missing("schemas_list.json", &normalized);
let golden = load_golden("schemas_list.json");
assert_eq!(
normalized, golden,
"schemas list robot output does not match golden snapshot"
);
}
#[test]
fn test_golden_schemas_show_robot_structure() {
let env = helpers::TestEnv::new();
helpers::fetch_fixture(&env, "petstore.json", "petstore");
let a = helpers::run_cmd(&env, &["schemas", "petstore", "--show", "Pet", "--robot"]).success();
let json = helpers::parse_robot_json(&a.get_output().stdout);
assert_robot_success_structure(&json, "schemas");
let normalized = normalize_robot(json);
write_golden_if_missing("schemas_show.json", &normalized);
let golden = load_golden("schemas_show.json");
assert_eq!(
normalized, golden,
"schemas show robot output does not match golden snapshot"
);
}
#[test]
fn test_golden_tags_robot_structure() {
let env = helpers::TestEnv::new();
helpers::fetch_fixture(&env, "petstore.json", "petstore");
let a = helpers::run_cmd(&env, &["tags", "petstore", "--robot"]).success();
let json = helpers::parse_robot_json(&a.get_output().stdout);
assert_robot_success_structure(&json, "tags");
let normalized = normalize_robot(json);
write_golden_if_missing("tags.json", &normalized);
let golden = load_golden("tags.json");
assert_eq!(
normalized, golden,
"tags robot output does not match golden snapshot"
);
}
#[test]
fn test_golden_aliases_robot_structure() {
let env = helpers::TestEnv::new();
helpers::fetch_fixture(&env, "petstore.json", "petstore");
let a = helpers::run_cmd(&env, &["aliases", "--list", "--robot"]).success();
let json = helpers::parse_robot_json(&a.get_output().stdout);
assert_robot_success_structure(&json, "aliases");
// Aliases output has dynamic timestamps so we only check structure,
// not golden snapshot equality. Normalize and check key shape.
let data = json.get("data").unwrap();
assert!(data.get("aliases").is_some_and(|v| v.is_array()));
assert!(data.get("count").is_some_and(|v| v.is_number()));
}
#[test]
fn test_golden_error_structure() {
let env = helpers::TestEnv::new();
// Do NOT fetch -- alias does not exist
let a = helpers::run_cmd(&env, &["list", "nonexistent", "--robot"]);
// Command should fail with non-zero exit
let a = a.failure();
// Error JSON goes to stderr
let stderr = std::str::from_utf8(&a.get_output().stderr).expect("stderr not UTF-8");
let json: Value = serde_json::from_str(stderr.trim()).expect("stderr is not valid robot JSON");
assert_robot_error_structure(&json);
// Verify error code
assert_eq!(
json["error"]["code"].as_str(),
Some("ALIAS_NOT_FOUND"),
"error code should be ALIAS_NOT_FOUND"
);
// Verify meta.command
assert_eq!(
json["meta"]["command"].as_str(),
Some("list"),
"meta.command should be list"
);
}