use serde_json::Value;
use crate::{helpers::spawn_http_server, support::TestWorkspace};
#[test]
fn run_outputs_json_report() {
let server_url = spawn_http_server(
200,
"OK",
"application/json",
r#"{"ok":true,"service":"hen"}"#,
);
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
&format!(
r#"JSON Fixture
Exercises machine-readable output.
---
Fetch fixture
GET {server_url}
^ & body.ok == true
[ true == false ] ^ & body.service == 'hen'
"#
),
);
let output = workspace.run_hen(["run", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["plan"], serde_json::json!([0]));
assert_eq!(parsed["selectedRequests"], serde_json::json!([0]));
assert_eq!(parsed["records"][0]["status"], 200);
assert_eq!(
parsed["records"][0]["assertions"],
serde_json::json!([
{
"assertion": "^ & body.ok == true",
"status": "passed",
"message": null,
"mismatch": null,
},
{
"assertion": "[ true == false ] ^ & body.service == 'hen'",
"status": "skipped",
"message": "guard evaluated to false",
"mismatch": null,
}
])
);
assert!(parsed["records"][0]["body"]
.as_str()
.expect("body should be serialized as a string")
.contains("\"service\":\"hen\""));
assert_eq!(parsed["records"][0]["bodyChars"], 27);
assert_eq!(parsed["records"][0]["bodyTruncated"], false);
assert_eq!(parsed["interrupted"], false);
assert_eq!(parsed["interruptSignal"], Value::Null);
assert_eq!(parsed["failures"], serde_json::json!([]));
}
#[test]
fn run_outputs_json_report_includes_dns_timing_for_hostname_requests() {
let server_url = spawn_http_server(200, "OK", "application/json", r#"{"ok":true}"#)
.replacen("127.0.0.1", "localhost", 1);
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
&format!(
r#"DNS Timing Fixture
Exercises hostname-based timing output.
---
Fetch fixture
GET {server_url}
^ & body.ok == true
"#
),
);
let output = workspace.run_hen(["run", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["records"][0]["timing"]["phases"][0]["name"], "dns");
assert!(parsed["records"][0]["timing"]["phases"][0]["durationMs"].as_u64().is_some());
assert_eq!(parsed["records"][0]["timing"]["phases"][1]["name"], "responseStart");
assert_eq!(parsed["records"][0]["timing"]["phases"][2]["name"], "bodyRead");
}
#[test]
fn run_outputs_json_report_for_failed_assertions() {
let server_url = spawn_http_server(
200,
"OK",
"application/json",
r#"{"ok":false,"service":"hen"}"#,
);
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
&format!(
r#"Failed Assertions Fixture
Exercises assertion-level failure reporting.
---
Fetch fixture
GET {server_url}
^ & body.service == 'hen'
[ true == false ] ^ & body.service == 'hen'
^ & body.ok == true
"#
),
);
let output = workspace.run_hen(["run", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["executionFailed"], true);
assert_eq!(parsed["interrupted"], false);
assert_eq!(parsed["interruptSignal"], Value::Null);
assert_eq!(parsed["records"], serde_json::json!([]));
assert_eq!(parsed["failures"][0]["protocol"], "http");
assert!(parsed["failures"][0]["startedAtUnixMs"].as_u64().is_some());
assert!(parsed["failures"][0]["durationMs"].as_u64().is_some());
assert!(parsed["failures"][0]["timing"]["totalMs"].as_u64().is_some());
assert_eq!(parsed["failures"][0]["timing"]["phases"][0]["name"], "responseStart");
assert!(parsed["failures"][0]["timing"]["phases"][0]["durationMs"].as_u64().is_some());
assert_eq!(parsed["failures"][0]["timing"]["phases"][1]["name"], "bodyRead");
assert!(parsed["failures"][0]["timing"]["phases"][1]["durationMs"].as_u64().is_some());
assert_eq!(parsed["failures"][0]["transcripts"][0]["direction"], "outgoing");
assert_eq!(parsed["failures"][0]["transcripts"][0]["label"], "http.request");
assert_eq!(parsed["failures"][0]["transcripts"][1]["direction"], "incoming");
assert_eq!(parsed["failures"][0]["transcripts"][1]["label"], "http.response");
assert!(parsed["failures"][0]["transcripts"][1]["body"]
.as_str()
.expect("failure transcript body should be serialized as a string")
.contains("\"ok\":false"));
assert_eq!(parsed["failures"][0]["retainedArtifacts"], serde_json::json!([]));
assert_eq!(
parsed["failures"][0]["assertions"],
serde_json::json!([
{
"assertion": "^ & body.service == 'hen'",
"status": "passed",
"message": null,
"mismatch": null,
},
{
"assertion": "[ true == false ] ^ & body.service == 'hen'",
"status": "skipped",
"message": "guard evaluated to false",
"mismatch": null,
},
{
"assertion": "^ & body.ok == true",
"status": "failed",
"message": "Assertion failed: ^ & body.ok == true (actual path: body.ok, actual type: boolean, actual value: false, operator: ==, compared type: boolean, compared value: true)",
"mismatch": {
"kind": "comparison",
"reason": "value_mismatch",
"target": null,
"path": "body.ok",
"actualPath": "body.ok",
"comparedPath": null,
"operator": "==",
"actual": {
"type": "boolean",
"value": false
},
"expected": {
"type": "boolean",
"value": true
}
},
}
])
);
}
#[test]
fn run_outputs_json_report_for_structural_match_failures() {
let server_url = spawn_http_server(
200,
"OK",
"application/json",
r#"{"user":{"profile":{"name":"Bob","active":true}}}"#,
);
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
&format!(
r#"Structural Failure Fixture
Exercises nested structural mismatch reporting.
---
Fetch fixture
GET {server_url}
^ & body.user ~= {{"profile":{{"name":"Alice"}}}}
"#
),
);
let output = workspace.run_hen(["run", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
let message = parsed["failures"][0]["assertions"][0]["message"]
.as_str()
.expect("message should be serialized as a string");
let mismatch = &parsed["failures"][0]["assertions"][0]["mismatch"];
assert!(message.contains("mismatch path: body.user.profile.name"));
assert!(message.contains("mismatch actual value: \"Bob\""));
assert!(message.contains("mismatch expected value: \"Alice\""));
assert_eq!(mismatch["kind"], "match");
assert_eq!(mismatch["reason"], "value_mismatch");
assert_eq!(mismatch["target"], Value::Null);
assert_eq!(mismatch["path"], "body.user.profile.name");
assert_eq!(mismatch["actualPath"], "body.user");
assert_eq!(mismatch["operator"], "~=");
assert_eq!(mismatch["actual"]["type"], "string");
assert_eq!(mismatch["actual"]["value"], "Bob");
assert_eq!(mismatch["expected"]["type"], "string");
assert_eq!(mismatch["expected"]["value"], "Alice");
}
#[test]
fn run_outputs_json_report_for_filtered_selector_failures() {
let server_url = spawn_http_server(
200,
"OK",
"application/json",
r#"{"jobs":[{"recipient":"bob@example.com","status":"queued"},{"recipient":"alice@example.com","status":"succeeded"}]}"#,
);
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
&format!(
r#"Filtered Selector Failure Fixture
Exercises concrete selector-path reporting in JSON output.
---
Fetch fixture
GET {server_url}
^ & body.jobs[? recipient == "alice@example.com"].status == 'queued'
"#
),
);
let output = workspace.run_hen(["run", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
let message = parsed["failures"][0]["assertions"][0]["message"]
.as_str()
.expect("message should be serialized as a string");
let mismatch = &parsed["failures"][0]["assertions"][0]["mismatch"];
assert!(message.contains("actual path: body.jobs[1].status"));
assert_eq!(mismatch["path"], "body.jobs[1].status");
assert_eq!(mismatch["actualPath"], "body.jobs[1].status");
assert_eq!(mismatch["target"], Value::Null);
assert_eq!(mismatch["actual"]["value"], "succeeded");
assert_eq!(mismatch["expected"]["value"], "queued");
}
#[test]
fn run_outputs_json_report_for_schema_failures() {
let server_url = spawn_http_server(
200,
"OK",
"application/json",
r#"{"id":"550e8400-e29b-41d4-a716-446655440000","address":{}}"#,
);
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
&format!(
r#"schema Address {{
postalCode: string
}}
schema User {{
id: UUID
address: Address
}}
---
Schema Failure Fixture
GET {server_url}
^ & body === User
"#
),
);
let output = workspace.run_hen(["run", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
let message = parsed["failures"][0]["assertions"][0]["message"]
.as_str()
.expect("message should be serialized as a string");
let mismatch = &parsed["failures"][0]["assertions"][0]["mismatch"];
assert!(message.contains("schema target: User"));
assert!(message.contains("mismatch path: body.address.postalCode"));
assert_eq!(mismatch["kind"], "schema");
assert_eq!(mismatch["reason"], "missing_required_field");
assert_eq!(mismatch["target"], "User");
assert_eq!(mismatch["path"], "body.address.postalCode");
assert_eq!(mismatch["actualPath"], "body");
assert_eq!(mismatch["operator"], "===");
assert_eq!(mismatch["actual"]["type"], "missing");
assert_eq!(mismatch["actual"]["value"], Value::Null);
assert_eq!(mismatch["expected"]["type"], "required_field");
assert_eq!(mismatch["expected"]["value"], "postalCode");
}