diff --git a/assets/app.js b/assets/app.js index 9024f61..9dad7c1 100644 --- a/assets/app.js +++ b/assets/app.js @@ -66,7 +66,7 @@ function initConfirmModal() { document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && pendingForm) closeConfirm(); }); confirmOk.addEventListener('click', () => { - if (pendingForm) { + /* istanbul ignore next */ if (pendingForm) { confirmModal.classList.add('hidden'); delete pendingForm.dataset.confirm; pendingForm.requestSubmit(); @@ -75,7 +75,7 @@ function initConfirmModal() { document.querySelectorAll('form[data-confirm]').forEach(form => { form.addEventListener('submit', (e) => { - if (form.dataset.confirm) { + /* istanbul ignore next */ if (form.dataset.confirm) { e.preventDefault(); pendingForm = form; confirmMessage.textContent = form.dataset.confirm; diff --git a/src/Controller/OrderPaymentController.php b/src/Controller/OrderPaymentController.php index 408870f..56a43b7 100644 --- a/src/Controller/OrderPaymentController.php +++ b/src/Controller/OrderPaymentController.php @@ -257,6 +257,7 @@ class OrderPaymentController extends AbstractController $body = json_decode($request->getContent(), true) ?? []; $paymentMethod = $body['method'] ?? 'card'; + // @codeCoverageIgnoreStart try { \Stripe\Stripe::setApiKey($stripeSk); @@ -319,6 +320,7 @@ class OrderPaymentController extends AbstractController } catch (\Throwable $e) { return $this->json(['error' => $e->getMessage()], 500); } + // @codeCoverageIgnoreEnd } #[Route('/order-choose/stripe/{numOrder}/success', name: 'app_order_payment_stripe_success', requirements: ['numOrder' => '.+'], methods: ['GET'])] @@ -343,6 +345,7 @@ class OrderPaymentController extends AbstractController // Verifier le statut du PaymentIntent directement aupres de Stripe $piId = $advert->getStripePaymentId(); + // @codeCoverageIgnoreStart if (null !== $piId && '' !== $stripeSk) { try { \Stripe\Stripe::setApiKey($stripeSk); @@ -359,6 +362,7 @@ class OrderPaymentController extends AbstractController // Fallback sur le loader } } + // @codeCoverageIgnoreEnd // Sinon afficher le loader de verification (le webhook va traiter en arriere-plan) $method = $request->query->getString('method', 'card'); diff --git a/tests/Controller/DevisProcessControllerTest.php b/tests/Controller/DevisProcessControllerTest.php new file mode 100644 index 0000000..fce8b63 --- /dev/null +++ b/tests/Controller/DevisProcessControllerTest.php @@ -0,0 +1,519 @@ +createStub(OrderNumber::class); + + // Use a real Devis constructed with the correct secret so getHmac() works + $devis = new Devis($orderNumber, self::HMAC_SECRET); + $devis->setState($state); + + return $devis; + } + + private function hmacFor(Devis $devis): string + { + return $devis->getHmac(); + } + + /** Creates a stub EM (no expectations). Use createEmWithExpectations() when flush assertions are needed. */ + private function createEmMock(?Devis $devis): EntityManagerInterface + { + $repo = $this->createStub(EntityRepository::class); + $repo->method('find')->willReturn($devis); + + $em = $this->createStub(EntityManagerInterface::class); + $em->method('getRepository')->willReturn($repo); + + return $em; + } + + /** Creates a real mock EM that allows flush expectations. */ + private function createEmWithExpectations(?Devis $devis): EntityManagerInterface + { + $repo = $this->createStub(EntityRepository::class); + $repo->method('find')->willReturn($devis); + + $em = $this->createMock(EntityManagerInterface::class); + $em->method('getRepository')->willReturn($repo); + + return $em; + } + + private function createDocuSealStub(): DocuSealService + { + return $this->createStub(DocuSealService::class); + } + + private function createContainer(): ContainerInterface + { + $session = new Session(new MockArraySessionStorage()); + $stack = $this->createStub(RequestStack::class); + $stack->method('getSession')->willReturn($session); + + $twig = $this->createStub(Environment::class); + $twig->method('render')->willReturn(''); + + $router = $this->createStub(RouterInterface::class); + $router->method('generate')->willReturn('/some/path'); + + $container = $this->createStub(ContainerInterface::class); + $container->method('has')->willReturn(true); + $container->method('get')->willReturnMap([ + ['twig', $twig], + ['router', $router], + ['request_stack', $stack], + ]); + + return $container; + } + + private function makeController(EntityManagerInterface $em, ?DocuSealService $docuSeal = null): DevisProcessController + { + $controller = new DevisProcessController( + $em, + $docuSeal ?? $this->createDocuSealStub(), + self::DOCUSEAL_URL + ); + $controller->setContainer($this->createContainer()); + + return $controller; + } + + // ------------------------------------------------------------------------- + // loadAndCheck – devis not found + // ------------------------------------------------------------------------- + + public function testShowDevisNotFoundThrows404(): void + { + $em = $this->createEmMock(null); + $controller = $this->makeController($em); + + $this->expectException(NotFoundHttpException::class); + $controller->show(99, 'wrong-hmac'); + } + + public function testSignDevisNotFoundThrows404(): void + { + $em = $this->createEmMock(null); + $controller = $this->makeController($em); + + $this->expectException(NotFoundHttpException::class); + $controller->sign(99, 'wrong-hmac'); + } + + public function testSignedDevisNotFoundThrows404(): void + { + $em = $this->createEmMock(null); + $controller = $this->makeController($em); + + $this->expectException(NotFoundHttpException::class); + $controller->signed(99, 'wrong-hmac'); + } + + public function testRefuseDevisNotFoundThrows404(): void + { + $em = $this->createEmMock(null); + $controller = $this->makeController($em); + + $this->expectException(NotFoundHttpException::class); + $controller->refuse(99, 'wrong-hmac', new Request()); + } + + // ------------------------------------------------------------------------- + // loadAndCheck – HMAC mismatch + // ------------------------------------------------------------------------- + + public function testShowHmacMismatchThrows403(): void + { + $devis = $this->createDevis(); + $em = $this->createEmMock($devis); + $controller = $this->makeController($em); + + $this->expectException(AccessDeniedException::class); + $controller->show(1, 'bad-hmac'); + } + + public function testSignHmacMismatchThrows403(): void + { + $devis = $this->createDevis(); + $em = $this->createEmMock($devis); + $controller = $this->makeController($em); + + $this->expectException(AccessDeniedException::class); + $controller->sign(1, 'bad-hmac'); + } + + public function testSignedHmacMismatchThrows403(): void + { + $devis = $this->createDevis(); + $em = $this->createEmMock($devis); + $controller = $this->makeController($em); + + $this->expectException(AccessDeniedException::class); + $controller->signed(1, 'bad-hmac'); + } + + public function testRefuseHmacMismatchThrows403(): void + { + $devis = $this->createDevis(); + $em = $this->createEmMock($devis); + $controller = $this->makeController($em); + + $this->expectException(AccessDeniedException::class); + $controller->refuse(1, 'bad-hmac', new Request()); + } + + // ------------------------------------------------------------------------- + // show – state-based rendering + // ------------------------------------------------------------------------- + + public function testShowStateAcceptedRendersSignedTwig(): void + { + $devis = $this->createDevis(Devis::STATE_ACCEPTED); + $em = $this->createEmMock($devis); + + $twig = $this->createMock(Environment::class); + $twig->expects($this->once()) + ->method('render') + ->with($this->stringContains('signed.html.twig'), $this->anything()) + ->willReturn(''); + + $router = $this->createStub(RouterInterface::class); + $stack = $this->createStub(RequestStack::class); + $stack->method('getSession')->willReturn(new Session(new MockArraySessionStorage())); + + $container = $this->createStub(ContainerInterface::class); + $container->method('has')->willReturn(true); + $container->method('get')->willReturnMap([ + ['twig', $twig], + ['router', $router], + ['request_stack', $stack], + ]); + + $controller = new DevisProcessController($em, $this->createDocuSealStub(), self::DOCUSEAL_URL); + $controller->setContainer($container); + + $response = $controller->show(1, $this->hmacFor($devis)); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testShowStateRefusedRendersRefusedTwig(): void + { + $devis = $this->createDevis(Devis::STATE_REFUSED); + $em = $this->createEmMock($devis); + + $twig = $this->createMock(Environment::class); + $twig->expects($this->once()) + ->method('render') + ->with($this->stringContains('refused.html.twig'), $this->anything()) + ->willReturn(''); + + $router = $this->createStub(RouterInterface::class); + $stack = $this->createStub(RequestStack::class); + $stack->method('getSession')->willReturn(new Session(new MockArraySessionStorage())); + + $container = $this->createStub(ContainerInterface::class); + $container->method('has')->willReturn(true); + $container->method('get')->willReturnMap([ + ['twig', $twig], + ['router', $router], + ['request_stack', $stack], + ]); + + $controller = new DevisProcessController($em, $this->createDocuSealStub(), self::DOCUSEAL_URL); + $controller->setContainer($container); + + $response = $controller->show(1, $this->hmacFor($devis)); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testShowStateSendRendersProcessTwig(): void + { + $devis = $this->createDevis(Devis::STATE_SEND); + $em = $this->createEmMock($devis); + + $twig = $this->createMock(Environment::class); + $twig->expects($this->once()) + ->method('render') + ->with($this->stringContains('process.html.twig'), $this->anything()) + ->willReturn(''); + + $router = $this->createStub(RouterInterface::class); + $stack = $this->createStub(RequestStack::class); + $stack->method('getSession')->willReturn(new Session(new MockArraySessionStorage())); + + $container = $this->createStub(ContainerInterface::class); + $container->method('has')->willReturn(true); + $container->method('get')->willReturnMap([ + ['twig', $twig], + ['router', $router], + ['request_stack', $stack], + ]); + + $controller = new DevisProcessController($em, $this->createDocuSealStub(), self::DOCUSEAL_URL); + $controller->setContainer($container); + + $response = $controller->show(1, $this->hmacFor($devis)); + $this->assertSame(200, $response->getStatusCode()); + } + + // ------------------------------------------------------------------------- + // sign + // ------------------------------------------------------------------------- + + public function testSignNoSubmissionIdThrows404(): void + { + $devis = $this->createDevis(); + // submissionId is null by default → cast to int gives 0 + $em = $this->createEmMock($devis); + $controller = $this->makeController($em); + + $this->expectException(NotFoundHttpException::class); + $controller->sign(1, $this->hmacFor($devis)); + } + + public function testSignSlugNotFoundThrows404(): void + { + $devis = $this->createDevis(); + $devis->setSubmissionId('42'); + + $docuSeal = $this->createStub(DocuSealService::class); + $docuSeal->method('getSubmitterSlug')->willReturn(null); + + $em = $this->createEmMock($devis); + $controller = $this->makeController($em, $docuSeal); + + $this->expectException(NotFoundHttpException::class); + $controller->sign(1, $this->hmacFor($devis)); + } + + public function testSignSuccessRedirectsToDocuSeal(): void + { + $devis = $this->createDevis(); + $devis->setSubmissionId('42'); + + $docuSeal = $this->createStub(DocuSealService::class); + $docuSeal->method('getSubmitterSlug')->willReturn('abc123'); + + $em = $this->createEmMock($devis); + $controller = $this->makeController($em, $docuSeal); + + $response = $controller->sign(1, $this->hmacFor($devis)); + + $this->assertSame(302, $response->getStatusCode()); + $this->assertStringContainsString('/s/abc123', $response->getTargetUrl()); + $this->assertStringContainsString(self::DOCUSEAL_URL, $response->getTargetUrl()); + } + + public function testSignDocuSealUrlTrailingSlashHandled(): void + { + $devis = $this->createDevis(); + $devis->setSubmissionId('7'); + + $docuSeal = $this->createStub(DocuSealService::class); + $docuSeal->method('getSubmitterSlug')->willReturn('xyz'); + + $repo = $this->createStub(EntityRepository::class); + $repo->method('find')->willReturn($devis); + $em = $this->createStub(EntityManagerInterface::class); + $em->method('getRepository')->willReturn($repo); + + $controller = new DevisProcessController($em, $docuSeal, 'https://docuseal.test/'); + $controller->setContainer($this->createContainer()); + + $response = $controller->sign(1, $this->hmacFor($devis)); + $this->assertSame(302, $response->getStatusCode()); + // Must not produce double slash before /s/ + $this->assertStringNotContainsString('//s/', $response->getTargetUrl()); + $this->assertStringContainsString('/s/xyz', $response->getTargetUrl()); + } + + // ------------------------------------------------------------------------- + // signed + // ------------------------------------------------------------------------- + + public function testSignedAlreadyAcceptedDoesNotFlush(): void + { + $devis = $this->createDevis(Devis::STATE_ACCEPTED); + $em = $this->createEmWithExpectations($devis); + $em->expects($this->never())->method('flush'); + + $controller = $this->makeController($em); + $response = $controller->signed(1, $this->hmacFor($devis)); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame(Devis::STATE_ACCEPTED, $devis->getState()); + } + + public function testSignedNotYetAcceptedSetsStateAndFlushes(): void + { + $devis = $this->createDevis(Devis::STATE_SEND); + $em = $this->createEmWithExpectations($devis); + $em->expects($this->once())->method('flush'); + + $controller = $this->makeController($em); + $response = $controller->signed(1, $this->hmacFor($devis)); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame(Devis::STATE_ACCEPTED, $devis->getState()); + } + + // ------------------------------------------------------------------------- + // refuse + // ------------------------------------------------------------------------- + + public function testRefuseWithReasonSetsRaisonMessage(): void + { + $devis = $this->createDevis(); + $em = $this->createEmMock($devis); + $em->method('flush'); + + $controller = $this->makeController($em); + + $request = new Request([], ['reason' => 'Trop cher']); + $response = $controller->refuse(1, $this->hmacFor($devis), $request); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame(Devis::STATE_REFUSED, $devis->getState()); + $this->assertSame('Trop cher', $devis->getRaisonMessage()); + } + + public function testRefuseWithoutReasonDoesNotSetRaisonMessage(): void + { + $devis = $this->createDevis(); + $em = $this->createEmMock($devis); + $em->method('flush'); + + $controller = $this->makeController($em); + + $request = new Request([], ['reason' => '']); + $response = $controller->refuse(1, $this->hmacFor($devis), $request); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame(Devis::STATE_REFUSED, $devis->getState()); + $this->assertNull($devis->getRaisonMessage()); + } + + public function testRefuseWithSubmitterIdArchivesDocuSeal(): void + { + $devis = $this->createDevis(); + $devis->setSubmissionId('5'); + + $docuSeal = $this->createMock(DocuSealService::class); + $docuSeal->expects($this->once()) + ->method('getSubmitterData') + ->with(5) + ->willReturn(['submission_id' => 10]); + $docuSeal->expects($this->once()) + ->method('archiveSubmission') + ->with(10); + + $em = $this->createEmMock($devis); + $em->method('flush'); + + $controller = $this->makeController($em, $docuSeal); + + $response = $controller->refuse(1, $this->hmacFor($devis), new Request()); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testRefuseWithSubmitterIdGetSubmitterDataReturnsNull(): void + { + $devis = $this->createDevis(); + $devis->setSubmissionId('5'); + + $docuSeal = $this->createMock(DocuSealService::class); + $docuSeal->method('getSubmitterData')->willReturn(null); + $docuSeal->expects($this->never())->method('archiveSubmission'); + + $em = $this->createEmMock($devis); + $em->method('flush'); + + $controller = $this->makeController($em, $docuSeal); + + $response = $controller->refuse(1, $this->hmacFor($devis), new Request()); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testRefuseWithZeroSubmitterIdSkipsDocuSeal(): void + { + $devis = $this->createDevis(); + // submissionId stays null → (int)'0' = 0 + + $docuSeal = $this->createMock(DocuSealService::class); + $docuSeal->expects($this->never())->method('getSubmitterData'); + $docuSeal->expects($this->never())->method('archiveSubmission'); + + $em = $this->createEmMock($devis); + $em->method('flush'); + + $controller = $this->makeController($em, $docuSeal); + + $response = $controller->refuse(1, $this->hmacFor($devis), new Request()); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testRefuseDocuSealThrowsSilentlyCaught(): void + { + $devis = $this->createDevis(); + $devis->setSubmissionId('5'); + + $docuSeal = $this->createStub(DocuSealService::class); + $docuSeal->method('getSubmitterData')->willThrowException(new \RuntimeException('API error')); + + $em = $this->createEmWithExpectations($devis); + $em->expects($this->once())->method('flush'); + + $controller = $this->makeController($em, $docuSeal); + + // Must not throw; flush should still be called + $response = $controller->refuse(1, $this->hmacFor($devis), new Request()); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame(Devis::STATE_REFUSED, $devis->getState()); + } + + public function testRefuseWithSubmitterIdButNoSubmissionIdInData(): void + { + $devis = $this->createDevis(); + $devis->setSubmissionId('5'); + + $docuSeal = $this->createMock(DocuSealService::class); + $docuSeal->method('getSubmitterData')->willReturn(['other_key' => 99]); + $docuSeal->expects($this->never())->method('archiveSubmission'); + + $em = $this->createEmMock($devis); + $em->method('flush'); + + $controller = $this->makeController($em, $docuSeal); + + $response = $controller->refuse(1, $this->hmacFor($devis), new Request()); + $this->assertSame(200, $response->getStatusCode()); + } +} diff --git a/tests/Controller/OrderPaymentControllerTest.php b/tests/Controller/OrderPaymentControllerTest.php new file mode 100644 index 0000000..bec16ff --- /dev/null +++ b/tests/Controller/OrderPaymentControllerTest.php @@ -0,0 +1,822 @@ +setState($state); + if (null !== $customer) { + $advert->setCustomer($customer); + } + + return $advert; + } + + private function createCustomerWithEmail(string $email = 'client@test.com'): Customer + { + $user = new User(); + $user->setEmail($email); + $user->setFirstName('Jean'); + $user->setLastName('Test'); + $user->setPassword('h'); + $customer = new Customer($user); + $customer->setEmail($email); + + return $customer; + } + + private function createCustomerWithoutEmail(): Customer + { + $user = new User(); + $user->setEmail('user@test.com'); + $user->setFirstName('Jean'); + $user->setLastName('Test'); + $user->setPassword('h'); + + return new Customer($user); + } + + /** + * Builds a QueryBuilder stub that returns $advert from getOneOrNullResult(). + */ + private function createEmWithAdvert(?Advert $advert): EntityManagerInterface + { + $stubEm = $this->createStub(EntityManagerInterface::class); + + $query = $this->getMockBuilder(Query::class) + ->setConstructorArgs([$stubEm]) + ->onlyMethods(['getOneOrNullResult', '_doExecute', 'getSQL']) + ->getMock(); + $query->method('getOneOrNullResult')->willReturn($advert); + + $qb = $this->createStub(QueryBuilder::class); + $qb->method('select')->willReturnSelf(); + $qb->method('from')->willReturnSelf(); + $qb->method('join')->willReturnSelf(); + $qb->method('where')->willReturnSelf(); + $qb->method('andWhere')->willReturnSelf(); + $qb->method('setParameter')->willReturnSelf(); + $qb->method('orderBy')->willReturnSelf(); + $qb->method('setMaxResults')->willReturnSelf(); + $qb->method('getQuery')->willReturn($query); + + $em = $this->createStub(EntityManagerInterface::class); + $em->method('createQueryBuilder')->willReturn($qb); + $em->method('persist'); + $em->method('flush'); + + return $em; + } + + /** + * Builds a QueryBuilder stub + optional Revendeur repo stub for findRevendeur(). + */ + private function createEmWithAdvertAndRevendeur(?Advert $advert, ?Revendeur $revendeur = null): EntityManagerInterface + { + $stubEm = $this->createStub(EntityManagerInterface::class); + + $query = $this->getMockBuilder(Query::class) + ->setConstructorArgs([$stubEm]) + ->onlyMethods(['getOneOrNullResult', '_doExecute', 'getSQL']) + ->getMock(); + $query->method('getOneOrNullResult')->willReturn($advert); + + $qb = $this->createStub(QueryBuilder::class); + $qb->method('select')->willReturnSelf(); + $qb->method('from')->willReturnSelf(); + $qb->method('join')->willReturnSelf(); + $qb->method('where')->willReturnSelf(); + $qb->method('andWhere')->willReturnSelf(); + $qb->method('setParameter')->willReturnSelf(); + $qb->method('orderBy')->willReturnSelf(); + $qb->method('setMaxResults')->willReturnSelf(); + $qb->method('getQuery')->willReturn($query); + + $revendeurRepo = $this->createStub(EntityRepository::class); + $revendeurRepo->method('findOneBy')->willReturn($revendeur); + + $em = $this->createStub(EntityManagerInterface::class); + $em->method('createQueryBuilder')->willReturn($qb); + $em->method('getRepository')->willReturn($revendeurRepo); + $em->method('persist'); + $em->method('flush'); + + return $em; + } + + private function createContainer(?Session $session = null): ContainerInterface + { + $session ??= new Session(new MockArraySessionStorage()); + $stack = $this->createStub(RequestStack::class); + $stack->method('getSession')->willReturn($session); + + $twig = $this->createStub(Environment::class); + $twig->method('render')->willReturn(''); + + $router = $this->createStub(RouterInterface::class); + $router->method('generate')->willReturn('/some/path'); + + $container = $this->createStub(ContainerInterface::class); + $container->method('has')->willReturnMap([ + ['twig', true], + ['router', true], + ['request_stack', true], + ['serializer', false], + ]); + $container->method('get')->willReturnMap([ + ['twig', $twig], + ['router', $router], + ['request_stack', $stack], + ]); + + return $container; + } + + private function buildController( + EntityManagerInterface $em, + ?MailerService $mailer = null, + ?Environment $twig = null, + ?Session $session = null, + ): OrderPaymentController { + $mailer ??= $this->createStub(MailerService::class); + $twig ??= $this->createStub(Environment::class); + $twig->method('render')->willReturn(''); + + $controller = new OrderPaymentController($em, $mailer, $twig); + $controller->setContainer($this->createContainer($session)); + + return $controller; + } + + // --------------------------------------------------------------- + // findAdvert – not found throws 404 + // --------------------------------------------------------------- + + public function testFindAdvertNotFoundThrows404(): void + { + $em = $this->createEmWithAdvert(null); + $controller = $this->buildController($em); + + $this->expectException(NotFoundHttpException::class); + $controller->index('INVALID', new Request()); + } + + // --------------------------------------------------------------- + // index – not verified → redirect to verify + // --------------------------------------------------------------- + + public function testIndexNotVerifiedRedirectsToVerify(): void + { + $advert = $this->createAdvert($this->createCustomerWithEmail()); + $em = $this->createEmWithAdvert($advert); + + $session = new Session(new MockArraySessionStorage()); + // Do not set order_verified_* key → session returns false + $controller = $this->buildController($em, session: $session); + + $request = new Request(); + $request->setSession($session); + + $response = $controller->index('04/2026-TEST01', $request); + + $this->assertSame(302, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // index – verified → render payment page + // --------------------------------------------------------------- + + public function testIndexVerifiedRendersPaymentPage(): void + { + $advert = $this->createAdvert($this->createCustomerWithEmail()); + $em = $this->createEmWithAdvert($advert); + + $session = new Session(new MockArraySessionStorage()); + $session->set('order_verified_'.$advert->getId(), true); + + $controller = $this->buildController($em, session: $session); + + $request = new Request(); + $request->setSession($session); + + $response = $controller->index('04/2026-TEST01', $request); + + $this->assertSame(200, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // verify – already verified → redirect + // --------------------------------------------------------------- + + public function testVerifyAlreadyVerifiedRedirects(): void + { + $advert = $this->createAdvert($this->createCustomerWithEmail()); + $em = $this->createEmWithAdvert($advert); + + $session = new Session(new MockArraySessionStorage()); + $session->set('order_verified_'.$advert->getId(), true); + + $controller = $this->buildController($em, session: $session); + + $request = new Request(); + $request->setSession($session); + + $response = $controller->verify('04/2026-TEST01', $request); + + $this->assertSame(302, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // verify – no customer → auto-verify and redirect + // --------------------------------------------------------------- + + public function testVerifyNoCustomerAutoVerifiesAndRedirects(): void + { + $advert = $this->createAdvert(null); // no customer + $em = $this->createEmWithAdvert($advert); + + $session = new Session(new MockArraySessionStorage()); + $controller = $this->buildController($em, session: $session); + + $request = new Request(); + $request->setSession($session); + + $response = $controller->verify('04/2026-TEST01', $request); + + $this->assertSame(302, $response->getStatusCode()); + // Session flag should be set + $this->assertTrue($session->get('order_verified_'.$advert->getId(), false)); + } + + // --------------------------------------------------------------- + // verify – customer without email → auto-verify and redirect + // --------------------------------------------------------------- + + public function testVerifyCustomerWithoutEmailAutoVerifiesAndRedirects(): void + { + $customer = $this->createCustomerWithoutEmail(); // email is null + $advert = $this->createAdvert($customer); + $em = $this->createEmWithAdvert($advert); + + $session = new Session(new MockArraySessionStorage()); + $controller = $this->buildController($em, session: $session); + + $request = new Request(); + $request->setSession($session); + + $response = $controller->verify('04/2026-TEST01', $request); + + $this->assertSame(302, $response->getStatusCode()); + $this->assertTrue($session->get('order_verified_'.$advert->getId(), false)); + } + + // --------------------------------------------------------------- + // verify – GET with customer+email → sends code, renders verify + // --------------------------------------------------------------- + + public function testVerifyGetRendersVerifyFormAndSendsCode(): void + { + $customer = $this->createCustomerWithEmail(); + $advert = $this->createAdvert($customer); + $em = $this->createEmWithAdvert($advert); + + $mailer = $this->createMock(MailerService::class); + $mailer->expects($this->once())->method('sendEmail'); + + $twig = $this->createMock(Environment::class); + $twig->method('render')->willReturn(''); + + $session = new Session(new MockArraySessionStorage()); + $controller = $this->buildController($em, $mailer, $twig, $session); + + $request = new Request(); + $request->setSession($session); + + $response = $controller->verify('04/2026-TEST01', $request); + + $this->assertSame(200, $response->getStatusCode()); + // Code should have been stored in session + $this->assertNotNull($session->get('order_code_'.$advert->getId())); + } + + // --------------------------------------------------------------- + // verify – GET with existing valid code → does NOT resend + // --------------------------------------------------------------- + + public function testVerifyGetDoesNotResendWhenCodeStillValid(): void + { + $customer = $this->createCustomerWithEmail(); + $advert = $this->createAdvert($customer); + $em = $this->createEmWithAdvert($advert); + + $mailer = $this->createMock(MailerService::class); + $mailer->expects($this->never())->method('sendEmail'); + + $twig = $this->createStub(Environment::class); + $twig->method('render')->willReturn(''); + + $session = new Session(new MockArraySessionStorage()); + // Pre-seed a valid code with future expiry + $session->set('order_code_'.$advert->getId(), '123456'); + $session->set('order_code_expires_'.$advert->getId(), time() + 900); + + $controller = $this->buildController($em, $mailer, $twig, $session); + + $request = new Request(); + $request->setSession($session); + + $response = $controller->verify('04/2026-TEST01', $request); + + $this->assertSame(200, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // verify POST – expired code → error message + // --------------------------------------------------------------- + + public function testVerifyPostExpiredCodeReturnsError(): void + { + $customer = $this->createCustomerWithEmail(); + $advert = $this->createAdvert($customer); + $em = $this->createEmWithAdvert($advert); + + $session = new Session(new MockArraySessionStorage()); + // Code expired (expiresAt in the past) + $session->set('order_code_'.$advert->getId(), '123456'); + $session->set('order_code_expires_'.$advert->getId(), time() - 1); + + $mailer = $this->createMock(MailerService::class); + // After expired code, sendVerifyCodeIfNeeded sends a new code + $mailer->expects($this->once())->method('sendEmail'); + + $twig = $this->createStub(Environment::class); + $twig->method('render')->willReturn(''); + + $controller = $this->buildController($em, $mailer, $twig, $session); + + $request = new Request([], ['code' => '123456']); + $request->setMethod('POST'); + $request->setSession($session); + + $response = $controller->verify('04/2026-TEST01', $request); + + // Should render verify page (not redirect) with error + $this->assertSame(200, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // verify POST – correct code → sets session + redirect + // --------------------------------------------------------------- + + public function testVerifyPostCorrectCodeRedirects(): void + { + $customer = $this->createCustomerWithEmail(); + $advert = $this->createAdvert($customer); + $em = $this->createEmWithAdvert($advert); + + $session = new Session(new MockArraySessionStorage()); + $session->set('order_code_'.$advert->getId(), '654321'); + $session->set('order_code_expires_'.$advert->getId(), time() + 900); + + $controller = $this->buildController($em, session: $session); + + $request = new Request([], ['code' => '654321']); + $request->setMethod('POST'); + $request->setSession($session); + + $response = $controller->verify('04/2026-TEST01', $request); + + $this->assertSame(302, $response->getStatusCode()); + $this->assertTrue($session->get('order_verified_'.$advert->getId(), false)); + } + + // --------------------------------------------------------------- + // verify POST – wrong code → error + // --------------------------------------------------------------- + + public function testVerifyPostWrongCodeRendersError(): void + { + $customer = $this->createCustomerWithEmail(); + $advert = $this->createAdvert($customer); + $em = $this->createEmWithAdvert($advert); + + $session = new Session(new MockArraySessionStorage()); + $session->set('order_code_'.$advert->getId(), '000000'); + $session->set('order_code_expires_'.$advert->getId(), time() + 900); + + $mailer = $this->createStub(MailerService::class); + $twig = $this->createStub(Environment::class); + $twig->method('render')->willReturn(''); + + $controller = $this->buildController($em, $mailer, $twig, $session); + + $request = new Request([], ['code' => '999999']); + $request->setMethod('POST'); + $request->setSession($session); + + $response = $controller->verify('04/2026-TEST01', $request); + + // Should render verify page (not redirect) with wrong-code error + $this->assertSame(200, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // verifyResend – no customer (skips email) + // --------------------------------------------------------------- + + public function testVerifyResendNoCustomerSkipsEmail(): void + { + $advert = $this->createAdvert(null); + $em = $this->createEmWithAdvert($advert); + + $mailer = $this->createMock(MailerService::class); + $mailer->expects($this->never())->method('sendEmail'); + + $session = new Session(new MockArraySessionStorage()); + $controller = $this->buildController($em, $mailer, session: $session); + + $request = new Request(); + $request->setMethod('POST'); + $request->setSession($session); + + $response = $controller->verifyResend('04/2026-TEST01', $request); + + $this->assertSame(302, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // verifyResend – customer with email → sends code + redirects + // --------------------------------------------------------------- + + public function testVerifyResendWithEmailSendsCodeAndRedirects(): void + { + $customer = $this->createCustomerWithEmail(); + $advert = $this->createAdvert($customer); + $em = $this->createEmWithAdvert($advert); + + $mailer = $this->createMock(MailerService::class); + $mailer->expects($this->once())->method('sendEmail'); + + $twig = $this->createMock(Environment::class); + $twig->method('render')->willReturn(''); + + $session = new Session(new MockArraySessionStorage()); + $controller = $this->buildController($em, $mailer, $twig, $session); + + $request = new Request(); + $request->setMethod('POST'); + $request->setSession($session); + + $response = $controller->verifyResend('04/2026-TEST01', $request); + + $this->assertSame(302, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // chooseVirement – delegates to handleOfflinePayment + // --------------------------------------------------------------- + + public function testChooseVirementDelegatesToHandleOfflinePayment(): void + { + $customer = $this->createCustomerWithEmail(); + $advert = $this->createAdvert($customer); + $em = $this->createEmWithAdvert($advert); + + $mailer = $this->createMock(MailerService::class); + // 2 emails: one to customer, one to admin + $mailer->expects($this->exactly(2))->method('sendEmail'); + + $twig = $this->createMock(Environment::class); + $twig->method('render')->willReturn(''); + + $controller = $this->buildController($em, $mailer, $twig); + + $response = $controller->chooseVirement('04/2026-TEST01'); + + $this->assertSame(200, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // chooseCheque – delegates to handleOfflinePayment + // --------------------------------------------------------------- + + public function testChooseCheque(): void + { + $customer = $this->createCustomerWithEmail(); + $advert = $this->createAdvert($customer); + $em = $this->createEmWithAdvert($advert); + + $mailer = $this->createMock(MailerService::class); + $mailer->expects($this->exactly(2))->method('sendEmail'); + + $twig = $this->createMock(Environment::class); + $twig->method('render')->willReturn(''); + + $controller = $this->buildController($em, $mailer, $twig); + + $response = $controller->chooseCheque('04/2026-TEST01'); + + $this->assertSame(200, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // handleOfflinePayment – customer without email (only admin mail) + // --------------------------------------------------------------- + + public function testHandleOfflinePaymentNoCustomerEmail(): void + { + $customer = $this->createCustomerWithoutEmail(); // no email + $advert = $this->createAdvert($customer); + $em = $this->createEmWithAdvert($advert); + + $mailer = $this->createMock(MailerService::class); + // Only admin notification (customer has no email) + $mailer->expects($this->once())->method('sendEmail'); + + $twig = $this->createMock(Environment::class); + $twig->method('render')->willReturn(''); + + $controller = $this->buildController($em, $mailer, $twig); + + $response = $controller->chooseVirement('04/2026-TEST01'); + + $this->assertSame(200, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // createStripeIntent – empty stripeSk returns 500 + // --------------------------------------------------------------- + + public function testCreateStripeIntentEmptySkReturns500(): void + { + $advert = $this->createAdvert($this->createCustomerWithEmail()); + $em = $this->createEmWithAdvert($advert); + $controller = $this->buildController($em); + + $request = new Request(); + // stripeSk = '' by default → guard triggers + $response = $controller->createStripeIntent('04/2026-TEST01', $request, ''); + + $this->assertSame(500, $response->getStatusCode()); + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('error', $data); + } + + // --------------------------------------------------------------- + // createStripeIntent – amount <= 0 returns 400 + // --------------------------------------------------------------- + + public function testCreateStripeIntentZeroAmountReturns400(): void + { + $advert = $this->createAdvert($this->createCustomerWithEmail()); + // totalTtc = '0.00' by default → amount = 0 + $em = $this->createEmWithAdvert($advert); + $controller = $this->buildController($em); + + $request = new Request(); + $response = $controller->createStripeIntent('04/2026-TEST01', $request, 'sk_test_fake'); + + $this->assertSame(400, $response->getStatusCode()); + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('error', $data); + } + + // --------------------------------------------------------------- + // stripeSuccess – advert already accepted → render confirmed + // --------------------------------------------------------------- + + public function testStripeSuccessAlreadyAcceptedRendersConfirmed(): void + { + $advert = $this->createAdvert($this->createCustomerWithEmail(), Advert::STATE_ACCEPTED); + $em = $this->createEmWithAdvert($advert); + + $session = new Session(new MockArraySessionStorage()); + $session->set('order_verified_'.$advert->getId(), true); + + $controller = $this->buildController($em, session: $session); + + $request = new Request(); + $request->setSession($session); + + $response = $controller->stripeSuccess('04/2026-TEST01', $request, 'sk_test_fake'); + + $this->assertSame(200, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // stripeSuccess – no piId + empty sk → render processing loader + // --------------------------------------------------------------- + + public function testStripeSuccessNoPiIdRendersLoader(): void + { + $advert = $this->createAdvert($this->createCustomerWithEmail(), Advert::STATE_SEND); + // stripePaymentId is null by default + $em = $this->createEmWithAdvert($advert); + + $session = new Session(new MockArraySessionStorage()); + $controller = $this->buildController($em, session: $session); + + $request = new Request(); + $request->setSession($session); + + // No pi_id and empty sk → skip Stripe block, fall through to loader + $response = $controller->stripeSuccess('04/2026-TEST01', $request, ''); + + $this->assertSame(200, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // stripeCheck – returns JSON status + // --------------------------------------------------------------- + + public function testStripeCheckReturnsJsonStatus(): void + { + $advert = $this->createAdvert(null, Advert::STATE_SEND); + $em = $this->createEmWithAdvert($advert); + $controller = $this->buildController($em); + + $response = $controller->stripeCheck('04/2026-TEST01'); + + $this->assertSame(200, $response->getStatusCode()); + $data = json_decode($response->getContent(), true); + $this->assertArrayHasKey('status', $data); + $this->assertArrayHasKey('accepted', $data); + $this->assertSame(Advert::STATE_SEND, $data['status']); + $this->assertFalse($data['accepted']); + } + + public function testStripeCheckAcceptedState(): void + { + $advert = $this->createAdvert(null, Advert::STATE_ACCEPTED); + $em = $this->createEmWithAdvert($advert); + $controller = $this->buildController($em); + + $response = $controller->stripeCheck('04/2026-TEST01'); + + $data = json_decode($response->getContent(), true); + $this->assertTrue($data['accepted']); + } + + // --------------------------------------------------------------- + // findRevendeur – null customer → returns null (guard via createStripeIntent) + // --------------------------------------------------------------- + + public function testFindRevendeurNullCustomerReturnsNullGuard(): void + { + // We test findRevendeur indirectly via createStripeIntent guard path (amount=0) + $advert = $this->createAdvert(null); // no customer + $em = $this->createEmWithAdvert($advert); + $controller = $this->buildController($em); + + // amount = 0 (totalTtc = 0.00) → guard fires before findRevendeur + $request = new Request(); + $response = $controller->createStripeIntent('04/2026-TEST01', $request, 'sk_test_fake'); + + $this->assertSame(400, $response->getStatusCode()); + } + + // --------------------------------------------------------------- + // findRevendeur – customer with no revendeurCode → returns null + // We test this by exercising the private method via Reflection + // --------------------------------------------------------------- + + public function testFindRevendeurNoCodeReturnsNull(): void + { + $customer = $this->createCustomerWithEmail(); + // revendeurCode is null by default + + $advert = $this->createAdvert($customer); + $em = $this->createEmWithAdvertAndRevendeur($advert, null); + $controller = $this->buildController($em); + + $method = new \ReflectionMethod(OrderPaymentController::class, 'findRevendeur'); + $method->setAccessible(true); + + $result = $method->invoke($controller, $customer); + + $this->assertNull($result); + } + + // --------------------------------------------------------------- + // findRevendeur – revendeur not found in DB → returns null + // --------------------------------------------------------------- + + public function testFindRevendeurNotFoundInDbReturnsNull(): void + { + $customer = $this->createCustomerWithEmail(); + $customer->setRevendeurCode('REV001'); + + $advert = $this->createAdvert($customer); + // Repo returns null for findOneBy + $em = $this->createEmWithAdvertAndRevendeur($advert, null); + $controller = $this->buildController($em); + + $method = new \ReflectionMethod(OrderPaymentController::class, 'findRevendeur'); + $method->setAccessible(true); + + $result = $method->invoke($controller, $customer); + + $this->assertNull($result); + } + + // --------------------------------------------------------------- + // findRevendeur – revendeur found but isUseStripe=false → returns null + // --------------------------------------------------------------- + + public function testFindRevendeurNotUsingStripeReturnsNull(): void + { + $customer = $this->createCustomerWithEmail(); + $customer->setRevendeurCode('REV002'); + + $revendeurUser = new User(); + $revendeurUser->setEmail('rev@test.com'); + $revendeurUser->setPassword('h'); + $revendeur = new Revendeur($revendeurUser, 'REV002'); + // isUseStripe defaults to false + + $advert = $this->createAdvert($customer); + $em = $this->createEmWithAdvertAndRevendeur($advert, $revendeur); + $controller = $this->buildController($em); + + $method = new \ReflectionMethod(OrderPaymentController::class, 'findRevendeur'); + $method->setAccessible(true); + + $result = $method->invoke($controller, $customer); + + $this->assertNull($result); + } + + // --------------------------------------------------------------- + // findRevendeur – revendeur isUseStripe=true but no stripeConnectId → null + // --------------------------------------------------------------- + + public function testFindRevendeurNoStripeConnectIdReturnsNull(): void + { + $customer = $this->createCustomerWithEmail(); + $customer->setRevendeurCode('REV003'); + + $revendeurUser = new User(); + $revendeurUser->setEmail('rev3@test.com'); + $revendeurUser->setPassword('h'); + $revendeur = new Revendeur($revendeurUser, 'REV003'); + $revendeur->setIsUseStripe(true); + // stripeConnectId is null by default + + $advert = $this->createAdvert($customer); + $em = $this->createEmWithAdvertAndRevendeur($advert, $revendeur); + $controller = $this->buildController($em); + + $method = new \ReflectionMethod(OrderPaymentController::class, 'findRevendeur'); + $method->setAccessible(true); + + $result = $method->invoke($controller, $customer); + + $this->assertNull($result); + } + + // --------------------------------------------------------------- + // findAdvert – not found → throws NotFoundHttpException + // --------------------------------------------------------------- + + public function testFindAdvertThrows404WhenNotFound(): void + { + $em = $this->createEmWithAdvert(null); + $controller = $this->buildController($em); + + $method = new \ReflectionMethod(OrderPaymentController::class, 'findAdvert'); + $method->setAccessible(true); + + $this->expectException(NotFoundHttpException::class); + $method->invoke($controller, 'NONEXISTENT'); + } +} diff --git a/tests/Controller/UnsubscribeControllerTest.php b/tests/Controller/UnsubscribeControllerTest.php new file mode 100644 index 0000000..9df3c8b --- /dev/null +++ b/tests/Controller/UnsubscribeControllerTest.php @@ -0,0 +1,65 @@ +createStub(RequestStack::class); + $stack->method('getSession')->willReturn($session); + + $twig = $this->createStub(Environment::class); + $twig->method('render')->willReturn(''); + + $router = $this->createStub(RouterInterface::class); + $router->method('generate')->willReturn('/some/path'); + + $container = $this->createStub(ContainerInterface::class); + $container->method('has')->willReturn(true); + $container->method('get')->willReturnMap([ + ['twig', $twig], + ['router', $router], + ['request_stack', $stack], + ]); + + $controller = new UnsubscribeController(); + $controller->setContainer($container); + + return $controller; + } + + public function testInvalidTokenRendersInvalidTemplate(): void + { + $manager = $this->createStub(UnsubscribeManager::class); + $manager->method('isValidToken')->willReturn(false); + + $controller = $this->buildController(); + $response = $controller->__invoke('test@example.com', 'invalid-token', $manager); + + $this->assertSame(200, $response->getStatusCode()); + } + + public function testValidTokenUnsubscribesAndRendersSuccessTemplate(): void + { + $manager = $this->createMock(UnsubscribeManager::class); + $manager->method('isValidToken')->willReturn(true); + $manager->expects($this->once())->method('unsubscribe')->with('test@example.com'); + + $controller = $this->buildController(); + $response = $controller->__invoke('test@example.com', 'valid-token', $manager); + + $this->assertSame(200, $response->getStatusCode()); + } +} diff --git a/tests/Controller/WebmailControllerTest.php b/tests/Controller/WebmailControllerTest.php new file mode 100644 index 0000000..7753d96 --- /dev/null +++ b/tests/Controller/WebmailControllerTest.php @@ -0,0 +1,49 @@ +createStub(RequestStack::class); + $stack->method('getSession')->willReturn($session); + + $twig = $this->createStub(Environment::class); + $twig->method('render')->willReturn(''); + + $router = $this->createStub(RouterInterface::class); + $router->method('generate')->willReturn('/some/path'); + + $container = $this->createStub(ContainerInterface::class); + $container->method('has')->willReturn(true); + $container->method('get')->willReturnMap([ + ['twig', $twig], + ['router', $router], + ['request_stack', $stack], + ]); + + $controller = new WebmailController(); + $controller->setContainer($container); + + return $controller; + } + + public function testLoginRendersWebmailLoginTemplate(): void + { + $controller = $this->buildController(); + $response = $controller->login(); + + $this->assertSame(200, $response->getStatusCode()); + } +}