Skip to main content

oxidite_testing/
request.rs

1use http::{Method, HeaderMap, HeaderName, HeaderValue};
2use http_body_util::Full;
3use bytes::Bytes;
4use serde::Serialize;
5use oxidite_core::types::OxiditeRequest;
6use http_body_util::BodyExt;
7
8/// Error type for test request construction.
9#[derive(Debug, thiserror::Error)]
10pub enum TestRequestError {
11    #[error("invalid header name: {0}")]
12    InvalidHeaderName(#[from] http::header::InvalidHeaderName),
13    #[error("invalid header value: {0}")]
14    InvalidHeaderValue(#[from] http::header::InvalidHeaderValue),
15    #[error("failed to serialize JSON body: {0}")]
16    JsonSerialize(#[from] serde_json::Error),
17    #[error("failed to build request: {0}")]
18    Build(#[from] http::Error),
19}
20
21/// Test request builder
22pub struct TestRequest {
23    method: Method,
24    uri: String,
25    headers: HeaderMap,
26    body: Vec<u8>,
27}
28
29impl TestRequest {
30    /// Create a new test request builder
31    pub fn new(method: Method, uri: impl Into<String>) -> Self {
32        Self {
33            method,
34            uri: uri.into(),
35            headers: HeaderMap::new(),
36            body: Vec::new(),
37        }
38    }
39
40    /// Create a GET request
41    pub fn get(uri: impl Into<String>) -> Self {
42        Self::new(Method::GET, uri)
43    }
44
45    /// Create a POST request
46    pub fn post(uri: impl Into<String>) -> Self {
47        Self::new(Method::POST, uri)
48    }
49
50    /// Create a PUT request
51    pub fn put(uri: impl Into<String>) -> Self {
52        Self::new(Method::PUT, uri)
53    }
54
55    /// Create a DELETE request
56    pub fn delete(uri: impl Into<String>) -> Self {
57        Self::new(Method::DELETE, uri)
58    }
59
60    /// Add a header
61    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
62        let name = HeaderName::from_bytes(name.into().as_bytes())
63            .expect("invalid header name in TestRequest::header");
64        let value = HeaderValue::from_str(&value.into())
65            .expect("invalid header value in TestRequest::header");
66        self.headers.insert(name, value);
67        self
68    }
69
70    /// Add a header without panicking on invalid header input.
71    pub fn try_header(
72        mut self,
73        name: impl Into<String>,
74        value: impl Into<String>,
75    ) -> Result<Self, TestRequestError> {
76        let name = HeaderName::from_bytes(name.into().as_bytes())?;
77        let value = HeaderValue::from_str(&value.into())?;
78        self.headers.insert(name, value);
79        Ok(self)
80    }
81
82    /// Set JSON body
83    pub fn json<T: Serialize>(mut self, body: &T) -> Self {
84        self.body = serde_json::to_vec(body).expect("failed to serialize JSON body in TestRequest::json");
85        self = self.header("content-type", "application/json");
86        self
87    }
88
89    /// Set JSON body without panicking.
90    pub fn try_json<T: Serialize>(mut self, body: &T) -> Result<Self, TestRequestError> {
91        self.body = serde_json::to_vec(body)?;
92        self.try_header("content-type", "application/json")
93    }
94
95    /// Set raw body
96    pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
97        self.body = body.into();
98        self
99    }
100
101    /// Build the request
102    pub fn build(self) -> http::Request<Full<Bytes>> {
103        let mut builder = http::Request::builder()
104            .method(self.method)
105            .uri(self.uri);
106
107        for (name, value) in self.headers.iter() {
108            builder = builder.header(name, value);
109        }
110
111        builder
112            .body(Full::new(Bytes::from(self.body)))
113            .expect("failed to build http::Request in TestRequest::build")
114    }
115
116    /// Build the request without panicking.
117    pub fn try_build(self) -> Result<http::Request<Full<Bytes>>, TestRequestError> {
118        let mut builder = http::Request::builder()
119            .method(self.method)
120            .uri(self.uri);
121
122        for (name, value) in &self.headers {
123            builder = builder.header(name, value);
124        }
125
126        Ok(builder.body(Full::new(Bytes::from(self.body)))?)
127    }
128
129    /// Build an Oxidite request body for direct handler/router invocation.
130    pub fn build_oxidite(self) -> OxiditeRequest {
131        let request = self.build();
132        let (parts, body) = request.into_parts();
133        let body = body.map_err(|e| match e {}).boxed();
134        http::Request::from_parts(parts, body)
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use serde::Deserialize;
142
143    #[derive(Serialize, Deserialize)]
144    struct TestData {
145        name: String,
146    }
147
148    #[test]
149    fn test_request_builder() {
150        let data = TestData {
151            name: "test".to_string(),
152        };
153
154        let request = TestRequest::post("/api/test")
155            .json(&data)
156            .header("x-custom", "value")
157            .build();
158
159        assert_eq!(request.method(), Method::POST);
160        assert_eq!(request.uri(), "/api/test");
161        assert!(request.headers().contains_key("content-type"));
162    }
163
164    #[test]
165    fn test_try_header_invalid_name() {
166        let result = TestRequest::get("/").try_header("bad header", "value");
167        assert!(result.is_err());
168    }
169
170    #[test]
171    fn test_build_oxidite() {
172        let request = TestRequest::get("/health").build_oxidite();
173        assert_eq!(request.uri(), "/health");
174    }
175}