265 lines
11 KiB
PHP
265 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Controller;
|
|
|
|
use App\BusProNet\Model\Travel;
|
|
use App\Controller\SecurityController;
|
|
use App\Form\Model\BookingDto;
|
|
use App\Form\Model\VoucherPurchaseDto;
|
|
use App\Model\PurchaseFlow;
|
|
use App\Service\BookingSessionManager;
|
|
use App\Service\BookingSummaryAssembler;
|
|
use App\Service\PurchaseFlowResolver;
|
|
use App\Service\VoucherPurchaseSessionManager;
|
|
use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface;
|
|
use League\Bundle\OAuth2ServerBundle\Model\Client;
|
|
use PHPUnit\Framework\TestCase;
|
|
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;
|
|
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
|
|
|
class SecurityControllerTest extends TestCase
|
|
{
|
|
private const AUTHORIZE_PATH = '/authorize?response_type=code&client_id=abc123&redirect_uri=https%3A%2F%2Fephub.example%2Fcallback';
|
|
|
|
public function testAnAuthorizeTargetPathRendersTheSelfExplainingLoginPageNamingTheClient(): void
|
|
{
|
|
$controller = $this->createController();
|
|
$controller->clientManager
|
|
->expects(self::once())
|
|
->method('find')
|
|
->with('abc123')
|
|
->willReturn(new Client('EPHub', 'abc123', null));
|
|
|
|
$response = $controller->login(...$this->arguments($controller, self::AUTHORIZE_PATH));
|
|
|
|
self::assertSame(200, $response->getStatusCode());
|
|
self::assertSame('security/login_oauth2.html.twig', $controller->renderedView);
|
|
self::assertTrue($controller->renderedParameters['oauth2']);
|
|
self::assertSame('EPHub', $controller->renderedParameters['client_name']);
|
|
}
|
|
|
|
public function testWithoutAnAuthorizeTargetPathTheStandardLoginPageIsRendered(): void
|
|
{
|
|
$controller = $this->createController();
|
|
$controller->clientManager->expects(self::never())->method('find');
|
|
|
|
$response = $controller->login(...$this->arguments($controller, null));
|
|
|
|
self::assertSame(200, $response->getStatusCode());
|
|
self::assertSame('security/login.html.twig', $controller->renderedView);
|
|
self::assertFalse($controller->renderedParameters['oauth2']);
|
|
self::assertNull($controller->renderedParameters['client_name']);
|
|
}
|
|
|
|
/**
|
|
* The name is cosmetic and this runs before authentication, so an unknown client must degrade
|
|
* to neutral wording rather than break the login.
|
|
*/
|
|
public function testAnUnresolvableClientIdFallsBackToNeutralWording(): void
|
|
{
|
|
$controller = $this->createController();
|
|
$controller->clientManager
|
|
->expects(self::once())
|
|
->method('find')
|
|
->willReturn(null);
|
|
|
|
$response = $controller->login(...$this->arguments($controller, self::AUTHORIZE_PATH));
|
|
|
|
self::assertSame(200, $response->getStatusCode());
|
|
self::assertSame('security/login_oauth2.html.twig', $controller->renderedView);
|
|
self::assertNull($controller->renderedParameters['client_name']);
|
|
}
|
|
|
|
public function testAnAuthorizeTargetPathWithoutAClientIdFallsBackToNeutralWording(): void
|
|
{
|
|
$controller = $this->createController();
|
|
$controller->clientManager->expects(self::never())->method('find');
|
|
|
|
$response = $controller->login(...$this->arguments($controller, '/authorize'));
|
|
|
|
self::assertSame(200, $response->getStatusCode());
|
|
self::assertSame('security/login_oauth2.html.twig', $controller->renderedView);
|
|
self::assertNull($controller->renderedParameters['client_name']);
|
|
}
|
|
|
|
/**
|
|
* A booking left half-finished in the same browser used to overwrite the pending target path
|
|
* with booking step 1, so the application that asked for the authorization never got its code.
|
|
*/
|
|
public function testAPendingAuthorizationWinsOverAHalfFinishedBooking(): void
|
|
{
|
|
$controller = $this->createController(bookingDto: $this->createBookingDto());
|
|
$controller->clientManager
|
|
->expects(self::once())
|
|
->method('find')
|
|
->willReturn(new Client('EPHub', 'abc123', null));
|
|
|
|
$session = $this->createSession(self::AUTHORIZE_PATH);
|
|
$session->set(BookingSessionManager::BOOKING_CREATE_KEY, $this->createBookingDto());
|
|
$response = $controller->login(...$this->arguments($controller, self::AUTHORIZE_PATH, $session));
|
|
|
|
self::assertSame(200, $response->getStatusCode());
|
|
self::assertSame('security/login_oauth2.html.twig', $controller->renderedView);
|
|
self::assertSame(self::AUTHORIZE_PATH, $session->get('_security.main.target_path'));
|
|
}
|
|
|
|
public function testAHalfFinishedBookingStillWinsWhenThereIsNoAuthorization(): void
|
|
{
|
|
$controller = $this->createController(bookingDto: $this->createBookingDto());
|
|
$controller->clientManager->expects(self::never())->method('find');
|
|
|
|
$session = $this->createSession(null);
|
|
$session->set(BookingSessionManager::BOOKING_CREATE_KEY, $this->createBookingDto());
|
|
$response = $controller->login(...$this->arguments($controller, null, $session));
|
|
|
|
self::assertSame(200, $response->getStatusCode());
|
|
self::assertSame('booking/create/authenticate.html.twig', $controller->renderedView);
|
|
self::assertSame('/app_booking_create_step_1', $session->get('_security.main.target_path'));
|
|
}
|
|
|
|
public function testAPendingVoucherPurchaseRendersTheVoucherLoginAndContinuesWithItsFirstStep(): void
|
|
{
|
|
$controller = $this->createController();
|
|
$controller->clientManager->expects(self::never())->method('find');
|
|
|
|
$session = $this->createSession(null);
|
|
$session->set(VoucherPurchaseSessionManager::SESSION_KEY, new VoucherPurchaseDto());
|
|
$response = $controller->login(...$this->arguments($controller, null, $session));
|
|
|
|
self::assertSame(200, $response->getStatusCode());
|
|
self::assertSame('voucher/authenticate.html.twig', $controller->renderedView);
|
|
self::assertSame('/app_voucher_step_1', $session->get('_security.main.target_path'));
|
|
self::assertNull($controller->renderedParameters['travel_title']);
|
|
}
|
|
|
|
public function testAPendingAuthorizationWinsOverAPendingVoucherPurchase(): void
|
|
{
|
|
$controller = $this->createController();
|
|
$controller->clientManager->expects(self::once())->method('find')->willReturn(new Client('EPHub', 'abc123', null));
|
|
|
|
$session = $this->createSession(self::AUTHORIZE_PATH);
|
|
$session->set(VoucherPurchaseSessionManager::SESSION_KEY, new VoucherPurchaseDto());
|
|
$controller->login(...$this->arguments($controller, self::AUTHORIZE_PATH, $session));
|
|
|
|
self::assertSame('security/login_oauth2.html.twig', $controller->renderedView);
|
|
self::assertSame(self::AUTHORIZE_PATH, $session->get('_security.main.target_path'));
|
|
}
|
|
|
|
/**
|
|
* A booking and a voucher purchase can both be pending, e.g. from two tabs. The one the visitor
|
|
* started last is the one they are signing in for.
|
|
*/
|
|
public function testTheLastStartedOfTwoPendingFlowsWins(): void
|
|
{
|
|
foreach ([PurchaseFlow::Voucher, PurchaseFlow::Booking] as $lastStarted) {
|
|
$controller = $this->createController(bookingDto: $this->createBookingDto());
|
|
$controller->clientManager->expects(self::never())->method('find');
|
|
|
|
$session = $this->createSession(null);
|
|
$session->set(BookingSessionManager::BOOKING_CREATE_KEY, $this->createBookingDto());
|
|
$session->set(VoucherPurchaseSessionManager::SESSION_KEY, new VoucherPurchaseDto());
|
|
$session->set(PurchaseFlowResolver::LAST_STARTED_KEY, $lastStarted->value);
|
|
$controller->login(...$this->arguments($controller, null, $session));
|
|
|
|
self::assertSame($lastStarted->authenticateTemplate(), $controller->renderedView);
|
|
self::assertSame('/'.$lastStarted->firstStepRoute(), $session->get('_security.main.target_path'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array{AuthenticationUtils, Request, BookingSessionManager, BookingSummaryAssembler, ClientManagerInterface, PurchaseFlowResolver}
|
|
*/
|
|
private function arguments(TestableSecurityController $controller, ?string $targetPath, ?Session $session = null): array
|
|
{
|
|
$request = Request::create('/', 'GET');
|
|
$request->setSession($session ?? $this->createSession($targetPath));
|
|
|
|
return [
|
|
$controller->authenticationUtils,
|
|
$request,
|
|
$controller->bookingSessionManager,
|
|
$controller->summaryAssembler,
|
|
$controller->clientManager,
|
|
new PurchaseFlowResolver(),
|
|
];
|
|
}
|
|
|
|
private function createSession(?string $targetPath): Session
|
|
{
|
|
$session = new Session(new MockArraySessionStorage());
|
|
if (null !== $targetPath) {
|
|
$session->set('_security.main.target_path', $targetPath);
|
|
}
|
|
|
|
return $session;
|
|
}
|
|
|
|
private function createBookingDto(): BookingDto
|
|
{
|
|
$travel = new Travel();
|
|
$travel->id = 123;
|
|
|
|
return new BookingDto($travel, 77);
|
|
}
|
|
|
|
private function createController(?BookingDto $bookingDto = null): TestableSecurityController
|
|
{
|
|
$authenticationUtils = $this->createStub(AuthenticationUtils::class);
|
|
$authenticationUtils->method('getLastAuthenticationError')->willReturn(null);
|
|
$authenticationUtils->method('getLastUsername')->willReturn('');
|
|
|
|
$bookingSessionManager = $this->createStub(BookingSessionManager::class);
|
|
$bookingSessionManager->method('getBookingDto')->willReturn($bookingDto);
|
|
|
|
return new TestableSecurityController(
|
|
$authenticationUtils,
|
|
$bookingSessionManager,
|
|
$this->createStub(BookingSummaryAssembler::class),
|
|
$this->createMock(ClientManagerInterface::class),
|
|
);
|
|
}
|
|
}
|
|
|
|
final class TestableSecurityController extends SecurityController
|
|
{
|
|
/**
|
|
* @var array<string, mixed>
|
|
*/
|
|
public array $renderedParameters = [];
|
|
|
|
public string $renderedView = '';
|
|
|
|
public function __construct(
|
|
public readonly AuthenticationUtils $authenticationUtils,
|
|
public readonly BookingSessionManager $bookingSessionManager,
|
|
public readonly BookingSummaryAssembler $summaryAssembler,
|
|
public readonly ClientManagerInterface $clientManager,
|
|
) {
|
|
}
|
|
|
|
protected function getUser(): ?UserInterface
|
|
{
|
|
return null;
|
|
}
|
|
|
|
protected function render(string $view, array $parameters = [], ?Response $response = null): Response
|
|
{
|
|
$this->renderedView = $view;
|
|
$this->renderedParameters = $parameters;
|
|
|
|
return new Response('ok');
|
|
}
|
|
|
|
protected function generateUrl(string $route, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string
|
|
{
|
|
return '/'.$route;
|
|
}
|
|
}
|