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