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))]
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 testing;
116
117#[cfg(test)]
118mod integrated_test_cookie_saving {
119    use super::*;
120
121    use axum::Router;
122    use axum::extract::Request;
123    use axum::routing::get;
124    use axum::routing::post;
125    use axum::routing::put;
126    use axum_extra::extract::cookie::Cookie as AxumCookie;
127    use axum_extra::extract::cookie::CookieJar;
128    use cookie::Cookie;
129    use cookie::time::OffsetDateTime;
130    use http_body_util::BodyExt;
131    use std::time::Duration;
132
133    const TEST_COOKIE_NAME: &'static str = &"test-cookie";
134
135    async fn get_cookie(cookies: CookieJar) -> (CookieJar, String) {
136        let cookie = cookies.get(&TEST_COOKIE_NAME);
137        let cookie_value = cookie
138            .map(|c| c.value().to_string())
139            .unwrap_or_else(|| "cookie-not-found".to_string());
140
141        (cookies, cookie_value)
142    }
143
144    async fn put_cookie(mut cookies: CookieJar, request: Request) -> (CookieJar, &'static str) {
145        let body_bytes = request
146            .into_body()
147            .collect()
148            .await
149            .expect("Should extract the body")
150            .to_bytes();
151        let body_text: String = String::from_utf8_lossy(&body_bytes).to_string();
152        let cookie = AxumCookie::new(TEST_COOKIE_NAME, body_text);
153        cookies = cookies.add(cookie);
154
155        (cookies, &"done")
156    }
157
158    async fn post_expire_cookie(mut cookies: CookieJar) -> (CookieJar, &'static str) {
159        let mut cookie = AxumCookie::new(TEST_COOKIE_NAME, "expired".to_string());
160        let expired_time = OffsetDateTime::now_utc() - Duration::from_secs(1);
161        cookie.set_expires(expired_time);
162        cookies = cookies.add(cookie);
163
164        (cookies, &"done")
165    }
166
167    fn new_test_router() -> Router {
168        Router::new()
169            .route("/cookie", put(put_cookie))
170            .route("/cookie", get(get_cookie))
171            .route("/expire", post(post_expire_cookie))
172    }
173
174    #[tokio::test]
175    async fn it_should_not_pass_cookies_created_back_up_to_server_by_default() {
176        // Run the server.
177        let server = TestServer::new(new_test_router()).expect("Should create test server");
178
179        // Create a cookie.
180        server.put(&"/cookie").text(&"new-cookie").await;
181
182        // Check it comes back.
183        let response_text = server.get(&"/cookie").await.text();
184
185        assert_eq!(response_text, "cookie-not-found");
186    }
187
188    #[tokio::test]
189    async fn it_should_not_pass_cookies_created_back_up_to_server_when_turned_off() {
190        // Run the server.
191        let server = TestServer::builder()
192            .do_not_save_cookies()
193            .build(new_test_router())
194            .expect("Should create test server");
195
196        // Create a cookie.
197        server.put(&"/cookie").text(&"new-cookie").await;
198
199        // Check it comes back.
200        let response_text = server.get(&"/cookie").await.text();
201
202        assert_eq!(response_text, "cookie-not-found");
203    }
204
205    #[tokio::test]
206    async fn it_should_pass_cookies_created_back_up_to_server_automatically() {
207        // Run the server.
208        let server = TestServer::builder()
209            .save_cookies()
210            .build(new_test_router())
211            .expect("Should create test server");
212
213        // Create a cookie.
214        server.put(&"/cookie").text(&"cookie-found!").await;
215
216        // Check it comes back.
217        let response_text = server.get(&"/cookie").await.text();
218
219        assert_eq!(response_text, "cookie-found!");
220    }
221
222    #[tokio::test]
223    async fn it_should_pass_cookies_created_back_up_to_server_when_turned_on_for_request() {
224        // Run the server.
225        let server = TestServer::builder()
226            .do_not_save_cookies() // it's off by default!
227            .build(new_test_router())
228            .expect("Should create test server");
229
230        // Create a cookie.
231        server
232            .put(&"/cookie")
233            .text(&"cookie-found!")
234            .save_cookies()
235            .await;
236
237        // Check it comes back.
238        let response_text = server.get(&"/cookie").await.text();
239
240        assert_eq!(response_text, "cookie-found!");
241    }
242
243    #[tokio::test]
244    async fn it_should_wipe_cookies_cleared_by_request() {
245        // Run the server.
246        let server = TestServer::builder()
247            .do_not_save_cookies() // it's off by default!
248            .build(new_test_router())
249            .expect("Should create test server");
250
251        // Create a cookie.
252        server
253            .put(&"/cookie")
254            .text(&"cookie-found!")
255            .save_cookies()
256            .await;
257
258        // Check it comes back.
259        let response_text = server.get(&"/cookie").clear_cookies().await.text();
260
261        assert_eq!(response_text, "cookie-not-found");
262    }
263
264    #[tokio::test]
265    async fn it_should_wipe_cookies_cleared_by_test_server() {
266        // Run the server.
267        let mut server = TestServer::builder()
268            .do_not_save_cookies() // it's off by default!
269            .build(new_test_router())
270            .expect("Should create test server");
271
272        // Create a cookie.
273        server
274            .put(&"/cookie")
275            .text(&"cookie-found!")
276            .save_cookies()
277            .await;
278
279        server.clear_cookies();
280
281        // Check it comes back.
282        let response_text = server.get(&"/cookie").await.text();
283
284        assert_eq!(response_text, "cookie-not-found");
285    }
286
287    #[tokio::test]
288    async fn it_should_send_cookies_added_to_request() {
289        // Run the server.
290        let server = TestServer::builder()
291            .do_not_save_cookies() // it's off by default!
292            .build(new_test_router())
293            .expect("Should create test server");
294
295        // Check it comes back.
296        let cookie = Cookie::new(TEST_COOKIE_NAME, "my-custom-cookie");
297
298        let response_text = server.get(&"/cookie").add_cookie(cookie).await.text();
299
300        assert_eq!(response_text, "my-custom-cookie");
301    }
302
303    #[tokio::test]
304    async fn it_should_send_cookies_added_to_test_server() {
305        // Run the server.
306        let mut server = TestServer::builder()
307            .do_not_save_cookies() // it's off by default!
308            .build(new_test_router())
309            .expect("Should create test server");
310
311        // Check it comes back.
312        let cookie = Cookie::new(TEST_COOKIE_NAME, "my-custom-cookie");
313        server.add_cookie(cookie);
314
315        let response_text = server.get(&"/cookie").await.text();
316
317        assert_eq!(response_text, "my-custom-cookie");
318    }
319
320    #[tokio::test]
321    async fn it_should_remove_expired_cookies_from_later_requests() {
322        // Run the server.
323        let mut server = TestServer::new(new_test_router()).expect("Should create test server");
324        server.save_cookies();
325
326        // Create a cookie.
327        server.put(&"/cookie").text(&"cookie-found!").await;
328
329        // Check it comes back.
330        let response_text = server.get(&"/cookie").await.text();
331        assert_eq!(response_text, "cookie-found!");
332
333        server.post(&"/expire").await;
334
335        // Then expire the cookie.
336        let found_cookie = server.post(&"/expire").await.maybe_cookie(TEST_COOKIE_NAME);
337        assert!(found_cookie.is_some());
338
339        // It's no longer found
340        let response_text = server.get(&"/cookie").await.text();
341        assert_eq!(response_text, "cookie-not-found");
342    }
343}
344
345#[cfg(feature = "typed-routing")]
346#[cfg(test)]
347mod integrated_test_typed_routing_and_query {
348    use super::*;
349
350    use axum::Router;
351    use axum::extract::Query;
352    use axum_extra::routing::RouterExt;
353    use axum_extra::routing::TypedPath;
354    use serde::Deserialize;
355    use serde::Serialize;
356
357    #[derive(TypedPath, Deserialize)]
358    #[typed_path("/path-query/{id}")]
359    struct TestingPathQuery {
360        id: u32,
361    }
362
363    #[derive(Serialize, Deserialize)]
364    struct QueryParams {
365        param: String,
366        other: Option<String>,
367    }
368
369    async fn route_get_with_param(
370        TestingPathQuery { id }: TestingPathQuery,
371        Query(params): Query<QueryParams>,
372    ) -> String {
373        let query = params.param;
374        if let Some(other) = params.other {
375            format!("get {id}, {query}&{other}")
376        } else {
377            format!("get {id}, {query}")
378        }
379    }
380
381    fn new_app() -> Router {
382        Router::new().typed_get(route_get_with_param)
383    }
384
385    #[tokio::test]
386    async fn it_should_send_typed_get_with_query_params() {
387        let server = TestServer::new(new_app()).unwrap();
388        let path = TestingPathQuery { id: 123 }.with_query_params(QueryParams {
389            param: "with-typed-query".to_string(),
390            other: None,
391        });
392
393        server
394            .typed_get(&path)
395            .expect_success()
396            .await
397            .assert_text("get 123, with-typed-query");
398    }
399
400    #[tokio::test]
401    async fn it_should_send_typed_get_with_added_query_param() {
402        let server = TestServer::new(new_app()).unwrap();
403        let path = TestingPathQuery { id: 123 };
404
405        server
406            .typed_get(&path)
407            .add_query_param("param", "with-added-query")
408            .expect_success()
409            .await
410            .assert_text("get 123, with-added-query");
411    }
412
413    #[tokio::test]
414    async fn it_should_send_both_typed_and_added_query() {
415        let server = TestServer::new(new_app()).unwrap();
416        let path = TestingPathQuery { id: 123 }.with_query_params(QueryParams {
417            param: "with-typed-query".to_string(),
418            other: None,
419        });
420
421        server
422            .typed_get(&path)
423            .add_query_param("other", "with-added-query")
424            .expect_success()
425            .await
426            .assert_text("get 123, with-typed-query&with-added-query");
427    }
428
429    #[tokio::test]
430    async fn it_should_send_replaced_query_when_cleared() {
431        let server = TestServer::new(new_app()).unwrap();
432        let path = TestingPathQuery { id: 123 }.with_query_params(QueryParams {
433            param: "with-typed-query".to_string(),
434            other: Some("with-typed-other".to_string()),
435        });
436
437        server
438            .typed_get(&path)
439            .clear_query_params()
440            .add_query_param("param", "with-added-query")
441            .expect_success()
442            .await
443            .assert_text("get 123, with-added-query");
444    }
445}