test auth contracts
This commit is contained in:
parent
3da9c586c3
commit
b3266b38c8
15 changed files with 457 additions and 165 deletions
|
|
@ -2,162 +2,46 @@
|
|||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Auth\Clock;
|
||||
use App\Auth\PasswordHasher;
|
||||
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
|
||||
public function test_login_returns_user_and_sets_session_cookie(): 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();
|
||||
$email = 'user@example.com';
|
||||
$password = 'correct-password';
|
||||
$passwordHash = app(PasswordHasher::class)->hash($password);
|
||||
app(UserRepository::class)->create(new CreateUserDto(
|
||||
email: new EmailAddress($email),
|
||||
passwordHash: $passwordHash,
|
||||
));
|
||||
|
||||
$response = $this->postJson('/api/login', [
|
||||
'email' => ' user@EXAMPLE.COM ',
|
||||
'password' => 'correct-password',
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
$response->assertOk()->assertExactJson([
|
||||
'user' => [
|
||||
'id' => $user->getId(),
|
||||
'email' => 'user@example.com',
|
||||
],
|
||||
]);
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('user.email', $email);
|
||||
|
||||
$cookie = $this->findAuthCookie(
|
||||
$response->headers->getCookies(),
|
||||
$cookie = $response->getCookie(
|
||||
AuthMiddleware::COOKIE_NAME,
|
||||
false,
|
||||
);
|
||||
$token = $cookie->getValue();
|
||||
|
||||
$this->assertMatchesRegularExpression(
|
||||
'/^[a-f0-9]{64}$/',
|
||||
$token,
|
||||
$this->assertNotNull($cookie);
|
||||
$this->assertNotNull(
|
||||
app(SessionRepository::class)->findByToken(
|
||||
$cookie->getValue(),
|
||||
),
|
||||
);
|
||||
$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.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue