#![expect(
clippy::unwrap_used,
clippy::expect_used,
clippy::string_slice,
reason = "legacy BDD tests use static fixtures; cleanup is tracked in policy/clippy-debt.toml"
)]
use axum::{
Router,
body::Body,
http::{Request, StatusCode},
};
use cucumber::{World, given, then, when};
use http_body_util::BodyExt;
use std::sync::Arc;
use std::time::Instant;
use tower::ServiceExt;
async fn send(router: Router, request: Request<Body>) -> (StatusCode, String) {
let response = router.oneshot(request).await.unwrap();
let status = response.status();
let body = response.into_body().collect().await.unwrap().to_bytes();
(status, String::from_utf8(body.to_vec()).unwrap())
}
fn connect_info() -> axum::extract::ConnectInfo<std::net::SocketAddr> {
axum::extract::ConnectInfo(std::net::SocketAddr::from(([127, 0, 0, 1], 8080)))
}
#[derive(Debug, World)]
#[world(init = Self::new)]
pub struct ServerWorld {
api_key: Option<String>,
request_body: Option<String>,
raw_body: Option<String>,
response_status: Option<u16>,
response_body: Option<String>,
}
impl ServerWorld {
fn new() -> Self {
Self {
api_key: None,
request_body: None,
raw_body: None,
response_status: None,
response_body: None,
}
}
fn create_router(&self) -> Router {
let metrics_handle = hl7v2_server::metrics::init_metrics_recorder();
let state = Arc::new(hl7v2_server::server::AppState {
start_time: Instant::now(),
metrics_handle: Arc::new(metrics_handle),
api_key: self.api_key.clone(),
cors_allowed_origins: Default::default(),
readiness_checks: hl7v2_server::ServerConfig::default().readiness_checks(),
bundle_output_root: None,
ack_policy: Default::default(),
quarantine: Default::default(),
});
hl7v2_server::routes::build_router(state)
}
}
#[given("the test server is running")]
fn given_server_running(world: &mut ServerWorld) {
world.api_key = None;
}
#[given(regex = r#"the test server is running with API key "([^"]+)""#)]
fn given_server_with_api_key(world: &mut ServerWorld, key: String) {
world.api_key = Some(key);
}
#[given("a valid HL7 ADT^A01 message payload")]
fn given_adt_a01_payload(world: &mut ServerWorld) {
let msg = "MSH|^~\\&|SendingApp|SendingFac|ReceivingApp|ReceivingFac|20231119120000||ADT^A01|MSG001|P|2.5\rPID|1||MRN123^^^Facility^MR||Doe^John^A||19800101|M\r";
let body = serde_json::json!({
"message": msg,
"mllp_framed": false,
"options": { "include_json": true }
});
world.request_body = Some(serde_json::to_string(&body).unwrap());
}
#[given("a valid HL7 ORU^R01 message payload")]
fn given_oru_r01_payload(world: &mut ServerWorld) {
let msg = "MSH|^~\\&|LabSys|Lab|LIS|Hospital|20231119140000||ORU^R01|MSG003|P|2.5\rPID|1||MRN789^^^Lab^MR||Patient^Test||19850610|M\rOBR|1|ORD123|FIL456|CBC^Complete Blood Count\rOBX|1|NM|WBC^White Blood Count||7.5|10^9/L\r";
let body = serde_json::json!({
"message": msg,
"mllp_framed": false,
"options": { "include_json": true }
});
world.request_body = Some(serde_json::to_string(&body).unwrap());
}
#[given("a malformed HL7 message payload")]
fn given_malformed_payload(world: &mut ServerWorld) {
let body = serde_json::json!({
"message": "This is not a valid HL7 message",
"mllp_framed": false
});
world.request_body = Some(serde_json::to_string(&body).unwrap());
}
#[given("an invalid JSON payload")]
fn given_invalid_json(world: &mut ServerWorld) {
world.raw_body = Some("not valid json".to_string());
}
#[given("a valid HL7 ADT^A01 message payload with profile")]
fn given_payload_with_profile(world: &mut ServerWorld) {
let msg = "MSH|^~\\&|SendingApp|SendingFac|ReceivingApp|ReceivingFac|20231119120000||ADT^A01|MSG001|P|2.5\rPID|1||MRN123^^^Facility^MR||Doe^John^A||19800101|M\r";
let profile = r#"
message_structure: "ADT_A01"
version: "2.5"
segments:
- id: "MSH"
required: true
- id: "PID"
required: true
constraints:
- path: "PID.3"
required: true
"#;
let body = serde_json::json!({
"message": msg,
"profile": profile,
"mllp_framed": false
});
world.request_body = Some(serde_json::to_string(&body).unwrap());
}
#[when(regex = r#"I send GET request to "([^"]+)""#)]
async fn when_get(world: &mut ServerWorld, uri: String) {
let router = world.create_router();
let request = Request::builder()
.extension(connect_info())
.uri(&uri)
.body(Body::empty())
.unwrap();
let (status, body) = send(router, request).await;
world.response_status = Some(status.as_u16());
world.response_body = Some(body);
}
#[when(regex = r#"I POST the message to "([^"]+)""#)]
async fn when_post_message(world: &mut ServerWorld, uri: String) {
let router = world.create_router();
let body_content = world.request_body.clone().unwrap_or_default();
let request = Request::builder()
.extension(connect_info())
.uri(&uri)
.method("POST")
.header("Content-Type", "application/json")
.body(Body::from(body_content))
.unwrap();
let (status, body) = send(router, request).await;
world.response_status = Some(status.as_u16());
world.response_body = Some(body);
}
#[when(regex = r#"I POST raw body to "([^"]+)""#)]
async fn when_post_raw(world: &mut ServerWorld, uri: String) {
let router = world.create_router();
let body_content = world
.raw_body
.clone()
.or_else(|| world.request_body.clone())
.unwrap_or_default();
let request = Request::builder()
.extension(connect_info())
.uri(&uri)
.method("POST")
.header("Content-Type", "application/json")
.body(Body::from(body_content))
.unwrap();
let (status, body) = send(router, request).await;
world.response_status = Some(status.as_u16());
world.response_body = Some(body);
}
#[when(regex = r#"I POST to "([^"]+)""#)]
async fn when_post_to(world: &mut ServerWorld, uri: String) {
let router = world.create_router();
let body_content = world.request_body.clone().unwrap_or_default();
let request = Request::builder()
.extension(connect_info())
.uri(&uri)
.method("POST")
.header("Content-Type", "application/json")
.body(Body::from(body_content))
.unwrap();
let (status, body) = send(router, request).await;
world.response_status = Some(status.as_u16());
world.response_body = Some(body);
}
#[when(regex = r#"I POST without credentials to "([^"]+)""#)]
async fn when_post_without_auth(world: &mut ServerWorld, uri: String) {
let router = world.create_router();
let body_content = world.request_body.clone().unwrap_or_default();
let request = Request::builder()
.extension(connect_info())
.uri(&uri)
.method("POST")
.header("Content-Type", "application/json")
.body(Body::from(body_content))
.unwrap();
let (status, body) = send(router, request).await;
world.response_status = Some(status.as_u16());
world.response_body = Some(body);
}
#[when(regex = r#"I POST with API key "([^"]+)" to "([^"]+)""#)]
async fn when_post_with_auth(world: &mut ServerWorld, key: String, uri: String) {
let router = world.create_router();
let body_content = world.request_body.clone().unwrap_or_default();
let request = Request::builder()
.extension(connect_info())
.uri(&uri)
.method("POST")
.header("Content-Type", "application/json")
.header("X-API-Key", &key)
.body(Body::from(body_content))
.unwrap();
let (status, body) = send(router, request).await;
world.response_status = Some(status.as_u16());
world.response_body = Some(body);
}
#[then(regex = r"the response status should be (\d+)")]
fn then_status(world: &mut ServerWorld, status: u16) {
let actual = world.response_status.expect("No response received");
assert_eq!(
actual,
status,
"Expected status {}, got {}. Body: {}",
status,
actual,
world.response_body.as_deref().unwrap_or("<empty>")
);
}
#[then(regex = r#"the response should contain "([^"]+)""#)]
fn then_response_contains(world: &mut ServerWorld, expected: String) {
let body = world.response_body.as_ref().expect("No response body");
assert!(
body.contains(&expected),
"Response body should contain '{}', got: {}",
expected,
if body.len() > 200 { &body[..200] } else { body }
);
}
#[then(regex = r#"the response Content-Type should contain "([^"]+)""#)]
fn then_content_type(_world: &mut ServerWorld, _expected: String) {
}
#[tokio::main]
async fn main() {
if let Some(filter) = std::env::args().skip(1).find(|arg| !arg.starts_with('-'))
&& !["bdd", "cucumber"]
.iter()
.any(|allowed| filter.contains(allowed))
{
return;
}
ServerWorld::cucumber().run_and_exit("./features").await;
}