1use std::sync::Arc;
31
32use axum::body::Body;
33use axum::http::{HeaderMap, HeaderName, HeaderValue, Method};
34
35use crate::core::engine::DiContainerBuilder;
36use crate::web::context::{Claims, RequestContext};
37
38pub struct TestRequest {
40 method: Method,
41 path: String,
42 query: String,
43 headers: HeaderMap,
44 body: bytes::Bytes,
45 claims: Option<serde_json::Value>,
46 container: DiContainerBuilder,
47}
48
49impl TestRequest {
50 pub fn new(method: Method, path: impl Into<String>) -> Self {
51 Self {
52 method,
53 path: path.into(),
54 query: String::new(),
55 headers: HeaderMap::new(),
56 body: bytes::Bytes::new(),
57 claims: None,
58 container: DiContainerBuilder::new(),
59 }
60 }
61
62 pub fn get(path: impl Into<String>) -> Self {
63 Self::new(Method::GET, path)
64 }
65 pub fn post(path: impl Into<String>) -> Self {
66 Self::new(Method::POST, path)
67 }
68 pub fn put(path: impl Into<String>) -> Self {
69 Self::new(Method::PUT, path)
70 }
71 pub fn delete(path: impl Into<String>) -> Self {
72 Self::new(Method::DELETE, path)
73 }
74
75 pub fn header(mut self, name: &str, value: &str) -> Self {
78 let n = name.parse::<HeaderName>().expect("valid header name");
79 let v = HeaderValue::from_str(value).expect("valid header value");
80 self.headers.insert(n, v);
81 self
82 }
83
84 pub fn query(mut self, q: impl Into<String>) -> Self {
86 self.query = q.into();
87 self
88 }
89
90 pub fn json(mut self, value: &serde_json::Value) -> Self {
92 self.body = bytes::Bytes::from(value.to_string());
93 self.headers
94 .insert("content-type", HeaderValue::from_static("application/json"));
95 self
96 }
97
98 pub fn body(mut self, bytes: impl Into<bytes::Bytes>) -> Self {
100 self.body = bytes.into();
101 self
102 }
103
104 pub fn claims(mut self, claims: serde_json::Value) -> Self {
108 self.claims = Some(claims);
109 self
110 }
111
112 pub fn provide<T: Send + Sync + 'static>(mut self, value: T) -> Self {
115 self.container.register(value);
116 self
117 }
118
119 pub async fn build(self) -> RequestContext {
122 let container = Box::leak(Box::new(self.container)).clone_freeze();
123
124 let uri = if self.query.is_empty() {
125 self.path.clone()
126 } else {
127 format!("{}?{}", self.path, self.query)
128 };
129 let mut req = axum::http::Request::builder()
130 .method(self.method)
131 .uri(&uri)
132 .body(Body::from(self.body))
133 .expect("test request builds");
134 *req.headers_mut() = self.headers;
135
136 let (parts, body) = req.into_parts();
137 let ctx = crate::web::boundary::assemble_context(
138 parts,
139 body,
140 Default::default(),
141 container,
142 "",
143 None,
144 )
145 .await
146 .expect("test request under the default body cap");
147
148 match self.claims {
149 Some(serde_json::Value::Object(map)) => {
150 ctx.__with_claims(Some(Arc::new(map as Claims)))
151 }
152 Some(other) => {
153 let mut map = serde_json::Map::new();
154 map.insert("value".into(), other);
155 ctx.__with_claims(Some(Arc::new(map)))
156 }
157 None => ctx,
158 }
159 }
160}
161
162pub struct TestServer {
164 pub base_url: String,
166 trigger: Arc<tokio::sync::Notify>,
167 server: tokio::task::JoinHandle<()>,
168}
169
170impl TestServer {
171 pub async fn launch<RootMod: crate::core::engine::Module>(
175 plugins: Vec<Box<dyn crate::core::plugins::ArclyPlugin>>,
176 config: crate::app::LaunchConfig,
177 ) -> Self {
178 let probe = std::net::TcpListener::bind("127.0.0.1:0").expect("port probe");
179 let addr = probe.local_addr().expect("probe addr").to_string();
180 drop(probe);
181
182 let base_url = format!("http://{addr}");
183 let info = crate::openapi::OpenApiInfo::new("test-server", "0");
184 let trigger = Arc::new(tokio::sync::Notify::new());
185 let config = config.shutdown_trigger(Arc::clone(&trigger));
186 let launch_addr = addr.clone();
187 let server = tokio::spawn(async move {
188 let _ =
189 crate::app::App::launch_configured::<RootMod>(&launch_addr, info, plugins, config)
190 .await;
191 });
192
193 for _ in 0..3000 {
197 if tokio::net::TcpStream::connect(&addr).await.is_ok() {
198 return Self {
199 base_url,
200 trigger,
201 server,
202 };
203 }
204 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
205 }
206 panic!("test server did not become ready on {addr}");
207 }
208
209 pub async fn shutdown(self) {
215 self.trigger.notify_one();
216 tokio::time::timeout(std::time::Duration::from_secs(30), self.server)
217 .await
218 .expect("graceful shutdown must complete within 30s")
219 .expect("server task must not panic");
220 }
221}
222
223trait CloneFreeze {
226 fn clone_freeze(&'static mut self) -> &'static crate::core::engine::FrozenDiContainer;
227}
228impl CloneFreeze for DiContainerBuilder {
229 fn clone_freeze(&'static mut self) -> &'static crate::core::engine::FrozenDiContainer {
230 std::mem::take(self).freeze()
231 }
232}