use std::collections::BTreeMap;
use camel_api::CamelError;
use crate::ScenarioDocument;
use crate::boot_scenario::boot_scenario;
use crate::env_layers::{LayeredEnv, ambient_std};
use crate::parse_scenario_document;
const ROUTE: &str = r#"
routes:
- id: boot-route
from: direct:start
steps:
- to: log:info
"#;
const DOC: &str = r#"
routeFiles: [routes.yaml]
scenario:
- sleep:
duration: 1s
"#;
fn project(
camel_toml: &str,
route: Option<(&str, &str)>,
doc: &str,
) -> (tempfile::TempDir, ScenarioDocument) {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::write(dir.path().join("Camel.toml"), camel_toml).expect("write Camel.toml");
if let Some((name, text)) = route {
std::fs::write(dir.path().join(name), text).expect("write route file");
}
let doc_path = dir.path().join("case.test.yaml");
std::fs::write(&doc_path, doc).expect("write document");
let document = parse_scenario_document(&doc_path).expect("document parses");
(dir, document)
}
fn empty_env() -> LayeredEnv {
LayeredEnv::new(BTreeMap::new(), BTreeMap::new(), Vec::new(), ambient_std())
}
fn expect_boot_error(
result: Result<crate::boot_scenario::ScenarioRun, CamelError>,
what: &str,
) -> CamelError {
match result {
Ok(_) => panic!("{what}"),
Err(err) => err,
}
}
#[tokio::test]
async fn boot_rejects_keycloak_offline() {
let (dir, doc) = project(
r#"
[security.keycloak]
server_url = "https://kc.example.com"
realm = "camel"
client_id = "camel-api"
client_secret = "kc-secret"
"#,
Some(("routes.yaml", ROUTE)),
DOC,
);
let err = expect_boot_error(
boot_scenario(&doc, dir.path(), &empty_env()).await,
"keycloak security must be rejected in the offline tier",
);
assert!(
matches!(err, CamelError::AuthProviderUnavailable(_)),
"expected AuthProviderUnavailable, got: {err:?}"
);
}
#[tokio::test]
async fn boot_rejects_oidc_offline() {
let (dir, doc) = project(
r#"
[security.oidc]
issuer = "https://oidc.example.test"
"#,
Some(("routes.yaml", ROUTE)),
DOC,
);
let err = expect_boot_error(
boot_scenario(&doc, dir.path(), &empty_env()).await,
"oidc security must be rejected in the offline tier",
);
assert!(
matches!(err, CamelError::AuthProviderUnavailable(_)),
"expected AuthProviderUnavailable, got: {err:?}"
);
}
#[tokio::test]
async fn boot_rejects_wasm_security_policies() {
let (dir, doc) = project(
r#"
[security.policies.wasm.demo]
path = "policies/demo.wasm"
"#,
Some(("routes.yaml", ROUTE)),
DOC,
);
let err = expect_boot_error(
boot_scenario(&doc, dir.path(), &empty_env()).await,
"wasm security policies must be rejected in the scenario tier",
);
assert!(
matches!(err, CamelError::Config(_)),
"expected Config, got: {err:?}"
);
assert!(
err.to_string()
.contains("not supported in the scenario tier"),
"error must name the v1 tier limitation: {err}"
);
}
#[tokio::test]
async fn boot_hermetic_env_not_resolved_from_process() {
unsafe { std::env::set_var("RC_6BSF_HERMETIC", "leak") };
let route = r#"
routes:
- id: hermetic
from: direct:${env:RC_6BSF_HERMETIC}
steps:
- to: log:info
"#;
let (dir, doc) = project("# minimal\n", Some(("routes.yaml", route)), DOC);
let result = boot_scenario(&doc, dir.path(), &empty_env()).await;
unsafe { std::env::remove_var("RC_6BSF_HERMETIC") };
let err = expect_boot_error(
result,
"the process environment must not resolve scenario placeholders",
);
assert!(
matches!(err, CamelError::Config(_)),
"expected the unresolved-placeholder Config error, got: {err:?}"
);
let display = err.to_string();
assert!(
display.contains("RC_6BSF_HERMETIC"),
"error must name the variable: {display}"
);
assert!(
display.contains("no layer of the scenario environment defines it"),
"error must name the hermetic resolution failure: {display}"
);
}
#[tokio::test]
async fn boot_missing_route_file_names_the_file() {
let doc = r#"
routeFiles: [nope.yaml]
scenario:
- sleep:
duration: 1s
"#;
let (dir, doc) = project("# minimal\n", None, doc);
let err = expect_boot_error(
boot_scenario(&doc, dir.path(), &empty_env()).await,
"a missing declared route file must fail the boot",
);
assert!(
matches!(err, CamelError::Io(_)),
"expected Io, got: {err:?}"
);
assert!(
err.to_string().contains("nope.yaml"),
"error must name the missing file: {err}"
);
}
#[tokio::test]
async fn boot_inline_routes_still_rejected() {
let doc = r#"
routes:
- id: inline-route
from: direct:start
steps:
- to: log:info
scenario:
- sleep:
duration: 1s
"#;
let (dir, doc) = project("# minimal\n", None, doc);
let err = expect_boot_error(
boot_scenario(&doc, dir.path(), &empty_env()).await,
"inline route sources must not boot in v1",
);
assert!(
matches!(err, CamelError::Config(_)),
"expected Config, got: {err:?}"
);
assert!(
err.to_string()
.contains("inline route sources cannot boot in v1"),
"error must keep the inline-routes message: {err}"
);
}
#[tokio::test]
async fn boot_rejects_test_suffix_route_file() {
let doc = r#"
routeFiles: [routes.test.yaml]
scenario:
- sleep:
duration: 1s
"#;
let (dir, doc) = project("# minimal\n", Some(("routes.test.yaml", ROUTE)), doc);
let err = expect_boot_error(
boot_scenario(&doc, dir.path(), &empty_env()).await,
"a reserved-suffix route file must fail the boot",
);
assert!(
matches!(err, CamelError::Config(_)),
"expected Config, got: {err:?}"
);
let display = err.to_string();
assert!(
display.contains("routes.test.yaml"),
"error must name the declared file: {display}"
);
assert!(
display.contains("`camel test`"),
"error must carry the `camel test` guidance: {display}"
);
}