From 414679f15bc338195fcf55ee9e99027a956b46a9 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Tue, 19 May 2026 20:01:25 +0300 Subject: [PATCH 1/3] add projectile file for emacs --- backend/.projectile | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/.projectile diff --git a/backend/.projectile b/backend/.projectile new file mode 100644 index 0000000..e69de29 From 56b528999eae42f926ac07c2c2cddfffcd690c61 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Wed, 20 May 2026 10:22:10 +0300 Subject: [PATCH 2/3] update fake token generator to take a preset number of tokens --- backend/tests/Fakes/FakeTokenGenerator.php | 18 +++++++++++++++++- .../Unit/Auth/UseCases/CreateSessionTest.php | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/backend/tests/Fakes/FakeTokenGenerator.php b/backend/tests/Fakes/FakeTokenGenerator.php index dcb8819..e10bbcf 100644 --- a/backend/tests/Fakes/FakeTokenGenerator.php +++ b/backend/tests/Fakes/FakeTokenGenerator.php @@ -3,11 +3,27 @@ namespace Tests\Fakes; use App\Auth\TokenGenerator; +use RuntimeException; class FakeTokenGenerator implements TokenGenerator { + private int $callCount = 0; + + /** + * @param string[] $tokens + */ + public function __construct(private array $tokens) {} + public function generate(): string { - return 'fake-token-123'; + if ($this->callCount >= count($this->tokens)) { + throw new RuntimeException( + 'FakeTokenGenerator exhausted' + ); + } + $token = $this->tokens[$this->callCount]; + $this->callCount++; + + return $token; } } diff --git a/backend/tests/Unit/Auth/UseCases/CreateSessionTest.php b/backend/tests/Unit/Auth/UseCases/CreateSessionTest.php index 0367f1c..034d7c5 100644 --- a/backend/tests/Unit/Auth/UseCases/CreateSessionTest.php +++ b/backend/tests/Unit/Auth/UseCases/CreateSessionTest.php @@ -22,7 +22,7 @@ class CreateSessionTest extends TestCase protected function setUp(): void { $this->sessionRepo = new FakeSessionRepository(); - $this->tokenGenerator = new FakeTokenGenerator(); + $this->tokenGenerator = new FakeTokenGenerator(['fake-token-123']); $this->clock = new FakeClock( new DateTimeImmutable('2026-05-18 12:00:00') ); From 9e70fae38d31a1cb6a8cf6ea7337af0fb20ed7cc Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Wed, 20 May 2026 10:22:52 +0300 Subject: [PATCH 3/3] test auth controller login, logout, and me methods --- backend/app/Controllers/AuthController.php | 105 ++++++++++ .../Unit/Controllers/AuthControllerTest.php | 183 ++++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 backend/app/Controllers/AuthController.php create mode 100644 backend/tests/Unit/Controllers/AuthControllerTest.php diff --git a/backend/app/Controllers/AuthController.php b/backend/app/Controllers/AuthController.php new file mode 100644 index 0000000..841b4c4 --- /dev/null +++ b/backend/app/Controllers/AuthController.php @@ -0,0 +1,105 @@ +authenticateUser->execute( + new AuthenticateUserRequest( + email: $request->input('email'), + password: $request->input('password'), + ) + ); + } catch (BadRequestException $exception) { + return new JsonResponse( + ['error' => $exception->getMessage()], 400 + ); + } catch (UnauthorizedException $exception) { + return new JsonResponse( + ['error' => $exception->getMessage()], 401 + ); + } + + $session = $this->createSession->execute($user); + + $response = new JsonResponse([ + 'user' => $this->buildUserPayload($user), + ], 200); + + return $response->withCookie(Cookie::create( + name: AuthMiddleware::COOKIE_NAME, + value: $session->getToken(), + expire: $session->getExpiresAt()->getTimestamp(), + path: '/', + domain: null, + secure: false, + httpOnly: true, + raw: false, + sameSite: Cookie::SAMESITE_LAX, + )); + } + + public function me(Request $request): JsonResponse + { + /** @var User $user */ + $user = $request->attributes->get('user'); + + return new JsonResponse([ + 'user' => $this->buildUserPayload($user), + ], 200); + } + + /** + * @return array{id: int, email: string, firstname: string, lastname: string} + */ + private function buildUserPayload(User $user): array + { + return [ + 'id' => $user->getId(), + 'email' => $user->getEmail()->value(), + ]; + } + + public function logout(Request $request): JsonResponse + { + $token = $request->cookie(AuthMiddleware::COOKIE_NAME); + if (is_string($token) && $token !== '') { + $this->logout->execute($token); + } + + $response = new JsonResponse(null, 204); + + return $response->withCookie(Cookie::create( + name: AuthMiddleware::COOKIE_NAME, + value: '', + expire: 1, + path: '/', + domain: null, + secure: false, + httpOnly: true, + raw: false, + sameSite: Cookie::SAMESITE_LAX, + )); + } +} diff --git a/backend/tests/Unit/Controllers/AuthControllerTest.php b/backend/tests/Unit/Controllers/AuthControllerTest.php new file mode 100644 index 0000000..1855c59 --- /dev/null +++ b/backend/tests/Unit/Controllers/AuthControllerTest.php @@ -0,0 +1,183 @@ +now = new DateTimeImmutable( + '2026-04-29T12:00:00', + new DateTimeZone('UTC') + ); + $this->clock = new FakeClock($this->now); + $this->tokenGenerator = new FakeTokenGenerator(['session-token-1']); + $this->userRepo = new FakeUserRepository(); + $this->hasher = new FakeHasher(); + $this->sessionRepo = new FakeSessionRepository(); + $authenticateUser = new AuthenticateUser( + $this->userRepo, + $this->hasher, + ); + $createSession = new CreateSession( + $this->sessionRepo, + $this->tokenGenerator, + $this->clock, + ); + $logout = new Logout($this->sessionRepo); + $this->controller = new AuthController( + $authenticateUser, + $createSession, + $logout, + ); + } + + private function seedStartupUser(string $email, string $password): void + { + $user = $this->userRepo->create( + new CreateUserDto( + email: new EmailAddress($email), + passwordHash: 'hashed-password', + ) + ); + } + + public function test_login_returns_200_and_sets_cookie_on_success(): void + { + $email = 'user@example.com'; + $password = 'password'; + $this->seedStartupUser($email, $password); + + $request = new Request([ + 'email' => $email, + 'password' => $password, + ]); + $response = $this->controller->login($request); + + $this->assertEquals(200, $response->getStatusCode()); + $body = json_decode($response->getContent(), true); + $this->assertSame($email, $body['user']['email']); + + $cookies = $response->headers->getCookies(); + $this->assertCount(1, $cookies); + $cookie = $cookies[0]; + $this->assertSame( + AuthMiddleware::COOKIE_NAME, + $cookie->getName() + ); + $this->assertSame('session-token-1', $cookie->getValue()); + $this->assertTrue($cookie->isHttpOnly()); + $this->assertSame('lax', $cookie->getSameSite()); + $this->assertNotNull( + $this->sessionRepo->findByToken('session-token-1') + ); + } + + public function test_login_returns_400_when_email_missing(): void + { + $request = new Request(['password' => 'correctpassword']); + $response = $this->controller->login($request); + $this->assertEquals(400, $response->getStatusCode()); + } + + public function test_login_returns_400_when_password_missing(): void + { + $request = new Request(['email' => 'user@example.com']); + $response = $this->controller->login($request); + $this->assertEquals(400, $response->getStatusCode()); + } + + public function test_login_returns_401_when_credentials_invalid(): void + { + $this->seedStartupUser('user@example.com', 'correctpassword'); + + $request = new Request([ + 'email' => 'user@example.com', + 'password' => 'wrongpassword', + ]); + $response = $this->controller->login($request); + $this->assertEquals(401, $response->getStatusCode()); + } + + public function test_logout_returns_204_and_clears_cookie(): void + { + $this->seedStartupUser('user@example.com', 'correctpassword'); + $loginRequest = new Request([ + 'email' => 'user@example.com', + 'password' => 'correctpassword', + ]); + $this->controller->login($loginRequest); + + $logoutRequest = new Request; + $logoutRequest->cookies->set( + AuthMiddleware::COOKIE_NAME, + 'session-token-1' + ); + $response = $this->controller->logout($logoutRequest); + + $this->assertEquals(204, $response->getStatusCode()); + $this->assertNull( + $this->sessionRepo->findByToken('session-token-1') + ); + + $cookies = $response->headers->getCookies(); + $this->assertCount(1, $cookies); + $this->assertSame( + AuthMiddleware::COOKIE_NAME, + $cookies[0]->getName() + ); + $this->assertSame('', $cookies[0]->getValue()); + } + + public function test_me_returns_200_with_user_when_authenticated(): void + { + $email = 'me@example.com'; + $user = $this->userRepo->create( + new CreateUserDto( + email: new EmailAddress($email), + passwordHash: 'password' + ) + ); + + $request = new Request; + $request->attributes->set('user', $user); + + $response = $this->controller->me($request); + + $this->assertEquals(200, $response->getStatusCode()); + $body = json_decode($response->getContent(), true); + $this->assertSame($user->getId(), $body['user']['id']); + $this->assertSame($email, $body['user']['email']); + } +}