'application/json', 'HTTP_PAYPAL_AUTH_ALGO' => 'SHA256withRSA', 'HTTP_PAYPAL_CERT_URL' => 'https://api.paypal.com/cert.pem', 'HTTP_PAYPAL_TRANSMISSION_ID' => 'transmission-1', 'HTTP_PAYPAL_TRANSMISSION_SIG' => 'signature', 'HTTP_PAYPAL_TRANSMISSION_TIME' => '2026-08-10T08:00:00Z', ], json_encode($event, JSON_THROW_ON_ERROR), ); } public function test_it_acknowledges_a_verified_unknown_event(): void { $verifier = Mockery::mock(WebhookVerifier::class); $verifier->shouldReceive('verify')->once()->andReturnTrue(); $service = Mockery::mock(WebhookService::class); $service->shouldNotReceive('handleCaptureCompleted'); $controller = new WebhookController($verifier, $service); $response = $controller->hook($this->request([ 'id' => 'WH-EVENT-IGNORED', 'event_type' => 'CHECKOUT.ORDER.APPROVED', ])); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('ignored', $response->getData(true)['status']); } public function test_it_rejects_an_invalid_signature(): void { $verifier = Mockery::mock(WebhookVerifier::class); $verifier->shouldReceive('verify')->once()->andReturnFalse(); $controller = new WebhookController( $verifier, Mockery::mock(WebhookService::class), ); $response = $controller->hook($this->request([ 'id' => 'WH-EVENT-BAD-SIGNATURE', 'event_type' => 'PAYMENT.CAPTURE.COMPLETED', ])); $this->assertSame(400, $response->getStatusCode()); $this->assertSame('invalid_signature', $response->getData(true)['status']); } public function test_it_returns_a_retryable_error_when_processing_fails(): void { $verifier = Mockery::mock(WebhookVerifier::class); $verifier->shouldReceive('verify')->once()->andReturnTrue(); $service = Mockery::mock(WebhookService::class); $service->shouldReceive('handleCaptureCompleted') ->once() ->andThrow(new \RuntimeException('temporary database failure')); $controller = new WebhookController($verifier, $service); $response = $controller->hook($this->request([ 'id' => 'WH-EVENT-RETRY', 'event_type' => 'PAYMENT.CAPTURE.COMPLETED', 'resource' => [], ])); $this->assertSame(500, $response->getStatusCode()); $this->assertSame('processing_failed', $response->getData(true)['status']); } }