Files
rust_browser/crates/net/tests/test_server.rs
2026-01-30 23:10:27 -05:00

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:"));
}
}