From 93f5f022e46ae4a85df342b0881bdb5a96609053 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Fri, 31 Jul 2026 11:34:50 +0300 Subject: [PATCH] test password login --- .../tests/Feature/Auth/AuthMiddlewareTest.php | 1 + .../Auth/EloquentSessionRepositoryTest.php | 2 + .../tests/Feature/Auth/LoginEndpointTest.php | 163 ++++++++++++++++++ backend/tests/Feature/Auth/MeEndpointTest.php | 1 + .../Feature/Database/DatabaseSeederTest.php | 13 ++ .../User/EloquentUserRepositoryTest.php | 40 +++++ 6 files changed, 220 insertions(+) create mode 100644 backend/tests/Feature/Auth/LoginEndpointTest.php diff --git a/backend/tests/Feature/Auth/AuthMiddlewareTest.php b/backend/tests/Feature/Auth/AuthMiddlewareTest.php index 8d84a1d..e9b6d25 100644 --- a/backend/tests/Feature/Auth/AuthMiddlewareTest.php +++ b/backend/tests/Feature/Auth/AuthMiddlewareTest.php @@ -101,6 +101,7 @@ class AuthMiddlewareTest extends TestCase ): User { $user = app(UserRepository::class)->create(new CreateUserDto( email: new EmailAddress('user@example.com'), + password: 'correct-password', )); app(SessionRepository::class)->create(new CreateSessionDto( token: $token, diff --git a/backend/tests/Feature/Auth/EloquentSessionRepositoryTest.php b/backend/tests/Feature/Auth/EloquentSessionRepositoryTest.php index 32d170d..0ce4b5f 100644 --- a/backend/tests/Feature/Auth/EloquentSessionRepositoryTest.php +++ b/backend/tests/Feature/Auth/EloquentSessionRepositoryTest.php @@ -20,6 +20,7 @@ class EloquentSessionRepositoryTest extends TestCase { $user = app(UserRepository::class)->create(new CreateUserDto( email: new EmailAddress('user@example.com'), + password: 'correct-password', )); $createdAt = $this->utc('2026-07-31T12:00:00'); $expiresAt = $this->utc('2026-08-07T12:00:00'); @@ -61,6 +62,7 @@ class EloquentSessionRepositoryTest extends TestCase { $user = app(UserRepository::class)->create(new CreateUserDto( email: new EmailAddress('user@example.com'), + password: 'correct-password', )); $repository = app(SessionRepository::class); $repository->create(new CreateSessionDto( diff --git a/backend/tests/Feature/Auth/LoginEndpointTest.php b/backend/tests/Feature/Auth/LoginEndpointTest.php new file mode 100644 index 0000000..ba01a77 --- /dev/null +++ b/backend/tests/Feature/Auth/LoginEndpointTest.php @@ -0,0 +1,163 @@ +currentTime = new DateTimeImmutable( + '2026-07-31T12:00:00', + new DateTimeZone('UTC'), + ); + $this->app->instance( + Clock::class, + new FakeClock($this->currentTime), + ); + config()->set('session.lifetime', 120); + config()->set('session.path', '/'); + config()->set('session.secure', true); + config()->set('session.same_site', 'lax'); + } + + public function test_login_validates_its_request(): void + { + $this->postJson('/api/login') + ->assertUnprocessable() + ->assertJsonValidationErrors(['email', 'password']); + + $this->postJson('/api/login', [ + 'email' => 'invalid-email', + 'password' => 'password', + ])->assertUnprocessable() + ->assertJsonValidationErrors(['email']); + } + + public function test_login_rejects_invalid_credentials_generically(): void + { + $this->createUser(); + + $this->postJson('/api/login', [ + 'email' => 'user@example.com', + 'password' => 'wrong-password', + ])->assertUnauthorized() + ->assertExactJson(['error' => 'invalid_credentials']) + ->assertCookieMissing(AuthMiddleware::COOKIE_NAME); + + $this->postJson('/api/login', [ + 'email' => 'unknown@example.com', + 'password' => 'correct-password', + ])->assertUnauthorized() + ->assertExactJson(['error' => 'invalid_credentials']) + ->assertCookieMissing(AuthMiddleware::COOKIE_NAME); + + $this->assertDatabaseCount('sessions', 0); + } + + public function test_login_creates_a_session_and_returns_the_user(): void + { + $user = $this->createUser(); + + $response = $this->postJson('/api/login', [ + 'email' => ' user@EXAMPLE.COM ', + 'password' => 'correct-password', + ]); + + $response->assertOk()->assertExactJson([ + 'user' => [ + 'id' => $user->getId(), + 'email' => 'user@example.com', + ], + ]); + + $cookie = $this->findAuthCookie( + $response->headers->getCookies(), + ); + $token = $cookie->getValue(); + + $this->assertMatchesRegularExpression( + '/^[a-f0-9]{64}$/', + $token, + ); + $this->assertTrue($cookie->isHttpOnly()); + $this->assertTrue($cookie->isSecure()); + $this->assertSame('/', $cookie->getPath()); + $this->assertSame('lax', $cookie->getSameSite()); + $this->assertSame( + $this->currentTime->modify('+120 minutes')->getTimestamp(), + $cookie->getExpiresTime(), + ); + + $session = app(SessionRepository::class)->findByToken($token); + + $this->assertNotNull($session); + $this->assertSame($user->getId(), $session->getUser()->getId()); + $this->assertEquals( + $this->currentTime, + $session->getCreatedAt(), + ); + $this->assertEquals( + $this->currentTime->modify('+120 minutes'), + $session->getExpiresAt(), + ); + } + + public function test_login_throttles_repeated_attempts(): void + { + $this->createUser(); + + for ($attempt = 1; $attempt <= 5; $attempt++) { + $this->postJson('/api/login', [ + 'email' => 'user@example.com', + 'password' => 'wrong-password', + ])->assertUnauthorized(); + } + + $this->postJson('/api/login', [ + 'email' => 'user@example.com', + 'password' => 'wrong-password', + ])->assertStatus(429); + } + + private function createUser(): User + { + return app(UserRepository::class)->create(new CreateUserDto( + email: new EmailAddress('user@example.com'), + password: 'correct-password', + )); + } + + /** + * @param array $cookies + */ + private function findAuthCookie(array $cookies): Cookie + { + foreach ($cookies as $cookie) { + if ($cookie->getName() === AuthMiddleware::COOKIE_NAME) { + return $cookie; + } + } + + $this->fail('The authentication cookie was not set.'); + } +} diff --git a/backend/tests/Feature/Auth/MeEndpointTest.php b/backend/tests/Feature/Auth/MeEndpointTest.php index ec19fc3..988fd85 100644 --- a/backend/tests/Feature/Auth/MeEndpointTest.php +++ b/backend/tests/Feature/Auth/MeEndpointTest.php @@ -25,6 +25,7 @@ class MeEndpointTest extends TestCase ); $user = app(UserRepository::class)->create(new CreateUserDto( email: new EmailAddress('user@example.com'), + password: 'correct-password', )); app(SessionRepository::class)->create(new CreateSessionDto( token: 'valid-token', diff --git a/backend/tests/Feature/Database/DatabaseSeederTest.php b/backend/tests/Feature/Database/DatabaseSeederTest.php index 26b6b06..361021d 100644 --- a/backend/tests/Feature/Database/DatabaseSeederTest.php +++ b/backend/tests/Feature/Database/DatabaseSeederTest.php @@ -2,6 +2,8 @@ namespace Tests\Feature\Database; +use App\Shared\ValueObject\EmailAddress; +use App\User\UserRepository; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -17,6 +19,17 @@ class DatabaseSeederTest extends TestCase $this->assertDatabaseHas('users', [ 'email' => 'user@example.com', ]); + $this->assertDatabaseMissing('users', [ + 'email' => 'user@example.com', + 'password' => 'password', + ]); $this->assertDatabaseCount('users', 1); + + $user = app(UserRepository::class)->findByCredentials( + new EmailAddress('user@example.com'), + 'password', + ); + + $this->assertNotNull($user); } } diff --git a/backend/tests/Feature/User/EloquentUserRepositoryTest.php b/backend/tests/Feature/User/EloquentUserRepositoryTest.php index 207523f..0d26dbb 100644 --- a/backend/tests/Feature/User/EloquentUserRepositoryTest.php +++ b/backend/tests/Feature/User/EloquentUserRepositoryTest.php @@ -17,6 +17,7 @@ class EloquentUserRepositoryTest extends TestCase $repository = app(UserRepository::class); $user = $repository->create(new CreateUserDto( email: new EmailAddress('Founder@EXAMPLE.COM'), + password: 'correct-password', )); $this->assertGreaterThan(0, $user->getId()); @@ -28,6 +29,10 @@ class EloquentUserRepositoryTest extends TestCase 'id' => $user->getId(), 'email' => 'Founder@example.com', ]); + $this->assertDatabaseMissing('users', [ + 'id' => $user->getId(), + 'password' => 'correct-password', + ]); $foundUser = $repository->find($user->getId()); @@ -45,4 +50,39 @@ class EloquentUserRepositoryTest extends TestCase $this->assertNull($repository->find(999)); } + + public function test_it_finds_a_user_with_matching_credentials(): void + { + $repository = app(UserRepository::class); + $createdUser = $repository->create(new CreateUserDto( + email: new EmailAddress('user@example.com'), + password: 'correct-password', + )); + + $foundUser = $repository->findByCredentials( + new EmailAddress('user@EXAMPLE.COM'), + 'correct-password', + ); + + $this->assertNotNull($foundUser); + $this->assertSame($createdUser->getId(), $foundUser->getId()); + } + + public function test_it_rejects_non_matching_credentials(): void + { + $repository = app(UserRepository::class); + $repository->create(new CreateUserDto( + email: new EmailAddress('user@example.com'), + password: 'correct-password', + )); + + $this->assertNull($repository->findByCredentials( + new EmailAddress('user@example.com'), + 'wrong-password', + )); + $this->assertNull($repository->findByCredentials( + new EmailAddress('unknown@example.com'), + 'correct-password', + )); + } }