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.'); } }