use camel_api::Value;
use camel_builder::{RouteBuilder, StepAccumulator};
use camel_component_direct::DirectComponent;
use camel_core::CamelContext;
use camel_template::{TemplateBundleConfig, TemplateComponent};
mod common;
const PAGE_TEMPLATE: &str =
"{% autoescape \"html\" %}<h1>{{headers.title}}</h1>{% endautoescape %}";
#[tokio::test(flavor = "multi_thread")]
async fn template_renders_end_to_end() {
let dir = tempfile::tempdir().expect("tempdir");
let entry = dir.path().join("page.html");
std::fs::write(&entry, PAGE_TEMPLATE).expect("write page");
let uri = format!("template:file://{}", entry.display());
let mut ctx = common::start_template_route(&uri, "t-render")
.await
.expect("context must start for a valid template");
let out = common::send_title(&ctx, "Hi").await;
assert_eq!(common::body_text(&out), "<h1>Hi</h1>", "rendered body");
assert_eq!(
out.input.headers.get("title"),
Some(&Value::String("Hi".to_string())),
"title header must be preserved"
);
let _ = ctx.stop().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn template_compile_once_no_hot_io() {
let dir = tempfile::tempdir().expect("tempdir");
let entry = dir.path().join("page.html");
std::fs::write(&entry, PAGE_TEMPLATE).expect("write page");
let uri = format!("template:file://{}", entry.display());
let mut ctx = common::start_template_route(&uri, "t-nohot")
.await
.expect("context must start for a valid template");
std::fs::remove_file(&entry).expect("remove source after start");
let first = common::send_title(&ctx, "Hi").await;
assert_eq!(
common::body_text(&first),
"<h1>Hi</h1>",
"first render after delete"
);
let second = common::send_title(&ctx, "Hi").await;
assert_eq!(
common::body_text(&second),
"<h1>Hi</h1>",
"second render after delete — hot path must not touch the filesystem"
);
let _ = ctx.stop().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn missing_template_fails_route_closed() {
let dir = tempfile::tempdir().expect("tempdir");
let missing = dir.path().join("missing.html");
let uri = format!("template:file://{}", missing.display());
let route_id = "t-missing".to_string();
let start_result = common::start_template_route(&uri, &route_id).await;
let (failed_closed, detail) = match start_result {
Err(e) => (true, format!("start error: {e}")),
Ok(mut ctx) => {
let status = ctx.runtime_route_status(&route_id).await.ok().flatten();
let closed = status.as_deref() == Some("Failed");
let _ = ctx.stop().await;
(closed, format!("start Ok; route status = {status:?}"))
}
};
assert!(failed_closed, "missing template must fail closed; {detail}");
}
#[tokio::test(flavor = "multi_thread")]
async fn bundle_enforces_configured_limits() {
let dir = tempfile::tempdir().expect("tempdir");
let entry = dir.path().join("page.html");
std::fs::write(&entry, "<h1>oversize</h1>").expect("write page");
let uri = format!("template:file://{}", entry.display());
let bundle_toml = r#"
[limits]
max-template-size = 4
"#;
let cfg: TemplateBundleConfig =
toml::from_str(bundle_toml).expect("bundle config must deserialize");
let component = TemplateComponent::new(cfg.limits, cfg.render_limits);
let mut ctx = CamelContext::builder()
.build()
.await
.expect("context build");
ctx.register_component(DirectComponent::new());
ctx.register_component(component);
let route = RouteBuilder::from("direct:in")
.route_id("t-bundle-tight")
.to(uri)
.build()
.expect("route build");
ctx.add_route_definition(route)
.await
.expect("add_route_definition");
let start_result = ctx.start().await;
let detail = match start_result {
Err(e) => format!("{e}"),
Ok(_) => panic!("tightened max-template-size must fail closed, but start() returned Ok"),
};
assert!(
detail.contains("max_template_size") || detail.contains("max-template-size"),
"expected max-template-size failure in start error chain, got: {detail}"
);
}