rustapi_testing/
client.rs

1//! TestClient for integration testing without network binding
2//!
3//! This module provides a test client that allows sending simulated HTTP requests
4//! through the full middleware and handler pipeline without starting a real server.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use rustapi_core::{RustApi, get};
10//! use rustapi_testing::TestClient;
11//!
12//! async fn hello() -> &'static str {
13//!     "Hello, World!"
14//! }
15//!
16//! #[tokio::test]
17//! async fn test_hello() {
18//!     let app = RustApi::new().route("/", get(hello));
19//!     let client = TestClient::new(app);
20//!     
21//!     let response = client.get("/").await;
22//!     response.assert_status(200);
23//!     assert_eq!(response.text(), "Hello, World!");
24//! }
25//! ```
26
27use bytes::Bytes;
28use http::{header, HeaderMap, HeaderValue, Method, StatusCode};
29use http_body_util::BodyExt;
30use rustapi_core::middleware::{BodyLimitLayer, BoxedNext, LayerStack, DEFAULT_BODY_LIMIT};
31use rustapi_core::{ApiError, BodyVariant, IntoResponse, Request, Response, RouteMatch, Router};
32use serde::{de::DeserializeOwned, Serialize};
33use std::future::Future;
34use std::pin::Pin;
35use std::sync::Arc;
36
37/// Test client for integration testing without network binding
38///
39/// TestClient wraps a RustApi instance and allows sending simulated HTTP requests
40/// through the full middleware and handler pipeline.
41pub struct TestClient {
42    router: Arc<Router>,
43    layers: Arc<LayerStack>,
44}
45
46impl TestClient {
47    /// Create a new test client from a RustApi instance
48    ///
49    /// # Example
50    ///
51    /// ```rust,ignore
52    /// let app = RustApi::new().route("/", get(handler));
53    /// let client = TestClient::new(app);
54    /// ```
55    pub fn new(app: rustapi_core::RustApi) -> Self {
56        // Get the router and layers from the app
57        let layers = app.layers().clone();
58        let router = app.into_router();
59
60        // Apply body limit layer if not already present
61        let mut layers = layers;
62        layers.prepend(Box::new(BodyLimitLayer::new(DEFAULT_BODY_LIMIT)));
63
64        Self {
65            router: Arc::new(router),
66            layers: Arc::new(layers),
67        }
68    }
69
70    /// Create a new test client with custom body limit
71    pub fn with_body_limit(app: rustapi_core::RustApi, limit: usize) -> Self {
72        let layers = app.layers().clone();
73        let router = app.into_router();
74
75        let mut layers = layers;
76        layers.prepend(Box::new(BodyLimitLayer::new(limit)));
77
78        Self {
79            router: Arc::new(router),
80            layers: Arc::new(layers),
81        }
82    }
83
84    /// Send a GET request
85    ///
86    /// # Example
87    ///
88    /// ```rust,ignore
89    /// let response = client.get("/users").await;
90    /// ```
91    pub async fn get(&self, path: &str) -> TestResponse {
92        self.request(TestRequest::get(path)).await
93    }
94
95    /// Send a POST request with JSON body
96    ///
97    /// # Example
98    ///
99    /// ```rust,ignore
100    /// let response = client.post_json("/users", &CreateUser { name: "Alice" }).await;
101    /// ```
102    pub async fn post_json<T: Serialize>(&self, path: &str, body: &T) -> TestResponse {
103        self.request(TestRequest::post(path).json(body)).await
104    }
105
106    /// Send a request with full control
107    ///
108    /// # Example
109    ///
110    /// ```rust,ignore
111    /// let response = client.request(
112    ///     TestRequest::put("/users/1")
113    ///         .header("Authorization", "Bearer token")
114    ///         .json(&UpdateUser { name: "Bob" })
115    /// ).await;
116    /// ```
117    pub async fn request(&self, req: TestRequest) -> TestResponse {
118        let method = req.method.clone();
119        let path = req.path.clone();
120
121        // Match the route to get path params
122        let (handler, params) = match self.router.match_route(&path, &method) {
123            RouteMatch::Found { handler, params } => (handler.clone(), params),
124            RouteMatch::NotFound => {
125                let response =
126                    ApiError::not_found(format!("No route found for {} {}", method, path))
127                        .into_response();
128                return TestResponse::from_response(response).await;
129            }
130            RouteMatch::MethodNotAllowed { allowed } => {
131                let allowed_str: Vec<&str> = allowed.iter().map(|m| m.as_str()).collect();
132                let mut response = ApiError::new(
133                    StatusCode::METHOD_NOT_ALLOWED,
134                    "method_not_allowed",
135                    format!("Method {} not allowed for {}", method, path),
136                )
137                .into_response();
138
139                response
140                    .headers_mut()
141                    .insert(header::ALLOW, allowed_str.join(", ").parse().unwrap());
142                return TestResponse::from_response(response).await;
143            }
144        };
145
146        // Build the internal Request
147        let uri: http::Uri = path.parse().unwrap_or_else(|_| "/".parse().unwrap());
148        let mut builder = http::Request::builder().method(method).uri(uri);
149
150        // Add headers
151        for (key, value) in req.headers.iter() {
152            builder = builder.header(key, value);
153        }
154
155        let http_req = builder.body(()).unwrap();
156        let (parts, _) = http_req.into_parts();
157
158        let body_bytes = req.body.unwrap_or_default();
159
160        let request = Request::new(
161            parts,
162            BodyVariant::Buffered(body_bytes),
163            self.router.state_ref(),
164            params,
165        );
166
167        // Create the final handler as a BoxedNext
168        let final_handler: BoxedNext = Arc::new(move |req: Request| {
169            let handler = handler.clone();
170            Box::pin(async move { handler(req).await })
171                as Pin<Box<dyn Future<Output = Response> + Send + 'static>>
172        });
173
174        // Execute through middleware stack
175        let response = self.layers.execute(request, final_handler).await;
176
177        TestResponse::from_response(response).await
178    }
179}
180
181/// Test request builder
182///
183/// Provides a fluent API for building test requests with custom methods,
184/// headers, and body content.
185#[derive(Debug, Clone)]
186pub struct TestRequest {
187    method: Method,
188    path: String,
189    headers: HeaderMap,
190    body: Option<Bytes>,
191}
192
193impl TestRequest {
194    /// Create a new request with the given method and path
195    fn new(method: Method, path: &str) -> Self {
196        Self {
197            method,
198            path: path.to_string(),
199            headers: HeaderMap::new(),
200            body: None,
201        }
202    }
203
204    /// Create a GET request
205    pub fn get(path: &str) -> Self {
206        Self::new(Method::GET, path)
207    }
208
209    /// Create a POST request
210    pub fn post(path: &str) -> Self {
211        Self::new(Method::POST, path)
212    }
213
214    /// Create a PUT request
215    pub fn put(path: &str) -> Self {
216        Self::new(Method::PUT, path)
217    }
218
219    /// Create a PATCH request
220    pub fn patch(path: &str) -> Self {
221        Self::new(Method::PATCH, path)
222    }
223
224    /// Create a DELETE request
225    pub fn delete(path: &str) -> Self {
226        Self::new(Method::DELETE, path)
227    }
228
229    /// Add a header to the request
230    ///
231    /// # Example
232    ///
233    /// ```rust,ignore
234    /// let req = TestRequest::get("/")
235    ///     .header("Authorization", "Bearer token")
236    ///     .header("Accept", "application/json");
237    /// ```
238    pub fn header(mut self, key: &str, value: &str) -> Self {
239        if let (Ok(name), Ok(val)) = (
240            key.parse::<http::header::HeaderName>(),
241            HeaderValue::from_str(value),
242        ) {
243            self.headers.insert(name, val);
244        }
245        self
246    }
247
248    /// Set the request body as JSON
249    ///
250    /// This automatically sets the Content-Type header to `application/json`.
251    ///
252    /// # Example
253    ///
254    /// ```rust,ignore
255    /// let req = TestRequest::post("/users")
256    ///     .json(&CreateUser { name: "Alice" });
257    /// ```
258    pub fn json<T: Serialize>(mut self, body: &T) -> Self {
259        match serde_json::to_vec(body) {
260            Ok(bytes) => {
261                self.body = Some(Bytes::from(bytes));
262                self.headers.insert(
263                    header::CONTENT_TYPE,
264                    HeaderValue::from_static("application/json"),
265                );
266            }
267            Err(_) => {
268                // If serialization fails, leave body empty
269            }
270        }
271        self
272    }
273
274    /// Set the request body as raw bytes
275    ///
276    /// # Example
277    ///
278    /// ```rust,ignore
279    /// let req = TestRequest::post("/upload")
280    ///     .body("raw content");
281    /// ```
282    pub fn body(mut self, body: impl Into<Bytes>) -> Self {
283        self.body = Some(body.into());
284        self
285    }
286
287    /// Set the Content-Type header
288    pub fn content_type(self, content_type: &str) -> Self {
289        self.header("content-type", content_type)
290    }
291}
292
293/// Test response with assertion helpers
294///
295/// Provides methods to inspect and assert on the response status, headers, and body.
296#[derive(Debug)]
297pub struct TestResponse {
298    status: StatusCode,
299    headers: HeaderMap,
300    body: Bytes,
301}
302
303impl TestResponse {
304    /// Create a TestResponse from an HTTP response
305    async fn from_response(response: Response) -> Self {
306        let (parts, body) = response.into_parts();
307        let body_bytes = body
308            .collect()
309            .await
310            .map(|b| b.to_bytes())
311            .unwrap_or_default();
312
313        Self {
314            status: parts.status,
315            headers: parts.headers,
316            body: body_bytes,
317        }
318    }
319
320    /// Get the response status code
321    pub fn status(&self) -> StatusCode {
322        self.status
323    }
324
325    /// Get the response headers
326    pub fn headers(&self) -> &HeaderMap {
327        &self.headers
328    }
329
330    /// Get the response body as bytes
331    pub fn body(&self) -> &Bytes {
332        &self.body
333    }
334
335    /// Get the response body as a string
336    ///
337    /// Returns an empty string if the body is not valid UTF-8.
338    pub fn text(&self) -> String {
339        String::from_utf8_lossy(&self.body).to_string()
340    }
341
342    /// Parse the response body as JSON
343    ///
344    /// # Example
345    ///
346    /// ```rust,ignore
347    /// let user: User = response.json().unwrap();
348    /// ```
349    pub fn json<T: DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
350        serde_json::from_slice(&self.body)
351    }
352
353    /// Assert that the response has the expected status code
354    ///
355    /// # Panics
356    ///
357    /// Panics if the status code doesn't match.
358    ///
359    /// # Example
360    ///
361    /// ```rust,ignore
362    /// response.assert_status(StatusCode::OK);
363    /// response.assert_status(200);
364    /// ```
365    pub fn assert_status<S: Into<StatusCode>>(&self, expected: S) -> &Self {
366        let expected = expected.into();
367        assert_eq!(
368            self.status,
369            expected,
370            "Expected status {}, got {}. Body: {}",
371            expected,
372            self.status,
373            self.text()
374        );
375        self
376    }
377
378    /// Assert that the response has the expected header value
379    ///
380    /// # Panics
381    ///
382    /// Panics if the header doesn't exist or doesn't match.
383    ///
384    /// # Example
385    ///
386    /// ```rust,ignore
387    /// response.assert_header("content-type", "application/json");
388    /// ```
389    pub fn assert_header(&self, key: &str, expected: &str) -> &Self {
390        let actual = self
391            .headers
392            .get(key)
393            .and_then(|v| v.to_str().ok())
394            .unwrap_or("");
395
396        assert_eq!(
397            actual, expected,
398            "Expected header '{}' to be '{}', got '{}'",
399            key, expected, actual
400        );
401        self
402    }
403
404    /// Assert that the response body matches the expected JSON value
405    ///
406    /// # Panics
407    ///
408    /// Panics if the body can't be parsed as JSON or doesn't match.
409    ///
410    /// # Example
411    ///
412    /// ```rust,ignore
413    /// response.assert_json(&User { id: 1, name: "Alice".to_string() });
414    /// ```
415    pub fn assert_json<T: DeserializeOwned + PartialEq + std::fmt::Debug>(
416        &self,
417        expected: &T,
418    ) -> &Self {
419        let actual: T = self.json().expect("Failed to parse response body as JSON");
420        assert_eq!(&actual, expected, "JSON body mismatch");
421        self
422    }
423
424    /// Assert that the response body contains the expected string
425    ///
426    /// # Panics
427    ///
428    /// Panics if the body doesn't contain the expected string.
429    pub fn assert_body_contains(&self, expected: &str) -> &Self {
430        let body = self.text();
431        assert!(
432            body.contains(expected),
433            "Expected body to contain '{}', got '{}'",
434            expected,
435            body
436        );
437        self
438    }
439}