em = self::getContainer()->get(EntityManagerInterface::class); // Build the hasher directly — avoids the chicken-and-egg where // UserPasswordHasherInterface gets compiled away before User exists. $factory = new PasswordHasherFactory([ User::class => new NativePasswordHasher(cost: 4), ]); $this->hasher = new UserPasswordHasher($factory); } public function testItPersistsAUserWithUuidIdentifierAndHashedPassword(): void { $hasher = $this->hasher; $user = new User(); $user->setEmail('alice@example.com'); $user->setPassword($hasher->hashPassword($user, 'secret123')); $this->em->persist($user); $this->em->flush(); $id = $user->getId(); self::assertInstanceOf(UuidV7::class, $id); $this->em->clear(); $reloaded = $this->em->find(User::class, $id); self::assertNotNull($reloaded, 'User must be retrievable from the database after an identity-map clear.'); self::assertInstanceOf(User::class, $reloaded); self::assertSame('alice@example.com', $reloaded->getEmail()); self::assertTrue( $hasher->isPasswordValid($reloaded, 'secret123'), 'Stored password hash must verify against the original plain-text password.', ); self::assertStringNotContainsString( 'secret123', $reloaded->getPassword(), 'Plain-text password must never be stored.', ); } public function testEmailMustBeUnique(): void { $hasher = $this->hasher; $first = new User(); $first->setEmail('bob@example.com'); $first->setPassword($hasher->hashPassword($first, 'pass1')); $this->em->persist($first); $this->em->flush(); $this->em->clear(); $second = new User(); $second->setEmail('bob@example.com'); $second->setPassword($hasher->hashPassword($second, 'pass2')); $this->em->persist($second); $this->expectException(UniqueConstraintViolationException::class); $this->em->flush(); } }