ugi 0.2.1

Runtime-agnostic Rust request client with HTTP/1.1, HTTP/2, HTTP/3, H2C, WebSocket, SSE, and gRPC support
Documentation
use flate2::Compression;
use flate2::write::GzEncoder;
use futures_lite::future::block_on;
use httpmock::prelude::*;
use serde_json::json;
use std::io::Write;
use ugi::{Client, CompressionMode, Version};

fn run<T>(value: T) -> T::Output
where
    T: std::future::IntoFuture,
{
    block_on(async move { value.await })
}

#[test]
fn client_defaults_and_request_overrides_compose_cleanly() {
    let server = MockServer::start();

    let profile = server.mock(|when, then| {
        when.method(GET)
            .path("/profile")
            .header("authorization", "Bearer client-token")
            .header("x-client", "request")
            .header("cookie", "theme=light");
        then.status(200)
            .header("content-type", "application/json")
            .body(json!({ "ok": true }).to_string());
    });

    let client = Client::builder()
        .base_url(server.base_url())
        .unwrap()
        .bearer_auth("client-token")
        .unwrap()
        .header("x-client", "base")
        .unwrap()
        .cookie("theme", "light")
        .build()
        .unwrap();

    let response = run(client
        .get("/profile")
        .header("x-client", "request")
        .unwrap())
    .unwrap();

    assert_eq!(response.version(), Version::Http11);
    assert_eq!(
        block_on(response.json::<serde_json::Value>()).unwrap(),
        json!({ "ok": true })
    );
    profile.assert();
}

#[test]
fn request_level_manual_compression_overrides_client_auto() {
    let server = MockServer::start();
    let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
    encoder.write_all(b"raw-compressed").unwrap();
    let compressed = encoder.finish().unwrap();

    let download = server.mock(|when, then| {
        when.method(GET)
            .path("/download")
            .header("accept-encoding", "gzip");
        then.status(200)
            .header("content-encoding", "gzip")
            .body(compressed.clone());
    });

    let client = Client::builder().build().unwrap();
    let response = run(client
        .get(server.url("/download"))
        .compression_mode(CompressionMode::Manual)
        .header("accept-encoding", "gzip")
        .unwrap())
    .unwrap();

    assert_eq!(response.headers().get("content-encoding"), Some("gzip"));
    assert_eq!(
        block_on(response.bytes()).unwrap().as_ref(),
        compressed.as_slice()
    );
    download.assert();
}

#[test]
fn metrics_are_available_after_base_url_request() {
    let server = MockServer::start();

    let mock = server.mock(|when, then| {
        when.method(GET).path("/metrics");
        then.status(200).body("ok");
    });

    let client = Client::builder()
        .base_url(server.base_url())
        .unwrap()
        .build()
        .unwrap();
    let response = run(client.get("/metrics")).unwrap();
    let metrics = response.metrics();

    assert_eq!(metrics.protocol(), Some(Version::Http11));
    assert!(metrics.ttfb().is_some());
    assert!(metrics.request_write_duration().is_some());
    mock.assert();
}

#[test]
fn auto_compression_decodes_gzip_response_body() {
    let server = MockServer::start();
    let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
    encoder.write_all(b"decoded-body").unwrap();
    let compressed = encoder.finish().unwrap();

    let mock = server.mock(|when, then| {
        when.method(GET)
            .path("/gzip")
            .matches(|req: &HttpMockRequest| {
                let headers = match &req.headers {
                    Some(h) => h,
                    None => return false,
                };
                let accept = headers
                    .iter()
                    .find(|(k, _)| k.eq_ignore_ascii_case("accept-encoding"))
                    .map(|(_, v)| v.to_ascii_lowercase());

                let accept = match accept {
                    Some(v) => v,
                    None => return false,
                };

                // gzip/deflate are always supported (flate2). br/zstd depend on features.
                if !accept.contains("gzip") || !accept.contains("deflate") {
                    return false;
                }
                if cfg!(feature = "brotli") && !accept.contains("br") {
                    return false;
                }
                if cfg!(feature = "zstd") && !accept.contains("zstd") {
                    return false;
                }

                true
            });
        then.status(200)
            .header("content-encoding", "gzip")
            .body(compressed.clone());
    });

    let response = run(ugi::get(server.url("/gzip"))).unwrap();
    assert_eq!(block_on(response.text()).unwrap(), "decoded-body");
    mock.assert();
}

#[test]
fn same_origin_redirect_preserves_client_authorization() {
    let server = MockServer::start();

    let start = server.mock(|when, then| {
        when.method(GET)
            .path("/start")
            .header("authorization", "Bearer token");
        then.status(302).header("location", "/final");
    });
    let final_mock = server.mock(|when, then| {
        when.method(GET)
            .path("/final")
            .header("authorization", "Bearer token");
        then.status(200).body("ok");
    });

    let client = Client::builder()
        .bearer_auth("token")
        .unwrap()
        .build()
        .unwrap();
    let response = run(client.get(server.url("/start"))).unwrap();

    assert_eq!(block_on(response.text()).unwrap(), "ok");
    start.assert();
    final_mock.assert();
}

#[test]
fn cookie_store_and_base_url_work_together() {
    let server = MockServer::start();

    let login = server.mock(|when, then| {
        when.method(GET).path("/login");
        then.status(200)
            .header("set-cookie", "sid=matrix; Path=/")
            .body("logged-in");
    });
    let me = server.mock(|when, then| {
        when.method(GET).path("/me").header("cookie", "sid=matrix");
        then.status(200).body("me");
    });

    let client = Client::builder()
        .base_url(server.base_url())
        .unwrap()
        .cookie_store()
        .build()
        .unwrap();

    let first = run(client.get("/login")).unwrap();
    assert_eq!(block_on(first.text()).unwrap(), "logged-in");

    let second = run(client.get("/me")).unwrap();
    assert_eq!(block_on(second.text()).unwrap(), "me");

    login.assert();
    me.assert();
}