diff --git a/backend/tests/Fakes/FakeClock.php b/backend/tests/Fakes/FakeClock.php index f32d5a4..f112836 100644 --- a/backend/tests/Fakes/FakeClock.php +++ b/backend/tests/Fakes/FakeClock.php @@ -4,12 +4,18 @@ namespace Tests\Fakes; use App\Auth\Clock; use DateTimeImmutable; -use DateTimeZone; class FakeClock implements Clock { + public function __construct(private DateTimeImmutable $currentTime) {} + public function now(): DateTimeImmutable { - return new DateTimeImmutable('2026-05-18 12:00:00', new DateTimeZone('UTC')); + return $this->currentTime; + } + + public function setTime(DateTimeImmutable $newTime): void + { + $this->currentTime = $newTime; } } diff --git a/backend/tests/Fakes/FakeSessionRepository.php b/backend/tests/Fakes/FakeSessionRepository.php index 120ccd8..c1733b0 100644 --- a/backend/tests/Fakes/FakeSessionRepository.php +++ b/backend/tests/Fakes/FakeSessionRepository.php @@ -14,10 +14,10 @@ class FakeSessionRepository implements SessionRepository public function create(CreateSessionDto $dto): Session { $session = new Session( - $dto->token, - $dto->user, - $dto->createdAt, - $dto->expiresAt + token: $dto->token, + user: $dto->user, + createdAt: $dto->createdAt, + expiresAt: $dto->expiresAt, ); $this->sessionsByToken[$dto->token] = $session; @@ -26,17 +26,16 @@ class FakeSessionRepository implements SessionRepository public function findByToken(string $token): ?Session { - if (! isset($this->sessionsByToken[$token])) { + $session = $this->sessionsByToken[$token] ?? null; + if ($session === null) { return null; } - $stored = $this->sessionsByToken[$token]; - return new Session( - $stored->getToken(), - $stored->getUser(), - $stored->getCreatedAt(), - $stored->getExpiresAt() + token: $session->getToken(), + user: $session->getUser(), + createdAt: $session->getCreatedAt(), + expiresAt: $session->getExpiresAt(), ); } diff --git a/backend/tests/Unit/Auth/Middleware/AuthMiddlewareTest.php b/backend/tests/Unit/Auth/Middleware/AuthMiddlewareTest.php new file mode 100644 index 0000000..45aaf3d --- /dev/null +++ b/backend/tests/Unit/Auth/Middleware/AuthMiddlewareTest.php @@ -0,0 +1,142 @@ +now = new DateTimeImmutable( + '2026-04-29T12:00:00', + new DateTimeZone('UTC') + ); + $this->sessionRepo = new FakeSessionRepository; + $this->clock = new FakeClock($this->now); + $this->middleware = new AuthMiddleware( + $this->sessionRepo, + $this->clock, + ); + } + + private function makeRequest(?string $token): Request + { + $request = Request::create('/api/anything', 'POST'); + if ($token !== null) { + $request->cookies->set('auth_token', $token); + } + + return $request; + } + + private function nextThatRecords(?Request &$captured): Closure + { + return function (Request $request) use (&$captured) { + $captured = $request; + + return new JsonResponse(['ok' => true], 200); + }; + } + + public function test_missing_cookie_returns_unauthorized_json(): void + { + $captured = null; + $response = $this->middleware->handle( + $this->makeRequest(null), + $this->nextThatRecords($captured), + ); + + $this->assertSame(401, $response->getStatusCode()); + $this->assertSame( + ['error' => 'unauthenticated'], + json_decode($response->getContent(), true), + ); + $this->assertNull($captured); + } + + public function test_unknown_token_returns_unauthorized(): void + { + $captured = null; + $response = $this->middleware->handle( + $this->makeRequest('does-not-exist'), + $this->nextThatRecords($captured), + ); + + $this->assertSame(401, $response->getStatusCode()); + $this->assertNull($captured); + } + + public function test_expired_session_returns_unauthorized_and_is_deleted(): void + { + $user = new User( + id: 7, + email: new EmailAddress('user@example.com'), + passwordHash: 'password', + ); + $this->sessionRepo->create(new CreateSessionDto( + token: 'expired-token', + user: $user, + createdAt: $this->now->modify('-8 days'), + expiresAt: $this->now->modify('-1 day'), + )); + + $captured = null; + $response = $this->middleware->handle( + $this->makeRequest('expired-token'), + $this->nextThatRecords($captured), + ); + + $this->assertSame(401, $response->getStatusCode()); + $this->assertNull($captured); + $this->assertNull( + $this->sessionRepo->findByToken('expired-token') + ); + } + + public function test_valid_session_attaches_user_and_calls_next(): void + { + $user = new User( + id: 7, + email: new EmailAddress('user@example.com'), + passwordHash: 'password', + ); + $this->sessionRepo->create(new CreateSessionDto( + token: 'valid-token', + user: $user, + createdAt: $this->now, + expiresAt: $this->now->modify('+7 days'), + )); + + $captured = null; + $response = $this->middleware->handle( + $this->makeRequest('valid-token'), + $this->nextThatRecords($captured), + ); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertNotNull($captured); + $attachedUser = $captured->attributes->get('user'); + $this->assertInstanceOf(User::class, $attachedUser); + $this->assertSame(7, $attachedUser->getId()); + } +} diff --git a/backend/tests/Unit/Auth/UseCases/CreateSessionTest.php b/backend/tests/Unit/Auth/UseCases/CreateSessionTest.php index d6368f2..0367f1c 100644 --- a/backend/tests/Unit/Auth/UseCases/CreateSessionTest.php +++ b/backend/tests/Unit/Auth/UseCases/CreateSessionTest.php @@ -6,10 +6,11 @@ use App\Auth\Clock; use App\Auth\UseCases\CreateSession\CreateSession; use App\Shared\ValueObject\EmailAddress; use App\User\User; -use PHPUnit\Framework\TestCase; +use DateTimeImmutable; use Tests\Fakes\FakeClock; use Tests\Fakes\FakeSessionRepository; use Tests\Fakes\FakeTokenGenerator; +use Tests\TestCase; class CreateSessionTest extends TestCase { @@ -22,9 +23,15 @@ class CreateSessionTest extends TestCase { $this->sessionRepo = new FakeSessionRepository(); $this->tokenGenerator = new FakeTokenGenerator(); - $this->clock = new FakeClock(); + $this->clock = new FakeClock( + new DateTimeImmutable('2026-05-18 12:00:00') + ); - $this->createSession = new CreateSession($this->sessionRepo, $this->tokenGenerator, $this->clock); + $this->createSession = new CreateSession( + $this->sessionRepo, + $this->tokenGenerator, + $this->clock + ); } public function testCreatesSessionForUser(): void diff --git a/backend/tests/Unit/Auth/UseCases/LogoutTest.php b/backend/tests/Unit/Auth/UseCases/LogoutTest.php index 2b28630..5dda63f 100644 --- a/backend/tests/Unit/Auth/UseCases/LogoutTest.php +++ b/backend/tests/Unit/Auth/UseCases/LogoutTest.php @@ -6,41 +6,51 @@ use App\Auth\CreateSessionDto; use App\Auth\UseCases\Logout\Logout; use App\Shared\ValueObject\EmailAddress; use App\User\User; -use PHPUnit\Framework\TestCase; +use DateTimeImmutable; +use DateTimeZone; use Tests\Fakes\FakeSessionRepository; +use Tests\TestCase; class LogoutTest extends TestCase { private FakeSessionRepository $sessionRepo; - private Logout $logout; + + private Logout $useCase; + + private DateTimeImmutable $now; protected function setUp(): void { - $this->sessionRepo = new FakeSessionRepository(); - $this->logout = new Logout($this->sessionRepo); + $this->now = new DateTimeImmutable( + '2026-04-29T12:00:00', + new DateTimeZone('UTC') + ); + $this->sessionRepo = new FakeSessionRepository; + $this->useCase = new Logout($this->sessionRepo); } - public function testDeletesSessionByToken(): void + public function test_existing_token_session_is_removed(): void { - $email = new EmailAddress('user@example.com'); - $user = new User(1, $email, 'hashed-password'); - - $session = $this->sessionRepo->create(new CreateSessionDto( - 'session-token', - $user, - new \DateTimeImmutable(), - new \DateTimeImmutable('+1 hour') + $this->sessionRepo->create(new CreateSessionDto( + token: 'token-abc', + user: new User( + id: 7, + email: new EmailAddress('a@b.com'), + passwordHash: 'password', + ), + createdAt: $this->now, + expiresAt: $this->now->modify('+7 days'), )); - $this->logout->execute('session-token'); + $this->useCase->execute('token-abc'); - $this->assertNull($this->sessionRepo->findByToken('session-token')); + $this->assertNull($this->sessionRepo->findByToken('token-abc')); } - public function testDeletesMissingTokenIsIdempotent(): void + public function test_unknown_token_does_not_throw(): void { - $this->logout->execute('nonexistent-token'); + $this->useCase->execute('unknown-token'); - $this->assertNull($this->sessionRepo->findByToken('nonexistent-token')); + $this->assertNull($this->sessionRepo->findByToken('unknown-token')); } }