74abecbe03
Auto Tag Develop / tag (push) Successful in 10s
## 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>
190 lines
7.3 KiB
PHP
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)
|
|
;
|
|
}
|
|
}
|