use color_eyre::eyre::Result;
use flume::Sender;
use http_types::{Method, Request, Response, StatusCode};
use smol::net::unix::UnixListener;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::{Arc, Condvar, Mutex};
use std::thread;
use tempfile::TempDir;
enum ControlMessage {
Shutdown,
}
pub struct TestHttpServer {
pub socket_path: PathBuf,
_temp_dir: TempDir,
control_tx: Sender<ControlMessage>,
ready_signal: Arc<(Mutex<bool>, Condvar)>,
}
impl TestHttpServer {
pub fn spawn<'scope, F, Fut>(
scope: &'scope thread::Scope<'scope, '_>,
handler: F,
) -> Result<TestHttpServer>
where
F: Fn(Request) -> Fut + Clone + Send + 'scope,
Fut: Future<Output = http_types::Result<Response>> + Send + 'scope,
{
let temp_dir = tempfile::tempdir().unwrap();
let socket_path = temp_dir.path().join("test.sock");
let socket_path_clone = socket_path.clone();
let (control_tx, control_rx) = flume::bounded(1);
let ready_signal = Arc::new((Mutex::new(false), Condvar::new()));
let ready_signal_clone = Arc::clone(&ready_signal);
let _ = scope.spawn(move || {
let handler = Rc::new(handler);
let local_executor = smol::LocalExecutor::new();
futures_lite::future::block_on(local_executor.run(async {
let listener =
UnixListener::bind(&socket_path_clone).expect("test server binds to uds");
{
let (lock, cvar) = &*ready_signal_clone;
let mut ready = lock.lock().unwrap();
*ready = true;
cvar.notify_one();
}
loop {
let do_shutdown = smol::future::race(
async {
let (stream, _) =
listener.accept().await.expect("server accepting sock conn");
let handler = handler.clone();
local_executor
.spawn(async move {
async_h1::server::accept(stream, |req| handler(req))
.await
.expect("handling connection");
})
.detach();
false
},
async {
if let Ok(ControlMessage::Shutdown) = control_rx.recv_async().await {
true
} else {
false
}
},
)
.await;
if do_shutdown {
break;
}
}
}));
});
Ok(Self {
socket_path,
_temp_dir: temp_dir,
control_tx,
ready_signal,
})
}
pub fn wait_until_ready(&self) {
let (lock, cvar) = &*self.ready_signal;
let mut ready = lock.lock().unwrap();
while !*ready {
ready = cvar.wait(ready).unwrap();
}
}
}
impl Drop for TestHttpServer {
fn drop(&mut self) {
let _ = self.control_tx.send(ControlMessage::Shutdown);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hello_name_endpoint() {
let handler = |req: Request| async move {
if req.method() == Method::Get {
let path = req.url().path();
if let Some(name) = path.strip_prefix('/') {
if !name.is_empty() {
let body = format!("Hello, {}!", name);
let mut response = Response::new(StatusCode::Ok);
response.set_body(body);
response.insert_header("content-type", "text/plain");
return Ok(response);
}
}
}
Ok(Response::new(StatusCode::NotFound))
};
std::thread::scope(|scope| {
let server = TestHttpServer::spawn(scope, handler).unwrap();
server.wait_until_ready();
let client = reqwest::blocking::Client::builder()
.unix_socket(server.socket_path.clone())
.build()
.expect("reqwest client");
let socket_url = format!("http://localhost/world");
let response = client.get(&socket_url).send().unwrap();
assert_eq!(response.status(), 200);
let body = response.text().unwrap();
assert_eq!(body, "Hello, world!");
let response = client.get("http://localhost/Alice").send().unwrap();
assert_eq!(response.status(), 200);
let body = response.text().unwrap();
assert_eq!(body, "Hello, Alice!");
});
}
}