cloud-sdk-reqwest 0.31.0

Optional provider-neutral reqwest transport boundary for cloud-sdk.
Documentation
use cloud_sdk::Method;
use cloud_sdk::ProviderId;
use cloud_sdk::authentication::{
    AsyncAuthenticatedTransport, AuthenticatedRequest, AuthenticationScopePolicy, ScopeRequirement,
};
use cloud_sdk::transport::{RequestTarget, ResponseBuffer, TransportRequest};

use super::{
    AsyncClientBuilder, BearerToken, HttpsEndpoint, TransportError, UserAgent, build_loopback,
    run_async_test, send_test, test_credential, test_timeouts,
};
use crate::shared::CustomEndpointAcknowledgement;

#[test]
fn missing_content_type_fails_before_async_network_access() {
    run_async_test(async {
        let Some(client) = build_loopback("http://127.0.0.1:9/v1") else {
            return;
        };
        let Ok(target) = RequestTarget::new("/servers") else {
            return;
        };
        let mut output = [0xa5_u8; 8];
        let result = send_test(
            &client,
            TransportRequest::new(Method::Post, target).with_body(b"{}"),
            &mut output,
        )
        .await;
        assert_eq!(
            result.map(|_| ()),
            Err(TransportError::RawHttp(
                super::super::RawHttpError::MissingContentType
            ))
        );
        assert_eq!(output, [0_u8; 8]);
    });
}

#[test]
fn scope_rejection_happens_before_async_network_or_header_work() {
    run_async_test(async {
        let Some(client) = build_loopback("http://127.0.0.1:9/v1") else {
            return;
        };
        let Ok(provider) = ProviderId::new("example") else {
            return;
        };
        let policy = AuthenticationScopePolicy::new(
            ScopeRequirement::Required(provider),
            ScopeRequirement::Forbidden,
            ScopeRequirement::Forbidden,
            ScopeRequirement::Forbidden,
            ScopeRequirement::Forbidden,
            ScopeRequirement::Forbidden,
        );
        let Ok(target) = RequestTarget::new("/must-not-send") else {
            return;
        };
        let request = AuthenticatedRequest::new(
            TransportRequest::new(Method::Get, target),
            policy,
            super::support::test_raw_response_policy(),
        );
        let mut body = [0xa5_u8; 8];
        let mut headers = [0xa5_u8; 8192];
        {
            let mut response = ResponseBuffer::new(&mut body, 8, &mut headers);
            assert_eq!(
                AsyncAuthenticatedTransport::send_authenticated(
                    &client,
                    request,
                    response.writer(),
                )
                .await,
                Err(cloud_sdk::transport::TransportFailure::not_sent(
                    TransportError::AuthenticationScopeRejected
                ))
            );
        }
        assert_eq!(body, [0_u8; 8]);
        assert_eq!(headers, [0_u8; 8192]);
    });
}

#[test]
fn async_builder_debug_redacts_endpoint_scope_and_token() {
    let endpoint = HttpsEndpoint::new_custom(
        "https://api.example.test/v1",
        CustomEndpointAcknowledgement::trusted_operator_configuration(),
    );
    let token = BearerToken::new("secret-token");
    let user_agent = UserAgent::new("cloud-sdk-test/0.41");
    let timeouts = test_timeouts();
    let (Ok(endpoint), Ok(token), Ok(user_agent), Some(timeouts)) =
        (endpoint, token, user_agent, timeouts)
    else {
        return;
    };
    let credential = test_credential(token, &endpoint);
    let builder = AsyncClientBuilder::new(endpoint, credential, user_agent, timeouts);
    let debug = std::format!("{builder:?}");
    assert!(debug.contains("[redacted]"));
    assert!(!debug.contains("secret-token"));
    assert!(!debug.contains("api.example.test"));
}