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 */ 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; } }