mod support;
use serde_json::Value;
use support::TestWorkspace;
fn assert_verify_failure(
source: &str,
expected_message: &str,
expected_location: &str,
expected_excerpt: &str,
) {
let workspace = TestWorkspace::new();
workspace.write_file("collection.hen", source);
let output = workspace.run_hen(["verify", "collection.hen"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
assert!(output.stdout.is_empty(), "stdout: {}", output.stdout);
assert!(
output.stderr.contains(expected_message),
"stderr: {}",
output.stderr
);
assert!(
output.stderr.contains(expected_location),
"stderr: {}",
output.stderr
);
assert!(
output.stderr.contains(expected_excerpt),
"stderr: {}",
output.stderr
);
}
#[test]
fn verify_reports_requests_and_required_inputs() {
let workspace = TestWorkspace::new();
workspace.write_file(
"fragments/imported.hen",
r#"Imported request
POST https://example.com/imported/[[ token ]]
"#,
);
workspace.write_file(
"collection.hen",
r#"Verify Fixture
Verifies imported requests.
? region = [[ region = us-east-1 ]]
---
Root request
GET https://example.com/root
---
<< fragments/imported.hen
"#,
);
let output = workspace.run_hen(["verify", "collection.hen"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
assert!(
output.stdout.contains("Verification passed"),
"stdout: {}",
output.stdout
);
assert!(
output.stdout.contains("[0] GET https://example.com/root"),
"stdout: {}",
output.stdout
);
assert!(
output
.stdout
.contains("[1] POST https://example.com/imported/[[ token ]]"),
"stdout: {}",
output.stdout
);
assert!(
output.stdout.contains("Required inputs"),
"stdout: {}",
output.stdout
);
assert!(output.stdout.contains("token"), "stdout: {}", output.stdout);
assert!(
output.stdout.contains("region (default: us-east-1)"),
"stdout: {}",
output.stdout
);
assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);
}
#[test]
fn verify_reports_available_environments() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Environment Verify Fixture
$ API_ORIGIN = https://api.example.com
env local
$ API_ORIGIN = http://localhost:3000
env staging
$ API_ORIGIN = https://staging.example.com
---
Get profile
GET {{ API_ORIGIN }}/profile
"#,
);
let output = workspace.run_hen(["verify", "collection.hen"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
assert!(output.stdout.contains("Available environments"), "stdout: {}", output.stdout);
assert!(output.stdout.contains("local"), "stdout: {}", output.stdout);
assert!(output.stdout.contains("staging"), "stdout: {}", output.stdout);
}
#[test]
fn verify_outputs_json_report_with_available_environments() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Environment Verify Fixture
$ API_ORIGIN = https://api.example.com
env local
$ API_ORIGIN = http://localhost:3000
---
Get profile
GET {{ API_ORIGIN }}/profile
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["availableEnvironments"][0], "local");
}
#[test]
fn verify_outputs_http_session_name_in_protocol_context() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"HTTP Session Verify Fixture
---
Login
session = web
POST https://example.com/login
^ & status == 200
---
Load profile
session = web
GET https://example.com/profile
^ & status == 200
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["requests"][0]["protocol"], "http");
assert_eq!(parsed["requests"][0]["protocolContext"]["sessionName"], "web");
assert_eq!(parsed["requests"][1]["protocol"], "http");
assert_eq!(parsed["requests"][1]["protocolContext"]["sessionName"], "web");
}
#[test]
fn verify_outputs_oauth_auth_profile_in_protocol_context() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = OAuth Verify Fixture
oauth api
grant = client_credentials
issuer = https://login.example.com
client_id = secret.env("CLIENT_ID")
client_secret = secret.env("CLIENT_SECRET")
access_token -> $API_ACCESS_TOKEN
---
Get profile
auth = api
GET https://example.com/profile
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["requests"][0]["protocolContext"]["authProfile"], "api");
}
#[test]
fn verify_rejects_unknown_oauth_profile_reference() {
assert_verify_failure(
r#"name = Unknown OAuth Fixture
oauth api
grant = client_credentials
issuer = https://login.example.com
client_id = secret.env("CLIENT_ID")
client_secret = secret.env("CLIENT_SECRET")
---
Get profile
auth = missing
GET https://example.com/profile
"#,
"request references unknown OAuth profile 'missing'",
"9:1",
"auth = missing",
);
}
#[test]
fn verify_does_not_execute_shell_substitutions() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"Shell Verify
$ shell_value = $(touch side-effect.txt)
---
Shell request
GET https://example.com/{{ shell_value }}
"#,
);
let output = workspace.run_hen(["verify", "collection.hen"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
assert!(
!workspace.root().join("side-effect.txt").exists(),
"verify executed a shell substitution"
);
}
#[test]
fn verify_accepts_local_secret_references_without_loading_them() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Secret Verify Fixture
$ API_TOKEN = secret.env("HEN_TEST_VERIFY_TOKEN")
$ CLIENT_ID = secret.file("./missing/client_id.txt")
---
Get profile
GET https://example.com/profile
* Authorization = Bearer {{ API_TOKEN }}
* X-Client-Id = {{ CLIENT_ID }}
"#,
);
let output = workspace.run_hen(["verify", "collection.hen"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
assert!(output.stdout.contains("Verification passed"), "stdout: {}", output.stdout);
}
#[test]
fn verify_rejects_unsupported_secret_provider() {
assert_verify_failure(
r#"name = Unsupported Secret Provider Fixture
$ API_TOKEN = secret.keychain("service/account")
---
Get profile
GET https://example.com/profile
"#,
"unsupported provider 'keychain'",
"2:1",
"$ API_TOKEN = secret.keychain(\"service/account\")",
);
}
#[test]
fn verify_accepts_websocket_authoring_slice() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"WebSocket Verify
---
Open socket
protocol = ws
session = chat
GET wss://example.com/chat
---
Send hello
session = chat
~~~json
{"type":"hello"}
~~~
---
Receive reply
session = chat
receive
within = 2s
"#,
);
let output = workspace.run_hen(["verify", "collection.hen"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
assert!(
output.stdout.contains("[0] GET wss://example.com/chat"),
"stdout: {}",
output.stdout
);
assert!(
output.stdout.contains("[1] GET wss://example.com/chat"),
"stdout: {}",
output.stdout
);
assert!(
output.stdout.contains("[2] GET wss://example.com/chat"),
"stdout: {}",
output.stdout
);
}
#[test]
fn verify_accepts_websocket_exchange_authoring_slice() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"WebSocket Exchange Verify
---
Open socket
protocol = ws
session = chat
GET wss://example.com/chat
---
Send hello
session = chat
within = 2s
~~~json
{"type":"hello"}
~~~
"#,
);
let output = workspace.run_hen(["verify", "collection.hen"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
assert!(
output.stdout.contains("[0] GET wss://example.com/chat"),
"stdout: {}",
output.stdout
);
assert!(
output.stdout.contains("[1] GET wss://example.com/chat"),
"stdout: {}",
output.stdout
);
}
#[test]
fn verify_reports_parse_failures_without_running_requests() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"Broken Verify
---
Broken assertion
GET https://example.com
^ & body =~ /foo/
"#,
);
let output = workspace.run_hen(["verify", "collection.hen"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
assert!(output.stdout.is_empty(), "stdout: {}", output.stdout);
assert!(output
.stderr
.contains("No valid operator found in assertion"));
}
#[test]
fn verify_reports_unknown_schema_reference_at_declaration_span() {
assert_verify_failure(
r#"name = Verify Unknown Reference
description = Ensure unknown schema references point at the owning declaration.
schema User {
profile: Profile
}
---
Get user
GET https://example.com/users/1
"#,
"User references unknown validation target Profile",
"--> 3:1",
"schema User {",
);
}
#[test]
fn verify_reports_invalid_scalar_base_reference_at_declaration_span() {
assert_verify_failure(
r#"name = Verify Invalid Scalar Base
description = Ensure schema-backed scalar bases point at the scalar declaration.
schema User {
id: UUID
}
scalar UserAlias = User
---
Get user
GET https://example.com/users/1
"#,
"scalar UserAlias cannot use schema User as a scalar base",
"--> 6:1",
"scalar UserAlias = User",
);
}
#[test]
fn verify_reports_unknown_schema_assertion_target_at_assertion_span() {
assert_verify_failure(
r#"name = Verify Unknown Assertion Target
description = Ensure schema assertions are validated during verify.
---
Get user
GET https://example.com/users/1
^ & body === MissingUser
"#,
"Unknown schema validation target 'MissingUser'",
"--> 6:1",
"^ & body === MissingUser",
);
}
#[test]
fn verify_reports_duplicate_declaration_names_at_second_declaration() {
assert_verify_failure(
r#"name = Verify Duplicate Declaration
description = Ensure duplicate declaration names point at the second definition.
schema User {
id: UUID
}
scalar User = string
---
Get user
GET https://example.com/users/1
"#,
"User is already defined",
"--> 6:1",
"scalar User = string",
);
}
#[test]
fn verify_reports_reserved_name_redefinitions_at_declaration_span() {
assert_verify_failure(
r#"name = Verify Reserved Name
description = Ensure built-in names cannot be redefined.
scalar UUID = string
---
Get user
GET https://example.com/users/1
"#,
"UUID is reserved and cannot be redefined",
"--> 3:1",
"scalar UUID = string",
);
}
#[test]
fn verify_reports_invalid_scalar_predicate_combinations_at_declaration_span() {
assert_verify_failure(
r#"name = Verify Invalid Scalar Expression
description = Ensure invalid scalar base combinations point at the declaration.
scalar Broken = string & integer
---
Get user
GET https://example.com/users/1
"#,
"scalar expressions can only declare one base type",
"--> 3:17",
"scalar Broken = string & integer",
);
}
#[test]
fn verify_reports_misplaced_declarations_after_requests_begin() {
assert_verify_failure(
r#"name = Verify Misplaced Declaration
description = Ensure declarations after requests begin get a targeted error.
---
Get user
GET https://example.com/users/1
schema User {
id: UUID
}
"#,
"schema and scalar declarations must appear before the first ---",
"--> 6:1",
"schema User {",
);
}