Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions Controller/Payment/Setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,32 @@ public function execute() {
try {
return $this->processAuthorization($order);
} catch (\Pstk\Paystack\Gateway\Exception\ApiException $e) {
$message = $e->getMessage();
$order->addStatusToHistory($order->getStatus(), $message);
// The exception message is built from curl_error() and Paystack's raw
// response body (see Gateway/PaystackApiClient::request()), so it can
// carry internal hostnames, TLS/proxy detail, or gateway-side state —
// the same leak D5 closed on Callback.php. Detail stays in the
// (admin-only) order history; the customer gets a fixed, safe string.
// Context array, not string concatenation, so an embedded newline in
// the gateway/curl text can't forge additional log lines.
$this->logger->error('Paystack setup API error', [
'error' => $e->getMessage(),
'exception' => $e,
]);
$order->addStatusToHistory($order->getStatus(), $e->getMessage());
$this->orderRepository->save($order);
$message = "We could not start your Paystack payment. Please try again "
. "or contact support if the problem continues.";
} catch (\Throwable $e) {
// Same rationale as the ApiException branch above, for anything else
// processAuthorization() can throw (a missing store URL, a malformed
// Paystack response, a save failure) — D5's Callback.php fix added
// this same bare-Throwable catch for the identical reason.
$this->logger->error('Paystack setup failed', [
'error' => $e->getMessage(),
'exception' => $e,
]);
$message = "We could not start your Paystack payment. Please try again "
. "or contact support if the problem continues.";
}
}

Expand Down
61 changes: 61 additions & 0 deletions Test/Unit/Controller/Payment/SetupTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,67 @@ public function testApiExceptionSavesStatusHistory(): void
->method('save')
->with($order);

// The raw ApiException message ("Invalid key" — built from curl_error()
// and Paystack's raw response body in real use) must never reach the
// customer-facing failure page; only the fixed, safe string may.
$this->messageManager->expects($this->once())
->method('addErrorMessage')
->with($this->callback(function ($message) {
$text = (string) $message;
return !str_contains($text, 'Invalid key')
&& str_contains($text, 'We could not start your Paystack payment');
}));

$controller->execute();
}

public function testGenericThrowableShowsSafeMessageAndDoesNotLeakDetail(): void
{
$controller = $this->createController();

$lastOrder = $this->createMock(Order::class);
$lastOrder->method('getIncrementId')->willReturn('000000001');
$this->checkoutSession->method('getLastRealOrder')->willReturn($lastOrder);

$payment = $this->createMock(Payment::class);
$payment->method('getMethod')->willReturn(Paystack::CODE);

$order = $this->createMock(Order::class);
$order->method('getPayment')->willReturn($payment);
$order->method('getStatus')->willReturn('pending');
$order->method('getCustomerFirstname')->willReturn('John');
$order->method('getCustomerLastname')->willReturn('Doe');
$order->method('getGrandTotal')->willReturn(100.00);
$order->method('getCustomerEmail')->willReturn('john@test.com');
$order->method('getIncrementId')->willReturn('000000001');
$order->method('getOrderCurrencyCode')->willReturn('NGN');

$this->orderInterface->method('loadByIncrementId')->willReturn($order);

$methodInstance = $this->createMock(MethodInterface::class);
$methodInstance->method('getCode')->willReturn(Paystack::CODE);
$this->paymentHelper->method('getMethodInstance')->willReturn($methodInstance);

$store = $this->createMock(Store::class);
$store->method('getBaseUrl')->willReturn('https://example.com/');
$this->storeManager->method('getStore')->willReturn($store);

$this->transactionValidator->method('expectedSubunits')->willReturn(10000);

// A non-ApiException throwable (e.g. a malformed Paystack response, a
// missing store URL) must be caught too — not just ApiException — and
// must never surface its own message to the customer.
$this->paystackClient->method('initializeTransaction')
->willThrowException(new \RuntimeException('unexpected internal detail'));

$this->messageManager->expects($this->once())
->method('addErrorMessage')
->with($this->callback(function ($message) {
$text = (string) $message;
return !str_contains($text, 'unexpected internal detail')
&& str_contains($text, 'We could not start your Paystack payment');
}));

$controller->execute();
}
}
Loading