use http::Method;
use pest::Parser;
use crate::request::RequestProtocol;
use super::*;
#[test]
fn schema_object_declaration_rule_accepts_indented_fields() {
let source = r#"schema User {
id: UUID
birthday?: DATE
}
"#;
assert!(CollectionParser::parse(Rule::schema_object_declaration, source).is_ok());
}
#[test]
fn preamble_rule_accepts_scalar_and_schema_declarations() {
let source = r#"
scalar Food = enum("pizza", "taco", "salad")
schema User {
id: UUID
birthday?: DATE
favoriteFoods: Food[]
}
"#;
assert!(CollectionParser::parse(Rule::preamble, source).is_ok());
}
#[test]
fn preamble_rule_accepts_environment_blocks() {
let source = r#"
name = Example Collection
$ API_ORIGIN = https://prod.example.com
env local
$ API_ORIGIN = http://localhost:3000
"#;
assert!(CollectionParser::parse(Rule::preamble, source).is_ok());
}
#[test]
fn preamble_rule_accepts_redaction_directives() {
let source = r#"
redact_header = X-Session-Token
redact_capture = SESSION_ID
redact_body = body.session.accessToken
"#;
assert!(CollectionParser::parse(Rule::preamble, source).is_ok());
}
#[test]
fn request_collection_rule_accepts_environment_preamble_before_requests() {
let source = r#"
name = Example Collection
$ API_ORIGIN = https://prod.example.com
env local
$ API_ORIGIN = http://localhost:3000
---
Get profile
GET {{ API_ORIGIN }}/profile
"#;
assert!(CollectionParser::parse(Rule::request_collection, source).is_ok());
}
#[test]
fn parse_request_collection_exposes_environment_preamble_items() {
let source = r#"
name = Example Collection
$ API_ORIGIN = https://prod.example.com
env local
$ API_ORIGIN = http://localhost:3000
---
Get profile
GET {{ API_ORIGIN }}/profile
"#;
let pair = super::declarations::parse_request_collection(source).unwrap();
let rules = pair.into_inner().map(|pair| pair.as_rule()).collect::<Vec<_>>();
assert!(rules.contains(&Rule::collection_name));
assert!(rules.contains(&Rule::variable));
assert!(rules.contains(&Rule::environment_block));
assert!(rules.contains(&Rule::requests));
}
#[test]
fn inspect_collection_syntax_accepts_environment_blocks() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
name = Example Collection
$ API_ORIGIN = https://prod.example.com
env local
$ API_ORIGIN = http://localhost:3000
---
Get profile
GET {{ API_ORIGIN }}/profile
"#;
let summary = inspect_collection_syntax(source, working_dir).unwrap();
assert_eq!(summary.requests.len(), 1);
assert_eq!(summary.requests[0].method, "GET");
}
#[test]
fn inspect_collection_syntax_rejects_invalid_redact_body_target() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
redact_body = header.Authorization
---
Get profile
GET https://example.com/profile
"#;
let err = inspect_collection_syntax(source, working_dir).unwrap_err();
assert!(
err.to_string()
.contains("redact_body must target a body path like body.token or json(body.payload).token")
);
}
#[test]
fn request_collection_exposes_declarations_before_requests() {
let source = r#"
scalar Food = enum("pizza", "taco", "salad")
schema User {
id: UUID
}
---
Get User
GET https://example.com/user/123
"#;
let collection = super::declarations::parse_request_collection(source).unwrap();
let rules: Vec<_> = collection.into_inner().map(|pair| pair.as_rule()).collect();
assert!(rules.contains(&Rule::declaration));
assert!(rules.contains(&Rule::requests));
}
#[test]
fn parse_collection_registers_schema_declarations() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
scalar Food = enum("pizza", "taco", "salad")
schema User {
id: UUID
birthday?: DATE
favoriteFoods: Food[]
}
---
Get User
GET https://example.com/user/123
^ & body === User
"#;
let collection = parse_collection(source, working_dir).unwrap();
assert!(matches!(
collection.schema_registry.get("Food"),
Some(crate::schema::RegistryEntry::Scalar(_))
));
assert!(matches!(
collection.schema_registry.get("User"),
Some(crate::schema::RegistryEntry::Schema(_))
));
assert_eq!(collection.requests.len(), 1);
}
#[test]
fn parse_collection_builds_graphql_requests() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
---
Get User GraphQL
protocol = graphql
POST https://example.com/graphql
operation = GetUser
variables = {"id":"123"}
~~~graphql
query GetUser($id: ID!) {
user(id: $id) {
id
}
}
~~~
^ & body.data.user.id == "123"
"#;
let collection = parse_collection(source, working_dir).unwrap();
assert_eq!(collection.requests.len(), 1);
let request = &collection.requests[0];
assert_eq!(request.protocol(), RequestProtocol::Graphql);
let graphql = request
.graphql_operation()
.expect("graphql operation should exist");
assert_eq!(graphql.http.method, Method::POST);
assert_eq!(graphql.http.url, "https://example.com/graphql");
assert_eq!(graphql.operation_name.as_deref(), Some("GetUser"));
assert_eq!(graphql.variables_json.as_deref(), Some("{\"id\":\"123\"}"));
assert!(graphql.document.contains("query GetUser($id: ID!)"));
assert!(graphql.http.form_data.is_empty());
assert!(graphql.http.body.is_none());
assert!(graphql.http.body_content_type.is_none());
}
#[test]
fn parse_collection_applies_selected_environment_overrides() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
name = Example Collection
$ API_ORIGIN = https://prod.example.com
env local
$ API_ORIGIN = http://localhost:3000
---
Get profile
GET {{ API_ORIGIN }}/profile
"#;
let default_collection = parse_collection(source, working_dir.clone()).unwrap();
let local_collection =
parse_collection_with_environment(source, working_dir, Some("local")).unwrap();
assert_eq!(default_collection.requests.len(), 1);
assert_eq!(local_collection.requests.len(), 1);
assert_eq!(
default_collection.requests[0].http_operation().url,
"https://prod.example.com/profile"
);
assert_eq!(
local_collection.requests[0].http_operation().url,
"http://localhost:3000/profile"
);
}
#[test]
fn parse_collection_applies_selected_environment_over_shell_backed_base_value() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
name = Example Collection
$ CLIENT_ID = $(printf base-client)
env local
$ CLIENT_ID = local-client
---
Get profile
GET https://example.com/profile
* X-Client-Id = {{ CLIENT_ID }}
"#;
let default_collection = parse_collection(source, working_dir.clone()).unwrap();
let local_collection =
parse_collection_with_environment(source, working_dir, Some("local")).unwrap();
assert_eq!(
default_collection.requests[0]
.http_operation()
.headers
.get("X-Client-Id")
.map(String::as_str),
Some("base-client")
);
assert_eq!(
local_collection.requests[0]
.http_operation()
.headers
.get("X-Client-Id")
.map(String::as_str),
Some("local-client")
);
}
#[test]
fn inspect_collection_syntax_accepts_local_secret_references_without_resolution() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
name = Secret Verify Fixture
$ API_TOKEN = secret.env("HEN_TEST_API_TOKEN")
$ CLIENT_ID = secret.file("./secrets/client_id.txt")
---
Get profile
GET https://example.com/profile
* Authorization = Bearer {{ API_TOKEN }}
* X-Client-Id = {{ CLIENT_ID }}
"#;
let summary = inspect_collection_syntax(source, working_dir).unwrap();
assert_eq!(summary.requests.len(), 1);
assert_eq!(summary.requests[0].method, "GET");
}
#[test]
fn parse_collection_resolves_file_secret_provider() {
let tempdir = tempfile::tempdir().expect("tempdir should exist");
let working_dir = tempdir.path().to_path_buf();
std::fs::write(working_dir.join("client_id.txt"), "demo-client-id\n")
.expect("secret file should be written");
let source = r#"
name = Secret File Fixture
$ CLIENT_ID = secret.file("./client_id.txt")
---
Get profile
GET https://example.com/profile
* X-Client-Id = {{ CLIENT_ID }}
"#;
let collection = parse_collection(source, working_dir).unwrap();
assert_eq!(
collection.requests[0]
.http_operation()
.headers
.get("X-Client-Id")
.map(String::as_str),
Some("demo-client-id")
);
}
#[test]
fn parse_collection_caches_file_secret_provider_values_within_one_parse() {
let tempdir = tempfile::tempdir().expect("tempdir should exist");
let working_dir = tempdir.path().to_path_buf();
std::fs::write(working_dir.join("secret.txt"), "first").expect("secret file should exist");
let source = r#"
name = Secret Cache Fixture
$ FIRST = secret.file("./secret.txt")
$ CHANGE = $(printf second > ./secret.txt)
$ SECOND = secret.file("./secret.txt")
---
Get profile
GET https://example.com/profile
* X-First = {{ FIRST }}
* X-Second = {{ SECOND }}
"#;
let collection = parse_collection(source, working_dir).unwrap();
assert_eq!(
collection.requests[0]
.http_operation()
.headers
.get("X-First")
.map(String::as_str),
Some("first")
);
assert_eq!(
collection.requests[0]
.http_operation()
.headers
.get("X-Second")
.map(String::as_str),
Some("first")
);
}
#[test]
fn inspect_collection_syntax_inherits_session_request_target() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
---
Initialize MCP
protocol = mcp
session = app
POST https://example.com/mcp
call = initialize
---
List tools
session = app
call = tools/list
"#;
let summary = inspect_collection_syntax(source, working_dir).unwrap();
assert_eq!(summary.requests.len(), 2);
assert_eq!(summary.requests[1].method, "POST");
assert_eq!(summary.requests[1].url, "https://example.com/mcp");
assert_eq!(summary.requests[1].protocol, "mcp");
}
#[test]
fn parse_collection_builds_sse_requests() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
---
Open stream
protocol = sse
session = prices
GET https://example.com/prices/stream
---
Receive price update
session = prices
receive
within = 5s
"#;
let collection = parse_collection(source, working_dir).unwrap();
assert_eq!(collection.requests.len(), 2);
let open = &collection.requests[0];
assert_eq!(open.protocol(), RequestProtocol::Sse);
assert_eq!(open.session_name.as_deref(), Some("prices"));
assert_eq!(open.http_operation().method, Method::GET);
assert_eq!(open.http_operation().url, "https://example.com/prices/stream");
assert!(matches!(
open.sse_operation().expect("sse operation should exist").action,
crate::request::SseAction::Open
));
let receive = &collection.requests[1];
assert_eq!(receive.protocol(), RequestProtocol::Sse);
assert_eq!(receive.session_name.as_deref(), Some("prices"));
assert_eq!(receive.http_operation().method, Method::GET);
assert_eq!(
receive.http_operation().url,
"https://example.com/prices/stream"
);
assert!(matches!(
receive.sse_operation().expect("sse operation should exist").action,
crate::request::SseAction::Receive { .. }
));
}
#[test]
fn inspect_collection_syntax_builds_sse_receive_context() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
---
Open stream
protocol = sse
session = prices
GET https://example.com/prices/stream
---
Receive price update
session = prices
receive
within = 5s
"#;
let summary = inspect_collection_syntax(source, working_dir).unwrap();
assert_eq!(summary.requests.len(), 2);
assert_eq!(summary.requests[1].method, "GET");
assert_eq!(summary.requests[1].url, "https://example.com/prices/stream");
assert_eq!(summary.requests[1].protocol, "sse");
assert_eq!(summary.requests[1].protocol_context.as_ref().unwrap()["action"], "receive");
assert_eq!(summary.requests[1].protocol_context.as_ref().unwrap()["within"], "5s");
}
#[test]
fn inspect_collection_syntax_rejects_sse_receive_without_within() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
---
Receive price update
protocol = sse
session = prices
GET https://example.com/prices/stream
receive
"#;
let err = inspect_collection_syntax(source, working_dir).unwrap_err();
assert!(err
.to_string()
.contains("SSE receive steps require 'within = ...'"));
}
#[test]
fn inspect_collection_syntax_rejects_invalid_sse_within_duration() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
---
Receive price update
protocol = sse
session = prices
GET https://example.com/prices/stream
receive
within = soon
"#;
let err = inspect_collection_syntax(source, working_dir).unwrap_err();
assert!(err
.to_string()
.contains("invalid within duration 'soon'"));
}
#[test]
fn parse_collection_builds_ws_requests() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
---
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 collection = parse_collection(source, working_dir).unwrap();
assert_eq!(collection.requests.len(), 3);
let open = &collection.requests[0];
assert_eq!(open.protocol(), RequestProtocol::Ws);
assert!(matches!(
open.ws_operation().expect("ws operation should exist").action,
crate::request::WsAction::Open
));
let send = &collection.requests[1];
assert_eq!(send.protocol(), RequestProtocol::Ws);
assert_eq!(send.http_operation().method, Method::GET);
assert_eq!(send.http_operation().url, "wss://example.com/chat");
assert!(matches!(
send.ws_operation().expect("ws operation should exist").action,
crate::request::WsAction::Send {
kind: crate::request::WsSendKind::Json,
..
}
));
let receive = &collection.requests[2];
assert_eq!(receive.protocol(), RequestProtocol::Ws);
assert_eq!(receive.http_operation().method, Method::GET);
assert_eq!(receive.http_operation().url, "wss://example.com/chat");
assert!(matches!(
receive.ws_operation().expect("ws operation should exist").action,
crate::request::WsAction::Receive { .. }
));
}
#[test]
fn parse_collection_builds_ws_exchange_requests() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
---
Open socket
protocol = ws
session = chat
GET wss://example.com/chat
---
Send hello
session = chat
within = 2s
~~~json
{"type":"hello"}
~~~
"#;
let collection = parse_collection(source, working_dir).unwrap();
assert_eq!(collection.requests.len(), 2);
let exchange = &collection.requests[1];
assert!(matches!(
exchange.ws_operation().expect("ws operation should exist").action,
crate::request::WsAction::Exchange {
kind: crate::request::WsSendKind::Json,
..
}
));
}
#[test]
fn inspect_collection_syntax_builds_ws_context() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
---
Open socket
protocol = ws
session = chat
GET wss://example.com/chat
---
Send hello
session = chat
~~~text
hello
~~~
---
Receive reply
session = chat
receive
within = 2s
"#;
let summary = inspect_collection_syntax(source, working_dir).unwrap();
assert_eq!(summary.requests.len(), 3);
assert_eq!(summary.requests[1].protocol, "ws");
assert_eq!(summary.requests[1].protocol_context.as_ref().unwrap()["action"], "send");
assert_eq!(summary.requests[1].protocol_context.as_ref().unwrap()["kind"], "text");
assert_eq!(summary.requests[2].protocol, "ws");
assert_eq!(summary.requests[2].protocol_context.as_ref().unwrap()["action"], "receive");
assert_eq!(summary.requests[2].protocol_context.as_ref().unwrap()["within"], "2s");
}
#[test]
fn inspect_collection_syntax_builds_ws_exchange_context() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
---
Open socket
protocol = ws
session = chat
GET wss://example.com/chat
---
Send hello
session = chat
within = 2s
~~~
hello
~~~
"#;
let summary = inspect_collection_syntax(source, working_dir).unwrap();
assert_eq!(summary.requests[1].protocol_context.as_ref().unwrap()["action"], "exchange");
assert_eq!(summary.requests[1].protocol_context.as_ref().unwrap()["kind"], "text");
assert_eq!(summary.requests[1].protocol_context.as_ref().unwrap()["within"], "2s");
}
#[test]
fn inspect_collection_syntax_accepts_defaulted_prompt_variable_assignments() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
name = WebSocket Fixture
description = Demonstrates WebSocket open authoring with a defaulted prompt variable.
$ WS_ORIGIN = [[ ws_origin = wss://example.com ]]
---
Open socket
protocol = ws
session = chat
GET {{ WS_ORIGIN }}/chat
"#;
let summary = inspect_collection_syntax(source, working_dir).unwrap();
assert_eq!(summary.requests.len(), 1);
assert_eq!(summary.requests[0].protocol, "ws");
assert!(summary.requests[0].url.ends_with("/chat"));
}
#[test]
fn inspect_collection_syntax_rejects_ws_send_without_body() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
---
Open socket
protocol = ws
session = chat
GET wss://example.com/chat
---
Send hello
session = chat
send = text
"#;
let err = inspect_collection_syntax(source, working_dir).unwrap_err();
assert!(err
.to_string()
.contains("WebSocket send steps require a body block"));
}
#[test]
fn inspect_collection_syntax_rejects_conflicting_ws_send_kind_and_body_type() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
---
Open socket
protocol = ws
session = chat
GET wss://example.com/chat
---
Send hello
session = chat
send = text
~~~json
{"type":"hello"}
~~~
"#;
let err = inspect_collection_syntax(source, working_dir).unwrap_err();
assert!(err
.to_string()
.contains("conflicts with body block type 'json'"));
}
#[test]
fn inspect_collection_syntax_rejects_session_request_without_target_anchor() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
---
List tools
protocol = mcp
session = app
call = tools/list
"#;
let err = inspect_collection_syntax(source, working_dir).unwrap_err();
assert!(err
.to_string()
.contains("session-backed request omitted its method/URL"));
}
#[test]
fn inspect_collection_syntax_rejects_mcp_tool_call_without_tool() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
---
Broken MCP
protocol = mcp
POST https://example.com/mcp
call = tools/call
"#;
let err = inspect_collection_syntax(source, working_dir).unwrap_err();
assert!(err.to_string().contains("requires 'tool = ...'"));
}
#[test]
fn inspect_collection_syntax_rejects_graphql_without_graphql_block() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
---
Broken GraphQL
protocol = graphql
POST https://example.com/graphql
~~~application/json
{"query":"query { viewer { id } }"}
~~~
"#;
let err = inspect_collection_syntax(source, working_dir).unwrap_err();
assert!(err.to_string().contains("~~~graphql document block"));
}
#[test]
fn inspect_collection_syntax_rejects_graphql_form_fields() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
---
Broken GraphQL
protocol = graphql
POST https://example.com/graphql
~ note = hello
~~~graphql
query Viewer {
viewer { id }
}
~~~
"#;
let err = inspect_collection_syntax(source, working_dir).unwrap_err();
assert!(err
.to_string()
.contains("GraphQL requests do not support form fields"));
}
#[test]
fn parsed_scalar_declarations_validate_values() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
scalar UserId = string & format(uuid)
scalar Status = enum("queued", "running", "done")
scalar Handle = string & len(3..24) & pattern(/^[a-z][a-z0-9_]*$/)
scalar Age = integer & range(0..130)
---
Get User
GET https://example.com/user/123
^ & body === UserId
"#;
let collection = parse_collection(source, working_dir).unwrap();
assert!(collection
.schema_registry
.validate_scalar_target(
"UserId",
&serde_json::json!("550e8400-e29b-41d4-a716-446655440000")
)
.is_ok());
assert!(collection
.schema_registry
.validate_scalar_target("Status", &serde_json::json!("running"))
.is_ok());
assert!(collection
.schema_registry
.validate_scalar_target("Handle", &serde_json::json!("alice_1"))
.is_ok());
assert!(collection
.schema_registry
.validate_scalar_target("Age", &serde_json::json!(42))
.is_ok());
let user_id_error = collection
.schema_registry
.validate_scalar_target("UserId", &serde_json::json!("not-a-uuid"))
.unwrap_err();
assert!(matches!(
user_id_error.reason,
crate::schema::ScalarValidationErrorKind::FormatMismatch { .. }
));
let handle_error = collection
.schema_registry
.validate_scalar_target("Handle", &serde_json::json!("ab"))
.unwrap_err();
assert!(matches!(
handle_error.reason,
crate::schema::ScalarValidationErrorKind::LengthOutOfRange { .. }
));
let age_error = collection
.schema_registry
.validate_scalar_target("Age", &serde_json::json!(131))
.unwrap_err();
assert!(matches!(
age_error.reason,
crate::schema::ScalarValidationErrorKind::RangeOutOfRange { .. }
));
}
#[test]
fn parsed_scalar_declarations_preserve_named_scalar_chains() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
scalar WORD = string & pattern(/^[a-z]+$/)
scalar HANDLE = WORD & len(3..24)
---
Get User
GET https://example.com/user/123
^ & body === HANDLE
"#;
let collection = parse_collection(source, working_dir).unwrap();
assert!(collection
.schema_registry
.validate_scalar_target("HANDLE", &serde_json::json!("alice"))
.is_ok());
let pattern_error = collection
.schema_registry
.validate_scalar_target("HANDLE", &serde_json::json!("Alice"))
.unwrap_err();
assert!(matches!(
pattern_error.reason,
crate::schema::ScalarValidationErrorKind::PatternMismatch { .. }
));
}
#[test]
fn parsed_schema_declarations_validate_objects() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
scalar Food = enum("pizza", "taco")
schema Address {
city: string
postalCode: string
}
schema User {
id: UUID
birthday?: DATE?
favoriteFoods: Food[]
address: Address
}
---
Get User
GET https://example.com/user/123
^ & body === User
"#;
let collection = parse_collection(source, working_dir).unwrap();
assert!(collection
.schema_registry
.validate_schema_target(
"User",
&serde_json::json!({
"id": "550e8400-e29b-41d4-a716-446655440000",
"birthday": null,
"favoriteFoods": ["pizza", "taco"],
"address": {
"city": "Austin",
"postalCode": "78701"
},
"extra": true
})
)
.is_ok());
let error = collection
.schema_registry
.validate_schema_target(
"User",
&serde_json::json!({
"id": "550e8400-e29b-41d4-a716-446655440000",
"favoriteFoods": ["pizza", "burger"],
"address": {
"city": "Austin",
"postalCode": "78701"
}
})
)
.unwrap_err();
assert_eq!(error.path, "$.favoriteFoods[1]");
assert!(matches!(
error.reason,
crate::schema::SchemaValidationErrorKind::ScalarViolation { .. }
));
}
#[test]
fn parsed_schema_declarations_validate_root_array_schemas() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
schema User {
id: UUID
}
schema Users = User[]
---
Get Users
GET https://example.com/users
^ & body === Users
"#;
let collection = parse_collection(source, working_dir).unwrap();
assert!(collection
.schema_registry
.validate_schema_target(
"Users",
&serde_json::json!([
{ "id": "550e8400-e29b-41d4-a716-446655440000" },
{ "id": "123e4567-e89b-12d3-a456-426614174000" }
])
)
.is_ok());
let error = collection
.schema_registry
.validate_schema_target(
"Users",
&serde_json::json!([
{ "id": "550e8400-e29b-41d4-a716-446655440000" },
{ "id": null }
])
)
.unwrap_err();
assert_eq!(error.path, "$[1].id");
assert!(matches!(
error.reason,
crate::schema::SchemaValidationErrorKind::TypeMismatch { .. }
));
}
#[test]
fn parse_collection_allows_forward_references_in_declarations() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
scalar HANDLE = WORD & len(3..24)
scalar WORD = string & pattern(/^[a-z]+$/)
schema User {
id: HANDLE
}
---
Get User
GET https://example.com/user/123
^ & body === User
"#;
let collection = parse_collection(source, working_dir).unwrap();
assert!(collection.schema_registry.get("HANDLE").is_some());
assert!(collection.schema_registry.get("WORD").is_some());
assert_eq!(collection.requests.len(), 1);
}
#[test]
fn parse_collection_rejects_declaration_after_first_request_delimiter() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
---
Get User
GET https://example.com/user/123
schema User {
id: UUID
}
"#;
assert!(parse_collection(source, working_dir).is_err());
}
#[test]
fn parse_collection_rejects_cyclic_schema_references() {
let working_dir = std::env::current_dir().unwrap();
let source = r#"
schema User {
address: Address
}
schema Address {
user: User
}
---
Get User
GET https://example.com/user/123
^ & body === User
"#;
let error = parse_collection(source, working_dir).unwrap_err();
assert!(error.to_string().contains("circular reference"));
assert!(error.to_string().contains("User"));
assert!(error.to_string().contains("Address"));
}
#[test]
fn inspect_collection_syntax_accepts_imported_schema_declarations() {
let temp_dir = tempfile::tempdir().unwrap();
let working_dir = temp_dir.path().to_path_buf();
let imported = working_dir.join("common.hen");
std::fs::write(
&imported,
r#"
scalar Food = enum("pizza", "taco")
schema User {
id: UUID
favoriteFoods: Food[]
}
"#,
)
.unwrap();
let source = r#"
<< ./common.hen
---
Get User
GET https://example.com/user/123
^ & body === User
"#;
let summary = inspect_collection_syntax(source, working_dir).unwrap();
assert_eq!(summary.requests.len(), 1);
assert_eq!(summary.requests[0].method, "GET");
assert_eq!(summary.requests[0].url, "https://example.com/user/123");
}
#[test]
fn parse_collection_rejects_duplicate_imported_declaration_names() {
let temp_dir = tempfile::tempdir().unwrap();
let working_dir = temp_dir.path().to_path_buf();
let imported = working_dir.join("common.hen");
std::fs::write(
&imported,
r#"
schema User {
id: UUID
}
"#,
)
.unwrap();
let source = r#"
<< ./common.hen
schema User {
id: UUID
}
---
Get User
GET https://example.com/user/123
^ & body === User
"#;
let error = parse_collection(source, working_dir).unwrap_err();
assert!(error.to_string().contains("User is already defined"));
}