Skip to content

Commit

Permalink
Merge pull request #111 from swlodarski-sumoheavy/10.0.x
Browse files Browse the repository at this point in the history
SP-966 - Validate incoming webhooks
p-maguire authored Dec 19, 2024
2 parents 6e518c4 + f9e17bd commit ba52121
Showing 14 changed files with 363 additions and 62 deletions.
9 changes: 9 additions & 0 deletions Exception/HMACVerificationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);

namespace Bitpay\BPCheckout\Exception;

class HMACVerificationException extends \Exception
{

}
9 changes: 7 additions & 2 deletions Model/BPRedirect.php
Original file line number Diff line number Diff line change
@@ -37,6 +37,7 @@ class BPRedirect
protected OrderRepository $orderRepository;
protected BitpayInvoiceRepository $bitpayInvoiceRepository;
protected ReturnHash $returnHashHelper;
protected EncryptorInterface $encryptor;

/**
* @param Session $checkoutSession
@@ -53,6 +54,7 @@ class BPRedirect
* @param OrderRepository $orderRepository
* @param BitpayInvoiceRepository $bitpayInvoiceRepository
* @param ReturnHash $returnHashHelper
* @param EncryptorInterface $encryptor
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -69,7 +71,8 @@ public function __construct(
Client $client,
OrderRepository $orderRepository,
BitpayInvoiceRepository $bitpayInvoiceRepository,
ReturnHash $returnHashHelper
ReturnHash $returnHashHelper,
EncryptorInterface $encryptor,
) {
$this->checkoutSession = $checkoutSession;
$this->orderInterface = $orderInterface;
@@ -85,6 +88,7 @@ public function __construct(
$this->orderRepository = $orderRepository;
$this->bitpayInvoiceRepository = $bitpayInvoiceRepository;
$this->returnHashHelper = $returnHashHelper;
$this->encryptor = $encryptor;
}

/**
@@ -150,7 +154,8 @@ public function execute(ResultInterface $defaultResult, string $returnId = null)
$order->getId(),
$invoiceID,
$invoice->getExpirationTime(),
$invoice->getAcceptanceWindow()
$invoice->getAcceptanceWindow(),
$this->encryptor->encrypt($this->config->getToken())
);
$this->transactionRepository->add($incrementId, $invoiceID, 'new');

12 changes: 9 additions & 3 deletions Model/BitpayInvoiceRepository.php
Original file line number Diff line number Diff line change
@@ -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);
}

/**
32 changes: 32 additions & 0 deletions Model/Ipn/WebhookVerifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);

namespace Bitpay\BPCheckout\Model\Ipn;

class WebhookVerifier
{
/**
* Verify the validity of webhooks (HMAC)
*
* @see https://developer.bitpay.com/reference/hmac-verification
*
* @param string $signingKey
* @param string $sigHeader
* @param string $webhookBody
*
* @return bool
*/
public function isValidHmac(string $signingKey, string $sigHeader, string $webhookBody): bool
{
$hmac = base64_encode(
hash_hmac(
'sha256',
$webhookBody,
$signingKey,
true
)
);

return $sigHeader === $hmac;
}
}
59 changes: 51 additions & 8 deletions Model/IpnManagement.php
Original file line number Diff line number Diff line change
@@ -5,20 +5,23 @@

use Bitpay\BPCheckout\Api\IpnManagementInterface;
use Bitpay\BPCheckout\Exception\IPNValidationException;
use Bitpay\BPCheckout\Exception\HMACVerificationException;
use Bitpay\BPCheckout\Helper\ReturnHash;
use Bitpay\BPCheckout\Logger\Logger;
use Bitpay\BPCheckout\Model\Ipn\BPCItem;
use Bitpay\BPCheckout\Model\Ipn\Validator;
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\Api\Data\OrderInterface;
use Magento\Sales\Model\OrderFactory;

/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -32,7 +35,7 @@ class IpnManagement implements IpnManagementInterface
protected UrlInterface $url;
protected Session $checkoutSession;
protected QuoteFactory $quoteFactory;
protected OrderInterface $orderInterface;
protected OrderFactory $orderFactory;
protected Registry $coreRegistry;
protected Logger $logger;
protected Config $config;
@@ -42,14 +45,33 @@ class IpnManagement implements IpnManagementInterface
protected Request $request;
protected Client $client;
protected Response $response;

/**
* @var BitpayInvoiceRepository
*/
protected BitpayInvoiceRepository $bitpayInvoiceRepository;

/**
* @var EncryptorInterface
*/
protected EncryptorInterface $encryptor;

/**
* @var WebhookVerifier
*/
protected WebhookVerifier $webhookVerifier;

/**
* @var ReturnHash
*/
protected ReturnHash $returnHashHelper;

/**
* @param ResponseFactory $responseFactory
* @param UrlInterface $url
* @param Registry $registry
* @param Session $checkoutSession
* @param OrderInterface $orderInterface
* @param OrderFactory $orderFactory
* @param QuoteFactory $quoteFactory
* @param Logger $logger
* @param Config $config
@@ -59,6 +81,9 @@ class IpnManagement implements IpnManagementInterface
* @param Request $request
* @param Client $client
* @param Response $response
* @param BitpayInvoiceRepository $bitpayInvoiceRepository
* @param EncryptorInterface $encryptor
* @param WebhookVerifier $webhookVerifier
* @param ReturnHash $returnHashHelper
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
@@ -67,7 +92,7 @@ public function __construct(
UrlInterface $url,
Registry $registry,
Session $checkoutSession,
OrderInterface $orderInterface,
OrderFactory $orderFactory,
QuoteFactory $quoteFactory,
Logger $logger,
Config $config,
@@ -77,13 +102,16 @@ public function __construct(
Request $request,
Client $client,
Response $response,
BitpayInvoiceRepository $bitpayInvoiceRepository,
EncryptorInterface $encryptor,
WebhookVerifier $webhookVerifier,
ReturnHash $returnHashHelper
) {
$this->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;
@@ -93,6 +121,9 @@ public function __construct(
$this->request = $request;
$this->client = $client;
$this->response = $response;
$this->bitpayInvoiceRepository = $bitpayInvoiceRepository;
$this->encryptor = $encryptor;
$this->webhookVerifier = $webhookVerifier;
$this->returnHashHelper = $returnHashHelper;
}

@@ -108,7 +139,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);
$invoiceCloseHandling = $this->config->getBitpayInvoiceCloseHandling();
if ($this->config->getBitpayCheckoutSuccess() === 'standard' && $invoiceCloseHandling === 'keep_order') {
$this->checkoutSession->setLastSuccessQuoteId($order->getQuoteId())
@@ -151,10 +182,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();
@@ -179,7 +222,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') {
13 changes: 10 additions & 3 deletions Model/ResourceModel/BitpayInvoice.php
Original file line number Diff line number Diff line change
@@ -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
]
);
}
21 changes: 20 additions & 1 deletion Test/Integration/Model/BPRedirectTest.php
Original file line number Diff line number Diff line change
@@ -24,6 +24,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;

/**
@@ -111,27 +112,45 @@ class BPRedirectTest extends TestCase
* @var EncryptorInterface|MockObject $encryptor
*/
private $encryptor;

/**
* @var ReturnHash $returnHash
*/
private $returnHash;


public function setUp(): void
{
$this->objectManager = Bootstrap::getObjectManager();
$this->checkoutSession = $this->objectManager->get(Session::class);
$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();

$this->returnHash = $this->objectManager->get(ReturnHash::class);

$this->bpRedirect = new BPRedirect(
$this->checkoutSession,
$this->orderInterface,
@@ -146,6 +165,7 @@ public function setUp(): void
$this->client,
$this->orderRepository,
$this->bitpayInvoiceRepository,
$this->returnHash,
$this->encryptor
);
}
@@ -197,7 +217,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);
}

65 changes: 48 additions & 17 deletions Test/Integration/Model/IpnManagementTest.php
Original file line number Diff line number Diff line change
@@ -9,20 +9,24 @@
use Bitpay\BPCheckout\Model\TransactionRepository;
use BitPaySDK\Model\Invoice\Buyer;
use Magento\Framework\ObjectManagerInterface;
use Bitpay\BPCheckout\Helper\ReturnHash;
use Bitpay\BPCheckout\Logger\Logger;
use Bitpay\BPCheckout\Model\BitpayInvoiceRepository;
use Bitpay\BPCheckout\Model\Ipn\WebhookVerifier;
use Bitpay\BPCheckout\Helper\ReturnHash;
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\Api\Data\OrderInterface;
use Magento\Sales\Model\OrderFactory;
use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;

/**
* @SuppressWarnings(PHPMD.TooManyFields)
@@ -56,9 +60,9 @@ class IpnManagementTest extends TestCase
private $quoteFactory;

/**
* @var OrderInterface $orderInterface
* @var OrderFactory|MockObject $orderFactory
*/
private $orderInterface;
private $orderFactory;

/**
* @var Registry $coreRegistry
@@ -101,7 +105,7 @@ class IpnManagementTest extends TestCase
private $objectManager;

/**
* @var Client $client
* @var Client|MockObject $client
*/
private $client;

@@ -110,10 +114,25 @@ class IpnManagementTest extends TestCase
*/
private $response;

/**
* @var BitpayInvoiceRepository|MockObject $bitpayInvoiceRepository
*/
private $bitpayInvoiceRepository;

/**
* @var EncryptorInterface|MockObject $encryptor
*/
private $encryptor;

/**
* @var WebhookVerifier|MockObject $webhookVerifier
*/
private $webhookVerifier;

/**
* @var ReturnHash $returnHash
*/
private ReturnHash $returnHash;
private $returnHash;

public function setUp(): void
{
@@ -122,24 +141,33 @@ 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->returnHash = $this->objectManager->get(ReturnHash::class);

$this->ipnManagement = new IpnManagement(
$this->responseFactory,
$this->url,
$this->coreRegistry,
$this->checkoutSession,
$this->orderInterface,
$this->orderFactory,
$this->quoteFactory,
$this->logger,
$this->config,
@@ -149,6 +177,9 @@ public function setUp(): void
$this->request,
$this->client,
$this->response,
$this->bitpayInvoiceRepository,
$this->encryptor,
$this->webhookVerifier,
$this->returnHash
);
}
@@ -159,15 +190,15 @@ 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->assertTrue($this->orderInterface->loadByIncrementId('100000001')->isDeleted());
$this->assertNull($this->orderFactory->create()->loadByIncrementId('100000001')->getId());
$this->assertEquals($quoteId, $this->checkoutSession->getQuoteId());
}

@@ -177,14 +208,14 @@ public function testPostClose()
*/
public function testPostCloseKeepOrder()
{
$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->assertFalse($this->orderInterface->loadByIncrementId('100000001')->isDeleted());
$this->assertEquals($order->getId(), $this->orderFactory->create()->loadByIncrementId('100000001')->getId());
$this->assertEquals($quoteId, $this->checkoutSession->getQuoteId());
}

@@ -224,7 +255,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']);
24 changes: 16 additions & 8 deletions Test/Unit/Model/BPRedirectTest.php
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@
use Magento\Framework\UrlInterface;
use \Magento\Framework\Controller\Result\Redirect;
use Magento\Framework\Controller\ResultFactory;
use Magento\Framework\Encryption\EncryptorInterface;
use Magento\Sales\Model\Order;
use Magento\Payment\Model\MethodInterface;
use Magento\Sales\Model\OrderRepository;
@@ -118,6 +119,11 @@ class BPRedirectTest extends TestCase
*/
private $returnHash;

/**
* @var EncryptorInterface|MockObject $encryptor
*/
private $encryptor;

public function setUp(): void
{
$this->checkoutSession = $this->getMock(Session::class);
@@ -136,6 +142,7 @@ public function setUp(): void
$this->orderRepository = $this->getMock(OrderRepository::class);
$this->bitpayInvoiceRepository = $this->getMock(BitpayInvoiceRepository::class);
$this->returnHash = $this->getMock(ReturnHash::class);
$this->encryptor = $this->getMock(EncryptorInterface::class);
$this->bpRedirect = $this->getClass();
}

@@ -175,7 +182,7 @@ public function testExecute(): void
$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);
@@ -196,7 +203,7 @@ public function testExecute(): void
$this->resultFactory->expects($this->once())->method('create')->willReturn($result);

/**
* @var \Magento\Framework\View\Result\Page
* @var \Magento\Framework\Controller\ResultInterface|MockObject
*/
$page = $this->getMock(\Magento\Framework\View\Result\Page::class);

@@ -214,7 +221,7 @@ public function testExecuteNoOrderId(): void
$this->resultFactory->expects($this->once())->method('create')->willReturn($result);

/**
* @var \Magento\Framework\View\Result\Page
* @var \Magento\Framework\Controller\ResultInterface|MockObject
*/
$page = $this->getMock(\Magento\Framework\View\Result\Page::class);

@@ -242,7 +249,7 @@ public function testExecuteNoBitpayPaymentMethod(): void
$this->order->expects($this->once())->method('load')->with($lastOrderId)->willReturn($order);

/**
* @var \Magento\Framework\View\Result\Page
* @var \Magento\Framework\Controller\ResultInterface|MockObject
*/
$page = $this->getMock(\Magento\Framework\View\Result\Page::class);

@@ -285,7 +292,7 @@ public function testExecuteException($exceptionType): void
$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);
@@ -299,7 +306,7 @@ public function testExecuteException($exceptionType): void
->willThrowException(new $exceptionType('something went wrong'));

/**
* @var \Magento\Framework\View\Result\Page
* @var \Magento\Framework\Controller\ResultInterface|MockObject
*/
$page = $this->getMock(\Magento\Framework\View\Result\Page::class);

@@ -337,7 +344,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);
@@ -380,7 +387,8 @@ private function getClass(): BPRedirect
$this->client,
$this->orderRepository,
$this->bitpayInvoiceRepository,
$this->returnHash
$this->returnHash,
$this->encryptor
);
}

43 changes: 43 additions & 0 deletions Test/Unit/Model/Ipn/WebhookVerifierTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Bitpay\BPCheckout\Test\Unit\Model\Ipn;

use Bitpay\BPCheckout\Model\Ipn\WebhookVerifier;
use PHPUnit\Framework\TestCase;

class WebhookVerifierTest extends TestCase
{
/**
* @var WebhookVerifier $webhookVerifier
*/
private $webhookVerifier;

public function setUp(): void
{
$this->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}'
)
);
}
}
127 changes: 114 additions & 13 deletions Test/Unit/Model/IpnManagementTest.php
Original file line number Diff line number Diff line change
@@ -7,19 +7,22 @@
use Bitpay\BPCheckout\Model\Config;
use Bitpay\BPCheckout\Model\Invoice;
use Bitpay\BPCheckout\Model\IpnManagement;
use Bitpay\BPCheckout\Helper\ReturnHash;
use Bitpay\BPCheckout\Model\Ipn\WebhookVerifier;
use Bitpay\BPCheckout\Logger\Logger;
use Bitpay\BPCheckout\Model\BitpayInvoiceRepository;
use Bitpay\BPCheckout\Helper\ReturnHash;
use Bitpay\BPCheckout\Model\TransactionRepository;
use BitPaySDK\Model\Invoice\Buyer;
use Magento\Checkout\Model\Session;
use Magento\Framework\App\ResponseFactory;
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;
@@ -50,9 +53,9 @@ class IpnManagementTest extends TestCase
private $quoteFactory;

/**
* @var OrderInterface|MockObject
* @var OrderFactory|MockObject
*/
private $orderInterface;
private $orderFactory;

/**
* @var Registry|MockObject
@@ -104,6 +107,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;

/**
* @var ReturnHash|MockObject
*/
@@ -115,7 +133,7 @@ public function setUp(): void
$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 +143,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->returnHashHelper = $this->getMock(ReturnHash::class);
$this->ipnManagement = $this->getClass();
}
@@ -138,11 +159,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);
@@ -169,7 +196,14 @@ public function testPostCloseKeepOrder(): void

$this->request->expects($this->once())->method('getParam')->willReturn($orderId);
$this->responseFactory->expects($this->once())->method('create')->willReturn($response);
$this->orderInterface->expects($this->once())->method('loadByIncrementId')->willReturn($order);

$order->expects($this->once())
->method('loadByIncrementId')
->with($orderId)
->willReturnSelf();
$this->orderFactory->expects($this->once())
->method('create')
->willReturn($order);

$this->checkoutSession
->method('__call')
@@ -195,11 +229,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);
@@ -214,13 +253,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();

@@ -379,6 +424,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';
@@ -403,7 +495,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
@@ -418,7 +516,7 @@ private function getClass(): IpnManagement
$this->url,
$this->coreRegistry,
$this->checkoutSession,
$this->orderInterface,
$this->orderFactory,
$this->quoteFactory,
$this->logger,
$this->config,
@@ -428,6 +526,9 @@ private function getClass(): IpnManagement
$this->request,
$this->client,
$this->response,
$this->bitpayInvoiceRepository,
$this->encryptor,
$this->webhookVerifier,
$this->returnHashHelper
);
}
7 changes: 1 addition & 6 deletions Test/Unit/Model/ResourceModel/TransactionTest.php
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@
class TransactionTest extends TestCase
{
/**
* @var Context $context
* @var Context|MockObject $context
*/
private $context;

@@ -28,11 +28,6 @@ class TransactionTest extends TestCase
*/
private $adapter;

/**
* @var Context|MockObject $context
*/
private $context;

public function setUp(): void
{
$this->prepareContext();
1 change: 1 addition & 0 deletions etc/db_schema.xml
Original file line number Diff line number Diff line change
@@ -25,6 +25,7 @@
comment="Expiration time to pay invoice"/>
<column xsi:type="int" name="acceptance_window" unsigned="false" nullable="true"
comment="Miliseconds pay an invoice before it expires"/>
<column xsi:type="varchar" name="bitpay_token" nullable="true" length="255" comment="Bitpay Token"/>

<constraint xsi:type="foreign" referenceId="BITPAY_INVOICE_ORDER_ID_SALES_ORDER_ENTITY_ID"
table="bitpay_invoice"
3 changes: 2 additions & 1 deletion etc/db_schema_whitelist.json
Original file line number Diff line number Diff line change
@@ -17,7 +17,8 @@
"order_id": true,
"expiration_time": true,
"acceptance_window": true,
"invoice_id": true
"invoice_id": true,
"bitpay_token": true
},
"index": {
"BITPAY_INVOICE_ORDER_ID": true

0 comments on commit ba52121

Please sign in to comment.