use crate::document::is_http_token;
use crate::{
DocError, Expectation, PartnerExpectation, ScenarioAction, ScenarioDocument, ScenarioTarget,
ValidateExpectation, parse_scenario_document,
};
fn parse_case(text: &str) -> Result<ScenarioDocument, DocError> {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("case.test.yaml");
std::fs::write(&path, text).expect("write case file");
parse_scenario_document(&path)
}
#[test]
fn mixed_vocabulary_rejected() {
let err = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
inputs:
- to: direct:start
body: hello
"#,
)
.expect_err("parse must fail");
assert!(
matches!(err, DocError::MixedVocabulary { .. }),
"expected MixedVocabulary, got {err}"
);
assert!(
err.to_string().contains("doc-validation"),
"error must name the doc-validation class: {err}"
);
}
#[test]
fn empty_scenario_rejected() {
let err = parse_case(
r#"
routeFiles: [routes.yaml]
scenario: []
"#,
)
.expect_err("parse must fail");
assert!(
matches!(err, DocError::Validation { .. }),
"expected Validation, got {err}"
);
let display = err.to_string();
assert!(
display.contains("doc-validation"),
"error must name the doc-validation class: {display}"
);
assert!(
display.contains("scenario"),
"error must name the `scenario` section: {display}"
);
}
#[test]
fn scenario_with_expects_rejected() {
let err = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
expects:
mock:result:
count: 1
"#,
)
.expect_err("parse must fail");
assert!(
matches!(err, DocError::MixedVocabulary { .. }),
"expected MixedVocabulary, got {err}"
);
}
#[test]
fn receive_without_deadline_rejected() {
let err = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
- receive:
from: http://127.0.0.1:18080/hook
"#,
)
.expect_err("parse must fail");
match err {
DocError::Validation { index, message } => {
assert_eq!(index, 1, "error must name the action index");
assert!(
message.contains("deadline"),
"message must name the missing deadline: {message}"
);
}
other => panic!("expected Validation, got {other}"),
}
}
#[test]
fn send_method_explicit_put_resolves() {
let doc = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
method: PUT
"#,
)
.expect("parse must succeed");
let action = doc.scenario.first().expect("one action");
match action {
ScenarioAction::Send { method, .. } => {
assert_eq!(method, "PUT", "explicit method must resolve verbatim");
}
other => panic!("expected Send, got {other:?}"),
}
}
#[test]
fn send_method_inferred_post_with_body() {
let doc = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: http://127.0.0.1:9999/hook
body: hello
"#,
)
.expect("parse must succeed");
let action = doc.scenario.first().expect("one action");
match action {
ScenarioAction::Send { method, .. } => {
assert_eq!(method, "POST", "a send with a body infers POST");
}
other => panic!("expected Send, got {other:?}"),
}
}
#[test]
fn send_method_inferred_get_bodyless() {
let doc = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: http://127.0.0.1:9999/hook
"#,
)
.expect("parse must succeed");
let action = doc.scenario.first().expect("one action");
match action {
ScenarioAction::Send { method, .. } => {
assert_eq!(method, "GET", "a bodyless send infers GET");
}
other => panic!("expected Send, got {other:?}"),
}
}
#[test]
fn send_method_lowercase_normalizes() {
let doc = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: http://127.0.0.1:9999/hook
method: delete
"#,
)
.expect("parse must succeed");
let action = doc.scenario.first().expect("one action");
match action {
ScenarioAction::Send { method, .. } => {
assert_eq!(method, "DELETE", "method must normalize to uppercase");
}
other => panic!("expected Send, got {other:?}"),
}
}
#[test]
fn send_method_invalid_token_is_doc_validation() {
let err = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
method: "P UT"
"#,
)
.expect_err("parse must fail");
let rendered = err.to_string();
match err {
DocError::Validation { index, message } => {
assert_eq!(index, 0, "error must name the action index");
assert!(
message.contains("HTTP method name"),
"message must name the HTTP method name requirement: {message}"
);
assert!(
message.contains("P UT"),
"message must name the offending value: {message}"
);
}
other => panic!("expected Validation, got {other}"),
}
assert!(
rendered.contains("doc-validation"),
"rendered error must name the doc-validation class: {rendered}"
);
}
#[test]
fn send_method_token_predicate_accepts_and_rejects() {
for accepted in ["PUT", "X-Custom", "PATCH2"] {
assert!(
is_http_token(accepted),
"predicate must accept `{accepted}`"
);
}
for rejected in ["", "P UT", "PUT/", "put;", "café"] {
assert!(
!is_http_token(rejected),
"predicate must reject `{rejected}`"
);
}
}
#[test]
fn scenario_with_env_accepted() {
let doc = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
env:
HTTP_PORT: "18080"
"#,
)
.expect("parse must succeed");
let env = doc.env.expect("env map must be present");
assert_eq!(
env.get("HTTP_PORT").map(String::as_str),
Some("18080"),
"env map must carry HTTP_PORT"
);
}
#[test]
fn reserved_provisioning_rejected() {
for value in ["testcontainer", "user-provided"] {
let text = format!(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to:
endpoint: http://127.0.0.1:9999/hook
provisioning: {value}
"#
);
let err = parse_case(&text).expect_err("parse must fail");
let rendered = err.to_string();
match err {
DocError::UnsupportedProvisioning {
value: seen,
endpoint,
} => {
assert_eq!(seen, value, "error must name the reserved value");
assert_eq!(
endpoint, "http://127.0.0.1:9999/hook",
"error must name the endpoint"
);
}
other => panic!("expected UnsupportedProvisioning, got {other}"),
}
assert!(
rendered.contains(value),
"rendered error must name the value: {rendered}"
);
}
}
#[test]
fn validate_last_received_target_parses() {
let doc = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- validate:
target:
lastReceived: http://127.0.0.1:9999/hook
expectation:
equals: ok
"#,
)
.expect("parse must succeed");
let action = doc.scenario.first().expect("one action");
match action {
ScenarioAction::Validate {
target,
expectation,
..
} => {
match target {
ScenarioTarget::LastReceived(endpoint) => assert_eq!(
endpoint.endpoint, "http://127.0.0.1:9999/hook",
"target must keep the endpoint reference"
),
other => panic!("expected LastReceived, got {other:?}"),
}
assert_eq!(
expectation,
&ValidateExpectation::Message(Expectation::Equals(camel_api::Value::String(
"ok".into()
))),
"expectation must parse as a literal equals"
);
}
other => panic!("expected Validate, got {other:?}"),
}
}
#[test]
fn validate_variable_target_parses() {
let doc = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- validate:
target:
variable: AUTH_TOKEN
expectation:
regex: "^Bearer .+$"
"#,
)
.expect("parse must succeed");
let action = doc.scenario.first().expect("one action");
match action {
ScenarioAction::Validate {
target,
expectation,
..
} => {
assert_eq!(
target,
&ScenarioTarget::Variable("AUTH_TOKEN".to_string()),
"target must parse as a scenario variable"
);
assert_eq!(
expectation,
&ValidateExpectation::Message(Expectation::Regex("^Bearer .+$".to_string())),
"expectation must parse as a regex matcher"
);
}
other => panic!("expected Validate, got {other:?}"),
}
}
#[test]
fn invalid_regex_rejected_at_load() {
let err = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- validate:
target:
variable: AUTH_TOKEN
expectation:
regex: "[unclosed"
"#,
)
.expect_err("parse must fail");
let rendered = err.to_string();
match err {
DocError::Validation { index, ref message } => {
assert_eq!(index, 0, "error must name the action index");
assert!(
message.contains("regex"),
"message must name the regex problem: {message}"
);
}
other => panic!("expected Validation, got {other}"),
}
assert!(
rendered.contains("doc-validation"),
"rendered error must name the doc-validation class: {rendered}"
);
}
#[test]
fn reserved_env_key_rejected() {
let err = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to:
endpoint: http://127.0.0.1:9999/hook
provisioning: harness
bindVar: PARTNER
env:
PARTNER: http://127.0.0.1:9999
"#,
)
.expect_err("parse must fail");
match err {
DocError::ReservedEnvKey { key, endpoint } => {
assert_eq!(key, "PARTNER", "error must name the reserved key");
assert_eq!(
endpoint, "http://127.0.0.1:9999/hook",
"error must name the endpoint that reserved the key"
);
}
other => panic!("expected ReservedEnvKey, got {other}"),
}
}
#[test]
fn partner_target_with_count_parses() {
let doc = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to:
endpoint: http://127.0.0.1:0/order
provisioning: harness
bindVar: PARTNER_URL
- validate:
target:
partner: http://127.0.0.1:0/order
expectation:
count: 3
method: POST
"#,
)
.expect("parse must succeed");
let action = doc.scenario.get(1).expect("two actions");
match action {
ScenarioAction::Validate {
target,
expectation,
deadline,
} => {
match target {
ScenarioTarget::Partner(endpoint) => assert_eq!(
endpoint.endpoint, "http://127.0.0.1:0/order",
"target must keep the partner endpoint reference"
),
other => panic!("expected Partner, got {other:?}"),
}
assert_eq!(
expectation,
&ValidateExpectation::Partner(PartnerExpectation {
count: 3,
method: Some("POST".to_string()),
path: None,
}),
"expectation must parse as a partner count with a method filter"
);
assert!(deadline.is_none(), "absent deadline means one snapshot");
}
other => panic!("expected Validate, got {other:?}"),
}
}
#[test]
fn undeclared_partner_target_is_load_error() {
let err = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
- validate:
target:
partner: http://127.0.0.1:9999/nowhere
expectation:
count: 1
"#,
)
.expect_err("parse must fail");
let rendered = err.to_string();
match err {
DocError::Validation { index, message } => {
assert_eq!(index, 1, "error must name the action index");
assert!(
message.contains("http://127.0.0.1:9999/nowhere"),
"message must name the unmatched URI: {message}"
);
}
other => panic!("expected Validation, got {other}"),
}
assert!(
rendered.contains("doc-validation"),
"rendered error must name the doc-validation class: {rendered}"
);
}
#[test]
fn deadline_on_lastreceived_is_load_error() {
let err = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- validate:
target:
lastReceived: http://127.0.0.1:9999/hook
expectation:
equals: ok
deadline: 5s
"#,
)
.expect_err("parse must fail");
match err {
DocError::Validation { index, message } => {
assert_eq!(index, 0, "error must name the action index");
assert!(
message.contains("deadline"),
"message must name `deadline`: {message}"
);
}
other => panic!("expected Validation, got {other}"),
}
}
#[test]
fn missing_count_is_load_error() {
let err = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to:
endpoint: http://127.0.0.1:0/order
provisioning: harness
- validate:
target:
partner: http://127.0.0.1:0/order
expectation:
method: POST
"#,
)
.expect_err("parse must fail");
match err {
DocError::Validation { index, message } => {
assert_eq!(index, 1, "error must name the action index");
assert!(
message.contains("count"),
"message must name `count`: {message}"
);
}
other => panic!("expected Validation, got {other}"),
}
}
#[test]
fn negative_count_is_load_error() {
let err = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to:
endpoint: http://127.0.0.1:0/order
provisioning: harness
- validate:
target:
partner: http://127.0.0.1:0/order
expectation:
count: -1
"#,
)
.expect_err("parse must fail");
match err {
DocError::Validation { index, message } => {
assert_eq!(index, 1, "error must name the action index");
assert!(
message.contains("count"),
"message must name `count`: {message}"
);
}
other => panic!("expected Validation, got {other}"),
}
}
#[test]
fn unknown_expectation_field_is_load_error() {
let err = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to:
endpoint: http://127.0.0.1:0/order
provisioning: harness
- validate:
target:
partner: http://127.0.0.1:0/order
expectation: {count: 1, duration: 5s}
"#,
)
.expect_err("parse must fail");
match err {
DocError::Validation { index, message } => {
assert_eq!(index, 1, "error must name the action index");
assert!(
message.contains("duration"),
"message must name the unknown field `duration`: {message}"
);
}
other => panic!("expected Validation, got {other}"),
}
}
#[test]
fn unparseable_deadline_is_load_error() {
let err = parse_case(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to:
endpoint: http://127.0.0.1:0/order
provisioning: harness
- validate:
target:
partner: http://127.0.0.1:0/order
expectation:
count: 1
deadline: 5x
"#,
)
.expect_err("parse must fail");
match err {
DocError::Validation { index, message } => {
assert_eq!(index, 1, "error must name the action index");
assert!(
message.contains("deadline"),
"message must name `deadline`: {message}"
);
}
other => panic!("expected Validation, got {other}"),
}
}