Skip to main content

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}