134 lines
3.8 KiB
Rust
134 lines
3.8 KiB
Rust
//! Test HTTP server for deterministic HTTP testing.
|
|
//!
|
|
//! This module provides a mini HTTP server using `tiny_http` for testing
|
|
//! HTTP loader functionality without depending on external network resources.
|
|
|
|
use std::net::TcpListener;
|
|
use std::sync::mpsc;
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
use tiny_http::{Header, Response as TinyResponse, Server, StatusCode};
|
|
|
|
/// A test HTTP server that runs on localhost.
|
|
pub struct TestServer {
|
|
/// The port the server is listening on.
|
|
port: u16,
|
|
}
|
|
|
|
impl TestServer {
|
|
/// Create a new test server on a random available port.
|
|
pub fn new() -> (Self, Server) {
|
|
// Find an available port
|
|
let listener = TcpListener::bind("127.0.0.1:0").expect("failed to bind to port");
|
|
let port = listener.local_addr().unwrap().port();
|
|
drop(listener);
|
|
|
|
// Create the server
|
|
let server = Server::http(format!("127.0.0.1:{}", port)).expect("failed to create server");
|
|
|
|
(Self { port }, server)
|
|
}
|
|
|
|
/// Returns the base URL for this server.
|
|
pub fn url(&self) -> String {
|
|
format!("http://127.0.0.1:{}", self.port)
|
|
}
|
|
|
|
/// Returns the port this server is listening on.
|
|
#[allow(dead_code)]
|
|
pub fn port(&self) -> u16 {
|
|
self.port
|
|
}
|
|
}
|
|
|
|
/// Response data for the test handler.
|
|
pub struct TestResponse {
|
|
pub status: u16,
|
|
pub content_type: &'static str,
|
|
pub headers: Vec<(&'static str, String)>,
|
|
pub body: Vec<u8>,
|
|
}
|
|
|
|
impl TestResponse {
|
|
pub fn ok(content_type: &'static str, body: impl Into<Vec<u8>>) -> Self {
|
|
Self {
|
|
status: 200,
|
|
content_type,
|
|
headers: vec![],
|
|
body: body.into(),
|
|
}
|
|
}
|
|
|
|
pub fn redirect(status: u16, location: impl Into<String>) -> Self {
|
|
Self {
|
|
status,
|
|
content_type: "text/plain",
|
|
headers: vec![("Location", location.into())],
|
|
body: vec![],
|
|
}
|
|
}
|
|
|
|
pub fn error(status: u16, body: impl Into<Vec<u8>>) -> Self {
|
|
Self {
|
|
status,
|
|
content_type: "text/plain",
|
|
headers: vec![],
|
|
body: body.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Serve requests in a background thread.
|
|
pub fn serve_background<F>(server: Server, handler: F) -> (thread::JoinHandle<()>, mpsc::Sender<()>)
|
|
where
|
|
F: Fn(&str) -> TestResponse + Send + 'static,
|
|
{
|
|
let (tx, rx) = mpsc::channel();
|
|
|
|
let handle = thread::spawn(move || {
|
|
loop {
|
|
// Check if we should stop
|
|
if rx.try_recv().is_ok() {
|
|
break;
|
|
}
|
|
|
|
// Handle request with timeout
|
|
if let Ok(Some(request)) = server.recv_timeout(Duration::from_millis(100)) {
|
|
let path = request.url().to_string();
|
|
let response_data = handler(&path);
|
|
|
|
let mut response = TinyResponse::from_data(response_data.body)
|
|
.with_status_code(StatusCode(response_data.status));
|
|
|
|
// Add Content-Type header
|
|
if let Ok(header) = Header::from_bytes("Content-Type", response_data.content_type) {
|
|
response = response.with_header(header);
|
|
}
|
|
|
|
// Add custom headers
|
|
for (name, value) in response_data.headers {
|
|
if let Ok(header) = Header::from_bytes(name, value.as_bytes()) {
|
|
response = response.with_header(header);
|
|
}
|
|
}
|
|
|
|
request.respond(response).ok();
|
|
}
|
|
}
|
|
});
|
|
|
|
(handle, tx)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_server_creates_on_available_port() {
|
|
let (server, _) = TestServer::new();
|
|
assert!(server.port() > 0);
|
|
assert!(server.url().starts_with("http://127.0.0.1:"));
|
|
}
|
|
}
|