leyline-http 0.1.1

HTTP client that mimics browsers on the wire
Documentation
#![expect(
    clippy::unwrap_used,
    reason = "test harness: unwrap doubles as the assertion"
)]
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};

use leyline::{Browser, Platform, Session};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;

async fn spawn_counting_proxy() -> (String, Arc<AtomicUsize>) {
    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let addr = listener.local_addr().unwrap();
    let accepted = Arc::new(AtomicUsize::new(0));
    let count = Arc::clone(&accepted);
    tokio::spawn(async move {
        loop {
            let (mut stream, _) = listener.accept().await.unwrap();
            count.fetch_add(1, Ordering::SeqCst);
            tokio::spawn(async move {
                let mut buf = [0u8; 4096];
                loop {
                    let n = match stream.read(&mut buf).await {
                        Ok(0) | Err(_) => return,
                        Ok(n) => n,
                    };
                    if !buf[..n].windows(4).any(|w| w == b"\r\n\r\n") {
                        continue;
                    }
                    let body = b"ok";
                    let head = format!(
                        "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: keep-alive\r\n\r\n",
                        body.len()
                    );
                    stream.write_all(head.as_bytes()).await.unwrap();
                    stream.write_all(body).await.unwrap();
                }
            });
        }
    });
    (format!("http://{addr}"), accepted)
}

#[tokio::test]
async fn fresh_pool_on_the_same_proxy_opens_a_new_connection() {
    let (proxy, accepted) = spawn_counting_proxy().await;
    let session = Session::builder()
        .browser(Browser::Chrome148)
        .platform(Platform::Windows)
        .proxy(&proxy)
        .build()
        .unwrap();
    let url = "http://gateway-rebind.test/first";

    session.get(url).send().await.unwrap().text().await.unwrap();
    session.get(url).send().await.unwrap().text().await.unwrap();
    assert_eq!(
        accepted.load(Ordering::SeqCst),
        1,
        "same session reuses the socket"
    );

    let rebound = session.with_proxy(&proxy).fresh_pool();
    rebound.get(url).send().await.unwrap().text().await.unwrap();
    assert_eq!(
        accepted.load(Ordering::SeqCst),
        2,
        "fresh_pool on the same gateway URL must open a new connection"
    );
}