use FakePasswordHasher in tests to eliminate bcrypt cost

Add a trivial prefix-based PasswordHasher fake and inject it into the
three test files that exercise CreateUser or AuthenticateUser. Drops
the full phpunit suite from ~7.4s to ~30ms (about 224x) without
losing coverage: the round-trip through hash/verify still validates
that CreateUser stores something other than the plaintext and that
AuthenticateUser only succeeds on a matching hash.

CreateUserTest is also refactored to use a setUp method, matching
the pattern already used in AuthenticateUserTest and AuthControllerTest.
This commit is contained in:
Yisroel Baum 2026-04-26 09:06:26 +03:00
parent 632085f5b6
commit bb6bd7cbb3
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
4 changed files with 76 additions and 39 deletions

View file

@ -0,0 +1,20 @@
<?php
namespace Tests\Fakes;
use App\Auth\PasswordHasher;
class FakePasswordHasher implements PasswordHasher
{
private const PREFIX = 'hashed:';
public function hash(string $plaintext): string
{
return self::PREFIX . $plaintext;
}
public function verify(string $plaintext, string $hash): bool
{
return $hash === self::PREFIX . $plaintext;
}
}

View file

@ -10,22 +10,31 @@ use App\User\UseCases\CreateUser;
use App\User\UseCases\CreateUserRequest; use App\User\UseCases\CreateUserRequest;
use App\User\User; use App\User\User;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Tests\Fakes\FakePasswordHasher;
use Tests\Fakes\FakeUserRepository; use Tests\Fakes\FakeUserRepository;
class AuthenticateUserTest extends TestCase class AuthenticateUserTest extends TestCase
{ {
private FakeUserRepository $userRepo; private FakeUserRepository $userRepo;
private FakePasswordHasher $passwordHasher;
private AuthenticateUser $useCase; private AuthenticateUser $useCase;
public function setUp(): void public function setUp(): void
{ {
$this->userRepo = new FakeUserRepository(); $this->userRepo = new FakeUserRepository();
$createUser = new CreateUser($this->userRepo); $this->passwordHasher = new FakePasswordHasher();
$createUser = new CreateUser(
$this->userRepo,
$this->passwordHasher,
);
$createUser->execute(new CreateUserRequest( $createUser->execute(new CreateUserRequest(
email: 'test@test.com', email: 'test@test.com',
password: 'password1', password: 'password1',
)); ));
$this->useCase = new AuthenticateUser($this->userRepo); $this->useCase = new AuthenticateUser(
$this->userRepo,
$this->passwordHasher,
);
} }
public function test_returns_user_on_valid_credentials(): void public function test_returns_user_on_valid_credentials(): void

View file

@ -6,67 +6,71 @@ use App\Exceptions\BadRequestException;
use App\User\User; use App\User\User;
use App\User\UseCases\CreateUser; use App\User\UseCases\CreateUser;
use App\User\UseCases\CreateUserRequest; use App\User\UseCases\CreateUserRequest;
use Tests\Fakes\FakePasswordHasher;
use Tests\Fakes\FakeUserRepository; use Tests\Fakes\FakeUserRepository;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
class CreateUserTest extends TestCase class CreateUserTest extends TestCase
{ {
private FakeUserRepository $userRepo;
private FakePasswordHasher $passwordHasher;
private CreateUser $useCase;
public function setUp(): void
{
$this->userRepo = new FakeUserRepository();
$this->passwordHasher = new FakePasswordHasher();
$this->useCase = new CreateUser(
$this->userRepo,
$this->passwordHasher,
);
}
public function test_create_user(): void public function test_create_user(): void
{ {
$userRepo = new FakeUserRepository(); $this->useCase->execute(new CreateUserRequest(
$useCase = new CreateUser($userRepo);
$useCase->execute(new CreateUserRequest(
email: 'test@test.com', email: 'test@test.com',
password: 'password1', password: 'password1',
)); ));
$user = $userRepo->find(0); $user = $this->userRepo->find(0);
$this->assertInstanceOf(User::class, $user); $this->assertInstanceOf(User::class, $user);
$this->assertEquals('test@test.com', $user->getEmail()); $this->assertEquals('test@test.com', $user->getEmail());
} }
public function test_throws_if_email_is_null(): void public function test_throws_if_email_is_null(): void
{ {
$userRepo = new FakeUserRepository();
$useCase = new CreateUser($userRepo);
$this->expectException(BadRequestException::class); $this->expectException(BadRequestException::class);
$this->expectExceptionMessage('email is required'); $this->expectExceptionMessage('email is required');
$useCase->execute(new CreateUserRequest( $this->useCase->execute(new CreateUserRequest(
email: null, email: null,
)); ));
} }
public function test_is_admin_defaults_to_false(): void public function test_is_admin_defaults_to_false(): void
{ {
$userRepo = new FakeUserRepository(); $this->useCase->execute(new CreateUserRequest(
$useCase = new CreateUser($userRepo);
$useCase->execute(new CreateUserRequest(
email: 'test@test.com', email: 'test@test.com',
password: 'password1', password: 'password1',
)); ));
$user = $userRepo->find(0); $user = $this->userRepo->find(0);
$this->assertFalse($user->isAdmin()); $this->assertFalse($user->isAdmin());
} }
public function test_is_admin_can_be_set_true(): void public function test_is_admin_can_be_set_true(): void
{ {
$userRepo = new FakeUserRepository(); $this->useCase->execute(new CreateUserRequest(
$useCase = new CreateUser($userRepo);
$useCase->execute(new CreateUserRequest(
email: 'test@test.com', email: 'test@test.com',
password: 'password1', password: 'password1',
isAdmin: true, isAdmin: true,
)); ));
$user = $userRepo->find(0); $user = $this->userRepo->find(0);
$this->assertTrue($user->isAdmin()); $this->assertTrue($user->isAdmin());
} }
public function test_throws_when_email_already_taken(): void public function test_throws_when_email_already_taken(): void
{ {
$userRepo = new FakeUserRepository(); $this->useCase->execute(new CreateUserRequest(
$useCase = new CreateUser($userRepo);
$useCase->execute(new CreateUserRequest(
email: 'test@test.com', email: 'test@test.com',
password: 'password1', password: 'password1',
)); ));
@ -74,7 +78,7 @@ class CreateUserTest extends TestCase
$this->expectException(BadRequestException::class); $this->expectException(BadRequestException::class);
$this->expectExceptionMessage('email already taken'); $this->expectExceptionMessage('email already taken');
$useCase->execute(new CreateUserRequest( $this->useCase->execute(new CreateUserRequest(
email: 'test@test.com', email: 'test@test.com',
password: 'password1', password: 'password1',
)); ));
@ -82,13 +86,10 @@ class CreateUserTest extends TestCase
public function test_throws_if_password_is_null(): void public function test_throws_if_password_is_null(): void
{ {
$userRepo = new FakeUserRepository();
$useCase = new CreateUser($userRepo);
$this->expectException(BadRequestException::class); $this->expectException(BadRequestException::class);
$this->expectExceptionMessage('password is required'); $this->expectExceptionMessage('password is required');
$useCase->execute(new CreateUserRequest( $this->useCase->execute(new CreateUserRequest(
email: 'test@test.com', email: 'test@test.com',
password: null, password: null,
)); ));
@ -96,15 +97,12 @@ class CreateUserTest extends TestCase
public function test_throws_if_password_too_short(): void public function test_throws_if_password_too_short(): void
{ {
$userRepo = new FakeUserRepository();
$useCase = new CreateUser($userRepo);
$this->expectException(BadRequestException::class); $this->expectException(BadRequestException::class);
$this->expectExceptionMessage( $this->expectExceptionMessage(
'password must be at least 8 characters' 'password must be at least 8 characters'
); );
$useCase->execute(new CreateUserRequest( $this->useCase->execute(new CreateUserRequest(
email: 'test@test.com', email: 'test@test.com',
password: 'short', password: 'short',
)); ));
@ -112,16 +110,17 @@ class CreateUserTest extends TestCase
public function test_stores_hashed_password(): void public function test_stores_hashed_password(): void
{ {
$userRepo = new FakeUserRepository(); $this->useCase->execute(new CreateUserRequest(
$useCase = new CreateUser($userRepo);
$useCase->execute(new CreateUserRequest(
email: 'test@test.com', email: 'test@test.com',
password: 'password1', password: 'password1',
)); ));
$user = $userRepo->find(0); $user = $this->userRepo->find(0);
$this->assertNotEquals('password1', $user->getPasswordHash()); $this->assertNotEquals('password1', $user->getPasswordHash());
$this->assertTrue( $this->assertTrue(
password_verify('password1', $user->getPasswordHash()) $this->passwordHasher->verify(
'password1',
$user->getPasswordHash()
)
); );
} }
} }

View file

@ -3,7 +3,6 @@
namespace Tests\e2e\Controllers; namespace Tests\e2e\Controllers;
use App\Auth\AuthController; use App\Auth\AuthController;
use App\Auth\AuthMiddleware;
use App\Auth\CreateSessionDto; use App\Auth\CreateSessionDto;
use App\Auth\UseCases\CreateSession; use App\Auth\UseCases\CreateSession;
use App\User\UseCases\AuthenticateUser; use App\User\UseCases\AuthenticateUser;
@ -18,6 +17,7 @@ use Slim\Psr7\Factory\ServerRequestFactory;
use Slim\Psr7\Factory\StreamFactory; use Slim\Psr7\Factory\StreamFactory;
use Slim\Psr7\Response; use Slim\Psr7\Response;
use Tests\Fakes\FakeClock; use Tests\Fakes\FakeClock;
use Tests\Fakes\FakePasswordHasher;
use Tests\Fakes\FakeSessionRepository; use Tests\Fakes\FakeSessionRepository;
use Tests\Fakes\FakeTokenGenerator; use Tests\Fakes\FakeTokenGenerator;
use Tests\Fakes\FakeUserRepository; use Tests\Fakes\FakeUserRepository;
@ -28,6 +28,7 @@ class AuthControllerTest extends TestCase
private FakeSessionRepository $sessionRepo; private FakeSessionRepository $sessionRepo;
private FakeTokenGenerator $tokenGenerator; private FakeTokenGenerator $tokenGenerator;
private FakeClock $clock; private FakeClock $clock;
private FakePasswordHasher $passwordHasher;
private CreateUser $createUser; private CreateUser $createUser;
private AuthenticateUser $authenticateUser; private AuthenticateUser $authenticateUser;
private CreateSession $createSession; private CreateSession $createSession;
@ -43,9 +44,16 @@ class AuthControllerTest extends TestCase
$this->clock = new FakeClock( $this->clock = new FakeClock(
new DateTimeImmutable('2025-01-01T12:00:00+00:00') new DateTimeImmutable('2025-01-01T12:00:00+00:00')
); );
$this->passwordHasher = new FakePasswordHasher();
$this->createUser = new CreateUser($this->userRepo); $this->createUser = new CreateUser(
$this->authenticateUser = new AuthenticateUser($this->userRepo); $this->userRepo,
$this->passwordHasher,
);
$this->authenticateUser = new AuthenticateUser(
$this->userRepo,
$this->passwordHasher,
);
$this->createSession = new CreateSession( $this->createSession = new CreateSession(
$this->sessionRepo, $this->sessionRepo,
$this->tokenGenerator, $this->tokenGenerator,
@ -63,7 +71,7 @@ class AuthControllerTest extends TestCase
private function makeJsonRequest( private function makeJsonRequest(
string $method, string $method,
string $path, string $path,
array $data = [], array $data,
): ServerRequestInterface { ): ServerRequestInterface {
$body = new StreamFactory()->createStream(json_encode($data)); $body = new StreamFactory()->createStream(json_encode($data));
return new ServerRequestFactory() return new ServerRequestFactory()
@ -243,6 +251,7 @@ class AuthControllerTest extends TestCase
$request = $this->makeJsonRequest( $request = $this->makeJsonRequest(
'POST', 'POST',
'/api/auth/logout', '/api/auth/logout',
[]
)->withCookieParams(['auth_token' => 'existing-session']); )->withCookieParams(['auth_token' => 'existing-session']);
$response = $this->controller->logout( $response = $this->controller->logout(