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}