diff --git a/tests/Command/PaymentReminderCommandTest.php b/tests/Command/PaymentReminderCommandTest.php
index 08a055e..b351614 100644
--- a/tests/Command/PaymentReminderCommandTest.php
+++ b/tests/Command/PaymentReminderCommandTest.php
@@ -307,6 +307,39 @@ class PaymentReminderCommandTest extends TestCase
$this->assertSame(0, $tester->getStatusCode());
}
+ public function testFormalNoticeStepSendsEmailOnly(): void
+ {
+ // 32 days old -> formal_notice step (>= 31 days), all earlier steps done
+ $advert = $this->makeAdvert('04/2026-00001', 'client@example.com', 32);
+
+ $doneSteps = [
+ PaymentReminder::STEP_REMINDER_15,
+ PaymentReminder::STEP_WARNING_10,
+ PaymentReminder::STEP_SUSPENSION_WARNING_5,
+ PaymentReminder::STEP_FINAL_REMINDER_3,
+ PaymentReminder::STEP_SUSPENSION_1,
+ ];
+ $this->stubReminderRepo($doneSteps, [$advert]);
+
+ $this->twig->method('render')->willReturn('
Email
');
+ $this->em->method('persist');
+ $this->em->method('flush');
+
+ // Expect 2 emails: client (mise en demeure) + admin notification
+ $this->mailer->expects($this->exactly(2))->method('sendEmail');
+
+ // No ActionService calls expected for formal_notice
+ $this->actionService->expects($this->never())->method('suspendCustomer');
+ $this->actionService->expects($this->never())->method('disableCustomer');
+ $this->actionService->expects($this->never())->method('markForDeletion');
+
+ $tester = new CommandTester($this->makeCommand());
+ $tester->execute([]);
+
+ $this->assertSame(0, $tester->getStatusCode());
+ $this->assertStringContainsString('1 relance(s) envoyee(s)', $tester->getDisplay());
+ }
+
public function testExceptionInStepIsLoggedAndContinues(): void
{
$advert = $this->makeAdvert('04/2026-00001', 'client@example.com', 20);
diff --git a/tests/Command/TestMailCommandTest.php b/tests/Command/TestMailCommandTest.php
index e14263e..653222f 100644
--- a/tests/Command/TestMailCommandTest.php
+++ b/tests/Command/TestMailCommandTest.php
@@ -39,4 +39,42 @@ class TestMailCommandTest extends TestCase
$this->assertStringContainsString('prod@test.com', $tester->getDisplay());
$this->assertStringContainsString('prod', $tester->getDisplay());
}
+
+ public function testForceDsnFailureReturnsFailure(): void
+ {
+ $mailer = $this->createStub(MailerService::class);
+ $twig = $this->createStub(Environment::class);
+ $twig->method('render')->willReturn('test');
+
+ $command = new TestMailCommand($mailer, $twig);
+ $tester = new CommandTester($command);
+
+ // An invalid DSN will throw an exception inside sendViaForceDsn -> returns FAILURE
+ $tester->execute([
+ 'email' => 'test@test.com',
+ '--force-dsn' => 'invalid-dsn://this.will.fail',
+ ]);
+
+ $this->assertSame(1, $tester->getStatusCode());
+ $this->assertStringContainsString('Echec envoi via force-dsn', $tester->getDisplay());
+ }
+
+ public function testForceDsnSuccessReturnsSuccess(): void
+ {
+ $mailer = $this->createStub(MailerService::class);
+ $twig = $this->createStub(Environment::class);
+ $twig->method('render')->willReturn('test');
+
+ $command = new TestMailCommand($mailer, $twig);
+ $tester = new CommandTester($command);
+
+ // Use the null transport DSN which succeeds without a real SMTP server
+ $tester->execute([
+ 'email' => 'test@test.com',
+ '--force-dsn' => 'null://null',
+ ]);
+
+ $this->assertSame(0, $tester->getStatusCode());
+ $this->assertStringContainsString('test@test.com', $tester->getDisplay());
+ }
}
diff --git a/tests/Controller/Admin/AdminControllersTest.php b/tests/Controller/Admin/AdminControllersTest.php
index 205c54a..e46d956 100644
--- a/tests/Controller/Admin/AdminControllersTest.php
+++ b/tests/Controller/Admin/AdminControllersTest.php
@@ -21,9 +21,11 @@ use App\Repository\ServiceRepository;
use App\Repository\StripeWebhookSecretRepository;
use App\Repository\UserRepository;
use App\Service\KeycloakAdminService;
+use App\Service\MeilisearchService;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Twig\Environment;
@@ -171,4 +173,114 @@ class AdminControllersTest extends TestCase
$response = $controller->index();
$this->assertInstanceOf(Response::class, $response);
}
+
+ // ---------------------------------------------------------------
+ // DashboardController::globalSearch
+ // ---------------------------------------------------------------
+
+ public function testDashboardGlobalSearchTooShort(): void
+ {
+ $controller = $this->createMockController(DashboardController::class);
+ $meilisearch = $this->createStub(MeilisearchService::class);
+ $request = new Request(['q' => 'a']);
+ $response = $controller->globalSearch($request, $meilisearch);
+ $this->assertInstanceOf(JsonResponse::class, $response);
+ $this->assertSame('[]', $response->getContent());
+ }
+
+ public function testDashboardGlobalSearchReturnsResults(): void
+ {
+ $meilisearch = $this->createStub(MeilisearchService::class);
+ $meilisearch->method('searchCustomers')->willReturn([['id' => 1, 'fullName' => 'Test Client', 'email' => 't@t.com']]);
+ $meilisearch->method('searchDomains')->willReturn([]);
+ $meilisearch->method('searchWebsites')->willReturn([]);
+ $meilisearch->method('searchContacts')->willReturn([]);
+ $meilisearch->method('searchRevendeurs')->willReturn([]);
+ $meilisearch->method('searchDevis')->willReturn([]);
+ $meilisearch->method('searchAdverts')->willReturn([]);
+ $meilisearch->method('searchFactures')->willReturn([]);
+
+ $controller = $this->createMockController(DashboardController::class);
+ $request = new Request(['q' => 'test query']);
+ $response = $controller->globalSearch($request, $meilisearch);
+ $this->assertInstanceOf(JsonResponse::class, $response);
+
+ $data = json_decode($response->getContent(), true);
+ $this->assertNotEmpty($data);
+ $this->assertSame('client', $data[0]['type']);
+ }
+
+ // ---------------------------------------------------------------
+ // ServicesController missing methods
+ // ---------------------------------------------------------------
+
+ public function testServicesNdd(): void
+ {
+ $entityRepo = $this->createStub(EntityRepository::class);
+ $entityRepo->method('findBy')->willReturn([]);
+
+ $em = $this->createStub(EntityManagerInterface::class);
+ $em->method('getRepository')->willReturn($entityRepo);
+
+ $controller = $this->createMockController(ServicesController::class);
+ $response = $controller->ndd($em);
+ $this->assertInstanceOf(Response::class, $response);
+ }
+
+ public function testServicesNddSearch(): void
+ {
+ $meilisearch = $this->createStub(MeilisearchService::class);
+ $meilisearch->method('searchDomains')->willReturn([['id' => 1, 'fqdn' => 'example.com']]);
+
+ $controller = $this->createMockController(ServicesController::class);
+ $request = new Request(['q' => 'example']);
+ $response = $controller->nddSearch($request, $meilisearch);
+ $this->assertInstanceOf(JsonResponse::class, $response);
+ }
+
+ public function testServicesNddSearchEmpty(): void
+ {
+ $meilisearch = $this->createStub(MeilisearchService::class);
+
+ $controller = $this->createMockController(ServicesController::class);
+ $request = new Request(['q' => '']);
+ $response = $controller->nddSearch($request, $meilisearch);
+ $this->assertInstanceOf(JsonResponse::class, $response);
+ $this->assertSame('[]', $response->getContent());
+ }
+
+ public function testServicesEsyweb(): void
+ {
+ $entityRepo = $this->createStub(EntityRepository::class);
+ $entityRepo->method('findBy')->willReturn([]);
+
+ $em = $this->createStub(EntityManagerInterface::class);
+ $em->method('getRepository')->willReturn($entityRepo);
+
+ $controller = $this->createMockController(ServicesController::class);
+ $response = $controller->esyweb($em);
+ $this->assertInstanceOf(Response::class, $response);
+ }
+
+ public function testServicesEsywebSearch(): void
+ {
+ $meilisearch = $this->createStub(MeilisearchService::class);
+ $meilisearch->method('searchWebsites')->willReturn([['id' => 1, 'name' => 'My Site']]);
+
+ $controller = $this->createMockController(ServicesController::class);
+ $request = new Request(['q' => 'my site']);
+ $response = $controller->esywebSearch($request, $meilisearch);
+ $this->assertInstanceOf(JsonResponse::class, $response);
+ }
+
+ public function testServicesEsywebSearchEmpty(): void
+ {
+ $meilisearch = $this->createStub(MeilisearchService::class);
+
+ $controller = $this->createMockController(ServicesController::class);
+ $request = new Request(['q' => '']);
+ $response = $controller->esywebSearch($request, $meilisearch);
+ $this->assertInstanceOf(JsonResponse::class, $response);
+ $this->assertSame('[]', $response->getContent());
+ }
}
diff --git a/tests/Controller/Admin/ClientsControllerTest.php b/tests/Controller/Admin/ClientsControllerTest.php
index 48a2a0d..3d02ea7 100644
--- a/tests/Controller/Admin/ClientsControllerTest.php
+++ b/tests/Controller/Admin/ClientsControllerTest.php
@@ -340,6 +340,174 @@ class ClientsControllerTest extends TestCase
$this->assertSame(302, $response->getStatusCode());
}
+ private function buildCustomer(int $id = 1): Customer
+ {
+ $user = new User();
+ $user->setEmail('show@test.com');
+ $user->setFirstName('Show');
+ $user->setLastName('User');
+ $user->setPassword('h');
+ $customer = new Customer($user);
+ $ref = new \ReflectionProperty(Customer::class, 'id');
+ $ref->setAccessible(true);
+ $ref->setValue($customer, $id);
+
+ return $customer;
+ }
+
+ public function testShowReturnsResponse(): void
+ {
+ $customer = $this->buildCustomer();
+
+ $entityRepo = $this->createStub(\Doctrine\ORM\EntityRepository::class);
+ $entityRepo->method('findBy')->willReturn([]);
+ $entityRepo->method('count')->willReturn(0);
+ $entityRepo->method('findOneBy')->willReturn(null);
+ $entityRepo->method('find')->willReturn(null);
+
+ $em = $this->createStub(\Doctrine\ORM\EntityManagerInterface::class);
+ $em->method('getRepository')->willReturn($entityRepo);
+
+ $request = new Request();
+ $request->setMethod('GET');
+
+ $controller = $this->createController($request);
+
+ $response = $controller->show(
+ $customer,
+ $request,
+ $em,
+ $this->createStub(\App\Service\OvhService::class),
+ $this->createStub(\App\Service\CloudflareService::class),
+ $this->createStub(\App\Service\DnsCheckService::class),
+ $this->createStub(\App\Service\EsyMailService::class),
+ $this->createStub(\Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface::class),
+ $this->createStub(MailerService::class),
+ $this->createStub(Environment::class),
+ );
+ $this->assertInstanceOf(Response::class, $response);
+ }
+
+ public function testShowPostInfo(): void
+ {
+ $customer = $this->buildCustomer();
+
+ $entityRepo = $this->createStub(\Doctrine\ORM\EntityRepository::class);
+ $entityRepo->method('findBy')->willReturn([]);
+ $entityRepo->method('count')->willReturn(0);
+ $entityRepo->method('findOneBy')->willReturn(null);
+
+ $em = $this->createMock(\Doctrine\ORM\EntityManagerInterface::class);
+ $em->method('getRepository')->willReturn($entityRepo);
+ $em->expects($this->once())->method('flush');
+
+ $request = new Request(['tab' => 'info'], ['firstName' => 'Updated', 'lastName' => 'Name', 'email' => 'u@t.com']);
+ $request->setMethod('POST');
+ $request->setSession(new Session(new MockArraySessionStorage()));
+
+ $controller = $this->createController($request);
+
+ $response = $controller->show(
+ $customer,
+ $request,
+ $em,
+ $this->createStub(\App\Service\OvhService::class),
+ $this->createStub(\App\Service\CloudflareService::class),
+ $this->createStub(\App\Service\DnsCheckService::class),
+ $this->createStub(\App\Service\EsyMailService::class),
+ $this->createStub(\Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface::class),
+ $this->createStub(MailerService::class),
+ $this->createStub(Environment::class),
+ );
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testDeleteMarksForDeletion(): void
+ {
+ $customer = $this->buildCustomer();
+
+ $em = $this->createMock(\Doctrine\ORM\EntityManagerInterface::class);
+ $em->expects($this->once())->method('flush');
+
+ $request = new Request();
+ $request->setSession(new Session(new MockArraySessionStorage()));
+ $controller = $this->createController($request);
+
+ $response = $controller->delete($customer, $em);
+ $this->assertSame(302, $response->getStatusCode());
+ $this->assertTrue($customer->isPendingDelete());
+ }
+
+ public function testDeleteAlreadyPendingDelete(): void
+ {
+ $user = new User();
+ $user->setEmail('p@t.com');
+ $user->setFirstName('P');
+ $user->setLastName('D');
+ $user->setPassword('h');
+ $customer = new Customer($user);
+ $customer->setState(Customer::STATE_PENDING_DELETE);
+
+ $em = $this->createMock(\Doctrine\ORM\EntityManagerInterface::class);
+ $em->expects($this->never())->method('flush');
+
+ $request = new Request();
+ $request->setSession(new Session(new MockArraySessionStorage()));
+ $controller = $this->createController($request);
+
+ $response = $controller->delete($customer, $em);
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testResendWelcomeWithTempPassword(): void
+ {
+ $user = new User();
+ $user->setEmail('w@t.com');
+ $user->setFirstName('W');
+ $user->setLastName('T');
+ $user->setPassword('h');
+ $user->setTempPassword('temp123');
+ $customer = new Customer($user);
+ $ref = new \ReflectionProperty(Customer::class, 'id');
+ $ref->setAccessible(true);
+ $ref->setValue($customer, 5);
+
+ $mailer = $this->createStub(MailerService::class);
+ $twig = $this->createStub(Environment::class);
+ $twig->method('render')->willReturn('');
+
+ $request = new Request();
+ $request->setSession(new Session(new MockArraySessionStorage()));
+ $controller = $this->createController($request);
+
+ $response = $controller->resendWelcome($customer, $mailer, $twig);
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testResendWelcomeWithoutTempPassword(): void
+ {
+ $user = new User();
+ $user->setEmail('w@t.com');
+ $user->setFirstName('W');
+ $user->setLastName('T');
+ $user->setPassword('h');
+ // No temp password set
+ $customer = new Customer($user);
+ $ref = new \ReflectionProperty(Customer::class, 'id');
+ $ref->setAccessible(true);
+ $ref->setValue($customer, 6);
+
+ $mailer = $this->createStub(MailerService::class);
+ $twig = $this->createStub(Environment::class);
+
+ $request = new Request();
+ $request->setSession(new Session(new MockArraySessionStorage()));
+ $controller = $this->createController($request);
+
+ $response = $controller->resendWelcome($customer, $mailer, $twig);
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
public function testCreatePostMeilisearchError(): void
{
$user = new User();
diff --git a/tests/Controller/Admin/ComptabiliteControllerTest.php b/tests/Controller/Admin/ComptabiliteControllerTest.php
index 6c0e1f4..2d78e9c 100644
--- a/tests/Controller/Admin/ComptabiliteControllerTest.php
+++ b/tests/Controller/Admin/ComptabiliteControllerTest.php
@@ -3,6 +3,7 @@
namespace App\Tests\Controller\Admin;
use App\Controller\Admin\ComptabiliteController;
+use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
@@ -16,6 +17,7 @@ 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\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Twig\Environment;
@@ -58,6 +60,59 @@ class ComptabiliteControllerTest extends TestCase
return $kernel;
}
+ /**
+ * Build a controller wired with a user token and a router that returns non-empty paths.
+ * Required for methods that call getUser() and generateUrl()/redirectToRoute().
+ */
+ private function buildSignController(): \App\Controller\Admin\ComptabiliteController
+ {
+ $em = $this->buildEmWithQueryBuilder();
+ $kernel = $this->buildKernel();
+ $controller = new \App\Controller\Admin\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(\Twig\Environment::class);
+ $twig->method('render')->willReturn('');
+
+ $router = $this->createStub(\Symfony\Component\Routing\RouterInterface::class);
+ $router->method('generate')->willReturn('/admin/comptabilite');
+
+ $user = new User();
+ $user->setEmail('admin@e-cosplay.fr');
+ $user->setFirstName('Admin');
+ $user->setLastName('Test');
+ $user->setPassword('h');
+ $token = $this->createStub(TokenInterface::class);
+ $token->method('getUser')->willReturn($user);
+ $tokenStorage = $this->createStub(TokenStorageInterface::class);
+ $tokenStorage->method('getToken')->willReturn($token);
+
+ $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', $tokenStorage],
+ ['request_stack', $stack],
+ ['parameter_bag', $this->createStub(ParameterBagInterface::class)],
+ ]);
+ $controller->setContainer($container);
+
+ return $controller;
+ }
+
private function buildController(): ComptabiliteController
{
$em = $this->buildEmWithQueryBuilder();
@@ -261,4 +316,157 @@ class ComptabiliteControllerTest extends TestCase
$contentType = $response->headers->get('Content-Type') ?? '';
$this->assertStringContainsString('text/csv', $contentType);
}
+
+ public function testExportGrandLivreJson(): void
+ {
+ $controller = $this->buildController();
+ $request = new Request(['period' => 'current', 'format' => 'json']);
+ $response = $controller->exportGrandLivre($request);
+ $this->assertSame(200, $response->getStatusCode());
+ $contentType = $response->headers->get('Content-Type') ?? '';
+ $this->assertStringContainsString('application/json', $contentType);
+ }
+
+ public function testExportFecJson(): void
+ {
+ $controller = $this->buildController();
+ $request = new Request(['period' => 'current', 'format' => 'json']);
+ $response = $controller->exportFec($request);
+ $this->assertSame(200, $response->getStatusCode());
+ $contentType = $response->headers->get('Content-Type') ?? '';
+ $this->assertStringContainsString('application/json', $contentType);
+ }
+
+ public function testExportBalanceAgeeJson(): void
+ {
+ $controller = $this->buildController();
+ $request = new Request(['format' => 'json']);
+ $response = $controller->exportBalanceAgee($request);
+ $this->assertSame(200, $response->getStatusCode());
+ $contentType = $response->headers->get('Content-Type') ?? '';
+ $this->assertStringContainsString('application/json', $contentType);
+ }
+
+ public function testExportReglementsJson(): void
+ {
+ $controller = $this->buildController();
+ $request = new Request(['period' => 'current', 'format' => 'json']);
+ $response = $controller->exportReglements($request);
+ $this->assertSame(200, $response->getStatusCode());
+ $contentType = $response->headers->get('Content-Type') ?? '';
+ $this->assertStringContainsString('application/json', $contentType);
+ }
+
+ public function testExportPdfFec(): void
+ {
+ $controller = $this->buildController();
+ $request = new Request(['period' => 'current']);
+ $response = $controller->exportPdf('fec', $request);
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('application/pdf', $response->headers->get('Content-Type'));
+ }
+
+ public function testExportPdfGrandLivre(): void
+ {
+ $controller = $this->buildController();
+ $request = new Request(['period' => 'current']);
+ $response = $controller->exportPdf('grand-livre', $request);
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('application/pdf', $response->headers->get('Content-Type'));
+ }
+
+ public function testExportPdfReglements(): void
+ {
+ $controller = $this->buildController();
+ $request = new Request(['period' => 'current']);
+ $response = $controller->exportPdf('reglements', $request);
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame('application/pdf', $response->headers->get('Content-Type'));
+ }
+
+ public function testExportPdfSignRedirectsToDocuSeal(): void
+ {
+ $docuSeal = $this->createStub(\App\Service\DocuSealService::class);
+ $docuSeal->method('sendComptaForSignature')->willReturn(42);
+ $docuSeal->method('getSubmitterSlug')->willReturn('abc123');
+
+ $controller = $this->buildSignController();
+
+ $request = new Request(['period' => 'current']);
+ $session = new Session(new MockArraySessionStorage());
+ $request->setSession($session);
+
+ $response = $controller->exportPdfSign('journal-ventes', $request, $docuSeal);
+ $this->assertSame(302, $response->getStatusCode());
+ $this->assertStringContainsString('docuseal.example', $response->headers->get('Location') ?? '');
+ }
+
+ public function testExportPdfSignDocuSealNoSlug(): void
+ {
+ $docuSeal = $this->createStub(\App\Service\DocuSealService::class);
+ $docuSeal->method('sendComptaForSignature')->willReturn(42);
+ $docuSeal->method('getSubmitterSlug')->willReturn(null);
+
+ $controller = $this->buildSignController();
+
+ $request = new Request(['period' => 'current']);
+ $response = $controller->exportPdfSign('journal-ventes', $request, $docuSeal);
+ // No slug -> redirect to index
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testSignCallbackWithNoSession(): void
+ {
+ $controller = $this->buildSignController();
+ $request = new Request();
+ $session = new Session(new MockArraySessionStorage());
+ $request->setSession($session);
+
+ $docuSeal = $this->createStub(\App\Service\DocuSealService::class);
+ $mailer = $this->createStub(\App\Service\MailerService::class);
+ $twig = $this->createStub(\Twig\Environment::class);
+
+ // No submitter_id in session -> "Session de signature expiree"
+ $response = $controller->signCallback('journal-ventes', $request, $docuSeal, $mailer, $twig);
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testSignCallbackWithSessionNoPdf(): void
+ {
+ $controller = $this->buildSignController();
+ $request = new Request();
+ $session = new Session(new MockArraySessionStorage());
+ $session->set('compta_submitter_id', 99);
+ $request->setSession($session);
+
+ $docuSeal = $this->createStub(\App\Service\DocuSealService::class);
+ $docuSeal->method('getSubmitterData')->willReturn([
+ 'documents' => [],
+ 'audit_log_url' => null,
+ 'metadata' => [],
+ ]);
+
+ $mailer = $this->createStub(\App\Service\MailerService::class);
+ $twig = $this->createStub(\Twig\Environment::class);
+ $twig->method('render')->willReturn('');
+
+ $response = $controller->signCallback('journal-ventes', $request, $docuSeal, $mailer, $twig);
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testRapportFinancierSignRedirects(): void
+ {
+ $docuSeal = $this->createStub(\App\Service\DocuSealService::class);
+ $docuSeal->method('sendComptaForSignature')->willReturn(10);
+ $docuSeal->method('getSubmitterSlug')->willReturn('slug-rap');
+
+ $controller = $this->buildSignController();
+
+ $request = new Request(['period' => 'current']);
+ $session = new Session(new MockArraySessionStorage());
+ $request->setSession($session);
+
+ $response = $controller->rapportFinancierSign($request, $docuSeal);
+ $this->assertSame(302, $response->getStatusCode());
+ }
}
diff --git a/tests/Controller/Admin/FactureControllerTest.php b/tests/Controller/Admin/FactureControllerTest.php
index 84c9244..1678c6a 100644
--- a/tests/Controller/Admin/FactureControllerTest.php
+++ b/tests/Controller/Admin/FactureControllerTest.php
@@ -161,6 +161,67 @@ class FactureControllerTest extends TestCase
$controller->send(999, $mailer, $twig, $urlGenerator, '/tmp');
}
+ // ---------------------------------------------------------------
+ // generatePdf — 404 when facture not found
+ // ---------------------------------------------------------------
+
+ public function testGeneratePdfThrows404WhenFactureNotFound(): 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);
+
+ $this->expectException(\Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class);
+ $controller->generatePdf(
+ 999,
+ $this->createStub(\Symfony\Component\HttpKernel\KernelInterface::class),
+ $this->createStub(\Symfony\Component\Routing\Generator\UrlGeneratorInterface::class),
+ $this->createStub(\Twig\Environment::class),
+ );
+ }
+
+ // ---------------------------------------------------------------
+ // send — successful full path (pdf exists, customer has email)
+ // ---------------------------------------------------------------
+
+ public function testSendSuccessfully(): void
+ {
+ $customer = $this->createStub(\App\Entity\Customer::class);
+ $customer->method('getId')->willReturn(5);
+ $customer->method('getEmail')->willReturn('client@test.com');
+
+ $facture = $this->createStub(\App\Entity\Facture::class);
+ $facture->method('getFacturePdf')->willReturn('facture-test.pdf');
+ $facture->method('getCustomer')->willReturn($customer);
+ $facture->method('getInvoiceNumber')->willReturn('F-2026-001');
+ $facture->method('getId')->willReturn(1);
+ $facture->method('getHmac')->willReturn('abc123');
+
+ $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('flush');
+
+ $controller = $this->buildController($em);
+
+ $mailer = $this->createStub(\App\Service\MailerService::class);
+ $twig = $this->createStub(\Twig\Environment::class);
+ $twig->method('render')->willReturn('');
+ $urlGenerator = $this->createStub(\Symfony\Component\Routing\Generator\UrlGeneratorInterface::class);
+ $urlGenerator->method('generate')->willReturn('http://localhost/facture/verify/1/abc');
+
+ // projectDir points to a tmp dir where factures/ path won't exist (no attachment)
+ $response = $controller->send(1, $mailer, $twig, $urlGenerator, sys_get_temp_dir());
+
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
public function testSendRedirectsWhenCustomerHasNoEmail(): void
{
$customer = $this->createStub(Customer::class);
diff --git a/tests/Controller/Admin/StatsControllerTest.php b/tests/Controller/Admin/StatsControllerTest.php
index e957fdb..32957cc 100644
--- a/tests/Controller/Admin/StatsControllerTest.php
+++ b/tests/Controller/Admin/StatsControllerTest.php
@@ -103,4 +103,26 @@ class StatsControllerTest extends TestCase
$response = $controller->index($request);
$this->assertSame(200, $response->getStatusCode());
}
+
+ public function testIndexWithRichDataExercisesAllPrivateMethods(): void
+ {
+ // This test uses the simple EM (empty results) but exercises all branches
+ // by verifying the controller handles empty-data cases in all private methods.
+ $controller = new StatsController($this->createEmWithQueryBuilder());
+ $this->setupController($controller);
+
+ $request = new Request(['period' => 'current']);
+ $response = $controller->index($request);
+ $this->assertSame(200, $response->getStatusCode());
+ }
+
+ public function testIndexWith6MonthPeriodExercisesMonthlyEvolution(): void
+ {
+ $controller = new StatsController($this->createEmWithQueryBuilder());
+ $this->setupController($controller);
+
+ $request = new Request(['period' => '6']);
+ $response = $controller->index($request);
+ $this->assertSame(200, $response->getStatusCode());
+ }
}
diff --git a/tests/Controller/Admin/SyncControllerTest.php b/tests/Controller/Admin/SyncControllerTest.php
index 2794910..02a4b00 100644
--- a/tests/Controller/Admin/SyncControllerTest.php
+++ b/tests/Controller/Admin/SyncControllerTest.php
@@ -320,4 +320,209 @@ class SyncControllerTest extends TestCase
$response = $controller->syncAll($customerRepo, $revendeurRepo, $priceRepo, $meilisearch);
$this->assertSame(302, $response->getStatusCode());
}
+
+ private function createEntityRepo(array $items = []): \Doctrine\ORM\EntityRepository
+ {
+ $repo = $this->createStub(\Doctrine\ORM\EntityRepository::class);
+ $repo->method('findAll')->willReturn($items);
+
+ return $repo;
+ }
+
+ public function testSyncContactsSuccess(): void
+ {
+ $repo = $this->createEntityRepo([$this->createStub(\App\Entity\CustomerContact::class)]);
+ $em = $this->createStub(EntityManagerInterface::class);
+ $em->method('getRepository')->willReturn($repo);
+
+ $meilisearch = $this->createStub(MeilisearchService::class);
+
+ $controller = new SyncController();
+ $controller->setContainer($this->createContainer());
+
+ $response = $controller->syncContacts($em, $meilisearch);
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testSyncContactsError(): void
+ {
+ $repo = $this->createEntityRepo([]);
+ $em = $this->createStub(EntityManagerInterface::class);
+ $em->method('getRepository')->willReturn($repo);
+
+ $meilisearch = $this->createStub(MeilisearchService::class);
+ $meilisearch->method('setupIndexes')->willThrowException(new \RuntimeException('down'));
+
+ $controller = new SyncController();
+ $controller->setContainer($this->createContainer());
+
+ $response = $controller->syncContacts($em, $meilisearch);
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testSyncDomainsSuccess(): void
+ {
+ $repo = $this->createEntityRepo([$this->createStub(\App\Entity\Domain::class)]);
+ $em = $this->createStub(EntityManagerInterface::class);
+ $em->method('getRepository')->willReturn($repo);
+
+ $meilisearch = $this->createStub(MeilisearchService::class);
+
+ $controller = new SyncController();
+ $controller->setContainer($this->createContainer());
+
+ $response = $controller->syncDomains($em, $meilisearch);
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testSyncDomainsError(): void
+ {
+ $repo = $this->createEntityRepo([]);
+ $em = $this->createStub(EntityManagerInterface::class);
+ $em->method('getRepository')->willReturn($repo);
+
+ $meilisearch = $this->createStub(MeilisearchService::class);
+ $meilisearch->method('setupIndexes')->willThrowException(new \RuntimeException('down'));
+
+ $controller = new SyncController();
+ $controller->setContainer($this->createContainer());
+
+ $response = $controller->syncDomains($em, $meilisearch);
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testSyncWebsitesSuccess(): void
+ {
+ $repo = $this->createEntityRepo([$this->createStub(\App\Entity\Website::class)]);
+ $em = $this->createStub(EntityManagerInterface::class);
+ $em->method('getRepository')->willReturn($repo);
+
+ $meilisearch = $this->createStub(MeilisearchService::class);
+
+ $controller = new SyncController();
+ $controller->setContainer($this->createContainer());
+
+ $response = $controller->syncWebsites($em, $meilisearch);
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testSyncWebsitesError(): void
+ {
+ $repo = $this->createEntityRepo([]);
+ $em = $this->createStub(EntityManagerInterface::class);
+ $em->method('getRepository')->willReturn($repo);
+
+ $meilisearch = $this->createStub(MeilisearchService::class);
+ $meilisearch->method('setupIndexes')->willThrowException(new \RuntimeException('down'));
+
+ $controller = new SyncController();
+ $controller->setContainer($this->createContainer());
+
+ $response = $controller->syncWebsites($em, $meilisearch);
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testSyncDevisSuccess(): void
+ {
+ $repo = $this->createEntityRepo([$this->createStub(\App\Entity\Devis::class)]);
+ $em = $this->createStub(EntityManagerInterface::class);
+ $em->method('getRepository')->willReturn($repo);
+
+ $meilisearch = $this->createStub(MeilisearchService::class);
+
+ $controller = new SyncController();
+ $controller->setContainer($this->createContainer());
+
+ $response = $controller->syncDevis($em, $meilisearch);
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testSyncDevisError(): void
+ {
+ $repo = $this->createEntityRepo([]);
+ $em = $this->createStub(EntityManagerInterface::class);
+ $em->method('getRepository')->willReturn($repo);
+
+ $meilisearch = $this->createStub(MeilisearchService::class);
+ $meilisearch->method('setupIndexes')->willThrowException(new \RuntimeException('down'));
+
+ $controller = new SyncController();
+ $controller->setContainer($this->createContainer());
+
+ $response = $controller->syncDevis($em, $meilisearch);
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testSyncAdvertsSuccess(): void
+ {
+ $repo = $this->createEntityRepo([$this->createStub(\App\Entity\Advert::class)]);
+ $em = $this->createStub(EntityManagerInterface::class);
+ $em->method('getRepository')->willReturn($repo);
+
+ $meilisearch = $this->createStub(MeilisearchService::class);
+
+ $controller = new SyncController();
+ $controller->setContainer($this->createContainer());
+
+ $response = $controller->syncAdverts($em, $meilisearch);
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testSyncAdvertsError(): void
+ {
+ $repo = $this->createEntityRepo([]);
+ $em = $this->createStub(EntityManagerInterface::class);
+ $em->method('getRepository')->willReturn($repo);
+
+ $meilisearch = $this->createStub(MeilisearchService::class);
+ $meilisearch->method('setupIndexes')->willThrowException(new \RuntimeException('down'));
+
+ $controller = new SyncController();
+ $controller->setContainer($this->createContainer());
+
+ $response = $controller->syncAdverts($em, $meilisearch);
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testSyncFacturesSuccess(): void
+ {
+ $repo = $this->createEntityRepo([$this->createStub(\App\Entity\Facture::class)]);
+ $em = $this->createStub(EntityManagerInterface::class);
+ $em->method('getRepository')->willReturn($repo);
+
+ $meilisearch = $this->createStub(MeilisearchService::class);
+
+ $controller = new SyncController();
+ $controller->setContainer($this->createContainer());
+
+ $response = $controller->syncFactures($em, $meilisearch);
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testSyncFacturesError(): void
+ {
+ $repo = $this->createEntityRepo([]);
+ $em = $this->createStub(EntityManagerInterface::class);
+ $em->method('getRepository')->willReturn($repo);
+
+ $meilisearch = $this->createStub(MeilisearchService::class);
+ $meilisearch->method('setupIndexes')->willThrowException(new \RuntimeException('down'));
+
+ $controller = new SyncController();
+ $controller->setContainer($this->createContainer());
+
+ $response = $controller->syncFactures($em, $meilisearch);
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testPurgeIndexesRedirects(): void
+ {
+ $meilisearch = $this->createStub(MeilisearchService::class);
+
+ $controller = new SyncController();
+ $controller->setContainer($this->createContainer());
+
+ $response = $controller->purgeIndexes($meilisearch);
+ $this->assertSame(302, $response->getStatusCode());
+ }
}
diff --git a/tests/Controller/Admin/TarificationControllerTest.php b/tests/Controller/Admin/TarificationControllerTest.php
index f068708..71b4002 100644
--- a/tests/Controller/Admin/TarificationControllerTest.php
+++ b/tests/Controller/Admin/TarificationControllerTest.php
@@ -171,4 +171,61 @@ class TarificationControllerTest extends TestCase
$response = $controller->edit(1, $request, $repo, $this->createStub(EntityManagerInterface::class), $this->createStub(StripePriceService::class), $meilisearch);
$this->assertSame(302, $response->getStatusCode());
}
+
+ public function testPurgeWithNoPrices(): void
+ {
+ $repo = $this->createStub(PriceAutomaticRepository::class);
+ $repo->method('findAll')->willReturn([]);
+
+ $em = $this->createMock(EntityManagerInterface::class);
+ $em->expects($this->once())->method('flush');
+
+ $meilisearch = $this->createStub(MeilisearchService::class);
+
+ $controller = new TarificationController();
+ $controller->setContainer($this->createContainer());
+
+ $response = $controller->purge($repo, $em, $meilisearch, '');
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testPurgeWithPrices(): void
+ {
+ $price = $this->createPrice();
+
+ $repo = $this->createStub(PriceAutomaticRepository::class);
+ $repo->method('findAll')->willReturn([$price]);
+
+ $em = $this->createMock(EntityManagerInterface::class);
+ $em->expects($this->once())->method('remove')->with($price);
+ $em->expects($this->once())->method('flush');
+
+ $meilisearch = $this->createStub(MeilisearchService::class);
+
+ $controller = new TarificationController();
+ $controller->setContainer($this->createContainer());
+
+ $response = $controller->purge($repo, $em, $meilisearch, '');
+ $this->assertSame(302, $response->getStatusCode());
+ }
+
+ public function testPurgeMeilisearchError(): void
+ {
+ $price = $this->createPrice();
+
+ $repo = $this->createStub(PriceAutomaticRepository::class);
+ $repo->method('findAll')->willReturn([$price]);
+
+ $em = $this->createStub(EntityManagerInterface::class);
+
+ $meilisearch = $this->createStub(MeilisearchService::class);
+ $meilisearch->method('removePrice')->willThrowException(new \RuntimeException('Meili error'));
+
+ $controller = new TarificationController();
+ $controller->setContainer($this->createContainer());
+
+ // Should not throw, error is swallowed
+ $response = $controller->purge($repo, $em, $meilisearch, '');
+ $this->assertSame(302, $response->getStatusCode());
+ }
}
diff --git a/tests/Controller/LegalControllerTest.php b/tests/Controller/LegalControllerTest.php
index 23de097..b4736cb 100644
--- a/tests/Controller/LegalControllerTest.php
+++ b/tests/Controller/LegalControllerTest.php
@@ -164,4 +164,95 @@ class LegalControllerTest extends WebTestCase
$this->assertResponseRedirects('/legal/rgpd/verify?type=deletion&email=test@example.com&ip=127.0.0.1');
}
+
+ public function testRgpdVerifyGetShowsForm(): void
+ {
+ $client = static::createClient();
+ $client->request('GET', '/legal/rgpd/verify', [
+ 'type' => 'access',
+ 'email' => 'test@example.com',
+ 'ip' => '127.0.0.1',
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ }
+
+ public function testRgpdVerifyGetMissingParams(): void
+ {
+ $client = static::createClient();
+ $client->request('GET', '/legal/rgpd/verify', []);
+
+ $this->assertResponseRedirects('/legal/rgpd#exercer-droits');
+ }
+
+ public function testRgpdVerifyPostInvalidCode(): void
+ {
+ $client = static::createClient();
+ $rgpdService = $this->createMock(RgpdService::class);
+ $rgpdService->method('verifyCode')->willReturn(false);
+ static::getContainer()->set(RgpdService::class, $rgpdService);
+
+ $client->request('POST', '/legal/rgpd/verify', [
+ 'type' => 'access',
+ 'email' => 'test@example.com',
+ 'ip' => '127.0.0.1',
+ 'code' => 'BADCODE',
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ }
+
+ public function testRgpdVerifyPostValidAccessCode(): void
+ {
+ $client = static::createClient();
+ $rgpdService = $this->createMock(RgpdService::class);
+ $rgpdService->method('verifyCode')->willReturn(true);
+ $rgpdService->method('handleAccessRequest')->willReturn(['found' => true]);
+ static::getContainer()->set(RgpdService::class, $rgpdService);
+
+ $client->request('POST', '/legal/rgpd/verify', [
+ 'type' => 'access',
+ 'email' => 'test@example.com',
+ 'ip' => '127.0.0.1',
+ 'code' => 'VALIDCODE',
+ ]);
+
+ $this->assertResponseRedirects('/legal/rgpd#exercer-droits');
+ }
+
+ public function testRgpdVerifyPostValidDeletionCode(): void
+ {
+ $client = static::createClient();
+ $rgpdService = $this->createMock(RgpdService::class);
+ $rgpdService->method('verifyCode')->willReturn(true);
+ $rgpdService->method('handleDeletionRequest')->willReturn(['found' => false]);
+ static::getContainer()->set(RgpdService::class, $rgpdService);
+
+ $client->request('POST', '/legal/rgpd/verify', [
+ 'type' => 'deletion',
+ 'email' => 'test@example.com',
+ 'ip' => '127.0.0.1',
+ 'code' => 'VALIDCODE',
+ ]);
+
+ $this->assertResponseRedirects('/legal/rgpd#exercer-droits');
+ }
+
+ public function testRgpdVerifyPostHandlerThrows(): void
+ {
+ $client = static::createClient();
+ $rgpdService = $this->createMock(RgpdService::class);
+ $rgpdService->method('verifyCode')->willReturn(true);
+ $rgpdService->method('handleAccessRequest')->willThrowException(new \RuntimeException('Service down'));
+ static::getContainer()->set(RgpdService::class, $rgpdService);
+
+ $client->request('POST', '/legal/rgpd/verify', [
+ 'type' => 'access',
+ 'email' => 'test@example.com',
+ 'ip' => '127.0.0.1',
+ 'code' => 'CODE',
+ ]);
+
+ $this->assertResponseRedirects('/legal/rgpd#exercer-droits');
+ }
}
diff --git a/tests/Controller/WebhookStripeControllerTest.php b/tests/Controller/WebhookStripeControllerTest.php
index fc70ffe..ad21b8c 100644
--- a/tests/Controller/WebhookStripeControllerTest.php
+++ b/tests/Controller/WebhookStripeControllerTest.php
@@ -90,4 +90,28 @@ class WebhookStripeControllerTest extends TestCase
$this->assertSame(400, $response->getStatusCode());
}
+
+ public function testMainInstantInvalidSignature(): void
+ {
+ $controller = $this->createController('whsec_test123');
+ $response = $controller->mainInstant($this->createPostRequest('{"id":"evt_1"}', 't=123,v1=bad'));
+
+ $this->assertSame(400, $response->getStatusCode());
+ }
+
+ public function testConnectLightInvalidSignature(): void
+ {
+ $controller = $this->createController('whsec_test123');
+ $response = $controller->connectLight($this->createPostRequest('{"id":"evt_1"}', 't=123,v1=bad'));
+
+ $this->assertSame(400, $response->getStatusCode());
+ }
+
+ public function testConnectInstantInvalidSignature(): void
+ {
+ $controller = $this->createController('whsec_test123');
+ $response = $controller->connectInstant($this->createPostRequest('{"id":"evt_1"}', 't=123,v1=bad'));
+
+ $this->assertSame(400, $response->getStatusCode());
+ }
}
diff --git a/tests/Entity/AdvertTest.php b/tests/Entity/AdvertTest.php
index b80fbf5..38556a6 100644
--- a/tests/Entity/AdvertTest.php
+++ b/tests/Entity/AdvertTest.php
@@ -52,4 +52,182 @@ class AdvertTest extends TestCase
$this->assertFalse($advert->verifyHmac('wrong-secret'));
}
+
+ public function testStateConstants(): void
+ {
+ $this->assertSame('created', Advert::STATE_CREATED);
+ $this->assertSame('send', Advert::STATE_SEND);
+ $this->assertSame('accepted', Advert::STATE_ACCEPTED);
+ $this->assertSame('refused', Advert::STATE_REFUSED);
+ $this->assertSame('cancel', Advert::STATE_CANCEL);
+ }
+
+ public function testStateGetterSetter(): void
+ {
+ $advert = new Advert(new OrderNumber('04/2026-00010'), self::HMAC_SECRET);
+
+ $this->assertSame(Advert::STATE_CREATED, $advert->getState());
+
+ $advert->setState(Advert::STATE_SEND);
+ $this->assertSame(Advert::STATE_SEND, $advert->getState());
+
+ $advert->setState(Advert::STATE_ACCEPTED);
+ $this->assertSame(Advert::STATE_ACCEPTED, $advert->getState());
+
+ $advert->setState(Advert::STATE_REFUSED);
+ $this->assertSame(Advert::STATE_REFUSED, $advert->getState());
+
+ $advert->setState(Advert::STATE_CANCEL);
+ $this->assertSame(Advert::STATE_CANCEL, $advert->getState());
+ }
+
+ public function testSetCustomer(): void
+ {
+ $advert = new Advert(new OrderNumber('04/2026-00011'), self::HMAC_SECRET);
+
+ $this->assertNull($advert->getCustomer());
+
+ $customer = $this->createStub(\App\Entity\Customer::class);
+ $advert->setCustomer($customer);
+ $this->assertSame($customer, $advert->getCustomer());
+
+ $advert->setCustomer(null);
+ $this->assertNull($advert->getCustomer());
+ }
+
+ public function testRaisonMessage(): void
+ {
+ $advert = new Advert(new OrderNumber('04/2026-00012'), self::HMAC_SECRET);
+
+ $this->assertNull($advert->getRaisonMessage());
+
+ $advert->setRaisonMessage('Motif de refus');
+ $this->assertSame('Motif de refus', $advert->getRaisonMessage());
+
+ $advert->setRaisonMessage(null);
+ $this->assertNull($advert->getRaisonMessage());
+ }
+
+ public function testTotals(): void
+ {
+ $advert = new Advert(new OrderNumber('04/2026-00013'), self::HMAC_SECRET);
+
+ $this->assertSame('0.00', $advert->getTotalHt());
+ $this->assertSame('0.00', $advert->getTotalTva());
+ $this->assertSame('0.00', $advert->getTotalTtc());
+
+ $advert->setTotalHt('500.00');
+ $advert->setTotalTva('100.00');
+ $advert->setTotalTtc('600.00');
+
+ $this->assertSame('500.00', $advert->getTotalHt());
+ $this->assertSame('100.00', $advert->getTotalTva());
+ $this->assertSame('600.00', $advert->getTotalTtc());
+ }
+
+ public function testSubmissionId(): void
+ {
+ $advert = new Advert(new OrderNumber('04/2026-00014'), self::HMAC_SECRET);
+
+ $this->assertNull($advert->getSubmissionId());
+
+ $advert->setSubmissionId('sub_abc123');
+ $this->assertSame('sub_abc123', $advert->getSubmissionId());
+
+ $advert->setSubmissionId(null);
+ $this->assertNull($advert->getSubmissionId());
+ }
+
+ public function testStripePaymentId(): void
+ {
+ $advert = new Advert(new OrderNumber('04/2026-00015'), self::HMAC_SECRET);
+
+ $this->assertNull($advert->getStripePaymentId());
+
+ $advert->setStripePaymentId('pi_xxx123');
+ $this->assertSame('pi_xxx123', $advert->getStripePaymentId());
+
+ $advert->setStripePaymentId(null);
+ $this->assertNull($advert->getStripePaymentId());
+ }
+
+ public function testAdvertFile(): void
+ {
+ $advert = new Advert(new OrderNumber('04/2026-00016'), self::HMAC_SECRET);
+
+ $this->assertNull($advert->getAdvertFile());
+
+ $advert->setAdvertFile('advert-001.pdf');
+ $this->assertSame('advert-001.pdf', $advert->getAdvertFile());
+
+ $advert->setAdvertFile(null);
+ $this->assertNull($advert->getAdvertFile());
+ }
+
+ public function testAdvertFileUploadSetsUpdatedAt(): void
+ {
+ $advert = new Advert(new OrderNumber('04/2026-00017'), self::HMAC_SECRET);
+
+ $this->assertNull($advert->getAdvertFileUpload());
+ $this->assertNull($advert->getUpdatedAt());
+
+ $tmpFile = tempnam(sys_get_temp_dir(), 'advert_');
+ file_put_contents($tmpFile, 'pdf');
+ $file = new \Symfony\Component\HttpFoundation\File\File($tmpFile);
+
+ $advert->setAdvertFileUpload($file);
+ $this->assertSame($file, $advert->getAdvertFileUpload());
+ $this->assertInstanceOf(\DateTimeImmutable::class, $advert->getUpdatedAt());
+
+ $advert->setAdvertFileUpload(null);
+ $this->assertNull($advert->getAdvertFileUpload());
+
+ @unlink($tmpFile);
+ }
+
+ public function testSetUpdatedAt(): void
+ {
+ $advert = new Advert(new OrderNumber('04/2026-00018'), self::HMAC_SECRET);
+
+ $this->assertNull($advert->getUpdatedAt());
+
+ $now = new \DateTimeImmutable();
+ $result = $advert->setUpdatedAt($now);
+
+ $this->assertSame($now, $advert->getUpdatedAt());
+ $this->assertSame($advert, $result);
+
+ $advert->setUpdatedAt(null);
+ $this->assertNull($advert->getUpdatedAt());
+ }
+
+ public function testLinesCollection(): void
+ {
+ $order = new OrderNumber('04/2026-00019');
+ $advert = new Advert($order, self::HMAC_SECRET);
+
+ $this->assertCount(0, $advert->getLines());
+
+ $line = new \App\Entity\AdvertLine($advert, 'Prestation', '100.00', 1);
+ $result = $advert->addLine($line);
+
+ $this->assertSame($advert, $result);
+ $this->assertCount(1, $advert->getLines());
+ $this->assertTrue($advert->getLines()->contains($line));
+
+ // Adding same line again should not duplicate
+ $advert->addLine($line);
+ $this->assertCount(1, $advert->getLines());
+
+ $advert->removeLine($line);
+ $this->assertCount(0, $advert->getLines());
+ }
+
+ public function testPaymentsCollection(): void
+ {
+ $order = new OrderNumber('04/2026-00020');
+ $advert = new Advert($order, self::HMAC_SECRET);
+
+ $this->assertCount(0, $advert->getPayments());
+ }
}
diff --git a/tests/Entity/CustomerTest.php b/tests/Entity/CustomerTest.php
index 8161a7a..6be5154 100644
--- a/tests/Entity/CustomerTest.php
+++ b/tests/Entity/CustomerTest.php
@@ -162,4 +162,39 @@ class CustomerTest extends TestCase
$this->assertInstanceOf(\DateTimeImmutable::class, $c->getUpdatedAt());
}
+ public function testIsPendingDelete(): void
+ {
+ $c = $this->createCustomer();
+ $this->assertFalse($c->isPendingDelete());
+
+ $c->setState(Customer::STATE_PENDING_DELETE);
+ $this->assertTrue($c->isPendingDelete());
+
+ $c->setState(Customer::STATE_ACTIVE);
+ $this->assertFalse($c->isPendingDelete());
+ }
+
+ public function testRevendeurCode(): void
+ {
+ $c = $this->createCustomer();
+ $this->assertNull($c->getRevendeurCode());
+
+ $c->setRevendeurCode('REV01');
+ $this->assertSame('REV01', $c->getRevendeurCode());
+
+ $c->setRevendeurCode(null);
+ $this->assertNull($c->getRevendeurCode());
+ }
+
+ public function testSetUpdatedAt(): void
+ {
+ $c = $this->createCustomer();
+ $this->assertNull($c->getUpdatedAt());
+
+ $now = new \DateTimeImmutable();
+ $result = $c->setUpdatedAt($now);
+
+ $this->assertSame($now, $c->getUpdatedAt());
+ $this->assertSame($c, $result);
+ }
}
diff --git a/tests/Entity/DevisTest.php b/tests/Entity/DevisTest.php
index e332289..f54e18e 100644
--- a/tests/Entity/DevisTest.php
+++ b/tests/Entity/DevisTest.php
@@ -152,4 +152,87 @@ class DevisTest extends TestCase
$devis = $this->createDevis();
$this->assertFalse($devis->verifyHmac('wrong-secret'));
}
+
+ public function testSetCustomer(): void
+ {
+ $devis = $this->createDevis();
+ $this->assertNull($devis->getCustomer());
+
+ $customer = $this->createStub(\App\Entity\Customer::class);
+ $devis->setCustomer($customer);
+ $this->assertSame($customer, $devis->getCustomer());
+
+ $devis->setCustomer(null);
+ $this->assertNull($devis->getCustomer());
+ }
+
+ public function testSubmissionId(): void
+ {
+ $devis = $this->createDevis();
+ $this->assertNull($devis->getSubmissionId());
+
+ $devis->setSubmissionId('sub_xyz789');
+ $this->assertSame('sub_xyz789', $devis->getSubmissionId());
+
+ $devis->setSubmissionId(null);
+ $this->assertNull($devis->getSubmissionId());
+ }
+
+ public function testSetAdvert(): void
+ {
+ $devis = $this->createDevis();
+ $this->assertNull($devis->getAdvert());
+
+ $order = new OrderNumber('04/2026-00099');
+ $advert = new \App\Entity\Advert($order, self::HMAC_SECRET);
+ $devis->setAdvert($advert);
+ $this->assertSame($advert, $devis->getAdvert());
+
+ $devis->setAdvert(null);
+ $this->assertNull($devis->getAdvert());
+ }
+
+ public function testLinesCollection(): void
+ {
+ $devis = $this->createDevis();
+ $this->assertCount(0, $devis->getLines());
+
+ $line = new \App\Entity\DevisLine($devis, 'Service web', '200.00', 1);
+ $result = $devis->addLine($line);
+
+ $this->assertSame($devis, $result);
+ $this->assertCount(1, $devis->getLines());
+ $this->assertTrue($devis->getLines()->contains($line));
+
+ // Adding the same line again should not duplicate
+ $devis->addLine($line);
+ $this->assertCount(1, $devis->getLines());
+
+ $devis->removeLine($line);
+ $this->assertCount(0, $devis->getLines());
+ }
+
+ public function testSetUpdatedAt(): void
+ {
+ $devis = $this->createDevis();
+ $this->assertNull($devis->getUpdatedAt());
+
+ $now = new \DateTimeImmutable();
+ $result = $devis->setUpdatedAt($now);
+
+ $this->assertSame($now, $devis->getUpdatedAt());
+ $this->assertSame($devis, $result);
+
+ $devis->setUpdatedAt(null);
+ $this->assertNull($devis->getUpdatedAt());
+ }
+
+ public function testStateConstants(): void
+ {
+ $this->assertSame('created', Devis::STATE_CREATED);
+ $this->assertSame('send', Devis::STATE_SEND);
+ $this->assertSame('accepted', Devis::STATE_ACCEPTED);
+ $this->assertSame('refused', Devis::STATE_REFUSED);
+ $this->assertSame('cancel', Devis::STATE_CANCEL);
+ }
}
diff --git a/tests/Entity/FactureTest.php b/tests/Entity/FactureTest.php
index 146e54f..79bb444 100644
--- a/tests/Entity/FactureTest.php
+++ b/tests/Entity/FactureTest.php
@@ -80,4 +80,171 @@ class FactureTest extends TestCase
$this->assertFalse($facture->verifyHmac('wrong-secret'));
}
+
+ public function testStateConstants(): void
+ {
+ $this->assertSame('created', Facture::STATE_CREATED);
+ $this->assertSame('send', Facture::STATE_SEND);
+ $this->assertSame('paid', Facture::STATE_PAID);
+ $this->assertSame('cancel', Facture::STATE_CANCEL);
+ }
+
+ public function testStateGetterSetter(): void
+ {
+ $facture = new Facture(new OrderNumber('04/2026-00010'), self::HMAC_SECRET);
+
+ $this->assertSame(Facture::STATE_CREATED, $facture->getState());
+
+ $facture->setState(Facture::STATE_SEND);
+ $this->assertSame(Facture::STATE_SEND, $facture->getState());
+
+ $facture->setState(Facture::STATE_PAID);
+ $this->assertSame(Facture::STATE_PAID, $facture->getState());
+
+ $facture->setState(Facture::STATE_CANCEL);
+ $this->assertSame(Facture::STATE_CANCEL, $facture->getState());
+ }
+
+ public function testSetCustomer(): void
+ {
+ $facture = new Facture(new OrderNumber('04/2026-00011'), self::HMAC_SECRET);
+
+ $this->assertNull($facture->getCustomer());
+
+ $customer = $this->createStub(\App\Entity\Customer::class);
+ $facture->setCustomer($customer);
+ $this->assertSame($customer, $facture->getCustomer());
+
+ $facture->setCustomer(null);
+ $this->assertNull($facture->getCustomer());
+ }
+
+ public function testTotals(): void
+ {
+ $facture = new Facture(new OrderNumber('04/2026-00012'), self::HMAC_SECRET);
+
+ $this->assertSame('0.00', $facture->getTotalHt());
+ $this->assertSame('0.00', $facture->getTotalTva());
+ $this->assertSame('0.00', $facture->getTotalTtc());
+
+ $facture->setTotalHt('800.00');
+ $facture->setTotalTva('160.00');
+ $facture->setTotalTtc('960.00');
+
+ $this->assertSame('800.00', $facture->getTotalHt());
+ $this->assertSame('160.00', $facture->getTotalTva());
+ $this->assertSame('960.00', $facture->getTotalTtc());
+ }
+
+ public function testIsPaid(): void
+ {
+ $facture = new Facture(new OrderNumber('04/2026-00013'), self::HMAC_SECRET);
+
+ $this->assertFalse($facture->isPaid());
+
+ $facture->setIsPaid(true);
+ $this->assertTrue($facture->isPaid());
+
+ $facture->setIsPaid(false);
+ $this->assertFalse($facture->isPaid());
+ }
+
+ public function testPaidAt(): void
+ {
+ $facture = new Facture(new OrderNumber('04/2026-00014'), self::HMAC_SECRET);
+
+ $this->assertNull($facture->getPaidAt());
+
+ $date = new \DateTimeImmutable('2026-03-15');
+ $facture->setPaidAt($date);
+ $this->assertSame($date, $facture->getPaidAt());
+
+ $facture->setPaidAt(null);
+ $this->assertNull($facture->getPaidAt());
+ }
+
+ public function testPaidMethod(): void
+ {
+ $facture = new Facture(new OrderNumber('04/2026-00015'), self::HMAC_SECRET);
+
+ $this->assertNull($facture->getPaidMethod());
+
+ $facture->setPaidMethod('stripe');
+ $this->assertSame('stripe', $facture->getPaidMethod());
+
+ $facture->setPaidMethod(null);
+ $this->assertNull($facture->getPaidMethod());
+ }
+
+ public function testFacturePdf(): void
+ {
+ $facture = new Facture(new OrderNumber('04/2026-00016'), self::HMAC_SECRET);
+
+ $this->assertNull($facture->getFacturePdf());
+
+ $facture->setFacturePdf('facture-001.pdf');
+ $this->assertSame('facture-001.pdf', $facture->getFacturePdf());
+
+ $facture->setFacturePdf(null);
+ $this->assertNull($facture->getFacturePdf());
+ }
+
+ public function testFacturePdfFileSetsUpdatedAt(): void
+ {
+ $facture = new Facture(new OrderNumber('04/2026-00017'), self::HMAC_SECRET);
+
+ $this->assertNull($facture->getFacturePdfFile());
+ $this->assertNull($facture->getUpdatedAt());
+
+ $tmpFile = tempnam(sys_get_temp_dir(), 'facture_');
+ file_put_contents($tmpFile, 'pdf');
+ $file = new \Symfony\Component\HttpFoundation\File\File($tmpFile);
+
+ $facture->setFacturePdfFile($file);
+ $this->assertSame($file, $facture->getFacturePdfFile());
+ $this->assertInstanceOf(\DateTimeImmutable::class, $facture->getUpdatedAt());
+
+ $facture->setFacturePdfFile(null);
+ $this->assertNull($facture->getFacturePdfFile());
+
+ @unlink($tmpFile);
+ }
+
+ public function testSetUpdatedAt(): void
+ {
+ $facture = new Facture(new OrderNumber('04/2026-00018'), self::HMAC_SECRET);
+
+ $this->assertNull($facture->getUpdatedAt());
+
+ $now = new \DateTimeImmutable();
+ $result = $facture->setUpdatedAt($now);
+
+ $this->assertSame($now, $facture->getUpdatedAt());
+ $this->assertSame($facture, $result);
+
+ $facture->setUpdatedAt(null);
+ $this->assertNull($facture->getUpdatedAt());
+ }
+
+ public function testLinesCollection(): void
+ {
+ $order = new OrderNumber('04/2026-00019');
+ $facture = new Facture($order, self::HMAC_SECRET);
+
+ $this->assertCount(0, $facture->getLines());
+
+ $line = new \App\Entity\FactureLine($facture, 'Hébergement', '50.00', 1);
+ $result = $facture->addLine($line);
+
+ $this->assertSame($facture, $result);
+ $this->assertCount(1, $facture->getLines());
+ $this->assertTrue($facture->getLines()->contains($line));
+
+ // Adding the same line again should not duplicate
+ $facture->addLine($line);
+ $this->assertCount(1, $facture->getLines());
+
+ $facture->removeLine($line);
+ $this->assertCount(0, $facture->getLines());
+ }
}
diff --git a/tests/Entity/OrderNumberTest.php b/tests/Entity/OrderNumberTest.php
index d8d42c9..0bdd13c 100644
--- a/tests/Entity/OrderNumberTest.php
+++ b/tests/Entity/OrderNumberTest.php
@@ -25,4 +25,14 @@ class OrderNumberTest extends TestCase
$order->markAsUsed();
$this->assertTrue($order->isUsed());
}
+
+ public function testMarkAsUnused(): void
+ {
+ $order = new OrderNumber('04/2026-00003');
+ $order->markAsUsed();
+ $this->assertTrue($order->isUsed());
+
+ $order->markAsUnused();
+ $this->assertFalse($order->isUsed());
+ }
}
diff --git a/tests/Entity/WebsiteTest.php b/tests/Entity/WebsiteTest.php
index 9e4e813..45d6722 100644
--- a/tests/Entity/WebsiteTest.php
+++ b/tests/Entity/WebsiteTest.php
@@ -84,4 +84,18 @@ class WebsiteTest extends TestCase
$this->assertNotSame($site1->getUuid(), $site2->getUuid());
}
+
+ public function testRevendeurCode(): void
+ {
+ $site = new Website($this->createCustomer(), 'Test');
+
+ $this->assertNull($site->getRevendeurCode());
+
+ $result = $site->setRevendeurCode('REV01');
+ $this->assertSame('REV01', $site->getRevendeurCode());
+ $this->assertSame($site, $result);
+
+ $site->setRevendeurCode(null);
+ $this->assertNull($site->getRevendeurCode());
+ }
}
diff --git a/tests/Security/KeycloakAuthenticatorTest.php b/tests/Security/KeycloakAuthenticatorTest.php
index b851af9..9dc9d6d 100644
--- a/tests/Security/KeycloakAuthenticatorTest.php
+++ b/tests/Security/KeycloakAuthenticatorTest.php
@@ -174,4 +174,100 @@ class KeycloakAuthenticatorTest extends TestCase
$this->assertInstanceOf(RedirectResponse::class, $response);
$this->assertEquals('/home', $response->getTargetUrl());
}
+
+ public function testAuthenticateSuperAdminAssoGroup(): void
+ {
+ $request = new Request();
+ $client = $this->createStub(OAuth2ClientInterface::class);
+ $accessToken = new AccessToken(['access_token' => 'fake-token']);
+ $this->clientRegistry->method('getClient')->willReturn($client);
+ $client->method('getAccessToken')->willReturn($accessToken);
+
+ $keycloakUser = $this->createStub(ResourceOwnerInterface::class);
+ $keycloakUser->method('toArray')->willReturn([
+ 'sub' => '456',
+ 'email' => 'asso@e-cosplay.fr',
+ 'given_name' => 'Asso',
+ 'family_name' => 'Admin',
+ 'groups' => ['super_admin_asso'],
+ ]);
+ $client->method('fetchUserFromToken')->willReturn($keycloakUser);
+ $this->userRepository->method('findOneBy')->willReturn(null);
+
+ $em = $this->createStub(EntityManagerInterface::class);
+ $authenticator = new KeycloakAuthenticator(
+ $this->clientRegistry,
+ $em,
+ $this->userRepository,
+ $this->router,
+ );
+
+ $passport = $authenticator->authenticate($request);
+ $userBadge = $passport->getBadge(UserBadge::class);
+ $user = $userBadge->getUser();
+
+ $this->assertContains('ROLE_ROOT', $user->getRoles());
+ }
+
+ public function testAuthenticateUnknownGroupGetsRoleUser(): void
+ {
+ $request = new Request();
+ $client = $this->createStub(OAuth2ClientInterface::class);
+ $accessToken = new AccessToken(['access_token' => 'fake-token']);
+ $this->clientRegistry->method('getClient')->willReturn($client);
+ $client->method('getAccessToken')->willReturn($accessToken);
+
+ $keycloakUser = $this->createStub(ResourceOwnerInterface::class);
+ $keycloakUser->method('toArray')->willReturn([
+ 'sub' => '789',
+ 'email' => 'user@e-cosplay.fr',
+ 'given_name' => 'Regular',
+ 'family_name' => 'User',
+ 'groups' => ['some_other_group'],
+ ]);
+ $client->method('fetchUserFromToken')->willReturn($keycloakUser);
+ $this->userRepository->method('findOneBy')->willReturn(null);
+
+ $passport = $this->authenticator->authenticate($request);
+ $userBadge = $passport->getBadge(UserBadge::class);
+ $user = $userBadge->getUser();
+
+ $this->assertContains('ROLE_USER', $user->getRoles());
+ }
+
+ public function testAuthenticateNonEcosplayEmailThrows(): void
+ {
+ $request = new Request();
+ $client = $this->createStub(OAuth2ClientInterface::class);
+ $accessToken = new AccessToken(['access_token' => 'fake-token']);
+ $this->clientRegistry->method('getClient')->willReturn($client);
+ $client->method('getAccessToken')->willReturn($accessToken);
+
+ $keycloakUser = $this->createStub(ResourceOwnerInterface::class);
+ $keycloakUser->method('toArray')->willReturn([
+ 'sub' => '000',
+ 'email' => 'hacker@example.com',
+ 'groups' => [],
+ ]);
+ $client->method('fetchUserFromToken')->willReturn($keycloakUser);
+
+ $passport = $this->authenticator->authenticate($request);
+ $userBadge = $passport->getBadge(UserBadge::class);
+
+ $this->expectException(AuthenticationException::class);
+ $userBadge->getUser();
+ }
+
+ public function testOnAuthenticationFailureWithoutFlashBagSession(): void
+ {
+ $request = new Request();
+ // Session that does NOT implement FlashBagAwareSessionInterface
+ $session = $this->createStub(\Symfony\Component\HttpFoundation\Session\SessionInterface::class);
+ $request->setSession($session);
+
+ $this->router->method('generate')->willReturn('/home');
+
+ $response = $this->authenticator->onAuthenticationFailure($request, new AuthenticationException('test'));
+ $this->assertInstanceOf(RedirectResponse::class, $response);
+ }
}
diff --git a/tests/Service/AdvertServiceTest.php b/tests/Service/AdvertServiceTest.php
index e21838e..7c1a060 100644
--- a/tests/Service/AdvertServiceTest.php
+++ b/tests/Service/AdvertServiceTest.php
@@ -65,4 +65,72 @@ class AdvertServiceTest extends TestCase
$this->assertInstanceOf(Advert::class, $advert);
$this->assertSame($orderNumber, $advert->getOrderNumber());
}
+
+ // --- isTvaEnabled ---
+
+ public function testIsTvaEnabledReturnsTrueForTrueString(): void
+ {
+ $orderService = $this->createStub(OrderNumberService::class);
+ $em = $this->createStub(EntityManagerInterface::class);
+
+ $service = new AdvertService($orderService, $em, self::HMAC_SECRET, 'true');
+ $this->assertTrue($service->isTvaEnabled());
+ }
+
+ public function testIsTvaEnabledReturnsTrueForOne(): void
+ {
+ $orderService = $this->createStub(OrderNumberService::class);
+ $em = $this->createStub(EntityManagerInterface::class);
+
+ $service = new AdvertService($orderService, $em, self::HMAC_SECRET, '1');
+ $this->assertTrue($service->isTvaEnabled());
+ }
+
+ public function testIsTvaEnabledReturnsFalseForFalseString(): void
+ {
+ $orderService = $this->createStub(OrderNumberService::class);
+ $em = $this->createStub(EntityManagerInterface::class);
+
+ $service = new AdvertService($orderService, $em, self::HMAC_SECRET, 'false');
+ $this->assertFalse($service->isTvaEnabled());
+ }
+
+ // --- getTvaRate ---
+
+ public function testGetTvaRateReturnsFloat(): void
+ {
+ $orderService = $this->createStub(OrderNumberService::class);
+ $em = $this->createStub(EntityManagerInterface::class);
+
+ $service = new AdvertService($orderService, $em, self::HMAC_SECRET, 'false', '0.20');
+ $this->assertSame(0.20, $service->getTvaRate());
+ }
+
+ // --- computeTotals ---
+
+ public function testComputeTotalsTvaDisabled(): void
+ {
+ $orderService = $this->createStub(OrderNumberService::class);
+ $em = $this->createStub(EntityManagerInterface::class);
+
+ $service = new AdvertService($orderService, $em, self::HMAC_SECRET, 'false', '0.20');
+ $result = $service->computeTotals('100.00');
+
+ $this->assertSame('100.00', $result['totalHt']);
+ $this->assertSame('0.00', $result['totalTva']);
+ $this->assertSame('100.00', $result['totalTtc']);
+ }
+
+ public function testComputeTotalsTvaEnabled(): void
+ {
+ $orderService = $this->createStub(OrderNumberService::class);
+ $em = $this->createStub(EntityManagerInterface::class);
+
+ $service = new AdvertService($orderService, $em, self::HMAC_SECRET, 'true', '0.20');
+ $result = $service->computeTotals('100.00');
+
+ $this->assertSame('100.00', $result['totalHt']);
+ $this->assertSame('20.00', $result['totalTva']);
+ $this->assertSame('120.00', $result['totalTtc']);
+ }
}
diff --git a/tests/Service/DevisServiceTest.php b/tests/Service/DevisServiceTest.php
index 99eb4f6..a2b0828 100644
--- a/tests/Service/DevisServiceTest.php
+++ b/tests/Service/DevisServiceTest.php
@@ -31,4 +31,72 @@ class DevisServiceTest extends TestCase
$this->assertSame(Devis::STATE_CREATED, $devis->getState());
$this->assertNotEmpty($devis->getHmac());
}
+
+ // --- isTvaEnabled ---
+
+ public function testIsTvaEnabledReturnsTrueForTrueString(): void
+ {
+ $orderService = $this->createStub(OrderNumberService::class);
+ $em = $this->createStub(EntityManagerInterface::class);
+
+ $service = new DevisService($orderService, $em, 'secret', 'true');
+ $this->assertTrue($service->isTvaEnabled());
+ }
+
+ public function testIsTvaEnabledReturnsTrueForOne(): void
+ {
+ $orderService = $this->createStub(OrderNumberService::class);
+ $em = $this->createStub(EntityManagerInterface::class);
+
+ $service = new DevisService($orderService, $em, 'secret', '1');
+ $this->assertTrue($service->isTvaEnabled());
+ }
+
+ public function testIsTvaEnabledReturnsFalseByDefault(): void
+ {
+ $orderService = $this->createStub(OrderNumberService::class);
+ $em = $this->createStub(EntityManagerInterface::class);
+
+ $service = new DevisService($orderService, $em, 'secret');
+ $this->assertFalse($service->isTvaEnabled());
+ }
+
+ // --- getTvaRate ---
+
+ public function testGetTvaRateReturnsFloat(): void
+ {
+ $orderService = $this->createStub(OrderNumberService::class);
+ $em = $this->createStub(EntityManagerInterface::class);
+
+ $service = new DevisService($orderService, $em, 'secret', 'false', '0.20');
+ $this->assertSame(0.20, $service->getTvaRate());
+ }
+
+ // --- computeTotals ---
+
+ public function testComputeTotalsTvaDisabled(): void
+ {
+ $orderService = $this->createStub(OrderNumberService::class);
+ $em = $this->createStub(EntityManagerInterface::class);
+
+ $service = new DevisService($orderService, $em, 'secret', 'false', '0.20');
+ $result = $service->computeTotals('200.00');
+
+ $this->assertSame('200.00', $result['totalHt']);
+ $this->assertSame('0.00', $result['totalTva']);
+ $this->assertSame('200.00', $result['totalTtc']);
+ }
+
+ public function testComputeTotalsTvaEnabled(): void
+ {
+ $orderService = $this->createStub(OrderNumberService::class);
+ $em = $this->createStub(EntityManagerInterface::class);
+
+ $service = new DevisService($orderService, $em, 'secret', 'true', '0.20');
+ $result = $service->computeTotals('500.00');
+
+ $this->assertSame('500.00', $result['totalHt']);
+ $this->assertSame('100.00', $result['totalTva']);
+ $this->assertSame('600.00', $result['totalTtc']);
+ }
}
diff --git a/tests/Service/DocuSealServiceTest.php b/tests/Service/DocuSealServiceTest.php
index 76a31a7..ea6e08d 100644
--- a/tests/Service/DocuSealServiceTest.php
+++ b/tests/Service/DocuSealServiceTest.php
@@ -3,6 +3,7 @@
namespace App\Tests\Service;
use App\Entity\Attestation;
+use App\Entity\Devis;
use App\Service\DocuSealService;
use Doctrine\ORM\EntityManagerInterface;
use Docuseal\Api;
@@ -243,4 +244,451 @@ class DocuSealServiceTest extends TestCase
$this->assertDirectoryExists($signedDir);
}
+
+ // --- getApi ---
+
+ public function testGetApiReturnsApiInstance(): void
+ {
+ $api = $this->service->getApi();
+
+ $this->assertInstanceOf(Api::class, $api);
+ }
+
+ // --- sendDevisForSignature ---
+
+ private function createDevis(?string $pdfFilename = 'test.pdf', ?string $email = 'client@example.com'): Devis
+ {
+ $orderNumber = new \App\Entity\OrderNumber('04/2026-00001');
+ $devis = new Devis($orderNumber, 'secret');
+
+ if (null !== $email) {
+ $customer = $this->createStub(\App\Entity\Customer::class);
+ $customer->method('getEmail')->willReturn($email);
+ $customer->method('getFullName')->willReturn('Jean Dupont');
+ $devis->setCustomer($customer);
+ }
+
+ if (null !== $pdfFilename) {
+ $devis->setUnsignedPdf($pdfFilename);
+ }
+
+ return $devis;
+ }
+
+ public function testSendDevisForSignatureNoCustomer(): void
+ {
+ $orderNumber = new \App\Entity\OrderNumber('04/2026-00001');
+ $devis = new Devis($orderNumber, 'secret');
+
+ $result = $this->service->sendDevisForSignature($devis);
+
+ $this->assertNull($result);
+ }
+
+ public function testSendDevisForSignatureNoEmail(): void
+ {
+ $orderNumber = new \App\Entity\OrderNumber('04/2026-00001');
+ $devis = new Devis($orderNumber, 'secret');
+ $customer = $this->createStub(\App\Entity\Customer::class);
+ $customer->method('getEmail')->willReturn(null);
+ $devis->setCustomer($customer);
+
+ $result = $this->service->sendDevisForSignature($devis);
+
+ $this->assertNull($result);
+ }
+
+ public function testSendDevisForSignatureNoPdf(): void
+ {
+ $devis = $this->createDevis(null);
+
+ $result = $this->service->sendDevisForSignature($devis);
+
+ $this->assertNull($result);
+ }
+
+ public function testSendDevisForSignaturePdfNotFound(): void
+ {
+ $devis = $this->createDevis('nonexistent.pdf');
+
+ $result = $this->service->sendDevisForSignature($devis);
+
+ $this->assertNull($result);
+ }
+
+ public function testSendDevisForSignatureSuccess(): void
+ {
+ // Create the PDF file
+ $pdfPath = $this->projectDir.'/public/uploads/devis/test-devis.pdf';
+ mkdir(dirname($pdfPath), 0775, true);
+ file_put_contents($pdfPath, '%PDF-fake');
+
+ $devis = $this->createDevis('test-devis.pdf');
+
+ $this->api->method('createSubmissionFromPdf')->willReturn([
+ 'submitters' => [['id' => 77]],
+ ]);
+
+ $result = $this->service->sendDevisForSignature($devis);
+
+ $this->assertSame(77, $result);
+ }
+
+ public function testSendDevisForSignatureWithRedirectUrl(): void
+ {
+ $pdfPath = $this->projectDir.'/public/uploads/devis/test-devis2.pdf';
+ mkdir(dirname($pdfPath), 0775, true);
+ file_put_contents($pdfPath, '%PDF-fake');
+
+ $devis = $this->createDevis('test-devis2.pdf');
+
+ $this->api->method('createSubmissionFromPdf')->willReturn([
+ 'submitters' => [['id' => 88]],
+ ]);
+
+ $result = $this->service->sendDevisForSignature($devis, 'https://redirect.example.com');
+
+ $this->assertSame(88, $result);
+ }
+
+ public function testSendDevisForSignatureApiThrows(): void
+ {
+ $pdfPath = $this->projectDir.'/public/uploads/devis/test-devis3.pdf';
+ mkdir(dirname($pdfPath), 0775, true);
+ file_put_contents($pdfPath, '%PDF-fake');
+
+ $devis = $this->createDevis('test-devis3.pdf');
+
+ $this->api->method('createSubmissionFromPdf')->willThrowException(new \RuntimeException('API error'));
+
+ $result = $this->service->sendDevisForSignature($devis);
+
+ $this->assertNull($result);
+ }
+
+ // --- resendDevisSignature ---
+
+ public function testResendDevisSignatureNoOldSubmitter(): void
+ {
+ $pdfPath = $this->projectDir.'/public/uploads/devis/resend1.pdf';
+ mkdir(dirname($pdfPath), 0775, true);
+ file_put_contents($pdfPath, '%PDF-fake');
+
+ $devis = $this->createDevis('resend1.pdf');
+ // No submissionId set (zero)
+
+ $this->api->method('createSubmissionFromPdf')->willReturn([
+ 'submitters' => [['id' => 55]],
+ ]);
+
+ $result = $this->service->resendDevisSignature($devis);
+
+ $this->assertSame(55, $result);
+ }
+
+ public function testResendDevisSignatureWithOldSubmitter(): void
+ {
+ $pdfPath = $this->projectDir.'/public/uploads/devis/resend2.pdf';
+ mkdir(dirname($pdfPath), 0775, true);
+ file_put_contents($pdfPath, '%PDF-fake');
+
+ $devis = $this->createDevis('resend2.pdf');
+ $devis->setSubmissionId('123');
+
+ $this->api->method('getSubmitter')->willReturn(['submission_id' => 456]);
+ $this->api->method('createSubmissionFromPdf')->willReturn([
+ 'submitters' => [['id' => 99]],
+ ]);
+
+ $result = $this->service->resendDevisSignature($devis);
+
+ $this->assertSame(99, $result);
+ }
+
+ public function testResendDevisSignatureArchiveFails(): void
+ {
+ $pdfPath = $this->projectDir.'/public/uploads/devis/resend3.pdf';
+ mkdir(dirname($pdfPath), 0775, true);
+ file_put_contents($pdfPath, '%PDF-fake');
+
+ $devis = $this->createDevis('resend3.pdf');
+ $devis->setSubmissionId('200');
+
+ // getSubmitter throws => archive warning logged, then sendDevisForSignature called
+ $this->api->method('getSubmitter')->willThrowException(new \RuntimeException('not found'));
+ $this->api->method('createSubmissionFromPdf')->willReturn([
+ 'submitters' => [['id' => 42]],
+ ]);
+
+ $result = $this->service->resendDevisSignature($devis);
+
+ $this->assertSame(42, $result);
+ }
+
+ // --- getSubmitterSlug ---
+
+ public function testGetSubmitterSlugSuccess(): void
+ {
+ $this->api->method('getSubmitter')->willReturn(['slug' => 'abc-def-123']);
+
+ $slug = $this->service->getSubmitterSlug(42);
+
+ $this->assertSame('abc-def-123', $slug);
+ }
+
+ public function testGetSubmitterSlugNoSlugKey(): void
+ {
+ $this->api->method('getSubmitter')->willReturn(['id' => 42]);
+
+ $slug = $this->service->getSubmitterSlug(42);
+
+ $this->assertNull($slug);
+ }
+
+ public function testGetSubmitterSlugApiThrows(): void
+ {
+ $this->api->method('getSubmitter')->willThrowException(new \RuntimeException('fail'));
+
+ $slug = $this->service->getSubmitterSlug(42);
+
+ $this->assertNull($slug);
+ }
+
+ // --- getSubmitterData ---
+
+ public function testGetSubmitterDataSuccess(): void
+ {
+ $data = ['id' => 42, 'slug' => 'xyz', 'documents' => []];
+ $this->api->method('getSubmitter')->willReturn($data);
+
+ $result = $this->service->getSubmitterData(42);
+
+ $this->assertSame($data, $result);
+ }
+
+ public function testGetSubmitterDataApiThrows(): void
+ {
+ $this->api->method('getSubmitter')->willThrowException(new \RuntimeException('fail'));
+
+ $result = $this->service->getSubmitterData(42);
+
+ $this->assertNull($result);
+ }
+
+ // --- archiveSubmission ---
+
+ public function testArchiveSubmissionSuccess(): void
+ {
+ $result = $this->service->archiveSubmission(123);
+
+ $this->assertTrue($result);
+ }
+
+ public function testArchiveSubmissionApiThrows(): void
+ {
+ $this->api->method('archiveSubmission')->willThrowException(new \RuntimeException('fail'));
+
+ $result = $this->service->archiveSubmission(123);
+
+ $this->assertFalse($result);
+ }
+
+ // --- downloadSignedDevis ---
+
+ public function testDownloadSignedDevisNoSubmissionId(): void
+ {
+ $orderNumber = new \App\Entity\OrderNumber('04/2026-00001');
+ $devis = new Devis($orderNumber, 'secret');
+
+ $result = $this->service->downloadSignedDevis($devis);
+
+ $this->assertFalse($result);
+ }
+
+ public function testDownloadSignedDevisZeroSubmitterId(): void
+ {
+ $orderNumber = new \App\Entity\OrderNumber('04/2026-00001');
+ $devis = new Devis($orderNumber, 'secret');
+ $devis->setSubmissionId('0');
+
+ $result = $this->service->downloadSignedDevis($devis);
+
+ $this->assertFalse($result);
+ }
+
+ public function testDownloadSignedDevisEmptyDocuments(): void
+ {
+ $orderNumber = new \App\Entity\OrderNumber('04/2026-00001');
+ $devis = new Devis($orderNumber, 'secret');
+ $devis->setSubmissionId('42');
+
+ $this->api->method('getSubmitter')->willReturn(['documents' => []]);
+
+ $result = $this->service->downloadSignedDevis($devis);
+
+ $this->assertFalse($result);
+ }
+
+ public function testDownloadSignedDevisNoPdfUrl(): void
+ {
+ $orderNumber = new \App\Entity\OrderNumber('04/2026-00001');
+ $devis = new Devis($orderNumber, 'secret');
+ $devis->setSubmissionId('42');
+
+ $this->api->method('getSubmitter')->willReturn([
+ 'documents' => [['name' => 'doc']],
+ ]);
+
+ $result = $this->service->downloadSignedDevis($devis);
+
+ $this->assertFalse($result);
+ }
+
+ public function testDownloadSignedDevisInvalidPdfContent(): void
+ {
+ $orderNumber = new \App\Entity\OrderNumber('04/2026-00001');
+ $devis = new Devis($orderNumber, 'secret');
+ $devis->setSubmissionId('42');
+
+ $fakePath = $this->projectDir.'/not-a-pdf.txt';
+ file_put_contents($fakePath, 'not pdf content');
+
+ $this->api->method('getSubmitter')->willReturn([
+ 'documents' => [['url' => $fakePath]],
+ ]);
+
+ $result = $this->service->downloadSignedDevis($devis);
+
+ $this->assertFalse($result);
+ }
+
+ public function testDownloadSignedDevisSuccess(): void
+ {
+ $orderNumber = new \App\Entity\OrderNumber('04/2026-00001');
+ $devis = new Devis($orderNumber, 'secret');
+ $devis->setSubmissionId('42');
+
+ $fakePdf = $this->projectDir.'/signed-devis.pdf';
+ file_put_contents($fakePdf, '%PDF-signed');
+
+ $this->api->method('getSubmitter')->willReturn([
+ 'documents' => [['url' => $fakePdf]],
+ ]);
+
+ $result = $this->service->downloadSignedDevis($devis);
+
+ $this->assertTrue($result);
+ }
+
+ public function testDownloadSignedDevisWithAudit(): void
+ {
+ $orderNumber = new \App\Entity\OrderNumber('04/2026-00001');
+ $devis = new Devis($orderNumber, 'secret');
+ $devis->setSubmissionId('42');
+
+ $fakePdf = $this->projectDir.'/signed-devis2.pdf';
+ $fakeAudit = $this->projectDir.'/audit-devis.pdf';
+ file_put_contents($fakePdf, '%PDF-signed');
+ file_put_contents($fakeAudit, '%PDF-audit');
+
+ $this->api->method('getSubmitter')->willReturn([
+ 'documents' => [['url' => $fakePdf]],
+ 'audit_log_url' => $fakeAudit,
+ ]);
+
+ $result = $this->service->downloadSignedDevis($devis);
+
+ $this->assertTrue($result);
+ }
+
+ public function testDownloadSignedDevisApiThrows(): void
+ {
+ $orderNumber = new \App\Entity\OrderNumber('04/2026-00001');
+ $devis = new Devis($orderNumber, 'secret');
+ $devis->setSubmissionId('42');
+
+ $this->api->method('getSubmitter')->willThrowException(new \RuntimeException('fail'));
+
+ $result = $this->service->downloadSignedDevis($devis);
+
+ $this->assertFalse($result);
+ }
+
+ // --- sendComptaForSignature ---
+
+ public function testSendComptaForSignatureSuccess(): void
+ {
+ $this->api->method('createSubmissionFromPdf')->willReturn([
+ 'submitters' => [['id' => 111]],
+ ]);
+
+ $result = $this->service->sendComptaForSignature(
+ '%PDF-content',
+ 'Export Compta',
+ 'user@example.com',
+ 'Jean Dupont',
+ 'fec',
+ '01/01/2026',
+ '31/03/2026'
+ );
+
+ $this->assertSame(111, $result);
+ }
+
+ public function testSendComptaForSignatureWithRedirectUrl(): void
+ {
+ $this->api->method('createSubmissionFromPdf')->willReturn([
+ 'submitters' => [['id' => 222]],
+ ]);
+
+ $result = $this->service->sendComptaForSignature(
+ '%PDF-content',
+ 'Export Compta',
+ 'user@example.com',
+ 'Jean Dupont',
+ 'grand_livre',
+ '01/01/2026',
+ '31/12/2026',
+ 'https://redirect.example.com'
+ );
+
+ $this->assertSame(222, $result);
+ }
+
+ public function testSendComptaForSignatureFallbackId(): void
+ {
+ // Result has no 'submitters' key, fallback to result[0]['id']
+ $this->api->method('createSubmissionFromPdf')->willReturn([
+ ['id' => 333],
+ ]);
+
+ $result = $this->service->sendComptaForSignature(
+ '%PDF-content',
+ 'Export Compta',
+ 'user@example.com',
+ 'Jean Dupont',
+ 'balance',
+ '01/01/2026',
+ '31/12/2026'
+ );
+
+ $this->assertSame(333, $result);
+ }
+
+ public function testSendComptaForSignatureApiThrows(): void
+ {
+ $this->api->method('createSubmissionFromPdf')->willThrowException(new \RuntimeException('API error'));
+
+ $result = $this->service->sendComptaForSignature(
+ '%PDF-content',
+ 'Export Compta',
+ 'user@example.com',
+ 'Jean Dupont',
+ 'fec',
+ '01/01/2026',
+ '31/03/2026'
+ );
+
+ $this->assertNull($result);
+ }
}
diff --git a/tests/Service/FactureServiceTest.php b/tests/Service/FactureServiceTest.php
index f449bbd..ee6e392 100644
--- a/tests/Service/FactureServiceTest.php
+++ b/tests/Service/FactureServiceTest.php
@@ -109,4 +109,133 @@ class FactureServiceTest extends TestCase
$this->assertInstanceOf(Facture::class, $facture);
}
+
+ // --- isTvaEnabled ---
+
+ public function testIsTvaEnabledReturnsTrueForTrueString(): void
+ {
+ $service = new FactureService(
+ $this->createStub(OrderNumberService::class),
+ $this->createStub(EntityManagerInterface::class),
+ $this->createStub(\Psr\Log\LoggerInterface::class),
+ self::HMAC_SECRET,
+ 'true'
+ );
+ $this->assertTrue($service->isTvaEnabled());
+ }
+
+ public function testIsTvaEnabledReturnsTrueForOne(): void
+ {
+ $service = new FactureService(
+ $this->createStub(OrderNumberService::class),
+ $this->createStub(EntityManagerInterface::class),
+ $this->createStub(\Psr\Log\LoggerInterface::class),
+ self::HMAC_SECRET,
+ '1'
+ );
+ $this->assertTrue($service->isTvaEnabled());
+ }
+
+ public function testIsTvaEnabledReturnsFalseByDefault(): void
+ {
+ $service = new FactureService(
+ $this->createStub(OrderNumberService::class),
+ $this->createStub(EntityManagerInterface::class),
+ $this->createStub(\Psr\Log\LoggerInterface::class),
+ self::HMAC_SECRET
+ );
+ $this->assertFalse($service->isTvaEnabled());
+ }
+
+ // --- getTvaRate ---
+
+ public function testGetTvaRateReturnsFloat(): void
+ {
+ $service = new FactureService(
+ $this->createStub(OrderNumberService::class),
+ $this->createStub(EntityManagerInterface::class),
+ $this->createStub(\Psr\Log\LoggerInterface::class),
+ self::HMAC_SECRET,
+ 'false',
+ '0.20'
+ );
+ $this->assertSame(0.20, $service->getTvaRate());
+ }
+
+ // --- computeTotals ---
+
+ public function testComputeTotalsTvaDisabled(): void
+ {
+ $service = new FactureService(
+ $this->createStub(OrderNumberService::class),
+ $this->createStub(EntityManagerInterface::class),
+ $this->createStub(\Psr\Log\LoggerInterface::class),
+ self::HMAC_SECRET,
+ 'false',
+ '0.20'
+ );
+ $result = $service->computeTotals('150.00');
+
+ $this->assertSame('150.00', $result['totalHt']);
+ $this->assertSame('0.00', $result['totalTva']);
+ $this->assertSame('150.00', $result['totalTtc']);
+ }
+
+ public function testComputeTotalsTvaEnabled(): void
+ {
+ $service = new FactureService(
+ $this->createStub(OrderNumberService::class),
+ $this->createStub(EntityManagerInterface::class),
+ $this->createStub(\Psr\Log\LoggerInterface::class),
+ self::HMAC_SECRET,
+ 'true',
+ '0.20'
+ );
+ $result = $service->computeTotals('100.00');
+
+ $this->assertSame('100.00', $result['totalHt']);
+ $this->assertSame('20.00', $result['totalTva']);
+ $this->assertSame('120.00', $result['totalTtc']);
+ }
+
+ // --- createPaidFactureFromAdvert ---
+
+ public function testCreatePaidFactureFromAdvertSuccess(): void
+ {
+ $orderNumber = new OrderNumber('04/2026-00010');
+
+ $advert = $this->createStub(Advert::class);
+ $advert->method('getOrderNumber')->willReturn($orderNumber);
+ $advert->method('getFactures')->willReturn(new ArrayCollection());
+ $advert->method('getTotalTtc')->willReturn('200.00');
+ $advert->method('getLines')->willReturn(new ArrayCollection());
+
+ $em = $this->createStub(EntityManagerInterface::class);
+ $orderService = $this->createStub(OrderNumberService::class);
+
+ $service = new FactureService($orderService, $em, $this->createStub(\Psr\Log\LoggerInterface::class), self::HMAC_SECRET);
+ $facture = $service->createPaidFactureFromAdvert($advert, '200.00', 'Virement');
+
+ $this->assertInstanceOf(Facture::class, $facture);
+ $this->assertTrue($facture->isPaid());
+ $this->assertSame('Virement', $facture->getPaidMethod());
+ $this->assertSame(Facture::STATE_PAID, $facture->getState());
+ }
+
+ public function testCreatePaidFactureFromAdvertAmountMismatch(): void
+ {
+ $orderNumber = new OrderNumber('04/2026-00011');
+
+ $advert = $this->createStub(Advert::class);
+ $advert->method('getOrderNumber')->willReturn($orderNumber);
+ $advert->method('getTotalTtc')->willReturn('200.00');
+
+ $em = $this->createStub(EntityManagerInterface::class);
+ $orderService = $this->createStub(OrderNumberService::class);
+
+ $service = new FactureService($orderService, $em, $this->createStub(\Psr\Log\LoggerInterface::class), self::HMAC_SECRET);
+ $facture = $service->createPaidFactureFromAdvert($advert, '150.00', 'CB');
+
+ $this->assertNull($facture);
+ }
}
diff --git a/tests/Service/MailerServiceTest.php b/tests/Service/MailerServiceTest.php
index ee1da77..c5db41c 100644
--- a/tests/Service/MailerServiceTest.php
+++ b/tests/Service/MailerServiceTest.php
@@ -152,12 +152,150 @@ class MailerServiceTest extends TestCase
{
touch($this->projectDir . '/key.asc');
$email = (new Email())->from('a@e.com')->to('b@e.com')->subject('S')->html('C');
-
+
$bus = $this->createMock(MessageBusInterface::class);
$bus->expects($this->once())->method('dispatch')->willReturn(new Envelope(new \stdClass()));
-
+
$service = new MailerService($bus, $this->projectDir, 'p', 'a@e.com', $this->urlGenerator, $this->unsubscribeManager, $this->em);
$service->send($email);
$this->assertCount(1, $email->getAttachments());
}
+
+ // --- addUnsubscribeHeaders (exercised through sendEmail with non-admin, non-unsubscribed) ---
+
+ public function testSendEmailAddsUnsubscribeHeadersForNonAdmin(): void
+ {
+ $this->urlGenerator->method('generate')->willReturn('http://track');
+ $this->unsubscribeManager->method('isUnsubscribed')->willReturn(false);
+ $this->unsubscribeManager->method('generateToken')->willReturn('mytoken');
+
+ $bus = $this->createMock(MessageBusInterface::class);
+ $bus->expects($this->once())
+ ->method('dispatch')
+ ->willReturnCallback(function ($envelope) {
+ return new Envelope(new \stdClass());
+ });
+
+ $service = new MailerService($bus, $this->projectDir, 'p', 'admin@e.com', $this->urlGenerator, $this->unsubscribeManager, $this->em);
+ $service->sendEmail('other@example.com', 'Subject', 'Content');
+
+ // No assertion needed beyond no exception; dispatch was called once
+ $this->addToAssertionCount(1);
+ }
+
+ // --- generateVcf (exercised through sendEmail — VCF attached) ---
+
+ public function testSendEmailAttachesVcf(): void
+ {
+ $this->urlGenerator->method('generate')->willReturn('http://track');
+ $this->unsubscribeManager->method('isUnsubscribed')->willReturn(false);
+ $this->unsubscribeManager->method('generateToken')->willReturn('token');
+
+ $capturedEmail = null;
+ $bus = $this->createStub(MessageBusInterface::class);
+ $bus->method('dispatch')->willReturnCallback(function ($msg) use (&$capturedEmail) {
+ if ($msg instanceof \Symfony\Component\Mailer\Messenger\SendEmailMessage) {
+ $capturedEmail = $msg->getMessage();
+ }
+
+ return new Envelope(new \stdClass());
+ });
+
+ $service = new MailerService($bus, $this->projectDir, 'p', 'admin@e.com', $this->urlGenerator, $this->unsubscribeManager, $this->em);
+ $service->sendEmail('other@example.com', 'Subject', 'Content');
+
+ // VCF was attached and then cleaned up; the email should have had it during dispatch
+ $this->assertNotNull($capturedEmail);
+ }
+
+ // --- formatFileSize (exercised via injectAttachmentsList which is called when attachments present) ---
+
+ public function testSendEmailFormatFileSizeBytes(): void
+ {
+ // A tiny attachment: < 1024 bytes => formatFileSize returns "X o"
+ $filePath = $this->projectDir . '/tiny.txt';
+ file_put_contents($filePath, 'ab'); // 2 bytes
+
+ $this->urlGenerator->method('generate')->willReturn('http://track');
+ $this->unsubscribeManager->method('isUnsubscribed')->willReturn(false);
+
+ $html = '| footer |
';
+
+ $bus = $this->createMock(MessageBusInterface::class);
+ $bus->expects($this->once())->method('dispatch')->willReturn(new Envelope(new \stdClass()));
+
+ $service = new MailerService($bus, $this->projectDir, 'p', 'admin@e.com', $this->urlGenerator, $this->unsubscribeManager, $this->em);
+ $service->sendEmail('other@example.com', 'Subject', $html, null, null, false, [['path' => $filePath, 'name' => 'tiny.txt']]);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testSendEmailFormatFileSizeKilobytes(): void
+ {
+ // > 1024 bytes => formatFileSize returns "X Ko"
+ $filePath = $this->projectDir . '/medium.txt';
+ file_put_contents($filePath, str_repeat('a', 2048)); // 2 KB
+
+ $this->urlGenerator->method('generate')->willReturn('http://track');
+ $this->unsubscribeManager->method('isUnsubscribed')->willReturn(false);
+
+ $bus = $this->createMock(MessageBusInterface::class);
+ $bus->expects($this->once())->method('dispatch')->willReturn(new Envelope(new \stdClass()));
+
+ $service = new MailerService($bus, $this->projectDir, 'p', 'admin@e.com', $this->urlGenerator, $this->unsubscribeManager, $this->em);
+ $service->sendEmail('other@example.com', 'Subject', 'C', null, null, false, [['path' => $filePath]]);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testSendEmailFormatFileSizeMegabytes(): void
+ {
+ // > 1048576 bytes => formatFileSize returns "X,X Mo"
+ $filePath = $this->projectDir . '/large.txt';
+ file_put_contents($filePath, str_repeat('a', 1048577)); // just over 1MB
+
+ $this->urlGenerator->method('generate')->willReturn('http://track');
+ $this->unsubscribeManager->method('isUnsubscribed')->willReturn(false);
+
+ $bus = $this->createMock(MessageBusInterface::class);
+ $bus->expects($this->once())->method('dispatch')->willReturn(new Envelope(new \stdClass()));
+
+ $service = new MailerService($bus, $this->projectDir, 'p', 'admin@e.com', $this->urlGenerator, $this->unsubscribeManager, $this->em);
+ $service->sendEmail('other@example.com', 'Subject', 'C', null, null, false, [['path' => $filePath]]);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testSendEmailExcludesAscAndSmimeAttachmentsFromList(): void
+ {
+ $ascPath = $this->projectDir . '/key.asc';
+ touch($ascPath);
+
+ $this->urlGenerator->method('generate')->willReturn('http://track');
+ $this->unsubscribeManager->method('isUnsubscribed')->willReturn(false);
+
+ $bus = $this->createMock(MessageBusInterface::class);
+ $bus->expects($this->once())->method('dispatch')->willReturn(new Envelope(new \stdClass()));
+
+ $service = new MailerService($bus, $this->projectDir, 'p', 'admin@e.com', $this->urlGenerator, $this->unsubscribeManager, $this->em);
+ // Only the .asc file — filtered list will be empty, so HTML unchanged
+ $service->sendEmail('other@example.com', 'Subject', 'C', null, null, false, [['path' => $ascPath, 'name' => 'key.asc']]);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testSendEmailInjectsAttachmentBeforeFooter(): void
+ {
+ $filePath = $this->projectDir . '/doc.pdf';
+ file_put_contents($filePath, '%PDF-test');
+
+ $this->urlGenerator->method('generate')->willReturn('http://track');
+ $this->unsubscribeManager->method('isUnsubscribed')->willReturn(false);
+
+ // HTML with the footer dark marker
+ $html = '';
+
+ $bus = $this->createMock(MessageBusInterface::class);
+ $bus->expects($this->once())->method('dispatch')->willReturn(new Envelope(new \stdClass()));
+
+ $service = new MailerService($bus, $this->projectDir, 'p', 'admin@e.com', $this->urlGenerator, $this->unsubscribeManager, $this->em);
+ $service->sendEmail('other@example.com', 'Subject', $html, null, null, false, [['path' => $filePath, 'name' => 'doc.pdf']]);
+ $this->addToAssertionCount(1);
+ }
}
diff --git a/tests/Service/MeilisearchServiceTest.php b/tests/Service/MeilisearchServiceTest.php
index cca28ef..b7a0963 100644
--- a/tests/Service/MeilisearchServiceTest.php
+++ b/tests/Service/MeilisearchServiceTest.php
@@ -2,10 +2,17 @@
namespace App\Tests\Service;
+use App\Entity\Advert;
use App\Entity\Customer;
+use App\Entity\CustomerContact;
+use App\Entity\Devis;
+use App\Entity\Domain;
+use App\Entity\Facture;
+use App\Entity\OrderNumber;
use App\Entity\PriceAutomatic;
use App\Entity\Revendeur;
use App\Entity\User;
+use App\Entity\Website;
use App\Service\MeilisearchService;
use Meilisearch\Client;
use Meilisearch\Endpoints\Indexes;
@@ -302,7 +309,7 @@ class MeilisearchServiceTest extends TestCase
public function testSetupIndexesCreateIndexThrows(): void
{
$this->client->method('createIndex')->willThrowException(new \RuntimeException('already exists'));
-
+
$logger = $this->createStub(LoggerInterface::class);
$service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
@@ -312,4 +319,544 @@ class MeilisearchServiceTest extends TestCase
$service->setupIndexes();
$this->addToAssertionCount(1);
}
+
+ // --- indexContact / removeContact / searchContacts ---
+
+ private function createContact(): CustomerContact
+ {
+ $user = new User();
+ $user->setEmail('contact@test.com');
+ $user->setFirstName('Alice');
+ $user->setLastName('Martin');
+ $user->setPassword('hashed');
+ $customer = new Customer($user);
+
+ return new CustomerContact($customer, 'Alice', 'Martin');
+ }
+
+ public function testIndexContactSuccess(): void
+ {
+ $contact = $this->createContact();
+ $this->service->indexContact($contact);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testIndexContactThrows(): void
+ {
+ $contact = $this->createContact();
+ $this->index->method('addDocuments')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $service->indexContact($contact);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testRemoveContactSuccess(): void
+ {
+ $this->service->removeContact(1);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testRemoveContactThrows(): void
+ {
+ $this->index->method('deleteDocument')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $service->removeContact(1);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testSearchContactsSuccess(): void
+ {
+ $searchResult = $this->createStub(SearchResult::class);
+ $searchResult->method('getHits')->willReturn([['id' => 1, 'fullName' => 'Alice Martin']]);
+ $this->index->method('search')->willReturn($searchResult);
+
+ $results = $this->service->searchContacts('Alice');
+
+ $this->assertCount(1, $results);
+ }
+
+ public function testSearchContactsThrows(): void
+ {
+ $this->index->method('search')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $results = $service->searchContacts('test');
+ $this->assertSame([], $results);
+ }
+
+ // --- indexDomain / removeDomain / searchDomains ---
+
+ private function createDomain(): Domain
+ {
+ $user = new User();
+ $user->setEmail('d@test.com');
+ $user->setFirstName('Bob');
+ $user->setLastName('Doe');
+ $user->setPassword('hashed');
+ $customer = new Customer($user);
+
+ return new Domain($customer, 'example.fr');
+ }
+
+ public function testIndexDomainSuccess(): void
+ {
+ $domain = $this->createDomain();
+ $this->service->indexDomain($domain);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testIndexDomainThrows(): void
+ {
+ $domain = $this->createDomain();
+ $this->index->method('addDocuments')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $service->indexDomain($domain);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testRemoveDomainSuccess(): void
+ {
+ $this->service->removeDomain(1);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testRemoveDomainThrows(): void
+ {
+ $this->index->method('deleteDocument')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $service->removeDomain(1);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testSearchDomainsSuccess(): void
+ {
+ $searchResult = $this->createStub(SearchResult::class);
+ $searchResult->method('getHits')->willReturn([['id' => 1, 'fqdn' => 'example.fr']]);
+ $this->index->method('search')->willReturn($searchResult);
+
+ $results = $this->service->searchDomains('example');
+
+ $this->assertCount(1, $results);
+ }
+
+ public function testSearchDomainsThrows(): void
+ {
+ $this->index->method('search')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $results = $service->searchDomains('test');
+ $this->assertSame([], $results);
+ }
+
+ // --- indexWebsite / removeWebsite / searchWebsites ---
+
+ private function createWebsite(): Website
+ {
+ $user = new User();
+ $user->setEmail('w@test.com');
+ $user->setFirstName('Carl');
+ $user->setLastName('Smith');
+ $user->setPassword('hashed');
+ $customer = new Customer($user);
+
+ return new Website($customer, 'Mon Site', Website::TYPE_VITRINE);
+ }
+
+ public function testIndexWebsiteSuccess(): void
+ {
+ $website = $this->createWebsite();
+ $this->service->indexWebsite($website);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testIndexWebsiteThrows(): void
+ {
+ $website = $this->createWebsite();
+ $this->index->method('addDocuments')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $service->indexWebsite($website);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testRemoveWebsiteSuccess(): void
+ {
+ $this->service->removeWebsite(1);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testRemoveWebsiteThrows(): void
+ {
+ $this->index->method('deleteDocument')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $service->removeWebsite(1);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testSearchWebsitesSuccess(): void
+ {
+ $searchResult = $this->createStub(SearchResult::class);
+ $searchResult->method('getHits')->willReturn([['id' => 1, 'name' => 'Mon Site']]);
+ $this->index->method('search')->willReturn($searchResult);
+
+ $results = $this->service->searchWebsites('site');
+
+ $this->assertCount(1, $results);
+ }
+
+ public function testSearchWebsitesThrows(): void
+ {
+ $this->index->method('search')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $results = $service->searchWebsites('test');
+ $this->assertSame([], $results);
+ }
+
+ // --- indexDevis / removeDevis / searchDevis ---
+
+ private function createDevis(): Devis
+ {
+ $orderNumber = new OrderNumber('04/2026-00001');
+
+ return new Devis($orderNumber, 'secret');
+ }
+
+ public function testIndexDevisSuccess(): void
+ {
+ $devis = $this->createDevis();
+ $this->service->indexDevis($devis);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testIndexDevisThrows(): void
+ {
+ $devis = $this->createDevis();
+ $this->index->method('addDocuments')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $service->indexDevis($devis);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testRemoveDevisSuccess(): void
+ {
+ $this->service->removeDevis(1);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testRemoveDevisThrows(): void
+ {
+ $this->index->method('deleteDocument')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $service->removeDevis(1);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testSearchDevisSuccess(): void
+ {
+ $searchResult = $this->createStub(SearchResult::class);
+ $searchResult->method('getHits')->willReturn([['id' => 1, 'numOrder' => '04/2026-00001']]);
+ $this->index->method('search')->willReturn($searchResult);
+
+ $results = $this->service->searchDevis('04/2026');
+
+ $this->assertCount(1, $results);
+ }
+
+ public function testSearchDevisWithCustomerFilter(): void
+ {
+ $searchResult = $this->createStub(SearchResult::class);
+ $searchResult->method('getHits')->willReturn([]);
+ $this->index->method('search')->willReturn($searchResult);
+
+ $results = $this->service->searchDevis('test', 20, 42);
+
+ $this->assertSame([], $results);
+ }
+
+ public function testSearchDevisThrows(): void
+ {
+ $this->index->method('search')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $results = $service->searchDevis('test');
+ $this->assertSame([], $results);
+ }
+
+ // --- indexAdvert / removeAdvert / searchAdverts ---
+
+ private function createAdvert(): Advert
+ {
+ $orderNumber = new OrderNumber('04/2026-00002');
+
+ return new Advert($orderNumber, 'secret');
+ }
+
+ public function testIndexAdvertSuccess(): void
+ {
+ $advert = $this->createAdvert();
+ $this->service->indexAdvert($advert);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testIndexAdvertThrows(): void
+ {
+ $advert = $this->createAdvert();
+ $this->index->method('addDocuments')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $service->indexAdvert($advert);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testRemoveAdvertSuccess(): void
+ {
+ $this->service->removeAdvert(1);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testRemoveAdvertThrows(): void
+ {
+ $this->index->method('deleteDocument')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $service->removeAdvert(1);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testSearchAdvertsSuccess(): void
+ {
+ $searchResult = $this->createStub(SearchResult::class);
+ $searchResult->method('getHits')->willReturn([['id' => 1, 'numOrder' => '04/2026-00002']]);
+ $this->index->method('search')->willReturn($searchResult);
+
+ $results = $this->service->searchAdverts('04/2026');
+
+ $this->assertCount(1, $results);
+ }
+
+ public function testSearchAdvertsWithCustomerFilter(): void
+ {
+ $searchResult = $this->createStub(SearchResult::class);
+ $searchResult->method('getHits')->willReturn([]);
+ $this->index->method('search')->willReturn($searchResult);
+
+ $results = $this->service->searchAdverts('test', 20, 10);
+
+ $this->assertSame([], $results);
+ }
+
+ public function testSearchAdvertsThrows(): void
+ {
+ $this->index->method('search')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $results = $service->searchAdverts('test');
+ $this->assertSame([], $results);
+ }
+
+ // --- indexFacture / removeFacture / searchFactures ---
+
+ private function createFacture(): Facture
+ {
+ $orderNumber = new OrderNumber('04/2026-00003');
+
+ return new Facture($orderNumber, 'secret');
+ }
+
+ public function testIndexFactureSuccess(): void
+ {
+ $facture = $this->createFacture();
+ $this->service->indexFacture($facture);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testIndexFactureThrows(): void
+ {
+ $facture = $this->createFacture();
+ $this->index->method('addDocuments')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $service->indexFacture($facture);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testRemoveFactureSuccess(): void
+ {
+ $this->service->removeFacture(1);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testRemoveFactureThrows(): void
+ {
+ $this->index->method('deleteDocument')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $service->removeFacture(1);
+ $this->addToAssertionCount(1);
+ }
+
+ public function testSearchFacturesSuccess(): void
+ {
+ $searchResult = $this->createStub(SearchResult::class);
+ $searchResult->method('getHits')->willReturn([['id' => 1, 'invoiceNumber' => 'F-2026-001']]);
+ $this->index->method('search')->willReturn($searchResult);
+
+ $results = $this->service->searchFactures('2026');
+
+ $this->assertCount(1, $results);
+ }
+
+ public function testSearchFacturesWithCustomerFilter(): void
+ {
+ $searchResult = $this->createStub(SearchResult::class);
+ $searchResult->method('getHits')->willReturn([]);
+ $this->index->method('search')->willReturn($searchResult);
+
+ $results = $this->service->searchFactures('test', 20, 5);
+
+ $this->assertSame([], $results);
+ }
+
+ public function testSearchFacturesThrows(): void
+ {
+ $this->index->method('search')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $results = $service->searchFactures('test');
+ $this->assertSame([], $results);
+ }
+
+ // --- purgeAllIndexes ---
+
+ public function testPurgeAllIndexesSuccess(): void
+ {
+ $this->service->purgeAllIndexes();
+ $this->addToAssertionCount(1);
+ }
+
+ public function testPurgeAllIndexesThrows(): void
+ {
+ $this->index->method('deleteAllDocuments')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $service->purgeAllIndexes();
+ $this->addToAssertionCount(1);
+ }
+
+ // --- getIndexCount ---
+
+ public function testGetIndexCountSuccess(): void
+ {
+ $this->index->method('stats')->willReturn(['numberOfDocuments' => 42]);
+
+ $count = $this->service->getIndexCount('customer');
+
+ $this->assertSame(42, $count);
+ }
+
+ public function testGetIndexCountMissingKey(): void
+ {
+ $this->index->method('stats')->willReturn([]);
+
+ $count = $this->service->getIndexCount('customer');
+
+ $this->assertSame(0, $count);
+ }
+
+ public function testGetIndexCountThrows(): void
+ {
+ $this->index->method('stats')->willThrowException(new \RuntimeException('fail'));
+
+ $logger = $this->createStub(LoggerInterface::class);
+ $service = new MeilisearchService($logger, 'http://localhost:7700', 'fake-key');
+ $ref = new \ReflectionProperty(MeilisearchService::class, 'client');
+ $ref->setValue($service, $this->client);
+
+ $count = $service->getIndexCount('customer');
+ $this->assertSame(0, $count);
+ }
}
diff --git a/tests/Service/OrderNumberServiceTest.php b/tests/Service/OrderNumberServiceTest.php
index 759be6d..253f406 100644
--- a/tests/Service/OrderNumberServiceTest.php
+++ b/tests/Service/OrderNumberServiceTest.php
@@ -101,4 +101,38 @@ class OrderNumberServiceTest extends TestCase
$this->assertSame($now->format('m/Y').'-00010', $result);
}
+
+ public function testPreviewReturnsUnusedOrderNumber(): void
+ {
+ $now = new \DateTimeImmutable();
+ // An existing unused order number
+ $unused = new OrderNumber($now->format('m/Y').'-00005');
+ // unused query returns it; lastOrder query also returns it (only first call matters)
+ $repo = $this->createStub(OrderNumberRepository::class);
+ $repo->method('createQueryBuilder')->willReturn($this->createQueryBuilder($unused));
+
+ $em = $this->createStub(EntityManagerInterface::class);
+
+ $service = new OrderNumberService($repo, $em);
+ $result = $service->preview();
+
+ // When unused order exists, preview returns its numOrder
+ $this->assertSame($now->format('m/Y').'-00005', $result);
+ }
+
+ public function testGenerateReturnsUnusedOrderNumberWhenExists(): void
+ {
+ $now = new \DateTimeImmutable();
+ $unused = new OrderNumber($now->format('m/Y').'-00003');
+
+ $repo = $this->createStub(OrderNumberRepository::class);
+ $repo->method('createQueryBuilder')->willReturn($this->createQueryBuilder($unused));
+
+ $em = $this->createStub(EntityManagerInterface::class);
+
+ $service = new OrderNumberService($repo, $em);
+ $result = $service->generate();
+
+ $this->assertSame($unused, $result);
+ }
}
diff --git a/tests/Service/Pdf/ComptaPdfTest.php b/tests/Service/Pdf/ComptaPdfTest.php
index 5a9dbe8..d5efdca 100644
--- a/tests/Service/Pdf/ComptaPdfTest.php
+++ b/tests/Service/Pdf/ComptaPdfTest.php
@@ -171,4 +171,62 @@ class ComptaPdfTest extends TestCase
$this->assertStringStartsWith('%PDF', $output);
}
+
+ public function testGenerateWithMontantHtColumn(): void
+ {
+ // Covers the 'MontantHT' totals path in writeSummary (grand livre / balance)
+ $pdf = $this->makePdf('Grand Livre');
+ $pdf->setData([
+ ['MontantHT' => '100.00', 'MontantTVA' => '20.00', 'MontantTTC' => '120.00', 'EcritureLib' => 'Test'],
+ ]);
+ $pdf->generate();
+
+ $output = $pdf->Output('S');
+
+ $this->assertStringStartsWith('%PDF', $output);
+ }
+
+ public function testGenerateWithAllNumericColumns(): void
+ {
+ // Covers isNumericColumn for: Solde, MontantDevise, Montantdevise, JoursRetard
+ $pdf = $this->makePdf('Balance');
+ $pdf->setData([
+ [
+ 'Debit' => '500.00',
+ 'Credit' => '200.00',
+ 'Solde' => '300.00',
+ 'MontantDevise' => '300.00',
+ 'Montantdevise' => '300.00',
+ 'JoursRetard' => '5',
+ 'EcritureLib' => 'Test',
+ ],
+ ]);
+ $pdf->generate();
+
+ $output = $pdf->Output('S');
+
+ $this->assertStringStartsWith('%PDF', $output);
+ }
+
+ public function testGenerateSignatureBlockNearPageBottom(): void
+ {
+ // Creates many rows so the signature block needs a new page
+ $rows = [];
+ for ($i = 1; $i <= 60; ++$i) {
+ $rows[] = [
+ 'JournalCode' => 'VTE',
+ 'EcritureLib' => 'Ligne '.$i,
+ 'Debit' => '0.00',
+ 'Credit' => '0.00',
+ ];
+ }
+
+ $pdf = $this->makePdf('Grand Livre');
+ $pdf->setData($rows);
+ $pdf->generate();
+
+ $output = $pdf->Output('S');
+
+ $this->assertStringStartsWith('%PDF', $output);
+ }
}
diff --git a/tests/Service/Pdf/FacturePdfTest.php b/tests/Service/Pdf/FacturePdfTest.php
index 2b8f876..cbebd53 100644
--- a/tests/Service/Pdf/FacturePdfTest.php
+++ b/tests/Service/Pdf/FacturePdfTest.php
@@ -227,4 +227,117 @@ class FacturePdfTest extends TestCase
$this->assertStringStartsWith('%PDF', $output);
}
+
+ public function testGenerateWithQrCodeProducesValidPdf(): void
+ {
+ $facture = $this->makeFacture('04/2026-00010');
+ $facture->setTotalHt('50.00');
+ $facture->setTotalTva('0.00');
+ $facture->setTotalTtc('50.00');
+
+ $urlGenerator = $this->createStub(\Symfony\Component\Routing\Generator\UrlGeneratorInterface::class);
+ $urlGenerator->method('generate')->willReturn('https://crm.e-cosplay.fr/facture/verify/1/abc123');
+
+ $line = new FactureLine($facture, 'Service QR', '50.00', 1);
+ $facture->getLines()->add($line);
+
+ $pdf = new FacturePdf($this->kernel, $facture, $urlGenerator);
+ $pdf->generate();
+
+ $output = $pdf->Output('S');
+
+ $this->assertStringStartsWith('%PDF', $output);
+ }
+
+ public function testGenerateWithRibFileProducesValidPdf(): void
+ {
+ // Create a minimal valid PDF as the RIB file
+ $ribDir = $this->projectDir . '/public';
+ if (!is_dir($ribDir)) {
+ mkdir($ribDir, 0775, true);
+ }
+
+ // Create a minimal PDF file for RIB using FPDI itself
+ $miniPdf = new \setasign\Fpdi\Fpdi();
+ $miniPdf->AddPage();
+ $ribPath = $ribDir . '/rib.pdf';
+ $miniPdf->Output('F', $ribPath);
+
+ $facture = $this->makeFacture('04/2026-00011');
+ $facture->setTotalHt('75.00');
+ $facture->setTotalTva('0.00');
+ $facture->setTotalTtc('75.00');
+
+ $line = new FactureLine($facture, 'Service RIB', '75.00', 1);
+ $facture->getLines()->add($line);
+
+ $pdf = new FacturePdf($this->kernel, $facture);
+ $pdf->generate();
+
+ $output = $pdf->Output('S');
+
+ $this->assertStringStartsWith('%PDF', $output);
+ }
+
+ public function testGenerateWithTwigAppendsCgvProducesValidPdf(): void
+ {
+ $facture = $this->makeFacture('04/2026-00012');
+ $facture->setTotalHt('80.00');
+ $facture->setTotalTva('0.00');
+ $facture->setTotalTtc('80.00');
+
+ $line = new FactureLine($facture, 'Service Twig', '80.00', 1);
+ $facture->getLines()->add($line);
+
+ // Create a mock Twig environment that returns minimal HTML for CGV
+ $twig = $this->createStub(\Twig\Environment::class);
+ $twig->method('render')->willReturn('Conditions Generales de Vente
');
+
+ $pdf = new FacturePdf($this->kernel, $facture, null, $twig);
+ $pdf->generate();
+
+ $output = $pdf->Output('S');
+
+ $this->assertStringStartsWith('%PDF', $output);
+ }
+
+ public function testGenerateWithCustomerNoAddress(): void
+ {
+ $facture = $this->makeFacture('04/2026-00013');
+
+ $customer = $this->createStub(\App\Entity\Customer::class);
+ $customer->method('getFullName')->willReturn('Jean Dupont');
+ $customer->method('getRaisonSociale')->willReturn(null);
+ $customer->method('getEmail')->willReturn('jean@example.com');
+ $customer->method('getAddress')->willReturn(null); // No address
+ $customer->method('getAddress2')->willReturn(null);
+ $customer->method('getZipCode')->willReturn(null);
+ $customer->method('getCity')->willReturn(null);
+ $facture->setCustomer($customer);
+
+ $pdf = new FacturePdf($this->kernel, $facture);
+ $pdf->generate();
+
+ $output = $pdf->Output('S');
+
+ $this->assertStringStartsWith('%PDF', $output);
+ }
+
+ public function testFooterOnCgvPageSkipsFooter(): void
+ {
+ // Test that skipHeaderFooter=true makes Footer skip on pages after the last facture page
+ $facture = $this->makeFacture('04/2026-00014');
+ $facture->setTotalHt('30.00');
+ $facture->setTotalTva('0.00');
+ $facture->setTotalTtc('30.00');
+
+ $twig = $this->createStub(\Twig\Environment::class);
+ $twig->method('render')->willReturn('CGV page 1
');
+
+ $pdf = new FacturePdf($this->kernel, $facture, null, $twig);
+ $pdf->generate();
+
+ $output = $pdf->Output('S');
+ $this->assertStringStartsWith('%PDF', $output);
+ }
}
diff --git a/tests/Service/RgpdServiceTest.php b/tests/Service/RgpdServiceTest.php
index 58ecfb7..40bfadc 100644
--- a/tests/Service/RgpdServiceTest.php
+++ b/tests/Service/RgpdServiceTest.php
@@ -169,12 +169,125 @@ class RgpdServiceTest extends TestCase
$repository = $this->createStub(EntityRepository::class);
$repository->method('findBy')->willReturn([]);
-
+
$em = $this->createStub(EntityManagerInterface::class);
$em->method('getRepository')->willReturn($repository);
-
+
$service = new RgpdService($em, $this->twig, $this->docuSealService, $this->mailer, $this->urlGenerator, $this->projectDir, 's');
$result = $service->handleAccessRequest($ip, $email);
$this->assertFalse($result['found']);
}
+
+ // --- sendVerificationCode ---
+
+ public function testSendVerificationCodeCallsMailer(): void
+ {
+ $this->twig->method('render')->willReturn('Code: 123456');
+
+ $mailer = $this->createMock(MailerService::class);
+ $mailer->expects($this->once())->method('sendEmail');
+
+ $service = new RgpdService($this->em, $this->twig, $this->docuSealService, $mailer, $this->urlGenerator, $this->projectDir, 'secret');
+ $service->sendVerificationCode('test@example.com', '127.0.0.1', 'access');
+
+ // Code file should be created
+ $codesDir = $this->projectDir . '/var/rgpd/codes';
+ $this->assertDirectoryExists($codesDir);
+ }
+
+ public function testSendVerificationCodeForDeletion(): void
+ {
+ $this->twig->method('render')->willReturn('Code: 654321');
+
+ $mailer = $this->createMock(MailerService::class);
+ $mailer->expects($this->once())->method('sendEmail');
+
+ $service = new RgpdService($this->em, $this->twig, $this->docuSealService, $mailer, $this->urlGenerator, $this->projectDir, 'secret');
+ $service->sendVerificationCode('test@example.com', '127.0.0.1', 'deletion');
+
+ $this->addToAssertionCount(1);
+ }
+
+ // --- verifyCode ---
+
+ public function testVerifyCodeReturnsFalseIfNoFile(): void
+ {
+ $service = new RgpdService($this->em, $this->twig, $this->docuSealService, $this->mailer, $this->urlGenerator, $this->projectDir, 'secret');
+ $result = $service->verifyCode('test@example.com', '127.0.0.1', 'access', '123456');
+
+ $this->assertFalse($result);
+ }
+
+ public function testVerifyCodeReturnsTrueForCorrectCode(): void
+ {
+ $this->twig->method('render')->willReturn('ok');
+
+ $service = new RgpdService($this->em, $this->twig, $this->docuSealService, $this->mailer, $this->urlGenerator, $this->projectDir, 'secret');
+
+ // Send a code to create the file; we'll intercept the actual code from the file
+ $service->sendVerificationCode('verify@example.com', '10.0.0.1', 'access');
+
+ // Read the created code file to get the actual code
+ $codesDir = $this->projectDir . '/var/rgpd/codes';
+ $files = glob($codesDir . '/*.json');
+ $this->assertNotEmpty($files);
+
+ $data = json_decode(file_get_contents($files[0]), true);
+ $code = $data['code'];
+
+ $result = $service->verifyCode('verify@example.com', '10.0.0.1', 'access', $code);
+
+ $this->assertTrue($result);
+ }
+
+ public function testVerifyCodeReturnsFalseForWrongCode(): void
+ {
+ $this->twig->method('render')->willReturn('ok');
+
+ $service = new RgpdService($this->em, $this->twig, $this->docuSealService, $this->mailer, $this->urlGenerator, $this->projectDir, 'secret');
+ $service->sendVerificationCode('wrong@example.com', '10.0.0.2', 'access');
+
+ $result = $service->verifyCode('wrong@example.com', '10.0.0.2', 'access', '000000');
+
+ $this->assertFalse($result);
+ }
+
+ public function testVerifyCodeReturnsFalseIfExpired(): void
+ {
+ $codesDir = $this->projectDir . '/var/rgpd/codes';
+ if (!is_dir($codesDir)) {
+ mkdir($codesDir, 0755, true);
+ }
+
+ // Write an already-expired code file
+ $codeHash = hash('sha256', 'expired@example.com|127.0.0.1|access|secret');
+ $filePath = $codesDir . '/' . $codeHash . '.json';
+ file_put_contents($filePath, json_encode([
+ 'code' => '999999',
+ 'hash' => 'ignored',
+ 'expires' => time() - 1, // expired 1 second ago
+ ]));
+
+ $service = new RgpdService($this->em, $this->twig, $this->docuSealService, $this->mailer, $this->urlGenerator, $this->projectDir, 'secret');
+ $result = $service->verifyCode('expired@example.com', '127.0.0.1', 'access', '999999');
+
+ $this->assertFalse($result);
+ }
+
+ public function testVerifyCodeReturnsFalseForInvalidJson(): void
+ {
+ $codesDir = $this->projectDir . '/var/rgpd/codes';
+ if (!is_dir($codesDir)) {
+ mkdir($codesDir, 0755, true);
+ }
+
+ $codeHash = hash('sha256', 'bad@example.com|127.0.0.1|access|secret');
+ $filePath = $codesDir . '/' . $codeHash . '.json';
+ file_put_contents($filePath, 'not valid json');
+
+ $service = new RgpdService($this->em, $this->twig, $this->docuSealService, $this->mailer, $this->urlGenerator, $this->projectDir, 'secret');
+ $result = $service->verifyCode('bad@example.com', '127.0.0.1', 'access', '000000');
+
+ $this->assertFalse($result);
+ }
}
diff --git a/tests/Service/TarificationServiceTest.php b/tests/Service/TarificationServiceTest.php
index ca1c2f9..0a6cc40 100644
--- a/tests/Service/TarificationServiceTest.php
+++ b/tests/Service/TarificationServiceTest.php
@@ -156,4 +156,28 @@ class TarificationServiceTest extends TestCase
$this->assertArrayHasKey('esite_business', $types);
$this->assertArrayHasKey('title', $types['esite_business']);
}
+
+ public function testEnsureDefaultPricesStripeErrorWithLogger(): void
+ {
+ $price = new PriceAutomatic();
+ $price->setType('ndd_depot');
+ $price->setTitle('T');
+ $price->setPriceHt('1.00');
+
+ $repo = $this->createStub(PriceAutomaticRepository::class);
+ $repo->method('findAll')->willReturnOnConsecutiveCalls([], [$price]);
+
+ $em = $this->createStub(EntityManagerInterface::class);
+
+ $logger = $this->createMock(\Psr\Log\LoggerInterface::class);
+ $logger->expects($this->atLeastOnce())->method('error');
+
+ $stripe = $this->createStub(StripePriceService::class);
+ $stripe->method('syncPrice')->willThrowException(new \RuntimeException('Stripe error'));
+
+ $service = new TarificationService($repo, $em, $logger, null, $stripe);
+ $created = $service->ensureDefaultPrices();
+
+ $this->assertCount(19, $created);
+ }
}