From cb9cc865183b1ec117b5f5c6a1260b47ec26c9a9 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Fri, 23 Oct 2026 04:33:53 +0530 Subject: [PATCH 1/2] test(provider): pin the 2 MB response cap read_json refuses a body over MAX_BODY_BYTES, either from an announced Content-Length or while streaming a body that has none, but nothing tested it. Add tests for both refusals and for a body exactly at the cap. --- crates/tinysearch/src/provider/http.rs | 4 ++ crates/tinysearch/src/provider/http_tests.rs | 62 ++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 crates/tinysearch/src/provider/http_tests.rs diff --git a/crates/tinysearch/src/provider/http.rs b/crates/tinysearch/src/provider/http.rs index b509eeb..de2343f 100644 --- a/crates/tinysearch/src/provider/http.rs +++ b/crates/tinysearch/src/provider/http.rs @@ -120,3 +120,7 @@ fn classify_body(value: &Value) -> Option { .any(|phrase| message.contains(phrase)) .then_some(Error::InsufficientBalance) } + +#[cfg(test)] +#[path = "http_tests.rs"] +mod tests; diff --git a/crates/tinysearch/src/provider/http_tests.rs b/crates/tinysearch/src/provider/http_tests.rs new file mode 100644 index 0000000..15c340f --- /dev/null +++ b/crates/tinysearch/src/provider/http_tests.rs @@ -0,0 +1,62 @@ +use super::*; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpListener, +}; + +/// Serves one `200` reply: `head` is the header block after the status line, +/// then `body` is written as-is. +async fn serve(head: &'static str, body: Vec) -> String { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = format!("http://{}", listener.local_addr().unwrap()); + tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + let mut request = [0; 4096]; + let _ = stream.read(&mut request).await; + let reply = format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n{head}\r\n"); + let _ = stream.write_all(reply.as_bytes()).await; + let _ = stream.write_all(&body).await; + }); + address +} + +async fn read(address: &str) -> Result { + read_json(reqwest::get(address).await.unwrap()).await +} + +fn too_large(result: Result) -> bool { + matches!(result, Err(Error::ProviderUnavailable(m)) if m == "provider response too large") +} + +#[tokio::test] +async fn a_streamed_body_past_the_cap_is_refused() { + // Chunked, so no Content-Length warns the reader: the cap must stop it. + let size = MAX_BODY_BYTES as usize + 1; + let mut body = format!("{size:x}\r\n").into_bytes(); + body.extend(std::iter::repeat_n(b' ', size)); + body.extend_from_slice(b"\r\n0\r\n\r\n"); + let address = serve("Transfer-Encoding: chunked\r\nConnection: close\r\n", body).await; + + assert!(too_large(read(&address).await)); +} + +#[tokio::test] +async fn an_announced_length_past_the_cap_is_refused_before_reading() { + let address = serve( + "Content-Length: 2000001\r\nConnection: close\r\n", + Vec::new(), + ) + .await; + + assert!(too_large(read(&address).await)); +} + +#[tokio::test] +async fn a_body_at_the_cap_is_read() { + let size = MAX_BODY_BYTES as usize; + let mut body = vec![b' '; size - 2]; + body.splice(0..0, *b"{}"); + let address = serve("Connection: close\r\n", body).await; + + assert_eq!(read(&address).await.unwrap(), serde_json::json!({})); +} From 0ca63b68db83794a0f1b17411b9cf15805b99254 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Fri, 23 Oct 2026 05:30:18 +0530 Subject: [PATCH 2/2] test(provider): follow the crate's clippy rules in the cap tests Return TestResult and use ? instead of unwrap, and convert the cap with usize::try_from instead of an as cast. --- crates/tinysearch/src/provider/http_tests.rs | 52 ++++++++++++-------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/crates/tinysearch/src/provider/http_tests.rs b/crates/tinysearch/src/provider/http_tests.rs index 15c340f..4f8be45 100644 --- a/crates/tinysearch/src/provider/http_tests.rs +++ b/crates/tinysearch/src/provider/http_tests.rs @@ -4,59 +4,71 @@ use tokio::{ net::TcpListener, }; +type TestResult = std::result::Result>; + /// Serves one `200` reply: `head` is the header block after the status line, /// then `body` is written as-is. -async fn serve(head: &'static str, body: Vec) -> String { - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let address = format!("http://{}", listener.local_addr().unwrap()); +async fn serve(head: &'static str, body: Vec) -> TestResult { + let listener = TcpListener::bind("127.0.0.1:0").await?; + let address = format!("http://{}", listener.local_addr()?); tokio::spawn(async move { - let (mut stream, _) = listener.accept().await.unwrap(); + let Ok((mut stream, _)) = listener.accept().await else { + return; + }; let mut request = [0; 4096]; let _ = stream.read(&mut request).await; let reply = format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n{head}\r\n"); let _ = stream.write_all(reply.as_bytes()).await; let _ = stream.write_all(&body).await; }); - address + Ok(address) } -async fn read(address: &str) -> Result { - read_json(reqwest::get(address).await.unwrap()).await +async fn read(address: &str) -> TestResult> { + Ok(read_json(reqwest::get(address).await?).await) } -fn too_large(result: Result) -> bool { +fn too_large(result: &Result) -> bool { matches!(result, Err(Error::ProviderUnavailable(m)) if m == "provider response too large") } +fn cap() -> TestResult { + Ok(usize::try_from(MAX_BODY_BYTES)?) +} + #[tokio::test] -async fn a_streamed_body_past_the_cap_is_refused() { +async fn a_streamed_body_past_the_cap_is_refused() -> TestResult<()> { // Chunked, so no Content-Length warns the reader: the cap must stop it. - let size = MAX_BODY_BYTES as usize + 1; + let size = cap()? + 1; let mut body = format!("{size:x}\r\n").into_bytes(); body.extend(std::iter::repeat_n(b' ', size)); body.extend_from_slice(b"\r\n0\r\n\r\n"); - let address = serve("Transfer-Encoding: chunked\r\nConnection: close\r\n", body).await; + let address = serve("Transfer-Encoding: chunked\r\nConnection: close\r\n", body).await?; - assert!(too_large(read(&address).await)); + let result = read(&address).await?; + assert!(too_large(&result), "{result:?}"); + Ok(()) } #[tokio::test] -async fn an_announced_length_past_the_cap_is_refused_before_reading() { +async fn an_announced_length_past_the_cap_is_refused_before_reading() -> TestResult<()> { let address = serve( "Content-Length: 2000001\r\nConnection: close\r\n", Vec::new(), ) - .await; + .await?; - assert!(too_large(read(&address).await)); + let result = read(&address).await?; + assert!(too_large(&result), "{result:?}"); + Ok(()) } #[tokio::test] -async fn a_body_at_the_cap_is_read() { - let size = MAX_BODY_BYTES as usize; - let mut body = vec![b' '; size - 2]; +async fn a_body_at_the_cap_is_read() -> TestResult<()> { + let mut body = vec![b' '; cap()? - 2]; body.splice(0..0, *b"{}"); - let address = serve("Connection: close\r\n", body).await; + let address = serve("Connection: close\r\n", body).await?; - assert_eq!(read(&address).await.unwrap(), serde_json::json!({})); + assert_eq!(read(&address).await??, serde_json::json!({})); + Ok(()) }