security = $this->createStub(Security::class); $this->employeeRepository = $this->createStub(EmployeeScopedRepositoryInterface::class); $this->workHourRepository = $this->createStub(WorkHourReadRepositoryInterface::class); $this->requestStack = new RequestStack(); } public function testThrowsWhenAnonymous(): void { $this->security->method('getUser')->willReturn(null); $this->expectException(AccessDeniedHttpException::class); $this->buildProvider()->provide(new Get()); } public function testThrowsWhenDateFormatInvalid(): void { $this->security->method('getUser')->willReturn(new User()); $this->requestStack->push(new Request(query: ['from' => '01-06-2026', 'to' => '2026-06-30'])); $this->expectException(UnprocessableEntityHttpException::class); $this->buildProvider()->provide(new Get()); } public function testThrowsWhenFromAfterTo(): void { $this->security->method('getUser')->willReturn(new User()); $this->requestStack->push(new Request(query: ['from' => '2026-06-30', 'to' => '2026-06-01'])); $this->expectException(UnprocessableEntityHttpException::class); $this->buildProvider()->provide(new Get()); } public function testThrowsWhenRangeTooLarge(): void { $this->security->method('getUser')->willReturn(new User()); $this->requestStack->push(new Request(query: ['from' => '2024-01-01', 'to' => '2026-01-01'])); $this->expectException(UnprocessableEntityHttpException::class); $this->buildProvider()->provide(new Get()); } public function testComputesValidatedDays(): void { $user = new User(); $alice = $this->buildEmployee(1); $bob = $this->buildEmployee(2); $driver = $this->buildEmployee(3); // 2026-06-01 : Alice + Bob validés → vert. // 2026-06-02 : Alice validée, Bob en attente → pas vert. // 2026-06-03 : seul un conducteur (validé) → exclu → total non-conducteur 0 → pas vert. $workHours = [ $this->buildWorkHour($alice, '2026-06-01', true), $this->buildWorkHour($bob, '2026-06-01', true), $this->buildWorkHour($alice, '2026-06-02', true), $this->buildWorkHour($bob, '2026-06-02', false), $this->buildWorkHour($driver, '2026-06-03', true), ]; $this->security->method('getUser')->willReturn($user); $this->requestStack->push(new Request(query: ['from' => '2026-06-01', 'to' => '2026-06-30'])); $this->employeeRepository->method('findScoped')->with($user)->willReturn([$alice, $bob, $driver]); $this->workHourRepository->method('findByDateRangeAndEmployees')->willReturn($workHours); $resolver = $this->createStub(EmployeeContractResolver::class); $resolver->method('resolveIsDriverForEmployeeAndDate') ->willReturnCallback(static fn (Employee $e): bool => 3 === $e->getId()) ; $result = $this->buildProvider($resolver)->provide(new Get()); self::assertSame('2026-06-01', $result->from); self::assertSame('2026-06-30', $result->to); self::assertSame(['2026-06-01'], $result->validatedDays); } public function testComputesValidatedDaysForDriverScope(): void { $user = new User(); $alice = $this->buildEmployee(1); // non-conducteur $driver = $this->buildEmployee(3); // conducteur // ?driver=1 : 01/06 conducteur validé → vert ; 02/06 conducteur en attente → non ; // 03/06 seule Alice (non-conducteur) validée → ignorée → non. $workHours = [ $this->buildWorkHour($driver, '2026-06-01', true), $this->buildWorkHour($driver, '2026-06-02', false), $this->buildWorkHour($alice, '2026-06-03', true), ]; $this->security->method('getUser')->willReturn($user); $this->requestStack->push(new Request(query: ['from' => '2026-06-01', 'to' => '2026-06-30', 'driver' => '1'])); $this->employeeRepository->method('findScoped')->willReturn([$alice, $driver]); $this->workHourRepository->method('findByDateRangeAndEmployees')->willReturn($workHours); $resolver = $this->createStub(EmployeeContractResolver::class); $resolver->method('resolveIsDriverForEmployeeAndDate') ->willReturnCallback(static fn (Employee $e): bool => 3 === $e->getId()) ; $result = $this->buildProvider($resolver)->provide(new Get()); self::assertSame(['2026-06-01'], $result->validatedDays); } public function testEmptyWhenNoWorkHours(): void { $user = new User(); $this->security->method('getUser')->willReturn($user); $this->requestStack->push(new Request(query: ['from' => '2026-06-01', 'to' => '2026-06-30'])); $this->employeeRepository->method('findScoped')->willReturn([]); $this->workHourRepository->method('findByDateRangeAndEmployees')->willReturn([]); $result = $this->buildProvider()->provide(new Get()); self::assertSame([], $result->validatedDays); } private function buildProvider(?EmployeeContractResolver $resolver = null): WorkHourValidationStatusProvider { $resolver ??= $this->createStub(EmployeeContractResolver::class); return new WorkHourValidationStatusProvider( $this->security, $this->requestStack, $this->employeeRepository, $this->workHourRepository, $resolver, ); } private function buildEmployee(int $id): Employee { $employee = new Employee() ->setFirstName('Test') ->setLastName('Employee') ; $reflection = new ReflectionObject($employee); $property = $reflection->getProperty('id'); $property->setAccessible(true); $property->setValue($employee, $id); return $employee; } private function buildWorkHour(Employee $employee, string $date, bool $isValid): WorkHour { return new WorkHour() ->setEmployee($employee) ->setWorkDate(new DateTimeImmutable($date)) ->setIsValid($isValid) ; } }