axum_test/
lib.rs

1//!
2//! Axum Test is a library for writing tests for web servers written using Axum:
3//!
4//!  * You create a [`TestServer`] within a test,
5//!  * use that to build [`TestRequest`] against your application,
6//!  * receive back a [`TestResponse`],
7//!  * then assert the response is how you expect.
8//!
9//! It includes built in support for serializing and deserializing request and response bodies using Serde,
10//! support for cookies and headers, and other common bits you would expect.
11//!
12//! `TestServer` will pass http requests directly to the handler,
13//! or can be run on a random IP / Port address.
14//!
15//! ## Getting Started
16//!
17//! Create a [`TestServer`] running your Axum [`Router`](::axum::Router):
18//!
19//! ```rust
20//! # async fn test() -> Result<(), Box<dyn ::std::error::Error>> {
21//! #
22//! use axum::Router;
23//! use axum::extract::Json;
24//! use axum::routing::put;
25//! use axum_test::TestServer;
26//! use serde_json::json;
27//! use serde_json::Value;
28//!
29//! async fn route_put_user(Json(user): Json<Value>) -> () {
30//!     // todo
31//! }
32//!
33//! let my_app = Router::new()
34//!     .route("/users", put(route_put_user));
35//!
36//! let server = TestServer::new(my_app)?;
37//! #
38//! # Ok(())
39//! # }
40//! ```
41//!
42//! Then make requests against it:
43//!
44//! ```rust
45//! # async fn test() -> Result<(), Box<dyn ::std::error::Error>> {
46//! #
47//! # use axum::Router;
48//! # use axum::extract::Json;
49//! # use axum::routing::put;
50//! # use axum_test::TestServer;
51//! # use serde_json::json;
52//! # use serde_json::Value;
53//! #
54//! # async fn put_user(Json(user): Json<Value>) -> () {}
55//! #
56//! # let my_app = Router::new()
57//! #     .route("/users", put(put_user));
58//! #
59//! # let server = TestServer::new(my_app)?;
60//! #
61//! let response = server.put("/users")
62//!     .json(&json!({
63//!         "username": "Terrance Pencilworth",
64//!     }))
65//!     .await;
66//! #
67//! # Ok(())
68//! # }
69//! ```
70//!
71
72#![allow(clippy::module_inception)]
73#![allow(clippy::derivable_impls)]
74#![allow(clippy::manual_range_contains)]
75#![forbid(unsafe_code)]
76#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
77
78pub(crate) mod internals;
79
80pub mod multipart;
81
82pub mod transport_layer;
83pub mod util;
84
85mod test_request;
86pub use self::test_request::*;
87
88mod test_response;
89pub use self::test_response::*;
90
91mod test_server_builder;
92pub use self::test_server_builder::*;
93
94mod test_server_config;
95pub use self::test_server_config::*;
96
97mod test_server;
98pub use self::test_server::*;
99
100#[cfg(feature = "ws")]
101mod test_web_socket;
102#[cfg(feature = "ws")]
103pub use self::test_web_socket::*;
104#[cfg(feature = "ws")]
105pub use tokio_tungstenite::tungstenite::Message as WsMessage;
106
107mod transport;
108pub use self::transport::*;
109
110pub mod expect_json;
111
112pub use http;
113
114#[cfg(test)]
115mod integrated_test_cookie_saving {
116    use super::*;
117
118    use axum::extract::Request;
119    use axum::routing::get;
120    use axum::routing::post;
121    use axum::routing::put;
122    use axum::Router;
123    use axum_extra::extract::cookie::Cookie as AxumCookie;
124    use axum_extra::extract::cookie::CookieJar;
125    use cookie::time::OffsetDateTime;
126    use cookie::Cookie;
127    use http_body_util::BodyExt;
128    use std::time::Duration;
129
130    const TEST_COOKIE_NAME: &'static str = &"test-cookie";
131
132    async fn get_cookie(cookies: CookieJar) -> (CookieJar, String) {
133        let cookie = cookies.get(&TEST_COOKIE_NAME);
134        let cookie_value = cookie
135            .map(|c| c.value().to_string())
136            .unwrap_or_else(|| "cookie-not-found".to_string());
137
138        (cookies, cookie_value)
139    }
140
141    async fn put_cookie(mut cookies: CookieJar, request: Request) -> (CookieJar, &'static str) {
142        let body_bytes = request
143            .into_body()
144            .collect()
145            .await
146            .expect("Should extract the body")
147            .to_bytes();
148        let body_text: String = String::from_utf8_lossy(&body_bytes).to_string();
149        let cookie = AxumCookie::new(TEST_COOKIE_NAME, body_text);
150        cookies = cookies.add(cookie);
151
152        (cookies, &"done")
153    }
154
155    async fn post_expire_cookie(mut cookies: CookieJar) -> (CookieJar, &'static str) {
156        let mut cookie = AxumCookie::new(TEST_COOKIE_NAME, "expired".to_string());
157        let expired_time = OffsetDateTime::now_utc() - Duration::from_secs(1);
158        cookie.set_expires(expired_time);
159        cookies = cookies.add(cookie);
160
161        (cookies, &"done")
162    }
163
164    fn new_test_router() -> Router {
165        Router::new()
166            .route("/cookie", put(put_cookie))
167            .route("/cookie", get(get_cookie))
168            .route("/expire", post(post_expire_cookie))
169    }
170
171    #[tokio::test]
172    async fn it_should_not_pass_cookies_created_back_up_to_server_by_default() {
173        // Run the server.
174        let server = TestServer::new(new_test_router()).expect("Should create test server");
175
176        // Create a cookie.
177        server.put(&"/cookie").text(&"new-cookie").await;
178
179        // Check it comes back.
180        let response_text = server.get(&"/cookie").await.text();
181
182        assert_eq!(response_text, "cookie-not-found");
183    }
184
185    #[tokio::test]
186    async fn it_should_not_pass_cookies_created_back_up_to_server_when_turned_off() {
187        // Run the server.
188        let server = TestServer::builder()
189            .do_not_save_cookies()
190            .build(new_test_router())
191            .expect("Should create test server");
192
193        // Create a cookie.
194        server.put(&"/cookie").text(&"new-cookie").await;
195
196        // Check it comes back.
197        let response_text = server.get(&"/cookie").await.text();
198
199        assert_eq!(response_text, "cookie-not-found");
200    }
201
202    #[tokio::test]
203    async fn it_should_pass_cookies_created_back_up_to_server_automatically() {
204        // Run the server.
205        let server = TestServer::builder()
206            .save_cookies()
207            .build(new_test_router())
208            .expect("Should create test server");
209
210        // Create a cookie.
211        server.put(&"/cookie").text(&"cookie-found!").await;
212
213        // Check it comes back.
214        let response_text = server.get(&"/cookie").await.text();
215
216        assert_eq!(response_text, "cookie-found!");
217    }
218
219    #[tokio::test]
220    async fn it_should_pass_cookies_created_back_up_to_server_when_turned_on_for_request() {
221        // Run the server.
222        let server = TestServer::builder()
223            .do_not_save_cookies() // it's off by default!
224            .build(new_test_router())
225            .expect("Should create test server");
226
227        // Create a cookie.
228        server
229            .put(&"/cookie")
230            .text(&"cookie-found!")
231            .save_cookies()
232            .await;
233
234        // Check it comes back.
235        let response_text = server.get(&"/cookie").await.text();
236
237        assert_eq!(response_text, "cookie-found!");
238    }
239
240    #[tokio::test]
241    async fn it_should_wipe_cookies_cleared_by_request() {
242        // Run the server.
243        let server = TestServer::builder()
244            .do_not_save_cookies() // it's off by default!
245            .build(new_test_router())
246            .expect("Should create test server");
247
248        // Create a cookie.
249        server
250            .put(&"/cookie")
251            .text(&"cookie-found!")
252            .save_cookies()
253            .await;
254
255        // Check it comes back.
256        let response_text = server.get(&"/cookie").clear_cookies().await.text();
257
258        assert_eq!(response_text, "cookie-not-found");
259    }
260
261    #[tokio::test]
262    async fn it_should_wipe_cookies_cleared_by_test_server() {
263        // Run the server.
264        let mut server = TestServer::builder()
265            .do_not_save_cookies() // it's off by default!
266            .build(new_test_router())
267            .expect("Should create test server");
268
269        // Create a cookie.
270        server
271            .put(&"/cookie")
272            .text(&"cookie-found!")
273            .save_cookies()
274            .await;
275
276        server.clear_cookies();
277
278        // Check it comes back.
279        let response_text = server.get(&"/cookie").await.text();
280
281        assert_eq!(response_text, "cookie-not-found");
282    }
283
284    #[tokio::test]
285    async fn it_should_send_cookies_added_to_request() {
286        // Run the server.
287        let server = TestServer::builder()
288            .do_not_save_cookies() // it's off by default!
289            .build(new_test_router())
290            .expect("Should create test server");
291
292        // Check it comes back.
293        let cookie = Cookie::new(TEST_COOKIE_NAME, "my-custom-cookie");
294
295        let response_text = server.get(&"/cookie").add_cookie(cookie).await.text();
296
297        assert_eq!(response_text, "my-custom-cookie");
298    }
299
300    #[tokio::test]
301    async fn it_should_send_cookies_added_to_test_server() {
302        // Run the server.
303        let mut server = TestServer::builder()
304            .do_not_save_cookies() // it's off by default!
305            .build(new_test_router())
306            .expect("Should create test server");
307
308        // Check it comes back.
309        let cookie = Cookie::new(TEST_COOKIE_NAME, "my-custom-cookie");
310        server.add_cookie(cookie);
311
312        let response_text = server.get(&"/cookie").await.text();
313
314        assert_eq!(response_text, "my-custom-cookie");
315    }
316
317    #[tokio::test]
318    async fn it_should_remove_expired_cookies_from_later_requests() {
319        // Run the server.
320        let mut server = TestServer::new(new_test_router()).expect("Should create test server");
321        server.save_cookies();
322
323        // Create a cookie.
324        server.put(&"/cookie").text(&"cookie-found!").await;
325
326        // Check it comes back.
327        let response_text = server.get(&"/cookie").await.text();
328        assert_eq!(response_text, "cookie-found!");
329
330        server.post(&"/expire").await;
331
332        // Then expire the cookie.
333        let found_cookie = server.post(&"/expire").await.maybe_cookie(TEST_COOKIE_NAME);
334        assert!(found_cookie.is_some());
335
336        // It's no longer found
337        let response_text = server.get(&"/cookie").await.text();
338        assert_eq!(response_text, "cookie-not-found");
339    }
340}
341
342#[cfg(feature = "typed-routing")]
343#[cfg(test)]
344mod integrated_test_typed_routing_and_query {
345    use super::*;
346
347    use axum::extract::Query;
348    use axum::Router;
349    use axum_extra::routing::RouterExt;
350    use axum_extra::routing::TypedPath;
351    use serde::Deserialize;
352    use serde::Serialize;
353
354    #[derive(TypedPath, Deserialize)]
355    #[typed_path("/path-query/{id}")]
356    struct TestingPathQuery {
357        id: u32,
358    }
359
360    #[derive(Serialize, Deserialize)]
361    struct QueryParams {
362        param: String,
363        other: Option<String>,
364    }
365
366    async fn route_get_with_param(
367        TestingPathQuery { id }: TestingPathQuery,
368        Query(params): Query<QueryParams>,
369    ) -> String {
370        let query = params.param;
371        if let Some(other) = params.other {
372            format!("get {id}, {query}&{other}")
373        } else {
374            format!("get {id}, {query}")
375        }
376    }
377
378    fn new_app() -> Router {
379        Router::new().typed_get(route_get_with_param)
380    }
381
382    #[tokio::test]
383    async fn it_should_send_typed_get_with_query_params() {
384        let server = TestServer::new(new_app()).unwrap();
385        let path = TestingPathQuery { id: 123 }.with_query_params(QueryParams {
386            param: "with-typed-query".to_string(),
387            other: None,
388        });
389
390        server
391            .typed_get(&path)
392            .expect_success()
393            .await
394            .assert_text("get 123, with-typed-query");
395    }
396
397    #[tokio::test]
398    async fn it_should_send_typed_get_with_added_query_param() {
399        let server = TestServer::new(new_app()).unwrap();
400        let path = TestingPathQuery { id: 123 };
401
402        server
403            .typed_get(&path)
404            .add_query_param("param", "with-added-query")
405            .expect_success()
406            .await
407            .assert_text("get 123, with-added-query");
408    }
409
410    #[tokio::test]
411    async fn it_should_send_both_typed_and_added_query() {
412        let server = TestServer::new(new_app()).unwrap();
413        let path = TestingPathQuery { id: 123 }.with_query_params(QueryParams {
414            param: "with-typed-query".to_string(),
415            other: None,
416        });
417
418        server
419            .typed_get(&path)
420            .add_query_param("other", "with-added-query")
421            .expect_success()
422            .await
423            .assert_text("get 123, with-typed-query&with-added-query");
424    }
425
426    #[tokio::test]
427    async fn it_should_send_replaced_query_when_cleared() {
428        let server = TestServer::new(new_app()).unwrap();
429        let path = TestingPathQuery { id: 123 }.with_query_params(QueryParams {
430            param: "with-typed-query".to_string(),
431            other: Some("with-typed-other".to_string()),
432        });
433
434        server
435            .typed_get(&path)
436            .clear_query_params()
437            .add_query_param("param", "with-added-query")
438            .expect_success()
439            .await
440            .assert_text("get 123, with-added-query");
441    }
442}