Skip to main content

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 app = Router::new()
34//!     .route("/users", put(route_put_user));
35//!
36//! let server = TestServer::new(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 app = Router::new()
57//! #     .route("/users", put(put_user));
58//! #
59//! # let server = TestServer::new(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//! # Actix Web
72//!
73//! Actix Web is also supported. All of the code examples through the
74//! docs presume the use of Axum, however the Axum Test side is
75//! identical for Actix Web.
76//!
77//! ```rust
78//! # async fn test() -> Result<(), Box<dyn ::std::error::Error>> {
79//! use actix_web::App;
80//! use actix_web::HttpResponse;
81//! use actix_web::web;
82//! use serde_json::Value;
83//! use serde_json::json;
84//!
85//! use axum_test::TestServer;
86//!
87//! async fn route_put_users(
88//!     body: web::Json<Value>,
89//! ) -> HttpResponse {
90//!     // ... todo ...
91//!
92//!     HttpResponse::Ok().json(json!("done!"))
93//! }
94//!
95//! let new_app = || App::new().route("/json", web::put().to(route_put_users));
96//! let server = TestServer::new(new_app);
97//!
98//! let response = server
99//!     .put("/users")
100//!     .json(&json!({
101//!         "username": "Terrance Pencilworth",
102//!     }))
103//!     .await;
104//! #
105//! # Ok(())
106//! # }
107//! ```
108//!
109
110#![allow(clippy::module_inception)]
111#![allow(clippy::collapsible_if)]
112#![allow(clippy::derivable_impls)]
113#![allow(clippy::manual_range_contains)]
114#![forbid(unsafe_code)]
115#![cfg_attr(docsrs, feature(doc_cfg))]
116
117pub(crate) mod internals;
118
119pub mod multipart;
120
121pub mod transport_layer;
122pub mod util;
123
124mod test_request;
125pub use self::test_request::*;
126
127mod test_response;
128pub use self::test_response::*;
129
130mod test_server_builder;
131pub use self::test_server_builder::*;
132
133mod test_server_config;
134pub use self::test_server_config::*;
135
136mod test_server;
137pub use self::test_server::*;
138
139#[cfg(feature = "ws")]
140mod test_web_socket;
141#[cfg(feature = "ws")]
142pub use self::test_web_socket::*;
143#[cfg(feature = "ws")]
144pub use tokio_tungstenite::tungstenite::Message as WsMessage;
145
146mod transport;
147pub use self::transport::*;
148
149pub mod expect_json;
150
151pub use http;
152
153#[cfg(test)]
154mod testing;
155
156#[cfg(test)]
157mod integrated_test_cookie_saving {
158    use super::*;
159    use axum::Router;
160    use axum::extract::Request;
161    use axum::routing::get;
162    use axum::routing::post;
163    use axum::routing::put;
164    use axum_extra::extract::cookie::Cookie as AxumCookie;
165    use axum_extra::extract::cookie::CookieJar;
166    use cookie::Cookie;
167    use cookie::time::OffsetDateTime;
168    use http_body_util::BodyExt;
169    use std::time::Duration;
170
171    const TEST_COOKIE_NAME: &'static str = &"test-cookie";
172
173    async fn get_cookie(cookies: CookieJar) -> (CookieJar, String) {
174        let cookie = cookies.get(&TEST_COOKIE_NAME);
175        let cookie_value = cookie
176            .map(|c| c.value().to_string())
177            .unwrap_or_else(|| "cookie-not-found".to_string());
178
179        (cookies, cookie_value)
180    }
181
182    async fn put_cookie(mut cookies: CookieJar, request: Request) -> (CookieJar, &'static str) {
183        let body_bytes = request
184            .into_body()
185            .collect()
186            .await
187            .expect("Should extract the body")
188            .to_bytes();
189        let body_text: String = String::from_utf8_lossy(&body_bytes).to_string();
190        let cookie = AxumCookie::new(TEST_COOKIE_NAME, body_text);
191        cookies = cookies.add(cookie);
192
193        (cookies, &"done")
194    }
195
196    async fn post_expire_cookie(mut cookies: CookieJar) -> (CookieJar, &'static str) {
197        let mut cookie = AxumCookie::new(TEST_COOKIE_NAME, "expired".to_string());
198        let expired_time = OffsetDateTime::now_utc() - Duration::from_secs(1);
199        cookie.set_expires(expired_time);
200        cookies = cookies.add(cookie);
201
202        (cookies, &"done")
203    }
204
205    fn new_test_router() -> Router {
206        Router::new()
207            .route("/cookie", put(put_cookie))
208            .route("/cookie", get(get_cookie))
209            .route("/expire", post(post_expire_cookie))
210    }
211
212    #[tokio::test]
213    async fn it_should_not_pass_cookies_created_back_up_to_server_by_default() {
214        // Run the server.
215        let server = TestServer::new(new_test_router());
216
217        // Create a cookie.
218        server.put(&"/cookie").text(&"new-cookie").await;
219
220        // Check it comes back.
221        let response_text = server.get(&"/cookie").await.text();
222
223        assert_eq!(response_text, "cookie-not-found");
224    }
225
226    #[tokio::test]
227    async fn it_should_not_pass_cookies_created_back_up_to_server_when_turned_off() {
228        // Run the server.
229        let server = TestServer::builder()
230            .do_not_save_cookies()
231            .build(new_test_router());
232
233        // Create a cookie.
234        server.put(&"/cookie").text(&"new-cookie").await;
235
236        // Check it comes back.
237        let response_text = server.get(&"/cookie").await.text();
238
239        assert_eq!(response_text, "cookie-not-found");
240    }
241
242    #[tokio::test]
243    async fn it_should_pass_cookies_created_back_up_to_server_automatically() {
244        // Run the server.
245        let server = TestServer::builder()
246            .save_cookies()
247            .build(new_test_router());
248
249        // Create a cookie.
250        server.put(&"/cookie").text(&"cookie-found!").await;
251
252        // Check it comes back.
253        let response_text = server.get(&"/cookie").await.text();
254
255        assert_eq!(response_text, "cookie-found!");
256    }
257
258    #[tokio::test]
259    async fn it_should_pass_cookies_created_back_up_to_server_when_turned_on_for_request() {
260        // Run the server.
261        let server = TestServer::builder()
262            .do_not_save_cookies() // it's off by default!
263            .build(new_test_router());
264
265        // Create a cookie.
266        server
267            .put(&"/cookie")
268            .text(&"cookie-found!")
269            .save_cookies()
270            .await;
271
272        // Check it comes back.
273        let response_text = server.get(&"/cookie").await.text();
274
275        assert_eq!(response_text, "cookie-found!");
276    }
277
278    #[tokio::test]
279    async fn it_should_wipe_cookies_cleared_by_request() {
280        // Run the server.
281        let server = TestServer::builder()
282            .do_not_save_cookies() // it's off by default!
283            .build(new_test_router());
284
285        // Create a cookie.
286        server
287            .put(&"/cookie")
288            .text(&"cookie-found!")
289            .save_cookies()
290            .await;
291
292        // Check it comes back.
293        let response_text = server.get(&"/cookie").clear_cookies().await.text();
294
295        assert_eq!(response_text, "cookie-not-found");
296    }
297
298    #[tokio::test]
299    async fn it_should_wipe_cookies_cleared_by_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
305        // Create a cookie.
306        server
307            .put(&"/cookie")
308            .text(&"cookie-found!")
309            .save_cookies()
310            .await;
311
312        server.clear_cookies();
313
314        // Check it comes back.
315        let response_text = server.get(&"/cookie").await.text();
316
317        assert_eq!(response_text, "cookie-not-found");
318    }
319
320    #[tokio::test]
321    async fn it_should_send_cookies_added_to_request() {
322        // Run the server.
323        let server = TestServer::builder()
324            .do_not_save_cookies() // it's off by default!
325            .build(new_test_router());
326
327        // Check it comes back.
328        let cookie = Cookie::new(TEST_COOKIE_NAME, "my-custom-cookie");
329
330        let response_text = server.get(&"/cookie").add_cookie(cookie).await.text();
331
332        assert_eq!(response_text, "my-custom-cookie");
333    }
334
335    #[tokio::test]
336    async fn it_should_send_cookies_added_to_test_server() {
337        // Run the server.
338        let mut server = TestServer::builder()
339            .do_not_save_cookies() // it's off by default!
340            .build(new_test_router());
341
342        // Check it comes back.
343        let cookie = Cookie::new(TEST_COOKIE_NAME, "my-custom-cookie");
344        server.add_cookie(cookie);
345
346        let response_text = server.get(&"/cookie").await.text();
347
348        assert_eq!(response_text, "my-custom-cookie");
349    }
350
351    #[tokio::test]
352    async fn it_should_remove_expired_cookies_from_later_requests() {
353        // Run the server.
354        let mut server = TestServer::new(new_test_router());
355        server.save_cookies();
356
357        // Create a cookie.
358        server.put(&"/cookie").text(&"cookie-found!").await;
359
360        // Check it comes back.
361        let response_text = server.get(&"/cookie").await.text();
362        assert_eq!(response_text, "cookie-found!");
363
364        server.post(&"/expire").await;
365
366        // Then expire the cookie.
367        let found_cookie = server.post(&"/expire").await.maybe_cookie(TEST_COOKIE_NAME);
368        assert!(found_cookie.is_some());
369
370        // It's no longer found
371        let response_text = server.get(&"/cookie").await.text();
372        assert_eq!(response_text, "cookie-not-found");
373    }
374}
375
376#[cfg(feature = "typed-routing")]
377#[cfg(test)]
378mod integrated_test_typed_routing_and_query {
379    use super::*;
380    use axum::Router;
381    use axum::extract::Query;
382    use axum_extra::routing::RouterExt;
383    use axum_extra::routing::TypedPath;
384    use serde::Deserialize;
385    use serde::Serialize;
386
387    #[derive(TypedPath, Deserialize)]
388    #[typed_path("/path-query/{id}")]
389    struct TestingPathQuery {
390        id: u32,
391    }
392
393    #[derive(Serialize, Deserialize)]
394    struct QueryParams {
395        param: String,
396        other: Option<String>,
397    }
398
399    async fn route_get_with_param(
400        TestingPathQuery { id }: TestingPathQuery,
401        Query(params): Query<QueryParams>,
402    ) -> String {
403        let query = params.param;
404        if let Some(other) = params.other {
405            format!("get {id}, {query}&{other}")
406        } else {
407            format!("get {id}, {query}")
408        }
409    }
410
411    fn new_app() -> Router {
412        Router::new().typed_get(route_get_with_param)
413    }
414
415    #[tokio::test]
416    async fn it_should_send_typed_get_with_query_params() {
417        let server = TestServer::new(new_app());
418        let path = TestingPathQuery { id: 123 }.with_query_params(QueryParams {
419            param: "with-typed-query".to_string(),
420            other: None,
421        });
422
423        server
424            .typed_get(&path)
425            .expect_success()
426            .await
427            .assert_text("get 123, with-typed-query");
428    }
429
430    #[tokio::test]
431    async fn it_should_send_typed_get_with_added_query_param() {
432        let server = TestServer::new(new_app());
433        let path = TestingPathQuery { id: 123 };
434
435        server
436            .typed_get(&path)
437            .add_query_param("param", "with-added-query")
438            .expect_success()
439            .await
440            .assert_text("get 123, with-added-query");
441    }
442
443    #[tokio::test]
444    async fn it_should_send_both_typed_and_added_query() {
445        let server = TestServer::new(new_app());
446        let path = TestingPathQuery { id: 123 }.with_query_params(QueryParams {
447            param: "with-typed-query".to_string(),
448            other: None,
449        });
450
451        server
452            .typed_get(&path)
453            .add_query_param("other", "with-added-query")
454            .expect_success()
455            .await
456            .assert_text("get 123, with-typed-query&with-added-query");
457    }
458
459    #[tokio::test]
460    async fn it_should_send_replaced_query_when_cleared() {
461        let server = TestServer::new(new_app());
462        let path = TestingPathQuery { id: 123 }.with_query_params(QueryParams {
463            param: "with-typed-query".to_string(),
464            other: Some("with-typed-other".to_string()),
465        });
466
467        server
468            .typed_get(&path)
469            .clear_query_params()
470            .add_query_param("param", "with-added-query")
471            .expect_success()
472            .await
473            .assert_text("get 123, with-added-query");
474    }
475}