Files
SIRH/tests/State/WorkHourValidationStatusProviderTest.php
T
tristan 74abecbe03
Auto Tag Develop / tag (push) Successful in 10s
feat(heures) : calendrier des jours validés (vue Jour) + harmonisation Malio UI (#30)
## Fonctionnel
- Calendrier MalioDate en vue Jour (écrans Heures ET Heures Conducteurs) : les jours entièrement validés par un admin sont peints en vert.
  - Endpoint `GET /work-hours/validation-status?from=&to=[&driver=1]` (scope conducteur inversé via `driver=1`), périmètre complet (ignore le filtre sites).
  - Chargement à la volée par mois (event `@month-change`), refresh après validation / saisie / absence.

## Harmonisation @malio/layer-ui 1.7.11
- `reserveMessageSpace=false` sur tous les champs (alignement).
- Tous les drawers migrés sur `MalioDrawer` (titre via slot `#header`, `AppDrawer` custom supprimé).
- Boutons d'action en `MalioButton` ; deux boutons côte à côte partagent l'espace.
- Inputs date en `MalioDate`, sélecteur semaine en `MalioDateWeek`.
- Boutons d'ajout uniformisés sur « Ajouter » + icône.

## Divers
- `.env` : `EXCLUDED_PUBLIC_HOLIDAYS="null"`.
- Doc : `doc/hours-validated-days.md`, `documentation-content.ts`, `CLAUDE.md`.
- Tests : provider `WorkHourValidationStatus` (suite complète 236/236 OK via pre-commit hook).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #30
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-16 13:53:03 +00:00

190 lines
7.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\State;
use ApiPlatform\Metadata\Get;
use App\Entity\Employee;
use App\Entity\User;
use App\Entity\WorkHour;
use App\Repository\Contract\EmployeeScopedRepositoryInterface;
use App\Repository\Contract\WorkHourReadRepositoryInterface;
use App\Service\Contracts\EmployeeContractResolver;
use App\State\WorkHourValidationStatusProvider;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use ReflectionObject;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
/**
* @internal
*/
final class WorkHourValidationStatusProviderTest extends TestCase
{
private Security $security;
private EmployeeScopedRepositoryInterface $employeeRepository;
private WorkHourReadRepositoryInterface $workHourRepository;
private RequestStack $requestStack;
protected function setUp(): void
{
$this->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)
;
}
}