288 lines
11 KiB
PHP
288 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Controller\Voucher;
|
|
|
|
use App\BusProNet\Client\VoucherClient;
|
|
use App\BusProNet\Exception\TimeoutException;
|
|
use App\BusProNet\Model\Notification;
|
|
use App\BusProNet\Model\VoucherPaymentMethod;
|
|
use App\BusProNet\Model\VoucherSaleResponse;
|
|
use App\BusProNet\Model\VoucherTemplate;
|
|
use App\Controller\Voucher\Step3Controller;
|
|
use App\Form\Model\VoucherPurchaseDto;
|
|
use App\Service\VoucherPurchaseSessionManager;
|
|
use App\Service\VoucherSaleRejectionMapper;
|
|
use App\Service\VoucherThumbnailResolver;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Psr\Log\LoggerInterface;
|
|
use Psr\Log\NullLogger;
|
|
use Symfony\Component\Form\FormInterface;
|
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpFoundation\Session\Session;
|
|
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
|
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
|
use Symfony\Component\Security\Core\User\UserInterface;
|
|
|
|
class Step3ControllerTest extends TestCase
|
|
{
|
|
private VoucherPurchaseSessionManager $sessionManager;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->sessionManager = new VoucherPurchaseSessionManager();
|
|
}
|
|
|
|
public function testAPassingDryRunOffersThePurchaseWithFreshConfirmations(): void
|
|
{
|
|
$request = $this->createRequest('GET', $dto = $this->createDto());
|
|
$dto->termsAccepted = true;
|
|
$dto->purchaseConfirmed = true;
|
|
|
|
$voucherClient = $this->createMock(VoucherClient::class);
|
|
$voucherClient->expects(self::once())->method('createVoucherSaleInquiry')->willReturn(new VoucherSaleResponse('möglich'));
|
|
$voucherClient->expects(self::never())->method('createVoucherSale');
|
|
|
|
$controller = $this->createController($voucherClient);
|
|
$controller->index($request);
|
|
|
|
self::assertTrue($controller->renderParameters['purchasable']);
|
|
self::assertFalse($dto->termsAccepted);
|
|
self::assertFalse($dto->purchaseConfirmed);
|
|
}
|
|
|
|
/**
|
|
* 650 is what Notification::isError() calls a success. On this path it is a missing address field.
|
|
*/
|
|
public function testAnAddressComplaintSendsTheBuyerBackToStep2(): void
|
|
{
|
|
$request = $this->createRequest('GET', $this->createDto());
|
|
|
|
$voucherClient = $this->createStub(VoucherClient::class);
|
|
$voucherClient->method('createVoucherSaleInquiry')->willReturn(
|
|
new Notification(650, 'Fehlende Adress Daten. Bitte folgende Eingabefelder prüfen: Strasse')
|
|
);
|
|
|
|
$controller = $this->createController($voucherClient);
|
|
$response = $controller->index($request);
|
|
|
|
self::assertInstanceOf(RedirectResponse::class, $response);
|
|
self::assertSame('/app_voucher_step_2', $response->getTargetUrl());
|
|
self::assertSame(2, $this->sessionManager->find($request)?->currentStep);
|
|
self::assertSame([['error', 'Fehlende Adress Daten. Bitte folgende Eingabefelder prüfen: Strasse']], $controller->flashes);
|
|
}
|
|
|
|
public function testAnAmountComplaintSendsTheBuyerBackToStep1(): void
|
|
{
|
|
$request = $this->createRequest('GET', $this->createDto());
|
|
|
|
$voucherClient = $this->createStub(VoucherClient::class);
|
|
$voucherClient->method('createVoucherSaleInquiry')->willReturn(
|
|
new Notification(608, 'Gutscheinbetrag darf nicht größer als 5.000,00 Euro sein')
|
|
);
|
|
|
|
$response = $this->createController($voucherClient)->index($request);
|
|
|
|
self::assertInstanceOf(RedirectResponse::class, $response);
|
|
self::assertSame('/app_voucher_step_1', $response->getTargetUrl());
|
|
}
|
|
|
|
public function testAComplaintNobodyCanFixKeepsTheOverviewWithoutABuyButton(): void
|
|
{
|
|
$request = $this->createRequest('GET', $this->createDto());
|
|
|
|
$voucherClient = $this->createStub(VoucherClient::class);
|
|
$voucherClient->method('createVoucherSaleInquiry')->willReturn(new Notification(619, 'Agentur-ID fehlt'));
|
|
|
|
$controller = $this->createController($voucherClient);
|
|
$controller->index($request);
|
|
|
|
self::assertFalse($controller->renderParameters['purchasable']);
|
|
}
|
|
|
|
public function testAPurchaseClearsTheSessionAndKeepsTheRedemptionCodeOutOfTheLog(): void
|
|
{
|
|
$request = $this->createRequest('POST', $this->createDto());
|
|
|
|
$voucherClient = $this->createMock(VoucherClient::class);
|
|
$voucherClient->expects(self::never())->method('createVoucherSaleInquiry');
|
|
$voucherClient->expects(self::once())->method('createVoucherSale')->willReturn(new VoucherSaleResponse(
|
|
status: 'erfolgt',
|
|
voucherId: 4711,
|
|
voucherNumber: 'K-001234',
|
|
redemptionCode: 'ABCD1234',
|
|
price: 111.0,
|
|
invoiceNumber: 'R-998877',
|
|
email: '[email protected]',
|
|
));
|
|
|
|
$logger = $this->createMock(LoggerInterface::class);
|
|
$logger->expects(self::once())->method('info')->with('Voucher successfully sold.', self::callback(
|
|
static fn (array $context) => !\in_array('ABCD1234', $context, true) && 'K-001234' === $context['voucher_number']
|
|
));
|
|
|
|
$controller = $this->createController($voucherClient, $this->createSubmittedForm(), $logger);
|
|
$response = $controller->index($request);
|
|
|
|
self::assertInstanceOf(RedirectResponse::class, $response);
|
|
self::assertSame('/app_voucher_success', $response->getTargetUrl());
|
|
self::assertNull($this->sessionManager->find($request));
|
|
self::assertContains(['voucher_number', 'K-001234'], $controller->flashes);
|
|
self::assertContains(['voucher_total', 111.0], $controller->flashes);
|
|
self::assertNotContains('ABCD1234', array_column($controller->flashes, 1));
|
|
}
|
|
|
|
/**
|
|
* The first submit cleared the session before redirecting, so the second one finds nothing to buy.
|
|
*/
|
|
public function testASecondSubmitCannotBuyTwice(): void
|
|
{
|
|
$request = Request::create('/gutschein/uebersicht', 'POST');
|
|
$request->setSession(new Session(new MockArraySessionStorage()));
|
|
|
|
$voucherClient = $this->createMock(VoucherClient::class);
|
|
$voucherClient->expects(self::never())->method('createVoucherSale');
|
|
|
|
$response = $this->createController($voucherClient, $this->createSubmittedForm())->index($request);
|
|
|
|
self::assertInstanceOf(RedirectResponse::class, $response);
|
|
self::assertSame('/app_voucher_error', $response->getTargetUrl());
|
|
}
|
|
|
|
/**
|
|
* A timeout leaves it open whether BusPro issued the voucher, so no blind retry is offered.
|
|
*/
|
|
public function testATimeoutDuringThePurchaseOffersNoRetry(): void
|
|
{
|
|
$request = $this->createRequest('POST', $this->createDto());
|
|
|
|
$voucherClient = $this->createStub(VoucherClient::class);
|
|
$voucherClient->method('createVoucherSale')->willThrowException(new TimeoutException('Stream timeout'));
|
|
|
|
$controller = $this->createController($voucherClient, $this->createSubmittedForm());
|
|
$controller->index($request);
|
|
|
|
self::assertFalse($controller->renderParameters['purchasable']);
|
|
self::assertNotNull($this->sessionManager->find($request));
|
|
self::assertStringContainsString('Kundenservice', $controller->flashes[0][1]);
|
|
}
|
|
|
|
private function createRequest(string $method, VoucherPurchaseDto $dto): Request
|
|
{
|
|
$request = Request::create('/gutschein/uebersicht', $method);
|
|
$request->setSession(new Session(new MockArraySessionStorage()));
|
|
$this->sessionManager->save($request, $dto);
|
|
|
|
return $request;
|
|
}
|
|
|
|
private function createDto(): VoucherPurchaseDto
|
|
{
|
|
$dto = new VoucherPurchaseDto();
|
|
$dto->currentStep = 3;
|
|
$dto->template = new VoucherTemplate(3, 'K', 'Allgemein');
|
|
$dto->paymentMethod = new VoucherPaymentMethod(2, 'Überweisung', 'UEBERW');
|
|
$dto->amount = 111;
|
|
|
|
return $dto;
|
|
}
|
|
|
|
/**
|
|
* @return FormInterface<VoucherPurchaseDto>
|
|
*/
|
|
private function createSubmittedForm(): FormInterface
|
|
{
|
|
$form = $this->createStub(FormInterface::class);
|
|
$form->method('isSubmitted')->willReturn(true);
|
|
$form->method('isValid')->willReturn(true);
|
|
|
|
return $form;
|
|
}
|
|
|
|
/**
|
|
* @param FormInterface<VoucherPurchaseDto>|null $form
|
|
*/
|
|
private function createController(VoucherClient $voucherClient, ?FormInterface $form = null, ?LoggerInterface $logger = null): TestableStep3Controller
|
|
{
|
|
return new TestableStep3Controller(
|
|
$this->sessionManager,
|
|
$this->createStub(VoucherThumbnailResolver::class),
|
|
$voucherClient,
|
|
new VoucherSaleRejectionMapper(),
|
|
$logger ?? new NullLogger(),
|
|
$form ?? $this->createStub(FormInterface::class),
|
|
);
|
|
}
|
|
}
|
|
|
|
final class TestableStep3Controller extends Step3Controller
|
|
{
|
|
/**
|
|
* @var list<array{string, mixed}>
|
|
*/
|
|
public array $flashes = [];
|
|
|
|
/**
|
|
* @var array<string, mixed>
|
|
*/
|
|
public array $renderParameters = [];
|
|
|
|
/**
|
|
* @param FormInterface<VoucherPurchaseDto> $form
|
|
*/
|
|
public function __construct(
|
|
VoucherPurchaseSessionManager $sessionManager,
|
|
VoucherThumbnailResolver $thumbnailResolver,
|
|
VoucherClient $voucherClient,
|
|
VoucherSaleRejectionMapper $rejectionMapper,
|
|
LoggerInterface $logger,
|
|
private readonly FormInterface $form,
|
|
) {
|
|
parent::__construct($sessionManager, $thumbnailResolver, $voucherClient, $rejectionMapper, $logger);
|
|
}
|
|
|
|
protected function getUser(): ?UserInterface
|
|
{
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return FormInterface<mixed>
|
|
*/
|
|
protected function createForm(string $type, mixed $data = null, array $options = []): FormInterface
|
|
{
|
|
return $this->form;
|
|
}
|
|
|
|
protected function addFlash(string $type, mixed $message): void
|
|
{
|
|
$this->flashes[] = [$type, $message];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $parameters
|
|
*/
|
|
protected function redirectToRoute(string $route, array $parameters = [], int $status = 302): RedirectResponse
|
|
{
|
|
return new RedirectResponse('/'.$route, $status);
|
|
}
|
|
|
|
protected function generateUrl(string $route, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string
|
|
{
|
|
return '/'.$route;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $parameters
|
|
*/
|
|
protected function render(string $view, array $parameters = [], ?Response $response = null): Response
|
|
{
|
|
$this->renderParameters = $parameters;
|
|
|
|
return new Response('');
|
|
}
|
|
}
|