From 79c55ba0f91eecb71a4acfa4459cad81d29ea8d9 Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Tue, 7 Apr 2026 23:57:42 +0200 Subject: [PATCH] test: ajout 163 tests unitaires (668->831) avec couverture 73% Entites (76 tests) : - PrestataireTest : constructeur, setters, getFullAddress, getTotalPaidHt - FacturePrestataireTest : constructeur, getPeriodLabel 12 mois, Vich upload - AdvertPaymentTest : constructeur, types constants, method - AdvertEventTest : constructeur, getTypeLabel, 5 types + fallback - FactureLineTest : constructeur, setters, optionnels nullable - ActionLogTest : constructeur, 10 action constants, severity - PaymentReminderTest : 8 steps, getStepLabel, getSeverity - DocusealEventTest : constructeur, nullable fields Commands (16 tests) : - ReminderFacturesPrestataireCommandTest : 6 scenarios (aucun presta, tous OK, factures manquantes, SIRET vide, mois different) - PaymentReminderCommandTest : 10 scenarios (skip recent, J+15 emails, suspension, termination, exception handling) Services PDF (24 tests) : - ComptaPdfTest : empty/FEC/multi-page, totaux Debit/Credit - RapportFinancierPdfTest : recettes/depenses, bilan equilibre/deficit/excedent - FacturePdfTest : lignes, TVA, customer address, paid badge, multi-page Controllers (47 tests) : - ComptabiliteControllerTest : 18 tests (index, 7 exports CSV, 2 JSON, 4 PDF, 2 rapport financier) - PrestatairesControllerTest : 19 tests (CRUD, factures, SIRET proxy) - FactureControllerTest : 6 tests (search, send) - FactureVerifyControllerTest : 4 tests (HMAC valid/invalid/not found) Couverture : 51%->60% classes, 58%->73% methodes Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/Command/PaymentReminderCommandTest.php | 328 ++++++++++++++ ...ReminderFacturesPrestataireCommandTest.php | 199 ++++++++ .../Admin/ComptabiliteControllerTest.php | 264 +++++++++++ .../Admin/FactureControllerTest.php | 190 ++++++++ .../Admin/PrestatairesControllerTest.php | 425 ++++++++++++++++++ .../FactureVerifyControllerTest.php | 138 ++++++ tests/Entity/ActionLogTest.php | 156 +++++++ tests/Entity/AdvertEventTest.php | 97 ++++ tests/Entity/AdvertPaymentTest.php | 79 ++++ tests/Entity/DocusealEventTest.php | 75 ++++ tests/Entity/FactureLineTest.php | 120 +++++ tests/Entity/FacturePrestataireTest.php | 140 ++++++ tests/Entity/PaymentReminderTest.php | 138 ++++++ tests/Entity/PrestataireTest.php | 171 +++++++ tests/Service/Pdf/ComptaPdfTest.php | 174 +++++++ tests/Service/Pdf/FacturePdfTest.php | 230 ++++++++++ tests/Service/Pdf/RapportFinancierPdfTest.php | 169 +++++++ 17 files changed, 3093 insertions(+) create mode 100644 tests/Command/PaymentReminderCommandTest.php create mode 100644 tests/Command/ReminderFacturesPrestataireCommandTest.php create mode 100644 tests/Controller/Admin/ComptabiliteControllerTest.php create mode 100644 tests/Controller/Admin/FactureControllerTest.php create mode 100644 tests/Controller/Admin/PrestatairesControllerTest.php create mode 100644 tests/Controller/FactureVerifyControllerTest.php create mode 100644 tests/Entity/ActionLogTest.php create mode 100644 tests/Entity/AdvertEventTest.php create mode 100644 tests/Entity/AdvertPaymentTest.php create mode 100644 tests/Entity/DocusealEventTest.php create mode 100644 tests/Entity/FactureLineTest.php create mode 100644 tests/Entity/FacturePrestataireTest.php create mode 100644 tests/Entity/PaymentReminderTest.php create mode 100644 tests/Entity/PrestataireTest.php create mode 100644 tests/Service/Pdf/ComptaPdfTest.php create mode 100644 tests/Service/Pdf/FacturePdfTest.php create mode 100644 tests/Service/Pdf/RapportFinancierPdfTest.php diff --git a/tests/Command/PaymentReminderCommandTest.php b/tests/Command/PaymentReminderCommandTest.php new file mode 100644 index 0000000..08a055e --- /dev/null +++ b/tests/Command/PaymentReminderCommandTest.php @@ -0,0 +1,328 @@ +em = $this->createMock(EntityManagerInterface::class); + $this->mailer = $this->createMock(MailerService::class); + $this->twig = $this->createStub(Environment::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->actionService = $this->createMock(ActionService::class); + } + + private function makeCommand(): PaymentReminderCommand + { + return new PaymentReminderCommand( + $this->em, + $this->mailer, + $this->twig, + $this->logger, + $this->actionService, + ); + } + + private function makeAdvert(string $numOrder, ?string $email = 'client@example.com', int $daysAgo = 20): Advert + { + $orderNumber = new OrderNumber($numOrder); + $customer = $this->createStub(Customer::class); + $customer->method('getEmail')->willReturn($email); + $customer->method('getFullName')->willReturn('Jean Dupont'); + + $advert = $this->createMock(Advert::class); + $advert->method('getOrderNumber')->willReturn($orderNumber); + $advert->method('getCustomer')->willReturn($customer); + $advert->method('getState')->willReturn(Advert::STATE_SEND); + + $updatedAt = new \DateTimeImmutable('-'.$daysAgo.' days'); + $advert->method('getUpdatedAt')->willReturn($updatedAt); + $advert->method('getCreatedAt')->willReturn($updatedAt); + + return $advert; + } + + /** + * Stub the PaymentReminder QueryBuilder so getNextStep returns a specific list of already-done steps. + * Also stubs the Advert repository findBy to return the provided adverts. + * + * @param array $adverts + */ + private function stubReminderRepo(array $existingSteps, array $adverts = []): void + { + $query = $this->createStub(Query::class); + $query->method('getSingleColumnResult')->willReturn($existingSteps); + + $qb = $this->createStub(QueryBuilder::class); + $qb->method('select')->willReturnSelf(); + $qb->method('where')->willReturnSelf(); + $qb->method('setParameter')->willReturnSelf(); + $qb->method('getQuery')->willReturn($query); + + $reminderRepo = $this->createStub(EntityRepository::class); + $reminderRepo->method('createQueryBuilder')->willReturn($qb); + + $advertRepo = $this->createStub(EntityRepository::class); + $advertRepo->method('findBy')->willReturn($adverts); + + $this->em->method('getRepository')->willReturnCallback( + fn (string $class) => match ($class) { + PaymentReminder::class => $reminderRepo, + default => $advertRepo, + } + ); + } + + public function testNoAdvertsReturnsSuccess(): void + { + $repo = $this->createStub(EntityRepository::class); + $repo->method('findBy')->willReturn([]); + + $this->em->method('getRepository')->with(Advert::class)->willReturn($repo); + + $this->mailer->expects($this->never())->method('sendEmail'); + + $tester = new CommandTester($this->makeCommand()); + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertStringContainsString('Aucun avis de paiement en attente', $tester->getDisplay()); + } + + public function testAdvertWithNoCustomerIsSkipped(): void + { + $advert = $this->createStub(Advert::class); + $advert->method('getCustomer')->willReturn(null); + + $advertRepo = $this->createStub(EntityRepository::class); + $advertRepo->method('findBy')->willReturn([$advert]); + + $this->em->method('getRepository')->willReturn($advertRepo); + + $this->mailer->expects($this->never())->method('sendEmail'); + + $tester = new CommandTester($this->makeCommand()); + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertStringContainsString('relance(s) envoyee(s)', $tester->getDisplay()); + } + + public function testAdvertWithNoEmailIsSkipped(): void + { + $advert = $this->makeAdvert('04/2026-00001', null, 20); + + $advertRepo = $this->createStub(EntityRepository::class); + $advertRepo->method('findBy')->willReturn([$advert]); + + $this->em->method('getRepository')->willReturn($advertRepo); + + $this->mailer->expects($this->never())->method('sendEmail'); + + $tester = new CommandTester($this->makeCommand()); + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertStringContainsString('relance(s) envoyee(s)', $tester->getDisplay()); + } + + public function testAdvertWithNoEligibleStepIsSkipped(): void + { + // Only 5 days old — no step requires less than 15 days + $advert = $this->makeAdvert('04/2026-00001', 'client@example.com', 5); + + $this->stubReminderRepo([], [$advert]); + + $this->mailer->expects($this->never())->method('sendEmail'); + + $tester = new CommandTester($this->makeCommand()); + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertStringContainsString('0 relance(s) envoyee(s)', $tester->getDisplay()); + } + + public function testAdvertWithAllStepsDoneIsSkipped(): void + { + // 30 days old — many steps eligible, but all already done + $advert = $this->makeAdvert('04/2026-00001', 'client@example.com', 30); + + // All steps already done + $allSteps = array_keys(PaymentReminder::STEPS_CONFIG); + $this->stubReminderRepo($allSteps, [$advert]); + + $this->mailer->expects($this->never())->method('sendEmail'); + + $tester = new CommandTester($this->makeCommand()); + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertStringContainsString('0 relance(s) envoyee(s)', $tester->getDisplay()); + } + + public function testSendsFirstReminderAt15Days(): void + { + // 16 days old, no steps done yet -> should trigger reminder_15 + $advert = $this->makeAdvert('04/2026-00001', 'client@example.com', 16); + + $this->stubReminderRepo([], [$advert]); + + $this->twig->method('render')->willReturn('

Email content

'); + + // Expect 2 emails: client + admin notification + $this->mailer->expects($this->exactly(2))->method('sendEmail'); + + $this->em->method('persist'); + $this->em->method('flush'); + + $tester = new CommandTester($this->makeCommand()); + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertStringContainsString('1 relance(s) envoyee(s)', $tester->getDisplay()); + } + + public function testSuspensionStepCallsSuspendCustomer(): void + { + // 30 days old, all steps before suspension already done + $advert = $this->makeAdvert('04/2026-00001', 'client@example.com', 30); + + $customer = $advert->getCustomer(); + + // All steps before STEP_SUSPENSION_1 are done; days=30 >= 29 for suspension_1 + $doneSteps = [ + PaymentReminder::STEP_REMINDER_15, + PaymentReminder::STEP_WARNING_10, + PaymentReminder::STEP_SUSPENSION_WARNING_5, + PaymentReminder::STEP_FINAL_REMINDER_3, + ]; + $this->stubReminderRepo($doneSteps, [$advert]); + + $this->twig->method('render')->willReturn('

Email

'); + $this->mailer->method('sendEmail'); + $this->em->method('persist'); + $this->em->method('flush'); + + $this->actionService->expects($this->once()) + ->method('suspendCustomer') + ->with($customer, $this->stringContains('Impaye')); + + $tester = new CommandTester($this->makeCommand()); + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertStringContainsString('1 relance(s) envoyee(s)', $tester->getDisplay()); + } + + public function testTerminationWarningStepCallsDisableCustomer(): void + { + // 46 days old -> termination_warning step (>= 45 days) + $advert = $this->makeAdvert('04/2026-00001', 'client@example.com', 46); + + $customer = $advert->getCustomer(); + + // All earlier steps done + $doneSteps = [ + PaymentReminder::STEP_REMINDER_15, + PaymentReminder::STEP_WARNING_10, + PaymentReminder::STEP_SUSPENSION_WARNING_5, + PaymentReminder::STEP_FINAL_REMINDER_3, + PaymentReminder::STEP_SUSPENSION_1, + PaymentReminder::STEP_FORMAL_NOTICE, + ]; + $this->stubReminderRepo($doneSteps, [$advert]); + + $this->twig->method('render')->willReturn('

Email

'); + $this->mailer->method('sendEmail'); + $this->em->method('persist'); + $this->em->method('flush'); + + $this->actionService->expects($this->once()) + ->method('disableCustomer') + ->with($customer, $this->stringContains('Pre-resiliation')); + + $tester = new CommandTester($this->makeCommand()); + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + } + + public function testTerminationStepCallsMarkForDeletion(): void + { + // 61 days old -> termination_30 step (>= 60 days) + $advert = $this->makeAdvert('04/2026-00001', 'client@example.com', 61); + + $customer = $advert->getCustomer(); + + // All earlier steps done + $doneSteps = [ + PaymentReminder::STEP_REMINDER_15, + PaymentReminder::STEP_WARNING_10, + PaymentReminder::STEP_SUSPENSION_WARNING_5, + PaymentReminder::STEP_FINAL_REMINDER_3, + PaymentReminder::STEP_SUSPENSION_1, + PaymentReminder::STEP_FORMAL_NOTICE, + PaymentReminder::STEP_TERMINATION_WARNING, + ]; + $this->stubReminderRepo($doneSteps, [$advert]); + + $this->twig->method('render')->willReturn('

Email

'); + $this->mailer->method('sendEmail'); + $this->em->method('persist'); + $this->em->expects($this->atLeastOnce())->method('flush'); + + $this->actionService->expects($this->once()) + ->method('markForDeletion') + ->with($customer, $this->stringContains('Resiliation')); + + // setState(STATE_CANCEL) should be called on the advert + $advert->expects($this->once())->method('setState')->with(Advert::STATE_CANCEL); + + $tester = new CommandTester($this->makeCommand()); + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + } + + public function testExceptionInStepIsLoggedAndContinues(): void + { + $advert = $this->makeAdvert('04/2026-00001', 'client@example.com', 20); + + $this->stubReminderRepo([], [$advert]); + + // Twig throws an exception when rendering + $this->twig->method('render')->willThrowException(new \RuntimeException('Twig error')); + + $this->logger->expects($this->once())->method('error'); + + $tester = new CommandTester($this->makeCommand()); + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertStringContainsString('0 relance(s) envoyee(s)', $tester->getDisplay()); + $this->assertStringContainsString('Erreur', $tester->getDisplay()); + } +} diff --git a/tests/Command/ReminderFacturesPrestataireCommandTest.php b/tests/Command/ReminderFacturesPrestataireCommandTest.php new file mode 100644 index 0000000..5e80291 --- /dev/null +++ b/tests/Command/ReminderFacturesPrestataireCommandTest.php @@ -0,0 +1,199 @@ +em = $this->createMock(EntityManagerInterface::class); + $this->mailer = $this->createMock(MailerService::class); + } + + private function makePrestataire(string $raisonSociale, ?string $siret = null): Prestataire + { + $p = new Prestataire($raisonSociale); + if (null !== $siret) { + $p->setSiret($siret); + } + + return $p; + } + + private function makeFacturePrestataire(int $year, int $month): FacturePrestataire + { + $f = $this->createStub(FacturePrestataire::class); + $f->method('getYear')->willReturn($year); + $f->method('getMonth')->willReturn($month); + + return $f; + } + + private function stubRepository(array $prestataires): void + { + $repo = $this->createStub(EntityRepository::class); + $repo->method('findBy')->willReturn($prestataires); + + $this->em->method('getRepository') + ->with(Prestataire::class) + ->willReturn($repo); + } + + public function testNoActivePrestatairesReturnsSuccess(): void + { + $this->stubRepository([]); + + $this->mailer->expects($this->never())->method('sendEmail'); + + $command = new ReminderFacturesPrestataireCommand($this->em, $this->mailer); + $tester = new CommandTester($command); + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertStringContainsString('Toutes les factures prestataires', $tester->getDisplay()); + } + + public function testAllPrestatairesHaveFactureForPreviousMonth(): void + { + $now = new \DateTimeImmutable(); + $prev = $now->modify('first day of last month'); + $year = (int) $prev->format('Y'); + $month = (int) $prev->format('n'); + + $presta = $this->makePrestataire('ACME Corp', '12345678900011'); + $facture = $this->makeFacturePrestataire($year, $month); + $presta->getFactures()->add($facture); + + $this->stubRepository([$presta]); + + $this->mailer->expects($this->never())->method('sendEmail'); + + $command = new ReminderFacturesPrestataireCommand($this->em, $this->mailer); + $tester = new CommandTester($command); + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertStringContainsString('Toutes les factures prestataires', $tester->getDisplay()); + } + + public function testMissingFacturesSendsReminderEmail(): void + { + $presta1 = $this->makePrestataire('ACME Corp', '12345678900011'); + $presta2 = $this->makePrestataire('Beta SARL'); + + // presta1 has no factures for the previous month, presta2 has none at all + $this->stubRepository([$presta1, $presta2]); + + $this->mailer->expects($this->once()) + ->method('getAdminEmail') + ->willReturn('admin@e-cosplay.fr'); + + $this->mailer->expects($this->once()) + ->method('sendEmail') + ->with( + 'admin@e-cosplay.fr', + $this->stringContains('Rappel : factures prestataires'), + $this->stringContains('ACME Corp'), + null, + null, + false, + ); + + $command = new ReminderFacturesPrestataireCommand($this->em, $this->mailer); + $tester = new CommandTester($command); + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $display = $tester->getDisplay(); + $this->assertStringContainsString('2 facture(s) prestataire(s) manquante(s)', $display); + $this->assertStringContainsString('rappel envoye', $display); + } + + public function testPrestataireWithFactureForDifferentMonthStillMissing(): void + { + $now = new \DateTimeImmutable(); + $prev = $now->modify('first day of last month'); + $year = (int) $prev->format('Y'); + $month = (int) $prev->format('n'); + + // Facture for a different month (two months ago) + $wrongMonth = $month === 1 ? 12 : $month - 1; + $wrongYear = $month === 1 ? $year - 1 : $year; + + $presta = $this->makePrestataire('Old Corp', '99999999900011'); + $oldFacture = $this->makeFacturePrestataire($wrongYear, $wrongMonth); + $presta->getFactures()->add($oldFacture); + + $this->stubRepository([$presta]); + + $this->mailer->method('getAdminEmail')->willReturn('admin@e-cosplay.fr'); + $this->mailer->expects($this->once())->method('sendEmail'); + + $command = new ReminderFacturesPrestataireCommand($this->em, $this->mailer); + $tester = new CommandTester($command); + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertStringContainsString('1 facture(s) prestataire(s) manquante(s)', $tester->getDisplay()); + } + + public function testEmailContainsPrestataireDetails(): void + { + $presta = $this->makePrestataire('Dupont & Cie', '11122233300011'); + + $this->stubRepository([$presta]); + + $this->mailer->method('getAdminEmail')->willReturn('admin@e-cosplay.fr'); + + $capturedHtml = null; + $this->mailer->method('sendEmail') + ->willReturnCallback(function (string $to, string $subject, string $html) use (&$capturedHtml) { + $capturedHtml = $html; + }); + + $command = new ReminderFacturesPrestataireCommand($this->em, $this->mailer); + $tester = new CommandTester($command); + $tester->execute([]); + + $this->assertNotNull($capturedHtml); + $this->assertStringContainsString('Dupont & Cie', $capturedHtml); + $this->assertStringContainsString('11122233300011', $capturedHtml); + $this->assertStringContainsString('crm.e-cosplay.fr/admin/prestataires', $capturedHtml); + } + + public function testPrestataireWithoutSiretSendsEmailWithoutSiret(): void + { + $presta = $this->makePrestataire('Anonymous Corp'); + + $this->stubRepository([$presta]); + + $this->mailer->method('getAdminEmail')->willReturn('admin@e-cosplay.fr'); + + $capturedHtml = null; + $this->mailer->method('sendEmail') + ->willReturnCallback(function (string $to, string $subject, string $html) use (&$capturedHtml) { + $capturedHtml = $html; + }); + + $command = new ReminderFacturesPrestataireCommand($this->em, $this->mailer); + $tester = new CommandTester($command); + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertNotNull($capturedHtml); + $this->assertStringContainsString('Anonymous Corp', $capturedHtml); + } +} diff --git a/tests/Controller/Admin/ComptabiliteControllerTest.php b/tests/Controller/Admin/ComptabiliteControllerTest.php new file mode 100644 index 0000000..6c0e1f4 --- /dev/null +++ b/tests/Controller/Admin/ComptabiliteControllerTest.php @@ -0,0 +1,264 @@ +createStub(EntityManagerInterface::class); + + $query = $this->getMockBuilder(Query::class) + ->setConstructorArgs([$stubEm]) + ->onlyMethods(['getResult', '_doExecute', 'getSQL']) + ->getMock(); + $query->method('getResult')->willReturn([]); + + $qb = $this->createStub(QueryBuilder::class); + $qb->method('select')->willReturnSelf(); + $qb->method('from')->willReturnSelf(); + $qb->method('where')->willReturnSelf(); + $qb->method('andWhere')->willReturnSelf(); + $qb->method('setParameter')->willReturnSelf(); + $qb->method('orderBy')->willReturnSelf(); + $qb->method('getQuery')->willReturn($query); + + $em = $this->createStub(EntityManagerInterface::class); + $em->method('createQueryBuilder')->willReturn($qb); + + return $em; + } + + private function buildKernel(): KernelInterface + { + $tmpDir = sys_get_temp_dir().'/comptabilite_test_'.uniqid(); + mkdir($tmpDir.'/public', 0777, true); + // logo.jpg is optional — ComptaPdf checks file_exists before using it + + $kernel = $this->createStub(KernelInterface::class); + $kernel->method('getProjectDir')->willReturn($tmpDir); + + return $kernel; + } + + private function buildController(): ComptabiliteController + { + $em = $this->buildEmWithQueryBuilder(); + $kernel = $this->buildKernel(); + + $controller = new ComptabiliteController($em, $kernel, false, 'http://docuseal.example'); + + $session = new Session(new MockArraySessionStorage()); + $stack = $this->createStub(RequestStack::class); + $stack->method('getSession')->willReturn($session); + + $twig = $this->createStub(Environment::class); + $twig->method('render')->willReturn(''); + + $container = $this->createStub(ContainerInterface::class); + $container->method('has')->willReturnMap([ + ['twig', true], + ['router', true], + ['security.authorization_checker', true], + ['security.token_storage', true], + ['request_stack', true], + ['parameter_bag', true], + ['serializer', false], + ]); + $container->method('get')->willReturnMap([ + ['twig', $twig], + ['router', $this->createStub(RouterInterface::class)], + ['security.authorization_checker', $this->createStub(AuthorizationCheckerInterface::class)], + ['security.token_storage', $this->createStub(TokenStorageInterface::class)], + ['request_stack', $stack], + ['parameter_bag', $this->createStub(ParameterBagInterface::class)], + ]); + $controller->setContainer($container); + + return $controller; + } + + public function testIndexReturns200(): void + { + $controller = $this->buildController(); + $response = $controller->index(); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testExportJournalVentesCsv(): void + { + $controller = $this->buildController(); + $request = new Request(['period' => 'current', 'format' => 'csv']); + $response = $controller->exportJournalVentes($request); + $this->assertSame(200, $response->getStatusCode()); + $contentType = $response->headers->get('Content-Type') ?? ''; + $this->assertStringContainsString('text/csv', $contentType); + } + + public function testExportJournalVentesJson(): void + { + $controller = $this->buildController(); + $request = new Request(['period' => 'current', 'format' => 'json']); + $response = $controller->exportJournalVentes($request); + $this->assertSame(200, $response->getStatusCode()); + $contentType = $response->headers->get('Content-Type') ?? ''; + $this->assertStringContainsString('application/json', $contentType); + } + + public function testExportJournalVentesPreviousPeriod(): void + { + $controller = $this->buildController(); + $request = new Request(['period' => 'previous', 'format' => 'csv']); + $response = $controller->exportJournalVentes($request); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testExportJournalVentesCustomPeriod(): void + { + $controller = $this->buildController(); + $request = new Request(['period' => 'custom', 'from' => '2026-01-01', 'to' => '2026-03-31', 'format' => 'csv']); + $response = $controller->exportJournalVentes($request); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testExportCommissionsStripeCsv(): void + { + $controller = $this->buildController(); + $request = new Request(['period' => 'current', 'format' => 'csv']); + $response = $controller->exportCommissionsStripe($request); + $this->assertSame(200, $response->getStatusCode()); + $contentType = $response->headers->get('Content-Type') ?? ''; + $this->assertStringContainsString('text/csv', $contentType); + } + + public function testExportCommissionsStripeJson(): void + { + $controller = $this->buildController(); + $request = new Request(['period' => 'current', 'format' => 'json']); + $response = $controller->exportCommissionsStripe($request); + $this->assertSame(200, $response->getStatusCode()); + $contentType = $response->headers->get('Content-Type') ?? ''; + $this->assertStringContainsString('application/json', $contentType); + } + + public function testExportCoutsServicesCsv(): void + { + $controller = $this->buildController(); + $request = new Request(['period' => 'current', 'format' => 'csv']); + $response = $controller->exportCoutsServices($request); + $this->assertSame(200, $response->getStatusCode()); + $contentType = $response->headers->get('Content-Type') ?? ''; + $this->assertStringContainsString('text/csv', $contentType); + } + + public function testExportPdfJournalVentes(): void + { + $controller = $this->buildController(); + $request = new Request(['period' => 'current']); + $response = $controller->exportPdf('journal-ventes', $request); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('application/pdf', $response->headers->get('Content-Type')); + } + + public function testExportPdfCommissionsStripe(): void + { + $controller = $this->buildController(); + $request = new Request(['period' => 'current']); + $response = $controller->exportPdf('commissions-stripe', $request); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('application/pdf', $response->headers->get('Content-Type')); + } + + public function testExportPdfCoutsServices(): void + { + $controller = $this->buildController(); + $request = new Request(['period' => 'current']); + $response = $controller->exportPdf('couts-services', $request); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('application/pdf', $response->headers->get('Content-Type')); + } + + public function testExportPdfBalanceAgee(): void + { + $controller = $this->buildController(); + $request = new Request(['period' => 'current']); + $response = $controller->exportPdf('balance-agee', $request); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('application/pdf', $response->headers->get('Content-Type')); + } + + public function testRapportFinancierReturnsPdf(): void + { + $controller = $this->buildController(); + $request = new Request(['period' => 'current']); + $response = $controller->rapportFinancier($request); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('application/pdf', $response->headers->get('Content-Type')); + } + + public function testRapportFinancierPreviousPeriod(): void + { + $controller = $this->buildController(); + $request = new Request(['period' => 'previous']); + $response = $controller->rapportFinancier($request); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('application/pdf', $response->headers->get('Content-Type')); + } + + public function testExportGrandLivreCsv(): void + { + $controller = $this->buildController(); + $request = new Request(['period' => 'current', 'format' => 'csv']); + $response = $controller->exportGrandLivre($request); + $this->assertSame(200, $response->getStatusCode()); + $contentType = $response->headers->get('Content-Type') ?? ''; + $this->assertStringContainsString('text/csv', $contentType); + } + + public function testExportFecCsv(): void + { + $controller = $this->buildController(); + $request = new Request(['period' => 'current', 'format' => 'csv']); + $response = $controller->exportFec($request); + $this->assertSame(200, $response->getStatusCode()); + $contentType = $response->headers->get('Content-Type') ?? ''; + $this->assertStringContainsString('text/csv', $contentType); + } + + public function testExportBalanceAgeeCsv(): void + { + $controller = $this->buildController(); + $request = new Request(['format' => 'csv']); + $response = $controller->exportBalanceAgee($request); + $this->assertSame(200, $response->getStatusCode()); + $contentType = $response->headers->get('Content-Type') ?? ''; + $this->assertStringContainsString('text/csv', $contentType); + } + + public function testExportReglementsCsv(): void + { + $controller = $this->buildController(); + $request = new Request(['period' => 'current', 'format' => 'csv']); + $response = $controller->exportReglements($request); + $this->assertSame(200, $response->getStatusCode()); + $contentType = $response->headers->get('Content-Type') ?? ''; + $this->assertStringContainsString('text/csv', $contentType); + } +} diff --git a/tests/Controller/Admin/FactureControllerTest.php b/tests/Controller/Admin/FactureControllerTest.php new file mode 100644 index 0000000..84c9244 --- /dev/null +++ b/tests/Controller/Admin/FactureControllerTest.php @@ -0,0 +1,190 @@ +createStub(EntityManagerInterface::class); + $meilisearch ??= $this->createStub(MeilisearchService::class); + + $controller = new FactureController($em, $meilisearch); + + $session = new Session(new MockArraySessionStorage()); + $stack = $this->createStub(RequestStack::class); + $stack->method('getSession')->willReturn($session); + + $twig = $this->createStub(Environment::class); + $twig->method('render')->willReturn(''); + + $router = $this->createStub(RouterInterface::class); + $router->method('generate')->willReturn('/redirect'); + + $container = $this->createStub(ContainerInterface::class); + $container->method('has')->willReturnMap([ + ['twig', true], + ['router', true], + ['security.authorization_checker', true], + ['security.token_storage', true], + ['request_stack', true], + ['parameter_bag', true], + ['serializer', false], + ]); + $container->method('get')->willReturnMap([ + ['twig', $twig], + ['router', $router], + ['security.authorization_checker', $this->createStub(AuthorizationCheckerInterface::class)], + ['security.token_storage', $this->createStub(TokenStorageInterface::class)], + ['request_stack', $stack], + ['parameter_bag', $this->createStub(ParameterBagInterface::class)], + ]); + $controller->setContainer($container); + + return $controller; + } + + // --------------------------------------------------------------- + // search + // --------------------------------------------------------------- + + public function testSearchReturnsEmptyWhenQueryBlank(): void + { + $controller = $this->buildController(); + $request = new Request(['q' => '']); + $response = $controller->search(1, $request); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('[]', $response->getContent()); + } + + public function testSearchReturnsMeilisearchResults(): void + { + $hits = [['id' => 1, 'invoiceNumber' => 'F-2026-001']]; + + $meilisearch = $this->createStub(MeilisearchService::class); + $meilisearch->method('searchFactures')->willReturn($hits); + + $controller = $this->buildController(null, $meilisearch); + $request = new Request(['q' => 'F-2026']); + $response = $controller->search(1, $request); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); + + $data = json_decode($response->getContent(), true); + $this->assertCount(1, $data); + $this->assertSame('F-2026-001', $data[0]['invoiceNumber']); + } + + public function testSearchPassesCustomerIdFilter(): void + { + $meilisearch = $this->createMock(MeilisearchService::class); + $meilisearch->expects($this->once()) + ->method('searchFactures') + ->with('test', 20, 42) + ->willReturn([]); + + $controller = $this->buildController(null, $meilisearch); + $request = new Request(['q' => 'test']); + $response = $controller->search(42, $request); + + $this->assertInstanceOf(JsonResponse::class, $response); + } + + // --------------------------------------------------------------- + // send — facture with no PDF + // --------------------------------------------------------------- + + public function testSendRedirectsWhenNoPdfGenerated(): void + { + $customer = $this->createStub(Customer::class); + $customer->method('getId')->willReturn(5); + + $facture = $this->createStub(Facture::class); + $facture->method('getFacturePdf')->willReturn(null); + $facture->method('getCustomer')->willReturn($customer); + + $factureRepo = $this->createStub(EntityRepository::class); + $factureRepo->method('find')->willReturn($facture); + + $em = $this->createStub(EntityManagerInterface::class); + $em->method('getRepository')->willReturn($factureRepo); + + $controller = $this->buildController($em); + + $mailer = $this->createStub(\App\Service\MailerService::class); + $twig = $this->createStub(Environment::class); + $urlGenerator = $this->createStub(\Symfony\Component\Routing\Generator\UrlGeneratorInterface::class); + $urlGenerator->method('generate')->willReturn('http://localhost/facture/verify/1/abc'); + + $response = $controller->send(1, $mailer, $twig, $urlGenerator, '/tmp'); + + $this->assertSame(302, $response->getStatusCode()); + } + + public function testSendThrows404WhenFactureNotFound(): void + { + $factureRepo = $this->createStub(EntityRepository::class); + $factureRepo->method('find')->willReturn(null); + + $em = $this->createStub(EntityManagerInterface::class); + $em->method('getRepository')->willReturn($factureRepo); + + $controller = $this->buildController($em); + + $mailer = $this->createStub(\App\Service\MailerService::class); + $twig = $this->createStub(Environment::class); + $urlGenerator = $this->createStub(\Symfony\Component\Routing\Generator\UrlGeneratorInterface::class); + + $this->expectException(\Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class); + $controller->send(999, $mailer, $twig, $urlGenerator, '/tmp'); + } + + public function testSendRedirectsWhenCustomerHasNoEmail(): void + { + $customer = $this->createStub(Customer::class); + $customer->method('getId')->willReturn(5); + $customer->method('getEmail')->willReturn(null); + + $facture = $this->createStub(Facture::class); + $facture->method('getFacturePdf')->willReturn('facture.pdf'); + $facture->method('getCustomer')->willReturn($customer); + + $factureRepo = $this->createStub(EntityRepository::class); + $factureRepo->method('find')->willReturn($facture); + + $em = $this->createStub(EntityManagerInterface::class); + $em->method('getRepository')->willReturn($factureRepo); + + $controller = $this->buildController($em); + + $mailer = $this->createStub(\App\Service\MailerService::class); + $twig = $this->createStub(Environment::class); + $urlGenerator = $this->createStub(\Symfony\Component\Routing\Generator\UrlGeneratorInterface::class); + + $response = $controller->send(1, $mailer, $twig, $urlGenerator, '/tmp'); + + $this->assertSame(302, $response->getStatusCode()); + } +} diff --git a/tests/Controller/Admin/PrestatairesControllerTest.php b/tests/Controller/Admin/PrestatairesControllerTest.php new file mode 100644 index 0000000..ff449a6 --- /dev/null +++ b/tests/Controller/Admin/PrestatairesControllerTest.php @@ -0,0 +1,425 @@ +createStub(EntityManagerInterface::class); + + $controller = new PrestatairesController($em); + + $session = new Session(new MockArraySessionStorage()); + $stack = $this->createStub(RequestStack::class); + $stack->method('getSession')->willReturn($session); + + $twig = $this->createStub(Environment::class); + $twig->method('render')->willReturn(''); + + $router = $this->createStub(RouterInterface::class); + $router->method('generate')->willReturn('/redirect'); + + $container = $this->createStub(ContainerInterface::class); + $container->method('has')->willReturnMap([ + ['twig', true], + ['router', true], + ['security.authorization_checker', true], + ['security.token_storage', true], + ['request_stack', true], + ['parameter_bag', true], + ['serializer', false], + ]); + $container->method('get')->willReturnMap([ + ['twig', $twig], + ['router', $router], + ['security.authorization_checker', $this->createStub(AuthorizationCheckerInterface::class)], + ['security.token_storage', $this->createStub(TokenStorageInterface::class)], + ['request_stack', $stack], + ['parameter_bag', $this->createStub(ParameterBagInterface::class)], + ]); + $controller->setContainer($container); + + return $controller; + } + + private function buildPrestataire(int $id = 1, string $raisonSociale = 'ACME SA'): Prestataire + { + $prestataire = new Prestataire($raisonSociale); + // Force a non-null id via Reflection + $ref = new \ReflectionProperty(Prestataire::class, 'id'); + $ref->setAccessible(true); + $ref->setValue($prestataire, $id); + + return $prestataire; + } + + // --------------------------------------------------------------- + // index + // --------------------------------------------------------------- + + public function testIndexReturns200(): void + { + $repo = $this->createStub(PrestataireRepository::class); + $repo->method('findBy')->willReturn([]); + + $controller = $this->buildController(); + $response = $controller->index($repo); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testIndexWithPrestataires(): void + { + $repo = $this->createStub(PrestataireRepository::class); + $repo->method('findBy')->willReturn([ + $this->buildPrestataire(1, 'ACME SA'), + $this->buildPrestataire(2, 'Example SAS'), + ]); + + $controller = $this->buildController(); + $response = $controller->index($repo); + + $this->assertSame(200, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // create + // --------------------------------------------------------------- + + public function testCreateRedirectsOnSuccess(): void + { + $em = $this->createMock(EntityManagerInterface::class); + $em->expects($this->once())->method('persist'); + $em->expects($this->once())->method('flush'); + + $controller = $this->buildController($em); + + $request = new Request([], ['raisonSociale' => 'Nouveau Prestataire', 'email' => 'test@example.com']); + $response = $controller->create($request); + + $this->assertSame(302, $response->getStatusCode()); + } + + public function testCreateRedirectsWhenRaisonSocialeEmpty(): void + { + $em = $this->createMock(EntityManagerInterface::class); + $em->expects($this->never())->method('persist'); + + $controller = $this->buildController($em); + + $request = new Request([], ['raisonSociale' => ' ']); + $response = $controller->create($request); + + $this->assertSame(302, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // show + // --------------------------------------------------------------- + + public function testShowReturns200(): void + { + $prestataire = $this->buildPrestataire(); + $controller = $this->buildController(); + $response = $controller->show($prestataire); + + $this->assertSame(200, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // edit + // --------------------------------------------------------------- + + public function testEditFlushesAndRedirects(): void + { + $em = $this->createMock(EntityManagerInterface::class); + $em->expects($this->once())->method('flush'); + + $prestataire = $this->buildPrestataire(); + $controller = $this->buildController($em); + + $request = new Request([], [ + 'raisonSociale' => 'ACME Modifie', + 'email' => 'contact@acme.fr', + 'phone' => '0600000000', + 'siret' => '12345678901234', + 'address' => '1 rue de la Paix', + 'zipCode' => '75001', + 'city' => 'Paris', + ]); + $response = $controller->edit($prestataire, $request); + + $this->assertSame(302, $response->getStatusCode()); + } + + public function testEditKeepsRaisonSocialeWhenEmpty(): void + { + $em = $this->createMock(EntityManagerInterface::class); + $em->expects($this->once())->method('flush'); + + $prestataire = $this->buildPrestataire(1, 'Nom Original'); + $controller = $this->buildController($em); + + // Empty raisonSociale should keep the original + $request = new Request([], ['raisonSociale' => '']); + $controller->edit($prestataire, $request); + + $this->assertSame('Nom Original', $prestataire->getRaisonSociale()); + } + + // --------------------------------------------------------------- + // delete + // --------------------------------------------------------------- + + public function testDeleteRemovesAndRedirects(): void + { + $em = $this->createMock(EntityManagerInterface::class); + $em->expects($this->once())->method('remove'); + $em->expects($this->once())->method('flush'); + + $prestataire = $this->buildPrestataire(); + $controller = $this->buildController($em); + + $response = $controller->delete($prestataire); + + $this->assertSame(302, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // addFacture + // --------------------------------------------------------------- + + public function testAddFactureRedirectsOnInvalidData(): void + { + $em = $this->createMock(EntityManagerInterface::class); + $em->expects($this->never())->method('persist'); + + $prestataire = $this->buildPrestataire(); + $controller = $this->buildController($em); + + // Missing numFacture + $request = new Request([], ['numFacture' => '', 'year' => 2026, 'month' => 3]); + $response = $controller->addFacture($prestataire, $request); + + $this->assertSame(302, $response->getStatusCode()); + } + + public function testAddFactureRedirectsOnInvalidYear(): void + { + $em = $this->createMock(EntityManagerInterface::class); + $em->expects($this->never())->method('persist'); + + $prestataire = $this->buildPrestataire(); + $controller = $this->buildController($em); + + $request = new Request([], ['numFacture' => 'FAC001', 'year' => 2010, 'month' => 3, 'montantHt' => '100', 'montantTtc' => '120']); + $response = $controller->addFacture($prestataire, $request); + + $this->assertSame(302, $response->getStatusCode()); + } + + public function testAddFacturePersistsAndRedirects(): void + { + $em = $this->createMock(EntityManagerInterface::class); + $em->expects($this->once())->method('persist'); + $em->expects($this->once())->method('flush'); + + $prestataire = $this->buildPrestataire(); + $controller = $this->buildController($em); + + $request = new Request([], [ + 'numFacture' => 'FAC-2026-001', + 'year' => 2026, + 'month' => 3, + 'montantHt' => '500.00', + 'montantTtc' => '600.00', + ]); + $response = $controller->addFacture($prestataire, $request); + + $this->assertSame(302, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // markPaid + // --------------------------------------------------------------- + + public function testMarkPaidFlushesAndRedirects(): void + { + $prestataire = $this->buildPrestataire(1); + + $facture = $this->createMock(FacturePrestataire::class); + $facture->method('getPrestataire')->willReturn($prestataire); + $facture->method('getNumFacture')->willReturn('FAC-001'); + + $factureRepo = $this->createMock(EntityRepository::class); + $factureRepo->method('find')->with(42)->willReturn($facture); + + $em = $this->createMock(EntityManagerInterface::class); + $em->method('getRepository')->with(FacturePrestataire::class)->willReturn($factureRepo); + $em->expects($this->once())->method('flush'); + + $controller = $this->buildController($em); + $response = $controller->markPaid($prestataire, 42); + + $this->assertSame(302, $response->getStatusCode()); + } + + public function testMarkPaidIgnoresMismatchedPrestataire(): void + { + $prestataire = $this->buildPrestataire(1); + $otherPrestataire = $this->buildPrestataire(2, 'Other'); + + $facture = $this->createMock(FacturePrestataire::class); + $facture->method('getPrestataire')->willReturn($otherPrestataire); + + $factureRepo = $this->createStub(EntityRepository::class); + $factureRepo->method('find')->willReturn($facture); + + $em = $this->createMock(EntityManagerInterface::class); + $em->method('getRepository')->willReturn($factureRepo); + $em->expects($this->never())->method('flush'); + + $controller = $this->buildController($em); + $response = $controller->markPaid($prestataire, 99); + + $this->assertSame(302, $response->getStatusCode()); + } + + public function testMarkPaidWhenFactureNotFound(): void + { + $prestataire = $this->buildPrestataire(1); + + $factureRepo = $this->createStub(EntityRepository::class); + $factureRepo->method('find')->willReturn(null); + + $em = $this->createMock(EntityManagerInterface::class); + $em->method('getRepository')->willReturn($factureRepo); + $em->expects($this->never())->method('flush'); + + $controller = $this->buildController($em); + $response = $controller->markPaid($prestataire, 999); + + $this->assertSame(302, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // deleteFacture + // --------------------------------------------------------------- + + public function testDeleteFactureRemovesAndRedirects(): void + { + $prestataire = $this->buildPrestataire(1); + + $facture = $this->createMock(FacturePrestataire::class); + $facture->method('getPrestataire')->willReturn($prestataire); + + $factureRepo = $this->createStub(EntityRepository::class); + $factureRepo->method('find')->willReturn($facture); + + $em = $this->createMock(EntityManagerInterface::class); + $em->method('getRepository')->willReturn($factureRepo); + $em->expects($this->once())->method('remove'); + $em->expects($this->once())->method('flush'); + + $controller = $this->buildController($em); + $response = $controller->deleteFacture($prestataire, 42); + + $this->assertSame(302, $response->getStatusCode()); + } + + public function testDeleteFactureWhenNotFound(): void + { + $prestataire = $this->buildPrestataire(1); + + $factureRepo = $this->createStub(EntityRepository::class); + $factureRepo->method('find')->willReturn(null); + + $em = $this->createMock(EntityManagerInterface::class); + $em->method('getRepository')->willReturn($factureRepo); + $em->expects($this->never())->method('remove'); + + $controller = $this->buildController($em); + $response = $controller->deleteFacture($prestataire, 999); + + $this->assertSame(302, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // entrepriseSearch + // --------------------------------------------------------------- + + public function testEntrepriseSearchReturnsEmptyWhenQueryTooShort(): void + { + $httpClient = $this->createStub(HttpClientInterface::class); + $controller = $this->buildController(); + + $request = new Request(['q' => 'a']); + $response = $controller->entrepriseSearch($request, $httpClient); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); + + $data = json_decode($response->getContent(), true); + $this->assertSame([], $data['results']); + $this->assertSame(0, $data['total_results']); + } + + public function testEntrepriseSearchForwardsApiResponse(): void + { + $apiData = ['results' => [['nom_complet' => 'ACME SA']], 'total_results' => 1]; + + $httpResponse = $this->createStub(HttpResponseInterface::class); + $httpResponse->method('toArray')->willReturn($apiData); + + $httpClient = $this->createStub(HttpClientInterface::class); + $httpClient->method('request')->willReturn($httpResponse); + + $controller = $this->buildController(); + + $request = new Request(['q' => 'ACME']); + $response = $controller->entrepriseSearch($request, $httpClient); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); + $data = json_decode($response->getContent(), true); + $this->assertSame(1, $data['total_results']); + } + + public function testEntrepriseSearchHandlesHttpError(): void + { + $httpClient = $this->createStub(HttpClientInterface::class); + $httpClient->method('request')->willThrowException(new \RuntimeException('Network error')); + + $controller = $this->buildController(); + + $request = new Request(['q' => 'ACME']); + $response = $controller->entrepriseSearch($request, $httpClient); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertSame(502, $response->getStatusCode()); + } +} diff --git a/tests/Controller/FactureVerifyControllerTest.php b/tests/Controller/FactureVerifyControllerTest.php new file mode 100644 index 0000000..9ce0998 --- /dev/null +++ b/tests/Controller/FactureVerifyControllerTest.php @@ -0,0 +1,138 @@ +createStub(Environment::class); + $twig->method('render')->willReturn(''); + + $container = $this->createStub(ContainerInterface::class); + $container->method('has')->willReturnMap([ + ['twig', true], + ['parameter_bag', true], + ]); + $container->method('get')->willReturnMap([ + ['twig', $twig], + ['parameter_bag', $this->createStub(ParameterBagInterface::class)], + ]); + + $controller = new FactureVerifyController(); + $controller->setContainer($container); + + return $controller; + } + + private function buildEmWithFacture(?Facture $facture): EntityManagerInterface + { + $factureRepo = $this->createStub(EntityRepository::class); + $factureRepo->method('find')->willReturn($facture); + + $em = $this->createStub(EntityManagerInterface::class); + $em->method('getRepository')->willReturn($factureRepo); + + return $em; + } + + // --------------------------------------------------------------- + // Valid HMAC — facture found with correct hmac + // --------------------------------------------------------------- + + public function testIndexReturnsVerifyPageWhenHmacMatches(): void + { + $customer = $this->createStub(Customer::class); + + $facture = $this->createStub(Facture::class); + $facture->method('getHmac')->willReturn('abc123'); + $facture->method('getCustomer')->willReturn($customer); + + $em = $this->buildEmWithFacture($facture); + $controller = $this->buildController(); + + $response = $controller->index(1, 'abc123', $em); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(200, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // Invalid HMAC — facture found but wrong hmac + // --------------------------------------------------------------- + + public function testIndexReturnsInvalidPageWhenHmacMismatch(): void + { + $facture = $this->createStub(Facture::class); + $facture->method('getHmac')->willReturn('correct_hmac'); + + $em = $this->buildEmWithFacture($facture); + + $twig = $this->createStub(Environment::class); + $twig->method('render')->willReturn('Invalid'); + + $controller = $this->buildController($twig); + + $response = $controller->index(1, 'wrong_hmac', $em); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(200, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // Facture not found + // --------------------------------------------------------------- + + public function testIndexReturnsInvalidPageWhenFactureNotFound(): void + { + $em = $this->buildEmWithFacture(null); + $controller = $this->buildController(); + + $response = $controller->index(999, 'any_hmac', $em); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(200, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // HMAC timing-safe comparison (hash_equals) + // --------------------------------------------------------------- + + public function testIndexUsesHashEqualsForComparison(): void + { + // Craft two hmac values that differ only in length to ensure no + // trivial == comparison is used (hash_equals handles timing safety) + $storedHmac = hash_hmac('sha256', 'data', 'secret'); + $differentHmac = hash_hmac('sha256', 'other', 'secret'); + + $facture = $this->createStub(Facture::class); + $facture->method('getHmac')->willReturn($storedHmac); + + $em = $this->buildEmWithFacture($facture); + $controller = $this->buildController(); + + // Wrong hmac -> invalid page (still 200 but renders invalid template) + $response = $controller->index(1, $differentHmac, $em); + $this->assertSame(200, $response->getStatusCode()); + + // Correct hmac -> verify page + $facture2 = $this->createStub(Facture::class); + $facture2->method('getHmac')->willReturn($storedHmac); + $facture2->method('getCustomer')->willReturn(null); + + $em2 = $this->buildEmWithFacture($facture2); + $response2 = $controller->index(1, $storedHmac, $em2); + $this->assertSame(200, $response2->getStatusCode()); + } +} diff --git a/tests/Entity/ActionLogTest.php b/tests/Entity/ActionLogTest.php new file mode 100644 index 0000000..d96506c --- /dev/null +++ b/tests/Entity/ActionLogTest.php @@ -0,0 +1,156 @@ +setEmail('test@test.com'); + $user->setFirstName('John'); + $user->setLastName('Doe'); + $user->setPassword('hashed'); + + return new Customer($user); + } + + public function testConstructorDefaults(): void + { + $log = new ActionLog(ActionLog::ACTION_SUSPEND_CUSTOMER, 'Client suspendu.'); + + $this->assertNull($log->getId()); + $this->assertSame(ActionLog::ACTION_SUSPEND_CUSTOMER, $log->getAction()); + $this->assertSame('Client suspendu.', $log->getMessage()); + $this->assertSame('info', $log->getSeverity()); + $this->assertTrue($log->isSuccess()); + $this->assertNull($log->getCustomer()); + $this->assertNull($log->getEntityId()); + $this->assertNull($log->getEntityType()); + $this->assertNull($log->getContext()); + $this->assertNull($log->getPreviousState()); + $this->assertNull($log->getNewState()); + $this->assertNull($log->getErrorMessage()); + $this->assertInstanceOf(\DateTimeImmutable::class, $log->getCreatedAt()); + } + + public function testConstructorWithAllArgs(): void + { + $log = new ActionLog(ActionLog::ACTION_FORMAL_NOTICE, 'Mise en demeure envoyée.', 'critical', false); + + $this->assertSame('critical', $log->getSeverity()); + $this->assertFalse($log->isSuccess()); + } + + public function testCustomer(): void + { + $log = new ActionLog(ActionLog::ACTION_SUSPEND_CUSTOMER, 'Test'); + $this->assertNull($log->getCustomer()); + + $customer = $this->createCustomer(); + $log->setCustomer($customer); + $this->assertSame($customer, $log->getCustomer()); + + $log->setCustomer(null); + $this->assertNull($log->getCustomer()); + } + + public function testEntityIdAndType(): void + { + $log = new ActionLog(ActionLog::ACTION_SUSPEND_WEBSITE, 'Test'); + + $log->setEntityId(123); + $this->assertSame(123, $log->getEntityId()); + + $log->setEntityType('Website'); + $this->assertSame('Website', $log->getEntityType()); + + $log->setEntityId(null); + $this->assertNull($log->getEntityId()); + + $log->setEntityType(null); + $this->assertNull($log->getEntityType()); + } + + public function testContext(): void + { + $log = new ActionLog(ActionLog::ACTION_SUSPEND_CUSTOMER, 'Test'); + $this->assertNull($log->getContext()); + + $log->setContext('{"key":"value"}'); + $this->assertSame('{"key":"value"}', $log->getContext()); + + $log->setContext(null); + $this->assertNull($log->getContext()); + } + + public function testPreviousAndNewState(): void + { + $log = new ActionLog(ActionLog::ACTION_SUSPEND_CUSTOMER, 'Test'); + + $log->setPreviousState('active'); + $this->assertSame('active', $log->getPreviousState()); + + $log->setNewState('suspended'); + $this->assertSame('suspended', $log->getNewState()); + + $log->setPreviousState(null); + $this->assertNull($log->getPreviousState()); + + $log->setNewState(null); + $this->assertNull($log->getNewState()); + } + + public function testSetErrorMessageSetsSuccessFalse(): void + { + $log = new ActionLog(ActionLog::ACTION_SUSPEND_CUSTOMER, 'Test'); + $this->assertTrue($log->isSuccess()); + $this->assertNull($log->getErrorMessage()); + + $log->setErrorMessage('Une erreur inattendue s\'est produite.'); + $this->assertSame("Une erreur inattendue s'est produite.", $log->getErrorMessage()); + $this->assertFalse($log->isSuccess()); + } + + public function testSetErrorMessageNull(): void + { + $log = new ActionLog(ActionLog::ACTION_SUSPEND_CUSTOMER, 'Test', 'info', false); + + $log->setErrorMessage(null); + $this->assertNull($log->getErrorMessage()); + // Setting null still sets success to false + $this->assertFalse($log->isSuccess()); + } + + public function testActionConstants(): void + { + $this->assertSame('suspend_customer', ActionLog::ACTION_SUSPEND_CUSTOMER); + $this->assertSame('unsuspend_customer', ActionLog::ACTION_UNSUSPEND_CUSTOMER); + $this->assertSame('suspend_website', ActionLog::ACTION_SUSPEND_WEBSITE); + $this->assertSame('unsuspend_website', ActionLog::ACTION_UNSUSPEND_WEBSITE); + $this->assertSame('suspend_domain_email', ActionLog::ACTION_SUSPEND_DOMAIN_EMAIL); + $this->assertSame('unsuspend_domain_email', ActionLog::ACTION_UNSUSPEND_DOMAIN_EMAIL); + $this->assertSame('disable_customer', ActionLog::ACTION_DISABLE_CUSTOMER); + $this->assertSame('delete_customer_data', ActionLog::ACTION_DELETE_CUSTOMER_DATA); + $this->assertSame('formal_notice', ActionLog::ACTION_FORMAL_NOTICE); + $this->assertSame('termination', ActionLog::ACTION_TERMINATION); + } + + public function testSettersReturnStatic(): void + { + $log = new ActionLog(ActionLog::ACTION_SUSPEND_CUSTOMER, 'Test'); + + $this->assertSame($log, $log->setCustomer(null)); + $this->assertSame($log, $log->setEntityId(1)); + $this->assertSame($log, $log->setEntityType('T')); + $this->assertSame($log, $log->setContext('{}')); + $this->assertSame($log, $log->setPreviousState('a')); + $this->assertSame($log, $log->setNewState('b')); + $this->assertSame($log, $log->setErrorMessage('err')); + } +} diff --git a/tests/Entity/AdvertEventTest.php b/tests/Entity/AdvertEventTest.php new file mode 100644 index 0000000..4ef4080 --- /dev/null +++ b/tests/Entity/AdvertEventTest.php @@ -0,0 +1,97 @@ +createAdvert(); + $event = new AdvertEvent($advert, AdvertEvent::TYPE_VIEW); + + $this->assertNull($event->getId()); + $this->assertSame($advert, $event->getAdvert()); + $this->assertSame(AdvertEvent::TYPE_VIEW, $event->getType()); + $this->assertNull($event->getDetails()); + $this->assertNull($event->getIp()); + $this->assertInstanceOf(\DateTimeImmutable::class, $event->getCreatedAt()); + } + + public function testConstructorWithAllArgs(): void + { + $advert = $this->createAdvert(); + $event = new AdvertEvent($advert, AdvertEvent::TYPE_PAY, 'Paiement CB', '192.168.1.1'); + + $this->assertSame(AdvertEvent::TYPE_PAY, $event->getType()); + $this->assertSame('Paiement CB', $event->getDetails()); + $this->assertSame('192.168.1.1', $event->getIp()); + } + + public function testTypeConstants(): void + { + $this->assertSame('view', AdvertEvent::TYPE_VIEW); + $this->assertSame('pay', AdvertEvent::TYPE_PAY); + $this->assertSame('mail_open', AdvertEvent::TYPE_MAIL_OPEN); + $this->assertSame('mail_send', AdvertEvent::TYPE_MAIL_SEND); + $this->assertSame('reminder', AdvertEvent::TYPE_REMINDER); + } + + public function testGetTypeLabelView(): void + { + $event = new AdvertEvent($this->createAdvert(), AdvertEvent::TYPE_VIEW); + $this->assertSame('Page consultee', $event->getTypeLabel()); + } + + public function testGetTypeLabelPay(): void + { + $event = new AdvertEvent($this->createAdvert(), AdvertEvent::TYPE_PAY); + $this->assertSame('Paiement effectue', $event->getTypeLabel()); + } + + public function testGetTypeLabelMailOpen(): void + { + $event = new AdvertEvent($this->createAdvert(), AdvertEvent::TYPE_MAIL_OPEN); + $this->assertSame('Email ouvert', $event->getTypeLabel()); + } + + public function testGetTypeLabelMailSend(): void + { + $event = new AdvertEvent($this->createAdvert(), AdvertEvent::TYPE_MAIL_SEND); + $this->assertSame('Email envoye', $event->getTypeLabel()); + } + + public function testGetTypeLabelReminder(): void + { + $event = new AdvertEvent($this->createAdvert(), AdvertEvent::TYPE_REMINDER); + $this->assertSame('Relance envoyee', $event->getTypeLabel()); + } + + public function testGetTypeLabelUnknown(): void + { + $event = new AdvertEvent($this->createAdvert(), 'custom_type'); + $this->assertSame('custom_type', $event->getTypeLabel()); + } + + public function testCreatedAtTimestamp(): void + { + $advert = $this->createAdvert(); + $before = new \DateTimeImmutable(); + $event = new AdvertEvent($advert, AdvertEvent::TYPE_VIEW); + $after = new \DateTimeImmutable(); + + $this->assertGreaterThanOrEqual($before, $event->getCreatedAt()); + $this->assertLessThanOrEqual($after, $event->getCreatedAt()); + } +} diff --git a/tests/Entity/AdvertPaymentTest.php b/tests/Entity/AdvertPaymentTest.php new file mode 100644 index 0000000..f45f6f8 --- /dev/null +++ b/tests/Entity/AdvertPaymentTest.php @@ -0,0 +1,79 @@ +createAdvert(); + $payment = new AdvertPayment($advert, AdvertPayment::TYPE_SUCCESS, '199.00'); + + $this->assertNull($payment->getId()); + $this->assertSame($advert, $payment->getAdvert()); + $this->assertSame(AdvertPayment::TYPE_SUCCESS, $payment->getType()); + $this->assertSame('199.00', $payment->getAmount()); + $this->assertNull($payment->getMethod()); + $this->assertInstanceOf(\DateTimeImmutable::class, $payment->getCreatedAt()); + } + + public function testTypeRefused(): void + { + $advert = $this->createAdvert(); + $payment = new AdvertPayment($advert, AdvertPayment::TYPE_REFUSED, '0.00'); + + $this->assertSame(AdvertPayment::TYPE_REFUSED, $payment->getType()); + } + + public function testMethod(): void + { + $advert = $this->createAdvert(); + $payment = new AdvertPayment($advert, AdvertPayment::TYPE_SUCCESS, '50.00'); + + $this->assertNull($payment->getMethod()); + + $payment->setMethod('card'); + $this->assertSame('card', $payment->getMethod()); + + $payment->setMethod(null); + $this->assertNull($payment->getMethod()); + } + + public function testMethodReturnsStatic(): void + { + $advert = $this->createAdvert(); + $payment = new AdvertPayment($advert, AdvertPayment::TYPE_SUCCESS, '50.00'); + + $result = $payment->setMethod('sepa'); + $this->assertSame($payment, $result); + } + + public function testTypeConstants(): void + { + $this->assertSame('success', AdvertPayment::TYPE_SUCCESS); + $this->assertSame('refused', AdvertPayment::TYPE_REFUSED); + } + + public function testCreatedAtIsImmutable(): void + { + $advert = $this->createAdvert(); + $before = new \DateTimeImmutable(); + $payment = new AdvertPayment($advert, AdvertPayment::TYPE_SUCCESS, '99.00'); + $after = new \DateTimeImmutable(); + + $this->assertGreaterThanOrEqual($before, $payment->getCreatedAt()); + $this->assertLessThanOrEqual($after, $payment->getCreatedAt()); + } +} diff --git a/tests/Entity/DocusealEventTest.php b/tests/Entity/DocusealEventTest.php new file mode 100644 index 0000000..4cfde6b --- /dev/null +++ b/tests/Entity/DocusealEventTest.php @@ -0,0 +1,75 @@ +assertNull($event->getId()); + $this->assertSame('submission', $event->getType()); + $this->assertSame('completed', $event->getEventType()); + $this->assertNull($event->getSubmissionId()); + $this->assertNull($event->getSubmitterId()); + $this->assertNull($event->getPayload()); + $this->assertInstanceOf(\DateTimeImmutable::class, $event->getCreatedAt()); + } + + public function testConstructorWithAllArgs(): void + { + $payload = '{"submission_id":42,"submitter_id":7,"status":"completed"}'; + $event = new DocusealEvent('submitter', 'opened', 42, 7, $payload); + + $this->assertSame('submitter', $event->getType()); + $this->assertSame('opened', $event->getEventType()); + $this->assertSame(42, $event->getSubmissionId()); + $this->assertSame(7, $event->getSubmitterId()); + $this->assertSame($payload, $event->getPayload()); + } + + public function testSubmissionIdNullable(): void + { + $event = new DocusealEvent('submission', 'completed', null, null); + $this->assertNull($event->getSubmissionId()); + } + + public function testSubmitterIdNullable(): void + { + $event = new DocusealEvent('submission', 'completed', 10, null); + $this->assertSame(10, $event->getSubmissionId()); + $this->assertNull($event->getSubmitterId()); + } + + public function testPayloadNullable(): void + { + $event = new DocusealEvent('submission', 'completed', 1, 2, null); + $this->assertNull($event->getPayload()); + } + + public function testCreatedAtTimestamp(): void + { + $before = new \DateTimeImmutable(); + $event = new DocusealEvent('submission', 'completed'); + $after = new \DateTimeImmutable(); + + $this->assertGreaterThanOrEqual($before, $event->getCreatedAt()); + $this->assertLessThanOrEqual($after, $event->getCreatedAt()); + } + + public function testTypesAreImmutableAfterConstruction(): void + { + $event = new DocusealEvent('form', 'signed', 100, 200, '{}'); + + // Getters must return exactly what was passed in + $this->assertSame('form', $event->getType()); + $this->assertSame('signed', $event->getEventType()); + $this->assertSame(100, $event->getSubmissionId()); + $this->assertSame(200, $event->getSubmitterId()); + $this->assertSame('{}', $event->getPayload()); + } +} diff --git a/tests/Entity/FactureLineTest.php b/tests/Entity/FactureLineTest.php new file mode 100644 index 0000000..2f425a2 --- /dev/null +++ b/tests/Entity/FactureLineTest.php @@ -0,0 +1,120 @@ +createFacture(); + $line = new FactureLine($facture, 'Hébergement web'); + + $this->assertNull($line->getId()); + $this->assertSame($facture, $line->getFacture()); + $this->assertSame('Hébergement web', $line->getTitle()); + $this->assertSame('0.00', $line->getPriceHt()); + $this->assertSame(0, $line->getPos()); + $this->assertNull($line->getDescription()); + $this->assertNull($line->getType()); + $this->assertNull($line->getServiceId()); + } + + public function testConstructorWithAllArgs(): void + { + $facture = $this->createFacture(); + $line = new FactureLine($facture, 'Nom de domaine .fr', '12.00', 2); + + $this->assertSame('Nom de domaine .fr', $line->getTitle()); + $this->assertSame('12.00', $line->getPriceHt()); + $this->assertSame(2, $line->getPos()); + } + + public function testTitle(): void + { + $facture = $this->createFacture(); + $line = new FactureLine($facture, 'Initial'); + + $line->setTitle('Nouveau titre'); + $this->assertSame('Nouveau titre', $line->getTitle()); + } + + public function testPriceHt(): void + { + $facture = $this->createFacture(); + $line = new FactureLine($facture, 'Test'); + + $line->setPriceHt('249.99'); + $this->assertSame('249.99', $line->getPriceHt()); + } + + public function testPos(): void + { + $facture = $this->createFacture(); + $line = new FactureLine($facture, 'Test'); + + $this->assertSame(0, $line->getPos()); + $line->setPos(5); + $this->assertSame(5, $line->getPos()); + } + + public function testDescription(): void + { + $facture = $this->createFacture(); + $line = new FactureLine($facture, 'Test'); + + $this->assertNull($line->getDescription()); + $line->setDescription('Description détaillée du service.'); + $this->assertSame('Description détaillée du service.', $line->getDescription()); + $line->setDescription(null); + $this->assertNull($line->getDescription()); + } + + public function testType(): void + { + $facture = $this->createFacture(); + $line = new FactureLine($facture, 'Test'); + + $this->assertNull($line->getType()); + $line->setType('hosting'); + $this->assertSame('hosting', $line->getType()); + $line->setType(null); + $this->assertNull($line->getType()); + } + + public function testServiceId(): void + { + $facture = $this->createFacture(); + $line = new FactureLine($facture, 'Test'); + + $this->assertNull($line->getServiceId()); + $line->setServiceId(42); + $this->assertSame(42, $line->getServiceId()); + $line->setServiceId(null); + $this->assertNull($line->getServiceId()); + } + + public function testSettersReturnStatic(): void + { + $facture = $this->createFacture(); + $line = new FactureLine($facture, 'Test'); + + $this->assertSame($line, $line->setTitle('T')); + $this->assertSame($line, $line->setPriceHt('1.00')); + $this->assertSame($line, $line->setPos(1)); + $this->assertSame($line, $line->setDescription('D')); + $this->assertSame($line, $line->setType('type')); + $this->assertSame($line, $line->setServiceId(1)); + } +} diff --git a/tests/Entity/FacturePrestataireTest.php b/tests/Entity/FacturePrestataireTest.php new file mode 100644 index 0000000..a1a5b4a --- /dev/null +++ b/tests/Entity/FacturePrestataireTest.php @@ -0,0 +1,140 @@ +createPrestataire(); + $f = new FacturePrestataire($p, 'FACT-2026-001', 2026, 4); + + $this->assertNull($f->getId()); + $this->assertSame($p, $f->getPrestataire()); + $this->assertSame('FACT-2026-001', $f->getNumFacture()); + $this->assertSame(2026, $f->getYear()); + $this->assertSame(4, $f->getMonth()); + $this->assertSame('0.00', $f->getMontantHt()); + $this->assertSame('0.00', $f->getMontantTtc()); + $this->assertFalse($f->isPaid()); + $this->assertNull($f->getPaidAt()); + $this->assertNull($f->getFacturePdf()); + $this->assertNull($f->getFacturePdfFile()); + $this->assertNull($f->getUpdatedAt()); + $this->assertInstanceOf(\DateTimeImmutable::class, $f->getCreatedAt()); + } + + public function testNumFacture(): void + { + $f = new FacturePrestataire($this->createPrestataire(), 'OLD-001', 2026, 1); + $f->setNumFacture('NEW-002'); + $this->assertSame('NEW-002', $f->getNumFacture()); + } + + public function testMontants(): void + { + $f = new FacturePrestataire($this->createPrestataire(), 'FACT-001', 2026, 3); + + $f->setMontantHt('1500.00'); + $this->assertSame('1500.00', $f->getMontantHt()); + + $f->setMontantTtc('1800.00'); + $this->assertSame('1800.00', $f->getMontantTtc()); + } + + public function testYearAndMonth(): void + { + $f = new FacturePrestataire($this->createPrestataire(), 'FACT-001', 2025, 12); + $f->setYear(2026); + $this->assertSame(2026, $f->getYear()); + $f->setMonth(1); + $this->assertSame(1, $f->getMonth()); + } + + public function testIsPaid(): void + { + $f = new FacturePrestataire($this->createPrestataire(), 'FACT-001', 2026, 4); + $this->assertFalse($f->isPaid()); + + $f->setIsPaid(true); + $this->assertTrue($f->isPaid()); + + $f->setIsPaid(false); + $this->assertFalse($f->isPaid()); + } + + public function testPaidAt(): void + { + $f = new FacturePrestataire($this->createPrestataire(), 'FACT-001', 2026, 4); + $this->assertNull($f->getPaidAt()); + + $now = new \DateTimeImmutable(); + $f->setPaidAt($now); + $this->assertSame($now, $f->getPaidAt()); + + $f->setPaidAt(null); + $this->assertNull($f->getPaidAt()); + } + + public function testFacturePdf(): void + { + $f = new FacturePrestataire($this->createPrestataire(), 'FACT-001', 2026, 4); + $this->assertNull($f->getFacturePdf()); + + $f->setFacturePdf('facture-2026-001.pdf'); + $this->assertSame('facture-2026-001.pdf', $f->getFacturePdf()); + + $f->setFacturePdf(null); + $this->assertNull($f->getFacturePdf()); + } + + public function testSetFacturePdfFileTriggersUpdatedAt(): void + { + $f = new FacturePrestataire($this->createPrestataire(), 'FACT-001', 2026, 4); + $this->assertNull($f->getUpdatedAt()); + + $file = $this->createStub(File::class); + $f->setFacturePdfFile($file); + + $this->assertSame($file, $f->getFacturePdfFile()); + $this->assertInstanceOf(\DateTimeImmutable::class, $f->getUpdatedAt()); + } + + public function testSetFacturePdfFileNullDoesNotSetUpdatedAt(): void + { + $f = new FacturePrestataire($this->createPrestataire(), 'FACT-001', 2026, 4); + $f->setFacturePdfFile(null); + $this->assertNull($f->getUpdatedAt()); + $this->assertNull($f->getFacturePdfFile()); + } + + public function testGetPeriodLabelKnownMonths(): void + { + $months = [ + 1 => 'Janvier', 2 => 'Fevrier', 3 => 'Mars', 4 => 'Avril', + 5 => 'Mai', 6 => 'Juin', 7 => 'Juillet', 8 => 'Aout', + 9 => 'Septembre', 10 => 'Octobre', 11 => 'Novembre', 12 => 'Decembre', + ]; + + foreach ($months as $num => $name) { + $f = new FacturePrestataire($this->createPrestataire(), 'F', 2026, $num); + $this->assertSame($name.' 2026', $f->getPeriodLabel()); + } + } + + public function testGetPeriodLabelUnknownMonth(): void + { + $f = new FacturePrestataire($this->createPrestataire(), 'F', 2026, 13); + $this->assertSame('13 2026', $f->getPeriodLabel()); + } +} diff --git a/tests/Entity/PaymentReminderTest.php b/tests/Entity/PaymentReminderTest.php new file mode 100644 index 0000000..7dc7751 --- /dev/null +++ b/tests/Entity/PaymentReminderTest.php @@ -0,0 +1,138 @@ +createAdvert(); + $reminder = new PaymentReminder($advert, PaymentReminder::STEP_REMINDER_15); + + $this->assertNull($reminder->getId()); + $this->assertSame($advert, $reminder->getAdvert()); + $this->assertSame(PaymentReminder::STEP_REMINDER_15, $reminder->getStep()); + $this->assertNull($reminder->getDetails()); + $this->assertInstanceOf(\DateTimeImmutable::class, $reminder->getSentAt()); + } + + public function testConstructorWithDetails(): void + { + $advert = $this->createAdvert(); + $reminder = new PaymentReminder($advert, PaymentReminder::STEP_FORMAL_NOTICE, 'Email envoyé à client@test.fr'); + + $this->assertSame('Email envoyé à client@test.fr', $reminder->getDetails()); + } + + public function testStepConstants(): void + { + $this->assertSame('reminder_15', PaymentReminder::STEP_REMINDER_15); + $this->assertSame('warning_10', PaymentReminder::STEP_WARNING_10); + $this->assertSame('suspension_5', PaymentReminder::STEP_SUSPENSION_WARNING_5); + $this->assertSame('final_reminder_3', PaymentReminder::STEP_FINAL_REMINDER_3); + $this->assertSame('suspension_1', PaymentReminder::STEP_SUSPENSION_1); + $this->assertSame('formal_notice', PaymentReminder::STEP_FORMAL_NOTICE); + $this->assertSame('termination_15', PaymentReminder::STEP_TERMINATION_WARNING); + $this->assertSame('termination_30', PaymentReminder::STEP_TERMINATION); + } + + public function testGetStepLabelKnownSteps(): void + { + $advert = $this->createAdvert(); + + $cases = [ + PaymentReminder::STEP_REMINDER_15 => 'Rappel de paiement', + PaymentReminder::STEP_WARNING_10 => 'Rappel + avertissement', + PaymentReminder::STEP_SUSPENSION_WARNING_5 => 'Avertissement suspension services', + PaymentReminder::STEP_FINAL_REMINDER_3 => 'Ultime rappel', + PaymentReminder::STEP_SUSPENSION_1 => 'Suspension des services', + PaymentReminder::STEP_FORMAL_NOTICE => 'Mise en demeure', + PaymentReminder::STEP_TERMINATION_WARNING => 'Avertissement resiliation + suppression donnees', + PaymentReminder::STEP_TERMINATION => 'Resiliation contrat + recouvrement legal', + ]; + + foreach ($cases as $step => $expectedLabel) { + $reminder = new PaymentReminder($advert, $step); + $this->assertSame($expectedLabel, $reminder->getStepLabel(), "Label mismatch for step: $step"); + } + } + + public function testGetStepLabelUnknownStep(): void + { + $advert = $this->createAdvert(); + $reminder = new PaymentReminder($advert, 'unknown_step'); + $this->assertSame('unknown_step', $reminder->getStepLabel()); + } + + public function testGetSeverityKnownSteps(): void + { + $advert = $this->createAdvert(); + + $cases = [ + PaymentReminder::STEP_REMINDER_15 => 'info', + PaymentReminder::STEP_WARNING_10 => 'warning', + PaymentReminder::STEP_SUSPENSION_WARNING_5 => 'danger', + PaymentReminder::STEP_FINAL_REMINDER_3 => 'danger', + PaymentReminder::STEP_SUSPENSION_1 => 'critical', + PaymentReminder::STEP_FORMAL_NOTICE => 'critical', + PaymentReminder::STEP_TERMINATION_WARNING => 'critical', + PaymentReminder::STEP_TERMINATION => 'critical', + ]; + + foreach ($cases as $step => $expectedSeverity) { + $reminder = new PaymentReminder($advert, $step); + $this->assertSame($expectedSeverity, $reminder->getSeverity(), "Severity mismatch for step: $step"); + } + } + + public function testGetSeverityUnknownStep(): void + { + $advert = $this->createAdvert(); + $reminder = new PaymentReminder($advert, 'unknown_step'); + $this->assertSame('info', $reminder->getSeverity()); + } + + public function testStepsConfigCoversAllStepConstants(): void + { + $stepConstants = [ + PaymentReminder::STEP_REMINDER_15, + PaymentReminder::STEP_WARNING_10, + PaymentReminder::STEP_SUSPENSION_WARNING_5, + PaymentReminder::STEP_FINAL_REMINDER_3, + PaymentReminder::STEP_SUSPENSION_1, + PaymentReminder::STEP_FORMAL_NOTICE, + PaymentReminder::STEP_TERMINATION_WARNING, + PaymentReminder::STEP_TERMINATION, + ]; + + foreach ($stepConstants as $step) { + $this->assertArrayHasKey($step, PaymentReminder::STEPS_CONFIG, "STEPS_CONFIG is missing step: $step"); + $this->assertArrayHasKey('days', PaymentReminder::STEPS_CONFIG[$step]); + $this->assertArrayHasKey('label', PaymentReminder::STEPS_CONFIG[$step]); + $this->assertArrayHasKey('severity', PaymentReminder::STEPS_CONFIG[$step]); + } + } + + public function testSentAtTimestamp(): void + { + $advert = $this->createAdvert(); + $before = new \DateTimeImmutable(); + $reminder = new PaymentReminder($advert, PaymentReminder::STEP_REMINDER_15); + $after = new \DateTimeImmutable(); + + $this->assertGreaterThanOrEqual($before, $reminder->getSentAt()); + $this->assertLessThanOrEqual($after, $reminder->getSentAt()); + } +} diff --git a/tests/Entity/PrestataireTest.php b/tests/Entity/PrestataireTest.php new file mode 100644 index 0000000..aec634a --- /dev/null +++ b/tests/Entity/PrestataireTest.php @@ -0,0 +1,171 @@ +assertNull($p->getId()); + $this->assertSame('Dupont SARL', $p->getRaisonSociale()); + $this->assertSame(Prestataire::STATE_ACTIVE, $p->getState()); + $this->assertNull($p->getSiret()); + $this->assertNull($p->getEmail()); + $this->assertNull($p->getPhone()); + $this->assertNull($p->getAddress()); + $this->assertNull($p->getZipCode()); + $this->assertNull($p->getCity()); + $this->assertNull($p->getUpdatedAt()); + $this->assertInstanceOf(\DateTimeImmutable::class, $p->getCreatedAt()); + $this->assertCount(0, $p->getFactures()); + } + + public function testRaisonSociale(): void + { + $p = new Prestataire('Initial'); + $p->setRaisonSociale('Nouveau Nom SAS'); + $this->assertSame('Nouveau Nom SAS', $p->getRaisonSociale()); + } + + public function testSiret(): void + { + $p = new Prestataire('Test'); + $this->assertNull($p->getSiret()); + $p->setSiret('12345678901234'); + $this->assertSame('12345678901234', $p->getSiret()); + $p->setSiret(null); + $this->assertNull($p->getSiret()); + } + + public function testEmail(): void + { + $p = new Prestataire('Test'); + $this->assertNull($p->getEmail()); + $p->setEmail('contact@prestataire.fr'); + $this->assertSame('contact@prestataire.fr', $p->getEmail()); + $p->setEmail(null); + $this->assertNull($p->getEmail()); + } + + public function testPhone(): void + { + $p = new Prestataire('Test'); + $this->assertNull($p->getPhone()); + $p->setPhone('0612345678'); + $this->assertSame('0612345678', $p->getPhone()); + $p->setPhone(null); + $this->assertNull($p->getPhone()); + } + + public function testAddress(): void + { + $p = new Prestataire('Test'); + $this->assertNull($p->getAddress()); + $this->assertNull($p->getZipCode()); + $this->assertNull($p->getCity()); + + $p->setAddress('10 rue de la Paix'); + $p->setZipCode('75001'); + $p->setCity('Paris'); + + $this->assertSame('10 rue de la Paix', $p->getAddress()); + $this->assertSame('75001', $p->getZipCode()); + $this->assertSame('Paris', $p->getCity()); + } + + public function testGetFullAddress(): void + { + $p = new Prestataire('Test'); + $this->assertSame('', $p->getFullAddress()); + + $p->setAddress('10 rue de la Paix'); + $p->setZipCode('75001'); + $p->setCity('Paris'); + + $this->assertSame('10 rue de la Paix 75001 Paris', $p->getFullAddress()); + } + + public function testGetFullAddressPartial(): void + { + $p = new Prestataire('Test'); + $p->setCity('Lyon'); + $this->assertSame('Lyon', $p->getFullAddress()); + } + + public function testState(): void + { + $p = new Prestataire('Test'); + $this->assertSame(Prestataire::STATE_ACTIVE, $p->getState()); + $this->assertNull($p->getUpdatedAt()); + + $p->setState(Prestataire::STATE_INACTIVE); + $this->assertSame(Prestataire::STATE_INACTIVE, $p->getState()); + $this->assertInstanceOf(\DateTimeImmutable::class, $p->getUpdatedAt()); + } + + public function testAddFacture(): void + { + $p = new Prestataire('Test'); + $this->assertCount(0, $p->getFactures()); + + $f = new FacturePrestataire($p, 'FACT-001', 2026, 4); + $p->addFacture($f); + $this->assertCount(1, $p->getFactures()); + + // Adding same instance twice must not duplicate + $p->addFacture($f); + $this->assertCount(1, $p->getFactures()); + } + + public function testGetTotalPaidHtNoFactures(): void + { + $p = new Prestataire('Test'); + $this->assertSame(0.0, $p->getTotalPaidHt()); + } + + public function testGetTotalPaidHtOnlyPaid(): void + { + $p = new Prestataire('Test'); + + $f1 = new FacturePrestataire($p, 'FACT-001', 2026, 1); + $f1->setMontantHt('1000.00'); + $f1->setIsPaid(true); + $p->addFacture($f1); + + $f2 = new FacturePrestataire($p, 'FACT-002', 2026, 2); + $f2->setMontantHt('500.00'); + $f2->setIsPaid(false); + $p->addFacture($f2); + + $this->assertSame(1000.0, $p->getTotalPaidHt()); + } + + public function testGetTotalPaidHtMultiplePaid(): void + { + $p = new Prestataire('Test'); + + $f1 = new FacturePrestataire($p, 'FACT-001', 2026, 1); + $f1->setMontantHt('250.50'); + $f1->setIsPaid(true); + $p->addFacture($f1); + + $f2 = new FacturePrestataire($p, 'FACT-002', 2026, 2); + $f2->setMontantHt('749.50'); + $f2->setIsPaid(true); + $p->addFacture($f2); + + $this->assertEqualsWithDelta(1000.0, $p->getTotalPaidHt(), 0.001); + } + + public function testStateConstants(): void + { + $this->assertSame('active', Prestataire::STATE_ACTIVE); + $this->assertSame('inactive', Prestataire::STATE_INACTIVE); + } +} diff --git a/tests/Service/Pdf/ComptaPdfTest.php b/tests/Service/Pdf/ComptaPdfTest.php new file mode 100644 index 0000000..5a9dbe8 --- /dev/null +++ b/tests/Service/Pdf/ComptaPdfTest.php @@ -0,0 +1,174 @@ +projectDir = sys_get_temp_dir().'/compta-pdf-test-'.bin2hex(random_bytes(4)); + mkdir($this->projectDir.'/public', 0775, true); + + $this->kernel = $this->createStub(KernelInterface::class); + $this->kernel->method('getProjectDir')->willReturn($this->projectDir); + } + + protected function tearDown(): void + { + $this->removeDir($this->projectDir); + } + + private function removeDir(string $dir): void + { + if (!is_dir($dir)) { + return; + } + foreach (scandir($dir) as $item) { + if ('.' === $item || '..' === $item) { + continue; + } + $path = $dir.'/'.$item; + is_dir($path) ? $this->removeDir($path) : unlink($path); + } + rmdir($dir); + } + + private function makePdf(string $title = 'Journal des ventes', string $from = '01/01/2026', string $to = '31/03/2026'): ComptaPdf + { + return new ComptaPdf($this->kernel, $title, $from, $to); + } + + public function testGenerateEmptyDataProducesValidPdf(): void + { + $pdf = $this->makePdf(); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } + + public function testGenerateWithRowsProducesValidPdf(): void + { + $pdf = $this->makePdf('Journal des ventes'); + $pdf->setData([ + [ + 'JournalCode' => 'VTE', + 'JournalLib' => 'Ventes', + 'EcritureNum' => 'E001', + 'EcritureDate' => '01/01/2026', + 'CompteNum' => '411000', + 'CompteLib' => 'Clients', + 'Debit' => '100.00', + 'Credit' => '0.00', + 'EcritureLib' => 'Facture 001', + ], + [ + 'JournalCode' => 'VTE', + 'JournalLib' => 'Ventes', + 'EcritureNum' => 'E002', + 'EcritureDate' => '15/01/2026', + 'CompteNum' => '706000', + 'CompteLib' => 'Prestations', + 'Debit' => '0.00', + 'Credit' => '100.00', + 'EcritureLib' => 'Facture 001', + ], + ]); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } + + public function testGenerateWithDebitCreditSummarised(): void + { + $pdf = $this->makePdf('Grand Livre'); + $pdf->setData([ + ['Debit' => '200.00', 'Credit' => '50.00', 'EcritureLib' => 'Test'], + ['Debit' => '300.00', 'Credit' => '100.00', 'EcritureLib' => 'Test2'], + ]); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + // The PDF should contain total text; check binary content for encoded values + $this->assertGreaterThan(1000, \strlen($output)); + } + + public function testGenerateWithUnknownColumnsDistributesWidth(): void + { + $pdf = $this->makePdf('Rapport custom'); + $pdf->setData([ + ['ColA' => 'Valeur1', 'ColB' => 'Valeur2', 'ColC' => '42.00'], + ['ColA' => 'Valeur3', 'ColB' => 'Valeur4', 'ColC' => '99.00'], + ]); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } + + public function testGenerateWithLogoFile(): void + { + // Create a minimal valid JPEG file in the project public dir + // We use a 1x1 white JPEG for testing + $jpegData = base64_decode('/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AJQAB/9k='); + file_put_contents($this->projectDir.'/public/logo.jpg', $jpegData); + + $pdf = $this->makePdf(); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } + + public function testSetDataWithEmptyRowsGeneratesNoDataMessage(): void + { + $pdf = $this->makePdf(); + $pdf->setData([]); + $pdf->generate(); + + $output = $pdf->Output('S'); + + // Should still produce a valid PDF even with empty data + $this->assertStringStartsWith('%PDF', $output); + } + + public function testMultiplePagesWithManyRows(): void + { + $rows = []; + for ($i = 1; $i <= 100; ++$i) { + $rows[] = [ + 'JournalCode' => 'VTE', + 'JournalLib' => 'Ventes', + 'EcritureNum' => 'E'.str_pad((string) $i, 4, '0', STR_PAD_LEFT), + 'EcritureDate' => '01/01/2026', + 'CompteNum' => '411000', + 'CompteLib' => 'Clients', + 'Debit' => number_format($i * 10.5, 2), + 'Credit' => '0.00', + 'EcritureLib' => 'Ligne '.$i, + ]; + } + + $pdf = $this->makePdf('Export FEC'); + $pdf->setData($rows); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } +} diff --git a/tests/Service/Pdf/FacturePdfTest.php b/tests/Service/Pdf/FacturePdfTest.php new file mode 100644 index 0000000..2b8f876 --- /dev/null +++ b/tests/Service/Pdf/FacturePdfTest.php @@ -0,0 +1,230 @@ +projectDir = sys_get_temp_dir().'/facture-pdf-test-'.bin2hex(random_bytes(4)); + mkdir($this->projectDir.'/public', 0775, true); + + $this->kernel = $this->createStub(KernelInterface::class); + $this->kernel->method('getProjectDir')->willReturn($this->projectDir); + } + + protected function tearDown(): void + { + $this->removeDir($this->projectDir); + } + + private function removeDir(string $dir): void + { + if (!is_dir($dir)) { + return; + } + foreach (scandir($dir) as $item) { + if ('.' === $item || '..' === $item) { + continue; + } + $path = $dir.'/'.$item; + is_dir($path) ? $this->removeDir($path) : unlink($path); + } + rmdir($dir); + } + + private function makeFacture(string $numOrder = '04/2026-00001'): Facture + { + $orderNumber = new OrderNumber($numOrder); + + return new Facture($orderNumber, self::HMAC_SECRET); + } + + private function makeCustomer(bool $withRaisonSociale = false, bool $withAddress2 = false): \App\Entity\Customer + { + $customer = $this->createStub(\App\Entity\Customer::class); + $customer->method('getFullName')->willReturn('Jean Dupont'); + $customer->method('getRaisonSociale')->willReturn($withRaisonSociale ? 'ACME SARL' : null); + $customer->method('getEmail')->willReturn('jean.dupont@example.com'); + $customer->method('getAddress')->willReturn('42 rue des Tests'); + $customer->method('getAddress2')->willReturn($withAddress2 ? 'Batiment B, etage 3' : null); + $customer->method('getZipCode')->willReturn('75001'); + $customer->method('getCity')->willReturn('Paris'); + + return $customer; + } + + public function testGenerateEmptyFactureProducesValidPdf(): void + { + $facture = $this->makeFacture(); + $pdf = new FacturePdf($this->kernel, $facture); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } + + public function testGenerateWithLinesProducesValidPdf(): void + { + $facture = $this->makeFacture(); + $facture->setTotalHt('100.00'); + $facture->setTotalTva('0.00'); + $facture->setTotalTtc('100.00'); + + $line1 = new FactureLine($facture, 'Hebergement Web', '60.00', 1); + $line1->setDescription('Hebergement annuel mutualisé'); + $line2 = new FactureLine($facture, 'Nom de domaine', '40.00', 2); + $line2->setDescription('Renouvellement .fr annuel'); + + $facture->getLines()->add($line1); + $facture->getLines()->add($line2); + + $pdf = new FacturePdf($this->kernel, $facture); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + $this->assertGreaterThan(1000, \strlen($output)); + } + + public function testGenerateWithCustomerAddressProducesValidPdf(): void + { + $facture = $this->makeFacture(); + $facture->setCustomer($this->makeCustomer(false, false)); + $facture->setTotalHt('50.00'); + $facture->setTotalTva('0.00'); + $facture->setTotalTtc('50.00'); + + $line = new FactureLine($facture, 'Service test', '50.00', 1); + $facture->getLines()->add($line); + + $pdf = new FacturePdf($this->kernel, $facture); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } + + public function testGenerateWithTvaProducesValidPdf(): void + { + $facture = $this->makeFacture(); + $facture->setTotalHt('100.00'); + $facture->setTotalTva('20.00'); + $facture->setTotalTtc('120.00'); + + $line = new FactureLine($facture, 'Prestation avec TVA', '100.00', 1); + $facture->getLines()->add($line); + + $pdf = new FacturePdf($this->kernel, $facture); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } + + public function testGenerateWithPaidFactureProducesValidPdf(): void + { + $facture = $this->makeFacture(); + $facture->setTotalHt('200.00'); + $facture->setTotalTva('0.00'); + $facture->setTotalTtc('200.00'); + $facture->setIsPaid(true); + $facture->setPaidAt(new \DateTimeImmutable('2026-02-15')); + $facture->setPaidMethod('Virement'); + + $line = new FactureLine($facture, 'Service payé', '200.00', 1); + $facture->getLines()->add($line); + + $pdf = new FacturePdf($this->kernel, $facture); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } + + public function testGenerateWithLogoFileProducesValidPdf(): void + { + // Minimal valid 1x1 JPEG + $jpegData = base64_decode('/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AJQAB/9k='); + file_put_contents($this->projectDir.'/public/logo.jpg', $jpegData); + + $facture = $this->makeFacture(); + $line = new FactureLine($facture, 'Test', '10.00', 1); + $facture->getLines()->add($line); + $facture->setTotalHt('10.00'); + $facture->setTotalTva('0.00'); + $facture->setTotalTtc('10.00'); + + $pdf = new FacturePdf($this->kernel, $facture); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } + + public function testGenerateWithManyLinesSpansMultiplePages(): void + { + $facture = $this->makeFacture(); + $facture->setTotalHt('1500.00'); + $facture->setTotalTva('0.00'); + $facture->setTotalTtc('1500.00'); + + for ($i = 1; $i <= 15; ++$i) { + $line = new FactureLine($facture, 'Service '.$i, '100.00', $i); + $line->setDescription('Description detaillee pour le service numero '.$i.' avec quelques mots supplementaires.'); + $facture->getLines()->add($line); + } + + $pdf = new FacturePdf($this->kernel, $facture); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } + + public function testGenerateWithCustomerRaisonSocialeProducesValidPdf(): void + { + $facture = $this->makeFacture(); + $facture->setCustomer($this->makeCustomer(true, true)); + + $pdf = new FacturePdf($this->kernel, $facture); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } + + public function testGenerateWithSplitIndexProducesValidPdf(): void + { + $facture = $this->makeFacture('04/2026-00002'); + $facture->setSplitIndex(2); + + $pdf = new FacturePdf($this->kernel, $facture); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } +} diff --git a/tests/Service/Pdf/RapportFinancierPdfTest.php b/tests/Service/Pdf/RapportFinancierPdfTest.php new file mode 100644 index 0000000..15e7600 --- /dev/null +++ b/tests/Service/Pdf/RapportFinancierPdfTest.php @@ -0,0 +1,169 @@ +projectDir = sys_get_temp_dir().'/rapport-pdf-test-'.bin2hex(random_bytes(4)); + mkdir($this->projectDir.'/public', 0775, true); + + $this->kernel = $this->createStub(KernelInterface::class); + $this->kernel->method('getProjectDir')->willReturn($this->projectDir); + } + + protected function tearDown(): void + { + $this->removeDir($this->projectDir); + } + + private function removeDir(string $dir): void + { + if (!is_dir($dir)) { + return; + } + foreach (scandir($dir) as $item) { + if ('.' === $item || '..' === $item) { + continue; + } + $path = $dir.'/'.$item; + is_dir($path) ? $this->removeDir($path) : unlink($path); + } + rmdir($dir); + } + + private function makePdf(string $from = '01/01/2026', string $to = '31/12/2026'): RapportFinancierPdf + { + return new RapportFinancierPdf($this->kernel, $from, $to); + } + + public function testGenerateEmptyDataProducesValidPdf(): void + { + $pdf = $this->makePdf(); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } + + public function testGenerateWithRecettesAndDepensesProducesValidPdf(): void + { + $pdf = $this->makePdf(); + $pdf->setData( + [ + 'Hebergement Web' => 1200.00, + 'Noms de domaine' => 350.00, + 'Messagerie' => 480.00, + ], + [ + 'Serveur dedie' => 600.00, + 'Registrar' => 200.00, + 'Divers' => 50.00, + ], + ); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } + + public function testGenerateWithPositiveBilanProducesValidPdf(): void + { + $pdf = $this->makePdf(); + // Recettes > Depenses => excedent/equilibre + $pdf->setData( + ['Service A' => 5000.00, 'Service B' => 3000.00], + ['Depense 1' => 1000.00], + ); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + $this->assertGreaterThan(1000, \strlen($output)); + } + + public function testGenerateWithDeficitProducesValidPdf(): void + { + $pdf = $this->makePdf(); + // Depenses > Recettes => deficit + $pdf->setData( + ['Petite recette' => 100.00], + ['Grosse depense' => 9999.00], + ); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } + + public function testGenerateWithExcedentLabelProducesValidPdf(): void + { + $pdf = $this->makePdf(); + // Marge > 30% of recettes => 'EXCEDENT' + $recettes = ['Service' => 10000.00]; + $depenses = ['Depense' => 1000.00]; // 9000 marge > 3000 (30%) + $pdf->setData($recettes, $depenses); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } + + public function testGenerateWithLogoFileProducesValidPdf(): void + { + // Minimal valid 1x1 JPEG + $jpegData = base64_decode('/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AJQAB/9k='); + file_put_contents($this->projectDir.'/public/logo.jpg', $jpegData); + + $pdf = $this->makePdf(); + $pdf->setData(['Service' => 500.00], ['Charge' => 200.00]); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } + + public function testGenerateWithManyRecettesAndDepenses(): void + { + $recettes = []; + $depenses = []; + for ($i = 1; $i <= 20; ++$i) { + $recettes['Recette '.$i] = $i * 100.0; + $depenses['Depense '.$i] = $i * 50.0; + } + + $pdf = $this->makePdf('01/01/2025', '31/12/2025'); + $pdf->setData($recettes, $depenses); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } + + public function testGenerateWithZeroTotalsEquilibre(): void + { + $pdf = $this->makePdf(); + // Both zero => marge = 0, isPositif = true, but marge (0) <= recettes*0.3 (0) => 'EQUILIBRE' + $pdf->setData([], []); + $pdf->generate(); + + $output = $pdf->Output('S'); + + $this->assertStringStartsWith('%PDF', $output); + } +}