now = new DateTimeImmutable( '2026-08-03T12:00:00', new DateTimeZone('UTC'), ); $this->userRepository = new FakeUserRepository; $this->tokenRepository = new FakeEmailConfirmationTokenRepository; $this->confirmUserEmail = new ConfirmUserEmail( $this->tokenRepository, $this->userRepository, new FakePasswordHasher, new FakeClock($this->now), ); } public function test_it_sets_the_password_and_consumes_the_token(): void { $this->createPendingUserToken( 'confirmation-token', $this->now->modify('+10 minutes'), ); $confirmedUser = $this->confirmUserEmail->execute( new ConfirmUserEmailRequest( token: 'confirmation-token', password: 'password123', ), ); $this->assertSame('hashed:password123', $confirmedUser->getPasswordHash()); $this->assertSame( 'hashed:password123', $this->userRepository->find($confirmedUser->getId()) ?->getPasswordHash(), ); $this->assertNull( $this->tokenRepository->findByToken('confirmation-token'), ); } public function test_it_rejects_an_expired_token(): void { $this->createPendingUserToken( 'expired-token', $this->now->modify('-1 minute'), ); $this->expectException(DomainException::class); $this->expectExceptionMessage('token expired'); $this->confirmUserEmail->execute(new ConfirmUserEmailRequest( token: 'expired-token', password: 'password123', )); } public function test_it_rejects_an_unknown_token(): void { $this->expectException(DomainException::class); $this->expectExceptionMessage('token not found'); $this->confirmUserEmail->execute(new ConfirmUserEmailRequest( token: 'unknown-token', password: 'password123', )); } public function test_it_requires_a_token(): void { $this->expectException(BadRequestException::class); $this->expectExceptionMessage('token is required'); $this->confirmUserEmail->execute(new ConfirmUserEmailRequest( token: null, password: 'password123', )); } public function test_it_requires_a_password(): void { $this->expectException(BadRequestException::class); $this->expectExceptionMessage('password is required'); $this->confirmUserEmail->execute(new ConfirmUserEmailRequest( token: 'confirmation-token', password: null, )); } public function test_it_rejects_a_short_password(): void { $this->expectException(BadRequestException::class); $this->expectExceptionMessage( 'password must be at least 8 characters', ); $this->confirmUserEmail->execute(new ConfirmUserEmailRequest( token: 'confirmation-token', password: 'short', )); } private function createPendingUserToken( string $token, DateTimeImmutable $availableTo, ): void { $user = $this->userRepository->create(new CreateUserDto( email: new EmailAddress('user@example.com'), passwordHash: null, )); $this->tokenRepository->create( new CreateEmailConfirmationTokenDto( user: $user, availableTo: $availableTo, token: $token, ), ); } }