use std::sync::Arc;
use axum::body::Body;
use axum::http::{HeaderMap, HeaderName, HeaderValue, Method};
use crate::core::engine::DiContainerBuilder;
use crate::web::context::{Claims, RequestContext};
pub struct TestRequest {
method: Method,
path: String,
query: String,
headers: HeaderMap,
body: bytes::Bytes,
claims: Option<serde_json::Value>,
container: DiContainerBuilder,
}
impl TestRequest {
pub fn new(method: Method, path: impl Into<String>) -> Self {
Self {
method,
path: path.into(),
query: String::new(),
headers: HeaderMap::new(),
body: bytes::Bytes::new(),
claims: None,
container: DiContainerBuilder::new(),
}
}
pub fn get(path: impl Into<String>) -> Self {
Self::new(Method::GET, path)
}
pub fn post(path: impl Into<String>) -> Self {
Self::new(Method::POST, path)
}
pub fn put(path: impl Into<String>) -> Self {
Self::new(Method::PUT, path)
}
pub fn delete(path: impl Into<String>) -> Self {
Self::new(Method::DELETE, path)
}
pub fn header(mut self, name: &str, value: &str) -> Self {
let n = name.parse::<HeaderName>().expect("valid header name");
let v = HeaderValue::from_str(value).expect("valid header value");
self.headers.insert(n, v);
self
}
pub fn query(mut self, q: impl Into<String>) -> Self {
self.query = q.into();
self
}
pub fn json(mut self, value: &serde_json::Value) -> Self {
self.body = bytes::Bytes::from(value.to_string());
self.headers
.insert("content-type", HeaderValue::from_static("application/json"));
self
}
pub fn body(mut self, bytes: impl Into<bytes::Bytes>) -> Self {
self.body = bytes.into();
self
}
pub fn claims(mut self, claims: serde_json::Value) -> Self {
self.claims = Some(claims);
self
}
pub fn provide<T: Send + Sync + 'static>(mut self, value: T) -> Self {
self.container.register(value);
self
}
pub async fn build(self) -> RequestContext {
let container = Box::leak(Box::new(self.container)).clone_freeze();
let uri = if self.query.is_empty() {
self.path.clone()
} else {
format!("{}?{}", self.path, self.query)
};
let mut req = axum::http::Request::builder()
.method(self.method)
.uri(&uri)
.body(Body::from(self.body))
.expect("test request builds");
*req.headers_mut() = self.headers;
let (parts, body) = req.into_parts();
let ctx = crate::web::boundary::assemble_context(
parts,
body,
Default::default(),
container,
"",
None,
)
.await
.expect("test request under the default body cap");
match self.claims {
Some(serde_json::Value::Object(map)) => {
ctx.__with_claims(Some(Arc::new(map as Claims)))
}
Some(other) => {
let mut map = serde_json::Map::new();
map.insert("value".into(), other);
ctx.__with_claims(Some(Arc::new(map)))
}
None => ctx,
}
}
}
pub struct TestServer {
pub base_url: String,
trigger: Arc<tokio::sync::Notify>,
server: tokio::task::JoinHandle<()>,
}
impl TestServer {
pub async fn launch<RootMod: crate::core::engine::Module>(
plugins: Vec<Box<dyn crate::core::plugins::ArclyPlugin>>,
config: crate::app::LaunchConfig,
) -> Self {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind test port");
let addr = listener.local_addr().expect("local addr").to_string();
let base_url = format!("http://{addr}");
let info = crate::openapi::OpenApiInfo::new("test-server", "0");
let trigger = Arc::new(tokio::sync::Notify::new());
let config = config.shutdown_trigger(Arc::clone(&trigger));
let server = tokio::spawn(async move {
if let Err(e) =
crate::app::App::launch_on_listener::<RootMod>(listener, info, plugins, config)
.await
{
tracing::error!(error = %e, "test server exited with error");
}
});
for _ in 0..3000 {
if server.is_finished() {
panic!("test server failed during boot on {addr} — check plugin on_init/on_start errors");
}
if tokio::net::TcpStream::connect(&addr).await.is_ok() {
return Self {
base_url,
trigger,
server,
};
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
panic!("test server did not become ready on {addr}");
}
pub async fn shutdown(self) {
self.trigger.notify_one();
tokio::time::timeout(std::time::Duration::from_secs(30), self.server)
.await
.expect("graceful shutdown must complete within 30s")
.expect("server task must not panic");
}
}
trait CloneFreeze {
fn clone_freeze(&'static mut self) -> &'static crate::core::engine::FrozenDiContainer;
}
impl CloneFreeze for DiContainerBuilder {
fn clone_freeze(&'static mut self) -> &'static crate::core::engine::FrozenDiContainer {
std::mem::take(self).freeze()
}
}