use serde_json::Value;
use crate::{
helpers::{
spawn_cookie_session_server, spawn_http_server, spawn_mcp_http_server, spawn_mcp_protocol_error_server,
spawn_mcp_sse_http_server, spawn_sse_http_server, spawn_ws_server,
},
support::TestWorkspace,
};
#[test]
fn run_outputs_json_report_for_graphql_requests() {
let server_url = spawn_http_server(
200,
"OK",
"application/json",
r#"{"data":{"user":{"id":"123","name":"Ada"}},"errors":null}"#,
);
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
&format!(
r#"GraphQL Fixture
Exercises GraphQL-over-HTTP reporting.
---
Fetch GraphQL fixture
protocol = graphql
POST {server_url}
operation = GetUser
variables = {{"id":"123"}}
~~~graphql
query GetUser($id: ID!) {{
user(id: $id) {{
id
name
}}
}}
~~~
^ & graphql.errors == null
^ & graphql.data.user.id == "123"
"#
),
);
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]["protocol"], "graphql");
assert_eq!(
parsed["records"][0]["protocolContext"]["operationName"],
"GetUser"
);
assert_eq!(parsed["records"][0]["protocolContext"]["variables"]["id"], "123");
assert_eq!(parsed["records"][0]["assertions"][0]["status"], "passed");
assert_eq!(parsed["records"][0]["assertions"][1]["status"], "passed");
assert_eq!(parsed["failures"], serde_json::json!([]));
}
#[test]
fn run_outputs_json_report_for_http_session_cookie_reuse() {
let server_url = spawn_cookie_session_server();
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
&format!(
r#"HTTP Session Fixture
Exercises session-backed HTTP cookie reuse.
---
Login
session = web
POST {server_url}/login
^ & status == 200
---
Load profile
session = web
GET {server_url}/profile
^ & status == 200
^ & body.authenticated == true
"#
),
);
let output = workspace.run_hen(["run", "collection.hen", "all", "--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]["protocol"], "http");
assert_eq!(parsed["records"][0]["protocolContext"]["sessionName"], "web");
assert_eq!(parsed["records"][0]["assertions"][0]["status"], "passed");
assert_eq!(parsed["records"][1]["protocol"], "http");
assert_eq!(parsed["records"][1]["protocolContext"]["sessionName"], "web");
assert_eq!(parsed["records"][1]["assertions"][0]["status"], "passed");
assert_eq!(parsed["records"][1]["assertions"][1]["status"], "passed");
assert_eq!(parsed["records"][1]["body"], r#"{"authenticated":true}"#);
assert_eq!(parsed["failures"], serde_json::json!([]));
}
#[test]
fn run_outputs_json_report_for_sse_requests() {
let server_url = spawn_sse_http_server();
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
&format!(
r#"SSE Fixture
Exercises SSE open and receive execution.
---
Open stream
protocol = sse
session = prices
GET {server_url}
^ & status == 200
---
Receive update
session = prices
receive
within = 2s
& sse.id -> $EVENT_ID
^ & sse.event == "price"
^ $EVENT_ID == "evt-1"
^ & body.symbol == "AAPL"
^ & body.price === NUMBER
"#
),
);
let output = workspace.run_hen(["run", "collection.hen", "all", "--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]["protocol"], "sse");
assert_eq!(parsed["records"][0]["status"], 200);
assert_eq!(parsed["records"][0]["protocolContext"]["action"], "open");
assert_eq!(parsed["records"][0]["protocolContext"]["sessionName"], "prices");
assert_eq!(parsed["records"][0]["assertions"][0]["status"], "passed");
assert_eq!(parsed["records"][1]["protocol"], "sse");
assert_eq!(parsed["records"][1]["protocolContext"]["action"], "receive");
assert_eq!(parsed["records"][1]["protocolContext"]["sessionName"], "prices");
assert_eq!(parsed["records"][1]["protocolContext"]["within"], "2s");
assert_eq!(parsed["records"][1]["protocolContext"]["event"], "price");
assert_eq!(parsed["records"][1]["protocolContext"]["id"], "evt-1");
assert_eq!(parsed["records"][1]["assertions"][0]["status"], "passed");
assert_eq!(parsed["records"][1]["assertions"][1]["status"], "passed");
assert_eq!(parsed["records"][1]["assertions"][2]["status"], "passed");
assert_eq!(parsed["records"][1]["assertions"][3]["status"], "passed");
assert!(parsed["records"][1]["body"]
.as_str()
.expect("body should be serialized as a string")
.contains("\"symbol\":\"AAPL\""));
assert_eq!(parsed["failures"], serde_json::json!([]));
}
#[test]
fn run_outputs_json_report_for_ws_requests() {
let server_url = spawn_ws_server();
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
&format!(
r#"WS Fixture
Exercises WebSocket open, send, and receive execution.
---
Open socket
protocol = ws
session = chat
GET {server_url}
^ & status == 101
---
Send hello
session = chat
~~~json
{{"type":"hello","room":"prices"}}
~~~
^ & status == 101
---
Receive reply
session = chat
receive
within = 2s
& ws.kind -> $KIND
^ $KIND == "text"
^ & ws.kind == "text"
^ & body.type == "ack"
^ & body.room == "prices"
"#
),
);
let output = workspace.run_hen(["run", "collection.hen", "all", "--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]["protocol"], "ws");
assert_eq!(parsed["records"][0]["status"], 101);
assert_eq!(parsed["records"][0]["protocolContext"]["action"], "open");
assert_eq!(parsed["records"][0]["protocolContext"]["sessionName"], "chat");
assert_eq!(parsed["records"][0]["assertions"][0]["status"], "passed");
assert_eq!(parsed["records"][1]["protocol"], "ws");
assert_eq!(parsed["records"][1]["status"], 101);
assert_eq!(parsed["records"][1]["protocolContext"]["action"], "send");
assert_eq!(parsed["records"][1]["protocolContext"]["sessionName"], "chat");
assert_eq!(parsed["records"][1]["protocolContext"]["kind"], "json");
assert_eq!(parsed["records"][1]["assertions"][0]["status"], "passed");
assert_eq!(parsed["records"][2]["protocol"], "ws");
assert_eq!(parsed["records"][2]["status"], 101);
assert_eq!(parsed["records"][2]["protocolContext"]["action"], "receive");
assert_eq!(parsed["records"][2]["protocolContext"]["sessionName"], "chat");
assert_eq!(parsed["records"][2]["protocolContext"]["within"], "2s");
assert_eq!(parsed["records"][2]["protocolContext"]["kind"], "text");
assert_eq!(parsed["records"][2]["assertions"][0]["status"], "passed");
assert_eq!(parsed["records"][2]["assertions"][1]["status"], "passed");
assert_eq!(parsed["records"][2]["assertions"][2]["status"], "passed");
assert_eq!(parsed["records"][2]["assertions"][3]["status"], "passed");
assert!(parsed["records"][2]["body"]
.as_str()
.expect("body should be serialized as a string")
.contains("\"type\":\"ack\""));
assert_eq!(parsed["failures"], serde_json::json!([]));
}
#[test]
fn run_outputs_json_report_for_ws_exchange_requests() {
let server_url = spawn_ws_server();
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
&format!(
r#"WS Exchange Fixture
Exercises WebSocket open and exchange execution.
---
Open socket
protocol = ws
session = chat
GET {server_url}
^ & status == 101
---
Send hello
session = chat
within = 2s
~~~json
{{"type":"hello","room":"prices"}}
~~~
& ws.kind -> $KIND
^ $KIND == "text"
^ & ws.kind == "text"
^ & body.type == "ack"
^ & body.room == "prices"
"#
),
);
let output = workspace.run_hen(["run", "collection.hen", "all", "--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]["protocol"], "ws");
assert_eq!(parsed["records"][0]["status"], 101);
assert_eq!(parsed["records"][0]["protocolContext"]["action"], "open");
assert_eq!(parsed["records"][0]["protocolContext"]["sessionName"], "chat");
assert_eq!(parsed["records"][0]["assertions"][0]["status"], "passed");
assert_eq!(parsed["records"][1]["protocol"], "ws");
assert_eq!(parsed["records"][1]["status"], 101);
assert_eq!(parsed["records"][1]["protocolContext"]["action"], "exchange");
assert_eq!(parsed["records"][1]["protocolContext"]["sessionName"], "chat");
assert_eq!(parsed["records"][1]["protocolContext"]["within"], "2s");
assert_eq!(parsed["records"][1]["protocolContext"]["kind"], "text");
assert!(parsed["records"][1]["timing"]["totalMs"].as_u64().is_some());
assert_eq!(parsed["records"][1]["timing"]["phases"][0]["name"], "send");
assert!(parsed["records"][1]["timing"]["phases"][0]["durationMs"].as_u64().is_some());
assert_eq!(parsed["records"][1]["timing"]["phases"][1]["name"], "wait");
assert!(parsed["records"][1]["timing"]["phases"][1]["durationMs"].as_u64().is_some());
assert_eq!(parsed["records"][1]["transcripts"][0]["direction"], "outgoing");
assert_eq!(parsed["records"][1]["transcripts"][0]["label"], "ws.exchange");
assert_eq!(parsed["records"][1]["transcripts"][0]["attributes"]["action"], "exchange");
assert_eq!(parsed["records"][1]["transcripts"][1]["direction"], "incoming");
assert_eq!(parsed["records"][1]["transcripts"][1]["label"], "ws.message");
assert_eq!(parsed["records"][1]["transcripts"][1]["attributes"]["kind"], "text");
assert_eq!(parsed["records"][1]["retainedArtifacts"], serde_json::json!([]));
assert_eq!(parsed["records"][1]["assertions"][0]["status"], "passed");
assert_eq!(parsed["records"][1]["assertions"][1]["status"], "passed");
assert_eq!(parsed["records"][1]["assertions"][2]["status"], "passed");
assert_eq!(parsed["records"][1]["assertions"][3]["status"], "passed");
assert!(parsed["records"][1]["body"]
.as_str()
.expect("body should be serialized as a string")
.contains("\"type\":\"ack\""));
assert_eq!(parsed["failures"], serde_json::json!([]));
}
#[test]
fn run_outputs_json_report_for_mcp_requests() {
let server_url = spawn_mcp_http_server();
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
&format!(
r#"MCP Fixture
Exercises HTTP MCP target execution.
---
Connect MCP
protocol = mcp
session = app
POST {server_url}
* Authorization = Bearer test-token
call = initialize
^ & body.result.serverInfo.name == "fixture-mcp"
---
List tools
protocol = mcp
session = app
POST {server_url}
call = tools/list
^ & body.result.tools[0].name == "search"
---
List resources
protocol = mcp
session = app
POST {server_url}
call = resources/list
^ & body.result.resources[0].name == "Fixture Resource"
---
Call tool
protocol = mcp
session = app
POST {server_url}
call = tools/call
tool = search
arguments = {{"query":"hedgehog"}}
^ & body.result.content[0].text == "search result"
"#
),
);
let output = workspace.run_hen(["run", "collection.hen", "all", "--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]["protocol"], "mcp");
assert_eq!(parsed["records"][0]["protocolContext"]["call"], "initialize");
assert_eq!(parsed["records"][0]["protocolContext"]["sessionName"], "app");
assert_eq!(parsed["records"][1]["protocol"], "mcp");
assert_eq!(parsed["records"][1]["protocolContext"]["call"], "tools/list");
assert_eq!(parsed["records"][1]["protocolContext"]["sessionName"], "app");
assert_eq!(parsed["records"][1]["assertions"][0]["status"], "passed");
assert_eq!(parsed["records"][2]["protocol"], "mcp");
assert_eq!(parsed["records"][2]["protocolContext"]["call"], "resources/list");
assert_eq!(parsed["records"][2]["protocolContext"]["sessionName"], "app");
assert_eq!(parsed["records"][2]["assertions"][0]["status"], "passed");
assert_eq!(parsed["records"][3]["protocol"], "mcp");
assert_eq!(parsed["records"][3]["protocolContext"]["call"], "tools/call");
assert_eq!(parsed["records"][3]["protocolContext"]["sessionName"], "app");
assert_eq!(parsed["records"][3]["protocolContext"]["tool"], "search");
assert_eq!(parsed["records"][3]["protocolContext"]["arguments"]["query"], "hedgehog");
assert_eq!(parsed["records"][3]["assertions"][0]["status"], "passed");
assert_eq!(parsed["failures"], serde_json::json!([]));
}
#[test]
fn run_outputs_json_report_for_mcp_protocol_errors() {
let server_url = spawn_mcp_protocol_error_server();
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
&format!(
r#"MCP Error Fixture
Exercises assertions against JSON-RPC error envelopes.
---
Connect MCP
protocol = mcp
session = app
POST {server_url}
* Authorization = Bearer test-token
call = initialize
^ & body.result.serverInfo.name == "fixture-mcp"
---
Call missing tool
protocol = mcp
session = app
POST {server_url}
call = tools/call
tool = missing-tool
arguments = {{}}
^ & body.error.code == -32601
^ & body.error.message == "Tool not found"
"#
),
);
let output = workspace.run_hen(["run", "collection.hen", "all", "--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]["protocol"], "mcp");
assert_eq!(parsed["records"][1]["protocol"], "mcp");
assert_eq!(parsed["records"][1]["protocolContext"]["call"], "tools/call");
assert_eq!(parsed["records"][1]["protocolContext"]["sessionName"], "app");
assert_eq!(parsed["records"][1]["protocolContext"]["tool"], "missing-tool");
assert_eq!(parsed["records"][1]["protocolContext"]["arguments"], serde_json::json!({}));
assert_eq!(parsed["records"][1]["assertions"][0]["status"], "passed");
assert_eq!(parsed["records"][1]["assertions"][1]["status"], "passed");
assert_eq!(parsed["failures"], serde_json::json!([]));
}
#[test]
fn run_outputs_json_report_for_mcp_sse_responses() {
let server_url = spawn_mcp_sse_http_server();
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
&format!(
r#"MCP SSE Fixture
Exercises SSE-wrapped MCP JSON-RPC responses.
---
Connect MCP
protocol = mcp
session = app
POST {server_url}
call = initialize
& body.result -> $INIT_RESULT
^ $INIT_RESULT ~= /protocolVersion/
^ & body.result.protocolVersion == "2025-06-18"
^ & body.result.serverInfo.name == "fixture-mcp"
"#
),
);
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");
let body = parsed["records"][0]["body"]
.as_str()
.expect("body should be serialized as a string");
assert!(body.contains("\"protocolVersion\":\"2025-06-18\""), "body: {body}");
assert!(!body.contains("event:"), "body: {body}");
assert_eq!(parsed["records"][0]["assertions"][0]["status"], "passed");
assert_eq!(parsed["records"][0]["assertions"][1]["status"], "passed");
assert_eq!(parsed["records"][0]["assertions"][2]["status"], "passed");
assert_eq!(parsed["failures"], serde_json::json!([]));
}