1#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
2use axum::{
3 Router,
4 body::{Body, Bytes, to_bytes},
5 http::{HeaderMap, HeaderName, HeaderValue, Method, Request, StatusCode, header},
6 response::Response,
7};
8use serde::Serialize;
9use std::future::{Future, IntoFuture};
10use std::pin::Pin;
11use tower::ServiceExt;
12
13#[cfg_attr(mutants, mutants::skip)]
14const DEFAULT_MAX_BODY: usize = 10 * 1024 * 1024;
15
16#[non_exhaustive]
18pub struct TestApp {
19 router: Router,
20 max_body_bytes: usize,
21}
22
23impl TestApp {
24 pub fn new(router: Router) -> Self {
26 Self {
27 router,
28 max_body_bytes: DEFAULT_MAX_BODY,
29 }
30 }
31
32 pub fn new_with_limit(router: Router, max_body_bytes: usize) -> Self {
34 Self {
35 router,
36 max_body_bytes,
37 }
38 }
39
40 pub fn get(&self, uri: &str) -> TestRequestBuilder {
42 TestRequestBuilder::new(self.router.clone(), Method::GET, uri, self.max_body_bytes)
43 }
44
45 pub fn post(&self, uri: &str) -> TestRequestBuilder {
47 TestRequestBuilder::new(self.router.clone(), Method::POST, uri, self.max_body_bytes)
48 }
49
50 pub fn put(&self, uri: &str) -> TestRequestBuilder {
52 TestRequestBuilder::new(self.router.clone(), Method::PUT, uri, self.max_body_bytes)
53 }
54
55 pub fn patch(&self, uri: &str) -> TestRequestBuilder {
57 TestRequestBuilder::new(self.router.clone(), Method::PATCH, uri, self.max_body_bytes)
58 }
59
60 pub fn delete(&self, uri: &str) -> TestRequestBuilder {
62 TestRequestBuilder::new(
63 self.router.clone(),
64 Method::DELETE,
65 uri,
66 self.max_body_bytes,
67 )
68 }
69}
70
71#[non_exhaustive]
74pub struct TestRequestBuilder {
75 router: Router,
76 method: Method,
77 uri: String,
78 headers: HeaderMap,
79 body: Option<Body>,
80 max_body_bytes: usize,
81}
82
83impl TestRequestBuilder {
84 pub(crate) fn new(router: Router, method: Method, uri: &str, max_body_bytes: usize) -> Self {
85 Self {
86 router,
87 method,
88 uri: uri.to_string(),
89 headers: HeaderMap::new(),
90 body: None,
91 max_body_bytes,
92 }
93 }
94
95 pub fn max_body_bytes(mut self, max_body_bytes: usize) -> Self {
97 self.max_body_bytes = max_body_bytes;
98 self
99 }
100
101 pub fn header<K, V>(mut self, key: K, value: V) -> Self
103 where
104 HeaderName: TryFrom<K>,
105 HeaderValue: TryFrom<V>,
106 {
107 if let (Ok(k), Ok(v)) = (HeaderName::try_from(key), HeaderValue::try_from(value)) {
108 self.headers.insert(k, v);
109 }
110 self
111 }
112
113 pub fn json<T: Serialize>(mut self, data: &T) -> Self {
115 self.headers.insert(
116 header::CONTENT_TYPE,
117 HeaderValue::from_static("application/json"),
118 );
119 let body_bytes = serde_json::to_vec(data).expect("Failed to serialize body as JSON");
120 self.body = Some(Body::from(body_bytes));
121 self
122 }
123
124 pub fn form<T: Serialize>(mut self, data: &T) -> Self {
126 self.headers.insert(
127 header::CONTENT_TYPE,
128 HeaderValue::from_static("application/x-www-form-urlencoded"),
129 );
130 let body_string = serde_urlencoded::to_string(data)
131 .expect("Failed to serialize body as form URL-encoded");
132 self.body = Some(Body::from(body_string));
133 self
134 }
135
136 pub fn body<B: Into<Body>>(mut self, body: B) -> Self {
138 self.body = Some(body.into());
139 self
140 }
141
142 pub async fn send(self) -> TestResponse {
144 let mut req_builder = Request::builder().method(self.method).uri(&self.uri);
145
146 for (k, v) in self.headers {
147 if let Some(k) = k {
148 req_builder = req_builder.header(k, v);
149 }
150 }
151
152 let body = self.body.unwrap_or_else(Body::empty);
153 let req = req_builder
154 .body(body)
155 .expect("Failed to build HTTP request");
156
157 let response = self
158 .router
159 .oneshot(req)
160 .await
161 .expect("Failed to execute request on Router");
162 TestResponse::new_with_limit(response, self.max_body_bytes).await
163 }
164}
165
166impl IntoFuture for TestRequestBuilder {
167 type Output = TestResponse;
168 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
169
170 fn into_future(self) -> Self::IntoFuture {
171 Box::pin(self.send())
172 }
173}
174
175#[non_exhaustive]
177pub struct TestResponse {
178 status: StatusCode,
179 headers: HeaderMap,
180 body_bytes: Bytes,
181}
182
183impl TestResponse {
184 pub(crate) async fn new_with_limit(response: Response, max_body_bytes: usize) -> Self {
185 let (parts, body) = response.into_parts();
186 let body_bytes = to_bytes(body, max_body_bytes)
187 .await
188 .unwrap_or_else(|err| {
189 panic!(
190 "Failed to read response body bytes: response body exceeds the maximum configured limit of {} bytes (or another stream error occurred: {})",
191 max_body_bytes, err
192 );
193 });
194
195 Self {
196 status: parts.status,
197 headers: parts.headers,
198 body_bytes,
199 }
200 }
201
202 #[cfg_attr(mutants, mutants::skip)]
204 pub fn status(&self) -> StatusCode {
205 self.status
206 }
207
208 #[cfg_attr(mutants, mutants::skip)]
210 pub fn headers(&self) -> &HeaderMap {
211 &self.headers
212 }
213
214 pub fn body_string(&self) -> String {
216 String::from_utf8_lossy(&self.body_bytes).to_string()
217 }
218
219 pub fn json<T: serde::de::DeserializeOwned>(&self) -> T {
221 serde_json::from_slice(&self.body_bytes).expect("Failed to parse response body as JSON")
222 }
223
224 pub fn cookie_value(&self, name: &str) -> Option<String> {
226 self.headers
227 .get_all(header::SET_COOKIE)
228 .iter()
229 .find_map(|value| {
230 let cookie_str = value.to_str().ok()?;
231 let main_part = cookie_str.split(';').next()?.trim();
232 let (cookie_name, cookie_val) = main_part.split_once('=')?;
233 if cookie_name.trim() == name {
234 Some(cookie_val.trim().to_string())
235 } else {
236 None
237 }
238 })
239 }
240
241 pub fn assert_status(&self, expected: u16) -> &Self {
243 assert_eq!(
244 self.status.as_u16(),
245 expected,
246 "Expected HTTP status code {}, but received {}.\nResponse Body: {}",
247 expected,
248 self.status.as_u16(),
249 self.body_string()
250 );
251 self
252 }
253
254 pub fn assert_see(&self, expected: &str) -> &Self {
256 let body_str = self.body_string();
257 assert!(
258 body_str.contains(expected),
259 "Expected response body to contain '{}', but it did not.\nResponse Body: {}",
260 expected,
261 body_str
262 );
263 self
264 }
265
266 pub fn assert_dont_see(&self, expected: &str) -> &Self {
268 let body_str = self.body_string();
269 assert!(
270 !body_str.contains(expected),
271 "Expected response body NOT to contain '{}', but it did.\nResponse Body: {}",
272 expected,
273 body_str
274 );
275 self
276 }
277
278 pub fn assert_header(&self, name: &str, expected: &str) -> &Self {
280 let header_val = self
281 .headers
282 .get(name)
283 .unwrap_or_else(|| {
284 panic!(
285 "Expected header '{}' to be present, but it was missing",
286 name
287 )
288 })
289 .to_str()
290 .unwrap_or_else(|_| panic!("Failed to convert value of header '{}' to a string", name));
291
292 assert_eq!(
293 header_val, expected,
294 "Expected header '{}' to be '{}', but got '{}'",
295 name, expected, header_val
296 );
297 self
298 }
299
300 pub fn assert_json<
302 T: serde::Serialize + serde::de::DeserializeOwned + std::fmt::Debug + PartialEq,
303 >(
304 &self,
305 expected: &T,
306 ) -> &Self {
307 let actual: T = self.json();
308 assert_eq!(
309 actual, *expected,
310 "JSON structures do not match.\nExpected: {:?}\nActual: {:?}",
311 expected, actual
312 );
313 self
314 }
315
316 pub fn assert_cookie(&self, name: &str, expected: &str) -> &Self {
318 let actual = self.cookie_value(name).unwrap_or_else(|| {
319 panic!(
320 "Expected cookie '{}' to be present, but it was missing",
321 name
322 )
323 });
324
325 assert_eq!(
326 actual, expected,
327 "Expected cookie '{}' to be '{}', but got '{}'",
328 name, expected, actual
329 );
330 self
331 }
332
333 pub fn assert_has_cookie(&self, name: &str) -> &Self {
335 assert!(
336 self.cookie_value(name).is_some(),
337 "Expected cookie '{}' to be present, but it was missing",
338 name
339 );
340 self
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn test_default_max_body() {
350 assert_eq!(DEFAULT_MAX_BODY, 10 * 1024 * 1024); }
352}
353
354#[cfg(test)]
355#[path = "testing_contract_tests.rs"]
356mod contract_tests;