use std::fs;
use std::path::PathBuf;
use camel_cli::commands::test::document::InputBody;
use camel_cli::commands::test::document::TestDocError;
use camel_cli::commands::test::document::parse_test_document;
use camel_cli::commands::test::runner::{TestDocResult, run_test_doc};
fn temp_dir(tag: &str) -> PathBuf {
let dir =
std::env::temp_dir().join(format!("camel-test-doc-ident-{tag}-{}", std::process::id()));
fs::create_dir_all(&dir).expect("create temp dir"); dir
}
fn assert_green(result: &TestDocResult, expected_endpoints: usize) {
assert!(
result.doc_error.is_none(),
"doc_error: {:?}",
result.doc_error
);
assert_eq!(result.endpoint_results.len(), expected_endpoints);
for er in &result.endpoint_results {
assert!(
er.outcome.is_ok(),
"endpoint {} failed: {:?}",
er.endpoint,
er.outcome
);
}
}
#[test]
fn repository_key_default_resolves() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
expects:
mock:out:
count: 1
repositories:
cache:
"${env:CACHE_REPO_NAME:-persistent}": memory
"#;
let doc = parse_test_document(yaml).expect("document should parse"); let stubs = doc.repository_stubs().expect("repositories block declared"); let cache = stubs.cache.as_ref().expect("cache map declared"); assert!(
cache.contains_key("persistent"),
"cache keys must contain the resolved default `persistent`, got: {cache:?}"
);
assert!(
!cache.keys().any(|key| key.contains("${env:")),
"cache keys must not keep raw placeholders, got: {cache:?}"
);
}
#[test]
fn bean_key_default_resolves() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
expects:
mock:out:
count: 1
beans:
"${env:BEAN_NAME:-audit}":
kind: echo
"#;
let doc = parse_test_document(yaml).expect("document should parse"); let beans = doc.bean_decls().expect("beans block declared"); assert!(
beans.contains_key("audit"),
"bean names must contain the resolved default `audit`, got: {beans:?}"
);
}
#[test]
fn repository_no_default_fails_naming_var() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
expects:
mock:out:
count: 1
repositories:
cache:
"${env:NO_SUCH_VAR}": memory
"#;
let err = parse_test_document(yaml).expect_err("unresolved placeholder must fail parse"); let display = err.to_string();
match err {
TestDocError::EnvUnresolved { var, field } => {
assert_eq!(var, "NO_SUCH_VAR");
assert_eq!(field, "repositories.cache");
}
other => panic!("expected EnvUnresolved, got: {other:?}"),
}
assert!(
display.contains(
"Environment variable 'NO_SUCH_VAR' not set (required by repositories.cache)"
),
"Display must mirror the route-side wording, got: {display}"
);
assert!(
!display.contains("memory"),
"Display must not leak the stub target, got: {display}"
);
}
#[test]
fn empty_default_blank_name_rejected() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
expects:
mock:out:
count: 1
repositories:
cache:
"${env:NAME:-}": memory
"#;
let err = parse_test_document(yaml).expect_err("blank resolved name must be rejected"); let TestDocError::InvalidRepositories(msg) = err else {
panic!("expected InvalidRepositories, got: {err:?}");
};
assert!(
msg.contains("non-blank"),
"the blank guard must see the resolved empty name, got: {msg}"
);
}
#[test]
fn memory_default_rejected() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
expects:
mock:out:
count: 1
repositories:
cache:
"${env:NAME:-memory}": memory
"#;
let err = parse_test_document(yaml).expect_err("`memory` resolved name must be rejected"); let display = err.to_string();
assert!(
display.contains("built-in repository name"),
"the built-in guard must see the resolved `memory` name, got: {display}"
);
}
#[test]
fn repository_key_collision_rejected() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
expects:
mock:out:
count: 1
repositories:
cache:
"${env:A:-x}": memory
x: memory
"#;
let err = parse_test_document(yaml).expect_err("collision must fail parse"); let TestDocError::InvalidRepositories(msg) = err else {
panic!("expected InvalidRepositories, got: {err:?}");
};
assert!(
msg.contains("duplicate repository name"),
"error must name the collision, got: {msg}"
);
assert!(
msg.contains("`x`"),
"error must name the resolved `x`, got: {msg}"
);
}
#[test]
fn bean_key_collision_rejected() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
expects:
mock:out:
count: 1
beans:
"${env:B:-audit}":
kind: echo
audit:
kind: echo
"#;
let err = parse_test_document(yaml).expect_err("bean collision must fail parse"); let TestDocError::InvalidBeans(msg) = err else {
panic!("expected InvalidBeans, got: {err:?}");
};
assert!(
msg.contains("duplicate bean name"),
"error must name the collision, got: {msg}"
);
assert!(
msg.contains("`audit`"),
"error must name the resolved `audit`, got: {msg}"
);
}
#[test]
fn intercept_source_and_target_default_resolve() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
inputs:
- to: "direct:in"
expects:
mock:out:
count: 1
intercepts:
"direct:${env:TARGET:-archive}":
skipTo: "mock:${env:SINK:-skipped}"
"#;
let doc = parse_test_document(yaml).expect("document should parse"); let intercepts = doc.intercepts.as_ref().expect("intercepts block declared"); let action = intercepts
.get("direct:archive")
.expect("resolved source key `direct:archive` present"); assert_eq!(
action.skip_to.as_deref(),
Some("mock:skipped"),
"the skipTo target must carry the resolved default, got: {action:?}"
);
assert!(
!intercepts.keys().any(|key| key.contains("${env:")),
"intercepts keys must not keep raw placeholders, got: {intercepts:?}"
);
}
#[test]
fn expects_and_sequence_mock_refs_resolve() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
expects:
"mock:${env:EP:-result}":
count: 1
"mock:${env:EP2:-other}":
count: 1
sequence:
- "mock:${env:EP:-result}"
- "mock:${env:EP2:-other}"
"#;
let doc = parse_test_document(yaml).expect("document should parse"); assert_eq!(doc.expects.len(), 2, "both expectations declared");
assert!(
doc.expects.contains_key("result"),
"expects keys must contain resolved bare `result`, got: {:?}",
doc.expects.keys().collect::<Vec<_>>()
);
assert!(
doc.expects.contains_key("other"),
"expects keys must contain resolved bare `other`, got: {:?}",
doc.expects.keys().collect::<Vec<_>>()
);
let sequence = doc.sequence.as_ref().expect("sequence declared"); assert_eq!(
sequence,
&["result".to_string(), "other".to_string()],
"sequence entries must resolve in place, got: {sequence:?}"
);
}
#[test]
fn input_to_default_resolves() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
inputs:
- to: "direct:${env:IN:-start}"
expects:
mock:out:
count: 1
"#;
let doc = parse_test_document(yaml).expect("document should parse"); assert_eq!(
doc.inputs[0].to, "direct:start",
"the input target must carry the resolved default, got: {:?}",
doc.inputs[0].to
);
}
#[test]
fn intercept_no_default_fails() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
inputs:
- to: "direct:in"
expects:
mock:out:
count: 1
intercepts:
"${env:SRC}":
skipTo: "mock:skipped"
"#;
let err = parse_test_document(yaml).expect_err("unresolved placeholder must fail parse"); let TestDocError::EnvUnresolved { var, field } = err else {
panic!("expected EnvUnresolved, got: {err:?}");
};
assert_eq!(var, "SRC");
assert_eq!(field, "intercepts");
}
#[test]
fn intercept_key_collision_rejected() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
inputs:
- to: "direct:in"
expects:
mock:out:
count: 1
intercepts:
"${env:T:-direct:archive}":
skipTo: "mock:skipped"
"direct:archive":
skipTo: "mock:skipped"
"#;
let err = parse_test_document(yaml).expect_err("collision must fail parse"); let TestDocError::InterceptInvalid(msg) = err else {
panic!("expected InterceptInvalid, got: {err:?}");
};
assert!(
msg.contains("duplicate source"),
"error must name the collision, got: {msg}"
);
assert!(
msg.contains("`direct:archive`"),
"error must name the resolved source `direct:archive`, got: {msg}"
);
}
#[test]
fn expects_collision_rejected() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
expects:
"mock:${env:A:-x}":
count: 1
"mock:x":
count: 1
"#;
let err = parse_test_document(yaml).expect_err("expects collision must fail parse"); let TestDocError::Yaml(msg) = err else {
panic!("expected Yaml, got: {err:?}");
};
assert!(
msg.contains("duplicate expectation endpoint"),
"error must name the collision, got: {msg}"
);
assert!(
msg.contains("`mock:x`"),
"error must name the FULL resolved key `mock:x`, got: {msg}"
);
}
#[test]
fn intercept_key_error_precedes_target_error() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
inputs:
- to: "direct:in"
expects:
mock:out:
count: 1
intercepts:
"${env:SRC}":
skipTo: "mock:${env:SINK}"
"#;
let err = parse_test_document(yaml).expect_err("unresolved key must fail parse first"); let TestDocError::EnvUnresolved { var, field } = err else {
panic!("expected EnvUnresolved, got: {err:?}");
};
assert_eq!(var, "SRC");
assert_eq!(field, "intercepts");
}
#[tokio::test(flavor = "multi_thread")]
async fn rc4hexo_repro_repository_matches_route() {
let dir = temp_dir("rc4hexo");
fs::write(
dir.join("route.yaml"),
r#"
routes:
- id: r1
from: "direct:in"
steps:
- cache:
repository: "${env:CACHE_REPO_NAME:-persistent}"
key: k
on_miss:
- to: "mock:out"
"#,
)
.expect("write route.yaml"); let yaml = r#"
routeFiles: [route.yaml]
inputs:
- to: "direct:in"
body: "x"
expects:
mock:out:
count: 1
repositories:
cache:
"${env:CACHE_REPO_NAME:-persistent}": memory
"#;
let doc = parse_test_document(yaml).expect("document should parse"); let (result, _) = run_test_doc(&doc, &dir).await;
assert_green(&result, 1);
}
#[tokio::test(flavor = "multi_thread")]
async fn bean_key_matches_route_e2e() {
let dir = temp_dir("bean-e2e");
fs::write(
dir.join("route.yaml"),
r#"
routes:
- id: r1
from: "direct:start"
steps:
- bean:
name: "${env:BEAN_NAME:-audit}"
method: handle
- to: "mock:out"
"#,
)
.expect("write route.yaml"); let yaml = r#"
routeFiles: [route.yaml]
inputs:
- to: "direct:start"
body: "x"
expects:
mock:out:
count: 1
beans:
"${env:BEAN_NAME:-audit}":
kind: echo
"#;
let doc = parse_test_document(yaml).expect("document should parse"); let (result, _) = run_test_doc(&doc, &dir).await;
assert_green(&result, 1);
}
#[tokio::test(flavor = "multi_thread")]
async fn intercept_applies_to_resolved_uri_e2e() {
let dir = temp_dir("intercept-e2e");
fs::write(
dir.join("route.yaml"),
r#"
routes:
- id: sender
from: "direct:start"
steps:
- to: "direct:${env:TARGET:-archive}"
- id: downstream
from: "direct:archive"
steps:
- to: "mock:real"
"#,
)
.expect("write route.yaml"); let yaml = r#"
routeFiles: [route.yaml]
inputs:
- to: "direct:start"
body: "x"
intercepts:
"direct:${env:TARGET:-archive}":
skipTo: "mock:${env:SINK:-skipped}"
expects:
mock:skipped:
count: 1
mock:real:
maxCount: 0
"#;
let doc = parse_test_document(yaml).expect("document should parse"); let (result, _) = run_test_doc(&doc, &dir).await;
assert_green(&result, 2);
}
#[tokio::test(flavor = "multi_thread")]
async fn escaped_placeholder_key_stays_literal() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
expects:
mock:out:
count: 1
repositories:
cache:
"$${env:CACHE_REPO_NAME:-persistent}": memory
"#;
let doc = parse_test_document(yaml).expect("escaped document should parse"); let stubs = doc.repository_stubs().expect("repositories block declared"); let cache = stubs.cache.as_ref().expect("cache map declared"); let keys: Vec<_> = cache.keys().collect();
assert_eq!(
keys,
vec!["${env:CACHE_REPO_NAME:-persistent}"],
"the cache map must carry exactly the single-$ literal key, got: {cache:?}"
);
let dir = temp_dir("escaped-e2e");
fs::write(
dir.join("route.yaml"),
r#"
routes:
- id: r1
from: "direct:in"
steps:
- cache:
repository: "$${env:CACHE_REPO_NAME:-persistent}"
key: k
on_miss:
- to: "mock:out"
"#,
)
.expect("write route.yaml"); let e2e = r#"
routeFiles: [route.yaml]
inputs:
- to: "direct:in"
body: "x"
expects:
mock:out:
count: 1
repositories:
cache:
"$${env:CACHE_REPO_NAME:-persistent}": memory
"#;
let doc = parse_test_document(e2e).expect("escaped e2e document should parse"); let (result, _) = run_test_doc(&doc, &dir).await;
assert_green(&result, 1);
}
#[tokio::test(flavor = "multi_thread")]
async fn assertion_data_stays_literal() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
inputs:
- to: "direct:in"
body: "${env:PAYLOAD:-leaked}"
headers:
X-T: "${env:T:-v}"
expectReply:
body: "${env:PAYLOAD:-leaked}"
expects:
mock:out:
bodies: ["${env:PAYLOAD:-leaked}"]
beans:
failer:
kind: fail
config:
message: "${env:MSG:-m}"
methods: ["${env:M:-run}"]
"#;
let doc = parse_test_document(yaml).expect("document should parse"); match &doc.inputs[0].body {
Some(InputBody::Text(text)) => {
assert_eq!(
text, "${env:PAYLOAD:-leaked}",
"the input body must stay the raw text"
);
}
other => panic!("expected InputBody::Text, got: {other:?}"),
}
let headers = doc.inputs[0].headers.as_ref().expect("headers declared"); assert!(
format!("{:?}", headers.get("X-T")).contains("${env:T:-v}"),
"the header value must still carry the raw text, got: {headers:?}"
);
let reply = doc.inputs[0]
.expect_reply
.as_ref()
.expect("expectReply declared"); let reply_body = reply.body.as_ref().expect("reply body matcher declared"); assert!(
format!("{reply_body:?}").contains("${env:PAYLOAD:-leaked}"),
"the reply body matcher must still target the raw text, got: {reply_body:?}"
);
let set = doc.expects.values().next().expect("expects declared"); let bodies = set.bodies.as_ref().expect("bodies matcher declared"); assert!(
format!("{:?}", bodies[0]).contains("${env:PAYLOAD:-leaked}"),
"the bodies matcher must still target the raw text, got: {:?}",
bodies[0]
);
let beans = doc.bean_decls().expect("beans block declared"); let failer = beans.get("failer").expect("bean `failer` declared"); let config = failer.config.as_ref().expect("bean config declared"); assert_eq!(
config.get("message"),
Some(&"${env:MSG:-m}".to_string()),
"the bean config value must stay the raw text, got: {config:?}"
);
let methods = failer.methods.as_ref().expect("bean methods declared"); assert_eq!(
methods,
&["${env:M:-run}".to_string()],
"the bean methods entries must stay raw text, got: {methods:?}"
);
let dir = temp_dir("assertion-literal");
let (result, _) = run_test_doc(&doc, &dir).await;
assert_green(&result, 2);
}
#[test]
fn settle_placeholder_not_interpolated() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
expects:
mock:out:
count: 1
settle: "${env:S:-500ms}"
"#;
let err = parse_test_document(yaml).expect_err("raw placeholder must not become a duration"); let TestDocError::SettleOutOfRange(raw) = err else {
panic!("expected SettleOutOfRange, got: {err:?}");
};
assert_eq!(
raw, "${env:S:-500ms}",
"the error must carry the raw text, not a substituted duration"
);
}
#[test]
fn stub_target_not_interpolated() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
expects:
mock:out:
count: 1
repositories:
cache:
ok-name: "${env:TGT:-memory}"
"#;
let err = parse_test_document(yaml).expect_err("raw target text must stay unsupported"); let TestDocError::InvalidRepositories(msg) = err else {
panic!("expected InvalidRepositories, got: {err:?}");
};
assert!(
msg.contains("unsupported stub target"),
"the target guard must reject the raw text, got: {msg}"
);
assert!(
msg.contains("${env:TGT:-memory}"),
"the error must name the RAW target text, got: {msg}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn route_files_paths_never_interpolate() {
let yaml = r#"
routeFiles: ["${env:ROUTE_DIR:-routes}/demo.yaml"]
expects:
mock:out:
count: 1
"#;
let doc = parse_test_document(yaml).expect("document should parse"); assert_eq!(
doc.route_files.as_ref().expect("routeFiles declared"), &["${env:ROUTE_DIR:-routes}/demo.yaml".to_string()],
"routeFiles must carry the literal entry"
);
let dir = temp_dir("routefiles-literal");
let (result, _) = run_test_doc(&doc, &dir).await;
let err = result.doc_error.expect("route load must fail"); assert!(
err.contains("${env:ROUTE_DIR:-routes}/demo.yaml"),
"the run must fail naming the literal path, got: {err}"
);
let yaml = r#"
routeFilesFromRoot: ["${env:ROOT_DIR:-cfg}/r.yaml"]
expects:
mock:out:
count: 1
"#;
let doc = parse_test_document(yaml).expect("document should parse"); assert_eq!(
doc.route_files_from_root
.as_ref()
.expect("routeFilesFromRoot declared"), &["${env:ROOT_DIR:-cfg}/r.yaml".to_string()],
"routeFilesFromRoot must carry the literal entry"
);
}
#[test]
fn env_non_string_value_rejected() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
expects:
mock:out:
count: 1
env:
CB_MS: 500
"#;
let err = parse_test_document(yaml).expect_err("non-string env value must fail parse"); let display = err.to_string();
assert!(
!display.contains("unknown field"),
"the `env` key must be a known field, got: {display}"
);
assert!(
display.contains("env.CB_MS"),
"serde error must locate the `env` field, got: {display}"
);
assert!(
display.contains("expected string"),
"serde error must state a string was expected, got: {display}"
);
}
#[test]
fn env_no_default_identifier_resolves() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
expects:
mock:out:
count: 1
env:
BEAN_NAME: audit
beans:
"${env:BEAN_NAME}":
kind: echo
"#;
let doc = parse_test_document(yaml).expect("document should parse"); let beans = doc.bean_decls().expect("beans block declared"); assert!(
beans.contains_key("audit"),
"bean names must contain the doc-env-resolved `audit`, got: {beans:?}"
);
}
#[test]
fn env_steers_identifier_key() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
expects:
mock:out:
count: 1
env:
CACHE_REPO_NAME: faststub
repositories:
cache:
"${env:CACHE_REPO_NAME:-persistent}": memory
"#;
let doc = parse_test_document(yaml).expect("document should parse"); let stubs = doc.repository_stubs().expect("repositories block declared"); let cache = stubs.cache.as_ref().expect("cache map declared"); assert!(
cache.contains_key("faststub"),
"cache keys must contain the doc-env-resolved `faststub`, got: {cache:?}"
);
}
#[test]
fn env_two_keys_collide_via_doc_values() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
expects:
mock:out:
count: 1
env:
A: y
repositories:
cache:
"${env:A:-x}": memory
y: memory
"#;
let err = parse_test_document(yaml).expect_err("collision must fail parse"); let TestDocError::InvalidRepositories(msg) = err else {
panic!("expected InvalidRepositories, got: {err:?}");
};
assert!(
msg.contains("repositories.cache"),
"error must name the map, got: {msg}"
);
assert!(
msg.contains("`y`"),
"error must name the doc-env-resolved `y`, got: {msg}"
);
}
#[test]
fn env_value_text_never_scanned_identifier() {
let yaml = r#"
routes:
- id: r1
from: "direct:in"
steps:
- to: "mock:out"
expects:
mock:out:
count: 1
env:
A: "${env:B}"
beans:
"${env:A:-d}":
kind: echo
"#;
let doc = parse_test_document(yaml).expect("document should parse"); let beans = doc.bean_decls().expect("beans block declared"); let keys: Vec<_> = beans.keys().collect();
assert_eq!(
keys,
vec!["${env:B}"],
"the bean key must be the literal env value text, got: {beans:?}"
);
}