use std::sync::{Arc, Mutex};
use serde_json::Value;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
#[derive(Debug, Clone)]
pub struct Received {
pub(crate) path: String,
pub(crate) headers: Vec<(String, String)>,
pub(crate) body: Value,
}
impl Received {
pub(crate) fn header(&self, name: &str) -> Option<&str> {
let name = name.to_ascii_lowercase();
self.headers
.iter()
.find(|(n, _)| *n == name)
.map(|(_, v)| v.as_str())
}
}
#[derive(Debug, Clone)]
pub struct Reply {
status: u16,
content_type: &'static str,
body: String,
}
impl Reply {
pub(crate) fn stream(content_type: &'static str, body: &str) -> Self {
Self {
status: 200,
content_type,
body: body.to_string(),
}
}
pub(crate) fn json(body: &Value) -> Self {
Self {
status: 200,
content_type: "application/json",
body: body.to_string(),
}
}
pub(crate) fn error(status: u16, body: &Value) -> Self {
Self {
status,
content_type: "application/json",
body: body.to_string(),
}
}
}
type Handler = dyn Fn(&Received) -> Reply + Send + Sync;
pub struct MockProvider {
pub(crate) url: String,
received: Arc<Mutex<Vec<Received>>>,
task: tokio::task::JoinHandle<()>,
}
impl MockProvider {
pub(crate) async fn start(
handler: impl Fn(&Received) -> Reply + Send + Sync + 'static,
) -> Self {
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let url = format!("http://{}", listener.local_addr().expect("addr"));
let received = Arc::new(Mutex::new(Vec::new()));
let handler: Arc<Handler> = Arc::new(handler);
let log = Arc::clone(&received);
let task = tokio::spawn(async move {
while let Ok((socket, _)) = listener.accept().await {
let handler = Arc::clone(&handler);
let log = Arc::clone(&log);
tokio::spawn(async move { serve(socket, &*handler, &log).await });
}
});
Self {
url,
received,
task,
}
}
pub(crate) fn received(&self) -> Vec<Received> {
self.received.lock().expect("log").clone()
}
pub(crate) fn bodies_to(&self, suffix: &str) -> Vec<Value> {
self.received()
.into_iter()
.filter(|r| {
r.path
.split('?')
.next()
.unwrap_or_default()
.ends_with(suffix)
})
.map(|r| r.body)
.collect()
}
}
impl Drop for MockProvider {
fn drop(&mut self) {
self.task.abort();
}
}
async fn serve(mut socket: TcpStream, handler: &Handler, log: &Mutex<Vec<Received>>) {
let Some(request) = read_request(&mut socket).await else {
return;
};
log.lock().expect("log").push(request.clone());
let reply = handler(&request);
let head = format!(
"HTTP/1.1 {} MOCK\r\ncontent-type: {}\r\ncontent-length: {}\r\nconnection: close\r\n\r\n",
reply.status,
reply.content_type,
reply.body.len()
);
let _ = socket.write_all(head.as_bytes()).await;
let _ = socket.write_all(reply.body.as_bytes()).await;
let _ = socket.shutdown().await;
}
async fn read_request(socket: &mut TcpStream) -> Option<Received> {
let mut buf = Vec::new();
let mut chunk = vec![0u8; 4096];
let header_end = loop {
let n = socket.read(&mut chunk).await.ok()?;
if n == 0 {
return None;
}
buf.extend_from_slice(&chunk[..n]);
if let Some(at) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
break at + 4;
}
};
let head = String::from_utf8_lossy(&buf[..header_end]).to_string();
let path = head.lines().next()?.split_whitespace().nth(1)?.to_string();
let headers: Vec<(String, String)> = head
.lines()
.skip(1)
.filter_map(|line| line.split_once(':'))
.map(|(name, value)| (name.trim().to_ascii_lowercase(), value.trim().to_string()))
.collect();
let length = headers
.iter()
.find(|(name, _)| name == "content-length")
.and_then(|(_, value)| value.parse::<usize>().ok())
.unwrap_or(0);
while buf.len() < header_end + length {
let n = socket.read(&mut chunk).await.ok()?;
if n == 0 {
break;
}
buf.extend_from_slice(&chunk[..n]);
}
let body = serde_json::from_slice(&buf[header_end..]).unwrap_or(Value::Null);
Some(Received {
path,
headers,
body,
})
}