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}