Skip to main content

purwa_testing/
http.rs

1//! Tower/Axum one-shot helpers for integration-style tests.
2
3use axum::Router;
4use axum::body::Body;
5use axum::http::{Method, Request, StatusCode};
6use axum::response::Response;
7use bytes::Bytes;
8use http_body_util::BodyExt;
9use tower::ServiceExt;
10
11/// Sends one HTTP request through the router and returns the response.
12///
13/// For Axum’s default router the service error is [`std::convert::Infallible`]; this should not panic.
14pub async fn oneshot(router: Router, request: Request<Body>) -> Response {
15    router.oneshot(request).await.unwrap()
16}
17
18/// Builds a GET (empty body) request, runs [`oneshot`], returns the status code.
19pub async fn oneshot_status(router: Router, uri: &str) -> StatusCode {
20    let req = Request::builder()
21        .method(Method::GET)
22        .uri(uri)
23        .body(Body::empty())
24        .expect("valid request");
25    oneshot(router, req).await.status()
26}
27
28/// Same as [`oneshot_status`] but allows choosing the method.
29pub async fn oneshot_status_with_method(router: Router, method: Method, uri: &str) -> StatusCode {
30    let req = Request::builder()
31        .method(method)
32        .uri(uri)
33        .body(Body::empty())
34        .expect("valid request");
35    oneshot(router, req).await.status()
36}
37
38/// Collects the full response body as bytes (useful for assertions).
39pub async fn oneshot_body_bytes(router: Router, uri: &str) -> Bytes {
40    let req = Request::builder()
41        .method(Method::GET)
42        .uri(uri)
43        .body(Body::empty())
44        .expect("valid request");
45    let res = oneshot(router, req).await;
46    response_body_bytes(res).await.expect("read body")
47}
48
49/// Reads the entire body after the response has been produced.
50pub async fn response_body_bytes(response: Response) -> Result<Bytes, std::io::Error> {
51    let collected = response
52        .into_body()
53        .collect()
54        .await
55        .map_err(|e| std::io::Error::other(e.to_string()))?;
56    Ok(collected.to_bytes())
57}
58
59/// Error from [`json_body`].
60#[derive(Debug)]
61pub enum JsonBodyError {
62    Body(std::io::Error),
63    Json(serde_json::Error),
64}
65
66impl std::fmt::Display for JsonBodyError {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            JsonBodyError::Body(e) => write!(f, "body: {e}"),
70            JsonBodyError::Json(e) => write!(f, "json: {e}"),
71        }
72    }
73}
74
75impl std::error::Error for JsonBodyError {
76    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
77        match self {
78            JsonBodyError::Body(e) => Some(e),
79            JsonBodyError::Json(e) => Some(e),
80        }
81    }
82}
83
84/// Collects the body and parses it as JSON.
85pub async fn json_body(response: Response) -> Result<serde_json::Value, JsonBodyError> {
86    let bytes = response_body_bytes(response)
87        .await
88        .map_err(JsonBodyError::Body)?;
89    serde_json::from_slice(&bytes).map_err(JsonBodyError::Json)
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use axum::routing::get;
96
97    #[tokio::test]
98    async fn oneshot_hits_route() {
99        let app = Router::new().route("/hi", get(|| async { "hello" }));
100        let status = oneshot_status(app, "/hi").await;
101        assert_eq!(status, StatusCode::OK);
102    }
103
104    #[tokio::test]
105    async fn oneshot_body_bytes_round_trip() {
106        let app = Router::new().route("/x", get(|| async { "payload" }));
107        let bytes = oneshot_body_bytes(app, "/x").await;
108        assert_eq!(&bytes[..], b"payload");
109    }
110
111    #[tokio::test]
112    async fn json_body_parses() {
113        let app = Router::new().route(
114            "/j",
115            get(|| async { axum::Json(serde_json::json!({ "a": 1 })) }),
116        );
117        let req = Request::builder().uri("/j").body(Body::empty()).unwrap();
118        let res = oneshot(app, req).await;
119        let v = json_body(res).await.expect("json");
120        assert_eq!(v["a"], 1);
121    }
122}