diff --git a/backend/tests/Feature/Auth/LogoutEndpointTest.php b/backend/tests/Feature/Auth/LogoutEndpointTest.php new file mode 100644 index 0000000..3986cb9 --- /dev/null +++ b/backend/tests/Feature/Auth/LogoutEndpointTest.php @@ -0,0 +1,58 @@ +create(new CreateUserDto( + email: new EmailAddress('user@example.com'), + passwordHash: 'hashed-password', + )); + app(SessionRepository::class)->create(new CreateSessionDto( + token: 'session-token', + user: $user, + createdAt: $now, + expiresAt: $now->modify('+7 days'), + )); + + $response = $this->withCredentials() + ->withUnencryptedCookie( + AuthMiddleware::COOKIE_NAME, + 'session-token', + )->postJson('/api/logout'); + + $response->assertNoContent(); + $response->assertCookieExpired(AuthMiddleware::COOKIE_NAME); + $this->assertNull( + app(SessionRepository::class)->findByToken('session-token'), + ); + } + + public function test_logout_rejects_a_request_without_a_cookie(): void + { + $response = $this->postJson('/api/logout'); + + $response + ->assertStatus(401) + ->assertExactJson(['error' => 'unauthenticated']); + } +} diff --git a/backend/tests/Unit/Auth/UseCases/LogoutTest.php b/backend/tests/Unit/Auth/UseCases/LogoutTest.php new file mode 100644 index 0000000..5accde6 --- /dev/null +++ b/backend/tests/Unit/Auth/UseCases/LogoutTest.php @@ -0,0 +1,58 @@ +sessionRepository = new FakeSessionRepository; + $this->useCase = new Logout($this->sessionRepository); + } + + public function test_existing_token_session_is_removed(): void + { + $now = new DateTimeImmutable( + '2026-07-31T12:00:00', + new DateTimeZone('UTC'), + ); + $this->sessionRepository->create(new CreateSessionDto( + token: 'session-token', + user: new User( + id: 7, + email: new EmailAddress('user@example.com'), + passwordHash: 'hashed-password', + ), + createdAt: $now, + expiresAt: $now->modify('+7 days'), + )); + + $this->useCase->execute('session-token'); + + $this->assertNull( + $this->sessionRepository->findByToken('session-token'), + ); + } + + public function test_unknown_token_does_not_throw(): void + { + $this->useCase->execute('unknown-token'); + + $this->assertNull( + $this->sessionRepository->findByToken('unknown-token'), + ); + } +} diff --git a/backend/tests/Unit/Http/Controllers/AuthControllerTest.php b/backend/tests/Unit/Http/Controllers/AuthControllerTest.php index cded75c..5372272 100644 --- a/backend/tests/Unit/Http/Controllers/AuthControllerTest.php +++ b/backend/tests/Unit/Http/Controllers/AuthControllerTest.php @@ -4,6 +4,7 @@ namespace Tests\Unit\Http\Controllers; use App\Auth\UseCases\AuthenticateUser\AuthenticateUser; use App\Auth\UseCases\CreateSession\CreateSession; +use App\Auth\UseCases\Logout\Logout; use App\Http\Controllers\AuthController; use App\Http\Middleware\AuthMiddleware; use App\Shared\ValueObject\EmailAddress; @@ -45,9 +46,11 @@ class AuthControllerTest extends TestCase new DateTimeZone('UTC'), )), ); + $logout = new Logout($this->sessionRepository); $this->controller = new AuthController( $authenticateUser, $createSession, + $logout, ); } @@ -117,6 +120,37 @@ class AuthControllerTest extends TestCase ); } + public function test_logout_deletes_session_and_clears_cookie(): void + { + $this->createUser('correct-password'); + $this->controller->login(new Request([ + 'email' => 'user@example.com', + 'password' => 'correct-password', + ])); + $request = new Request; + $request->cookies->set( + AuthMiddleware::COOKIE_NAME, + 'session-token', + ); + + $response = $this->controller->logout($request); + + $this->assertSame(204, $response->getStatusCode()); + $this->assertNull( + $this->sessionRepository->findByToken('session-token'), + ); + $cookies = $response->headers->getCookies(); + $this->assertCount(1, $cookies); + $this->assertSame( + AuthMiddleware::COOKIE_NAME, + $cookies[0]->getName(), + ); + $this->assertSame('', $cookies[0]->getValue()); + $this->assertSame(1, $cookies[0]->getExpiresTime()); + $this->assertTrue($cookies[0]->isHttpOnly()); + $this->assertSame('lax', $cookies[0]->getSameSite()); + } + private function createUser(string $password): void { $this->userRepository->create(new CreateUserDto(