createMock(Connection::class); $connection->expects(self::once()) ->method('executeStatement') ->with( self::stringContains('INSERT INTO users'), self::callback(static function (array $params): bool { return $params['email'] === 'test@example.com' && $params['statut'] === 'pending' && $params['school_name'] === 'École Test' && str_contains($params['roles'], 'ROLE_PARENT'); }), ); $repository = new DoctrineUserRepository($connection); $user = User::creer( email: new Email('test@example.com'), role: Role::PARENT, tenantId: TenantId::fromString(self::TENANT_ALPHA_ID), schoolName: 'École Test', dateNaissance: null, createdAt: new DateTimeImmutable('2026-01-15T10:00:00+00:00'), ); $repository->save($user); } #[Test] public function findByIdReturnsUserWhenFound(): void { $userId = '550e8400-e29b-41d4-a716-446655440001'; $connection = $this->createMock(Connection::class); $connection->method('fetchAssociative') ->willReturn($this->makeRow($userId)); $repository = new DoctrineUserRepository($connection); $user = $repository->findById(UserId::fromString($userId)); self::assertNotNull($user); self::assertSame($userId, (string) $user->id); self::assertSame('test@example.com', (string) $user->email); self::assertSame(Role::PARENT, $user->role); self::assertSame('École Test', $user->schoolName); } #[Test] public function findByIdReturnsNullWhenNotFound(): void { $connection = $this->createMock(Connection::class); $connection->method('fetchAssociative')->willReturn(false); $repository = new DoctrineUserRepository($connection); $user = $repository->findById(UserId::fromString('550e8400-e29b-41d4-a716-446655440001')); self::assertNull($user); } #[Test] public function getThrowsWhenUserNotFound(): void { $connection = $this->createMock(Connection::class); $connection->method('fetchAssociative')->willReturn(false); $repository = new DoctrineUserRepository($connection); $this->expectException(UserNotFoundException::class); $repository->get(UserId::fromString('550e8400-e29b-41d4-a716-446655440001')); } #[Test] public function findByEmailReturnsUserWhenFound(): void { $userId = '550e8400-e29b-41d4-a716-446655440001'; $tenantId = TenantId::fromString(self::TENANT_ALPHA_ID); $connection = $this->createMock(Connection::class); $connection->method('fetchAssociative') ->with( self::stringContains('tenant_id = :tenant_id AND email = :email'), self::callback(static fn (array $params) => $params['email'] === 'test@example.com' && $params['tenant_id'] === self::TENANT_ALPHA_ID), ) ->willReturn($this->makeRow($userId)); $repository = new DoctrineUserRepository($connection); $user = $repository->findByEmail(new Email('test@example.com'), $tenantId); self::assertNotNull($user); self::assertSame($userId, (string) $user->id); } #[Test] public function findByEmailReturnsNullForDifferentTenant(): void { $connection = $this->createMock(Connection::class); $connection->method('fetchAssociative')->willReturn(false); $repository = new DoctrineUserRepository($connection); $user = $repository->findByEmail( new Email('test@example.com'), TenantId::fromString(self::TENANT_BETA_ID), ); self::assertNull($user); } #[Test] public function findAllByTenantReturnsUsersForTenant(): void { $connection = $this->createMock(Connection::class); $connection->method('fetchAllAssociative') ->willReturn([ $this->makeRow('550e8400-e29b-41d4-a716-446655440001'), $this->makeRow('550e8400-e29b-41d4-a716-446655440002', 'other@example.com'), ]); $repository = new DoctrineUserRepository($connection); $users = $repository->findAllByTenant(TenantId::fromString(self::TENANT_ALPHA_ID)); self::assertCount(2, $users); } #[Test] public function hydrateHandlesConsentementParental(): void { $userId = '550e8400-e29b-41d4-a716-446655440001'; $row = $this->makeRow($userId); $row['consentement_parent_id'] = '660e8400-e29b-41d4-a716-446655440001'; $row['consentement_eleve_id'] = '770e8400-e29b-41d4-a716-446655440001'; $row['consentement_date'] = '2026-01-20T14:00:00+00:00'; $row['consentement_ip'] = '192.168.1.1'; $connection = $this->createMock(Connection::class); $connection->method('fetchAssociative')->willReturn($row); $repository = new DoctrineUserRepository($connection); $user = $repository->findById(UserId::fromString($userId)); self::assertNotNull($user); self::assertNotNull($user->consentementParental); self::assertSame('660e8400-e29b-41d4-a716-446655440001', $user->consentementParental->parentId); self::assertSame('770e8400-e29b-41d4-a716-446655440001', $user->consentementParental->eleveId); self::assertSame('192.168.1.1', $user->consentementParental->ipAddress); } #[Test] public function hydrateHandlesMultipleRoles(): void { $userId = '550e8400-e29b-41d4-a716-446655440001'; $row = $this->makeRow($userId); $row['roles'] = '["ROLE_PROF", "ROLE_ADMIN"]'; $connection = $this->createMock(Connection::class); $connection->method('fetchAssociative')->willReturn($row); $repository = new DoctrineUserRepository($connection); $user = $repository->findById(UserId::fromString($userId)); self::assertNotNull($user); self::assertCount(2, $user->roles); self::assertSame(Role::PROF, $user->roles[0]); self::assertSame(Role::ADMIN, $user->roles[1]); } #[Test] public function hydrateHandlesBlockedUser(): void { $userId = '550e8400-e29b-41d4-a716-446655440001'; $row = $this->makeRow($userId); $row['statut'] = StatutCompte::SUSPENDU->value; $row['blocked_at'] = '2026-01-20T14:00:00+00:00'; $row['blocked_reason'] = 'Comportement inapproprié'; $connection = $this->createMock(Connection::class); $connection->method('fetchAssociative')->willReturn($row); $repository = new DoctrineUserRepository($connection); $user = $repository->findById(UserId::fromString($userId)); self::assertNotNull($user); self::assertSame(StatutCompte::SUSPENDU, $user->statut); self::assertNotNull($user->blockedAt); self::assertSame('Comportement inapproprié', $user->blockedReason); } #[Test] public function savePreservesConsentementParental(): void { $consentement = ConsentementParental::accorder( parentId: '660e8400-e29b-41d4-a716-446655440001', eleveId: '770e8400-e29b-41d4-a716-446655440001', at: new DateTimeImmutable('2026-01-20T14:00:00+00:00'), ipAddress: '192.168.1.1', ); $connection = $this->createMock(Connection::class); $connection->expects(self::once()) ->method('executeStatement') ->with( self::anything(), self::callback(static function (array $params) { return $params['consentement_parent_id'] === '660e8400-e29b-41d4-a716-446655440001' && $params['consentement_eleve_id'] === '770e8400-e29b-41d4-a716-446655440001' && $params['consentement_ip'] === '192.168.1.1'; }), ); $repository = new DoctrineUserRepository($connection); $user = User::reconstitute( id: UserId::fromString('550e8400-e29b-41d4-a716-446655440001'), email: new Email('minor@example.com'), roles: [Role::ELEVE], tenantId: TenantId::fromString(self::TENANT_ALPHA_ID), schoolName: 'École Test', statut: StatutCompte::EN_ATTENTE, dateNaissance: new DateTimeImmutable('2015-05-15'), createdAt: new DateTimeImmutable('2026-01-15T10:00:00+00:00'), hashedPassword: null, activatedAt: null, consentementParental: $consentement, ); $repository->save($user); } /** * @return array */ private function makeRow(string $userId, string $email = 'test@example.com'): array { return [ 'id' => $userId, 'tenant_id' => self::TENANT_ALPHA_ID, 'email' => $email, 'first_name' => 'Jean', 'last_name' => 'Dupont', 'roles' => '["ROLE_PARENT"]', 'hashed_password' => null, 'statut' => 'pending', 'school_name' => 'École Test', 'date_naissance' => null, 'created_at' => '2026-01-15T10:00:00+00:00', 'activated_at' => null, 'invited_at' => null, 'blocked_at' => null, 'blocked_reason' => null, 'consentement_parent_id' => null, 'consentement_eleve_id' => null, 'consentement_date' => null, 'consentement_ip' => null, ]; } }