em = self::getContainer()->get(EntityManagerInterface::class); $this->admin = new User(); $this->admin->setUsername('mcp-report-admin-'.uniqid()); $this->admin->setPassword('x'); $this->admin->setRoles(['ROLE_ADMIN']); $this->em->persist($this->admin); $this->client = new Client(); $this->client->setName('Test Client '.uniqid()); $this->em->persist($this->client); $this->prospect = new Prospect(); $this->prospect->setCompany('Test Prospect '.uniqid()); $this->em->persist($this->prospect); $this->prestataire = new Prestataire(); $this->prestataire->setName('Test Prestataire '.uniqid()); $this->em->persist($this->prestataire); $this->em->flush(); } public function testCreateRequiresExactlyOneParent(): void { try { ($this->createTool())('subject', null, null, null); self::fail('Expected error when no parent provided.'); } catch (InvalidArgumentException $e) { self::assertStringContainsString('Exactly one of clientId, prospectId or prestataireId', $e->getMessage()); } try { ($this->createTool())('subject', $this->client->getId(), null, $this->prestataire->getId()); self::fail('Expected error when two parents provided.'); } catch (InvalidArgumentException $e) { self::assertStringContainsString('Exactly one of clientId, prospectId or prestataireId', $e->getMessage()); } } public function testCreateRejectsInvalidType(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid type "lunch". Allowed: note, call, meeting, email.'); ($this->createTool())('Lunch at noon', $this->client->getId(), null, null, null, null, 'lunch'); } public function testCreateAcceptsAllValidTypes(): void { foreach (['note', 'call', 'meeting', 'email'] as $type) { $data = json_decode( ($this->createTool())('subject', $this->client->getId(), null, null, null, '2026-01-15', $type), true, ); self::assertSame($type, $data['type']); } } public function testCreateDefaultsTypeToNote(): void { $data = json_decode(($this->createTool())('subject', $this->client->getId(), null, null), true); self::assertSame(ReportType::Note->value, $data['type']); } public function testCreateRejectsInvalidDate(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid occurredAt "not-a-date"'); ($this->createTool())('subject', $this->client->getId(), null, null, null, 'not-a-date'); } public function testCreateDefaultsOccurredAtToToday(): void { $data = json_decode(($this->createTool())('subject', $this->client->getId(), null, null), true); self::assertSame(new DateTimeImmutable('today')->format('Y-m-d'), $data['occurredAt']); } public function testCreateAutoFillsAuthorFromCurrentUser(): void { $this->loginAdmin(); $data = json_decode(($this->createTool())('subject', $this->client->getId(), null, null), true); self::assertNotNull($data['author']); self::assertSame($this->admin->getId(), $data['author']['id']); self::assertSame($this->admin->getUsername(), $data['author']['username']); } public function testGetReturnsReport(): void { $created = json_decode( ($this->createTool())('My subject', $this->prestataire->getId() ? null : null, null, $this->prestataire->getId(), 'body text', '2026-03-01', 'meeting'), true, ); $data = json_decode(($this->getTool())((int) $created['id']), true); self::assertSame('My subject', $data['subject']); self::assertSame('body text', $data['body']); self::assertSame('2026-03-01', $data['occurredAt']); self::assertSame('meeting', $data['type']); self::assertSame($this->prestataire->getId(), $data['prestataireId']); self::assertSame([], $data['documents']); } public function testListOrderedByOccurredAtDesc(): void { ($this->createTool())('oldest', $this->client->getId(), null, null, null, '2026-01-01'); ($this->createTool())('newest', $this->client->getId(), null, null, null, '2026-12-01'); ($this->createTool())('middle', $this->client->getId(), null, null, null, '2026-06-15'); $data = json_decode(($this->listTool())($this->client->getId(), null, null), true); self::assertCount(3, $data); self::assertSame('newest', $data[0]['subject']); self::assertSame('middle', $data[1]['subject']); self::assertSame('oldest', $data[2]['subject']); } public function testListRejectsMultipleFilters(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('At most one of clientId, prospectId or prestataireId'); ($this->listTool())($this->client->getId(), $this->prospect->getId(), null); } public function testUpdateChangesTypeAndDate(): void { $created = json_decode(($this->createTool())('s', $this->client->getId(), null, null, null, '2026-01-01', 'note'), true); $data = json_decode(($this->updateTool())((int) $created['id'], 'new subject', null, '2026-02-02', 'call'), true); self::assertSame('new subject', $data['subject']); self::assertSame('2026-02-02', $data['occurredAt']); self::assertSame('call', $data['type']); } public function testUpdateRejectsInvalidType(): void { $created = json_decode(($this->createTool())('s', $this->client->getId(), null, null), true); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid type "lunch"'); ($this->updateTool())((int) $created['id'], null, null, null, 'lunch'); } public function testDeleteRemovesReport(): void { $created = json_decode(($this->createTool())('Bye', $this->client->getId(), null, null), true); $id = (int) $created['id']; $data = json_decode(($this->deleteTool())($id), true); self::assertTrue($data['success']); $this->em->clear(); self::assertNull(self::getContainer()->get(CommercialReportRepositoryInterface::class)->findById($id)); } private function loginAdmin(): void { $token = new UsernamePasswordToken($this->admin, 'main', $this->admin->getRoles()); self::getContainer()->get(TokenStorageInterface::class)->setToken($token); } private function securityFor(bool $admin = true): Security { $security = $this->createMock(Security::class); $security->method('isGranted')->willReturn($admin); $security->method('getUser')->willReturn($admin ? $this->admin : null); return $security; } private function createTool(): CreateCommercialReportTool { $c = self::getContainer(); return new CreateCommercialReportTool( $this->em, $c->get(ClientRepositoryInterface::class), $c->get(ProspectRepositoryInterface::class), $c->get(PrestataireRepositoryInterface::class), $this->securityFor(), ); } private function getTool(): GetCommercialReportTool { return new GetCommercialReportTool( self::getContainer()->get(CommercialReportRepositoryInterface::class), $this->securityFor(), ); } private function listTool(): ListCommercialReportsTool { return new ListCommercialReportsTool( self::getContainer()->get(CommercialReportRepositoryInterface::class), $this->securityFor(), ); } private function updateTool(): UpdateCommercialReportTool { return new UpdateCommercialReportTool( self::getContainer()->get(CommercialReportRepositoryInterface::class), $this->em, $this->securityFor(), ); } private function deleteTool(): DeleteCommercialReportTool { return new DeleteCommercialReportTool( self::getContainer()->get(CommercialReportRepositoryInterface::class), $this->em, $this->securityFor(), ); } }