Files
Lesstime/tests/Functional/Mcp/AbsenceRequestLifecycleTest.php
Matthieu 2b148fa65a feat(absences) : outils MCP CRUD pour les absences
Expose le module Absences via le serveur MCP et comble les trous CRUD
existants (projets, groupes, métadonnées de tâches, clients, users RH).

Absences (réutilise AbsenceDayCalculator + AbsenceBalanceService pour ne
pas contourner la logique de soldes) :
- list/get/create/review/cancel/delete-absence-request
- list/update-absence-policy, list/update-absence-balance
- create-absence-request prend un userId explicite (agir au nom d'un employé) ;
  review/cancel maintiennent les soldes (pending/taken) cohérents
- AbsenceRequestRepository::findFiltered pour les filtres de liste

Trous CRUD comblés :
- delete-project, delete-group
- CRUD tag, effort, priority
- CRUD status (couplé au workflow, avec category)
- CRUD client, get/update-user (champs RH, sans password ni roles)

Sérialisation centralisée (Serializer::absenceRequest/Policy/Balance/client/userFull).
Instructions MCP (mcp.yaml) mises à jour : statuts par workflow + domaine absences.

Tests : tests/Functional/Mcp/AbsenceRequestLifecycleTest (création / approbation /
annulation admin) vérifient le cycle complet et la cohérence des soldes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:10:56 +02:00

178 lines
6.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Functional\Mcp;
use App\Entity\AbsencePolicy;
use App\Entity\User;
use App\Enum\AbsenceType;
use App\Mcp\Tool\Absence\CancelAbsenceRequestTool;
use App\Mcp\Tool\Absence\CreateAbsenceRequestTool;
use App\Mcp\Tool\Absence\ReviewAbsenceRequestTool;
use App\Repository\AbsenceBalanceRepository;
use App\Repository\AbsencePolicyRepository;
use App\Repository\AbsenceRequestRepository;
use App\Repository\UserRepository;
use App\Service\AbsenceBalanceService;
use App\Service\AbsenceDayCalculator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Bundle\SecurityBundle\Security;
/**
* @internal
*/
class AbsenceRequestLifecycleTest extends KernelTestCase
{
private EntityManagerInterface $em;
private User $employee;
private User $admin;
protected function setUp(): void
{
self::bootKernel();
$this->em = self::getContainer()->get(EntityManagerInterface::class);
// Employé dédié au test.
$this->employee = new User();
$this->employee->setUsername('mcp-test-employee-'.uniqid());
$this->employee->setPassword('x');
$this->employee->setRoles(['ROLE_USER']);
$this->employee->setIsEmployee(true);
$this->employee->setReferencePeriodStart('06-01');
$this->em->persist($this->employee);
$this->admin = new User();
$this->admin->setUsername('mcp-test-admin-'.uniqid());
$this->admin->setPassword('x');
$this->admin->setRoles(['ROLE_ADMIN']);
$this->em->persist($this->admin);
// Policy CP active (créée si absente — findOneByType peut déjà exister via fixtures).
$policy = $this->em->getRepository(AbsencePolicy::class)->findOneBy(['type' => AbsenceType::PaidLeave]);
if (null === $policy) {
$policy = new AbsencePolicy();
$policy->setType(AbsenceType::PaidLeave);
$policy->setCountWorkingDaysOnly(true);
$policy->setActive(true);
$this->em->persist($policy);
} else {
$policy->setActive(true);
}
$this->em->flush();
}
public function testCreateReservesPendingDays(): void
{
$tool = $this->createTool($this->admin);
// Lundi 2026-06-01 → vendredi 2026-06-05 = 5 jours ouvrés.
$json = ($tool)(
$this->employee->getId(),
'cp',
'2026-06-01',
'2026-06-05',
);
$data = json_decode($json, true);
self::assertSame('pending', $data['status']);
// JSON ne préserve pas le type float pour un entier (5.0 -> 5) : on compare la valeur.
self::assertSame(5.0, (float) $data['countedDays']);
self::assertSame($this->employee->getId(), $data['user']['id']);
$balance = self::getContainer()->get(AbsenceBalanceRepository::class)
->findOneForPeriod($this->employee, AbsenceType::PaidLeave, '2026-2027')
;
self::assertNotNull($balance);
self::assertSame(5.0, $balance->getPending());
}
public function testApproveMovesPendingToTaken(): void
{
$created = json_decode(
($this->createTool($this->admin))($this->employee->getId(), 'cp', '2026-06-01', '2026-06-05'),
true,
);
$json = ($this->reviewTool($this->admin))($created['id'], 'approve');
$data = json_decode($json, true);
self::assertSame('approved', $data['status']);
self::assertSame($this->admin->getId(), $data['reviewedBy']['id']);
$balance = self::getContainer()->get(AbsenceBalanceRepository::class)
->findOneForPeriod($this->employee, AbsenceType::PaidLeave, '2026-2027')
;
self::assertSame(0.0, $balance->getPending());
self::assertSame(5.0, $balance->getTaken());
}
public function testAdminCancelApprovedReleasesTaken(): void
{
$created = json_decode(
($this->createTool($this->admin))($this->employee->getId(), 'cp', '2026-06-01', '2026-06-05'),
true,
);
($this->reviewTool($this->admin))($created['id'], 'approve');
$data = json_decode(($this->cancelTool($this->admin))($created['id']), true);
self::assertSame('cancelled', $data['status']);
$balance = self::getContainer()->get(AbsenceBalanceRepository::class)
->findOneForPeriod($this->employee, AbsenceType::PaidLeave, '2026-2027')
;
self::assertSame(0.0, $balance->getTaken());
self::assertSame(0.0, $balance->getPending());
}
private function securityFor(User $user): Security
{
$security = $this->createMock(Security::class);
$security->method('isGranted')->willReturn(true);
$security->method('getUser')->willReturn($user);
return $security;
}
private function createTool(User $actor): CreateAbsenceRequestTool
{
$c = self::getContainer();
return new CreateAbsenceRequestTool(
$c->get(EntityManagerInterface::class),
$c->get(UserRepository::class),
$c->get(AbsencePolicyRepository::class),
$c->get(AbsenceRequestRepository::class),
$c->get(AbsenceDayCalculator::class),
$c->get(AbsenceBalanceService::class),
$this->securityFor($actor),
);
}
private function reviewTool(User $actor): ReviewAbsenceRequestTool
{
$c = self::getContainer();
return new ReviewAbsenceRequestTool(
$c->get(EntityManagerInterface::class),
$c->get(AbsenceRequestRepository::class),
$c->get(AbsenceBalanceService::class),
$this->securityFor($actor),
);
}
private function cancelTool(User $actor): CancelAbsenceRequestTool
{
$c = self::getContainer();
return new CancelAbsenceRequestTool(
$c->get(EntityManagerInterface::class),
$c->get(AbsenceRequestRepository::class),
$c->get(AbsenceBalanceService::class),
$this->securityFor($actor),
);
}
}