mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-07-10 09:30:14 +02:00
Add fullscreen toggle and return-to-page logout redirect (#200)
* feat: add fullscreen toggle and return-to-page logout redirect Add a Stimulus-driven fullscreen button/keypress to the quiz and elimination screens, and redirect admins back to the page they were on after logging out (unless it was a backoffice or elimination page). * docs: expand CLAUDE.md domain context and testing conventions Document the WIDM quiz/elimination domain model in more depth, and add guidance to prefer plain TestCase over WebTestCase/KernelTestCase and to apply the Boy Scout Rule for small nearby cleanups. * fix: drop f-keypress fullscreen shortcut Candidate names can start with "f" while typing, so the keypress conflicted with the enter-name form. Button-only toggle remains.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tvdt\Tests\Security;
|
||||
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Http\Event\LogoutEvent;
|
||||
use Tvdt\Security\LogoutRedirectListener;
|
||||
|
||||
#[CoversClass(LogoutRedirectListener::class)]
|
||||
final class LogoutRedirectListenerTest extends TestCase
|
||||
{
|
||||
public function testLogoutRedirectsBackToTheGivenTarget(): void
|
||||
{
|
||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$urlGenerator->expects($this->never())->method('generate');
|
||||
$listener = new LogoutRedirectListener($urlGenerator);
|
||||
|
||||
$event = new LogoutEvent(Request::create('/logout?target=/krtek'), null);
|
||||
$listener->onLogout($event);
|
||||
|
||||
$this->assertSame('/krtek', $event->getResponse()?->headers->get('Location'));
|
||||
}
|
||||
|
||||
public function testLogoutWithoutTargetRedirectsToSeasonSelect(): void
|
||||
{
|
||||
$listener = new LogoutRedirectListener($this->seasonSelectUrlGenerator());
|
||||
|
||||
$event = new LogoutEvent(Request::create('/logout'), null);
|
||||
$listener->onLogout($event);
|
||||
|
||||
$this->assertSame('/', $event->getResponse()?->headers->get('Location'));
|
||||
}
|
||||
|
||||
#[DataProvider('blockedTargetProvider')]
|
||||
public function testLogoutIgnoresBlockedOrUnsafeTargets(string $target): void
|
||||
{
|
||||
$listener = new LogoutRedirectListener($this->seasonSelectUrlGenerator());
|
||||
|
||||
$event = new LogoutEvent(Request::create('/logout?target='.urlencode($target)), null);
|
||||
$listener->onLogout($event);
|
||||
|
||||
$this->assertSame('/', $event->getResponse()?->headers->get('Location'));
|
||||
}
|
||||
|
||||
/** @return iterable<string, array{string}> */
|
||||
public static function blockedTargetProvider(): iterable
|
||||
{
|
||||
yield 'backoffice page' => ['/backoffice/season/krtek'];
|
||||
yield 'elimination page' => ['/elimination/00000000-0000-0000-0000-000000000000'];
|
||||
yield 'protocol-relative url' => ['//evil.example.org'];
|
||||
yield 'absolute url' => ['https://evil.example.org'];
|
||||
}
|
||||
|
||||
private function seasonSelectUrlGenerator(): UrlGeneratorInterface&MockObject
|
||||
{
|
||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$urlGenerator->expects($this->once())
|
||||
->method('generate')
|
||||
->with('tvdt_quiz_select_season')
|
||||
->willReturn('/');
|
||||
|
||||
return $urlGenerator;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user