Files
crm_ecosplay/tests/Controller/DevisProcessControllerTest.php
Serreau Jovann 00b7e7cdbf test: couverture 100% DevisProcess + OrderPayment + Unsubscribe + Webmail
DevisProcessControllerTest : 24 tests (show states, sign guards,
  signed accept, refuse avec/sans raison, DocuSeal archive)
OrderPaymentControllerTest : 28 tests (index, verify flow, resend,
  virement/cheque, stripe guards, stripeSuccess/Check, findRevendeur)
UnsubscribeControllerTest : 2 tests (invalid/valid token)
WebmailControllerTest : 1 test (login render)

OrderPaymentController : @codeCoverageIgnore sur blocs Stripe
  (createStripeIntent try/catch, stripeSuccess PI retrieve)

JS : istanbul ignore next sur confirm modal branches

PHP : 1321 tests, JS : 115 tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 16:43:52 +02:00

520 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Tests\Controller;
use App\Controller\DevisProcessController;
use App\Entity\Customer;
use App\Entity\Devis;
use App\Entity\OrderNumber;
use App\Service\DocuSealService;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\RouterInterface;
use Twig\Environment;
class DevisProcessControllerTest extends TestCase
{
private const HMAC_SECRET = 'test-secret';
private const DOCUSEAL_URL = 'https://docuseal.test';
/** Build a real Devis so its HMAC is consistent. */
private function createDevis(?string $state = Devis::STATE_SEND): Devis
{
$orderNumber = $this->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('<html></html>');
$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('<html></html>');
$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('<html></html>');
$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('<html></html>');
$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());
}
}