'user.created', 'user_id' => 123, 'email' => 'test@example.com' ], 'user.created'); expect($payload->get('user_id'))->toBe(123); expect($payload->get('email'))->toBe('test@example.com'); expect($payload->getEventType())->toBe('user.created'); expect($payload->isEmpty())->toBeFalse(); expect($payload->has('user_id'))->toBeTrue(); expect($payload->has('missing'))->toBeFalse(); }); it('creates webhook payload from request', function () { $data = ['event' => 'payment.succeeded', 'amount' => 1000]; $rawBody = json_encode($data); $headers = [ 'X-Event-Type' => 'payment.succeeded', 'X-Webhook-ID' => 'wh_test_123' ]; $payload = WebhookPayload::fromRequest($data, $rawBody, $headers); expect($payload->getEventType())->toBe('payment.succeeded'); expect($payload->getWebhookId())->toBe('wh_test_123'); expect($payload->get('amount'))->toBe(1000); expect($payload->getHeader('X-Event-Type'))->toBe('payment.succeeded'); }); it('gets default values for missing keys', function () { $payload = WebhookPayload::create(['key' => 'value']); expect($payload->get('missing', 'default'))->toBe('default'); expect($payload->get('key', 'default'))->toBe('value'); }); it('converts to array and json', function () { $data = ['event' => 'test', 'data' => 'value']; $payload = WebhookPayload::create($data); expect($payload->toArray())->toBe($data); expect($payload->jsonSerialize())->toBe($data); }); it('detects empty payloads', function () { $empty = WebhookPayload::create([]); $filled = WebhookPayload::create(['key' => 'value']); expect($empty->isEmpty())->toBeTrue(); expect($filled->isEmpty())->toBeFalse(); }); it('gets headers with case variations', function () { $headers = [ 'X-Event-Type' => 'test.event', 'x-custom-header' => 'value' // Lowercase key ]; $payload = WebhookPayload::fromRequest([], '{}', $headers); // Exact match expect($payload->getHeader('X-Event-Type'))->toBe('test.event'); // Case-insensitive fallback for lowercase keys expect($payload->getHeader('X-Custom-Header'))->toBe('value'); expect($payload->getHeader('missing'))->toBeNull(); }); }); describe('WebhookProvider', function () { it('creates stripe provider', function () { $stripe = WebhookProvider::stripe(); expect($stripe->toString())->toBe('stripe'); expect($stripe->name)->toBe('stripe'); expect($stripe->signatureHeader)->toBe('Stripe-Signature'); expect($stripe->eventTypeHeader)->toBe('Stripe-Event'); expect($stripe->signatureAlgorithm)->toBe('sha256'); expect($stripe->isKnownProvider())->toBeTrue(); }); it('creates github provider', function () { $github = WebhookProvider::github(); expect($github->toString())->toBe('github'); expect($github->name)->toBe('github'); expect($github->signatureHeader)->toBe('X-Hub-Signature-256'); expect($github->eventTypeHeader)->toBe('X-GitHub-Event'); expect($github->signatureAlgorithm)->toBe('sha256'); expect($github->isKnownProvider())->toBeTrue(); }); it('creates legal service provider', function () { $legal = WebhookProvider::legalService(); expect($legal->toString())->toBe('legal-service'); expect($legal->name)->toBe('legal-service'); expect($legal->signatureHeader)->toBe('X-Legal-Signature'); expect($legal->eventTypeHeader)->toBe('X-Legal-Event-Type'); expect($legal->isKnownProvider())->toBeTrue(); }); it('creates generic provider', function () { $generic = WebhookProvider::generic('custom-service'); expect($generic->toString())->toBe('custom-service'); expect($generic->name)->toBe('custom-service'); expect($generic->signatureHeader)->toBe('X-Signature'); expect($generic->eventTypeHeader)->toBe('X-Event-Type'); expect($generic->signatureAlgorithm)->toBe('sha256'); expect($generic->isKnownProvider())->toBeFalse(); }); it('creates provider from string', function () { $stripe = WebhookProvider::fromString('stripe'); expect($stripe->toString())->toBe('stripe'); expect($stripe->signatureHeader)->toBe('Stripe-Signature'); $github = WebhookProvider::fromString('github'); expect($github->toString())->toBe('github'); expect($github->signatureHeader)->toBe('X-Hub-Signature-256'); $custom = WebhookProvider::fromString('my-service'); expect($custom->toString())->toBe('my-service'); expect($custom->signatureHeader)->toBe('X-Signature'); }); it('compares providers for equality', function () { $stripe1 = WebhookProvider::stripe(); $stripe2 = WebhookProvider::fromString('stripe'); $github = WebhookProvider::github(); expect($stripe1->equals($stripe2))->toBeTrue(); expect($stripe1->equals($github))->toBeFalse(); }); it('validates provider names', function () { expect(fn() => WebhookProvider::create('')) ->toThrow(\InvalidArgumentException::class, 'Provider name cannot be empty'); }); it('normalizes provider names', function () { $provider = WebhookProvider::create(' Custom-Service '); expect($provider->toString())->toBe('custom-service'); }); }); describe('WebhookSignature', function () { it('creates signature with algorithm', function () { $signature = WebhookSignature::create('abc123def456', 'sha256', 'hex'); expect($signature->toString())->toBe('abc123def456'); expect($signature->value)->toBe('abc123def456'); expect($signature->algorithm)->toBe('sha256'); expect($signature->encoding)->toBe('hex'); }); it('creates signature from header', function () { $sig1 = WebhookSignature::fromHeader('sha256=abc123'); expect($sig1->toString())->toBe('abc123'); expect($sig1->algorithm)->toBe('sha256'); $sig2 = WebhookSignature::fromHeader('v1=def456'); expect($sig2->toString())->toBe('def456'); expect($sig2->algorithm)->toBe('v1'); $sig3 = WebhookSignature::fromHeader('xyz789'); expect($sig3->toString())->toBe('xyz789'); expect($sig3->algorithm)->toBe('sha256'); // Default }); it('creates signature from stripe header', function () { $stripeHeader = 't=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd'; $signature = WebhookSignature::fromStripeHeader($stripeHeader); expect($signature->toString())->toBe('5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd'); expect($signature->algorithm)->toBe('sha256'); }); it('throws on invalid stripe header', function () { // Stripe header requires "key=value" format with v1 signature expect(fn() => WebhookSignature::fromStripeHeader('invalid')) ->toThrow(\InvalidArgumentException::class); }); it('formats signature for header', function () { $signature = WebhookSignature::create('abc123', 'sha256'); expect($signature->toHeaderFormat())->toBe('sha256=abc123'); expect($signature->toHeaderFormat('v1'))->toBe('v1=abc123'); }); it('compares signatures securely', function () { $sig1 = WebhookSignature::create('abc123', 'sha256'); $sig2 = WebhookSignature::create('abc123', 'sha256'); $sig3 = WebhookSignature::create('def456', 'sha256'); $sig4 = WebhookSignature::create('abc123', 'sha512'); expect($sig1->equals($sig2))->toBeTrue(); expect($sig1->equals($sig3))->toBeFalse(); expect($sig1->equals($sig4))->toBeFalse(); // Different algorithm }); it('verifies signature with sha1', function () { $payload = 'test payload'; $secret = 'secret_key'; $expectedSig = hash_hmac('sha1', $payload, $secret); $signature = WebhookSignature::create($expectedSig, 'sha1'); expect($signature->verify($payload, $secret))->toBeTrue(); }); it('verifies signature with sha256', function () { $payload = 'test payload'; $secret = 'secret_key'; $expectedSig = hash_hmac('sha256', $payload, $secret); $signature = WebhookSignature::create($expectedSig, 'sha256'); expect($signature->verify($payload, $secret))->toBeTrue(); }); it('verifies signature with sha512', function () { $payload = 'test payload'; $secret = 'secret_key'; $expectedSig = hash_hmac('sha512', $payload, $secret); $signature = WebhookSignature::create($expectedSig, 'sha512'); expect($signature->verify($payload, $secret))->toBeTrue(); }); it('fails verification with wrong signature', function () { $signature = WebhookSignature::create('wrong_signature', 'sha256'); expect($signature->verify('test payload', 'secret_key'))->toBeFalse(); }); it('fails verification with wrong secret', function () { $payload = 'test payload'; $correctSecret = 'correct_secret'; $wrongSecret = 'wrong_secret'; $expectedSig = hash_hmac('sha256', $payload, $correctSecret); $signature = WebhookSignature::create($expectedSig, 'sha256'); expect($signature->verify($payload, $wrongSecret))->toBeFalse(); }); it('throws on unsupported algorithm', function () { $signature = WebhookSignature::create('abc123', 'md5'); expect(fn() => $signature->verify('payload', 'secret')) ->toThrow(\InvalidArgumentException::class, 'Unsupported algorithm: md5'); }); it('validates signature value', function () { expect(fn() => WebhookSignature::create('', 'sha256')) ->toThrow(\InvalidArgumentException::class, 'Webhook signature cannot be empty'); }); }); });