From 1421e86e3d71436e8ac4facb8ef89086b8e506c1 Mon Sep 17 00:00:00 2001 From: Szymon Wlodarski Date: Wed, 27 Nov 2024 13:33:58 +0100 Subject: [PATCH] SP-966 - Validate incoming webhooks --- Exception/HMACVerificationException.php | 9 ++ Model/BPRedirect.php | 3 +- Model/BitpayInvoiceRepository.php | 12 +- Model/Ipn/WebhookVerifier.php | 32 +++++ Model/IpnManagement.php | 57 +++++++-- Model/ResourceModel/BitpayInvoice.php | 13 +- Test/Integration/Model/BPRedirectTest.php | 13 +- Test/Integration/Model/IpnManagementTest.php | 58 +++++++-- Test/Unit/Model/BPRedirectTest.php | 36 ++++-- Test/Unit/Model/Ipn/WebhookVerifierTest.php | 43 +++++++ Test/Unit/Model/IpnManagementTest.php | 127 ++++++++++++++++--- etc/db_schema.xml | 1 + etc/db_schema_whitelist.json | 3 +- 13 files changed, 345 insertions(+), 62 deletions(-) create mode 100644 Exception/HMACVerificationException.php create mode 100644 Model/Ipn/WebhookVerifier.php create mode 100644 Test/Unit/Model/Ipn/WebhookVerifierTest.php diff --git a/Exception/HMACVerificationException.php b/Exception/HMACVerificationException.php new file mode 100644 index 0000000..87ccf0a --- /dev/null +++ b/Exception/HMACVerificationException.php @@ -0,0 +1,9 @@ +getId(), $invoiceID, $invoice->getExpirationTime(), - $invoice->getAcceptanceWindow() + $invoice->getAcceptanceWindow(), + $this->encryptor->encrypt($this->config->getToken()) ); $this->transactionRepository->add($incrementId, $invoiceID, 'new'); diff --git a/Model/BitpayInvoiceRepository.php b/Model/BitpayInvoiceRepository.php index bae178e..8afe623 100755 --- a/Model/BitpayInvoiceRepository.php +++ b/Model/BitpayInvoiceRepository.php @@ -21,11 +21,17 @@ public function __construct(BitpayInvoice $bitpayInvoice) * @param string $invoiceID * @param int $expirationTime * @param int|null $acceptanceWindow + * @param string|null $bitpayToken * @return void */ - public function add(string $orderId, string $invoiceID, int $expirationTime, ?int $acceptanceWindow): void - { - $this->bitpayInvoice->add($orderId, $invoiceID, $expirationTime, $acceptanceWindow); + public function add( + string $orderId, + string $invoiceID, + int $expirationTime, + ?int $acceptanceWindow, + ?string $bitpayToken + ): void { + $this->bitpayInvoice->add($orderId, $invoiceID, $expirationTime, $acceptanceWindow, $bitpayToken); } /** diff --git a/Model/Ipn/WebhookVerifier.php b/Model/Ipn/WebhookVerifier.php new file mode 100644 index 0000000..676a985 --- /dev/null +++ b/Model/Ipn/WebhookVerifier.php @@ -0,0 +1,32 @@ +coreRegistry = $registry; $this->responseFactory = $responseFactory; $this->url = $url; $this->quoteFactory = $quoteFactory; - $this->orderInterface = $orderInterface; + $this->orderFactory = $orderFactory; $this->checkoutSession = $checkoutSession; $this->logger = $logger; $this->config = $config; @@ -89,6 +113,9 @@ public function __construct( $this->request = $request; $this->client = $client; $this->response = $response; + $this->bitpayInvoiceRepository = $bitpayInvoiceRepository; + $this->encryptor = $encryptor; + $this->webhookVerifier = $webhookVerifier; } /** @@ -103,7 +130,7 @@ public function postClose() $response = $this->responseFactory->create(); try { $orderID = $this->request->getParam('orderID', null); - $order = $this->orderInterface->loadByIncrementId($orderID); + $order = $this->orderFactory->create()->loadByIncrementId($orderID); $orderData = $order->getData(); $quoteID = $orderData['quote_id']; $quote = $this->quoteFactory->create()->loadByIdWithoutStore($quoteID); @@ -134,10 +161,22 @@ public function postClose() public function postIpn() { try { - $allData = $this->serializer->unserialize($this->request->getContent()); + $requestBody = $this->request->getContent(); + $allData = $this->serializer->unserialize($requestBody); $data = $allData['data']; $event = $allData['event']; $orderId = $data['orderId']; + + $bitPayInvoiceData = $this->bitpayInvoiceRepository->getByOrderId($orderId); + if (!empty($bitPayInvoiceData['bitpay_token'])) { + $signingKey = $this->encryptor->decrypt($bitPayInvoiceData['bitpay_token']); + $xSignature = $this->request->getHeader('x-signature'); + + if (!$this->webhookVerifier->isValidHmac($signingKey, $xSignature, $requestBody)) { + throw new HMACVerificationException('HMAC Verification Failed!'); + } + } + $orderInvoiceId = $data['id']; $row = $this->transactionRepository->findBy($orderId, $orderInvoiceId); $client = $this->client->initialize(); @@ -162,7 +201,7 @@ public function postIpn() $invoiceStatus = $this->invoice->getBPCCheckInvoiceStatus($client, $orderInvoiceId); $updateWhere = ['order_id = ?' => $orderId, 'transaction_id = ?' => $orderInvoiceId]; $this->transactionRepository->update('transaction_status', $invoiceStatus, $updateWhere); - $order = $this->orderInterface->loadByIncrementId($orderId); + $order = $this->orderFactory->create()->loadByIncrementId($orderId); switch ($event['name']) { case Invoice::COMPLETED: if ($invoiceStatus == 'complete') { diff --git a/Model/ResourceModel/BitpayInvoice.php b/Model/ResourceModel/BitpayInvoice.php index 536401a..40e8f7c 100755 --- a/Model/ResourceModel/BitpayInvoice.php +++ b/Model/ResourceModel/BitpayInvoice.php @@ -26,10 +26,16 @@ public function _construct() * @param string $invoiceID * @param int $expirationTime * @param int|null $acceptanceWindow + * @param string|null $bitpayToken * @return void */ - public function add(string $orderId, string $invoiceID, int $expirationTime, ?int $acceptanceWindow) - { + public function add( + string $orderId, + string $invoiceID, + int $expirationTime, + ?int $acceptanceWindow, + ?string $bitpayToken + ) { $connection = $this->getConnection(); $table_name = $connection->getTableName(self::TABLE_NAME); $connection->insert( @@ -38,7 +44,8 @@ public function add(string $orderId, string $invoiceID, int $expirationTime, ?in 'order_id' => $orderId, 'invoice_id' => $invoiceID, 'expiration_time' => $expirationTime, - 'acceptance_window'=> $acceptanceWindow + 'acceptance_window'=> $acceptanceWindow, + 'bitpay_token' => $bitpayToken ] ); } diff --git a/Test/Integration/Model/BPRedirectTest.php b/Test/Integration/Model/BPRedirectTest.php index e145100..8ae5fa1 100755 --- a/Test/Integration/Model/BPRedirectTest.php +++ b/Test/Integration/Model/BPRedirectTest.php @@ -23,6 +23,7 @@ use Magento\Sales\Model\OrderRepository; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; use Magento\Framework\Encryption\EncryptorInterface; /** @@ -91,7 +92,7 @@ class BPRedirectTest extends TestCase */ private $resultFactory; /** - * @var Client $client + * @var Client|MockObject $client */ private $client; @@ -117,16 +118,25 @@ public function setUp(): void $this->orderInterface = $this->objectManager->get(OrderInterface::class); $this->config = $this->objectManager->get(Config::class); $this->transactionRepository = $this->objectManager->get(TransactionRepository::class); + /** + * @var Invoice|MockObject + */ $this->invoice = $this->getMockBuilder(Invoice::class)->disableOriginalConstructor()->getMock(); $this->messageManager = $this->objectManager->get(Manager::class); $this->registry = $this->objectManager->get(Registry::class); $this->url = $this->objectManager->get(UrlInterface::class); $this->logger = $this->objectManager->get(Logger::class); $this->resultFactory = $this->objectManager->get(ResultFactory::class); + /** + * @var Client|MockObject + */ $this->client = $this->getMockBuilder(Client::class)->disableOriginalConstructor()->getMock(); $this->orderRepository = $this->objectManager->get(OrderRepository::class); $this->bitpayInvoiceRepository = $this->objectManager->get(BitpayInvoiceRepository::class); $this->bitpayInvoiceRepository = $this->objectManager->get(BitpayInvoiceRepository::class); + /** + * @var EncryptorInterface|MockObject + */ $this->encryptor = $this->getMockBuilder(EncryptorInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -196,7 +206,6 @@ public function testExecute(): void $this->assertEquals('100000001', $result[0]['order_id']); $this->assertEquals('new', $result[0]['transaction_status']); $this->assertEquals('test', $this->config->getBitpayEnv()); - $this->assertEquals('redirect', $this->config->getBitpayUx()); $this->assertEquals($bitpayMethodCode, $methodCode); } diff --git a/Test/Integration/Model/IpnManagementTest.php b/Test/Integration/Model/IpnManagementTest.php index 579ac8b..2ecc448 100644 --- a/Test/Integration/Model/IpnManagementTest.php +++ b/Test/Integration/Model/IpnManagementTest.php @@ -9,21 +9,24 @@ use Bitpay\BPCheckout\Model\TransactionRepository; use BitPaySDK\Model\Invoice\Buyer; use Magento\Framework\ObjectManagerInterface; -use Bitpay\BPCheckout\Api\IpnManagementInterface; use Bitpay\BPCheckout\Logger\Logger; -use Bitpay\BPCheckout\Model\Ipn\BPCItem; +use Bitpay\BPCheckout\Model\BitpayInvoiceRepository; +use Bitpay\BPCheckout\Model\Ipn\WebhookVerifier; use Magento\Checkout\Model\Session; use Magento\Framework\App\ResponseFactory; use Magento\Framework\DataObject; +use Magento\Framework\Encryption\EncryptorInterface; use Magento\Framework\Registry; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\UrlInterface; use Magento\Framework\Webapi\Rest\Request; use Magento\Framework\Webapi\Rest\Response; use Magento\Quote\Model\QuoteFactory; +use Magento\Sales\Model\OrderFactory; use Magento\Sales\Api\Data\OrderInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; /** * @SuppressWarnings(PHPMD.TooManyFields) @@ -42,7 +45,7 @@ class IpnManagementTest extends TestCase private $responseFactory; /** - * @var OrderInterface $url + * @var UrlInterface $url */ private $url; @@ -57,9 +60,9 @@ class IpnManagementTest extends TestCase private $quoteFactory; /** - * @var OrderInterface $orderInterface + * @var OrderFactory|MockObject $orderFactory */ - private $orderInterface; + private $orderFactory; /** * @var Registry $coreRegistry @@ -87,7 +90,7 @@ class IpnManagementTest extends TestCase private $transactionRepository; /** - * @var Invoice|\PHPUnit\Framework\MockObject\MockObject $invoice + * @var Invoice|MockObject $invoice */ private $invoice; @@ -102,7 +105,7 @@ class IpnManagementTest extends TestCase private $objectManager; /** - * @var Client $client + * @var Client|MockObject $client */ private $client; @@ -111,6 +114,21 @@ class IpnManagementTest extends TestCase */ private $response; + /** + * @var BitpayInvoiceRepository|MockObject $bitpayInvoiceRepository + */ + private $bitpayInvoiceRepository; + + /** + * @var EncryptorInterface|MockObject $encryptor + */ + private $encryptor; + + /** + * @var WebhookVerifier|MockObject $webhookVerifier + */ + protected $webhookVerifier; + public function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); @@ -118,22 +136,31 @@ public function setUp(): void $this->responseFactory = $this->objectManager->get(ResponseFactory::class); $this->url = $this->objectManager->get(UrlInterface::class); $this->quoteFactory = $this->objectManager->get(QuoteFactory::class); - $this->orderInterface = $this->objectManager->get(OrderInterface::class); + $this->orderFactory = $this->objectManager->get(OrderFactory::class); $this->checkoutSession = $this->objectManager->get(Session::class); $this->logger = $this->objectManager->get(Logger::class); $this->config = $this->objectManager->get(Config::class); $this->serializer = $this->objectManager->get(Json::class); $this->transactionRepository = $this->objectManager->get(TransactionRepository::class); + /** + * @var Invoice|MockObject + */ $this->invoice = $this->getMockBuilder(Invoice::class)->disableOriginalConstructor()->getMock(); $this->request = $this->objectManager->get(Request::class); + /** + * @var Client|MockObject + */ $this->client = $this->getMockBuilder(Client::class)->disableOriginalConstructor()->getMock(); $this->response = $this->objectManager->get(Response::class); + $this->bitpayInvoiceRepository = $this->objectManager->get(BitpayInvoiceRepository::class); + $this->encryptor =$this->objectManager->get(EncryptorInterface::class); + $this->webhookVerifier = $this->objectManager->get(WebhookVerifier::class); $this->ipnManagement = new IpnManagement( $this->responseFactory, $this->url, $this->coreRegistry, $this->checkoutSession, - $this->orderInterface, + $this->orderFactory, $this->quoteFactory, $this->logger, $this->config, @@ -142,7 +169,10 @@ public function setUp(): void $this->invoice, $this->request, $this->client, - $this->response + $this->response, + $this->bitpayInvoiceRepository, + $this->encryptor, + $this->webhookVerifier, ); } @@ -151,14 +181,14 @@ public function setUp(): void */ public function testPostClose() { - $order = $this->orderInterface->loadByIncrementId('100000001'); - $this->request->setParam('orderID', $order->getEntityId()); + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $this->request->setParam('orderID', $order->getIncrementId()); $quoteId = $order->getQuoteId(); /** @var \Magento\Quote\Model\Quote $quote */ $this->quoteFactory->create()->loadByIdWithoutStore($quoteId); $this->ipnManagement->postClose(); - $this->orderInterface->loadByIncrementId('100000001'); + $this->orderFactory->create()->loadByIncrementId('100000001'); $this->assertEquals($quoteId, $this->checkoutSession->getQuoteId()); } @@ -198,7 +228,7 @@ public function testPostIpn() $this->ipnManagement->postIpn(); - $order = $this->orderInterface->loadByIncrementId($orderId); + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); $result = $this->transactionRepository->findBy($orderId, $orderInvoiceId); $this->assertEquals('complete', $result[0]['transaction_status']); diff --git a/Test/Unit/Model/BPRedirectTest.php b/Test/Unit/Model/BPRedirectTest.php index 964c3ff..2030f3d 100755 --- a/Test/Unit/Model/BPRedirectTest.php +++ b/Test/Unit/Model/BPRedirectTest.php @@ -18,8 +18,6 @@ use Magento\Framework\Message\Manager; use Magento\Framework\Registry; use Magento\Framework\UrlInterface; -use Magento\Quote\Api\Data\PaymentInterface; -use Magento\Sales\Api\Data\OrderInterface; use \Magento\Framework\Controller\Result\Redirect; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Encryption\EncryptorInterface; @@ -41,7 +39,7 @@ class BPRedirectTest extends TestCase private $bpRedirect; /** - * @var Seesion|MockObject $checkoutSession + * @var Session|MockObject $checkoutSession */ private $checkoutSession; @@ -66,12 +64,12 @@ class BPRedirectTest extends TestCase private $client; /** - * @var OrderRepository $orderRepository + * @var OrderRepository|MockObject $orderRepository */ private $orderRepository; /** - * @var BitpayInvoiceRepository $bitpayInvoiceRepository + * @var BitpayInvoiceRepository|MockObject $bitpayInvoiceRepository */ private $bitpayInvoiceRepository; @@ -167,14 +165,17 @@ public function testExecute(): void 'getUrl' ) ->withConsecutive(['bitpay-invoice', ['_query' => ['order_id' => $incrementId]]], ['checkout/cart']) - ->willReturnOnConsecutiveCalls('http://localhost/bitpay-invoice?order_id=' . $incrementId, 'http://localhost/checkout/cart'); + ->willReturnOnConsecutiveCalls( + 'http://localhost/bitpay-invoice?order_id=' . $incrementId, + 'http://localhost/checkout/cart' + ); $billingAddress->expects($this->once())->method('getData') ->willReturn(['first_name' => 'test', 'last_name' => 'test1']); $billingAddress->expects($this->once())->method('getFirstName')->willReturn('test'); $billingAddress->expects($this->once())->method('getLastName')->willReturn('test1'); $order = $this->getOrder($incrementId, $payment, $billingAddress, $lastOrderId); - $this->prepareConfig($baseUrl, 'redirect'); + $this->prepareConfig($baseUrl); $method->expects($this->once())->method('getCode')->willReturn(Config::BITPAY_PAYMENT_METHOD_NAME); $payment->expects($this->once())->method('getMethodInstance')->willReturn($method); $this->order->expects($this->once())->method('load')->with($lastOrderId)->willReturn($order); @@ -194,6 +195,9 @@ public function testExecute(): void $result->expects($this->once())->method('setUrl')->willReturnSelf(); $this->resultFactory->expects($this->once())->method('create')->willReturn($result); + /** + * @var \Magento\Framework\Controller\ResultInterface|MockObject + */ $page = $this->getMock(\Magento\Framework\View\Result\Page::class); $this->bpRedirect->execute($page); @@ -210,6 +214,9 @@ public function testExecuteNoOrderId(): void $result->expects($this->once())->method('setUrl')->willReturnSelf(); $this->resultFactory->expects($this->once())->method('create')->willReturn($result); + /** + * @var \Magento\Framework\Controller\ResultInterface|MockObject + */ $page = $this->getMock(\Magento\Framework\View\Result\Page::class); $this->bpRedirect->execute($page); @@ -235,6 +242,9 @@ public function testExecuteNoBitpayPaymentMethod(): void $order->expects($this->once())->method('getPayment')->willReturn($payment); $this->order->expects($this->once())->method('load')->with($lastOrderId)->willReturn($order); + /** + * @var \Magento\Framework\Controller\ResultInterface|MockObject + */ $page = $this->getMock(\Magento\Framework\View\Result\Page::class); $this->assertSame($page, $this->bpRedirect->execute($page)); @@ -266,14 +276,17 @@ public function testExecuteException($exceptionType): void 'getUrl' ) ->withConsecutive(['bitpay-invoice', ['_query' => ['order_id' => $incrementId]]], ['checkout/cart']) - ->willReturnOnConsecutiveCalls('http://localhost/bitpay-invoice?order_id=' . $incrementId, 'http://localhost/checkout/cart'); + ->willReturnOnConsecutiveCalls( + 'http://localhost/bitpay-invoice?order_id=' . $incrementId, + 'http://localhost/checkout/cart' + ); $billingAddress->expects($this->once())->method('getData') ->willReturn(['first_name' => 'test', 'last_name' => 'test1']); $billingAddress->expects($this->once())->method('getFirstName')->willReturn('test'); $billingAddress->expects($this->once())->method('getLastName')->willReturn('test1'); $order = $this->getOrder($incrementId, $payment, $billingAddress, null); - $this->prepareConfig($baseUrl, 'redirect'); + $this->prepareConfig($baseUrl); $method->expects($this->once())->method('getCode')->willReturn(Config::BITPAY_PAYMENT_METHOD_NAME); $payment->expects($this->once())->method('getMethodInstance')->willReturn($method); $this->order->expects($this->once())->method('load')->with($lastOrderId)->willReturn($order); @@ -286,6 +299,9 @@ public function testExecuteException($exceptionType): void ->method('BPCCreateInvoice') ->willThrowException(new $exceptionType('something went wrong')); + /** + * @var \Magento\Framework\Controller\ResultInterface|MockObject + */ $page = $this->getMock(\Magento\Framework\View\Result\Page::class); $this->bpRedirect->execute($page); @@ -322,7 +338,7 @@ private function getOrder(string $incrementId, MockObject $payment, MockObject $ return $order; } - private function prepareConfig(string $baseUrl, string $ux): void + private function prepareConfig(string $baseUrl): void { $this->config->expects($this->once())->method('getBPCheckoutOrderStatus')->willReturn('pending'); $this->config->expects($this->once())->method('getBaseUrl')->willReturn($baseUrl); diff --git a/Test/Unit/Model/Ipn/WebhookVerifierTest.php b/Test/Unit/Model/Ipn/WebhookVerifierTest.php new file mode 100644 index 0000000..6b3a201 --- /dev/null +++ b/Test/Unit/Model/Ipn/WebhookVerifierTest.php @@ -0,0 +1,43 @@ +webhookVerifier = new WebhookVerifier(); + } + + public function testIsValidHmac(): void + { + $this->assertTrue( + $this->webhookVerifier->isValidHmac( + 'testkey', + 'SKEpFPexQ4ko9QAEre51+n+ypvQQidUheDl3+4irEOQ=', + '{"data":{"test":true}' + ) + ); + } + + public function testIsValidHmacFalse(): void + { + $this->assertFalse( + $this->webhookVerifier->isValidHmac( + 'differentkey', + 'SKEpFPexQ4ko9QAEre51+n+ypvQQidUheDl3+4irEOQ=', + '{"data":{"test":true}' + ) + ); + } +} diff --git a/Test/Unit/Model/IpnManagementTest.php b/Test/Unit/Model/IpnManagementTest.php index 16e822f..aa38a69 100644 --- a/Test/Unit/Model/IpnManagementTest.php +++ b/Test/Unit/Model/IpnManagementTest.php @@ -8,23 +8,21 @@ use Bitpay\BPCheckout\Model\Config; use Bitpay\BPCheckout\Model\Invoice; use Bitpay\BPCheckout\Model\IpnManagement; -use Bitpay\BPCheckout\Api\IpnManagementInterface; +use Bitpay\BPCheckout\Model\Ipn\WebhookVerifier; use Bitpay\BPCheckout\Logger\Logger; -use Bitpay\BPCheckout\Model\Ipn\BPCItem; +use Bitpay\BPCheckout\Model\BitpayInvoiceRepository; use Bitpay\BPCheckout\Model\TransactionRepository; use BitPaySDK\Model\Invoice\Buyer; -use Hoa\Iterator\Mock; use Magento\Checkout\Model\Session; use Magento\Framework\App\ResponseFactory; -use Magento\Framework\App\Response; -use Magento\Framework\DataObject; +use Magento\Framework\Encryption\EncryptorInterface; use Magento\Framework\Registry; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\UrlInterface; use Magento\Framework\Webapi\Rest\Request; use Magento\Quote\Model\Quote; use Magento\Quote\Model\QuoteFactory; -use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\OrderFactory; use Magento\Sales\Model\Order; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -55,9 +53,9 @@ class IpnManagementTest extends TestCase private $quoteFactory; /** - * @var OrderInterface|MockObject + * @var OrderFactory|MockObject */ - private $orderInterface; + private $orderFactory; /** * @var Registry|MockObject @@ -100,22 +98,37 @@ class IpnManagementTest extends TestCase private $ipnManagement; /** - * @var Client $client + * @var Client|mockObject $client */ private $client; /** - * @var \Magento\Framework\Webapi\Rest\Response $response + * @var \Magento\Framework\Webapi\Rest\Response|MockObject $response */ private $response; + /** + * @var BitpayInvoiceRepository|MockObject $bitpayInvoiceRepository + */ + private $bitpayInvoiceRepository; + + /** + * @var EncryptorInterface|MockObject $encryptor + */ + private $encryptor; + + /** + * @var WebhookVerifier|MockObject $webhookVerifier + */ + protected $webhookVerifier; + public function setUp(): void { $this->coreRegistry = $this->getMock(Registry::class); $this->responseFactory = $this->getMock(ResponseFactory::class); $this->url = $this->getMock(UrlInterface::class); $this->quoteFactory = $this->getMock(QuoteFactory::class); - $this->orderInterface = $this->getMock(\Magento\Sales\Model\Order::class); + $this->orderFactory = $this->getMock(\Magento\Sales\Model\OrderFactory::class); $this->checkoutSession = $this->getMock(Session::class); $this->logger = $this->getMock(Logger::class); $this->config = $this->getMock(Config::class); @@ -125,6 +138,9 @@ public function setUp(): void $this->request = $this->getMock(Request::class); $this->client = $this->getMock(Client::class); $this->response = $this->getMock(\Magento\Framework\Webapi\Rest\Response::class); + $this->bitpayInvoiceRepository = $this->getMock(BitpayInvoiceRepository::class); + $this->encryptor = $this->getMock(EncryptorInterface::class); + $this->webhookVerifier = $this->getMock(WebhookVerifier::class); $this->ipnManagement = $this->getClass(); } @@ -137,11 +153,17 @@ public function testPostClose(): void $order = $this->getMock(Order::class); $orderId = '000000012'; $this->url->expects($this->once())->method('getUrl')->willReturn($cartUrl); - + $order->expects($this->once()) + ->method('loadByIncrementId') + ->with($orderId) + ->willReturnSelf(); $this->request->expects($this->once())->method('getParam')->willReturn($orderId); $this->responseFactory->expects($this->once())->method('create')->willReturn($response); $order->expects($this->once())->method('getData')->willReturn(['quote_id' => $quoteId]); - $this->orderInterface->expects($this->once())->method('loadByIncrementId')->willReturn($order); + + $this->orderFactory->expects($this->once()) + ->method('create') + ->willReturn($order); $quote->expects($this->once())->method('loadByIdWithoutStore')->willReturnSelf(); $quote->expects($this->once())->method('getId')->willReturn($quoteId); @@ -165,11 +187,16 @@ public function testPostCloseQuoteNotFound(): void $this->url->expects($this->once()) ->method('getUrl') ->willReturn('http://localhost/checkout/cart?reload=1'); - + $order->expects($this->once()) + ->method('loadByIncrementId') + ->with($orderId) + ->willReturnSelf(); $this->responseFactory->expects($this->once())->method('create')->willReturn($response); $this->request->expects($this->once())->method('getParam')->willReturn($orderId); $order->expects($this->once())->method('getData')->willReturn(['quote_id' => $quoteId]); - $this->orderInterface->expects($this->once())->method('loadByIncrementId')->willReturn($order); + $this->orderFactory->expects($this->once()) + ->method('create') + ->willReturn($order); $quote->expects($this->once())->method('loadByIdWithoutStore')->willReturnSelf(); $quote->expects($this->once())->method('getId')->willReturn(null); $this->quoteFactory->expects($this->once())->method('create')->willReturn($quote); @@ -184,13 +211,19 @@ public function testPostCloseExeception(): void $orderId = '000000012'; $response = $this->getMock(\Magento\Framework\HTTP\PhpEnvironment\Response::class); $order = $this->getMock(Order::class); + $order->expects($this->once()) + ->method('loadByIncrementId') + ->with($orderId) + ->willReturnSelf(); $this->url->expects($this->once()) ->method('getUrl') ->willReturn('http://localhost/checkout/cart?reload=1'); $this->responseFactory->expects($this->once())->method('create')->willReturn($response); $this->request->expects($this->once())->method('getParam')->willReturn($orderId); $order->expects($this->once())->method('getData')->willReturn([]); - $this->orderInterface->expects($this->once())->method('loadByIncrementId')->willReturn($order); + $this->orderFactory->expects($this->once()) + ->method('create') + ->willReturn($order); $response->expects($this->once())->method('setRedirect')->willReturnSelf(); @@ -312,6 +345,53 @@ public function testPostIpnCompleteInvalid(): void $this->ipnManagement->postIpn(); } + public function testPostIpnHmacVerificationSuccess(): void + { + $this->bitpayInvoiceRepository->expects($this->once())->method('getByOrderId')->willReturn([ + 'order_id' => 12, + 'invoice_id' => '12', + 'expiration_time' => 1726740384932, + 'acceptance_window'=> '', + 'bitpay_token' => '0:3:testtokenencoded' + ]); + $this->encryptor->expects($this->once())->method('decrypt')->willReturn('testtoken'); + $this->request->expects($this->once())->method('getHeader')->with('x-signature')->willReturn('test'); + $this->webhookVerifier->expects($this->once())->method('isValidHmac')->willReturn(true); + $this->response->expects($this->never())->method('addMessage'); + + $this->preparePostIpn('invoice_completed', 'test'); + + $this->ipnManagement->postIpn(); + } + + public function testPostIpnHmacVerificationFailure(): void + { + $orderInvoiceId = '12'; + $data = $this->prepareData($orderInvoiceId, 'invoice_completed'); + $serializer = new Json(); + $serializerData = $serializer->serialize($data); + $this->serializer->expects($this->once())->method('unserialize')->willReturn($data); + $this->request->expects($this->once())->method('getContent')->willReturn($serializerData); + + $this->bitpayInvoiceRepository->expects($this->once())->method('getByOrderId')->willReturn([ + 'order_id' => 12, + 'invoice_id' => '12', + 'expiration_time' => 1726740384932, + 'acceptance_window'=> '', + 'bitpay_token' => '0:3:testtokenencoded' + ]); + $this->encryptor->expects($this->once())->method('decrypt')->willReturn('testtoken'); + $this->request->expects($this->once())->method('getHeader')->with('x-signature')->willReturn('test'); + $this->webhookVerifier->expects($this->once())->method('isValidHmac')->willReturn(false); + + $this->response->expects($this->once()) + ->method('addMessage') + ->with('HMAC Verification Failed!', 500) + ->willReturnSelf(); + + $this->ipnManagement->postIpn(); + } + private function preparePostIpn(string $eventName, string $invoiceStatus): void { $orderInvoiceId = '12'; @@ -336,7 +416,13 @@ private function preparePostIpn(string $eventName, string $invoiceStatus): void $this->config->expects($this->once())->method('getToken')->willReturn('test'); $this->invoice->expects($this->once())->method('getBPCCheckInvoiceStatus')->willReturn($invoiceStatus); $order = $this->getMock(Order::class); - $this->orderInterface->expects($this->once())->method('loadByIncrementId')->willReturn($order); + $order->expects($this->once()) + ->method('loadByIncrementId') + ->with($data['data']['orderId']) + ->willReturnSelf(); + $this->orderFactory->expects($this->once()) + ->method('create') + ->willReturn($order); } private function getMock(string $className): MockObject @@ -351,7 +437,7 @@ private function getClass(): IpnManagement $this->url, $this->coreRegistry, $this->checkoutSession, - $this->orderInterface, + $this->orderFactory, $this->quoteFactory, $this->logger, $this->config, @@ -360,7 +446,10 @@ private function getClass(): IpnManagement $this->invoice, $this->request, $this->client, - $this->response + $this->response, + $this->bitpayInvoiceRepository, + $this->encryptor, + $this->webhookVerifier ); } diff --git a/etc/db_schema.xml b/etc/db_schema.xml index 9ae2172..d2f635f 100644 --- a/etc/db_schema.xml +++ b/etc/db_schema.xml @@ -25,6 +25,7 @@ comment="Expiration time to pay invoice"/> +