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>
This commit is contained in:
Serreau Jovann
2026-04-08 16:43:52 +02:00
parent 9b871c88d3
commit 00b7e7cdbf
6 changed files with 1461 additions and 2 deletions

View File

@@ -0,0 +1,519 @@
<?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());
}
}

View File

@@ -0,0 +1,822 @@
<?php
namespace App\Tests\Controller;
use App\Controller\OrderPaymentController;
use App\Entity\Advert;
use App\Entity\Customer;
use App\Entity\OrderNumber;
use App\Entity\Revendeur;
use App\Entity\User;
use App\Service\MailerService;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
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\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\RouterInterface;
use Twig\Environment;
class OrderPaymentControllerTest extends TestCase
{
// ---------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------
private function createAdvert(?Customer $customer = null, string $state = Advert::STATE_SEND): Advert
{
$orderNumber = new OrderNumber('04/2026-TEST01');
$advert = new Advert($orderNumber, 'secret');
$advert->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('<html></html>');
$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('<html></html>');
$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('<html></html>');
$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('<html></html>');
$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('<html></html>');
$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('<html></html>');
$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('<html></html>');
$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('<html></html>');
$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('<html></html>');
$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('<html></html>');
$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');
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Tests\Controller;
use App\Controller\UnsubscribeController;
use App\Service\UnsubscribeManager;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\Routing\RouterInterface;
use Twig\Environment;
class UnsubscribeControllerTest extends TestCase
{
private function buildController(): UnsubscribeController
{
$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],
]);
$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());
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Tests\Controller;
use App\Controller\WebmailController;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\Routing\RouterInterface;
use Twig\Environment;
class WebmailControllerTest extends TestCase
{
private function buildController(): WebmailController
{
$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],
]);
$controller = new WebmailController();
$controller->setContainer($container);
return $controller;
}
public function testLoginRendersWebmailLoginTemplate(): void
{
$controller = $this->buildController();
$response = $controller->login();
$this->assertSame(200, $response->getStatusCode());
}
}