test password login
This commit is contained in:
parent
879c294582
commit
93f5f022e4
6 changed files with 220 additions and 0 deletions
|
|
@ -101,6 +101,7 @@ class AuthMiddlewareTest extends TestCase
|
||||||
): User {
|
): User {
|
||||||
$user = app(UserRepository::class)->create(new CreateUserDto(
|
$user = app(UserRepository::class)->create(new CreateUserDto(
|
||||||
email: new EmailAddress('user@example.com'),
|
email: new EmailAddress('user@example.com'),
|
||||||
|
password: 'correct-password',
|
||||||
));
|
));
|
||||||
app(SessionRepository::class)->create(new CreateSessionDto(
|
app(SessionRepository::class)->create(new CreateSessionDto(
|
||||||
token: $token,
|
token: $token,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ class EloquentSessionRepositoryTest extends TestCase
|
||||||
{
|
{
|
||||||
$user = app(UserRepository::class)->create(new CreateUserDto(
|
$user = app(UserRepository::class)->create(new CreateUserDto(
|
||||||
email: new EmailAddress('user@example.com'),
|
email: new EmailAddress('user@example.com'),
|
||||||
|
password: 'correct-password',
|
||||||
));
|
));
|
||||||
$createdAt = $this->utc('2026-07-31T12:00:00');
|
$createdAt = $this->utc('2026-07-31T12:00:00');
|
||||||
$expiresAt = $this->utc('2026-08-07T12: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(
|
$user = app(UserRepository::class)->create(new CreateUserDto(
|
||||||
email: new EmailAddress('user@example.com'),
|
email: new EmailAddress('user@example.com'),
|
||||||
|
password: 'correct-password',
|
||||||
));
|
));
|
||||||
$repository = app(SessionRepository::class);
|
$repository = app(SessionRepository::class);
|
||||||
$repository->create(new CreateSessionDto(
|
$repository->create(new CreateSessionDto(
|
||||||
|
|
|
||||||
163
backend/tests/Feature/Auth/LoginEndpointTest.php
Normal file
163
backend/tests/Feature/Auth/LoginEndpointTest.php
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Auth;
|
||||||
|
|
||||||
|
use App\Auth\Clock;
|
||||||
|
use App\Auth\SessionRepository;
|
||||||
|
use App\Http\Middleware\AuthMiddleware;
|
||||||
|
use App\Shared\ValueObject\EmailAddress;
|
||||||
|
use App\User\CreateUserDto;
|
||||||
|
use App\User\User;
|
||||||
|
use App\User\UserRepository;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeZone;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Symfony\Component\HttpFoundation\Cookie;
|
||||||
|
use Tests\Fakes\FakeClock;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class LoginEndpointTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
private DateTimeImmutable $currentTime;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->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<int, Cookie> $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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,7 @@ class MeEndpointTest extends TestCase
|
||||||
);
|
);
|
||||||
$user = app(UserRepository::class)->create(new CreateUserDto(
|
$user = app(UserRepository::class)->create(new CreateUserDto(
|
||||||
email: new EmailAddress('user@example.com'),
|
email: new EmailAddress('user@example.com'),
|
||||||
|
password: 'correct-password',
|
||||||
));
|
));
|
||||||
app(SessionRepository::class)->create(new CreateSessionDto(
|
app(SessionRepository::class)->create(new CreateSessionDto(
|
||||||
token: 'valid-token',
|
token: 'valid-token',
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace Tests\Feature\Database;
|
namespace Tests\Feature\Database;
|
||||||
|
|
||||||
|
use App\Shared\ValueObject\EmailAddress;
|
||||||
|
use App\User\UserRepository;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
|
@ -17,6 +19,17 @@ class DatabaseSeederTest extends TestCase
|
||||||
$this->assertDatabaseHas('users', [
|
$this->assertDatabaseHas('users', [
|
||||||
'email' => 'user@example.com',
|
'email' => 'user@example.com',
|
||||||
]);
|
]);
|
||||||
|
$this->assertDatabaseMissing('users', [
|
||||||
|
'email' => 'user@example.com',
|
||||||
|
'password' => 'password',
|
||||||
|
]);
|
||||||
$this->assertDatabaseCount('users', 1);
|
$this->assertDatabaseCount('users', 1);
|
||||||
|
|
||||||
|
$user = app(UserRepository::class)->findByCredentials(
|
||||||
|
new EmailAddress('user@example.com'),
|
||||||
|
'password',
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertNotNull($user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ class EloquentUserRepositoryTest extends TestCase
|
||||||
$repository = app(UserRepository::class);
|
$repository = app(UserRepository::class);
|
||||||
$user = $repository->create(new CreateUserDto(
|
$user = $repository->create(new CreateUserDto(
|
||||||
email: new EmailAddress('Founder@EXAMPLE.COM'),
|
email: new EmailAddress('Founder@EXAMPLE.COM'),
|
||||||
|
password: 'correct-password',
|
||||||
));
|
));
|
||||||
|
|
||||||
$this->assertGreaterThan(0, $user->getId());
|
$this->assertGreaterThan(0, $user->getId());
|
||||||
|
|
@ -28,6 +29,10 @@ class EloquentUserRepositoryTest extends TestCase
|
||||||
'id' => $user->getId(),
|
'id' => $user->getId(),
|
||||||
'email' => 'Founder@example.com',
|
'email' => 'Founder@example.com',
|
||||||
]);
|
]);
|
||||||
|
$this->assertDatabaseMissing('users', [
|
||||||
|
'id' => $user->getId(),
|
||||||
|
'password' => 'correct-password',
|
||||||
|
]);
|
||||||
|
|
||||||
$foundUser = $repository->find($user->getId());
|
$foundUser = $repository->find($user->getId());
|
||||||
|
|
||||||
|
|
@ -45,4 +50,39 @@ class EloquentUserRepositoryTest extends TestCase
|
||||||
|
|
||||||
$this->assertNull($repository->find(999));
|
$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',
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue