1use std::sync::Arc;
41
42use serde::de::DeserializeOwned;
43use serde_json::Value;
44
45use rainier_container::{scope_facade_application, Application, FacadeScope};
46use rainier_http::{Method, Request, Response, StatusCode};
47use rainier_server::Kernel;
48use rainier_support::Result;
49
50pub struct TestApp {
55 app: Arc<Application>,
56 kernel: Arc<Kernel>,
57 _scope: FacadeScope,
59 defaults: Vec<(String, String)>,
61}
62
63impl TestApp {
64 pub fn new(app: Arc<Application>) -> Result<Self> {
70 let kernel = app.resolve::<Kernel>()?;
71 let scope = scope_facade_application(Arc::clone(&app));
72
73 Ok(Self { app, kernel, _scope: scope, defaults: Vec::new() })
74 }
75
76 pub fn app(&self) -> &Arc<Application> {
78 &self.app
79 }
80
81 pub fn resolve<T: Send + Sync + 'static>(&self) -> Result<Arc<T>> {
83 self.app.resolve::<T>()
84 }
85
86 #[must_use = "this returns a configured harness rather than configuring in place"]
92 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
93 self.defaults.push((name.into(), value.into()));
94 self
95 }
96
97 #[must_use = "this returns a configured harness rather than configuring in place"]
99 pub fn with_token(self, token: impl std::fmt::Display) -> Self {
100 self.with_header("authorization", format!("Bearer {token}"))
101 }
102
103 pub async fn get(&self, uri: &str) -> TestResponse {
105 self.send(self.request(Method::GET, uri).build()).await
106 }
107
108 pub async fn delete(&self, uri: &str) -> TestResponse {
110 self.send(self.request(Method::DELETE, uri).build()).await
111 }
112
113 pub async fn post(&self, uri: &str, body: &impl serde::Serialize) -> TestResponse {
115 self.send(self.request(Method::POST, uri).json(body).build()).await
116 }
117
118 pub async fn put(&self, uri: &str, body: &impl serde::Serialize) -> TestResponse {
120 self.send(self.request(Method::PUT, uri).json(body).build()).await
121 }
122
123 pub async fn patch(&self, uri: &str, body: &impl serde::Serialize) -> TestResponse {
125 self.send(self.request(Method::PATCH, uri).json(body).build()).await
126 }
127
128 pub async fn post_empty(&self, uri: &str) -> TestResponse {
130 self.send(self.request(Method::POST, uri).build()).await
131 }
132
133 pub fn request(&self, method: Method, uri: &str) -> rainier_http::RequestBuilder {
138 let mut builder = Request::builder().method(method).uri(uri);
139 for (name, value) in &self.defaults {
140 builder = builder.header(name.as_str(), value.as_str());
141 }
142 builder
143 }
144
145 pub async fn send(&self, request: Request) -> TestResponse {
151 TestResponse::of(self.kernel.handle_request(request).await).await
152 }
153}
154
155pub struct TestResponse {
162 status: StatusCode,
163 headers: Vec<(String, String)>,
164 body: String,
165}
166
167impl TestResponse {
168 pub async fn of(response: Response) -> Self {
170 let status = response.status();
171 let headers = response
172 .headers()
173 .iter()
174 .filter_map(|(name, value)| {
175 Some((name.as_str().to_string(), value.to_str().ok()?.to_string()))
176 })
177 .collect();
178
179 let body = response.into_string().await.unwrap_or_default();
180
181 Self { status, headers, body }
182 }
183
184 pub fn status(&self) -> StatusCode {
186 self.status
187 }
188
189 pub fn text(&self) -> &str {
191 &self.body
192 }
193
194 pub fn header(&self, name: &str) -> Option<&str> {
196 self.headers
197 .iter()
198 .find(|(header, _)| header.eq_ignore_ascii_case(name))
199 .map(|(_, value)| value.as_str())
200 }
201
202 pub fn json(&self) -> Value {
209 serde_json::from_str(&self.body).unwrap_or_else(|e| {
210 panic!(
211 "the response body is not JSON: {e}\n status: {}\n body: {}",
212 self.status, self.body
213 )
214 })
215 }
216
217 pub fn json_as<T: DeserializeOwned>(&self) -> T {
223 serde_json::from_str(&self.body).unwrap_or_else(|e| {
224 panic!("the response body is not the expected shape: {e}\n body: {}", self.body)
225 })
226 }
227
228 pub fn assert_status(&self, expected: StatusCode) -> &Self {
239 assert_eq!(
240 self.status, expected,
241 "expected {expected}, got {}\n body: {}",
242 self.status, self.body
243 );
244 self
245 }
246
247 pub fn assert_ok(&self) -> &Self {
253 assert!(
254 self.status.is_success(),
255 "expected a 2xx, got {}\n body: {}",
256 self.status,
257 self.body
258 );
259 self
260 }
261
262 pub fn assert_created(&self) -> &Self {
268 self.assert_status(StatusCode::CREATED)
269 }
270
271 pub fn assert_no_content(&self) -> &Self {
277 self.assert_status(StatusCode::NO_CONTENT)
278 }
279
280 pub fn assert_not_found(&self) -> &Self {
286 self.assert_status(StatusCode::NOT_FOUND)
287 }
288
289 pub fn assert_unauthorized(&self) -> &Self {
295 self.assert_status(StatusCode::UNAUTHORIZED)
296 }
297
298 pub fn assert_forbidden(&self) -> &Self {
304 self.assert_status(StatusCode::FORBIDDEN)
305 }
306
307 pub fn assert_invalid(&self) -> &Self {
313 self.assert_status(StatusCode::UNPROCESSABLE_ENTITY)
314 }
315
316 pub fn assert_json_path(&self, path: &str, expected: impl Into<Value>) -> &Self {
324 let expected = expected.into();
325 let document = self.json();
326
327 match json_path(&document, path) {
328 Some(actual) => assert_eq!(
329 *actual, expected,
330 "at `{path}`: expected {expected}, got {actual}\n body: {}",
331 self.body
332 ),
333 None => panic!("`{path}` is not in the response\n body: {}", self.body),
334 }
335 self
336 }
337
338 pub fn assert_json_missing(&self, path: &str) -> &Self {
347 assert!(
348 json_path(&self.json(), path).is_none(),
349 "`{path}` should not be in the response\n body: {}",
350 self.body
351 );
352 self
353 }
354
355 pub fn assert_contains(&self, text: &str) -> &Self {
361 assert!(
362 self.body.contains(text),
363 "the body does not contain {text:?}\n body: {}",
364 self.body
365 );
366 self
367 }
368
369 pub fn assert_header(&self, name: &str, expected: &str) -> &Self {
375 match self.header(name) {
376 Some(actual) => assert_eq!(actual, expected, "header `{name}`"),
377 None => panic!("`{name}` is not on the response — it has {:?}", self.header_names()),
378 }
379 self
380 }
381
382 pub fn assert_header_missing(&self, name: &str) -> &Self {
388 assert!(self.header(name).is_none(), "`{name}` should not be on the response");
389 self
390 }
391
392 fn header_names(&self) -> Vec<&str> {
393 self.headers.iter().map(|(name, _)| name.as_str()).collect()
394 }
395}
396
397fn json_path<'a>(value: &'a Value, path: &str) -> Option<&'a Value> {
399 let mut current = value;
400
401 for segment in path.split('.') {
402 current = match current {
403 Value::Array(items) => items.get(segment.parse::<usize>().ok()?)?,
404 other => other.get(segment)?,
405 };
406 }
407 Some(current)
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413 use serde_json::json;
414
415 fn document() -> Value {
416 json!({
417 "database": { "status": "ok", "latency_ms": 3 },
418 "data": [{ "title": "First" }, { "title": "Second" }],
419 "nothing": null,
420 })
421 }
422
423 #[test]
424 fn a_dotted_path_walks_objects_and_arrays() {
425 let value = document();
426
427 assert_eq!(json_path(&value, "database.status"), Some(&json!("ok")));
428 assert_eq!(json_path(&value, "data.1.title"), Some(&json!("Second")));
429 assert_eq!(json_path(&value, "database.latency_ms"), Some(&json!(3)));
430 }
431
432 #[test]
433 fn an_absent_path_is_none_rather_than_a_panic() {
434 let value = document();
435
436 assert_eq!(json_path(&value, "database.nope"), None);
437 assert_eq!(json_path(&value, "data.9.title"), None);
438 assert_eq!(json_path(&value, "data.title"), None, "an array indexed by a name");
439 }
440
441 #[test]
442 fn a_null_is_present_and_not_missing() {
443 assert_eq!(json_path(&document(), "nothing"), Some(&json!(null)));
446 }
447
448 #[tokio::test]
449 async fn a_response_is_read_once_and_asserted_many_times() {
450 let response = TestResponse::of(
451 Response::ok(r#"{"database":{"status":"ok"}}"#)
452 .with_header("content-type", "application/json"),
453 )
454 .await;
455
456 response
457 .assert_ok()
458 .assert_status(StatusCode::OK)
459 .assert_json_path("database.status", "ok")
460 .assert_json_missing("database.error")
461 .assert_header("content-type", "application/json")
462 .assert_contains("status");
463
464 assert_eq!(response.json()["database"]["status"], "ok");
465 }
466
467 #[tokio::test]
468 #[should_panic(expected = "expected 404 Not Found, got 200 OK")]
469 async fn a_failed_status_assertion_prints_the_body() {
470 TestResponse::of(Response::ok("hello")).await.assert_not_found();
471 }
472
473 #[tokio::test]
474 #[should_panic(expected = "`database.status` is not in the response")]
475 async fn a_missing_path_says_which_path() {
476 TestResponse::of(Response::ok("{}")).await.assert_json_path("database.status", "ok");
477 }
478}