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>
This commit is contained in:
177
tests/Functional/Mcp/AbsenceRequestLifecycleTest.php
Normal file
177
tests/Functional/Mcp/AbsenceRequestLifecycleTest.php
Normal file
@@ -0,0 +1,177 @@
|
||||
<?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),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user