use core::future::Future;
use core::task::{Context, Poll, Waker};
use cloud_sdk::authentication::{AuthenticationScopePolicy, ScopeRequirement};
use cloud_sdk::operation::{
BodyReplayability, ContentTypePolicy, CostIntent, OperationImpact, OperationMetadata,
PreparedExecutionError, PreparedRequest, ProviderService, RequestIdPolicy, RequestSemantics,
ResponseBodyPolicy, ResponsePolicy, ResponsePolicyError, RetryEligibility,
};
use cloud_sdk::transport::{
ContentType, EndpointIdentity, EndpointIdentityError, EndpointPolicy, EndpointScheme,
HeaderName, MediaType, RawResponsePolicy, RequestHeader, RequestHeaders, RequestTarget,
ResponseMediaPolicy, StatusCode, TransportRequest,
};
use cloud_sdk::{
Method, ProviderId, ProviderMarker, ServiceId, ServiceMarker, provider_id, service_id,
};
use crate::{
ExpectedRequest, FixtureBody, LocalMockTransport, MockError, MockExchange, MockTransport,
PreparedRequestRecord, ResponseFixture,
};
static OK_STATUS: [StatusCode; 1] = [StatusCode::OK];
static JSON_MEDIA: [MediaType<'static>; 1] = [MediaType::JSON];
static JSON_REQUEST_HEADERS: [RequestHeader<'static>; 1] =
[RequestHeader::content_type(ContentType::JSON)];
enum ExampleProvider {}
impl ProviderMarker for ExampleProvider {
const ID: ProviderId = provider_id!("example");
}
enum ComputeService {}
impl ServiceMarker for ComputeService {
type Provider = ExampleProvider;
const ID: ServiceId = service_id!("compute");
}
#[test]
fn prepared_records_capture_policy_and_redact_request_values() {
let prepared = mutation_prepared_request(16);
assert!(prepared.is_ok());
let Ok(prepared) = prepared else {
unreachable!("testkit security fixture construction failed");
};
let record = PreparedRequestRecord::capture(prepared);
assert_eq!(record.method(), Method::Post);
assert_eq!(record.target_len(), 8);
assert_eq!(record.body_len(), 2);
assert!(record.has_request_content_type());
assert_eq!(record.header_count(), 1);
assert_eq!(record.sensitive_header_count(), 0);
assert_eq!(record.service().provider_id(), ExampleProvider::ID);
assert_eq!(record.service().service_id(), ComputeService::ID);
assert_eq!(record.metadata().impact(), OperationImpact::Mutation);
assert_eq!(
record.metadata().retry_eligibility(),
RetryEligibility::ExplicitPolicy
);
assert_eq!(record.response_policy().max_body_bytes(), 16);
assert_eq!(
record.authentication_policy().provider_requirement(),
ScopeRequirement::Required(ExampleProvider::ID)
);
assert_eq!(
record.authentication_policy().service_requirement(),
ScopeRequirement::Required(ComputeService::ID)
);
assert!(matches!(
record.authentication_policy().endpoint_requirement(),
ScopeRequirement::Required(_)
));
assert_eq!(
record.authentication_policy().audience_requirement(),
ScopeRequirement::Forbidden
);
assert_eq!(record.raw_response_policy().max_body_bytes(), 16);
assert_eq!(
record.body_replayability(),
BodyReplayability::NotReplayable
);
assert!(record.raw_response_policy().admits_header("content-type"));
assert!(record.raw_response_policy().admits_header("x-request-id"));
let debug = alloc::format!("{record:?}");
assert!(debug.contains("[redacted]"));
assert!(!debug.contains("/servers"));
assert!(!debug.contains("{}"));
}
#[test]
fn bound_mock_executes_prepared_requests_for_blocking_and_async_contracts() {
let prepared = prepared_request(16);
let first_exchange = successful_exchange();
let second_exchange = successful_exchange();
let endpoint = official_endpoint();
assert!(
prepared.is_ok() && first_exchange.is_ok() && second_exchange.is_ok() && endpoint.is_ok()
);
let (Ok(prepared), Ok(first_exchange), Ok(second_exchange), Ok(endpoint)) =
(prepared, first_exchange, second_exchange, endpoint)
else {
unreachable!("testkit security fixture construction failed");
};
let exchanges = [first_exchange, second_exchange];
let mock = MockTransport::new(&exchanges).with_endpoint(endpoint);
let mut blocking_output = [0_u8; 32];
let mut blocking_headers = [0_u8; 8192];
let blocking = prepared.execute_blocking(&mock, &mut blocking_output, &mut blocking_headers);
assert!(
blocking
.is_ok_and(|response| { response.with_borrowed(|checked| checked.body() == b"{}") })
);
let mut async_output = [0_u8; 32];
let mut async_headers = [0_u8; 8192];
let future = prepared.execute_async(&mock, &mut async_output, &mut async_headers);
let mut future = core::pin::pin!(future);
let waker = Waker::noop();
let mut context = Context::from_waker(waker);
let asynchronous = Future::poll(future.as_mut(), &mut context);
assert!(matches!(asynchronous, Poll::Ready(Ok(_))));
assert!(mock.is_complete());
}
#[test]
fn local_async_mock_executes_a_checked_prepared_request() {
let prepared = prepared_request(16);
let exchange = successful_exchange();
let endpoint = official_endpoint();
assert!(prepared.is_ok() && exchange.is_ok() && endpoint.is_ok());
let (Ok(prepared), Ok(exchange), Ok(endpoint)) = (prepared, exchange, endpoint) else {
unreachable!("testkit security fixture construction failed");
};
let exchanges = [exchange];
let mock = LocalMockTransport::new(&exchanges).with_endpoint(endpoint);
let mut output = [0_u8; 32];
let mut headers = [0_u8; 8192];
let future = prepared.execute_local_async(&mock, &mut output, &mut headers);
let mut future = core::pin::pin!(future);
let mut context = Context::from_waker(Waker::noop());
assert!(matches!(
Future::poll(future.as_mut(), &mut context),
Poll::Ready(Ok(_))
));
assert!(mock.is_complete());
}
#[test]
fn mock_models_endpoint_status_content_type_and_empty_body_failures() {
let prepared = prepared_request(16);
let expected = expected_request();
let endpoint = official_endpoint();
let other = other_endpoint();
assert!(prepared.is_ok() && expected.is_ok() && endpoint.is_ok() && other.is_ok());
let (Ok(prepared), Ok(expected), Ok(endpoint), Ok(other)) =
(prepared, expected, endpoint, other)
else {
unreachable!("testkit security fixture construction failed");
};
let Ok(json_body) = FixtureBody::new(b"{}") else {
unreachable!("testkit security fixture construction failed");
};
let Ok(empty_body) = FixtureBody::new(b"") else {
unreachable!("testkit security fixture construction failed");
};
let success = ResponseFixture::success(json_body).with_content_type("application/json");
let exchanges = [MockExchange::new(expected, success)];
let wrong_endpoint = MockTransport::new(&exchanges).with_endpoint(other);
let mut output = [0_u8; 16];
let mut response_headers = [0_u8; 8192];
assert!(matches!(
prepared.execute_blocking(&wrong_endpoint, &mut output, &mut response_headers),
Err(PreparedExecutionError::EndpointMismatch)
));
assert_eq!(wrong_endpoint.remaining(), 1);
let error = ResponseFixture::error(StatusCode::TOO_MANY_REQUESTS, json_body);
assert!(error.is_ok());
if let Ok(error) = error {
let exchanges = [MockExchange::new(
expected,
error.with_content_type("application/json"),
)];
let mock = MockTransport::new(&exchanges).with_endpoint(endpoint);
assert!(matches!(
prepared.execute_blocking(&mock, &mut output, &mut response_headers),
Err(PreparedExecutionError::ResponsePolicy(
ResponsePolicyError::UnexpectedStatus
))
));
} else {
unreachable!("provider-error fixture construction failed");
}
for (fixture, expected_error) in [
(
ResponseFixture::success(json_body),
ResponsePolicyError::MissingContentType,
),
(
ResponseFixture::success(json_body).with_content_type("text/plain"),
ResponsePolicyError::UnexpectedContentType,
),
(
ResponseFixture::success(empty_body).with_content_type("application/json"),
ResponsePolicyError::MissingBody,
),
] {
let exchanges = [MockExchange::new(expected, fixture)];
let mock = MockTransport::new(&exchanges).with_endpoint(endpoint);
assert!(matches!(
prepared.execute_blocking(&mock, &mut output, &mut response_headers),
Err(PreparedExecutionError::ResponsePolicy(error))
if error == expected_error
));
}
}
#[test]
fn mock_models_oversized_responses_and_retry_classification_mistakes() {
let prepared = prepared_request(2);
let expected = expected_request();
let endpoint = official_endpoint();
assert!(prepared.is_ok() && expected.is_ok() && endpoint.is_ok());
let (Ok(prepared), Ok(expected), Ok(endpoint)) = (prepared, expected, endpoint) else {
unreachable!("testkit security fixture construction failed");
};
let Ok(oversized_body) = FixtureBody::new(b"123") else {
unreachable!("testkit security fixture construction failed");
};
let fixture = ResponseFixture::success(oversized_body).with_content_type("application/json");
let exchanges = [MockExchange::new(expected, fixture)];
let mock = MockTransport::new(&exchanges).with_endpoint(endpoint);
let mut output = [0_u8; 64];
let mut response_headers = [0_u8; 8192];
assert!(matches!(
prepared.execute_blocking(&mock, &mut output, &mut response_headers),
Err(PreparedExecutionError::Transport(
MockError::ResponseBufferTooSmall
))
));
assert_eq!(mock.remaining(), 1);
let Some(mutation) = mutation_prepared_request(2).ok() else {
unreachable!("testkit security fixture construction failed");
};
let record = PreparedRequestRecord::capture(mutation);
assert_ne!(record.metadata().impact(), OperationImpact::ReadOnly);
assert_ne!(record.metadata().semantics(), RequestSemantics::Safe);
assert_eq!(
record.metadata().retry_eligibility(),
RetryEligibility::ExplicitPolicy
);
}
#[test]
fn mock_rejects_unbound_endpoints_request_media_mismatch_and_invalid_fixture_media() {
let prepared = prepared_request(16);
let exchange = successful_exchange();
assert!(prepared.is_ok() && exchange.is_ok());
let (Ok(prepared), Ok(exchange)) = (prepared, exchange) else {
unreachable!("testkit security fixture construction failed");
};
let exchanges = [exchange];
let unbound = MockTransport::new(&exchanges);
let mut output = [0xA5_u8; 16];
let mut response_headers = [0xA5_u8; 8192];
assert!(matches!(
prepared.execute_blocking(&unbound, &mut output, &mut response_headers),
Err(PreparedExecutionError::EndpointIdentity(
EndpointIdentityError::UnboundTransport
))
));
assert_eq!(unbound.remaining(), 1);
assert_eq!(output, [0_u8; 16]);
let endpoint = official_endpoint();
let target = RequestTarget::new("/servers");
let body = FixtureBody::new(b"{}");
assert!(endpoint.is_ok() && target.is_ok() && body.is_ok());
let (Ok(endpoint), Ok(target), Ok(body)) = (endpoint, target, body) else {
unreachable!("testkit security fixture construction failed");
};
let no_media_expectation = ExpectedRequest::new(Method::Get, target).with_body(b"{}");
let exchanges = [MockExchange::new(
no_media_expectation,
ResponseFixture::success(body).with_content_type("application/json"),
)];
let mock = MockTransport::new(&exchanges).with_endpoint(endpoint);
assert!(matches!(
prepared.execute_blocking(&mock, &mut output, &mut response_headers),
Err(PreparedExecutionError::Transport(
MockError::HeadersMismatch
))
));
assert_eq!(mock.remaining(), 1);
let expected = expected_request();
assert!(expected.is_ok());
if let Ok(expected) = expected {
let exchanges = [MockExchange::new(
expected,
ResponseFixture::success(body).with_content_type("application/json; charset"),
)];
let mock = MockTransport::new(&exchanges).with_endpoint(endpoint);
output.fill(0xA5);
assert!(matches!(
prepared.execute_blocking(&mock, &mut output, &mut response_headers),
Err(PreparedExecutionError::Transport(
MockError::InvalidFixtureMetadata
))
));
assert_eq!(output, [0_u8; 16]);
assert_eq!(mock.remaining(), 1);
} else {
unreachable!("expected-request fixture construction failed");
}
}
fn prepared_request(max_body_bytes: usize) -> Result<PreparedRequest<'static>, ()> {
build_prepared_request(
max_body_bytes,
OperationImpact::ReadOnly,
RequestSemantics::Safe,
RetryEligibility::ExplicitPolicy,
CostIntent::NoKnownCost,
)
}
fn mutation_prepared_request(max_body_bytes: usize) -> Result<PreparedRequest<'static>, ()> {
build_prepared_request(
max_body_bytes,
OperationImpact::Mutation,
RequestSemantics::Idempotent,
RetryEligibility::ExplicitPolicy,
CostIntent::MayIncurCost,
)
}
fn build_prepared_request(
max_body_bytes: usize,
impact: OperationImpact,
semantics: RequestSemantics,
retry: RetryEligibility,
cost: CostIntent,
) -> Result<PreparedRequest<'static>, ()> {
let target = RequestTarget::new("/servers").map_err(|_| ())?;
let headers = RequestHeaders::new(&JSON_REQUEST_HEADERS).map_err(|_| ())?;
let method = if matches!(impact, OperationImpact::ReadOnly) {
Method::Get
} else {
Method::Post
};
let request = TransportRequest::new(method, target)
.with_body(b"{}")
.with_headers(headers);
let metadata =
OperationMetadata::new(impact, semantics, retry, cost, RequestIdPolicy::Protected)
.map_err(|_| ())?;
let response_policy = ResponsePolicy::new(
&OK_STATUS,
ContentTypePolicy::Required(&JSON_MEDIA),
ResponseBodyPolicy::Required,
max_body_bytes,
)
.map_err(|_| ())?;
let endpoint = official_endpoint().map_err(|_| ())?;
let authentication_policy = AuthenticationScopePolicy::new(
ScopeRequirement::Required(ExampleProvider::ID),
ScopeRequirement::Required(ComputeService::ID),
ScopeRequirement::Required(endpoint),
ScopeRequirement::Forbidden,
ScopeRequirement::Forbidden,
ScopeRequirement::Forbidden,
);
let content_type = HeaderName::new("content-type").map_err(|_| ())?;
let request_id = HeaderName::new("x-request-id").map_err(|_| ())?;
let raw_response_policy = RawResponsePolicy::new(
max_body_bytes,
max_body_bytes,
ResponseMediaPolicy::Required(&JSON_MEDIA),
ResponseMediaPolicy::Required(&JSON_MEDIA),
&[content_type, request_id],
8,
)
.map_err(|_| ())?;
PreparedRequest::new(
request,
ProviderService::from_marker::<ComputeService>(EndpointPolicy::fixed(endpoint)),
metadata,
response_policy,
authentication_policy,
raw_response_policy,
cloud_sdk::operation::RequestBodySensitivity::Public,
)
.map_err(|_| ())
}
fn expected_request() -> Result<ExpectedRequest<'static>, ()> {
let target = RequestTarget::new("/servers").map_err(|_| ())?;
let headers = RequestHeaders::new(&JSON_REQUEST_HEADERS).map_err(|_| ())?;
Ok(ExpectedRequest::new(Method::Get, target)
.with_body(b"{}")
.with_headers(headers))
}
fn successful_exchange() -> Result<MockExchange<'static>, ()> {
let body = FixtureBody::new(b"{}").map_err(|_| ())?;
Ok(MockExchange::new(
expected_request()?,
ResponseFixture::success(body).with_content_type("application/json; charset=utf-8"),
))
}
fn official_endpoint() -> Result<EndpointIdentity<'static>, EndpointIdentityError> {
EndpointIdentity::new(EndpointScheme::Https, "api.hetzner.cloud", 443, "/v1")
}
fn other_endpoint() -> Result<EndpointIdentity<'static>, EndpointIdentityError> {
EndpointIdentity::new(EndpointScheme::Https, "example.invalid", 443, "/v1")
}