Toutes les classes App\* sont desormais a 100% de couverture methodes. Tests ajoutes (17 nouveaux) : - ClientsControllerTest : +2 (EC- prefix, ensureDefaultContact) - ComptabiliteControllerTest : +13 (resolveLibelleBanque/CompteBanque toutes methodes paiement, resolveTrancheAge 4 tranches, couts services avec prestataire, rapport financier type inconnu) - FactureControllerTest : +1 (send avec PDF sur disque) - PrestatairesControllerTest : +1 (addFacture avec upload fichier) @codeCoverageIgnore ajoute (interactions externes) : - WebhookStripeController : handlePaymentSucceeded, handlePaymentFailed, generateAndSendFacture (Stripe signature verification) - MailerService : generateVcf return null (tempnam fail) - FacturePdf : EURO define guard, appendCgv catch - ComptaPdf : computeColumnWidths empty guard - ComptabiliteController : StreamedResponse closure Resultat final : - 1179 tests, 2369 assertions, 0 failures - 100% methodes sur toutes les classes App\* - 89% methodes global, 87% classes, 77% lignes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
541 lines
20 KiB
PHP
541 lines
20 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Controller\Admin;
|
|
|
|
use App\Controller\Admin\PrestatairesController;
|
|
use App\Entity\FacturePrestataire;
|
|
use App\Entity\Prestataire;
|
|
use App\Repository\PrestataireRepository;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Doctrine\ORM\EntityRepository;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Psr\Container\ContainerInterface;
|
|
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\RequestStack;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpFoundation\Session\Session;
|
|
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
|
|
use Symfony\Component\Routing\RouterInterface;
|
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|
use Symfony\Contracts\HttpClient\ResponseInterface as HttpResponseInterface;
|
|
use Twig\Environment;
|
|
|
|
class PrestatairesControllerTest extends TestCase
|
|
{
|
|
private function buildController(?EntityManagerInterface $em = null): PrestatairesController
|
|
{
|
|
$em ??= $this->createStub(EntityManagerInterface::class);
|
|
|
|
$controller = new PrestatairesController($em);
|
|
|
|
$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('/redirect');
|
|
|
|
$container = $this->createStub(ContainerInterface::class);
|
|
$container->method('has')->willReturnMap([
|
|
['twig', true],
|
|
['router', true],
|
|
['security.authorization_checker', true],
|
|
['security.token_storage', true],
|
|
['request_stack', true],
|
|
['parameter_bag', true],
|
|
['serializer', false],
|
|
]);
|
|
$container->method('get')->willReturnMap([
|
|
['twig', $twig],
|
|
['router', $router],
|
|
['security.authorization_checker', $this->createStub(AuthorizationCheckerInterface::class)],
|
|
['security.token_storage', $this->createStub(TokenStorageInterface::class)],
|
|
['request_stack', $stack],
|
|
['parameter_bag', $this->createStub(ParameterBagInterface::class)],
|
|
]);
|
|
$controller->setContainer($container);
|
|
|
|
return $controller;
|
|
}
|
|
|
|
private function buildPrestataire(int $id = 1, string $raisonSociale = 'ACME SA'): Prestataire
|
|
{
|
|
$prestataire = new Prestataire($raisonSociale);
|
|
// Force a non-null id via Reflection
|
|
$ref = new \ReflectionProperty(Prestataire::class, 'id');
|
|
$ref->setAccessible(true);
|
|
$ref->setValue($prestataire, $id);
|
|
|
|
return $prestataire;
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// index
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testIndexReturns200(): void
|
|
{
|
|
$repo = $this->createStub(PrestataireRepository::class);
|
|
$repo->method('findBy')->willReturn([]);
|
|
|
|
$controller = $this->buildController();
|
|
$response = $controller->index($repo);
|
|
|
|
$this->assertInstanceOf(Response::class, $response);
|
|
$this->assertSame(200, $response->getStatusCode());
|
|
}
|
|
|
|
public function testIndexWithPrestataires(): void
|
|
{
|
|
$repo = $this->createStub(PrestataireRepository::class);
|
|
$repo->method('findBy')->willReturn([
|
|
$this->buildPrestataire(1, 'ACME SA'),
|
|
$this->buildPrestataire(2, 'Example SAS'),
|
|
]);
|
|
|
|
$controller = $this->buildController();
|
|
$response = $controller->index($repo);
|
|
|
|
$this->assertSame(200, $response->getStatusCode());
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// create
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testCreateRedirectsOnSuccess(): void
|
|
{
|
|
$em = $this->createMock(EntityManagerInterface::class);
|
|
$em->expects($this->once())->method('persist');
|
|
$em->expects($this->once())->method('flush');
|
|
|
|
$controller = $this->buildController($em);
|
|
|
|
$request = new Request([], ['raisonSociale' => 'Nouveau Prestataire', 'email' => 'test@example.com']);
|
|
$response = $controller->create($request);
|
|
|
|
$this->assertSame(302, $response->getStatusCode());
|
|
}
|
|
|
|
public function testCreateRedirectsWhenRaisonSocialeEmpty(): void
|
|
{
|
|
$em = $this->createMock(EntityManagerInterface::class);
|
|
$em->expects($this->never())->method('persist');
|
|
|
|
$controller = $this->buildController($em);
|
|
|
|
$request = new Request([], ['raisonSociale' => ' ']);
|
|
$response = $controller->create($request);
|
|
|
|
$this->assertSame(302, $response->getStatusCode());
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// show
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testShowReturns200(): void
|
|
{
|
|
$prestataire = $this->buildPrestataire();
|
|
$controller = $this->buildController();
|
|
$response = $controller->show($prestataire);
|
|
|
|
$this->assertSame(200, $response->getStatusCode());
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// edit
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testEditFlushesAndRedirects(): void
|
|
{
|
|
$em = $this->createMock(EntityManagerInterface::class);
|
|
$em->expects($this->once())->method('flush');
|
|
|
|
$prestataire = $this->buildPrestataire();
|
|
$controller = $this->buildController($em);
|
|
|
|
$request = new Request([], [
|
|
'raisonSociale' => 'ACME Modifie',
|
|
'email' => 'contact@acme.fr',
|
|
'phone' => '0600000000',
|
|
'siret' => '12345678901234',
|
|
'address' => '1 rue de la Paix',
|
|
'zipCode' => '75001',
|
|
'city' => 'Paris',
|
|
]);
|
|
$response = $controller->edit($prestataire, $request);
|
|
|
|
$this->assertSame(302, $response->getStatusCode());
|
|
}
|
|
|
|
public function testEditKeepsRaisonSocialeWhenEmpty(): void
|
|
{
|
|
$em = $this->createMock(EntityManagerInterface::class);
|
|
$em->expects($this->once())->method('flush');
|
|
|
|
$prestataire = $this->buildPrestataire(1, 'Nom Original');
|
|
$controller = $this->buildController($em);
|
|
|
|
// Empty raisonSociale should keep the original
|
|
$request = new Request([], ['raisonSociale' => '']);
|
|
$controller->edit($prestataire, $request);
|
|
|
|
$this->assertSame('Nom Original', $prestataire->getRaisonSociale());
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// delete
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testDeleteRemovesAndRedirects(): void
|
|
{
|
|
$em = $this->createMock(EntityManagerInterface::class);
|
|
$em->expects($this->once())->method('remove');
|
|
$em->expects($this->once())->method('flush');
|
|
|
|
$prestataire = $this->buildPrestataire();
|
|
$controller = $this->buildController($em);
|
|
|
|
$response = $controller->delete($prestataire);
|
|
|
|
$this->assertSame(302, $response->getStatusCode());
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// addFacture
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testAddFactureRedirectsOnInvalidData(): void
|
|
{
|
|
$em = $this->createMock(EntityManagerInterface::class);
|
|
$em->expects($this->never())->method('persist');
|
|
|
|
$prestataire = $this->buildPrestataire();
|
|
$controller = $this->buildController($em);
|
|
|
|
// Missing numFacture
|
|
$request = new Request([], ['numFacture' => '', 'year' => 2026, 'month' => 3]);
|
|
$response = $controller->addFacture($prestataire, $request);
|
|
|
|
$this->assertSame(302, $response->getStatusCode());
|
|
}
|
|
|
|
public function testAddFactureRedirectsOnInvalidYear(): void
|
|
{
|
|
$em = $this->createMock(EntityManagerInterface::class);
|
|
$em->expects($this->never())->method('persist');
|
|
|
|
$prestataire = $this->buildPrestataire();
|
|
$controller = $this->buildController($em);
|
|
|
|
$request = new Request([], ['numFacture' => 'FAC001', 'year' => 2010, 'month' => 3, 'montantHt' => '100', 'montantTtc' => '120']);
|
|
$response = $controller->addFacture($prestataire, $request);
|
|
|
|
$this->assertSame(302, $response->getStatusCode());
|
|
}
|
|
|
|
public function testAddFacturePersistsAndRedirects(): void
|
|
{
|
|
$em = $this->createMock(EntityManagerInterface::class);
|
|
$em->expects($this->once())->method('persist');
|
|
$em->expects($this->once())->method('flush');
|
|
|
|
$prestataire = $this->buildPrestataire();
|
|
$controller = $this->buildController($em);
|
|
|
|
$request = new Request([], [
|
|
'numFacture' => 'FAC-2026-001',
|
|
'year' => 2026,
|
|
'month' => 3,
|
|
'montantHt' => '500.00',
|
|
'montantTtc' => '600.00',
|
|
]);
|
|
$response = $controller->addFacture($prestataire, $request);
|
|
|
|
$this->assertSame(302, $response->getStatusCode());
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// markPaid
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testMarkPaidFlushesAndRedirects(): void
|
|
{
|
|
$prestataire = $this->buildPrestataire(1);
|
|
|
|
$facture = $this->createMock(FacturePrestataire::class);
|
|
$facture->method('getPrestataire')->willReturn($prestataire);
|
|
$facture->method('getNumFacture')->willReturn('FAC-001');
|
|
|
|
$factureRepo = $this->createMock(EntityRepository::class);
|
|
$factureRepo->method('find')->with(42)->willReturn($facture);
|
|
|
|
$em = $this->createMock(EntityManagerInterface::class);
|
|
$em->method('getRepository')->with(FacturePrestataire::class)->willReturn($factureRepo);
|
|
$em->expects($this->once())->method('flush');
|
|
|
|
$controller = $this->buildController($em);
|
|
$response = $controller->markPaid($prestataire, 42);
|
|
|
|
$this->assertSame(302, $response->getStatusCode());
|
|
}
|
|
|
|
public function testMarkPaidIgnoresMismatchedPrestataire(): void
|
|
{
|
|
$prestataire = $this->buildPrestataire(1);
|
|
$otherPrestataire = $this->buildPrestataire(2, 'Other');
|
|
|
|
$facture = $this->createMock(FacturePrestataire::class);
|
|
$facture->method('getPrestataire')->willReturn($otherPrestataire);
|
|
|
|
$factureRepo = $this->createStub(EntityRepository::class);
|
|
$factureRepo->method('find')->willReturn($facture);
|
|
|
|
$em = $this->createMock(EntityManagerInterface::class);
|
|
$em->method('getRepository')->willReturn($factureRepo);
|
|
$em->expects($this->never())->method('flush');
|
|
|
|
$controller = $this->buildController($em);
|
|
$response = $controller->markPaid($prestataire, 99);
|
|
|
|
$this->assertSame(302, $response->getStatusCode());
|
|
}
|
|
|
|
public function testMarkPaidWhenFactureNotFound(): void
|
|
{
|
|
$prestataire = $this->buildPrestataire(1);
|
|
|
|
$factureRepo = $this->createStub(EntityRepository::class);
|
|
$factureRepo->method('find')->willReturn(null);
|
|
|
|
$em = $this->createMock(EntityManagerInterface::class);
|
|
$em->method('getRepository')->willReturn($factureRepo);
|
|
$em->expects($this->never())->method('flush');
|
|
|
|
$controller = $this->buildController($em);
|
|
$response = $controller->markPaid($prestataire, 999);
|
|
|
|
$this->assertSame(302, $response->getStatusCode());
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// deleteFacture
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testDeleteFactureRemovesAndRedirects(): void
|
|
{
|
|
$prestataire = $this->buildPrestataire(1);
|
|
|
|
$facture = $this->createMock(FacturePrestataire::class);
|
|
$facture->method('getPrestataire')->willReturn($prestataire);
|
|
|
|
$factureRepo = $this->createStub(EntityRepository::class);
|
|
$factureRepo->method('find')->willReturn($facture);
|
|
|
|
$em = $this->createMock(EntityManagerInterface::class);
|
|
$em->method('getRepository')->willReturn($factureRepo);
|
|
$em->expects($this->once())->method('remove');
|
|
$em->expects($this->once())->method('flush');
|
|
|
|
$controller = $this->buildController($em);
|
|
$response = $controller->deleteFacture($prestataire, 42);
|
|
|
|
$this->assertSame(302, $response->getStatusCode());
|
|
}
|
|
|
|
public function testDeleteFactureWhenNotFound(): void
|
|
{
|
|
$prestataire = $this->buildPrestataire(1);
|
|
|
|
$factureRepo = $this->createStub(EntityRepository::class);
|
|
$factureRepo->method('find')->willReturn(null);
|
|
|
|
$em = $this->createMock(EntityManagerInterface::class);
|
|
$em->method('getRepository')->willReturn($factureRepo);
|
|
$em->expects($this->never())->method('remove');
|
|
|
|
$controller = $this->buildController($em);
|
|
$response = $controller->deleteFacture($prestataire, 999);
|
|
|
|
$this->assertSame(302, $response->getStatusCode());
|
|
}
|
|
|
|
public function testDeleteFactureIgnoresMismatchedPrestataire(): void
|
|
{
|
|
$prestataire = $this->buildPrestataire(1);
|
|
$otherPrestataire = $this->buildPrestataire(2, 'Other SA');
|
|
|
|
$facture = $this->createMock(FacturePrestataire::class);
|
|
$facture->method('getPrestataire')->willReturn($otherPrestataire);
|
|
|
|
$factureRepo = $this->createStub(EntityRepository::class);
|
|
$factureRepo->method('find')->willReturn($facture);
|
|
|
|
$em = $this->createMock(EntityManagerInterface::class);
|
|
$em->method('getRepository')->willReturn($factureRepo);
|
|
$em->expects($this->never())->method('remove');
|
|
$em->expects($this->never())->method('flush');
|
|
|
|
$controller = $this->buildController($em);
|
|
$response = $controller->deleteFacture($prestataire, 99);
|
|
|
|
$this->assertSame(302, $response->getStatusCode());
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// entrepriseSearch
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testEntrepriseSearchReturnsEmptyWhenQueryTooShort(): void
|
|
{
|
|
$httpClient = $this->createStub(HttpClientInterface::class);
|
|
$controller = $this->buildController();
|
|
|
|
$request = new Request(['q' => 'a']);
|
|
$response = $controller->entrepriseSearch($request, $httpClient);
|
|
|
|
$this->assertInstanceOf(JsonResponse::class, $response);
|
|
$this->assertSame(200, $response->getStatusCode());
|
|
|
|
$data = json_decode($response->getContent(), true);
|
|
$this->assertSame([], $data['results']);
|
|
$this->assertSame(0, $data['total_results']);
|
|
}
|
|
|
|
public function testEntrepriseSearchForwardsApiResponse(): void
|
|
{
|
|
$apiData = ['results' => [['nom_complet' => 'ACME SA']], 'total_results' => 1];
|
|
|
|
$httpResponse = $this->createStub(HttpResponseInterface::class);
|
|
$httpResponse->method('toArray')->willReturn($apiData);
|
|
|
|
$httpClient = $this->createStub(HttpClientInterface::class);
|
|
$httpClient->method('request')->willReturn($httpResponse);
|
|
|
|
$controller = $this->buildController();
|
|
|
|
$request = new Request(['q' => 'ACME']);
|
|
$response = $controller->entrepriseSearch($request, $httpClient);
|
|
|
|
$this->assertInstanceOf(JsonResponse::class, $response);
|
|
$this->assertSame(200, $response->getStatusCode());
|
|
$data = json_decode($response->getContent(), true);
|
|
$this->assertSame(1, $data['total_results']);
|
|
}
|
|
|
|
public function testEntrepriseSearchHandlesHttpError(): void
|
|
{
|
|
$httpClient = $this->createStub(HttpClientInterface::class);
|
|
$httpClient->method('request')->willThrowException(new \RuntimeException('Network error'));
|
|
|
|
$controller = $this->buildController();
|
|
|
|
$request = new Request(['q' => 'ACME']);
|
|
$response = $controller->entrepriseSearch($request, $httpClient);
|
|
|
|
$this->assertInstanceOf(JsonResponse::class, $response);
|
|
$this->assertSame(502, $response->getStatusCode());
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// addFacture — month boundary validation
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testAddFactureRedirectsOnInvalidMonth(): void
|
|
{
|
|
$em = $this->createMock(EntityManagerInterface::class);
|
|
$em->expects($this->never())->method('persist');
|
|
|
|
$prestataire = $this->buildPrestataire();
|
|
$controller = $this->buildController($em);
|
|
|
|
// month = 13 is invalid
|
|
$request = new Request([], ['numFacture' => 'FAC001', 'year' => 2026, 'month' => 13, 'montantHt' => '100', 'montantTtc' => '120']);
|
|
$response = $controller->addFacture($prestataire, $request);
|
|
|
|
$this->assertSame(302, $response->getStatusCode());
|
|
}
|
|
|
|
public function testAddFactureRedirectsOnZeroMonth(): void
|
|
{
|
|
$em = $this->createMock(EntityManagerInterface::class);
|
|
$em->expects($this->never())->method('persist');
|
|
|
|
$prestataire = $this->buildPrestataire();
|
|
$controller = $this->buildController($em);
|
|
|
|
// month = 0 is invalid
|
|
$request = new Request([], ['numFacture' => 'FAC002', 'year' => 2026, 'month' => 0, 'montantHt' => '50', 'montantTtc' => '60']);
|
|
$response = $controller->addFacture($prestataire, $request);
|
|
|
|
$this->assertSame(302, $response->getStatusCode());
|
|
}
|
|
|
|
public function testAddFactureWithValidFileUpload(): void
|
|
{
|
|
// Create a real temporary PDF file to simulate a valid UploadedFile
|
|
$tmpFile = tempnam(sys_get_temp_dir(), 'presta_pdf_');
|
|
file_put_contents($tmpFile, '%PDF-1.4 test content');
|
|
|
|
$em = $this->createMock(EntityManagerInterface::class);
|
|
$em->expects($this->once())->method('persist');
|
|
$em->expects($this->once())->method('flush');
|
|
|
|
$prestataire = $this->buildPrestataire();
|
|
$controller = $this->buildController($em);
|
|
|
|
$uploadedFile = new \Symfony\Component\HttpFoundation\File\UploadedFile(
|
|
$tmpFile,
|
|
'facture.pdf',
|
|
'application/pdf',
|
|
null,
|
|
true // test mode — bypass is_uploaded_file check
|
|
);
|
|
|
|
$request = new Request([], [
|
|
'numFacture' => 'FAC-UPLOAD-001',
|
|
'year' => 2026,
|
|
'month' => 4,
|
|
'montantHt' => '300.00',
|
|
'montantTtc' => '360.00',
|
|
]);
|
|
$request->files->set('facturePdf', $uploadedFile);
|
|
|
|
$response = $controller->addFacture($prestataire, $request);
|
|
|
|
@unlink($tmpFile);
|
|
|
|
$this->assertSame(302, $response->getStatusCode());
|
|
}
|
|
|
|
public function testCreateWithAllFields(): void
|
|
{
|
|
$em = $this->createMock(EntityManagerInterface::class);
|
|
$em->expects($this->once())->method('persist');
|
|
$em->expects($this->once())->method('flush');
|
|
|
|
$controller = $this->buildController($em);
|
|
|
|
$request = new Request([], [
|
|
'raisonSociale' => 'Full SARL',
|
|
'siret' => '12345678901234',
|
|
'email' => 'full@example.com',
|
|
'phone' => '0600000000',
|
|
'address' => '1 rue des Tests',
|
|
'zipCode' => '75001',
|
|
'city' => 'Paris',
|
|
]);
|
|
$response = $controller->create($request);
|
|
|
|
$this->assertSame(302, $response->getStatusCode());
|
|
}
|
|
}
|