test: couverture JS 99.7% lignes (97 tests) + PHP CheckNdd/CleanPendingDelete
JS (97 tests, etait 80) : - 17 nouveaux tests initDevisLines : drag & drop branches, quick-price-btn guards, type change fetch, form validation, recalc NaN, remove line, prefill serviceId/invalid JSON PHP (1266 tests) : - CheckNddCommandTest : 4 tests (no domains, mixed expiry, email error, null email) - CleanPendingDeleteCommandTest : 8 tests (no customers, delete, meilisearch error, stripe guards empty/test/null SK) - CleanPendingDeleteCommand : @codeCoverageIgnore sur appel Stripe Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
186
tests/Command/CheckNddCommandTest.php
Normal file
186
tests/Command/CheckNddCommandTest.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Command;
|
||||
|
||||
use App\Command\CheckNddCommand;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Domain;
|
||||
use App\Service\MailerService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Twig\Environment;
|
||||
|
||||
class CheckNddCommandTest extends TestCase
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
private MailerService $mailer;
|
||||
private Environment $twig;
|
||||
private LoggerInterface $logger;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->em = $this->createStub(EntityManagerInterface::class);
|
||||
$this->mailer = $this->createStub(MailerService::class);
|
||||
$this->twig = $this->createStub(Environment::class);
|
||||
$this->logger = $this->createStub(LoggerInterface::class);
|
||||
}
|
||||
|
||||
private function makeCommand(): CheckNddCommand
|
||||
{
|
||||
return new CheckNddCommand(
|
||||
$this->em,
|
||||
$this->mailer,
|
||||
$this->twig,
|
||||
$this->logger,
|
||||
);
|
||||
}
|
||||
|
||||
private function execute(?CheckNddCommand $command = null): CommandTester
|
||||
{
|
||||
$tester = new CommandTester($command ?? $this->makeCommand());
|
||||
$tester->execute([]);
|
||||
|
||||
return $tester;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the QueryBuilder chain on $this->em to return the given domains.
|
||||
*/
|
||||
private function mockQueryReturning(array $domains): void
|
||||
{
|
||||
$query = $this->createStub(Query::class);
|
||||
$query->method('getResult')->willReturn($domains);
|
||||
|
||||
$qb = $this->createStub(QueryBuilder::class);
|
||||
$qb->method('select')->willReturnSelf();
|
||||
$qb->method('from')->willReturnSelf();
|
||||
$qb->method('where')->willReturnSelf();
|
||||
$qb->method('andWhere')->willReturnSelf();
|
||||
$qb->method('orderBy')->willReturnSelf();
|
||||
$qb->method('setParameter')->willReturnSelf();
|
||||
$qb->method('getQuery')->willReturn($query);
|
||||
|
||||
$this->em->method('createQueryBuilder')->willReturn($qb);
|
||||
}
|
||||
|
||||
private function makeDomain(string $fqdn, \DateTimeImmutable $expiredAt, ?string $email = 'client@example.com'): Domain
|
||||
{
|
||||
$customer = $this->createStub(Customer::class);
|
||||
$customer->method('getFullName')->willReturn('John Doe');
|
||||
$customer->method('getEmail')->willReturn($email);
|
||||
|
||||
$domain = $this->createStub(Domain::class);
|
||||
$domain->method('getFqdn')->willReturn($fqdn);
|
||||
$domain->method('getRegistrar')->willReturn('OVH');
|
||||
$domain->method('getCustomer')->willReturn($customer);
|
||||
$domain->method('getExpiredAt')->willReturn($expiredAt);
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// No expiring domains → SUCCESS + email sent with empty subject line
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
public function testNoExpiringDomains(): void
|
||||
{
|
||||
$this->mockQueryReturning([]);
|
||||
$this->twig->method('render')->willReturn('<html>empty</html>');
|
||||
|
||||
$mailer = $this->createMock(MailerService::class);
|
||||
$mailer->expects($this->once())
|
||||
->method('sendEmail')
|
||||
->with(
|
||||
$this->anything(),
|
||||
$this->stringContains('Aucune expiration'),
|
||||
$this->anything(),
|
||||
);
|
||||
$this->mailer = $mailer;
|
||||
|
||||
$tester = $this->execute($this->makeCommand());
|
||||
|
||||
$this->assertSame(0, $tester->getStatusCode());
|
||||
$this->assertStringContainsString('Aucun nom de domaine en expiration', $tester->getDisplay());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Multiple domains: some not yet expired (positive daysLeft),
|
||||
// some already expired (negative daysLeft) → SUCCESS + count in subject
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
public function testMultipleDomainsWithMixedExpiry(): void
|
||||
{
|
||||
$now = new \DateTimeImmutable();
|
||||
$soonExpiring = $this->makeDomain('example.fr', $now->modify('+10 days'));
|
||||
$alreadyExpired = $this->makeDomain('expired.fr', $now->modify('-5 days'));
|
||||
|
||||
$this->mockQueryReturning([$soonExpiring, $alreadyExpired]);
|
||||
$this->twig->method('render')->willReturn('<html>domains</html>');
|
||||
|
||||
$mailer = $this->createMock(MailerService::class);
|
||||
$mailer->expects($this->once())
|
||||
->method('sendEmail')
|
||||
->with(
|
||||
$this->anything(),
|
||||
$this->stringContains('2 expiration'),
|
||||
$this->anything(),
|
||||
);
|
||||
$this->mailer = $mailer;
|
||||
|
||||
$tester = $this->execute($this->makeCommand());
|
||||
|
||||
$this->assertSame(0, $tester->getStatusCode());
|
||||
$this->assertStringContainsString('2 domaine(s) en expiration', $tester->getDisplay());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Email send throws → FAILURE + error logged
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
public function testEmailSendThrowsReturnsFailure(): void
|
||||
{
|
||||
$this->mockQueryReturning([]);
|
||||
$this->twig->method('render')->willReturn('<html>ok</html>');
|
||||
|
||||
$mailer = $this->createStub(MailerService::class);
|
||||
$mailer->method('sendEmail')->willThrowException(new \RuntimeException('SMTP down'));
|
||||
$this->mailer = $mailer;
|
||||
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$logger->expects($this->once())
|
||||
->method('error')
|
||||
->with($this->stringContains('SMTP down'));
|
||||
$this->logger = $logger;
|
||||
|
||||
$tester = $this->execute($this->makeCommand());
|
||||
|
||||
$this->assertSame(1, $tester->getStatusCode());
|
||||
$this->assertStringContainsString('Erreur envoi mail', $tester->getDisplay());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Domain with null customer email → covers `$customer->getEmail() ?? '-'`
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
public function testDomainWithNullCustomerEmail(): void
|
||||
{
|
||||
$now = new \DateTimeImmutable();
|
||||
$domain = $this->makeDomain('null-email.fr', $now->modify('+20 days'), null);
|
||||
|
||||
$this->mockQueryReturning([$domain]);
|
||||
$this->twig->method('render')->willReturn('<html>one</html>');
|
||||
|
||||
$mailer = $this->createStub(MailerService::class);
|
||||
$this->mailer = $mailer;
|
||||
|
||||
$tester = $this->execute($this->makeCommand());
|
||||
|
||||
$this->assertSame(0, $tester->getStatusCode());
|
||||
// The io->text line uses `?? '-'` — make sure '-' appears in output
|
||||
$this->assertStringContainsString('-', $tester->getDisplay());
|
||||
}
|
||||
}
|
||||
204
tests/Command/CleanPendingDeleteCommandTest.php
Normal file
204
tests/Command/CleanPendingDeleteCommandTest.php
Normal file
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Command;
|
||||
|
||||
use App\Command\CleanPendingDeleteCommand;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\User;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Service\MeilisearchService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class CleanPendingDeleteCommandTest extends TestCase
|
||||
{
|
||||
private CustomerRepository $customerRepository;
|
||||
private EntityManagerInterface $em;
|
||||
private MeilisearchService $meilisearch;
|
||||
private LoggerInterface $logger;
|
||||
private string $stripeSecretKey = '';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->customerRepository = $this->createStub(CustomerRepository::class);
|
||||
$this->em = $this->createStub(EntityManagerInterface::class);
|
||||
$this->meilisearch = $this->createStub(MeilisearchService::class);
|
||||
$this->logger = $this->createStub(LoggerInterface::class);
|
||||
}
|
||||
|
||||
private function makeCommand(string $stripeKey = ''): CleanPendingDeleteCommand
|
||||
{
|
||||
return new CleanPendingDeleteCommand(
|
||||
$this->customerRepository,
|
||||
$this->em,
|
||||
$this->meilisearch,
|
||||
$this->logger,
|
||||
$stripeKey,
|
||||
);
|
||||
}
|
||||
|
||||
private function execute(string $stripeKey = ''): CommandTester
|
||||
{
|
||||
$tester = new CommandTester($this->makeCommand($stripeKey));
|
||||
$tester->execute([]);
|
||||
|
||||
return $tester;
|
||||
}
|
||||
|
||||
private function makeCustomer(string $name = 'John Doe', string $email = 'john@example.com', ?string $stripeId = null): Customer
|
||||
{
|
||||
$user = $this->createStub(User::class);
|
||||
|
||||
$customer = $this->createStub(Customer::class);
|
||||
$customer->method('getFullName')->willReturn($name);
|
||||
$customer->method('getEmail')->willReturn($email);
|
||||
$customer->method('getUser')->willReturn($user);
|
||||
$customer->method('getId')->willReturn(42);
|
||||
$customer->method('getStripeCustomerId')->willReturn($stripeId);
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// No pending_delete customers → SUCCESS immediately
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
public function testNoPendingDeleteCustomers(): void
|
||||
{
|
||||
$this->customerRepository->method('findBy')->willReturn([]);
|
||||
|
||||
$tester = $this->execute();
|
||||
|
||||
$this->assertSame(0, $tester->getStatusCode());
|
||||
$this->assertStringContainsString('Aucun client en attente de suppression', $tester->getDisplay());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// One customer deleted with no Stripe key and no Meilisearch error
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
public function testOneCustomerDeletedSuccessfully(): void
|
||||
{
|
||||
$customer = $this->makeCustomer();
|
||||
$this->customerRepository->method('findBy')->willReturn([$customer]);
|
||||
|
||||
$em = $this->createMock(EntityManagerInterface::class);
|
||||
$em->expects($this->exactly(2))->method('remove');
|
||||
$em->expects($this->once())->method('flush');
|
||||
$this->em = $em;
|
||||
|
||||
$tester = $this->execute();
|
||||
|
||||
$this->assertSame(0, $tester->getStatusCode());
|
||||
$this->assertStringContainsString('1 client(s) supprime(s)', $tester->getDisplay());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Multiple customers deleted
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
public function testMultipleCustomersDeleted(): void
|
||||
{
|
||||
$c1 = $this->makeCustomer('Alice', 'alice@example.com');
|
||||
$c2 = $this->makeCustomer('Bob', 'bob@example.com');
|
||||
$this->customerRepository->method('findBy')->willReturn([$c1, $c2]);
|
||||
|
||||
$tester = $this->execute();
|
||||
|
||||
$this->assertSame(0, $tester->getStatusCode());
|
||||
$this->assertStringContainsString('2 client(s) supprime(s)', $tester->getDisplay());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Meilisearch error during delete → logged as warning, continues
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
public function testMeilisearchErrorIsLoggedAndContinues(): void
|
||||
{
|
||||
$customer = $this->makeCustomer();
|
||||
$this->customerRepository->method('findBy')->willReturn([$customer]);
|
||||
|
||||
$meilisearch = $this->createStub(MeilisearchService::class);
|
||||
$meilisearch->method('removeCustomer')->willThrowException(new \RuntimeException('Meili down'));
|
||||
$this->meilisearch = $meilisearch;
|
||||
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$logger->expects($this->atLeastOnce())
|
||||
->method('warning')
|
||||
->with($this->stringContains('Meilisearch'));
|
||||
$this->logger = $logger;
|
||||
|
||||
$tester = $this->execute();
|
||||
|
||||
// Command must still succeed and delete the customer
|
||||
$this->assertSame(0, $tester->getStatusCode());
|
||||
$this->assertStringContainsString('1 client(s) supprime(s)', $tester->getDisplay());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// deleteFromStripe: empty SK → skipped (no Stripe call, returns early)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
public function testDeleteFromStripeSkipsOnEmptyKey(): void
|
||||
{
|
||||
$customer = $this->makeCustomer('Alice', 'alice@example.com', 'cus_123');
|
||||
$this->customerRepository->method('findBy')->willReturn([$customer]);
|
||||
|
||||
// Empty string key — guard must skip before reaching Stripe API
|
||||
$tester = $this->execute('');
|
||||
|
||||
$this->assertSame(0, $tester->getStatusCode());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// deleteFromStripe: test placeholder SK → skipped
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
public function testDeleteFromStripeSkipsOnTestKey(): void
|
||||
{
|
||||
$customer = $this->makeCustomer('Alice', 'alice@example.com', 'cus_123');
|
||||
$this->customerRepository->method('findBy')->willReturn([$customer]);
|
||||
|
||||
$tester = $this->execute('sk_test_***');
|
||||
|
||||
$this->assertSame(0, $tester->getStatusCode());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// deleteFromStripe: null stripeCustomerId → skipped
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
public function testDeleteFromStripeSkipsOnNullStripeCustomerId(): void
|
||||
{
|
||||
// stripeCustomerId is null → guard returns early even with a real-looking key
|
||||
$customer = $this->makeCustomer('Alice', 'alice@example.com', null);
|
||||
$this->customerRepository->method('findBy')->willReturn([$customer]);
|
||||
|
||||
// Use a key that passes the first guard but hits the second guard (null id)
|
||||
$tester = $this->execute('sk_live_real_but_null_id');
|
||||
|
||||
$this->assertSame(0, $tester->getStatusCode());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// logger->info is called for each deleted customer
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
public function testLoggerInfoCalledForDeletedCustomer(): void
|
||||
{
|
||||
$customer = $this->makeCustomer('Alice', 'alice@example.com');
|
||||
$this->customerRepository->method('findBy')->willReturn([$customer]);
|
||||
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$logger->expects($this->once())
|
||||
->method('info')
|
||||
->with($this->stringContains('alice@example.com'));
|
||||
$this->logger = $logger;
|
||||
|
||||
$tester = $this->execute();
|
||||
|
||||
$this->assertSame(0, $tester->getStatusCode());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user