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>
520 lines
19 KiB
PHP
520 lines
19 KiB
PHP
<?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());
|
||
}
|
||
}
|