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); } public function testGenerateWithQrCodeProducesValidPdf(): void { $facture = $this->makeFacture('04/2026-00010'); $facture->setTotalHt('50.00'); $facture->setTotalTva('0.00'); $facture->setTotalTtc('50.00'); $urlGenerator = $this->createStub(\Symfony\Component\Routing\Generator\UrlGeneratorInterface::class); $urlGenerator->method('generate')->willReturn('https://crm.e-cosplay.fr/facture/verify/1/abc123'); $line = new FactureLine($facture, 'Service QR', '50.00', 1); $facture->getLines()->add($line); $pdf = new FacturePdf($this->kernel, $facture, $urlGenerator); $pdf->generate(); $output = $pdf->Output('S'); $this->assertStringStartsWith('%PDF', $output); } public function testGenerateWithRibFileProducesValidPdf(): void { // Create a minimal valid PDF as the RIB file $ribDir = $this->projectDir . '/public'; if (!is_dir($ribDir)) { mkdir($ribDir, 0775, true); } // Create a minimal PDF file for RIB using FPDI itself $miniPdf = new \setasign\Fpdi\Fpdi(); $miniPdf->AddPage(); $ribPath = $ribDir . '/rib.pdf'; $miniPdf->Output('F', $ribPath); $facture = $this->makeFacture('04/2026-00011'); $facture->setTotalHt('75.00'); $facture->setTotalTva('0.00'); $facture->setTotalTtc('75.00'); $line = new FactureLine($facture, 'Service RIB', '75.00', 1); $facture->getLines()->add($line); $pdf = new FacturePdf($this->kernel, $facture); $pdf->generate(); $output = $pdf->Output('S'); $this->assertStringStartsWith('%PDF', $output); } public function testGenerateWithTwigAppendsCgvProducesValidPdf(): void { $facture = $this->makeFacture('04/2026-00012'); $facture->setTotalHt('80.00'); $facture->setTotalTva('0.00'); $facture->setTotalTtc('80.00'); $line = new FactureLine($facture, 'Service Twig', '80.00', 1); $facture->getLines()->add($line); // Create a mock Twig environment that returns minimal HTML for CGV $twig = $this->createStub(\Twig\Environment::class); $twig->method('render')->willReturn('

Conditions Generales de Vente

'); $pdf = new FacturePdf($this->kernel, $facture, null, $twig); $pdf->generate(); $output = $pdf->Output('S'); $this->assertStringStartsWith('%PDF', $output); } public function testGenerateWithCustomerNoAddress(): void { $facture = $this->makeFacture('04/2026-00013'); $customer = $this->createStub(\App\Entity\Customer::class); $customer->method('getFullName')->willReturn('Jean Dupont'); $customer->method('getRaisonSociale')->willReturn(null); $customer->method('getEmail')->willReturn('jean@example.com'); $customer->method('getAddress')->willReturn(null); // No address $customer->method('getAddress2')->willReturn(null); $customer->method('getZipCode')->willReturn(null); $customer->method('getCity')->willReturn(null); $facture->setCustomer($customer); $pdf = new FacturePdf($this->kernel, $facture); $pdf->generate(); $output = $pdf->Output('S'); $this->assertStringStartsWith('%PDF', $output); } public function testFooterOnCgvPageSkipsFooter(): void { // Test that skipHeaderFooter=true makes Footer skip on pages after the last facture page $facture = $this->makeFacture('04/2026-00014'); $facture->setTotalHt('30.00'); $facture->setTotalTva('0.00'); $facture->setTotalTtc('30.00'); $twig = $this->createStub(\Twig\Environment::class); $twig->method('render')->willReturn('

CGV page 1

'); $pdf = new FacturePdf($this->kernel, $facture, null, $twig); $pdf->generate(); $output = $pdf->Output('S'); $this->assertStringStartsWith('%PDF', $output); } }