Files
michaelschiemer/tests/Unit/Framework/Webhook/WebhookServiceTest.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- 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.
2025-10-25 19:18:37 +02:00

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');
});
});
});