Skip to main content

Crate cloud_sdk_testkit

Crate cloud_sdk_testkit 

Source
Expand description

provider-neutral no_std testkit for cloud-sdk.
Deterministic mock transport, prepared-request records, bounded response fixtures, and adversarial corpora.


cloud-sdk Rust crate overview

§cloud-sdk-testkit

Stable version 1.0 provider-neutral testing support for the main cloud-sdk crate and its provider crates. The default graph is no_std, allocation-free, network-free, filesystem-free, and runtime-free.

Static ordered exchanges are the simplest choice for fixed request sequences. Use bounded dynamic scenarios when a response must depend on the current request or when pagination, polling, cancellation, partial I/O, or injected failures need deterministic multi-request coverage.

§Install

[dev-dependencies]
cloud-sdk = "=1.0.0"
cloud-sdk-testkit = "=1.0.0"

§Mock Transport

use cloud_sdk::Method;
use cloud_sdk::transport::{
    BlockingTransport, RequestTarget, ResponseBuffer, TransportRequest,
};
use cloud_sdk_testkit::{
    ExpectedRequest, FixtureBody, MockExchange, MockTransport, ResponseFixture,
};

let Ok(target) = RequestTarget::new("/resources?page=1") else {
    return;
};
let Ok(body) = FixtureBody::new(br#"{"resources":[]}"#) else {
    return;
};
let exchanges = [MockExchange::new(
    ExpectedRequest::new(Method::Get, target),
    ResponseFixture::success(body),
)];
let transport = MockTransport::new(&exchanges);
let mut output = [0_u8; 64];
let output_capacity = output.len();
let mut response_headers = [0_u8; cloud_sdk::transport::MAX_RESPONSE_HEADER_BYTES];
let mut response =
    ResponseBuffer::new(&mut output, output_capacity, &mut response_headers);

if transport
    .send(
        TransportRequest::new(Method::Get, target),
        response.writer(),
    )
    .is_err()
{
    return;
}

assert!(response
    .with_response(|view| {
        view.status().get() == 200
            && view.body() == br#"{"resources":[]}"#
    })
    .is_ok_and(core::convert::identity));
assert!(transport.is_complete());

The same mock implements the executor-neutral async contract without adding a runtime dependency:

use cloud_sdk::Method;
use cloud_sdk::transport::{
    RequestTarget, ResponseBuffer, TransportRequest, drive_async,
};
use cloud_sdk_testkit::{
    ExpectedRequest, FixtureBody, MockExchange, MockTransport, ResponseFixture,
};

let Ok(target) = RequestTarget::new("/resources/42") else { return };
let Ok(body) = FixtureBody::new(br#"{"id":42}"#) else { return };
let exchanges = [MockExchange::new(
    ExpectedRequest::new(Method::Get, target),
    ResponseFixture::success(body),
)];
let transport = MockTransport::new(&exchanges);
let mut output = [0_u8; 32];
let output_capacity = output.len();
let mut response_headers = [0_u8; cloud_sdk::transport::MAX_RESPONSE_HEADER_BYTES];
let mut response =
    ResponseBuffer::new(&mut output, output_capacity, &mut response_headers);
if drive_async(
    &transport,
    TransportRequest::new(Method::Get, target),
    response.writer(),
)
.await
.is_err()
{
    return;
}

assert!(response
    .with_response(|view| view.body() == br#"{"id":42}"#)
    .is_ok_and(core::convert::identity));

LocalMockTransport is deliberately !Sync and exercises the local async basic and authenticated contracts without a runtime. Use drive_local or PreparedRequest::execute_local_async to compile-check browser, embedded, and single-threaded workflows. Existing MockTransport automatically satisfies the local contract through its transaction-wrapped Send async implementation.

§Raw Delivery Faults

RawFaultExecutor injects a deterministic NotSent, PossiblySent, ResponseStarted, or unknown-delivery failure into both raw executor traits. Unknown delivery deliberately becomes PossiblySent:

use cloud_sdk_testkit::{RawFault, RawFaultExecutor};

let executor = RawFaultExecutor::new(RawFault::Unknown);
assert_eq!(executor, RawFaultExecutor::new(RawFault::Unknown));

Each exchange is consumed only after method, target, ordered headers, body, and complete response capacity match. Failures are distinct and payload-free. Debug output redacts request targets, header values, request bodies, and response bodies.

§Streaming Fixtures

StreamFixtureSource preserves ordered chunk boundaries, including explicit empty chunks. StreamFixtureSink uses caller storage and a deterministic maximum write size to exercise short writes and backpressure:

use cloud_sdk::transport::{
    StreamFraming, StreamKind, StreamLimits, StreamOutcome, StreamPolicy,
    StreamSinkMode, drive_blocking_stream,
};
use cloud_sdk_testkit::{StreamFixtureSink, StreamFixtureSource};

let chunks: &[&[u8]] = &[b"ab", b"", b"cde"];
let Ok(mut source) = StreamFixtureSource::new(chunks) else { return };
let mut output = [0_u8; 5];
let Ok(mut sink) = StreamFixtureSink::new(&mut output, 2) else { return };
let Ok(limits) = StreamLimits::new(5, 3, 3, 7, 1) else { return };
let Ok(policy) = StreamPolicy::new(
    StreamKind::FiniteDownload,
    StreamFraming::Declared(5),
    StreamSinkMode::Transactional,
    limits,
) else { return };
let mut scratch = [0_u8; 3];
let mut outcome = StreamOutcome::new();

assert!(drive_blocking_stream(
    policy,
    &mut source,
    &mut sink,
    &mut scratch,
    &mut outcome,
).is_ok());
assert_eq!(sink.bytes(), b"abcde");
assert_eq!(sink.writes(), 3);

The same fixtures implement the Send async contracts and therefore the local async contracts. Sources are non-replayable by default; use StreamFixtureSource::with_replayability only with an exact StreamSourceId. See the main streaming contract.

Use with_fault_at_observation and with_fault_at_write for exact one-based I/O failures. A validated StreamPatternSource with EndlessEmpty models an endless zero-progress peer, while AlternatingEmptyData requires a nonempty borrowed chunk and alternates it with explicit empty chunks. Pattern sources never emit StreamRead::End; the core stream policy must stop them through its byte, chunk, observation, or zero-progress bounds.

§Prepared Request Assertions

Bind MockTransport with with_endpoint before executing a PreparedRequest. Endpoint mismatches fail before an exchange is consumed. ExpectedRequest::with_headers checks the exact ordered request-header block. ResponseFixture::with_headers adds complete bounded raw metadata, while with_content_type models missing, accepted, unexpected, or malformed typed content metadata.

Core clears the complete caller buffer independently of the mock, so prepared tests can assert cleanup even when endpoint or fixture validation fails before a response is returned. The mock’s sanitizer implementation remains available only for tests that deliberately exercise the additive hook.

PreparedRequestRecord::capture records method, redacted target/body lengths, provider service and endpoint policy, complete operation metadata, checked response policy, authentication scope, raw response policy, and explicit body replayability without copying request values. Tests can therefore assert both safety classification and the complete authenticated wire contract.

§Fixture Builders

ResponseFixture builds deterministic success, paginated, action, rate-limit, and error responses. PaginationFixture, ActionFixture, and RateLimitFixture reject incoherent metadata before a fixture can be used. Use ResponseFixture::with_rate_limit and with_content_type to attach transport metadata to paginated, action, success, or error responses.

FixtureBody supports borrowed bytes and compact repeated-byte bodies up to 8 MiB plus one byte. Writes preflight capacity and leave undersized destination buffers unchanged.

§Dynamic Scenarios

DynamicMockTransport invokes a borrowed ProviderFixtureBuilder for each request and records only successful steps. DynamicResponder adapts a closure; provider crates can implement the trait directly for reusable fixture builders. Selection failures, undersized response storage, and cancellation do not consume a sequence number or create a record.

use cloud_sdk::Method;
use cloud_sdk::transport::{
    BlockingTransport, RequestTarget, ResponseBuffer, TransportRequest,
};
use cloud_sdk_testkit::{
    DynamicMockTransport, DynamicRequest, DynamicResponder, FixtureBody,
    RequestRecordSlot, ResponseFixture,
};

let Ok(target) = RequestTarget::new("/resources") else { return };
let Ok(body) = FixtureBody::new(br#"{"resources":[]}"#) else { return };
let fixture = ResponseFixture::success(body);
let responder = DynamicResponder::new(|request: DynamicRequest<'_>| {
    if request.method() == Method::Get && request.target() == target {
        Ok(&fixture)
    } else {
        Err(())
    }
});
let records = [const { RequestRecordSlot::new() }; 2];
let Ok(transport) = DynamicMockTransport::new(responder, &records) else {
    return;
};
let mut output = [0_u8; 32];
let output_capacity = output.len();
let mut headers = [0_u8; 64];
let mut response = ResponseBuffer::new(&mut output, output_capacity, &mut headers);

assert!(transport
    .send(
        TransportRequest::new(Method::Get, target),
        response.writer(),
    )
    .is_ok());
assert_eq!(transport.recorded(), 1);
let Some(record) = transport.record(0) else { return };
assert_eq!(record.body_len(), 0);
assert_eq!(record.status().get(), 200);

PaginationScript requires page one through the declared last page with stable page size and totals. ActionScript requires nondecreasing progress, running intermediate steps, and exactly one final success or error. Both are finite, bounded to MAX_DYNAMIC_RECORDS, and implement ProviderFixtureBuilder.

RequestRecordSlot uses caller-owned atomic storage. Its public observation contains a finite method class, encoded target length, body length, header count, response status, and sequence number. It never retains target bytes, header names or values, request bodies, response bodies, or extension-method tokens.

§Adversarial Corpus

adversarial_corpus() returns reusable cases for:

  • malformed JSON;
  • additive unknown fields;
  • missing required fields;
  • an oversized response represented without an 8 MiB static allocation;
  • invalid pagination metadata;
  • an invalid action state and progress value.

Provider crates consume applicable cases in their own parser tests. The Hetzner Serde boundary exercises this corpus without making the testkit depend on cloud-sdk-hetzner.

§Features

FeatureDefaultEffect
defaultyesEmpty; keeps the testkit allocation-free, runtime-free, and no_std.
allocnoEnables allocation-bearing test helpers and cloud-sdk/alloc.
stdnoEnables alloc and standard-library integration without selecting a runtime.

Docs.rs builds with all features. The mock transport remains network-free in every configuration.

§Security Notes

This crate is test infrastructure, not a production transport. Core secret-capable header types do not expose ordinary equality. The mock uses a private exact byte matcher solely for deterministic expectations; it must not be exposed as a remote secret comparison oracle. Authentication, base URLs, headers, timeout policy, TLS, retry behavior, and secret ownership remain responsibilities of concrete transport adapters.

The testkit stores only borrowed expectations and fixture bodies. Dynamic responders execute synchronously during a transport poll and must remain deterministic, bounded, and free of blocking I/O. Callers must keep borrowed data alive and must still sanitize secret-bearing test buffers when their threat model requires it.

Structs§

ActionFixture
Action metadata for polling tests.
ActionScript
Coherent finite running-to-terminal action response sequence.
AdversarialFixture
Named adversarial response body.
DynamicMockTransport
Bounded allocation-free dynamic mock transport.
DynamicRequest
Read-only request view supplied to dynamic fixture builders.
DynamicResponder
Closure adapter for ProviderFixtureBuilder.
ExpectedRequest
Expected request fields for one mock exchange.
LocalMockTransport
Ordered mock transport whose futures are intentionally local-only.
MockExchange
One expected request and deterministic response.
MockTransport
Ordered no-allocation mock implementation of BlockingTransport.
PaginationFixture
Pagination metadata for a deterministic response.
PaginationScript
Coherent finite pagination response sequence.
PreparedRequestRecord
Non-secret record of one prepared request for policy assertions.
RateLimitFixture
Rate-limit metadata fixture.
RawFaultError
Payload-free deterministic raw fault.
RawFaultExecutor
No-allocation executor that fails at one selected delivery phase.
RecordedRequest
One committed request observation without request or response payloads.
RequestRecordSlot
Caller-owned atomic slot for one committed request observation.
ResponseFixture
Provider-neutral response body plus optional interpreted metadata.
StreamFixtureSink
Caller-buffered deterministic sink with configurable short writes.
StreamFixtureSource
Borrowed ordered chunks for one deterministic finite source.
StreamPatternSource
Non-terminating stream source used to verify cancellation and hard bounds.

Enums§

ActionState
Provider-neutral action lifecycle fixture.
AdversarialKind
Adversarial response category.
DynamicMockConfigError
Invalid dynamic mock configuration.
DynamicMockError
Dynamic mock failure. Builder details are never rendered by Debug or Display.
FixtureBody
Borrowed or compact repeated-byte fixture body.
FixtureBodyError
Fixture body construction or write error.
FixtureKind
Fixture response category.
FixtureMetadataError
Fixture metadata validation error.
MockError
Deterministic mock transport failure.
RawFault
Failure point injected by RawFaultExecutor.
RecordedMethod
Payload-free HTTP method classification.
ResponseFixtureError
Response fixture construction error.
ScenarioScriptError
Invalid or exhausted deterministic scenario script.
StreamFixtureError
Invalid fixture or deterministic fixture I/O failure.
StreamPattern
Non-terminating deterministic source pattern for hard-limit tests.

Constants§

DEFAULT_RESPONSE_LIMIT
Common response limit used by the initial provider response boundary.
MAX_DYNAMIC_RECORDS
Maximum caller-owned record slots accepted by one dynamic mock.
MAX_FIXTURE_BODY_BYTES
Maximum fixture body length, including one byte beyond the common 8 MiB response-policy ceiling for oversized-input tests.
MAX_STREAM_FIXTURE_CHUNKS
Maximum chunks in one borrowed stream fixture.

Traits§

ProviderFixtureBuilder
Provider-neutral contract for choosing one deterministic response fixture.

Functions§

adversarial_corpus
Creates the fixed six-case adversarial response corpus.