userRepository = new InMemoryUserRepository(); $this->clock = new class implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-02-07 10:00:00'); } }; $this->handler = new GetUsersHandler($this->userRepository, $this->clock); } #[Test] public function returnsAllUsersForTenant(): void { $this->seedUsers(); $query = new GetUsersQuery(tenantId: self::TENANT_ID); $result = ($this->handler)($query); self::assertCount(3, $result->items); self::assertSame(3, $result->total); self::assertSame(1, $result->page); self::assertSame(30, $result->limit); } #[Test] public function filtersUsersByRole(): void { $this->seedUsers(); $query = new GetUsersQuery( tenantId: self::TENANT_ID, role: Role::PROF->value, ); $result = ($this->handler)($query); self::assertCount(2, $result->items); self::assertSame(2, $result->total); foreach ($result->items as $dto) { self::assertSame(Role::PROF->value, $dto->role); } } #[Test] public function filtersUsersByStatut(): void { $this->seedUsers(); $query = new GetUsersQuery( tenantId: self::TENANT_ID, statut: 'pending', ); $result = ($this->handler)($query); self::assertCount(2, $result->items); self::assertSame(2, $result->total); foreach ($result->items as $dto) { self::assertSame('pending', $dto->statut); } } #[Test] public function excludesUsersFromOtherTenants(): void { $this->seedUsers(); $otherUser = User::inviter( email: new Email('other@example.com'), role: Role::ADMIN, tenantId: TenantId::fromString(self::OTHER_TENANT_ID), schoolName: 'Autre École', firstName: 'Autre', lastName: 'User', invitedAt: new DateTimeImmutable('2026-02-01 10:00:00'), ); $this->userRepository->save($otherUser); $query = new GetUsersQuery(tenantId: self::TENANT_ID); $result = ($this->handler)($query); self::assertCount(3, $result->items); self::assertSame(3, $result->total); } #[Test] public function calculatesInvitationExpiree(): void { $user = User::inviter( email: new Email('old@example.com'), role: Role::PROF, tenantId: TenantId::fromString(self::TENANT_ID), schoolName: 'École Alpha', firstName: 'Old', lastName: 'Invitation', invitedAt: new DateTimeImmutable('2026-01-25 10:00:00'), ); $this->userRepository->save($user); $query = new GetUsersQuery(tenantId: self::TENANT_ID); $result = ($this->handler)($query); self::assertCount(1, $result->items); self::assertTrue($result->items[0]->invitationExpiree); } #[Test] public function paginatesResults(): void { $this->seedUsers(); $query = new GetUsersQuery( tenantId: self::TENANT_ID, page: 1, limit: 2, ); $result = ($this->handler)($query); self::assertCount(2, $result->items); self::assertSame(3, $result->total); self::assertSame(1, $result->page); self::assertSame(2, $result->limit); self::assertSame(2, $result->totalPages()); } #[Test] public function returnsSecondPage(): void { $this->seedUsers(); $query = new GetUsersQuery( tenantId: self::TENANT_ID, page: 2, limit: 2, ); $result = ($this->handler)($query); self::assertCount(1, $result->items); self::assertSame(3, $result->total); self::assertSame(2, $result->page); } #[Test] public function searchesByFirstName(): void { $this->seedUsers(); $query = new GetUsersQuery( tenantId: self::TENANT_ID, search: 'Jean', ); $result = ($this->handler)($query); self::assertCount(1, $result->items); self::assertSame('Jean', $result->items[0]->firstName); } #[Test] public function searchesByLastName(): void { $this->seedUsers(); $query = new GetUsersQuery( tenantId: self::TENANT_ID, search: 'Martin', ); $result = ($this->handler)($query); self::assertCount(1, $result->items); self::assertSame('Martin', $result->items[0]->lastName); } #[Test] public function searchesByEmail(): void { $this->seedUsers(); $query = new GetUsersQuery( tenantId: self::TENANT_ID, search: 'parent@', ); $result = ($this->handler)($query); self::assertCount(1, $result->items); self::assertSame('parent@example.com', $result->items[0]->email); } #[Test] public function searchIsCaseInsensitive(): void { $this->seedUsers(); $query = new GetUsersQuery( tenantId: self::TENANT_ID, search: 'jean', ); $result = ($this->handler)($query); self::assertCount(1, $result->items); self::assertSame('Jean', $result->items[0]->firstName); } #[Test] public function searchCombinesWithRoleFilter(): void { $this->seedUsers(); $query = new GetUsersQuery( tenantId: self::TENANT_ID, role: Role::PROF->value, search: 'Jean', ); $result = ($this->handler)($query); self::assertCount(1, $result->items); self::assertSame('Jean', $result->items[0]->firstName); self::assertSame(Role::PROF->value, $result->items[0]->role); } #[Test] public function searchResetsCountCorrectly(): void { $this->seedUsers(); $query = new GetUsersQuery( tenantId: self::TENANT_ID, search: 'nonexistent', ); $result = ($this->handler)($query); self::assertCount(0, $result->items); self::assertSame(0, $result->total); } #[Test] public function clampsPageZeroToOne(): void { $query = new GetUsersQuery(tenantId: self::TENANT_ID, page: 0); self::assertSame(1, $query->page); } #[Test] public function clampsNegativePageToOne(): void { $query = new GetUsersQuery(tenantId: self::TENANT_ID, page: -5); self::assertSame(1, $query->page); } #[Test] public function clampsLimitZeroToOne(): void { $query = new GetUsersQuery(tenantId: self::TENANT_ID, limit: 0); self::assertSame(1, $query->limit); } #[Test] public function clampsExcessiveLimitToHundred(): void { $query = new GetUsersQuery(tenantId: self::TENANT_ID, limit: 999); self::assertSame(100, $query->limit); } #[Test] public function clampsNegativeLimitToOne(): void { $query = new GetUsersQuery(tenantId: self::TENANT_ID, limit: -10); self::assertSame(1, $query->limit); } private function seedUsers(): void { $tenantId = TenantId::fromString(self::TENANT_ID); $teacher1 = User::inviter( email: new Email('teacher1@example.com'), role: Role::PROF, tenantId: $tenantId, schoolName: 'École Alpha', firstName: 'Jean', lastName: 'Dupont', invitedAt: new DateTimeImmutable('2026-02-01 10:00:00'), ); $this->userRepository->save($teacher1); $teacher2 = User::inviter( email: new Email('teacher2@example.com'), role: Role::PROF, tenantId: $tenantId, schoolName: 'École Alpha', firstName: 'Marie', lastName: 'Martin', invitedAt: new DateTimeImmutable('2026-02-01 10:00:00'), ); $teacher2->activer( '$argon2id$hashed', new DateTimeImmutable('2026-02-02 10:00:00'), new ConsentementParentalPolicy($this->clock), ); $this->userRepository->save($teacher2); $parent = User::inviter( email: new Email('parent@example.com'), role: Role::PARENT, tenantId: $tenantId, schoolName: 'École Alpha', firstName: 'Pierre', lastName: 'Parent', invitedAt: new DateTimeImmutable('2026-02-01 10:00:00'), ); $this->userRepository->save($parent); } }