From bb6bd7cbb31950cc3c10b99231c5077672f96bfc Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 26 Apr 2026 09:06:26 +0300 Subject: [PATCH] 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. --- tests/Fakes/FakePasswordHasher.php | 20 ++++++ .../User/UseCases/AuthenticateUserTest.php | 13 +++- tests/Unit/User/UseCases/CreateUserTest.php | 65 +++++++++---------- tests/e2e/Controllers/AuthControllerTest.php | 17 +++-- 4 files changed, 76 insertions(+), 39 deletions(-) create mode 100644 tests/Fakes/FakePasswordHasher.php diff --git a/tests/Fakes/FakePasswordHasher.php b/tests/Fakes/FakePasswordHasher.php new file mode 100644 index 0000000..4e9a17d --- /dev/null +++ b/tests/Fakes/FakePasswordHasher.php @@ -0,0 +1,20 @@ +userRepo = new FakeUserRepository(); - $createUser = new CreateUser($this->userRepo); + $this->passwordHasher = new FakePasswordHasher(); + $createUser = new CreateUser( + $this->userRepo, + $this->passwordHasher, + ); $createUser->execute(new CreateUserRequest( email: 'test@test.com', 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 diff --git a/tests/Unit/User/UseCases/CreateUserTest.php b/tests/Unit/User/UseCases/CreateUserTest.php index 93971c4..941c98d 100644 --- a/tests/Unit/User/UseCases/CreateUserTest.php +++ b/tests/Unit/User/UseCases/CreateUserTest.php @@ -6,67 +6,71 @@ use App\Exceptions\BadRequestException; use App\User\User; use App\User\UseCases\CreateUser; use App\User\UseCases\CreateUserRequest; +use Tests\Fakes\FakePasswordHasher; use Tests\Fakes\FakeUserRepository; use PHPUnit\Framework\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 { - $userRepo = new FakeUserRepository(); - $useCase = new CreateUser($userRepo); - $useCase->execute(new CreateUserRequest( + $this->useCase->execute(new CreateUserRequest( email: 'test@test.com', password: 'password1', )); - $user = $userRepo->find(0); + $user = $this->userRepo->find(0); $this->assertInstanceOf(User::class, $user); $this->assertEquals('test@test.com', $user->getEmail()); } public function test_throws_if_email_is_null(): void { - $userRepo = new FakeUserRepository(); - $useCase = new CreateUser($userRepo); - $this->expectException(BadRequestException::class); $this->expectExceptionMessage('email is required'); - $useCase->execute(new CreateUserRequest( + $this->useCase->execute(new CreateUserRequest( email: null, )); } public function test_is_admin_defaults_to_false(): void { - $userRepo = new FakeUserRepository(); - $useCase = new CreateUser($userRepo); - $useCase->execute(new CreateUserRequest( + $this->useCase->execute(new CreateUserRequest( email: 'test@test.com', password: 'password1', )); - $user = $userRepo->find(0); + $user = $this->userRepo->find(0); $this->assertFalse($user->isAdmin()); } public function test_is_admin_can_be_set_true(): void { - $userRepo = new FakeUserRepository(); - $useCase = new CreateUser($userRepo); - $useCase->execute(new CreateUserRequest( + $this->useCase->execute(new CreateUserRequest( email: 'test@test.com', password: 'password1', isAdmin: true, )); - $user = $userRepo->find(0); + $user = $this->userRepo->find(0); $this->assertTrue($user->isAdmin()); } public function test_throws_when_email_already_taken(): void { - $userRepo = new FakeUserRepository(); - $useCase = new CreateUser($userRepo); - $useCase->execute(new CreateUserRequest( + $this->useCase->execute(new CreateUserRequest( email: 'test@test.com', password: 'password1', )); @@ -74,7 +78,7 @@ class CreateUserTest extends TestCase $this->expectException(BadRequestException::class); $this->expectExceptionMessage('email already taken'); - $useCase->execute(new CreateUserRequest( + $this->useCase->execute(new CreateUserRequest( email: 'test@test.com', password: 'password1', )); @@ -82,13 +86,10 @@ class CreateUserTest extends TestCase public function test_throws_if_password_is_null(): void { - $userRepo = new FakeUserRepository(); - $useCase = new CreateUser($userRepo); - $this->expectException(BadRequestException::class); $this->expectExceptionMessage('password is required'); - $useCase->execute(new CreateUserRequest( + $this->useCase->execute(new CreateUserRequest( email: 'test@test.com', password: null, )); @@ -96,15 +97,12 @@ class CreateUserTest extends TestCase public function test_throws_if_password_too_short(): void { - $userRepo = new FakeUserRepository(); - $useCase = new CreateUser($userRepo); - $this->expectException(BadRequestException::class); $this->expectExceptionMessage( 'password must be at least 8 characters' ); - $useCase->execute(new CreateUserRequest( + $this->useCase->execute(new CreateUserRequest( email: 'test@test.com', password: 'short', )); @@ -112,16 +110,17 @@ class CreateUserTest extends TestCase public function test_stores_hashed_password(): void { - $userRepo = new FakeUserRepository(); - $useCase = new CreateUser($userRepo); - $useCase->execute(new CreateUserRequest( + $this->useCase->execute(new CreateUserRequest( email: 'test@test.com', password: 'password1', )); - $user = $userRepo->find(0); + $user = $this->userRepo->find(0); $this->assertNotEquals('password1', $user->getPasswordHash()); $this->assertTrue( - password_verify('password1', $user->getPasswordHash()) + $this->passwordHasher->verify( + 'password1', + $user->getPasswordHash() + ) ); } } diff --git a/tests/e2e/Controllers/AuthControllerTest.php b/tests/e2e/Controllers/AuthControllerTest.php index d7bc415..58b209d 100644 --- a/tests/e2e/Controllers/AuthControllerTest.php +++ b/tests/e2e/Controllers/AuthControllerTest.php @@ -3,7 +3,6 @@ namespace Tests\e2e\Controllers; use App\Auth\AuthController; -use App\Auth\AuthMiddleware; use App\Auth\CreateSessionDto; use App\Auth\UseCases\CreateSession; use App\User\UseCases\AuthenticateUser; @@ -18,6 +17,7 @@ use Slim\Psr7\Factory\ServerRequestFactory; use Slim\Psr7\Factory\StreamFactory; use Slim\Psr7\Response; use Tests\Fakes\FakeClock; +use Tests\Fakes\FakePasswordHasher; use Tests\Fakes\FakeSessionRepository; use Tests\Fakes\FakeTokenGenerator; use Tests\Fakes\FakeUserRepository; @@ -28,6 +28,7 @@ class AuthControllerTest extends TestCase private FakeSessionRepository $sessionRepo; private FakeTokenGenerator $tokenGenerator; private FakeClock $clock; + private FakePasswordHasher $passwordHasher; private CreateUser $createUser; private AuthenticateUser $authenticateUser; private CreateSession $createSession; @@ -43,9 +44,16 @@ class AuthControllerTest extends TestCase $this->clock = new FakeClock( new DateTimeImmutable('2025-01-01T12:00:00+00:00') ); + $this->passwordHasher = new FakePasswordHasher(); - $this->createUser = new CreateUser($this->userRepo); - $this->authenticateUser = new AuthenticateUser($this->userRepo); + $this->createUser = new CreateUser( + $this->userRepo, + $this->passwordHasher, + ); + $this->authenticateUser = new AuthenticateUser( + $this->userRepo, + $this->passwordHasher, + ); $this->createSession = new CreateSession( $this->sessionRepo, $this->tokenGenerator, @@ -63,7 +71,7 @@ class AuthControllerTest extends TestCase private function makeJsonRequest( string $method, string $path, - array $data = [], + array $data, ): ServerRequestInterface { $body = new StreamFactory()->createStream(json_encode($data)); return new ServerRequestFactory() @@ -243,6 +251,7 @@ class AuthControllerTest extends TestCase $request = $this->makeJsonRequest( 'POST', '/api/auth/logout', + [] )->withCookieParams(['auth_token' => 'existing-session']); $response = $this->controller->logout(