use super::*;
use std::{
collections::HashMap,
path::PathBuf,
time::{Duration, SystemTime},
};
use http::{Method, StatusCode};
use serde_json::{json, Value};
use crate::{
automation::{CollectionSummary, RunOutcome, VerificationResult},
parser::{SyntaxRequestSummary, SyntaxSummary},
request::{
ArtifactMetadata, AssertionOutcome, AssertionStatus, ExecutionArtifact,
ExecutionRecord, HttpArtifactMetadata, RequestExecution, RequestProtocol,
},
};
#[test]
fn verification_result_json_matches_golden_fixture() {
let result = VerificationResult {
path: Some(PathBuf::from("/workspace/collection.hen")),
summary: SyntaxSummary {
name: "Fixture Collection".to_string(),
description: "Collection used for structured output fixtures.".to_string(),
requests: vec![
SyntaxRequestSummary {
index: 0,
description: "Get one".to_string(),
method: "GET".to_string(),
url: "https://example.com/one".to_string(),
protocol: "http".to_string(),
protocol_context: None,
},
SyntaxRequestSummary {
index: 1,
description: "Get two".to_string(),
method: "POST".to_string(),
url: "https://example.com/two".to_string(),
protocol: "http".to_string(),
protocol_context: None,
},
],
},
required_inputs: vec![
crate::automation::PromptRequirement {
name: "api_token".to_string(),
default: None,
},
crate::automation::PromptRequirement {
name: "region".to_string(),
default: Some("us-east-1".to_string()),
},
],
};
assert_json_fixture(
verification_result_json(&result),
include_str!("../../tests/fixtures/golden/verify_hen_syntax.json"),
);
}
#[test]
fn run_outcome_json_matches_golden_fixture() {
let result = RunOutcome {
collection: CollectionSummary {
path: PathBuf::from("/workspace/collection.hen"),
name: "Fixture Collection".to_string(),
description: "Collection used for structured output fixtures.".to_string(),
requests: vec![crate::automation::RequestSummary {
index: 0,
description: "Get one".to_string(),
method: "GET".to_string(),
url: "https://example.com/one".to_string(),
protocol: "http".to_string(),
protocol_context: None,
dependencies: vec![],
}],
required_inputs: vec![crate::automation::PromptRequirement {
name: "api_token".to_string(),
default: None,
}],
},
plan: vec![0],
selected_requests: vec![0],
primary_target: Some(0),
records: vec![ExecutionRecord {
index: 0,
description: "Get one".to_string(),
method: Method::GET,
url: "https://example.com/one".to_string(),
execution: RequestExecution {
output: "hello world".to_string(),
export_env: HashMap::new(),
artifact: ExecutionArtifact {
protocol: RequestProtocol::Http,
status: StatusCode::OK,
headers: Default::default(),
body: "hello world".to_string(),
json: None,
metadata: ArtifactMetadata::Http(HttpArtifactMetadata {
status: StatusCode::OK,
headers: Default::default(),
}),
timing_phases: vec![],
transcripts: vec![],
retained_artifacts: vec![],
},
assertions: vec![
AssertionOutcome {
assertion: "^ & body.ok == true".to_string(),
status: AssertionStatus::Passed,
message: None,
mismatch: None,
},
AssertionOutcome {
assertion: "[ true == false ] ^ & body.service == 'hen'".to_string(),
status: AssertionStatus::Skipped,
message: Some("guard evaluated to false".to_string()),
mismatch: None,
},
],
},
started_at: SystemTime::UNIX_EPOCH + Duration::from_millis(1_704_067_200_000),
duration: Duration::from_millis(25),
}],
failures: vec![],
execution_failed: false,
interrupted: None,
};
assert_json_fixture(
run_outcome_json(
&result,
BodyReportOptions {
include_body: true,
max_body_chars: Some(5),
},
),
include_str!("../../tests/fixtures/golden/run_hen.json"),
);
}
#[test]
fn run_outcome_ndjson_emits_summary_and_record_lines() {
let result = sample_run_outcome();
let lines = run_outcome_ndjson(
&result,
BodyReportOptions {
include_body: true,
max_body_chars: None,
},
)
.lines()
.map(|line| serde_json::from_str::<Value>(line).expect("ndjson line should parse"))
.collect::<Vec<_>>();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0]["type"], "run");
assert_eq!(lines[0]["interrupted"], false);
assert_eq!(lines[0]["interruptSignal"], Value::Null);
assert_eq!(lines[1]["type"], "record");
assert_eq!(lines[1]["protocol"], "http");
assert_eq!(lines[1]["protocolContext"], Value::Null);
assert_eq!(lines[1]["status"], 200);
assert_eq!(lines[1]["assertions"][0]["status"], "passed");
assert_eq!(lines[1]["assertions"][1]["status"], "skipped");
}
#[test]
fn run_record_json_includes_graphql_protocol_context() {
let record = ExecutionRecord {
index: 0,
description: "Get user via GraphQL".to_string(),
method: Method::POST,
url: "https://example.com/graphql".to_string(),
execution: RequestExecution {
output: r#"{"data":{"user":{"id":"123"}},"errors":null}"#.to_string(),
export_env: HashMap::new(),
artifact: ExecutionArtifact {
protocol: RequestProtocol::Graphql,
status: StatusCode::OK,
headers: Default::default(),
body: r#"{"data":{"user":{"id":"123"}},"errors":null}"#.to_string(),
json: Some(serde_json::json!({
"data": { "user": { "id": "123" } },
"errors": null
})),
metadata: ArtifactMetadata::Http(HttpArtifactMetadata {
status: StatusCode::OK,
headers: Default::default(),
}),
timing_phases: vec![],
transcripts: vec![crate::request::ArtifactTranscript {
direction: crate::request::ArtifactTranscriptDirection::Outgoing,
label: "graphql.request".to_string(),
body: String::new(),
attributes: HashMap::from([
("operationName".to_string(), "GetUser".to_string()),
("variables".to_string(), r#"{"id":"123"}"#.to_string()),
]),
}],
retained_artifacts: vec![],
},
assertions: vec![],
},
started_at: SystemTime::UNIX_EPOCH,
duration: Duration::from_millis(10),
};
let json = run_record_json(&record, BodyReportOptions::default());
assert_eq!(json["protocol"], "graphql");
assert_eq!(json["protocolContext"]["operationName"], "GetUser");
assert_eq!(json["protocolContext"]["variables"]["id"], "123");
assert_eq!(json["timing"]["totalMs"], 10);
assert_eq!(json["timing"]["phases"], json!([]));
assert_eq!(json["transcripts"][0]["direction"], "outgoing");
assert_eq!(json["transcripts"][0]["label"], "graphql.request");
assert_eq!(json["transcripts"][0]["attributes"]["operationName"], "GetUser");
assert_eq!(json["retainedArtifacts"], json!([]));
}
#[test]
fn run_record_json_applies_body_options_to_artifacts() {
let record = ExecutionRecord {
index: 1,
description: "Artifact export".to_string(),
method: Method::POST,
url: "https://example.com/artifacts".to_string(),
execution: RequestExecution {
output: "response body".to_string(),
export_env: HashMap::new(),
artifact: ExecutionArtifact {
protocol: RequestProtocol::Http,
status: StatusCode::OK,
headers: Default::default(),
body: "response body".to_string(),
json: None,
metadata: ArtifactMetadata::Http(HttpArtifactMetadata {
status: StatusCode::OK,
headers: Default::default(),
}),
timing_phases: vec![crate::request::ArtifactTimingPhase::new(
"bodyRead",
Duration::from_millis(2),
)],
transcripts: vec![crate::request::ArtifactTranscript {
direction: crate::request::ArtifactTranscriptDirection::Outgoing,
label: "http.request".to_string(),
body: "abcdef".to_string(),
attributes: HashMap::new(),
}],
retained_artifacts: vec![crate::request::RetainedArtifact {
name: "trace.txt".to_string(),
content_type: "text/plain".to_string(),
body: "uvwxyz".to_string(),
}],
},
assertions: vec![],
},
started_at: SystemTime::UNIX_EPOCH,
duration: Duration::from_millis(5),
};
let json = run_record_json(
&record,
BodyReportOptions {
include_body: false,
max_body_chars: Some(3),
},
);
assert_eq!(json["body"], Value::Null);
assert_eq!(json["timing"]["totalMs"], 5);
assert_eq!(json["timing"]["phases"][0]["name"], "bodyRead");
assert_eq!(json["timing"]["phases"][0]["durationMs"], 2);
assert_eq!(json["transcripts"][0]["body"], Value::Null);
assert_eq!(json["transcripts"][0]["bodyChars"], 6);
assert_eq!(json["transcripts"][0]["bodyCharLimit"], 3);
assert_eq!(json["retainedArtifacts"][0]["body"], Value::Null);
assert_eq!(json["retainedArtifacts"][0]["bodyChars"], 6);
assert_eq!(json["retainedArtifacts"][0]["bodyCharLimit"], 3);
}
#[test]
fn run_outcome_junit_renders_testcase() {
let junit = run_outcome_junit(&sample_run_outcome());
assert!(junit.contains("<testsuite"));
assert!(junit.contains(
"<testcase classname=\"Fixture Collection\" name=\"#0 GET https://example.com/one :: ^ & body.ok == true\""
));
assert!(junit.contains("tests=\"2\""));
assert!(junit.contains("skipped=\"1\""));
assert!(junit.contains("<skipped message=\"guard evaluated to false\"/>"));
}
#[test]
fn run_outcome_junit_includes_interruption_case() {
let mut result = sample_run_outcome();
result.execution_failed = true;
result.interrupted = Some(crate::request::InterruptSignal::Sigterm);
let junit = run_outcome_junit(&result);
assert!(junit.contains("errors=\"1\""));
assert!(junit.contains("run interrupted by SIGTERM"));
assert!(junit.contains("Execution interrupted by SIGTERM"));
}
fn sample_run_outcome() -> RunOutcome {
RunOutcome {
collection: CollectionSummary {
path: PathBuf::from("/workspace/collection.hen"),
name: "Fixture Collection".to_string(),
description: "Collection used for structured output fixtures.".to_string(),
requests: vec![crate::automation::RequestSummary {
index: 0,
description: "Get one".to_string(),
method: "GET".to_string(),
url: "https://example.com/one".to_string(),
protocol: "http".to_string(),
protocol_context: None,
dependencies: vec![],
}],
required_inputs: vec![crate::automation::PromptRequirement {
name: "api_token".to_string(),
default: None,
}],
},
plan: vec![0],
selected_requests: vec![0],
primary_target: Some(0),
records: vec![ExecutionRecord {
index: 0,
description: "Get one".to_string(),
method: Method::GET,
url: "https://example.com/one".to_string(),
execution: RequestExecution {
output: "hello world".to_string(),
export_env: HashMap::new(),
artifact: ExecutionArtifact {
protocol: RequestProtocol::Http,
status: StatusCode::OK,
headers: Default::default(),
body: "hello world".to_string(),
json: None,
metadata: ArtifactMetadata::Http(HttpArtifactMetadata {
status: StatusCode::OK,
headers: Default::default(),
}),
timing_phases: vec![],
transcripts: vec![],
retained_artifacts: vec![],
},
assertions: vec![
AssertionOutcome {
assertion: "^ & body.ok == true".to_string(),
status: AssertionStatus::Passed,
message: None,
mismatch: None,
},
AssertionOutcome {
assertion: "[ true == false ] ^ & body.service == 'hen'".to_string(),
status: AssertionStatus::Skipped,
message: Some("guard evaluated to false".to_string()),
mismatch: None,
},
],
},
started_at: SystemTime::UNIX_EPOCH + Duration::from_millis(1_704_067_200_000),
duration: Duration::from_millis(25),
}],
failures: vec![],
execution_failed: false,
interrupted: None,
}
}
fn assert_json_fixture(value: Value, expected: &str) {
let actual = serde_json::to_string_pretty(&value).expect("json should serialize");
assert_eq!(actual.trim(), expected.trim());
}