reinhardt_testkit/server.rs
1//! HTTP server test utilities
2//!
3//! This module provides utilities for testing HTTP servers, including
4//! spawning test servers and various test handler implementations.
5
6use reinhardt_http::Handler;
7use reinhardt_http::{Request, Response};
8use reinhardt_server::HttpServer;
9use std::sync::Arc;
10use tokio::net::TcpListener;
11use tokio::task::JoinHandle;
12
13/// Spawns a test server on a random available port
14///
15/// # Arguments
16///
17/// * `handler` - The handler to use for the test server
18///
19/// # Returns
20///
21/// Returns a tuple containing:
22/// * The server URL (e.g., "http://127.0.0.1:12345")
23/// * A JoinHandle to the running server task
24///
25/// # Example
26///
27/// ```no_run
28/// use reinhardt_testkit::server::{spawn_test_server, EchoPathHandler};
29/// use std::sync::Arc;
30///
31/// # async fn example() {
32/// let handler = Arc::new(EchoPathHandler);
33/// let (url, handle) = spawn_test_server(handler).await;
34/// println!("Server running at: {}", url);
35/// # }
36/// ```
37pub async fn spawn_test_server(handler: Arc<dyn Handler>) -> (String, JoinHandle<()>) {
38 // Bind to port 0 to get a random available port
39 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
40 let addr = listener.local_addr().unwrap();
41 let url = format!("http://{}", addr);
42
43 // Create server
44 let server = HttpServer::new(handler);
45
46 // Spawn server in background task
47 let handle = tokio::spawn(async move {
48 // Accept connections manually since we need to use our existing listener
49 loop {
50 match listener.accept().await {
51 Ok((stream, socket_addr)) => {
52 let handler_clone = server.handler();
53 tokio::spawn(async move {
54 if let Err(e) =
55 HttpServer::handle_connection(stream, socket_addr, handler_clone, None)
56 .await
57 {
58 eprintln!("Error handling connection: {:?}", e);
59 }
60 });
61 }
62 Err(e) => {
63 eprintln!("Error accepting connection: {:?}", e);
64 break;
65 }
66 }
67 }
68 });
69
70 // Give the server a moment to start
71
72 (url, handle)
73}
74
75/// Shuts down a test server gracefully
76///
77/// # Arguments
78///
79/// * `handle` - The JoinHandle returned by [`spawn_test_server`]
80///
81/// # Example
82///
83/// ```no_run
84/// use reinhardt_testkit::server::{spawn_test_server, shutdown_test_server, EchoPathHandler};
85/// use std::sync::Arc;
86///
87/// # async fn example() {
88/// let handler = Arc::new(EchoPathHandler);
89/// let (url, handle) = spawn_test_server(handler).await;
90/// // ... perform tests ...
91/// shutdown_test_server(handle).await;
92/// # }
93/// ```
94pub async fn shutdown_test_server(handle: JoinHandle<()>) {
95 handle.abort();
96 // Give it a moment to clean up
97}
98
99/// Simple test handler that echoes the request path
100///
101/// Returns the request path as the response body.
102///
103/// # Example
104///
105/// ```no_run
106/// use reinhardt_testkit::server::{spawn_test_server, EchoPathHandler};
107/// use std::sync::Arc;
108///
109/// # async fn example() {
110/// let handler = Arc::new(EchoPathHandler);
111/// let (url, handle) = spawn_test_server(handler).await;
112/// // A request to "/test/path" will return "test/path"
113/// # }
114/// ```
115pub struct EchoPathHandler;
116
117#[async_trait::async_trait]
118impl Handler for EchoPathHandler {
119 async fn handle(&self, request: Request) -> reinhardt_core::exception::Result<Response> {
120 let path = request.path().to_string();
121 Ok(Response::ok().with_body(path))
122 }
123}
124
125/// Test handler that returns specific status codes based on path
126///
127/// Responds with different HTTP status codes based on the request path:
128/// * `/200` - Returns 200 OK
129/// * `/404` - Returns 404 Not Found
130/// * `/500` - Returns 500 Internal Server Error
131/// * Other paths - Returns 200 OK with "Default" body
132///
133/// # Example
134///
135/// ```no_run
136/// use reinhardt_testkit::server::{spawn_test_server, StatusCodeHandler};
137/// use std::sync::Arc;
138///
139/// # async fn example() {
140/// let handler = Arc::new(StatusCodeHandler);
141/// let (url, handle) = spawn_test_server(handler).await;
142/// // A request to "/404" will return 404 Not Found
143/// # }
144/// ```
145pub struct StatusCodeHandler;
146
147#[async_trait::async_trait]
148impl Handler for StatusCodeHandler {
149 async fn handle(&self, request: Request) -> reinhardt_core::exception::Result<Response> {
150 match request.path() {
151 "/200" => Ok(Response::ok().with_body("OK")),
152 "/404" => Ok(Response::not_found().with_body("Not Found")),
153 "/500" => Ok(Response::internal_server_error().with_body("Internal Server Error")),
154 _ => Ok(Response::ok().with_body("Default")),
155 }
156 }
157}
158
159/// Test handler that echoes the request method
160///
161/// Returns the HTTP method (GET, POST, etc.) as the response body.
162///
163/// # Example
164///
165/// ```no_run
166/// use reinhardt_testkit::server::{spawn_test_server, MethodEchoHandler};
167/// use std::sync::Arc;
168///
169/// # async fn example() {
170/// let handler = Arc::new(MethodEchoHandler);
171/// let (url, handle) = spawn_test_server(handler).await;
172/// // A GET request will return "GET" as the response body
173/// # }
174/// ```
175pub struct MethodEchoHandler;
176
177#[async_trait::async_trait]
178impl Handler for MethodEchoHandler {
179 async fn handle(&self, request: Request) -> reinhardt_core::exception::Result<Response> {
180 let method = request.method.as_str().to_string();
181 Ok(Response::ok().with_body(method))
182 }
183}
184
185/// Test handler with configurable delay
186///
187/// Useful for testing timeouts and async behavior. The handler
188/// waits for a specified duration before returning a response.
189///
190/// # Fields
191///
192/// * `delay_ms` - Delay in milliseconds before responding
193/// * `response_body` - The body to return in the response
194///
195/// # Example
196///
197/// ```no_run
198/// use reinhardt_testkit::server::{spawn_test_server, DelayedHandler};
199/// use std::sync::Arc;
200///
201/// # async fn example() {
202/// let handler = Arc::new(DelayedHandler {
203/// delay_ms: 100,
204/// response_body: "Delayed response".to_string(),
205/// });
206/// let (url, handle) = spawn_test_server(handler).await;
207/// // Responses will be delayed by 100ms
208/// # }
209/// ```
210pub struct DelayedHandler {
211 /// Delay in milliseconds before returning the response.
212 pub delay_ms: u64,
213 /// The body content to return in the response.
214 pub response_body: String,
215}
216
217#[async_trait::async_trait]
218impl Handler for DelayedHandler {
219 async fn handle(&self, _request: Request) -> reinhardt_core::exception::Result<Response> {
220 tokio::time::sleep(std::time::Duration::from_millis(self.delay_ms)).await;
221 Ok(Response::ok().with_body(self.response_body.clone()))
222 }
223}
224
225/// Test handler that echoes the request body
226///
227/// Returns the request body as the response body.
228///
229/// # Example
230///
231/// ```no_run
232/// use reinhardt_testkit::server::{spawn_test_server, BodyEchoHandler};
233/// use std::sync::Arc;
234///
235/// # async fn example() {
236/// let handler = Arc::new(BodyEchoHandler);
237/// let (url, handle) = spawn_test_server(handler).await;
238/// // A POST request with body "test data" will return "test data"
239/// # }
240/// ```
241pub struct BodyEchoHandler;
242
243#[async_trait::async_trait]
244impl Handler for BodyEchoHandler {
245 async fn handle(&self, request: Request) -> reinhardt_core::exception::Result<Response> {
246 let body = request.read_body()?;
247 Ok(Response::ok().with_body(body))
248 }
249}
250
251/// Test handler that returns a large response
252///
253/// Useful for testing response size limits and memory handling.
254///
255/// # Fields
256///
257/// * `size_kb` - Size of the response in kilobytes
258///
259/// # Example
260///
261/// ```no_run
262/// use reinhardt_testkit::server::{spawn_test_server, LargeResponseHandler};
263/// use std::sync::Arc;
264///
265/// # async fn example() {
266/// let handler = Arc::new(LargeResponseHandler {
267/// size_kb: 1024, // 1MB response
268/// });
269/// let (url, handle) = spawn_test_server(handler).await;
270/// // Responses will be 1MB of repeated 'x' characters
271/// # }
272/// ```
273pub struct LargeResponseHandler {
274 /// Size of the response body in kilobytes.
275 pub size_kb: usize,
276}
277
278#[async_trait::async_trait]
279impl Handler for LargeResponseHandler {
280 async fn handle(&self, _request: Request) -> reinhardt_core::exception::Result<Response> {
281 let data = "x".repeat(self.size_kb * 1024);
282 Ok(Response::ok().with_body(data))
283 }
284}
285
286/// Test handler that returns different responses based on path
287///
288/// A simple router-like handler for testing routing behavior:
289/// * `/` - Returns "Home"
290/// * `/api` - Returns JSON `{"status": "ok"}`
291/// * `/notfound` - Returns 404 with "Not Found"
292/// * Other paths - Returns 404 with "Unknown path"
293///
294/// # Example
295///
296/// ```no_run
297/// use reinhardt_testkit::server::{spawn_test_server, RouterHandler};
298/// use std::sync::Arc;
299///
300/// # async fn example() {
301/// let handler = Arc::new(RouterHandler);
302/// let (url, handle) = spawn_test_server(handler).await;
303/// // A request to "/" will return "Home"
304/// // A request to "/api" will return JSON
305/// # }
306/// ```
307pub struct RouterHandler;
308
309#[async_trait::async_trait]
310impl Handler for RouterHandler {
311 async fn handle(&self, request: Request) -> reinhardt_core::exception::Result<Response> {
312 let path = request.uri.path();
313
314 match path {
315 "/" => Ok(Response::ok().with_body("Home")),
316 "/api" => Ok(Response::ok().with_body(r#"{"status": "ok"}"#)),
317 "/notfound" => Ok(Response::not_found().with_body("Not Found")),
318 _ => Ok(Response::not_found().with_body("Unknown path")),
319 }
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use rstest::rstest;
327
328 fn test_request() -> Request {
329 Request::builder().uri("/test").build().unwrap()
330 }
331
332 #[rstest]
333 #[case(100)]
334 #[case(200)]
335 #[tokio::test]
336 async fn delayed_handler_actually_delays(#[case] delay_ms: u64) {
337 // Arrange
338 let handler = DelayedHandler {
339 delay_ms,
340 response_body: "delayed".to_string(),
341 };
342
343 // Act
344 let start = tokio::time::Instant::now();
345 let response = handler.handle(test_request()).await.unwrap();
346 let elapsed = start.elapsed();
347
348 // Assert
349 assert!(
350 elapsed.as_millis() >= u128::from(delay_ms),
351 "Expected at least {}ms delay, but elapsed was {}ms",
352 delay_ms,
353 elapsed.as_millis()
354 );
355 assert_eq!(String::from_utf8_lossy(&response.body), "delayed");
356 }
357
358 #[rstest]
359 #[tokio::test]
360 async fn delayed_handler_zero_delay_returns_immediately() {
361 // Arrange
362 let handler = DelayedHandler {
363 delay_ms: 0,
364 response_body: "instant".to_string(),
365 };
366
367 // Act
368 let start = tokio::time::Instant::now();
369 let response = handler.handle(test_request()).await.unwrap();
370 let elapsed = start.elapsed();
371
372 // Assert
373 assert!(
374 elapsed.as_millis() < 50,
375 "Zero delay should return almost immediately, but took {}ms",
376 elapsed.as_millis()
377 );
378 assert_eq!(String::from_utf8_lossy(&response.body), "instant");
379 }
380}