cloud-sdk-reqwest 0.32.4

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

use cloud_sdk::Method;
use cloud_sdk::authentication::{AuthenticationScopePolicy, ScopeRequirement};
use cloud_sdk::operation::{PreparedExecutionError, PreparedRequest, ProviderService};
use cloud_sdk::transport::{BoundTransport, EndpointPolicy, RequestTarget, TransportRequest};

use super::super::{
    AsyncBasicClient, AsyncBasicClientBuilder, BasicCredential, BasicCredentialScope,
    BasicPassword, BasicUsername, BuildError, HttpsEndpoint, TransportError, UserAgent,
};
use super::{run_async_test, test_timeouts};
use crate::test_server::spawn;

fn credential(endpoint: &HttpsEndpoint) -> Option<BasicCredential> {
    BasicCredential::new(
        BasicUsername::new("Aladdin").ok()?,
        BasicPassword::new("open sesame").ok()?,
        BasicCredentialScope::new(
            cloud_sdk::provider_id!("hetzner"),
            cloud_sdk::service_id!("robot"),
            endpoint.clone(),
        ),
    )
    .ok()
}

fn build_loopback(endpoint: &str) -> Option<AsyncBasicClient> {
    let endpoint = HttpsEndpoint::local_http(endpoint).ok()?;
    let credential = credential(&endpoint)?;
    AsyncBasicClientBuilder::new(
        endpoint,
        credential,
        UserAgent::new("cloud-sdk-basic-test/0.42").ok()?,
        test_timeouts()?,
    )
    .build_for_loopback()
    .ok()
}

fn request<'a>(client: &'a AsyncBasicClient, target: RequestTarget<'a>) -> PreparedRequest<'a> {
    let endpoint = client
        .endpoint_identity()
        .unwrap_or_else(|_| unreachable!());
    let policy = AuthenticationScopePolicy::new(
        ScopeRequirement::Required(cloud_sdk::provider_id!("hetzner")),
        ScopeRequirement::Required(cloud_sdk::service_id!("robot")),
        ScopeRequirement::Required(endpoint),
        ScopeRequirement::Forbidden,
        ScopeRequirement::Forbidden,
        ScopeRequirement::Forbidden,
    );
    super::support::prepared_with_policy(
        TransportRequest::new(Method::Get, target),
        ProviderService::new(
            cloud_sdk::provider_id!("hetzner"),
            cloud_sdk::service_id!("robot"),
            EndpointPolicy::fixed(endpoint),
        ),
        policy,
    )
}

#[test]
fn asynchronous_basic_client_sends_exact_authorization_and_target() {
    run_async_test(async {
        let Ok(server) = spawn(
            "200 OK",
            &[("Content-Type", "application/json")],
            b"{}",
            Duration::ZERO,
        ) else {
            unreachable!("security fixture construction failed");
        };
        let Some(client) = build_loopback(&server.endpoint) else {
            unreachable!("security fixture construction failed");
        };
        let Ok(target) = RequestTarget::new("/server/321") else {
            unreachable!("security fixture construction failed");
        };
        let mut body = [0_u8; 8];
        let mut headers = [0_u8; 512];
        let prepared = request(&client, target);
        let result = prepared
            .execute_async(&client, &mut body, &mut headers)
            .await;
        assert!(result.is_ok());

        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("get /v1/server/321 http/1.1\r\n"));
        assert!(wire.contains("authorization: basic qwxhzgrpbjpvcgvuihnlc2ftzq==\r\n"));
    });
}

#[test]
fn asynchronous_basic_builder_rejects_a_different_credential_endpoint() {
    let Ok(configured) = HttpsEndpoint::local_http("http://127.0.0.1:3000/v1") else {
        unreachable!("security fixture construction failed");
    };
    let Ok(credential_endpoint) = HttpsEndpoint::local_http("http://127.0.0.1:3001/v1") else {
        unreachable!("security fixture construction failed");
    };
    let Some(credential) = credential(&credential_endpoint) else {
        unreachable!("security fixture construction failed");
    };
    let Some(timeouts) = test_timeouts() else {
        unreachable!("security fixture construction failed");
    };
    let Ok(user_agent) = UserAgent::new("cloud-sdk-basic-test/0.42") else {
        unreachable!("security fixture construction failed");
    };
    assert!(matches!(
        AsyncBasicClientBuilder::new(configured, credential, user_agent, timeouts)
            .build_for_loopback(),
        Err(BuildError::CredentialEndpointMismatch)
    ));
}

#[test]
fn asynchronous_basic_scope_rejection_clears_response_storage() {
    run_async_test(async {
        let Some(client) = build_loopback("http://127.0.0.1:1/v1") else {
            unreachable!("security fixture construction failed");
        };
        let Ok(endpoint) = client.endpoint_identity() else {
            unreachable!("security fixture construction failed");
        };
        let Ok(target) = RequestTarget::new("/server") else {
            unreachable!("security fixture construction failed");
        };
        let policy = AuthenticationScopePolicy::new(
            ScopeRequirement::Optional(cloud_sdk::provider_id!("hetzner")),
            ScopeRequirement::Required(cloud_sdk::service_id!("robot")),
            ScopeRequirement::Required(endpoint),
            ScopeRequirement::Forbidden,
            ScopeRequirement::Forbidden,
            ScopeRequirement::Forbidden,
        );
        let request = super::support::prepared_with_policy(
            TransportRequest::new(Method::Get, target),
            ProviderService::new(
                cloud_sdk::provider_id!("hetzner"),
                cloud_sdk::service_id!("robot"),
                EndpointPolicy::fixed(endpoint),
            ),
            policy,
        );
        let mut body = [0xa5_u8; 8];
        let mut headers = [0xa5_u8; 512];
        assert!(matches!(
            request.execute_async(&client, &mut body, &mut headers).await,
            Err(PreparedExecutionError::Transport(failure))
                if failure == cloud_sdk::transport::TransportFailure::not_sent(
                    TransportError::AuthenticationScopeRejected
                )
        ));
        assert_eq!(body, [0_u8; 8]);
        assert_eq!(headers, [0_u8; 512]);
    });
}