cloud-sdk-reqwest 1.0.0

Optional provider-neutral reqwest transport boundary for cloud-sdk.
Documentation
use std::io::Cursor;
use std::string::String;
use std::time::Duration;

use cloud_sdk::Method;
use cloud_sdk::transport::{
    ContentType, RequestHeader, RequestHeaders, RequestTarget, StatusCode, TransportRequest,
};

use super::body::{ReadBodyError, read_bounded};
use super::{
    BearerToken, BlockingClientBuilder, CustomEndpointAcknowledgement, EndpointError,
    HttpsEndpoint, RequestTimeouts, TimeoutError, TransportError, UserAgent,
};
use crate::test_server::spawn;

mod authentication_policy;
mod basic;
mod endpoint_policy;
mod lifecycle;
mod method_domain;
mod raw_executor;
mod response_content_type;
mod support;

use support::{send_test, test_credential};

fn test_timeouts() -> Option<RequestTimeouts> {
    RequestTimeouts::new(Duration::from_secs(2), Duration::from_secs(1)).ok()
}

fn custom_endpoint(value: &str) -> Result<HttpsEndpoint, EndpointError> {
    HttpsEndpoint::new_custom(
        value,
        CustomEndpointAcknowledgement::trusted_operator_configuration(),
    )
}

fn build_loopback(endpoint: &str) -> Option<super::BlockingClient> {
    let endpoint = HttpsEndpoint::local_http(endpoint).ok()?;
    let token = BearerToken::new("test-token").ok()?;
    let user_agent = UserAgent::new("cloud-sdk-test/0.18").ok()?;
    let timeouts = test_timeouts()?;
    let credential = test_credential(token, &endpoint);
    let builder = BlockingClientBuilder::new(endpoint, credential, user_agent, timeouts);
    builder.build_for_loopback().ok()
}

#[test]
fn timeouts_are_explicit_nonzero_and_bounded() {
    assert_eq!(
        RequestTimeouts::new(Duration::ZERO, Duration::from_secs(1)),
        Err(TimeoutError::Zero)
    );
    assert_eq!(
        RequestTimeouts::new(Duration::from_secs(1), Duration::from_secs(2)),
        Err(TimeoutError::ExceedsTotal)
    );
    assert!(test_timeouts().is_some());
}

#[test]
fn bounded_reads_detect_overflow_without_panicking() {
    let mut exact_reader = Cursor::new(b"response".as_slice());
    let mut exact = [0_u8; 8];
    assert_eq!(read_bounded(&mut exact_reader, &mut exact), Ok(8));
    assert_eq!(exact, *b"response");

    let mut oversized_reader = Cursor::new(b"oversized".as_slice());
    let mut short = [0_u8; 4];
    assert!(matches!(
        read_bounded(&mut oversized_reader, &mut short),
        Err(ReadBodyError::TooLarge)
    ));
}

#[test]
fn blocking_client_sends_exact_headers_target_and_body_once() {
    let server = spawn(
        "503 Service Unavailable",
        &[],
        b"retry-later",
        Duration::ZERO,
    );
    let Ok(server) = server else {
        unreachable!("security fixture construction failed")
    };
    let client = build_loopback(&server.endpoint);
    assert!(client.is_some());
    let Some(client) = client else {
        unreachable!("security fixture construction failed")
    };
    let target = RequestTarget::new("/servers?name=test%20server");
    assert!(target.is_ok());
    let Ok(target) = target else {
        unreachable!("security fixture construction failed")
    };
    let sensitive = RequestHeader::sensitive("x-test-secret", "redacted-value");
    assert!(sensitive.is_ok());
    let Ok(sensitive) = sensitive else {
        unreachable!("security fixture construction failed")
    };
    let entries = [
        RequestHeader::accept(cloud_sdk::transport::MediaType::JSON),
        RequestHeader::content_type(ContentType::JSON),
        sensitive,
    ];
    let headers = RequestHeaders::new(&entries);
    assert!(headers.is_ok());
    let Ok(headers) = headers else {
        unreachable!("security fixture construction failed")
    };
    let request = TransportRequest::new(Method::Post, target)
        .with_body(br#"{"name":"server"}"#)
        .with_headers(headers);
    let mut output = [0xa5_u8; 32];
    let response = send_test(&client, request, &mut output);
    assert!(matches!(
        response,
        Err(TransportError::ResponseCommitFailed)
    ));

    let recorded = server.request.recv_timeout(Duration::from_secs(2));
    assert!(recorded.is_ok());
    let Ok(recorded) = recorded else {
        unreachable!("security fixture construction failed");
    };
    let wire = String::from_utf8_lossy(&recorded.bytes).to_ascii_lowercase();
    assert!(wire.starts_with("post /v1/servers?name=test%20server http/1.1\r\n"));
    assert!(wire.contains("authorization: bearer test-token\r\n"));
    assert!(wire.contains("user-agent: cloud-sdk-test/0.18\r\n"));
    assert!(wire.contains("accept: application/json\r\n"));
    assert!(wire.contains("content-type: application/json\r\n"));
    assert!(wire.contains("x-test-secret: redacted-value\r\n"));
    assert!(wire.ends_with(r#"{"name":"server"}"#));
}

#[test]
fn redirects_are_not_followed_or_admitted_and_oversized_bodies_are_cleared() {
    let redirect = spawn(
        "302 Found",
        &[("Location", "https://evil.example/steal")],
        b"redirect",
        Duration::ZERO,
    );
    let Ok(redirect) = redirect else {
        unreachable!("security fixture construction failed")
    };
    let Some(client) = build_loopback(&redirect.endpoint) else {
        unreachable!("security fixture construction failed");
    };
    let Ok(target) = RequestTarget::new("/servers") else {
        unreachable!("security fixture construction failed");
    };
    let mut output = [0_u8; 16];
    let response = send_test(
        &client,
        TransportRequest::new(Method::Get, target),
        &mut output,
    );
    assert!(matches!(
        response,
        Err(TransportError::ResponseCommitFailed)
    ));
    assert_eq!(output, [0_u8; 16]);

    let oversized = spawn("200 OK", &[], b"oversized", Duration::ZERO);
    let Ok(oversized) = oversized else {
        unreachable!("security fixture construction failed")
    };
    let Some(client) = build_loopback(&oversized.endpoint) else {
        unreachable!("security fixture construction failed");
    };
    let mut short = [0xa5_u8; 4];
    assert!(matches!(
        send_test(
            &client,
            TransportRequest::new(Method::Get, target),
            &mut short,
        ),
        Err(TransportError::RawHttp(
            super::RawHttpError::ResponseTooLarge
        ))
    ));
    assert_eq!(short, [0_u8; 4]);
}

#[test]
fn checked_response_exposes_content_type_without_transport_rate_limit_decoding() {
    let server = spawn(
        "200 OK",
        &[
            ("Content-Type", "application/json; charset=utf-8"),
            ("RateLimit-Limit", "3600"),
            ("RateLimit-Remaining", "3599"),
            ("RateLimit-Reset", "42"),
        ],
        b"{}",
        Duration::ZERO,
    );
    let Ok(server) = server else {
        unreachable!("security fixture construction failed")
    };
    let Some(client) = build_loopback(&server.endpoint) else {
        unreachable!("security fixture construction failed");
    };
    let Ok(target) = RequestTarget::new("/servers") else {
        unreachable!("security fixture construction failed");
    };
    let mut output = [0_u8; 8];
    let response = send_test(
        &client,
        TransportRequest::new(Method::Get, target),
        &mut output,
    );
    assert!(response.is_ok());
    let Ok(response) = response else {
        unreachable!("security fixture construction failed")
    };
    let Some(content_type) = response.content_type() else {
        unreachable!("security fixture construction failed");
    };
    assert_eq!(content_type, "application/json; charset=utf-8");
    assert_eq!(response.rate_limit(), None);
}

#[test]
fn incomplete_rate_limit_headers_are_retained_for_provider_decoding() {
    let server = spawn(
        "200 OK",
        &[("RateLimit-Limit", "3600")],
        b"secret",
        Duration::ZERO,
    );
    let Ok(server) = server else {
        unreachable!("security fixture construction failed")
    };
    let Some(client) = build_loopback(&server.endpoint) else {
        unreachable!("security fixture construction failed");
    };
    let Ok(target) = RequestTarget::new("/servers") else {
        unreachable!("security fixture construction failed");
    };
    let mut output = [0xa5_u8; 8];
    let response = send_test(
        &client,
        TransportRequest::new(Method::Get, target),
        &mut output,
    );
    assert!(response.is_ok());
}

#[test]
fn duplicate_rate_limit_headers_fail_closed() {
    let server = spawn(
        "200 OK",
        &[
            ("RateLimit-Limit", "3600"),
            ("RateLimit-Limit", "7200"),
            ("RateLimit-Remaining", "3599"),
            ("RateLimit-Reset", "42"),
        ],
        b"secret",
        Duration::ZERO,
    );
    let Ok(server) = server else {
        unreachable!("security fixture construction failed")
    };
    let Some(client) = build_loopback(&server.endpoint) else {
        unreachable!("security fixture construction failed");
    };
    let Ok(target) = RequestTarget::new("/servers") else {
        unreachable!("security fixture construction failed");
    };
    let mut output = [0xa5_u8; 8];
    assert!(matches!(
        send_test(
            &client,
            TransportRequest::new(Method::Get, target),
            &mut output,
        ),
        Err(TransportError::RawHttp(
            super::RawHttpError::DuplicateResponseHeader
        ))
    ));
    assert_eq!(output, [0_u8; 8]);
}

#[test]
fn nonempty_body_requires_content_type_before_network_access() {
    let Some(client) = build_loopback("http://127.0.0.1:9/v1") else {
        unreachable!("security fixture construction failed");
    };
    let Ok(target) = RequestTarget::new("/servers") else {
        unreachable!("security fixture construction failed");
    };
    let mut output = [0xa5_u8; 8];
    assert!(matches!(
        send_test(
            &client,
            TransportRequest::new(Method::Post, target).with_body(b"{}"),
            &mut output,
        ),
        Err(TransportError::RawHttp(
            super::RawHttpError::MissingContentType
        ))
    ));
    assert_eq!(output, [0_u8; 8]);
}

#[test]
fn response_timeout_is_payload_free_and_clears_output() {
    let server = spawn("200 OK", &[], b"late", Duration::from_millis(100));
    let Ok(server) = server else {
        unreachable!("security fixture construction failed")
    };
    let endpoint = HttpsEndpoint::local_http(&server.endpoint);
    let token = BearerToken::new("test-token");
    let user_agent = UserAgent::new("cloud-sdk-test/0.18");
    let timeouts = RequestTimeouts::new(Duration::from_millis(40), Duration::from_millis(20));
    let (Ok(endpoint), Ok(token), Ok(user_agent), Ok(timeouts)) =
        (endpoint, token, user_agent, timeouts)
    else {
        unreachable!("security fixture construction failed");
    };
    let credential = test_credential(token, &endpoint);
    let builder = BlockingClientBuilder::new(endpoint, credential, user_agent, timeouts);
    let client = builder.build_for_loopback();
    let Ok(client) = client else {
        unreachable!("security fixture construction failed")
    };
    let Ok(target) = RequestTarget::new("/slow") else {
        unreachable!("security fixture construction failed");
    };
    let mut output = [0xa5_u8; 8];
    assert!(matches!(
        send_test(
            &client,
            TransportRequest::new(Method::Get, target),
            &mut output,
        ),
        Err(TransportError::RawHttp(super::RawHttpError::TimedOut))
    ));
    assert_eq!(output, [0_u8; 8]);
}

#[test]
fn status_constant_remains_compatible_with_transport_response() {
    assert_eq!(StatusCode::OK.get(), 200);
}