- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
279 lines
11 KiB
PHP
279 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Unit\Framework\Webhook;
|
|
|
|
use App\Framework\Webhook\ValueObjects\WebhookPayload;
|
|
use App\Framework\Webhook\ValueObjects\WebhookProvider;
|
|
use App\Framework\Webhook\ValueObjects\WebhookSignature;
|
|
|
|
describe('WebhookService', function () {
|
|
describe('WebhookPayload', function () {
|
|
it('creates webhook payload from data', function () {
|
|
$payload = WebhookPayload::create([
|
|
'event' => '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');
|
|
});
|
|
});
|
|
});
|