NovinkaZverejnili sme 50+ Laravel balíkov ako open source
Custom AI apps, agents and automation — Roundly ConsultingRoundly
Všetky balíky

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:

SlovesoOverenieNegácieKolekcia
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 call

Podpisuje 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:

FactoryStavy
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 kryptomien

Odoslaní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.