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) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-07 23:57:42 +02:00
parent 6f5ce58d66
commit 79c55ba0f9
17 changed files with 3093 additions and 0 deletions

View File

@@ -0,0 +1,328 @@
<?php
namespace App\Tests\Command;
use App\Command\PaymentReminderCommand;
use App\Entity\Advert;
use App\Entity\Customer;
use App\Entity\OrderNumber;
use App\Entity\PaymentReminder;
use App\Service\ActionService;
use App\Service\MailerService;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Tester\CommandTester;
use Twig\Environment;
class PaymentReminderCommandTest extends TestCase
{
private EntityManagerInterface $em;
private MailerService $mailer;
private Environment $twig;
private LoggerInterface $logger;
private ActionService $actionService;
protected function setUp(): void
{
$this->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<Advert> $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('<p>Email content</p>');
// 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('<p>Email</p>');
$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('<p>Email</p>');
$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('<p>Email</p>');
$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());
}
}

View File

@@ -0,0 +1,199 @@
<?php
namespace App\Tests\Command;
use App\Command\ReminderFacturesPrestataireCommand;
use App\Entity\FacturePrestataire;
use App\Entity\Prestataire;
use App\Service\MailerService;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Tester\CommandTester;
class ReminderFacturesPrestataireCommandTest extends TestCase
{
private EntityManagerInterface $em;
private MailerService $mailer;
protected function setUp(): void
{
$this->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);
}
}

View File

@@ -0,0 +1,264 @@
<?php
namespace App\Tests\Controller\Admin;
use App\Controller\Admin\ComptabiliteController;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Twig\Environment;
class ComptabiliteControllerTest extends TestCase
{
private function buildEmWithQueryBuilder(): EntityManagerInterface
{
$stubEm = $this->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('<html></html>');
$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);
}
}

View File

@@ -0,0 +1,190 @@
<?php
namespace App\Tests\Controller\Admin;
use App\Controller\Admin\FactureController;
use App\Entity\Customer;
use App\Entity\Facture;
use App\Service\MeilisearchService;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Twig\Environment;
class FactureControllerTest extends TestCase
{
private function buildController(?EntityManagerInterface $em = null, ?MeilisearchService $meilisearch = null): FactureController
{
$em ??= $this->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('<html></html>');
$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());
}
}

View File

@@ -0,0 +1,425 @@
<?php
namespace App\Tests\Controller\Admin;
use App\Controller\Admin\PrestatairesController;
use App\Entity\FacturePrestataire;
use App\Entity\Prestataire;
use App\Repository\PrestataireRepository;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface as HttpResponseInterface;
use Twig\Environment;
class PrestatairesControllerTest extends TestCase
{
private function buildController(?EntityManagerInterface $em = null): PrestatairesController
{
$em ??= $this->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('<html></html>');
$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());
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace App\Tests\Controller;
use App\Controller\FactureVerifyController;
use App\Entity\Customer;
use App\Entity\Facture;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\Response;
use Twig\Environment;
class FactureVerifyControllerTest extends TestCase
{
private function buildController(?Environment $twig = null): FactureVerifyController
{
$twig ??= $this->createStub(Environment::class);
$twig->method('render')->willReturn('<html></html>');
$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('<html>Invalid</html>');
$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());
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace App\Tests\Entity;
use App\Entity\ActionLog;
use App\Entity\Customer;
use App\Entity\User;
use PHPUnit\Framework\TestCase;
class ActionLogTest extends TestCase
{
private function createCustomer(): Customer
{
$user = new User();
$user->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'));
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Advert;
use App\Entity\AdvertEvent;
use App\Entity\OrderNumber;
use PHPUnit\Framework\TestCase;
class AdvertEventTest extends TestCase
{
private function createAdvert(): Advert
{
$order = new OrderNumber('04/2026-99002');
return new Advert($order, 'test-secret');
}
public function testConstructorMinimalArgs(): void
{
$advert = $this->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());
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Advert;
use App\Entity\AdvertPayment;
use App\Entity\OrderNumber;
use PHPUnit\Framework\TestCase;
class AdvertPaymentTest extends TestCase
{
private function createAdvert(): Advert
{
$order = new OrderNumber('04/2026-99001');
return new Advert($order, 'test-secret');
}
public function testConstructor(): void
{
$advert = $this->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());
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace App\Tests\Entity;
use App\Entity\DocusealEvent;
use PHPUnit\Framework\TestCase;
class DocusealEventTest extends TestCase
{
public function testConstructorMinimalArgs(): void
{
$event = new DocusealEvent('submission', 'completed');
$this->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());
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Facture;
use App\Entity\FactureLine;
use App\Entity\OrderNumber;
use PHPUnit\Framework\TestCase;
class FactureLineTest extends TestCase
{
private function createFacture(): Facture
{
$order = new OrderNumber('04/2026-88001');
return new Facture($order, 'test-secret');
}
public function testConstructorMinimalArgs(): void
{
$facture = $this->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));
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace App\Tests\Entity;
use App\Entity\FacturePrestataire;
use App\Entity\Prestataire;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\File\File;
class FacturePrestataireTest extends TestCase
{
private function createPrestataire(): Prestataire
{
return new Prestataire('Test Prestataire SARL');
}
public function testConstructor(): void
{
$p = $this->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());
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Advert;
use App\Entity\OrderNumber;
use App\Entity\PaymentReminder;
use PHPUnit\Framework\TestCase;
class PaymentReminderTest extends TestCase
{
private function createAdvert(): Advert
{
$order = new OrderNumber('04/2026-99003');
return new Advert($order, 'test-secret');
}
public function testConstructorMinimalArgs(): void
{
$advert = $this->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());
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace App\Tests\Entity;
use App\Entity\FacturePrestataire;
use App\Entity\Prestataire;
use PHPUnit\Framework\TestCase;
class PrestataireTest extends TestCase
{
public function testConstructor(): void
{
$p = new Prestataire('Dupont SARL');
$this->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);
}
}

View File

@@ -0,0 +1,174 @@
<?php
namespace App\Tests\Service\Pdf;
use App\Service\Pdf\ComptaPdf;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpKernel\KernelInterface;
class ComptaPdfTest extends TestCase
{
private KernelInterface $kernel;
private string $projectDir;
protected function setUp(): void
{
$this->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);
}
}

View File

@@ -0,0 +1,230 @@
<?php
namespace App\Tests\Service\Pdf;
use App\Entity\Facture;
use App\Entity\FactureLine;
use App\Entity\OrderNumber;
use App\Service\Pdf\FacturePdf;
use Doctrine\Common\Collections\ArrayCollection;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpKernel\KernelInterface;
class FacturePdfTest extends TestCase
{
private const HMAC_SECRET = 'test-hmac-secret';
private KernelInterface $kernel;
private string $projectDir;
protected function setUp(): void
{
$this->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);
}
}

View File

@@ -0,0 +1,169 @@
<?php
namespace App\Tests\Service\Pdf;
use App\Service\Pdf\RapportFinancierPdf;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpKernel\KernelInterface;
class RapportFinancierPdfTest extends TestCase
{
private KernelInterface $kernel;
private string $projectDir;
protected function setUp(): void
{
$this->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);
}
}