Skip to content

Testing

phil/standard-webhooks

What the library's own tests cover

The unit suite lives in tests/Unit and runs with PHPUnit 13. It needs ext-sodium, and no database, kernel or network. Times are fixed (@1793448000), never read from the system clock.

Test classWhat it pins
SignerAndVerifierTestThe signed content msg_1.1614265330.{"a":1}; v1a and v1 round trips; either key verifying during a rotation; a body changed by one byte failing; another id or timestamp failing; a signature of another scheme never accepted; no trusted key meaning NoValidSignature; a genuine but stale delivery rejected for its timestamp, with its exact message.
Ed25519KeyTestThe RFC 8032 vector; the public key verifying its secret key's signature and nothing else; whsk_ and whpk_ round trips; a seed and its 64-byte form being the same key; a mismatched public half refused; lengths and prefixes; the redacted dump.
HmacSecretTestThe Standard Webhooks v1 vector; 32-byte generation; verification of its own signatures only; the inclusive 24 and 64 byte bounds; prefixes; the redacted dump.
Base64TestBoth codecs encode the same padded alphabet, round-trip, and refuse the same inputs.
SignatureListTestBoth schemes in one header; unreadable entries dropped; repeated spaces; the cap of eight, enforced before any entry is read; at least one signature on building.
WebhookIdTest, WebhookTimestampTest, ToleranceTest, WebhookHeadersTestCharacter sets and bounds, strict Unix seconds, the inclusive two-sided tolerance, missing headers.
EnvelopeTestThe recommended fields; only type required; empty data; malformed bodies; the 64-level nesting bound; audience match and mismatch messages.
WebhookReceiverTestA cached key verifying without reload; an unknown key triggering exactly one reload; a forged delivery rejected after one reload; a stale delivery never reloading; audience acceptance and rejection; an empty expected audience refused. Uses the CountingKeyProvider fixture.
KeysTestKeys read by their prefix, others refused; FixedKeys giving its keys back on refresh and keeping them out of a stack trace; generated ids, random and valid.

Reference vectors

The two vectors every implementation must reproduce are listed in full in signatures.md: RFC 8032 section 7.1 test 1 for Ed25519, and the v1 example of the Standard Webhooks reference libraries for HMAC-SHA256. A change that breaks either is a change of algorithm, not a refactoring.

Running the tests

From the root of the monorepo:

vendor/bin/phpunit packages/standard-webhooks/tests

Installed on its own, in a fresh copy of the library:

cd standard-webhooks
composer install
vendor/bin/phpunit --no-configuration tests

Infection keeps the library at 100 % of mutants killed.

Testing your adapters and your endpoint

VerificationKeyProvider: assert that keys() does not fetch, that refresh() fetches, that a second refresh() within your budget returns the cached keys without fetching, and that a failing source leaves the cached keys in place. A counting fixture like the library's CountingKeyProvider is enough to test the receiver around it:

final class CountingKeyProvider implements VerificationKeyProvider
{
    public int $refreshes = 0;

    public function __construct(private readonly array $cached, private readonly array $reloaded) {}

    public function keys(): array { return $this->cached; }

    public function refresh(): array { ++$this->refreshes; return $this->reloaded; }
}

ProcessedWebhookRepository, against the real database: the first claim() returns true, the second false; a claim inside a transaction that rolls back leaves the id claimable; two connections claiming the same id concurrently yield exactly one true.

Your endpoint: sign test deliveries with the library's Signer and a generated key, then send them through your controller:

$key = Ed25519SecretKey::generate();
$body = '{"type":"invoice.paid","data":{"id":"abc"},"audience":"receiver-1"}';
$headers = new Signer()->sign(WebhookId::fromString('msg_1'), WebhookTimestamp::fromDateTime($now), $body, $key)->toArray();

$client->request('POST', '/webhooks', server: [
    'HTTP_WEBHOOK_ID' => $headers['webhook-id'],
    'HTTP_WEBHOOK_TIMESTAMP' => $headers['webhook-timestamp'],
    'HTTP_WEBHOOK_SIGNATURE' => $headers['webhook-signature'],
], content: $body);

Cover at least: a valid delivery answered 2xx and processed once; the same delivery again answered 2xx and not processed; a body with one byte changed refused; a timestamp one second past the tolerance refused; a delivery for another audience refused; an unknown event type not answered 2xx.

To test a receiver written without the library, use the fixed v1a example in signatures.md with signatureVerifies()-style logic, or the v1 vector.

Benchmarks

The phpbench benchmarks in tests/Benchmark measure the hot paths at several sizes. From the root of the monorepo:

composer bench -- packages/standard-webhooks/tests/Benchmark