Testovanie
Webhooks::fake()
use RoundlyConsulting\Webhooks\Facades\Webhooks;
use RoundlyConsulting\Webhooks\Testing\SentWebhook;
$fake = Webhooks::fake();
$sendInvoiceWebhook($invoice); // your code calls Webhooks::send() / broadcast() / …
$fake->assertSent($endpoint, 'invoice.paid', fn (array $payload): bool => $payload['invoice'] === 42);
$fake->assertSent('invoice.paid', payload: fn (array $payload): bool => $payload['invoice'] === 42); // without the endpoint
$fake->assertSent(fn (SentWebhook $sent): bool => $sent->endpoint->is($endpoint) && ! $sent->broadcast);
$fake->assertSentTimes(1, 'invoice.paid');
$fake->assertNotSent($otherEndpoint);
$fake->assertBroadcast('invoice.paid', $team, fn (array $payload): bool => $payload['invoice'] === 42);
$fake->assertBroadcastTimes(1, 'invoice.paid');
$fake->assertNotBroadcast('invoice.void');Každé zaznamenané sloveso má overenie aj jeho negáciu:
| Sloveso | Overenie | Negácie | Kolekcia |
|---|---|---|---|
send() | assertSent($endpoint|$type|Closure, ?$type, ?$payload), assertSentTimes($n, …) | assertNotSent(…), assertNothingSent() | sent() — SentWebhook: endpoint, eventType, payload, body, broadcast, owner, webhookId |
broadcast() | assertBroadcast($type, ?$owner, ?$payload), assertBroadcastTimes($n, $type, ?$owner, ?$payload) | assertNotBroadcast($type, ?$owner), assertNothingBroadcast() | broadcasts() — RecordedBroadcast: eventType, owner, payload |
redeliver() · redeliverFailed() | assertRedelivered($delivery|$endpoint) | assertNothingRedelivered() | redelivered() |
replay() | assertReplayed($delivery) | assertNothingReplayed() | replayed() |
rotateSecret() | assertSecretRotated($endpoint) | assertSecretNotRotated($endpoint), assertNoSecretsRotated() | — |
disable() | assertEndpointDisabled($endpoint) | assertEndpointNotDisabled($endpoint), assertNoEndpointsDisabled() | — |
- send(), broadcast(), ping(), redeliver(), redeliverFailed() a replay() overujú presne ako v produkcii — vypnuté endpointy alebo endpointy bez odberu, chybné typy udalostí, príliš veľký obsah, práve spracúvané či vyčistené doručenia vyhodia rovnaké výnimky — no nič nezaradia, nič neodošlú a nezapíšu žiadne doručenie.
- Fake send() vráti neuložené OutboundDelivery: exists je false a getKey() je null — porovnávajte jeho webhookId() (msg_…). redeliverFailed() vráti počty, ktoré by produkcia zaradila a preskočila.
- Správa endpointov, receive() a verify() bežia naozaj; rotácie kľúčov a disable() bežia naozaj a zároveň sa zaznamenajú. dispatchDue() — a teda webhooks:retry — nič neodošle a vráti 0.
- Broadcast zaznamená aj SentWebhook pre každý endpoint, ktorý ho prijme (broadcast: true); ping() sa zaznamená ako odoslanie webhook.ping; opakovanie idempotentného odoslania sa znova nezaznamená.
- Fake rozširuje manažér, takže aj injektovaný WebhooksManager je fake, a vidí volania cez inbound(), outbound(), for($owner), trait HasWebhooks aj udalosti BroadcastsAsWebhook. Prežije reset scope, takže test so skutočným queue:work stále komunikuje s tým istým fake.
SignedWebhookRequest — prichádzajúce požiadavky pre testy
use RoundlyConsulting\Webhooks\Testing\SignedWebhookRequest;
SignedWebhookRequest::for($endpoint)
->payload(['type' => 'invoice.paid', 'data' => ['id' => 42]]) // or ->body('exact bytes')
->send() // through the HTTP kernel: TestResponse
->assertAccepted();
SignedWebhookRequest::for($endpoint)->tamper()->send()->assertUnauthorized();
SignedWebhookRequest::for($endpoint)->staleBy(600)->send()->assertUnauthorized();
SignedWebhookRequest::for($endpoint)->unsigned()->send()->assertUnauthorized(); // missing_signature
SignedWebhookRequest::for($endpoint)->withoutHeader('webhook-timestamp')->send()->assertUnauthorized();
SignedWebhookRequest::for($endpoint)->idempotencyKey('order-7')->send();
SignedWebhookRequest::for($rotatedEndpoint)->signedWithPrevious()->send(); // rotation grace
SignedWebhookRequest::for($endpoint)->id('msg_1')->at($instant)->secret('whsec_…')->withHeader('X-A', 'b');
SignedWebhookRequest::for($rfc9421Endpoint)->keyId('partner')->send(); // signed by sentinel
$data = SignedWebhookRequest::for($endpoint)->toRequest(); // SignedRequestData: uri, headers, body
$this->call('POST', $data->uri, [], [], [], $data->server(), $data->body); // your own callPodpisuje vlastnou schémou endpointu: predvolenou schémou, akoukoľvek schémou implementujúcou SignsOutboundWebhooks (recepty github a stripe ju implementujú), alebo — pri schéme bez sign() — closure, ktorú zadáte. Inak toRequest() vyhodí LogicException s názvom schémy; nikdy nepošle hlavičky, ktoré by schéma neprijala.
SignedWebhookRequest::for($endpoint)
->signUsing(fn (OutgoingWebhook $message, EndpointSecrets $secrets): SignatureHeaders => new SignatureHeaders([/* … */]))
->send();tamper() pri predvolenej schéme a RFC 9421 zmení bajt podpisu, pri ostatných podpísané telo. signedWithPrevious() na endpointe bez predchádzajúceho kľúča vyhodí LogicException, rovnako ako endpoint RFC 9421 bez keyId() aj bez scheme_options.key_id.
Factories
Každý model balíka má factory a každá vytvára nakonfigurovanú triedu modelu, takže vymenený model dostane vlastné inštancie:
| Factory | Stavy |
|---|---|
InboundEndpoint::factory() | withSecret(string $secret), disabled(), scheme(string $scheme), handler(string $alias), ownedBy(Model $owner) |
OutboundEndpoint::factory() | url(string $url), subscribedTo(array $types), withSecret(string $secret), withHeaders(array $headers), disabled(), ownedBy(Model $owner) |
OutboundDelivery::factory() | status(OutboundStatus $status) |
InboundDelivery::factory() · OutboundAttempt::factory() | — |
$endpoint = InboundEndpoint::factory()->handler('billing')->ownedBy($team)->create();
$outbound = OutboundEndpoint::factory()->url('https://hooks.example.com/in')->subscribedTo(['invoice.*'])->create();
$failed = OutboundDelivery::factory()->status(OutboundStatus::Failed)->create();Odchádzajúci endpoint z factory pri uložení preskočí kontrolu SSRF — jeho URL sa preloží až pri pokuse. Na tieto preklady odpovedzte cez FakeHostResolver.
DNS pre testy odosielania
use RoundlyConsulting\Webhooks\Testing\FakeHostResolver;
$dns = FakeHostResolver::map(['hooks.example.com' => '93.184.216.34'])->install(); // = SafeFetch::fakeDns()
Http::fake(['*' => Http::response('ok')]);FakeHostResolver::map(array $records) prijíma hostiteľ ⇒ jedna adresa alebo zoznam (A aj AAAA); install() vráti fake resolver zo Safe Fetch s jeho overeniami (assertLookedUp($host, $times)). Meno, ktoré tabuľka nepozná, sa nepreloží, takže test sa nikdy nedostane k skutočnému DNS. Laravel odovzdá stubu Http::fake() voľby požiadavky, takže test vidí aj pripnutie:
Http::fake(fn (Request $request, array $options) => Http::response($options['curl'][CURLOPT_RESOLVE][0]));Registre a fake Sentinelu
Schémy a handlery registrované za behu platia pre celý proces; v teste ich odstránite (nakonfigurované ostanú):
Webhooks::schemes()->forget('github');
Webhooks::handlers()->forget('billing');Pri Sentinel::fake() overenie prichádzajúcich a podpis odchádzajúcich správ zaznamenajú verifyWithSecrets a signWithSecret — nikdy tajný kľúč — a vypočítajú skutočný výsledok; deduplikácia sa správa tak ako fake Sentinelu.
Prejavte lásku k open source
Tento balík je zadarmo pod licenciou MIT. Ak vám šetrí čas, jednorazový príspevok alebo členstvo na Patreone nám pomôže ho ďalej udržiavať, testovať a dokumentovať.
Ďalšie spôsoby podpory vrátane kryptomienOdoslaním daru súhlasíte s našimi podmienkami prijímania darov.
Chcete to zabudovať do svojho produktu?
Naše balíky integrujeme do zákazkových Laravel a AI riešení. Napíšte nám, na čom pracujete, a ozveme sa do 48 hodín.