Skip to main content

toxi_testing/
server.rs

1use toxi_core::{Router, ToxiRequest, ToxiResponse, Result};
2use bytes::Bytes;
3use http_body_util::{BodyExt, Full};
4use tower::Service;
5
6/// Test server for integration testing.
7///
8/// This server is **single-threaded by design** — it holds a single `&mut` reference
9/// to the underlying service and processes one request at a time.  For concurrent
10/// tests, create a separate `TestServer` per test function (the server is cheap to
11/// construct).
12///
13/// # Example
14///
15/// ```rust,ignore
16/// use toxi_testing::{test_router, TestRequest};
17///
18/// #[tokio::test]
19/// async fn test_hello() {
20///     let mut server = test_router(my_router());
21///     let req = TestRequest::get("/hello").build_toxi();
22///     let resp = server.call(req).await.unwrap();
23///     assert_eq!(resp.status(), 200);
24/// }
25/// ```
26pub struct TestServer<S> {
27    service: S,
28}
29
30impl<S> Clone for TestServer<S>
31where
32    S: Clone,
33{
34    fn clone(&self) -> Self {
35        Self {
36            service: self.service.clone(),
37        }
38    }
39}
40
41impl<S> TestServer<S>
42where
43    S: Service<ToxiRequest, Response = ToxiResponse> + Clone + Send + 'static,
44    S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
45    S::Future: Send,
46{
47    /// Create a new test server from a service
48    pub fn new(service: S) -> Self {
49        Self { service }
50    }
51
52    /// Send a request to the test server.
53    ///
54    /// Because the underlying Tower `Service` requires `&mut self` for `ready()`,
55    /// this method takes `&mut self`.  If you need concurrent requests, either
56    /// clone the test server or construct a fresh one per concurrent task.
57    pub async fn call(&mut self, request: ToxiRequest) -> Result<ToxiResponse> {
58        use tower::ServiceExt;
59        self.service
60            .ready()
61            .await
62            .map_err(|e| toxi_core::Error::InternalServerError(format!("Service not ready: {:?}", e.into())))?
63            .call(request)
64            .await
65            .map_err(|e| toxi_core::Error::InternalServerError(format!("Request failed: {:?}", e.into())))
66    }
67
68    /// Send a plain HTTP test request after converting its body to Toxi's boxed body.
69    pub async fn call_http(&mut self, request: http::Request<Full<Bytes>>) -> Result<ToxiResponse> {
70        let (parts, body) = request.into_parts();
71        let request = http::Request::from_parts(parts, body.map_err(|e| match e {}).boxed());
72        self.call(request).await
73    }
74}
75
76/// Helper to test a router
77pub fn test_router(router: Router) -> TestServer<Router> {
78    TestServer::new(router)
79}