Skip to main content

volter_testing/
request.rs

1//! [`TestRequestBuilder`] — accumulates request parameters and sends them
2//! through the router.
3
4use bytes::Bytes;
5use http::HeaderMap;
6use http::HeaderName;
7use http::HeaderValue;
8use http::Method;
9use serde::Serialize;
10use tower::Service;
11
12use volter_core::BoxBody;
13use volter_core::Request;
14use volter_router::Router;
15
16use crate::response::TestResponse;
17
18/// A request builder produced by [`TestClient`](crate::TestClient).
19///
20/// Accumulates method, path, headers, and body, then executes the request
21/// via [`send`](TestRequestBuilder::send).
22///
23/// # Examples
24///
25/// ```rust
26/// use volter_testing::TestClient;
27/// use volter_router::{Router, get};
28///
29/// async fn handler() -> &'static str { "ok" }
30///
31/// # #[tokio::main(flavor = "current_thread")]
32/// # async fn main() {
33/// let client = TestClient::new(Router::new().route("/", get(handler)));
34/// let response = client.get("/").send().await;
35/// assert_eq!(response.status(), 200);
36/// # }
37/// ```
38pub struct TestRequestBuilder<S> {
39    pub(crate) router: Router<S>,
40    pub(crate) method: Method,
41    pub(crate) path: String,
42    pub(crate) headers: HeaderMap,
43    pub(crate) body: Option<Bytes>,
44}
45
46impl<S: Clone + Send + 'static> TestRequestBuilder<S> {
47    /// Add a header to the request.
48    ///
49    /// Existing headers with the same name are **not** removed — the value is
50    /// appended.
51    pub fn header(mut self, name: HeaderName, value: HeaderValue) -> Self {
52        self.headers.append(name, value);
53        self
54    }
55
56    /// Set the request body to a JSON-serialized value.
57    ///
58    /// Also sets the `Content-Type` header to `application/json`.
59    pub fn json<T: Serialize>(mut self, value: &T) -> Self {
60        // Serialization to Vec<u8> only fails for types containing non-finite
61        // f32/f64 values — acceptable in a test helper where the caller
62        // controls the input.
63        let bytes = serde_json::to_vec(value).unwrap_or_else(|_| Vec::new());
64        self.headers.insert(
65            http::header::CONTENT_TYPE,
66            HeaderValue::from_static("application/json"),
67        );
68        self.body = Some(Bytes::from(bytes));
69        self
70    }
71
72    /// Set the request body to a raw byte slice.
73    pub fn body(mut self, body: impl Into<Bytes>) -> Self {
74        self.body = Some(body.into());
75        self
76    }
77
78    /// Build the `http::Request`, dispatch it through the cloned router, and
79    /// return a [`TestResponse`].
80    pub async fn send(mut self) -> TestResponse {
81        let request = self.build_request();
82        let response = self
83            .router
84            .call(request)
85            .await
86            .unwrap_or_else(|never| match never {});
87        TestResponse::new(response)
88    }
89
90    /// Construct the inner `http::Request<BoxBody>`.
91    fn build_request(&self) -> Request<BoxBody> {
92        let body: BoxBody = match &self.body {
93            Some(bytes) => volter_core::full_body(bytes.clone()),
94            None => volter_core::empty_body(),
95        };
96
97        let mut request = Request::builder()
98            .method(self.method.clone())
99            .uri(&self.path)
100            .body(body)
101            // Builder failure only happens for invalid method / uri, both
102            // of which are caller-supplied — acceptable in a test helper.
103            .unwrap_or_else(|_| Request::new(volter_core::empty_body()));
104
105        // Copy headers into the request.
106        for (name, value) in &self.headers {
107            request.headers_mut().insert(name, value.clone());
108        }
109
110        request
111    }
112}