#![cfg(feature = "http")]
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use camel_api::Value;
use camel_bundles::BootHandle;
use camel_core::CamelContext;
use camel_integration_test::env_layers::ambient_std;
use camel_integration_test::{
Expectation, HttpPartner, LayeredEnv, PartnerAdapter, PartnerRouter, ScenarioAction,
ScenarioDocument, ScenarioFailure, ScenarioTarget, ScenarioVars, ScenarioVerdict,
ValidateExpectation, boot_scenario, parse_scenario_document, run_scenario_document,
};
const CONSUMER_ENDPOINT: &str = "${INBOUND}/in";
const FIXED_CONSUMER_PORT: u16 = 28180;
fn fixture_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/inbound")
}
fn fixed_fixture_root() -> PathBuf {
fixture_root().join("fixed")
}
fn layered_env(
doc: &ScenarioDocument,
harness_provisioned: BTreeMap<String, String>,
) -> LayeredEnv {
LayeredEnv::new(
doc.env.clone().unwrap_or_default(),
harness_provisioned,
doc.env_passthrough.clone().unwrap_or_default(),
ambient_std(),
)
}
struct BootedFixture {
doc: ScenarioDocument,
router: PartnerRouter,
ctx: Arc<tokio::sync::Mutex<CamelContext>>,
boot: BootHandle,
inbound_bound: Option<std::net::SocketAddr>,
}
async fn boot_document(doc_path: &Path, root: &Path) -> BootedFixture {
let doc = parse_scenario_document(doc_path).expect("fixture document must parse");
let partner = HttpPartner::start(Vec::new())
.await
.expect("partner constructor must bind its loopback listener");
let env = layered_env(&doc, BTreeMap::new());
let run = boot_scenario(&doc, root, &env)
.await
.expect("the full boot must succeed");
let ctx = Arc::new(tokio::sync::Mutex::new(run.ctx));
let mut adapters: BTreeMap<String, Box<dyn PartnerAdapter>> = BTreeMap::new();
adapters.insert(CONSUMER_ENDPOINT.to_string(), Box::new(partner));
BootedFixture {
doc,
router: PartnerRouter::new(adapters),
ctx,
boot: run.boot,
inbound_bound: run.inbound_bound,
}
}
async fn boot_fixture() -> BootedFixture {
boot_document(&fixture_root().join("consumer.test.yaml"), &fixture_root()).await
}
async fn boot_fixed_fixture() -> BootedFixture {
boot_document(
&fixed_fixture_root().join("consumer.test.yaml"),
&fixed_fixture_root(),
)
.await
}
fn inbound_vars(bound: std::net::SocketAddr) -> ScenarioVars {
let mut vars = ScenarioVars::new();
vars.set("INBOUND", Value::String(format!("http://{bound}")));
vars
}
#[tokio::test]
async fn inbound_consumer_honest_readiness() {
let fixture = boot_fixture().await;
let bound = fixture
.inbound_bound
.expect("the staged inbound listener must have bound");
let connected = tokio::net::TcpStream::connect(bound)
.await
.expect("connect must succeed immediately after boot_scenario returns");
drop(connected);
let mut vars = inbound_vars(bound);
let mut outcome = run_scenario_document(&fixture.doc, &fixture.router, &mut vars, None).await;
outcome.inbound_bound = fixture.inbound_bound;
assert_eq!(
outcome.verdict,
Some(ScenarioVerdict::Pass),
"every action must pass: {outcome:?}"
);
let mut ctx = fixture.ctx.lock().await;
fixture
.boot
.shutdown(&mut ctx)
.await
.expect("clean shutdown must complete");
}
#[tokio::test]
async fn inbound_response_validated_on_wire() {
let fixture = boot_fixture().await;
let bound = fixture
.inbound_bound
.expect("the staged inbound listener must have bound");
let mut vars = inbound_vars(bound);
let outcome = run_scenario_document(&fixture.doc, &fixture.router, &mut vars, None).await;
assert_eq!(
outcome.verdict,
Some(ScenarioVerdict::Pass),
"status, header, and body validations must pass: {outcome:?}"
);
assert_eq!(
vars.get("status"),
Some(&Value::Number(201.into())),
"the status selector must have read the wire response code"
);
let corrupted = ScenarioDocument {
source_path: fixture.doc.source_path.clone(),
route_source: fixture.doc.route_source,
scenario: fixture
.doc
.scenario
.iter()
.map(|action| {
if let ScenarioAction::Validate { target, .. } = action
&& matches!(target, ScenarioTarget::LastReceived(endpoint) if endpoint.endpoint == CONSUMER_ENDPOINT)
{
ScenarioAction::Validate {
target: target.clone(),
expectation: ValidateExpectation::Message(Expectation::Equals(
Value::String("never-the-served-body".to_string()),
)),
deadline: None,
elapsed_at_least: None,
}
} else {
action.clone()
}
})
.collect(),
env: fixture.doc.env.clone(),
env_passthrough: fixture.doc.env_passthrough.clone(),
profile: fixture.doc.profile.clone(),
partners: None,
send_deadline: fixture.doc.send_deadline,
inbound: fixture.doc.inbound,
logs: fixture.doc.logs,
};
let mut vars = inbound_vars(bound);
let outcome = run_scenario_document(&corrupted, &fixture.router, &mut vars, None).await;
assert_eq!(outcome.verdict, None, "the corrupted body must fail");
let mismatch = outcome
.per_action
.last()
.and_then(|result| result.as_ref().err())
.expect("the failing action must carry a failure");
assert!(
matches!(mismatch, ScenarioFailure::ValidationMismatch { .. }),
"expected ValidationMismatch, got {mismatch:?}"
);
let rendered = mismatch.to_string();
assert!(
rendered.contains(CONSUMER_ENDPOINT),
"the mismatch must name the receiving endpoint: {rendered}"
);
assert!(
rendered.contains("never-the-served-body"),
"the mismatch must state the demanded body: {rendered}"
);
let mut ctx = fixture.ctx.lock().await;
fixture
.boot
.shutdown(&mut ctx)
.await
.expect("clean shutdown must complete");
}
#[tokio::test]
async fn fixed_port_backcompat() {
let fixture = boot_fixed_fixture().await;
assert!(
fixture.inbound_bound.is_none(),
"the fixed-port fixture declares no inbound: section"
);
let connected = tokio::net::TcpStream::connect(("127.0.0.1", FIXED_CONSUMER_PORT))
.await
.expect("the pinned consumer port must accept immediately after boot");
drop(connected);
let mut vars = ScenarioVars::new();
let outcome = run_scenario_document(&fixture.doc, &fixture.router, &mut vars, None).await;
assert_eq!(
outcome.verdict,
Some(ScenarioVerdict::Pass),
"the literal-port document must serve end to end: {outcome:?}"
);
let mut ctx = fixture.ctx.lock().await;
fixture
.boot
.shutdown(&mut ctx)
.await
.expect("clean shutdown must complete");
}