use super::*;
use camel_integration_test::parse_scenario_document;
use std::fs;
use std::path::PathBuf;
struct TempProject(PathBuf);
impl TempProject {
fn new(tag: &str) -> Self {
let dir =
std::env::temp_dir().join(format!("camel-cli-scenario-{tag}-{}", std::process::id()));
fs::create_dir_all(&dir).expect("create temp project root"); Self(dir)
}
fn root(&self) -> &Path {
&self.0
}
}
impl Drop for TempProject {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn write_project(root: &Path, doc_yaml: &str) -> PathBuf {
write_project_with_camel_toml(root, "log_level = \"info\"\n", doc_yaml)
}
fn write_project_with_camel_toml(root: &Path, camel_toml: &str, doc_yaml: &str) -> PathBuf {
fs::write(root.join("Camel.toml"), camel_toml).expect("write Camel.toml"); fs::write(
root.join("routes.yaml"),
"routes:\n - id: noop\n from: direct:start\n steps:\n - log: \"noop\"\n",
)
.expect("write routes.yaml"); let doc_path = root.join("scenario.test.yaml");
fs::write(&doc_path, doc_yaml).expect("write scenario doc"); doc_path
}
#[test]
fn partner_scripts_map_defaults() {
let _wasm_acks_guard = crate::commands::run::WASM_ACKS_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let project = TempProject::new("partner-scripts-map-defaults");
let doc_path = write_project(
project.root(),
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
partners:
http://127.0.0.1:0/orders:
- method: POST
path: /orders
response:
body:
id: ord-7
"#,
);
let doc = parse_scenario_document(&doc_path).expect("parse scenario doc"); let scripts = partner_scripts_for(&doc, "http://127.0.0.1:0/orders")
.expect("the declared entry must map"); assert_eq!(scripts.len(), 1, "the entry carries one script");
let scripted = &scripts[0];
assert_eq!(scripted.method.as_deref(), Some("POST"));
assert_eq!(scripted.path.as_deref(), Some("/orders"));
assert_eq!(scripted.status, 200, "absent status defaults to 200");
assert!(
scripted.headers.is_empty(),
"absent headers default to empty"
);
assert_eq!(
scripted.body,
br#"{"id":"ord-7"}"#.to_vec(),
"the body must be the JSON serialization"
);
}
#[test]
fn partner_scripts_none_when_absent() {
let _wasm_acks_guard = crate::commands::run::WASM_ACKS_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let project = TempProject::new("partner-scripts-none");
let doc_path = write_project(
project.root(),
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
"#,
);
let doc = parse_scenario_document(&doc_path).expect("parse scenario doc"); assert!(
partner_scripts_for(&doc, "http://127.0.0.1:0/orders").is_none(),
"a document without a partners entry maps to None (caller binds permissive)"
);
}
#[tokio::test(flavor = "multi_thread")]
#[allow(clippy::await_holding_lock)]
async fn driver_binds_permissive_when_partners_absent() {
let _wasm_acks_guard = crate::commands::run::WASM_ACKS_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let project = TempProject::new("driver-binds-permissive");
let root = project.root();
let doc_path = write_project(
root,
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to:
endpoint: http://127.0.0.1:0/orders
provisioning: harness
- receive:
from:
endpoint: http://127.0.0.1:0/orders
provisioning: harness
deadline: 2s
extract:
status: status
body: body
- validate:
target: { variable: status }
expectation: 200
- validate:
target: { variable: body }
expectation: ""
"#,
);
let doc = parse_scenario_document(&doc_path).expect("parse scenario doc"); let result = run_scenario_full_boot(&doc, root).await;
assert_eq!(result.doc_error, None, "permissive bind must not error");
assert!(!result.apparatus, "no apparatus failure is expected");
assert_eq!(result.action_results.len(), 4, "every action must run");
for row in &result.action_results {
assert!(
row.outcome.is_ok(),
"send + receive + validates must pass against the permissive 200 empty partner: {:?}",
row.outcome
);
}
}
#[tokio::test(flavor = "multi_thread")]
#[allow(clippy::await_holding_lock)]
async fn driver_binds_no_partner_for_plain_strings() {
let _wasm_acks_guard = crate::commands::run::WASM_ACKS_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let target = camel_integration_test::HttpPartner::start_permissive(200)
.await
.expect("bind the test-owned listener"); let uri = format!("http://{}/x", target.bound_addr());
let project = TempProject::new("driver-plain-string");
let root = project.root();
let doc_path = write_project(
root,
&format!(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: {uri}
- receive:
from: {uri}
deadline: 2s
"#
),
);
let doc = parse_scenario_document(&doc_path).expect("parse scenario doc");
let wired = wire_endpoint_refs(&doc);
let (adapters, harness_provisioned) = bind_partners(&doc, &wired)
.await
.expect("bind step must succeed"); assert!(
!adapters.contains_key(&uri),
"a plain-string ref must not get a partner listener"
);
assert!(
harness_provisioned.is_empty(),
"no harness bind means no env-tier binding"
);
let result = run_scenario_full_boot(&doc, root).await;
assert_eq!(result.doc_error, None, "the plain-string send must dial");
assert_eq!(result.action_results.len(), 2, "send + receive must run");
for row in &result.action_results {
assert!(
row.outcome.is_ok(),
"the send must reach the literal URI and the receive must read the roundtrip: {:?}",
row.outcome
);
}
let recorded = target.recorder().recorded_requests();
assert_eq!(
recorded.len(),
1,
"exactly one wire arrival on the literal URI"
);
assert_eq!(recorded[0].path, "/x");
}
#[tokio::test(flavor = "multi_thread")]
#[allow(clippy::await_holding_lock)]
async fn partners_key_typo_fails_load() {
let _wasm_acks_guard = crate::commands::run::WASM_ACKS_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let project = TempProject::new("partners-key-typo");
let doc_path = write_project(
project.root(),
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to:
endpoint: http://127.0.0.1:0/orders
provisioning: harness
partners:
http://127.0.0.1:0/order:
- method: POST
response:
status: 201
"#,
);
let mut out = Vec::new();
let mut err = Vec::new();
let summary = super::super::run_tests(&[doc_path], &mut out, &mut err).await;
assert_eq!(
summary.exit_code, 2,
"doc-validation is apparatus class, exit 2"
);
let err = String::from_utf8(err).expect("stderr is utf-8"); assert!(
err.contains("doc-validation"),
"doc-validation class: {err}"
);
assert!(
err.contains("http://127.0.0.1:0/order"),
"the error must name the unmatched key: {err}"
);
assert!(
!err.contains("partner-bind"),
"the cross-check must fail before any partner binds: {err}"
);
}
#[tokio::test(flavor = "multi_thread")]
#[allow(clippy::await_holding_lock)]
async fn undeclared_partner_target_exits_two() {
let _wasm_acks_guard = crate::commands::run::WASM_ACKS_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let project = TempProject::new("undeclared-partner-target");
let doc_path = write_project(
project.root(),
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
- validate:
target: {partner: http://127.0.0.1:0/nowhere}
expectation: {count: 1}
"#,
);
let mut out = Vec::new();
let mut err = Vec::new();
let summary = super::super::run_tests(&[doc_path], &mut out, &mut err).await;
assert_eq!(
summary.exit_code, 2,
"doc-validation is apparatus class, exit 2"
);
let err = String::from_utf8(err).expect("stderr is utf-8"); assert!(
err.contains("doc-validation"),
"doc-validation class: {err}"
);
assert!(
err.contains("http://127.0.0.1:0/nowhere"),
"the error must name the undeclared partner URI: {err}"
);
}
#[tokio::test(flavor = "multi_thread")]
#[allow(clippy::await_holding_lock)]
async fn keycloak_boot_reports_infra_unavailable() {
let _wasm_acks_guard = crate::commands::run::WASM_ACKS_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let project = TempProject::new("keycloak-infra-unavailable");
let root = project.root();
let doc_path = write_project_with_camel_toml(
root,
r#"
[security.keycloak]
server_url = "https://kc.example.com"
realm = "camel"
client_id = "camel-api"
client_secret = "kc-secret"
"#,
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
"#,
);
let doc = parse_scenario_document(&doc_path).expect("parse scenario doc"); let result = run_scenario_full_boot(&doc, root).await;
let doc_error = result
.doc_error
.as_deref()
.expect("the rejected boot must report a doc error"); assert!(
doc_error.starts_with("infra-unavailable:"),
"AuthProviderUnavailable must classify as infra-unavailable, got: {doc_error}"
);
assert!(
doc_error.contains("scenario boot failed"),
"the doc error must name the boot failure: {doc_error}"
);
assert!(
result.apparatus,
"infra-unavailable is apparatus class (exit 2)"
);
}
#[tokio::test(flavor = "multi_thread")]
#[allow(clippy::await_holding_lock)]
async fn full_boot_feeds_secret_query_keys() {
let _wasm_acks_guard = crate::commands::run::WASM_ACKS_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let project = TempProject::new("full-boot-secret-keys");
let root = project.root();
let doc_path = write_project(
root,
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
"#,
);
let doc = parse_scenario_document(&doc_path).expect("parse scenario doc"); let run = camel_integration_test::boot_scenario(&doc, root, &tier_env())
.await
.expect("minimal scenario must boot through the shared wiring"); let ctx = std::sync::Arc::new(tokio::sync::Mutex::new(run.ctx));
let keys = http_secret_query_keys(&ctx).await;
for secret in ["authUsername", "authPassword", "authBearerToken"] {
assert!(
keys.iter().any(|key| key == secret),
"the booted http metadata must classify {secret} as secret: {keys:?}"
);
}
assert!(
!keys.iter().any(|key| key == "httpMethod"),
"a non-secret uri option must never be classified secret: {keys:?}"
);
let mut guard = ctx.lock().await;
let _ = run.boot.shutdown(&mut guard).await;
}
#[tokio::test(flavor = "multi_thread")]
#[allow(clippy::await_holding_lock)]
async fn boot_failure_stays_full_boot_failure() {
let _wasm_acks_guard = crate::commands::run::WASM_ACKS_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let project = TempProject::new("boot-stays-full-boot-failure");
let root = project.root();
let doc_path = write_project(
root,
r#"
routeFiles: [nope.yaml]
scenario:
- send:
to: direct:start
"#,
);
let doc = parse_scenario_document(&doc_path).expect("parse scenario doc"); let result = run_scenario_full_boot(&doc, root).await;
let doc_error = result
.doc_error
.as_deref()
.expect("the rejected boot must report a doc error"); assert!(
doc_error.starts_with("full-boot-failure:"),
"a non-auth boot error must keep full-boot-failure, got: {doc_error}"
);
assert!(
result.apparatus,
"full-boot-failure is apparatus class (exit 2)"
);
}
fn tier_env() -> camel_integration_test::LayeredEnv {
use std::collections::BTreeMap;
camel_integration_test::LayeredEnv::new(
BTreeMap::new(),
BTreeMap::new(),
Vec::new(),
camel_integration_test::ambient_std(),
)
}
fn write_tier_project(root: &Path, camel_toml: &str, routes_yaml: &str, doc_yaml: &str) -> PathBuf {
fs::write(root.join("Camel.toml"), camel_toml).expect("write Camel.toml"); fs::write(root.join("routes.yaml"), routes_yaml).expect("write routes.yaml"); let doc_path = root.join("scenario.test.yaml");
fs::write(&doc_path, doc_yaml).expect("write scenario doc"); doc_path
}
async fn direct_oneshot(
ctx: &camel_core::CamelContext,
uri: &str,
exchange: camel_api::Exchange,
) -> Result<camel_api::Exchange, camel_api::CamelError> {
use tower::ServiceExt;
const RETRY_SLEEP: std::time::Duration = std::time::Duration::from_millis(20);
const RETRY_DEADLINE: std::time::Duration = std::time::Duration::from_secs(1);
let deadline = tokio::time::Instant::now() + RETRY_DEADLINE;
loop {
let producer_ctx = ctx.producer_context();
let component = ctx
.registry()
.get("direct")
.expect("direct component registered by the bundle cascade"); let endpoint = component
.create_endpoint(uri, ctx)
.expect("direct endpoint creation must succeed"); let producer = endpoint
.create_producer(
std::sync::Arc::new(camel_component_api::NoOpComponentContext),
&producer_ctx,
)
.expect("direct producer creation must succeed"); match producer.oneshot(exchange.clone()).await {
Ok(reply) => return Ok(reply),
Err(e) => {
let is_startup_race = matches!(e, camel_api::CamelError::EndpointCreationFailed(_));
if is_startup_race && tokio::time::Instant::now() < deadline {
tokio::time::sleep(RETRY_SLEEP).await;
continue;
}
return Err(e);
}
}
}
}
#[cfg(feature = "security")]
async fn native_provider_registry(
camel_toml: &str,
ctx: &camel_core::CamelContext,
) -> camel_auth::ProviderRegistry {
let config: camel_config::CamelConfig =
toml::from_str(camel_toml).expect("parse test CamelConfig"); let sec = camel_bundles::security_boot::build_security_compile_context_from_config(
&config,
ctx.registry_arc(),
)
.await
.expect("shared security builder must succeed"); sec.provider_registry()
}
#[cfg(feature = "security")]
#[tokio::test(flavor = "multi_thread")]
#[allow(clippy::await_holding_lock)]
async fn security_policy_route_boots_in_tier() {
let _wasm_acks_guard = crate::commands::run::WASM_ACKS_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
use camel_api::security_policy::{
AccessMode, CredentialSource, RouteSecurityPlan, TransportId,
};
use camel_api::{Body, CamelError, Exchange, Message};
use camel_auth::credential_source::ExtractedToken;
use camel_auth::kernel::{install_carrier, kernel_authenticate};
let camel_toml = r#"
[security.native]
subject = "dev-user"
issuer = "native"
bearer_token = "dev-token"
roles = ["admin"]
"#;
let routes_yaml = r#"
routes:
- id: sec-gate
from: direct:sec
security_policy:
roles: ["admin"]
provider: "native"
steps:
- to: mock:out
- set_header:
key: sec-route-trace
value: passed
"#;
let project = TempProject::new("sec-policy-tier");
let root = project.root();
let doc_path = write_tier_project(
root,
camel_toml,
routes_yaml,
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:sec
"#,
);
let doc = parse_scenario_document(&doc_path).expect("parse scenario doc"); let mut run = camel_integration_test::boot_scenario(&doc, root, &tier_env())
.await
.expect("security_policy route must boot through the shared wiring");
let providers = native_provider_registry(camel_toml, &run.ctx).await;
let mut message = Message::new(Body::Text("ping".to_string()));
message.set_header(
"Authorization",
serde_json::Value::String("Bearer dev-token".to_string()),
);
let mut exchange = Exchange::new(message);
let plan = RouteSecurityPlan {
access_mode: AccessMode::Authenticated,
provider_ref: Some("native".to_string()),
transport: TransportId::Http,
credential_sources: vec![CredentialSource::AuthorizationHeader],
audience_binding: None,
};
let credentials = ExtractedToken {
token: "dev-token".to_string(),
source: CredentialSource::AuthorizationHeader,
};
let principal = kernel_authenticate(&plan, &providers, &credentials)
.await
.expect("native credential must authenticate via the shared builder's registry"); install_carrier(&mut exchange, &principal);
let reply = direct_oneshot(&run.ctx, "direct:sec", exchange)
.await
.expect("credentialed send must complete the security_policy route"); assert_eq!(
reply
.input
.header("sec-route-trace")
.and_then(serde_json::Value::as_str),
Some("passed"),
"the reply must carry the route-set trace (policy passed, mock producer delivered)"
);
let plain = Exchange::new(Message::new(Body::Text("ping".to_string())));
let err = direct_oneshot(&run.ctx, "direct:sec", plain)
.await
.expect_err("credential-less send must be refused"); assert!(
matches!(err, CamelError::Unauthenticated(_)),
"refusal must be Unauthenticated, got: {err:?}"
);
let _ = run.boot.shutdown(&mut run.ctx).await;
}
#[tokio::test(flavor = "multi_thread")]
#[allow(clippy::await_holding_lock)]
async fn public_bind_without_ack_fails_closed_in_tier() {
let _wasm_acks_guard = crate::commands::run::WASM_ACKS_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
use camel_api::CamelError;
let camel_toml = r#"
[binds."0.0.0.0:41998"]
"#;
let routes_yaml = r#"
routes:
- id: pub-bind-tier
from: http://0.0.0.0:41998/pub
steps:
- to: mock:out
"#;
let project = TempProject::new("pub-bind-tier");
let root = project.root();
let doc_path = write_tier_project(
root,
camel_toml,
routes_yaml,
r#"
routeFiles: [routes.yaml]
scenario:
- sleep:
duration: 1s
"#,
);
let doc = parse_scenario_document(&doc_path).expect("parse scenario doc"); let err = match camel_integration_test::boot_scenario(&doc, root, &tier_env()).await {
Ok(_) => panic!("unacknowledged non-loopback Public bind must refuse the boot"),
Err(err) => err,
};
match err {
CamelError::RouteError(msg) => assert!(
msg.contains("non-loopback address; acknowledge via [binds"),
"refusal must name the acknowledgement path: {msg}"
),
other => panic!("expected RouteError, got {other:?}"),
}
}
#[tokio::test(flavor = "multi_thread")]
#[allow(clippy::await_holding_lock)]
async fn stream_cache_threshold_smoke_boot() {
let _wasm_acks_guard = crate::commands::run::WASM_ACKS_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let camel_toml = r#"
log_level = "info"
[stream_caching]
threshold = 512
"#;
let routes_yaml = r#"
routes:
- id: sc-tier
from: direct:sc
steps:
- stream_cache: true
- to: log:info
"#;
let project = TempProject::new("stream-cache-tier");
let root = project.root();
let doc_path = write_tier_project(
root,
camel_toml,
routes_yaml,
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:sc
"#,
);
let doc = parse_scenario_document(&doc_path).expect("parse scenario doc"); let mut run = camel_integration_test::boot_scenario(&doc, root, &tier_env())
.await
.expect("stream_cache route with a config threshold must boot"); let _ = run.boot.shutdown(&mut run.ctx).await;
}
#[tokio::test(flavor = "multi_thread")]
#[allow(clippy::await_holding_lock)]
async fn templated_route_file_materializes_in_tier() {
let _wasm_acks_guard = crate::commands::run::WASM_ACKS_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
use camel_api::{Body, Exchange, Message};
let camel_toml = "log_level = \"info\"\n";
let routes_yaml = r#"
routes: []
templates:
- id: direct-mock
parameters:
- name: name
routes:
- id: "materialized-direct"
from: "direct:{{name}}"
steps:
- to: "mock:{{name}}"
templated_routes:
- route_template_ref: direct-mock
route_id: "t1"
parameters:
name: t1
- route_template_ref: direct-mock
route_id: "t2"
parameters:
name: t2
"#;
let project = TempProject::new("templated-tier");
let root = project.root();
let doc_path = write_tier_project(
root,
camel_toml,
routes_yaml,
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:t1
- send:
to: direct:t2
"#,
);
let doc = parse_scenario_document(&doc_path).expect("parse scenario doc"); let mut run = camel_integration_test::boot_scenario(&doc, root, &tier_env())
.await
.expect("both templated routes must materialize and boot");
for (uri, lane) in [("direct:t1", "t1"), ("direct:t2", "t2")] {
let exchange = Exchange::new(Message::new(Body::Text(format!("ping {lane}"))));
match direct_oneshot(&run.ctx, uri, exchange).await {
Ok(_) => {}
Err(e) => panic!("send to {uri} must complete the materialized route: {e}"),
}
}
let _ = run.boot.shutdown(&mut run.ctx).await;
}
#[tokio::test(flavor = "multi_thread")]
#[allow(clippy::await_holding_lock)]
async fn sql_dynamic_query_fails_closed_in_tier() {
let _wasm_acks_guard = crate::commands::run::WASM_ACKS_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
use camel_api::CamelError;
let camel_toml = "log_level = \"info\"\n";
let routes_yaml = r#"
routes:
- id: sql-tier
from: direct:sql
steps:
- to: "sql:select 1?db_url=postgres://x/y&useMessageBodyForSql=true"
"#;
let project = TempProject::new("sql-tier");
let root = project.root();
let doc_path = write_tier_project(
root,
camel_toml,
routes_yaml,
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:sql
"#,
);
let doc = parse_scenario_document(&doc_path).expect("parse scenario doc"); let err = match camel_integration_test::boot_scenario(&doc, root, &tier_env()).await {
Ok(_) => panic!("the ADR-0033 fail-closed sql check must refuse the boot"),
Err(err) => err,
};
match err {
CamelError::Config(msg) => assert!(
msg.contains("sql-dynamic-query"),
"startup refusal must name the sql-dynamic-query check: {msg}"
),
other => panic!("expected Config, got {other:?}"),
}
}
#[tokio::test(flavor = "multi_thread")]
#[allow(clippy::await_holding_lock)]
async fn wasm_security_policies_rejected_in_tier_doc() {
let _wasm_acks_guard = crate::commands::run::WASM_ACKS_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let project = TempProject::new("wasm-tier-doc");
let root = project.root();
let doc_path = write_project_with_camel_toml(
root,
r#"
[security.policies.wasm.demo]
path = "policies/demo.wasm"
"#,
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
"#,
);
let doc = parse_scenario_document(&doc_path).expect("parse scenario doc"); let result = run_scenario_full_boot(&doc, root).await;
let doc_error = result
.doc_error
.as_deref()
.expect("the rejected boot must report a doc error"); assert!(
doc_error.starts_with("full-boot-failure:"),
"a wasm Config rejection must keep full-boot-failure, got: {doc_error}"
);
assert!(
doc_error.contains("not supported in the scenario tier"),
"the doc error must name the v1 tier limitation: {doc_error}"
);
assert!(
result.apparatus,
"full-boot-failure is apparatus class (exit 2)"
);
}