nidus_testing/response.rs
1use axum::body::Bytes;
2use http::{HeaderMap, HeaderValue, StatusCode, header::ToStrError};
3use serde::de::DeserializeOwned;
4use serde_json::Value;
5use std::str;
6
7/// Captured in-memory HTTP response.
8///
9/// `TestResponse` owns the status, headers, and collected body returned by
10/// [`crate::TestRequest::send`]. Assertion helpers panic with ordinary test
11/// assertion failures, while `try_json`, `text`, and `header_str` expose
12/// fallible parsing.
13///
14/// ```
15/// # use axum::{Json, Router, routing::get};
16/// use http::StatusCode;
17/// # use nidus_testing::TestApp;
18/// use serde_json::json;
19/// # #[tokio::main]
20/// # async fn main() -> Result<(), http::header::ToStrError> {
21/// # let app = TestApp::from_router(
22/// # Router::new().route("/health", get(|| async { Json(json!({ "ok": true })) })),
23/// # );
24///
25/// let response = app.get("/health").send().await;
26/// response.assert_status(StatusCode::OK);
27/// assert_eq!(response.header_str("content-type")?.unwrap(), "application/json");
28/// response.assert_json(json!({ "ok": true }));
29/// # Ok(())
30/// # }
31/// ```
32pub struct TestResponse {
33 status: StatusCode,
34 headers: HeaderMap,
35 body: Bytes,
36}
37
38impl TestResponse {
39 pub(crate) fn new(status: StatusCode, headers: HeaderMap, body: Bytes) -> Self {
40 Self {
41 status,
42 headers,
43 body,
44 }
45 }
46
47 /// Returns the response status code.
48 pub fn status(&self) -> StatusCode {
49 self.status
50 }
51
52 /// Returns the raw response body bytes.
53 pub fn body(&self) -> &[u8] {
54 &self.body
55 }
56
57 /// Returns the response headers.
58 pub fn headers(&self) -> &HeaderMap {
59 &self.headers
60 }
61
62 /// Returns a response header by name.
63 pub fn header(&self, name: impl AsRef<str>) -> Option<&HeaderValue> {
64 self.headers.get(name.as_ref())
65 }
66
67 /// Returns a response header as a UTF-8 string when present.
68 pub fn header_str(&self, name: impl AsRef<str>) -> Result<Option<&str>, ToStrError> {
69 self.header(name).map(HeaderValue::to_str).transpose()
70 }
71
72 /// Asserts the response status code.
73 pub fn assert_status(&self, expected: StatusCode) {
74 assert_eq!(self.status, expected);
75 }
76
77 /// Asserts a response header as a UTF-8 string.
78 pub fn assert_header(&self, name: impl AsRef<str>, expected: &str) {
79 let name = name.as_ref();
80 let actual = self
81 .header(name)
82 .unwrap_or_else(|| panic!("missing response header `{name}`"));
83 assert_eq!(
84 actual
85 .to_str()
86 .unwrap_or_else(|_| panic!("response header `{name}` was not valid UTF-8")),
87 expected
88 );
89 }
90
91 /// Decodes the response body as JSON.
92 ///
93 /// Panics when the body is not valid JSON for `T`. Use [`Self::try_json`]
94 /// when asserting malformed responses.
95 pub fn json<T>(&self) -> T
96 where
97 T: DeserializeOwned,
98 {
99 self.try_json().expect("test response was not valid JSON")
100 }
101
102 /// Tries to decode the response body as JSON.
103 pub fn try_json<T>(&self) -> serde_json::Result<T>
104 where
105 T: DeserializeOwned,
106 {
107 serde_json::from_slice(&self.body)
108 }
109
110 /// Returns the response body as UTF-8 text.
111 pub fn text(&self) -> std::result::Result<&str, str::Utf8Error> {
112 str::from_utf8(&self.body)
113 }
114
115 /// Asserts the response body as UTF-8 text.
116 ///
117 /// This consumes the response so tests cannot accidentally assert against a
118 /// body after moving it into another helper.
119 pub fn assert_text(self, expected: &str) {
120 let text = self.text().expect("test response was not UTF-8");
121 assert_eq!(text, expected);
122 }
123
124 /// Asserts the response body as JSON.
125 ///
126 /// This consumes the response and compares against a `serde_json::Value`.
127 pub fn assert_json(self, expected: Value) {
128 let actual: Value = self.json();
129 assert_eq!(actual, expected);
130 }
131}