Testing

The Repost.Client.Testing package: the scripted StubTransport, recorded-request assertions, and deterministic clocks and generators.

Validation, defaults, serialization, the typed method tree: everything a send does before the network runs for real under the stub transport, so tests exercise your schema without any HTTP. The compiler is part of the test surface too: wrong payloads, unknown events, and missing fields fail at build time.

The Repost.Client.Testing package holds the test doubles. Add it in your test project:

dotnet add package Repost.Client.Testing

The stub transport

StubTransport is a scripted, no-network ITransport: enqueue the responses each attempt should return, plug it into the Transport option, send, and assert on what it recorded. The Authorization header is stripped from every recording, and the body bytes are copied verbatim:

using Repost.Client;
using Repost.Client.Testing;
using Contoso.Events;
 
var transport = new StubTransport().EnqueueResponse(
    202,
    "{\"id\":\"msg_1\",\"type\":\"book.created\",\"customerId\":\"cus_123\","
        + "\"timestamp\":\"1970-01-01T00:00:00.000Z\"}");
 
await using var repost = new RepostClient(new RepostClientOptions
{
    ApiKey = "test-key",
    Transport = transport,
    MaxAttempts = 1,
});
 
SendResult result = await repost.Webhooks.Book.CreatedAsync(new BookCreatedInput
{
    CustomerId = "cus_123",
    Data = new Book
    {
        Title = "Dune",
        Authors = new[] { new Author { Name = "Frank Herbert" } },
        Price = 9.99,
        Currency = Currency.Usd,
    },
});
 
Assert.Equal("msg_1", result.Id);
 
RecordedRequest sent = transport.Requests[0];
string body = Encoding.UTF8.GetString(sent.Body);
Assert.Contains("\"title\":\"Dune\"", body);

Credentials still resolve before the stub runs, so tests exercise the production configuration path; any grammar-valid key satisfies it. The returned SendResult echoes the id, type, customer, and timestamp from the scripted response body.

StubTransport is scripted, not zero-config: an attempt with an empty script faults with InvalidOperationException, a raw transport defect the runtime never retries. Enqueue one response per attempt you expect.

Scripting failures and retries

Beyond EnqueueResponse, the transport scripts the other attempt shapes: EnqueueFailure(TransportFailureException) for a classified, retry-eligible network failure, EnqueueException(Exception) for a raw defect, and EnqueuePending() for a response your test completes later through the returned ControlledResponse. To drive a retry test, enqueue two 500s and a 202, raise MaxAttempts, and assert transport.Requests.Count == 3. AwaitRequestCountAsync(count, timeout) waits for in-flight sends to reach a request count.

Deterministic values

Pin the injectable seams to freeze timestamps and generated ids. TestDefaultValueGenerators and TestIdempotencyKeyGenerator provide Fixed, Sequence, and Failing factories:

var options = new RepostClientOptions
{
    ApiKey = "test-key",
    Transport = transport,
    DefaultValueGenerators = TestDefaultValueGenerators.Fixed(
        DateTimeOffset.UnixEpoch,
        "00000000-0000-4000-8000-000000000000",
        "ck0nf0rm4nc3f1x3dcu1d000"),
    IdempotencyKeyGenerator = TestIdempotencyKeyGenerator.Fixed("book_dune:created"),
};

With Now() pinned, the envelope timestamp and every @default(now()) field are fixed, so you can script an exact accepted-response body. The package also ships ManualTimeProvider (a fully deterministic clock you Advance to fire retry timers), DeterministicRetryEntropy (Zero or a scripted jitter sequence), and RecordingObserver (asserts on the lifecycle events the runtime emits).

Continue