From bfe874e026674f755bb242d661f24a5233373c9d Mon Sep 17 00:00:00 2001 From: Maciej Kwiatkowski Date: Thu, 28 Nov 2024 11:42:34 +0100 Subject: [PATCH 1/3] SP-1024 Implementing IPN HMAC Verification --- .../BaseUpdateInvoiceValidator.php | 2 +- .../BitPaySignatureValidator.php | 50 ++++ .../BitPaySignatureValidatorInterface.php | 19 ++ .../UpdateInvoiceIpnValidator.php | 14 +- .../UpdateInvoiceUsingBitPayIpn.php | 8 +- .../UpdateInvoice/UpdateInvoiceValidator.php | 2 +- .../Invoice/UpdateInvoiceController.php | 11 +- .../Laravel/AppServiceProvider.php | 10 +- .../SignatureVerificationFailed.php | 13 + routes/web.php | 1 - .../UpdateInvoice/UpdateInvoiceTest.php | 233 ++++++++++++++++++ .../UpdateInvoice/UpdateInvoiceTestCase.php | 83 ------- .../ValidateBitPayWebhookTest.php | 78 ++++++ 13 files changed, 427 insertions(+), 97 deletions(-) create mode 100644 app/Features/Invoice/UpdateInvoice/BitPaySignatureValidator.php create mode 100644 app/Features/Invoice/UpdateInvoice/BitPaySignatureValidatorInterface.php create mode 100644 app/Shared/Exceptions/SignatureVerificationFailed.php create mode 100644 tests/Integration/Features/Invoice/UpdateInvoice/UpdateInvoiceTest.php delete mode 100644 tests/Integration/Features/Invoice/UpdateInvoice/UpdateInvoiceTestCase.php create mode 100644 tests/Unit/Features/Invoice/UpdateInvoice/ValidateBitPayWebhookTest.php diff --git a/app/Features/Invoice/UpdateInvoice/BaseUpdateInvoiceValidator.php b/app/Features/Invoice/UpdateInvoice/BaseUpdateInvoiceValidator.php index 5a856a6..9f1814e 100644 --- a/app/Features/Invoice/UpdateInvoice/BaseUpdateInvoiceValidator.php +++ b/app/Features/Invoice/UpdateInvoice/BaseUpdateInvoiceValidator.php @@ -13,7 +13,7 @@ final class BaseUpdateInvoiceValidator implements UpdateInvoiceValidator { - public function execute(?array $data, ?BitPayInvoice $bitPayInvoice): void + public function execute(?array $data, ?BitPayInvoice $bitPayInvoice, array $headers): void { if (!$data) { throw new ValidationFailed('Missing data'); diff --git a/app/Features/Invoice/UpdateInvoice/BitPaySignatureValidator.php b/app/Features/Invoice/UpdateInvoice/BitPaySignatureValidator.php new file mode 100644 index 0000000..1b744ce --- /dev/null +++ b/app/Features/Invoice/UpdateInvoice/BitPaySignatureValidator.php @@ -0,0 +1,50 @@ +bitpayConfiguration = $bitpayConfiguration; + } + + public function execute(array $data, array $headers): void + { + $token = $this->bitpayConfiguration->getToken(); + + if (!$token) { + throw new RuntimeException(self::MISSING_TOKEN_MESSAGE); + } + + $sigHeader = $headers['x-signature'][0] ?? null; + + if (!$sigHeader) { + throw new SignatureVerificationFailed(self::MISSING_SIGNATURE_MESSAGE); + } + + $hmac = base64_encode(hash_hmac( + 'sha256', + json_encode($data), + $token, + true + )); + + if ($sigHeader !== $hmac) { + throw new SignatureVerificationFailed(self::INVALID_SIGNATURE_MESSAGE); + } + } +} diff --git a/app/Features/Invoice/UpdateInvoice/BitPaySignatureValidatorInterface.php b/app/Features/Invoice/UpdateInvoice/BitPaySignatureValidatorInterface.php new file mode 100644 index 0000000..1b0512a --- /dev/null +++ b/app/Features/Invoice/UpdateInvoice/BitPaySignatureValidatorInterface.php @@ -0,0 +1,19 @@ +updateInvoiceValidator = $updateInvoiceValidator; $this->logger = $logger; + $this->bitPaySignatureValidator = $bitPaySignatureValidator; } - public function execute(?array $data, ?BitPayInvoice $bitPayInvoice): void + public function execute(?array $data, ?BitPayInvoice $bitPayInvoice, ?array $headers): void { try { - $this->updateInvoiceValidator->execute($data, $bitPayInvoice); + $this->bitPaySignatureValidator->execute($data, $headers); + $this->updateInvoiceValidator->execute($data, $bitPayInvoice, $headers); if (!$bitPayInvoice) { throw new ValidationFailed(self::MISSING_BITPAY_MESSAGE); diff --git a/app/Features/Invoice/UpdateInvoice/UpdateInvoiceUsingBitPayIpn.php b/app/Features/Invoice/UpdateInvoice/UpdateInvoiceUsingBitPayIpn.php index 4543884..52e46c1 100644 --- a/app/Features/Invoice/UpdateInvoice/UpdateInvoiceUsingBitPayIpn.php +++ b/app/Features/Invoice/UpdateInvoice/UpdateInvoiceUsingBitPayIpn.php @@ -16,6 +16,7 @@ use App\Models\Invoice\InvoicePayment; use App\Models\Invoice\InvoicePaymentCurrency; use App\Models\Invoice\InvoiceRepositoryInterface; +use App\Shared\Exceptions\SignatureVerificationFailed; class UpdateInvoiceUsingBitPayIpn { @@ -45,7 +46,7 @@ public function __construct( $this->bitPayConfiguration = $bitPayConfiguration; } - public function execute(string $uuid, array $data): void + public function execute(string $uuid, array $data, array $headers): void { $invoice = $this->invoiceRepository->findOneByUuid($uuid); if (!$invoice) { @@ -54,6 +55,7 @@ public function execute(string $uuid, array $data): void try { $client = $this->bitPayClientFactory->create(); + $bitPayInvoice = $client->getInvoice( $invoice->getBitpayId(), $this->bitPayConfiguration->getFacade(), @@ -61,11 +63,13 @@ public function execute(string $uuid, array $data): void ); $updateInvoiceData = $this->bitPayUpdateMapper->execute($data)->toArray(); - $this->updateInvoiceValidator->execute($data, $bitPayInvoice); + $this->updateInvoiceValidator->execute($data, $bitPayInvoice, $headers); $this->updateInvoice($invoice, $updateInvoiceData); $this->sendUpdateInvoiceNotification->execute($invoice, $data['event'] ?? null); + } catch (SignatureVerificationFailed $e) { + throw $e; } catch (\Exception | \TypeError $e) { $this->logger->error('INVOICE_UPDATE_FAIL', 'Failed to update invoice', [ 'id' => $invoice->id diff --git a/app/Features/Invoice/UpdateInvoice/UpdateInvoiceValidator.php b/app/Features/Invoice/UpdateInvoice/UpdateInvoiceValidator.php index 87b8cf1..6221bec 100644 --- a/app/Features/Invoice/UpdateInvoice/UpdateInvoiceValidator.php +++ b/app/Features/Invoice/UpdateInvoice/UpdateInvoiceValidator.php @@ -16,5 +16,5 @@ interface UpdateInvoiceValidator /** * @throws ValidationFailed */ - public function execute(?array $data, ?BitPayInvoice $bitPayInvoice): void; + public function execute(?array $data, ?BitPayInvoice $bitPayInvoice, array $headers): void; } diff --git a/app/Http/Controllers/Invoice/UpdateInvoiceController.php b/app/Http/Controllers/Invoice/UpdateInvoiceController.php index 2c2c3f7..331b154 100644 --- a/app/Http/Controllers/Invoice/UpdateInvoiceController.php +++ b/app/Http/Controllers/Invoice/UpdateInvoiceController.php @@ -14,6 +14,7 @@ use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; +use App\Shared\Exceptions\SignatureVerificationFailed; class UpdateInvoiceController extends Controller { @@ -30,17 +31,19 @@ public function execute(Request $request, string $uuid): Response { $this->logger->info('IPN_RECEIVED', 'Received IPN', $request->request->all()); - /** @var array $data */ - $data = $request->request->get('data'); - $event = $request->request->get('event'); + $payload = json_decode($request->getContent(), true); + $data = $payload['data']; + $event = $payload['event']; $data['uuid'] = $uuid; $data['event'] = $event['name'] ?? null; try { - $this->updateInvoice->execute($uuid, $data); + $this->updateInvoice->execute($uuid, $data, $request->headers->all()); } catch (MissingInvoice $e) { return response(null, Response::HTTP_NOT_FOUND); + } catch (SignatureVerificationFailed $e) { + return response($e->getMessage(), Response::HTTP_UNAUTHORIZED); } catch (\Exception $e) { return response('Unable to process update', Response::HTTP_BAD_REQUEST); } diff --git a/app/Infrastructure/Laravel/AppServiceProvider.php b/app/Infrastructure/Laravel/AppServiceProvider.php index 5c645cb..a9bd422 100644 --- a/app/Infrastructure/Laravel/AppServiceProvider.php +++ b/app/Infrastructure/Laravel/AppServiceProvider.php @@ -17,6 +17,8 @@ use App\Features\Invoice\UpdateInvoice\SendUpdateInvoiceEventStream; use App\Features\Invoice\UpdateInvoice\UpdateInvoiceIpnValidator; use App\Features\Invoice\UpdateInvoice\UpdateInvoiceValidator; +use App\Features\Invoice\UpdateInvoice\BitPaySignatureValidator; +use App\Features\Invoice\UpdateInvoice\BitPaySignatureValidatorInterface; use App\Features\Shared\Logger; use App\Features\Shared\SseConfiguration; use App\Features\Shared\UrlProvider; @@ -87,12 +89,18 @@ function () { SseMercureConfiguration::class ); + $this->app->bind( + BitPaySignatureValidatorInterface::class, + BitPaySignatureValidator::class + ); + $this->app->bind( UpdateInvoiceIpnValidator::class, function () { return new UpdateInvoiceIpnValidator( $this->app->make(BaseUpdateInvoiceValidator::class), - $this->app->make(Logger::class) + $this->app->make(Logger::class), + $this->app->make(BitPaySignatureValidator::class) ); } ); diff --git a/app/Shared/Exceptions/SignatureVerificationFailed.php b/app/Shared/Exceptions/SignatureVerificationFailed.php new file mode 100644 index 0000000..3a9a611 --- /dev/null +++ b/app/Shared/Exceptions/SignatureVerificationFailed.php @@ -0,0 +1,13 @@ +name('createInvoice'); Route::get('/invoices/{id}', [GetInvoiceViewController::class, 'execute'])->name('invoiceView'); Route::post('/invoices/{uuid}', [UpdateInvoiceController::class, 'execute'])->name('updateInvoice'); - diff --git a/tests/Integration/Features/Invoice/UpdateInvoice/UpdateInvoiceTest.php b/tests/Integration/Features/Invoice/UpdateInvoice/UpdateInvoiceTest.php new file mode 100644 index 0000000..c54c2b9 --- /dev/null +++ b/tests/Integration/Features/Invoice/UpdateInvoice/UpdateInvoiceTest.php @@ -0,0 +1,233 @@ +mock(BitPayClientFactory::class, function (MockInterface $mock) { + $mock->shouldReceive('create')->andReturn(new class ('', '') extends PosClient { + public function getInvoice( + string $invoiceId, + string $facade = Facade::Merchant, + bool $signRequest = true + ): Invoice { + $invoice = new Invoice(); + $invoice->setId(ExampleInvoice::BITPAY_ID); + $invoice->setOrderId(ExampleInvoice::BITPAY_ORDER_ID); + + return $invoice; + } + }); + }); + $this->mock(SendUpdateInvoiceEventStream::class, function (MockInterface $mock) { + $mock->shouldReceive('execute')->times(1); + }); + $this->mock(BitPayConfigurationInterface::class, function (MockInterface $mock) { + $mock->shouldReceive('getToken')->andReturn(ExampleInvoice::TOKEN); + $mock->shouldReceive('getDesign')->andReturn($this->createStub(Design::class)); + $mock->shouldReceive('getFacade')->andReturn(Facade::MERCHANT); + $mock->shouldReceive('isSignRequest')->andReturn(true); + }); + + $testedClass = $this->getTestedClass(); + $signature = base64_encode(hash_hmac('sha256', json_encode($data), ExampleInvoice::TOKEN, true)); + $testedClass->execute(ExampleInvoice::UUID, $data, ['x-signature' => [$signature]]); + + $invoice = $this->app->make(InvoiceRepositoryInterface::class)->findOne(1); + + Assert::assertEquals(ExampleInvoice::TOKEN, $invoice->token); + Assert::assertEquals('someBitpayId', $invoice->bitpay_id); + Assert::assertEquals('https://test.bitpay.com/invoice?id=MV9fy5iNDkqrg4qrfYpw75', $invoice->bitpay_url); + // phpcs:disable Generic.Files.LineLength.TooLong + Assert::assertEquals("{\"store\":\"store-1\",\"register\":\"2\",\"reg_transaction_no\":\"87678\",\"price\":\"76.70\"}", $invoice->pos_data_json); + Assert::assertEquals('expired', $invoice->status); + Assert::assertEquals(76.7, $invoice->price); + Assert::assertEquals('USD', $invoice->currency_code); + Assert::assertEquals('false', $invoice->exception_status); + + $eth = $invoice->getInvoicePayment()->paymentCurrencies()->where('currency_code', 'ETH')->first(); + $btc = $invoice->getInvoicePayment()->paymentCurrencies()->where('currency_code', 'BTC')->first(); + Assert::assertEquals(48312000000000000, $eth->total); + Assert::assertEquals(48312000000000000, $eth->subtotal); + Assert::assertEquals(347100, $btc->total); + Assert::assertEquals(342800, $btc->subtotal); + Assert::assertEquals(0, $invoice->getInvoicePayment()->amount_paid); + Assert::assertEquals('someBitpayOrderId', $invoice->bitpay_order_id); + } + + /** + * @test + */ + public function it_should_fail_updating_invoice_with_invalid_webhook_signature(): void + { + // given + $fileData = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'bitPayUpdate.json'); + $data = json_decode($fileData, true, 512, JSON_THROW_ON_ERROR); + + ExampleInvoice::createSaved(); + + $this->mock(BitPayClientFactory::class, function (MockInterface $mock) { + $mock->shouldReceive('create')->andReturn(new class ('', '') extends PosClient { + public function getInvoice( + string $invoiceId, + string $facade = Facade::Merchant, + bool $signRequest = true + ): Invoice { + $invoice = new Invoice(); + $invoice->setId(ExampleInvoice::BITPAY_ID); + $invoice->setOrderId(ExampleInvoice::BITPAY_ORDER_ID); + + return $invoice; + } + }); + }); + + $this->mock(SendUpdateInvoiceEventStream::class, function (MockInterface $mock) { + $mock->shouldReceive('execute')->never(); + }); + + $this->mock(BitPayConfigurationInterface::class, function (MockInterface $mock) { + $mock->shouldReceive('getToken')->andReturn(ExampleInvoice::TOKEN); + $mock->shouldReceive('getDesign')->andReturn($this->createStub(Design::class)); + $mock->shouldReceive('getFacade')->andReturn(Facade::MERCHANT); + $mock->shouldReceive('isSignRequest')->andReturn(true); + }); + + $headers = ['x-signature' => ['invalid-signature']]; + + $testedClass = $this->getTestedClass(); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage(BitPaySignatureValidator::INVALID_SIGNATURE_MESSAGE); + $testedClass->execute(ExampleInvoice::UUID, $data, $headers); + } + + /** + * @test + */ + public function it_should_fail_updating_invoice_with_missing_configuration_token(): void + { + // given + $fileData = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'bitPayUpdate.json'); + $data = json_decode($fileData, true, 512, JSON_THROW_ON_ERROR); + + ExampleInvoice::createSaved(); + + $this->mock(BitPayClientFactory::class, function (MockInterface $mock) { + $mock->shouldReceive('create')->andReturn(new class ('', '') extends PosClient { + public function getInvoice( + string $invoiceId, + string $facade = Facade::Merchant, + bool $signRequest = true + ): Invoice { + $invoice = new Invoice(); + $invoice->setId(ExampleInvoice::BITPAY_ID); + $invoice->setOrderId(ExampleInvoice::BITPAY_ORDER_ID); + + return $invoice; + } + }); + }); + + $this->mock(SendUpdateInvoiceEventStream::class, function (MockInterface $mock) { + $mock->shouldReceive('execute')->never(); + }); + + $this->mock(BitPayConfigurationInterface::class, function (MockInterface $mock) { + $mock->shouldReceive('getToken')->andReturn(null); + $mock->shouldReceive('getFacade')->andReturn(Facade::MERCHANT); + $mock->shouldReceive('isSignRequest')->andReturn(true); + }); + + // when + $testedClass = $this->getTestedClass(); + + // then + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage(BitPaySignatureValidator::MISSING_TOKEN_MESSAGE); + $testedClass->execute(ExampleInvoice::UUID, $data, [ + 'x-signature' => 'signature' + ]); + } + + /** + * @test + */ + public function it_should_fail_updating_invoice_with_missing_sig_header(): void + { + // given + $fileData = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'bitPayUpdate.json'); + $data = json_decode($fileData, true, 512, JSON_THROW_ON_ERROR); + + ExampleInvoice::createSaved(); + + $this->mock(BitPayClientFactory::class, function (MockInterface $mock) { + $mock->shouldReceive('create')->andReturn(new class ('', '') extends PosClient { + public function getInvoice( + string $invoiceId, + string $facade = Facade::Merchant, + bool $signRequest = true + ): Invoice { + $invoice = new Invoice(); + $invoice->setId(ExampleInvoice::BITPAY_ID); + $invoice->setOrderId(ExampleInvoice::BITPAY_ORDER_ID); + + return $invoice; + } + }); + }); + + $this->mock(SendUpdateInvoiceEventStream::class, function (MockInterface $mock) { + $mock->shouldReceive('execute')->never(); + }); + + $this->mock(BitPayConfigurationInterface::class, function (MockInterface $mock) { + $mock->shouldReceive('getToken')->andReturn(ExampleInvoice::TOKEN); + $mock->shouldReceive('getFacade')->andReturn(Facade::MERCHANT); + $mock->shouldReceive('isSignRequest')->andReturn(true); + }); + + $testedClass = $this->getTestedClass(); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage(BitPaySignatureValidator::MISSING_SIGNATURE_MESSAGE); + $testedClass->execute(ExampleInvoice::UUID, $data, []); // Empty headers array + } + + private function getTestedClass(): UpdateInvoiceUsingBitPayIpn + { + return $this->app->make(UpdateInvoiceUsingBitPayIpn::class); + } +} diff --git a/tests/Integration/Features/Invoice/UpdateInvoice/UpdateInvoiceTestCase.php b/tests/Integration/Features/Invoice/UpdateInvoice/UpdateInvoiceTestCase.php deleted file mode 100644 index 6600449..0000000 --- a/tests/Integration/Features/Invoice/UpdateInvoice/UpdateInvoiceTestCase.php +++ /dev/null @@ -1,83 +0,0 @@ -mock(BitPayClientFactory::class, function (MockInterface $mock) { - $mock->shouldReceive('create')->andReturn(new class ('', '') extends PosClient { - public function getInvoice( - string $invoiceId, - string $facade = Facade::Merchant, - bool $signRequest = true - ): Invoice { - $invoice = new Invoice(); - $invoice->setId(ExampleInvoice::BITPAY_ID); - $invoice->setOrderId(ExampleInvoice::BITPAY_ORDER_ID); - - return $invoice; - } - }); - }); - $this->mock(SendUpdateInvoiceEventStream::class, function (MockInterface $mock) { - $mock->shouldReceive('execute')->times(1); - }); - - $testedClass = $this->getTestedClass(); - $testedClass->execute(ExampleInvoice::UUID, $data); - - $invoice = $this->app->make(InvoiceRepositoryInterface::class)->findOne(1); - - Assert::assertEquals(ExampleInvoice::TOKEN, $invoice->token); - Assert::assertEquals('someBitpayId', $invoice->bitpay_id); - Assert::assertEquals('https://test.bitpay.com/invoice?id=MV9fy5iNDkqrg4qrfYpw75', $invoice->bitpay_url); - // phpcs:disable Generic.Files.LineLength.TooLong - Assert::assertEquals("{\"store\":\"store-1\",\"register\":\"2\",\"reg_transaction_no\":\"87678\",\"price\":\"76.70\"}", $invoice->pos_data_json); - Assert::assertEquals('expired', $invoice->status); - Assert::assertEquals(76.7, $invoice->price); - Assert::assertEquals('USD', $invoice->currency_code); - Assert::assertEquals('false', $invoice->exception_status); - - $eth = $invoice->getInvoicePayment()->paymentCurrencies()->where('currency_code', 'ETH')->first(); - $btc = $invoice->getInvoicePayment()->paymentCurrencies()->where('currency_code', 'BTC')->first(); - Assert::assertEquals(48312000000000000, $eth->total); - Assert::assertEquals(48312000000000000, $eth->subtotal); - Assert::assertEquals(347100, $btc->total); - Assert::assertEquals(342800, $btc->subtotal); - Assert::assertEquals(0, $invoice->getInvoicePayment()->amount_paid); - Assert::assertEquals('someBitpayOrderId', $invoice->bitpay_order_id); - } - - private function getTestedClass(): UpdateInvoiceUsingBitPayIpn - { - return $this->app->make(UpdateInvoiceUsingBitPayIpn::class); - } -} diff --git a/tests/Unit/Features/Invoice/UpdateInvoice/ValidateBitPayWebhookTest.php b/tests/Unit/Features/Invoice/UpdateInvoice/ValidateBitPayWebhookTest.php new file mode 100644 index 0000000..36ef579 --- /dev/null +++ b/tests/Unit/Features/Invoice/UpdateInvoice/ValidateBitPayWebhookTest.php @@ -0,0 +1,78 @@ +createMock(BitPayConfigurationInterface::class); + $bitpayConfig->expects($this->once()) + ->method('getToken') + ->willReturn(null); + + $validator = new BitPaySignatureValidator($bitpayConfig); + $this->expectException(\RuntimeException::class); + $validator->execute([], [ + 'x-signature' => 'test-signature', + ]); + } + + /** + * @test + */ + public function it_should_return_error_when_signature_header_is_missing(): void + { + $bitpayConfig = $this->createMock(BitPayConfigurationInterface::class); + $validator = new BitPaySignatureValidator($bitpayConfig); + $this->expectException(\RuntimeException::class); + + $validator->execute([], []); + } + + /** + * @test + */ + public function it_should_return_error_when_signature_does_not_match(): void + { + $bitpayConfig = $this->createMock(BitPayConfigurationInterface::class); + $bitpayConfig->expects($this->once()) + ->method('getToken') + ->willReturn('test-token'); + + $headers = ['x-signature' => 'invalid-signature']; + + $validator = new BitPaySignatureValidator($bitpayConfig); + $this->expectException(\RuntimeException::class); + $validator->execute([], $headers); + } + + /** + * @test + */ + public function it_should_allow_request_when_signature_is_valid(): void + { + $token = 'test-token'; + $testContent = ['test-content']; + + $bitpayConfig = $this->createMock(BitPayConfigurationInterface::class); + $bitpayConfig->expects($this->once()) + ->method('getToken') + ->willReturn($token); + + $expectedSignature = base64_encode(hash_hmac('sha256', json_encode($testContent), $token, true)); + + $validator = new BitPaySignatureValidator($bitpayConfig); + + $validator->execute($testContent, ['x-signature' => [$expectedSignature]]); + } +} From 76a71e33cde34c88ab2efac2f66ab9225e7bbd12 Mon Sep 17 00:00:00 2001 From: Maciej Kwiatkowski Date: Thu, 28 Nov 2024 11:54:42 +0100 Subject: [PATCH 2/3] SP-1024 Adding stricter assertions to unit tests --- .../UpdateInvoice/ValidateBitPayWebhookTest.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/Unit/Features/Invoice/UpdateInvoice/ValidateBitPayWebhookTest.php b/tests/Unit/Features/Invoice/UpdateInvoice/ValidateBitPayWebhookTest.php index 36ef579..a1c5159 100644 --- a/tests/Unit/Features/Invoice/UpdateInvoice/ValidateBitPayWebhookTest.php +++ b/tests/Unit/Features/Invoice/UpdateInvoice/ValidateBitPayWebhookTest.php @@ -6,6 +6,7 @@ use App\Features\Shared\Configuration\BitPayConfigurationInterface; use App\Features\Invoice\UpdateInvoice\BitPaySignatureValidator; +use App\Shared\Exceptions\SignatureVerificationFailed; use Tests\Unit\AbstractUnitTestCase; class ValidateBitPayWebhookTest extends AbstractUnitTestCase @@ -22,9 +23,7 @@ public function it_should_return_error_when_signing_key_is_missing(): void $validator = new BitPaySignatureValidator($bitpayConfig); $this->expectException(\RuntimeException::class); - $validator->execute([], [ - 'x-signature' => 'test-signature', - ]); + $validator->execute([], []); } /** @@ -33,8 +32,12 @@ public function it_should_return_error_when_signing_key_is_missing(): void public function it_should_return_error_when_signature_header_is_missing(): void { $bitpayConfig = $this->createMock(BitPayConfigurationInterface::class); + $bitpayConfig->expects($this->once()) + ->method('getToken') + ->willReturn('test-token'); + $validator = new BitPaySignatureValidator($bitpayConfig); - $this->expectException(\RuntimeException::class); + $this->expectException(SignatureVerificationFailed::class); $validator->execute([], []); } @@ -49,10 +52,10 @@ public function it_should_return_error_when_signature_does_not_match(): void ->method('getToken') ->willReturn('test-token'); - $headers = ['x-signature' => 'invalid-signature']; + $headers = ['x-signature' => ['invalid-signature']]; $validator = new BitPaySignatureValidator($bitpayConfig); - $this->expectException(\RuntimeException::class); + $this->expectException(SignatureVerificationFailed::class); $validator->execute([], $headers); } From 3c644e3cae002459d4821636df4474d9e6b86981 Mon Sep 17 00:00:00 2001 From: Maciej Kwiatkowski Date: Thu, 28 Nov 2024 12:04:52 +0100 Subject: [PATCH 3/3] SP-1024 Adding signature verification fail log --- .../Invoice/UpdateInvoice/UpdateInvoiceUsingBitPayIpn.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/Features/Invoice/UpdateInvoice/UpdateInvoiceUsingBitPayIpn.php b/app/Features/Invoice/UpdateInvoice/UpdateInvoiceUsingBitPayIpn.php index 52e46c1..196cdc7 100644 --- a/app/Features/Invoice/UpdateInvoice/UpdateInvoiceUsingBitPayIpn.php +++ b/app/Features/Invoice/UpdateInvoice/UpdateInvoiceUsingBitPayIpn.php @@ -69,6 +69,9 @@ public function execute(string $uuid, array $data, array $headers): void $this->sendUpdateInvoiceNotification->execute($invoice, $data['event'] ?? null); } catch (SignatureVerificationFailed $e) { + $this->logger->error('SIGNATURE_VERIFICATION_FAIL', $e->getMessage(), [ + 'id' => $invoice->id + ]); throw $e; } catch (\Exception | \TypeError $e) { $this->logger->error('INVOICE_UPDATE_FAIL', 'Failed to update invoice', [