Skip to main content

crux_http/testing/
response_builder.rs

1use http::{HeaderValue, StatusCode};
2
3use crate::response::Response;
4
5/// Allows users to build an http response.
6///
7/// This is mostly expected to be useful in tests rather than application code.
8///
9/// Only responses a feature can actually receive are buildable: a
10/// [`Response`](crate::Response) never carries a 4xx or 5xx status, because `crux_http`
11/// converts those to [`HttpError::Http`](crate::HttpError::Http) before the app sees
12/// them. Use [`rejection`](super::rejection) to build a rejection.
13pub struct ResponseBuilder<Body> {
14    response: Response<Body>,
15}
16
17impl ResponseBuilder<Vec<u8>> {
18    /// Constructs a new `ResponseBuilder` with the 200 OK status code.
19    #[must_use]
20    pub fn ok() -> Self {
21        Self::with_status(200)
22    }
23
24    /// Constructs a new `ResponseBuilder` with the specified status code.
25    ///
26    /// # Panics
27    ///
28    /// Panics if `status` is outside the valid HTTP range (100–999), or if it is a client
29    /// (4xx) or server (5xx) error. Such a response is not a state any app can observe —
30    /// `crux_http` delivers it as an [`HttpError::Http`](crate::HttpError::Http) on the
31    /// `Err` side, so a test that builds one asserts against a branch the app can never
32    /// take. Build the rejection a feature really receives with
33    /// [`rejection`](super::rejection):
34    ///
35    /// ```
36    /// # use crux_http::testing::rejection;
37    /// let result = rejection::<Vec<u8>>(409, r#"{"error":"already booked"}"#);
38    /// assert_eq!(result.unwrap_err().code(), Some(409));
39    /// ```
40    #[must_use]
41    pub fn with_status(status: u16) -> Self {
42        let status = StatusCode::from_u16(status).expect(
43            "ResponseBuilder::with_status called with an out-of-range code (must be 100–999)",
44        );
45        assert!(
46            !status.is_client_error() && !status.is_server_error(),
47            "ResponseBuilder::with_status called with {status}, but a Response never carries a \
48             client (4xx) or server (5xx) status — those reach the app as \
49             Err(HttpError::Http {{ .. }}). Use crux_http::testing::rejection instead."
50        );
51        let response = Response::new_with_status(status);
52        Self { response }
53    }
54}
55
56impl<Body> ResponseBuilder<Body> {
57    /// Sets the body of the Response.
58    pub fn body<NewBody>(self, body: NewBody) -> ResponseBuilder<NewBody> {
59        let response = self.response.with_body(body);
60        ResponseBuilder { response }
61    }
62
63    /// Sets a header on the response, replacing any existing value for that name.
64    ///
65    /// # Panics
66    /// Panics if `value` is not a valid header value.
67    #[must_use]
68    pub fn header(
69        mut self,
70        name: impl http::header::IntoHeaderName,
71        value: impl AsRef<str>,
72    ) -> Self {
73        let value = HeaderValue::from_str(value.as_ref()).expect("invalid header value");
74        self.response.insert_header(name, value);
75        self
76    }
77
78    /// Appends a header value, keeping any existing values for that name.
79    ///
80    /// Use this when building responses with multiple values for the same header
81    /// (e.g. `Set-Cookie`).
82    ///
83    /// # Panics
84    /// Panics if `value` is not a valid header value.
85    #[must_use]
86    pub fn append_header(
87        mut self,
88        name: impl http::header::IntoHeaderName,
89        value: impl AsRef<str>,
90    ) -> Self {
91        let value = HeaderValue::from_str(value.as_ref()).expect("invalid header value");
92        self.response.append_header(name, value);
93        self
94    }
95
96    /// Builds the response.
97    pub fn build(self) -> Response<Body> {
98        self.response
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::ResponseBuilder;
105
106    #[test]
107    fn builds_any_non_error_status() {
108        for status in [200, 201, 204, 299, 302, 304] {
109            assert_eq!(
110                ResponseBuilder::with_status(status).build().status(),
111                status
112            );
113        }
114    }
115
116    #[test]
117    #[should_panic(expected = "a Response never carries a client (4xx) or server (5xx) status")]
118    fn refuses_a_client_error_status() {
119        let _ = ResponseBuilder::with_status(409);
120    }
121
122    #[test]
123    #[should_panic(expected = "a Response never carries a client (4xx) or server (5xx) status")]
124    fn refuses_a_server_error_status() {
125        let _ = ResponseBuilder::with_status(503);
126    }
127}