diff --git a/backend/tests/Unit/User/UseCases/ConfirmUserEmailTest.php b/backend/tests/Unit/User/UseCases/ConfirmUserEmailTest.php new file mode 100644 index 0000000..2263902 --- /dev/null +++ b/backend/tests/Unit/User/UseCases/ConfirmUserEmailTest.php @@ -0,0 +1,182 @@ +now = new DateTimeImmutable( + '2026-05-06T12:00:00', + new DateTimeZone('UTC'), + ); + $this->userRepo = new FakeUserRepository; + $this->tokenRepo = new FakeEmailConfirmationTokenRepository( + $this->userRepo, + ); + $this->hasher = new FakePasswordHasher; + $this->clock = new FakeClock($this->now); + $this->useCase = new ConfirmUserEmail( + $this->userRepo, + $this->tokenRepo, + $this->hasher, + $this->clock, + ); + } + + private function seedUserAndToken(DateTimeImmutable $availableTo): User + { + $user = $this->userRepo->create(new CreateUserDto( + email: new EmailAddress('user@example.com'), + displayName: 'user', + passwordHash: '', + isAdmin: false, + emailConfirmedAt: null, + )); + $this->tokenRepo->create(new CreateEmailConfirmationTokenDto( + user: $user, + availableTo: $availableTo, + )); + + return $user; + } + + private function tokenStringForUser(User $user): string + { + $token = $this->tokenRepo->findByUser($user); + $this->assertNotNull($token); + + return $token->getToken(); + } + + public function test_null_token_throws_bad_request(): void + { + $this->expectException(BadRequestException::class); + $this->useCase->execute(new ConfirmUserEmailRequest( + token: null, + password: 'longenoughpassword', + )); + } + + public function test_null_password_throws_bad_request(): void + { + $this->expectException(BadRequestException::class); + $this->useCase->execute(new ConfirmUserEmailRequest( + token: 'sometoken', + password: null, + )); + } + + public function test_short_password_throws_bad_request(): void + { + $this->expectException(BadRequestException::class); + $this->useCase->execute(new ConfirmUserEmailRequest( + token: 'sometoken', + password: 'short', + )); + } + + public function test_unknown_token_throws_domain_exception(): void + { + $this->expectException(DomainException::class); + $this->useCase->execute(new ConfirmUserEmailRequest( + token: 'no-such-token', + password: 'longenoughpassword', + )); + } + + public function test_expired_token_throws_domain_exception(): void + { + $user = $this->seedUserAndToken( + $this->now->modify('-1 minute'), + ); + $tokenStr = $this->tokenStringForUser($user); + + $this->expectException(DomainException::class); + $this->useCase->execute(new ConfirmUserEmailRequest( + token: $tokenStr, + password: 'longenoughpassword', + )); + } + + public function test_valid_confirmation_sets_password_hash(): void + { + $user = $this->seedUserAndToken( + $this->now->modify('+1 day'), + ); + $tokenStr = $this->tokenStringForUser($user); + + $this->useCase->execute(new ConfirmUserEmailRequest( + token: $tokenStr, + password: 'longenoughpassword', + )); + + $reloaded = $this->userRepo->find($user->getId()); + $this->assertNotNull($reloaded); + $this->assertSame( + $this->hasher->hash('longenoughpassword'), + $reloaded->getPasswordHash(), + ); + } + + public function test_valid_confirmation_marks_email_confirmed(): void + { + $user = $this->seedUserAndToken( + $this->now->modify('+1 day'), + ); + $tokenStr = $this->tokenStringForUser($user); + + $this->useCase->execute(new ConfirmUserEmailRequest( + token: $tokenStr, + password: 'longenoughpassword', + )); + + $reloaded = $this->userRepo->find($user->getId()); + $this->assertNotNull($reloaded); + $this->assertTrue($reloaded->isEmailConfirmed()); + $this->assertEquals($this->now, $reloaded->getEmailConfirmedAt()); + } + + public function test_valid_confirmation_consumes_token(): void + { + $user = $this->seedUserAndToken( + $this->now->modify('+1 day'), + ); + $tokenStr = $this->tokenStringForUser($user); + + $this->useCase->execute(new ConfirmUserEmailRequest( + token: $tokenStr, + password: 'longenoughpassword', + )); + + $this->assertNull($this->tokenRepo->findByToken($tokenStr)); + } +}