test auth middleware

This commit is contained in:
Yisroel Baum 2026-07-31 09:53:12 +03:00
parent 34e26f81f5
commit 19d930b7e8
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
4 changed files with 325 additions and 0 deletions

View file

@ -0,0 +1,18 @@
<?php
namespace Tests\Fakes;
use App\Auth\Clock;
use DateTimeImmutable;
class FakeClock implements Clock
{
public function __construct(
private DateTimeImmutable $currentTime,
) {}
public function now(): DateTimeImmutable
{
return $this->currentTime;
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace Tests\Fakes;
use App\Auth\CreateSessionDto;
use App\Auth\Session;
use App\Auth\SessionRepository;
class FakeSessionRepository implements SessionRepository
{
/**
* @var array<string, Session>
*/
private array $sessions = [];
public function create(CreateSessionDto $dto): Session
{
$session = new Session(
token: $dto->token,
user: $dto->user,
createdAt: $dto->createdAt,
expiresAt: $dto->expiresAt,
);
$this->sessions[$dto->token] = $session;
return $session;
}
public function findByToken(string $token): ?Session
{
return $this->sessions[$token] ?? null;
}
public function deleteByToken(string $token): void
{
unset($this->sessions[$token]);
}
}

View file

@ -0,0 +1,110 @@
<?php
namespace Tests\Feature\Auth;
use App\Auth\CreateSessionDto;
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 Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Tests\TestCase;
class AuthMiddlewareTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Route::middleware(AuthMiddleware::class)->get(
'/test/authenticated-user',
function (Request $request): JsonResponse {
$user = $request->attributes->get('user');
if (! $user instanceof User) {
return new JsonResponse(['error' => 'missing user'], 500);
}
return new JsonResponse([
'id' => $user->getId(),
'email' => $user->getEmail()->value(),
]);
},
);
}
public function test_valid_cookie_reaches_the_protected_route(): void
{
$user = $this->createUserAndSession(
token: 'valid-token',
expiresAt: $this->utc('2030-08-07T12:00:00'),
);
$response = $this->withUnencryptedCookie(
AuthMiddleware::COOKIE_NAME,
'valid-token',
)->getJson('/test/authenticated-user');
$response->assertOk()->assertExactJson([
'id' => $user->getId(),
'email' => 'user@example.com',
]);
}
public function test_missing_cookie_is_rejected(): void
{
$response = $this->getJson('/test/authenticated-user');
$response
->assertStatus(401)
->assertExactJson(['error' => 'unauthenticated']);
}
public function test_expired_cookie_is_rejected_and_deleted(): void
{
$this->createUserAndSession(
token: 'expired-token',
expiresAt: $this->utc('2020-08-07T12:00:00'),
);
$response = $this->withUnencryptedCookie(
AuthMiddleware::COOKIE_NAME,
'expired-token',
)->getJson('/test/authenticated-user');
$response->assertStatus(401);
$this->assertNull(
app(SessionRepository::class)->findByToken('expired-token'),
);
}
private function createUserAndSession(
string $token,
DateTimeImmutable $expiresAt,
): User {
$user = app(UserRepository::class)->create(new CreateUserDto(
email: new EmailAddress('user@example.com'),
));
app(SessionRepository::class)->create(new CreateSessionDto(
token: $token,
user: $user,
createdAt: $this->utc('2026-07-31T12:00:00'),
expiresAt: $expiresAt,
));
return $user;
}
private function utc(string $time): DateTimeImmutable
{
return new DateTimeImmutable($time, new DateTimeZone('UTC'));
}
}

View file

@ -0,0 +1,159 @@
<?php
namespace Tests\Unit\Auth\Middleware;
use App\Auth\CreateSessionDto;
use App\Http\Middleware\AuthMiddleware;
use App\Shared\ValueObject\EmailAddress;
use App\User\User;
use Closure;
use DateTimeImmutable;
use DateTimeZone;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use PHPUnit\Framework\TestCase;
use Tests\Fakes\FakeClock;
use Tests\Fakes\FakeSessionRepository;
class AuthMiddlewareTest extends TestCase
{
private FakeSessionRepository $sessionRepository;
private DateTimeImmutable $now;
private AuthMiddleware $middleware;
protected function setUp(): void
{
$this->now = new DateTimeImmutable(
'2026-07-31T12:00:00',
new DateTimeZone('UTC'),
);
$this->sessionRepository = new FakeSessionRepository;
$this->middleware = new AuthMiddleware(
sessionRepository: $this->sessionRepository,
clock: new FakeClock($this->now),
);
}
public function test_missing_cookie_returns_unauthenticated(): void
{
$capturedRequest = null;
$response = $this->middleware->handle(
$this->requestWithToken(null),
$this->captureNextRequest($capturedRequest),
);
$this->assertSame(401, $response->getStatusCode());
$this->assertSame(
['error' => 'unauthenticated'],
json_decode($response->getContent(), true),
);
$this->assertNull($capturedRequest);
}
public function test_empty_cookie_returns_unauthenticated(): void
{
$capturedRequest = null;
$response = $this->middleware->handle(
$this->requestWithToken(''),
$this->captureNextRequest($capturedRequest),
);
$this->assertSame(401, $response->getStatusCode());
$this->assertNull($capturedRequest);
}
public function test_unknown_token_returns_unauthenticated(): void
{
$capturedRequest = null;
$response = $this->middleware->handle(
$this->requestWithToken('unknown-token'),
$this->captureNextRequest($capturedRequest),
);
$this->assertSame(401, $response->getStatusCode());
$this->assertNull($capturedRequest);
}
public function test_expired_session_is_deleted(): void
{
$this->sessionRepository->create(new CreateSessionDto(
token: 'expired-token',
user: $this->user(),
createdAt: $this->now->modify('-8 days'),
expiresAt: $this->now->modify('-1 day'),
));
$capturedRequest = null;
$response = $this->middleware->handle(
$this->requestWithToken('expired-token'),
$this->captureNextRequest($capturedRequest),
);
$this->assertSame(401, $response->getStatusCode());
$this->assertNull($capturedRequest);
$this->assertNull(
$this->sessionRepository->findByToken('expired-token'),
);
}
public function test_valid_session_attaches_user_and_calls_next(): void
{
$user = $this->user();
$this->sessionRepository->create(new CreateSessionDto(
token: 'valid-token',
user: $user,
createdAt: $this->now,
expiresAt: $this->now->modify('+7 days'),
));
$capturedRequest = null;
$response = $this->middleware->handle(
$this->requestWithToken('valid-token'),
$this->captureNextRequest($capturedRequest),
);
$this->assertSame(200, $response->getStatusCode());
$this->assertNotNull($capturedRequest);
$this->assertSame(
$user,
$capturedRequest->attributes->get('user'),
);
}
private function requestWithToken(?string $token): Request
{
$request = Request::create('/anything', 'GET');
if ($token !== null) {
$request->cookies->set(AuthMiddleware::COOKIE_NAME, $token);
}
return $request;
}
/**
* @param Request|null $capturedRequest
* @return Closure(Request): JsonResponse
*/
private function captureNextRequest(
?Request &$capturedRequest,
): Closure {
return function (Request $request) use (&$capturedRequest) {
$capturedRequest = $request;
return new JsonResponse(['ok' => true]);
};
}
private function user(): User
{
return new User(
id: 7,
email: new EmailAddress('user@example.com'),
);
}
}