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
140 changes: 140 additions & 0 deletions tests/Action/AuthorizeActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
use ReflectionClass;
use ReflectionException;
use Setono\Payum\Quickpay\Action\AuthorizeAction;
use Setono\Payum\Quickpay\Api;
use Setono\Quickpay\Client\Client;

class AuthorizeActionTest extends ActionTestAbstract
{
Expand Down Expand Up @@ -123,4 +125,142 @@ public function shouldCreatePaymentLinkAndRedirectToIt(): void
self::assertTrue($body['auto_capture']);
self::assertSame(266017, $body['agreement_id']);
}

/**
* A consumer that routes callbacks itself presets `callback_url` and executes Authorize without
* a token. That path must not need the token factory at all — the action only mints a notify
* token when the request carries a token to mint it from.
*
* @test
*/
public function shouldCreateTheLinkWithoutATokenWhenTheCallbackUrlIsPreset(): void
{
$details = new ArrayObject([
'quickpayPaymentId' => 1001,
'amount' => 100,
'continue_url' => 'theContinueUrl',
'cancel_url' => 'theCancelUrl',
'callback_url' => 'thePresetCallbackUrl',
]);

/** @var Authorize $authorize */
$authorize = new $this->requestClass($details);

$action = new AuthorizeAction();
$action->setGateway($this->gateway);
$action->setApi($this->api);
// Deliberately no setGenericTokenFactory().

$this->queueResponse('{"url":"https://payment.quickpay.net/payments/1001/payment-window"}');

try {
$action->execute($authorize);
self::fail('An HttpRedirect reply should have been thrown');
} catch (HttpRedirect $redirect) {
self::assertSame('https://payment.quickpay.net/payments/1001/payment-window', $redirect->getUrl());
}

$requests = $this->getRequests();
self::assertCount(1, $requests);
self::assertSame('thePresetCallbackUrl', $this->decodeBody($requests[0])['callback_url']);
}

/**
* @test
*/
public function shouldThrowBeforeAnyRequestWhenARequiredDetailIsMissing(): void
{
// No callback_url, and no token to mint one from.
$details = new ArrayObject([
'quickpayPaymentId' => 1001,
'amount' => 100,
'continue_url' => 'theContinueUrl',
'cancel_url' => 'theCancelUrl',
]);

/** @var Authorize $authorize */
$authorize = new $this->requestClass($details);

$action = new AuthorizeAction();
$action->setGateway($this->gateway);
$action->setApi($this->api);

$this->expectException(LogicException::class);
$this->expectExceptionMessage('callback_url');

try {
$action->execute($authorize);
} finally {
self::assertCount(0, $this->getRequests(), 'The link request must not be issued');
}
}

/**
* @test
*/
public function shouldThrowWhenQuickpayReturnsNoLinkUrl(): void
{
$details = new ArrayObject([
'quickpayPaymentId' => 1001,
'amount' => 100,
'continue_url' => 'theContinueUrl',
'cancel_url' => 'theCancelUrl',
'callback_url' => 'theCallbackUrl',
]);

/** @var Authorize $authorize */
$authorize = new $this->requestClass($details);

$action = new AuthorizeAction();
$action->setGateway($this->gateway);
$action->setApi($this->api);

$this->queueResponse('{"url":null}');

$this->expectException(LogicException::class);
$this->expectExceptionMessage('did not return a payment link url');

$action->execute($authorize);
}

/**
* `branding_id` is optional, so if it silently stopped being read the link would simply be
* created without it and Quickpay would fall back to the account default — the same failure mode
* the factory tests guard against for `agreement`. Pin that the option reaches the wire.
*
* @test
*/
public function shouldPassTheBrandingIdToTheLink(): void
{
$api = new Api(
client: new Client('test-apikey', $this->httpClient),
privateKey: 'test-privatekey',
brandingId: 424242,
);

$details = new ArrayObject([
'quickpayPaymentId' => 1001,
'amount' => 100,
'continue_url' => 'theContinueUrl',
'cancel_url' => 'theCancelUrl',
'callback_url' => 'theCallbackUrl',
]);

/** @var Authorize $authorize */
$authorize = new $this->requestClass($details);

$action = new AuthorizeAction();
$action->setGateway($this->gateway);
$action->setApi($api);

$this->queueResponse('{"url":"https://payment.quickpay.net/payments/1001/payment-window"}');

try {
$action->execute($authorize);
self::fail('An HttpRedirect reply should have been thrown');
} catch (HttpRedirect) {
}

self::assertSame(424242, $this->decodeBody($this->getRequests()[0])['branding_id']);
}
}
51 changes: 51 additions & 0 deletions tests/Action/NotifyActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,57 @@ public function shouldRejectMissingChecksum(): void
self::assertCount(0, $this->getRequests(), 'No API call should be made for an unsigned callback');
}

/**
* Symfony's HeaderBag lower-cases header names, and the Symfony bridge is what feeds
* GetHttpRequest in production Sylius/Symfony setups — so the lower-cased spelling is the shape
* the checksum lookup actually meets there. It must match case-insensitively.
*
* @test
*/
public function shouldAcceptALowerCasedChecksumHeader(): void
{
$body = '{"id":1001}';
$this->httpRequestAction->setHttpRequest($body, [
strtolower(CallbackValidator::CHECKSUM_HEADER) => hash_hmac('sha256', $body, 'test-privatekey'),
]);

// No operations: ConfirmPayment fetches and finds nothing to confirm.
$this->queuePayment(['state' => PaymentState::Initial->value, 'operations' => []]);

$action = new NotifyAction();
$action->setGateway($this->gateway);
$action->setApi($this->api);

$action->execute($this->notify());

$requests = $this->getRequests();
self::assertCount(1, $requests);
$this->assertRequest($requests[0], 'GET', '#/payments/1001$#');
}

/**
* Bridges may expose a header's value as a list. The first entry is the checksum.
*
* @test
*/
public function shouldAcceptAListValuedChecksumHeader(): void
{
$body = '{"id":1001}';
$this->httpRequestAction->setHttpRequest($body, [
CallbackValidator::CHECKSUM_HEADER => [hash_hmac('sha256', $body, 'test-privatekey')],
]);

$this->queuePayment(['state' => PaymentState::Initial->value, 'operations' => []]);

$action = new NotifyAction();
$action->setGateway($this->gateway);
$action->setApi($this->api);

$action->execute($this->notify());

self::assertCount(1, $this->getRequests());
}

private function notify(): Notify
{
return new Notify(new ArrayObject(['quickpayPaymentId' => 1001, 'amount' => 100]));
Expand Down
Loading
Loading