Skip to main content

anvil_test/
client.rs

1//! HTTP test client wrapping a tower::Service constructed from an Application.
2
3use std::convert::Infallible;
4
5use anvil_core::Application;
6use axum::body::Body;
7use axum::Router;
8use http::{Method, Request, StatusCode};
9use http_body_util::BodyExt;
10use serde::de::DeserializeOwned;
11use tower::ServiceExt;
12
13pub struct TestClient {
14    router: Router,
15}
16
17impl TestClient {
18    pub async fn new(app: Application) -> Self {
19        Self {
20            router: app.into_router(),
21        }
22    }
23
24    pub fn from_router(router: Router) -> Self {
25        Self { router }
26    }
27
28    pub async fn get(&self, path: &str) -> TestResponse {
29        self.request(Method::GET, path, None).await
30    }
31
32    pub async fn post(&self, path: &str, body: serde_json::Value) -> TestResponse {
33        self.request(Method::POST, path, Some(body)).await
34    }
35
36    pub async fn put(&self, path: &str, body: serde_json::Value) -> TestResponse {
37        self.request(Method::PUT, path, Some(body)).await
38    }
39
40    pub async fn delete(&self, path: &str) -> TestResponse {
41        self.request(Method::DELETE, path, None).await
42    }
43
44    async fn request(
45        &self,
46        method: Method,
47        path: &str,
48        body: Option<serde_json::Value>,
49    ) -> TestResponse {
50        let mut req = Request::builder().method(method).uri(path);
51        let body = match body {
52            Some(v) => {
53                req = req.header("content-type", "application/json");
54                Body::from(serde_json::to_vec(&v).unwrap())
55            }
56            None => Body::empty(),
57        };
58        let response = self
59            .router
60            .clone()
61            .oneshot(req.body(body).unwrap())
62            .await
63            .unwrap();
64
65        let status = response.status();
66        let bytes = response
67            .into_body()
68            .collect()
69            .await
70            .map(|c| c.to_bytes())
71            .unwrap_or_default();
72
73        TestResponse {
74            status,
75            body: bytes.to_vec(),
76        }
77    }
78}
79
80pub struct TestResponse {
81    pub status: StatusCode,
82    pub body: Vec<u8>,
83}
84
85impl TestResponse {
86    pub fn assert_status(&self, expected: u16) -> &Self {
87        assert_eq!(
88            self.status.as_u16(),
89            expected,
90            "expected status {expected}, got {} — body: {}",
91            self.status,
92            self.body_text()
93        );
94        self
95    }
96
97    pub fn assert_ok(&self) -> &Self {
98        assert!(
99            self.status.is_success(),
100            "expected success, got {} — body: {}",
101            self.status,
102            self.body_text()
103        );
104        self
105    }
106
107    pub fn body_text(&self) -> String {
108        String::from_utf8_lossy(&self.body).to_string()
109    }
110
111    pub fn json<T: DeserializeOwned>(&self) -> T {
112        serde_json::from_slice(&self.body).expect("response was not valid JSON")
113    }
114
115    pub fn assert_contains(&self, needle: &str) -> &Self {
116        let body = self.body_text();
117        assert!(
118            body.contains(needle),
119            "expected response body to contain '{needle}', got: {body}"
120        );
121        self
122    }
123}
124
125// Silence unused import warning in deps-only mode.
126fn _force_link() {
127    let _ = std::any::type_name::<Infallible>();
128}