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