Add PHPUnit tests with coverage for all src classes
- 21 test files covering controllers, services, entities, enums, messages - CI: add test job with Xdebug coverage (clover + text) - SonarQube: configure coverage report path and test sources Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
91
tests/Service/CacheServiceTest.php
Normal file
91
tests/Service/CacheServiceTest.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Service;
|
||||
|
||||
use App\Enum\CacheKey;
|
||||
use App\Service\CacheService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
|
||||
class CacheServiceTest extends TestCase
|
||||
{
|
||||
private CacheItemPoolInterface $pool;
|
||||
private CacheService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->pool = $this->createMock(CacheItemPoolInterface::class);
|
||||
$this->service = new CacheService($this->pool);
|
||||
}
|
||||
|
||||
public function testGetReturnsNullOnCacheMiss(): void
|
||||
{
|
||||
$item = $this->createMock(CacheItemInterface::class);
|
||||
$item->method('isHit')->willReturn(false);
|
||||
$this->pool->method('getItem')->willReturn($item);
|
||||
|
||||
self::assertNull($this->service->get(CacheKey::HOME_PAGE));
|
||||
}
|
||||
|
||||
public function testGetReturnsValueOnCacheHit(): void
|
||||
{
|
||||
$item = $this->createMock(CacheItemInterface::class);
|
||||
$item->method('isHit')->willReturn(true);
|
||||
$item->method('get')->willReturn('cached-data');
|
||||
$this->pool->method('getItem')->willReturn($item);
|
||||
|
||||
self::assertSame('cached-data', $this->service->get(CacheKey::HOME_PAGE));
|
||||
}
|
||||
|
||||
public function testSetStoresValueWithTtl(): void
|
||||
{
|
||||
$item = $this->createMock(CacheItemInterface::class);
|
||||
$item->expects(self::once())->method('set')->with('value');
|
||||
$item->expects(self::once())->method('expiresAfter')->with(CacheKey::HOME_PAGE->ttl());
|
||||
$this->pool->method('getItem')->willReturn($item);
|
||||
$this->pool->expects(self::once())->method('save')->with($item);
|
||||
|
||||
$this->service->set(CacheKey::HOME_PAGE, 'value');
|
||||
}
|
||||
|
||||
public function testExistsDelegatesToHasItem(): void
|
||||
{
|
||||
$this->pool->expects(self::once())->method('hasItem')->willReturn(true);
|
||||
|
||||
self::assertTrue($this->service->exists(CacheKey::HOME_PAGE));
|
||||
}
|
||||
|
||||
public function testDeleteDelegatesToDeleteItem(): void
|
||||
{
|
||||
$this->pool->expects(self::once())->method('deleteItem');
|
||||
|
||||
$this->service->delete(CacheKey::HOME_PAGE);
|
||||
}
|
||||
|
||||
public function testRememberReturnsCachedValueOnHit(): void
|
||||
{
|
||||
$item = $this->createMock(CacheItemInterface::class);
|
||||
$item->method('isHit')->willReturn(true);
|
||||
$item->method('get')->willReturn('cached');
|
||||
$this->pool->method('getItem')->willReturn($item);
|
||||
|
||||
$result = $this->service->remember(CacheKey::HOME_PAGE, fn () => 'fresh');
|
||||
|
||||
self::assertSame('cached', $result);
|
||||
}
|
||||
|
||||
public function testRememberCallsCallbackOnMiss(): void
|
||||
{
|
||||
$item = $this->createMock(CacheItemInterface::class);
|
||||
$item->method('isHit')->willReturn(false);
|
||||
$item->expects(self::once())->method('set')->with('fresh');
|
||||
$item->expects(self::once())->method('expiresAfter')->with(CacheKey::HOME_PAGE->ttl());
|
||||
$this->pool->method('getItem')->willReturn($item);
|
||||
$this->pool->expects(self::once())->method('save')->with($item);
|
||||
|
||||
$result = $this->service->remember(CacheKey::HOME_PAGE, fn () => 'fresh');
|
||||
|
||||
self::assertSame('fresh', $result);
|
||||
}
|
||||
}
|
||||
77
tests/Service/MailerServiceTest.php
Normal file
77
tests/Service/MailerServiceTest.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Service;
|
||||
|
||||
use App\Service\MailerService;
|
||||
use App\Service\UnsubscribeManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class MailerServiceTest extends TestCase
|
||||
{
|
||||
private MessageBusInterface $bus;
|
||||
private UnsubscribeManager $unsubscribeManager;
|
||||
private EntityManagerInterface $em;
|
||||
private UrlGeneratorInterface $urlGenerator;
|
||||
private MailerService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->bus = $this->createMock(MessageBusInterface::class);
|
||||
$this->unsubscribeManager = $this->createMock(UnsubscribeManager::class);
|
||||
$this->em = $this->createMock(EntityManagerInterface::class);
|
||||
$this->urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
|
||||
$this->service = new MailerService(
|
||||
$this->bus,
|
||||
sys_get_temp_dir(),
|
||||
'passphrase',
|
||||
$this->urlGenerator,
|
||||
$this->unsubscribeManager,
|
||||
$this->em,
|
||||
);
|
||||
}
|
||||
|
||||
public function testSendEmailSkipsUnsubscribedRecipient(): void
|
||||
{
|
||||
$this->unsubscribeManager->method('isUnsubscribed')->willReturn(true);
|
||||
$this->bus->expects(self::never())->method('dispatch');
|
||||
|
||||
$this->service->sendEmail('user@example.com', 'Subject', '<p>Body</p>');
|
||||
}
|
||||
|
||||
public function testSendEmailDoesNotSkipWhitelistedAddress(): void
|
||||
{
|
||||
$this->unsubscribeManager->method('isUnsubscribed')->willReturn(true);
|
||||
$this->urlGenerator->method('generate')->willReturn('https://example.com/track/abc');
|
||||
$this->em->expects(self::once())->method('persist');
|
||||
$this->em->expects(self::once())->method('flush');
|
||||
$this->bus->expects(self::once())->method('dispatch')->willReturn(new Envelope(new \stdClass()));
|
||||
|
||||
$this->service->sendEmail('contact@e-cosplay.fr', 'Subject', '<p>Body</p>');
|
||||
}
|
||||
|
||||
public function testSendEmailDispatchesForNonUnsubscribedUser(): void
|
||||
{
|
||||
$this->unsubscribeManager->method('isUnsubscribed')->willReturn(false);
|
||||
$this->unsubscribeManager->method('generateToken')->willReturn('token123');
|
||||
$this->urlGenerator->method('generate')->willReturn('https://example.com/url');
|
||||
$this->em->expects(self::once())->method('persist');
|
||||
$this->em->expects(self::once())->method('flush');
|
||||
$this->bus->expects(self::once())->method('dispatch')->willReturn(new Envelope(new \stdClass()));
|
||||
|
||||
$this->service->sendEmail('user@example.com', 'Test', '<p>Content</p>');
|
||||
}
|
||||
|
||||
public function testSendEmailWithoutUnsubscribeHeaders(): void
|
||||
{
|
||||
$this->urlGenerator->method('generate')->willReturn('https://example.com/url');
|
||||
$this->em->expects(self::once())->method('persist');
|
||||
$this->bus->expects(self::once())->method('dispatch')->willReturn(new Envelope(new \stdClass()));
|
||||
|
||||
$this->service->sendEmail('user@example.com', 'Test', '<p>Content</p>', withUnsubscribe: false);
|
||||
}
|
||||
}
|
||||
102
tests/Service/MeilisearchServiceTest.php
Normal file
102
tests/Service/MeilisearchServiceTest.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Service;
|
||||
|
||||
use App\Message\MeilisearchMessage;
|
||||
use App\Service\MeilisearchService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
class MeilisearchServiceTest extends TestCase
|
||||
{
|
||||
private HttpClientInterface $httpClient;
|
||||
private MessageBusInterface $bus;
|
||||
private MeilisearchService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->httpClient = $this->createMock(HttpClientInterface::class);
|
||||
$this->bus = $this->createMock(MessageBusInterface::class);
|
||||
$this->service = new MeilisearchService(
|
||||
$this->httpClient,
|
||||
$this->bus,
|
||||
'http://meilisearch:7700',
|
||||
'test-key',
|
||||
);
|
||||
}
|
||||
|
||||
public function testIndexExistsReturnsTrue(): void
|
||||
{
|
||||
$response = $this->createMock(ResponseInterface::class);
|
||||
$response->method('getStatusCode')->willReturn(200);
|
||||
$this->httpClient->method('request')->willReturn($response);
|
||||
|
||||
self::assertTrue($this->service->indexExists('events'));
|
||||
}
|
||||
|
||||
public function testIndexExistsReturnsFalseOnException(): void
|
||||
{
|
||||
$this->httpClient->method('request')->willThrowException(new \RuntimeException('fail'));
|
||||
|
||||
self::assertFalse($this->service->indexExists('events'));
|
||||
}
|
||||
|
||||
public function testCreateIndexDispatchesMessage(): void
|
||||
{
|
||||
$this->bus->expects(self::once())
|
||||
->method('dispatch')
|
||||
->with(self::callback(fn (MeilisearchMessage $m) => 'createIndex' === $m->action && 'events' === $m->index))
|
||||
->willReturn(new Envelope(new \stdClass()));
|
||||
|
||||
$this->service->createIndex('events');
|
||||
}
|
||||
|
||||
public function testDeleteIndexDispatchesMessage(): void
|
||||
{
|
||||
$this->bus->expects(self::once())
|
||||
->method('dispatch')
|
||||
->with(self::callback(fn (MeilisearchMessage $m) => 'deleteIndex' === $m->action))
|
||||
->willReturn(new Envelope(new \stdClass()));
|
||||
|
||||
$this->service->deleteIndex('events');
|
||||
}
|
||||
|
||||
public function testAddDocumentsDispatchesMessage(): void
|
||||
{
|
||||
$docs = [['id' => 1, 'title' => 'Test']];
|
||||
$this->bus->expects(self::once())
|
||||
->method('dispatch')
|
||||
->with(self::callback(fn (MeilisearchMessage $m) => 'addDocuments' === $m->action && $m->payload['documents'] === $docs))
|
||||
->willReturn(new Envelope(new \stdClass()));
|
||||
|
||||
$this->service->addDocuments('events', $docs);
|
||||
}
|
||||
|
||||
public function testSearchMakesPostRequest(): void
|
||||
{
|
||||
$response = $this->createMock(ResponseInterface::class);
|
||||
$response->method('getStatusCode')->willReturn(200);
|
||||
$response->method('toArray')->willReturn(['hits' => []]);
|
||||
$this->httpClient->method('request')
|
||||
->with('POST', self::stringContains('/indexes/events/search'), self::anything())
|
||||
->willReturn($response);
|
||||
|
||||
$result = $this->service->search('events', 'test');
|
||||
|
||||
self::assertArrayHasKey('hits', $result);
|
||||
}
|
||||
|
||||
public function testRequestReturnsEmptyArrayOn204(): void
|
||||
{
|
||||
$response = $this->createMock(ResponseInterface::class);
|
||||
$response->method('getStatusCode')->willReturn(204);
|
||||
$this->httpClient->method('request')->willReturn($response);
|
||||
|
||||
$result = $this->service->request('DELETE', '/indexes/events');
|
||||
|
||||
self::assertSame([], $result);
|
||||
}
|
||||
}
|
||||
82
tests/Service/UnsubscribeManagerTest.php
Normal file
82
tests/Service/UnsubscribeManagerTest.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Service;
|
||||
|
||||
use App\Service\UnsubscribeManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class UnsubscribeManagerTest extends TestCase
|
||||
{
|
||||
private string $tempDir;
|
||||
private UnsubscribeManager $manager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tempDir = sys_get_temp_dir().'/unsubscribe_test_'.uniqid();
|
||||
mkdir($this->tempDir.'/var', 0755, true);
|
||||
$this->manager = new UnsubscribeManager($this->tempDir, 'test-secret');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$file = $this->tempDir.'/var/unsubscribed.json';
|
||||
if (file_exists($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
if (is_dir($this->tempDir.'/var')) {
|
||||
rmdir($this->tempDir.'/var');
|
||||
}
|
||||
if (is_dir($this->tempDir)) {
|
||||
rmdir($this->tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
public function testGenerateTokenIsDeterministic(): void
|
||||
{
|
||||
$token1 = $this->manager->generateToken('user@example.com');
|
||||
$token2 = $this->manager->generateToken('user@example.com');
|
||||
|
||||
self::assertSame($token1, $token2);
|
||||
}
|
||||
|
||||
public function testGenerateTokenNormalizesEmail(): void
|
||||
{
|
||||
$token1 = $this->manager->generateToken('User@Example.com');
|
||||
$token2 = $this->manager->generateToken(' user@example.com ');
|
||||
|
||||
self::assertSame($token1, $token2);
|
||||
}
|
||||
|
||||
public function testIsValidTokenReturnsTrueForMatchingToken(): void
|
||||
{
|
||||
$token = $this->manager->generateToken('user@example.com');
|
||||
|
||||
self::assertTrue($this->manager->isValidToken('user@example.com', $token));
|
||||
}
|
||||
|
||||
public function testIsValidTokenReturnsFalseForWrongToken(): void
|
||||
{
|
||||
self::assertFalse($this->manager->isValidToken('user@example.com', 'wrong-token'));
|
||||
}
|
||||
|
||||
public function testIsUnsubscribedReturnsFalseByDefault(): void
|
||||
{
|
||||
self::assertFalse($this->manager->isUnsubscribed('user@example.com'));
|
||||
}
|
||||
|
||||
public function testUnsubscribeAndIsUnsubscribed(): void
|
||||
{
|
||||
$this->manager->unsubscribe('user@example.com');
|
||||
|
||||
self::assertTrue($this->manager->isUnsubscribed('user@example.com'));
|
||||
}
|
||||
|
||||
public function testUnsubscribeIsIdempotent(): void
|
||||
{
|
||||
$this->manager->unsubscribe('user@example.com');
|
||||
$this->manager->unsubscribe('user@example.com');
|
||||
|
||||
$data = json_decode(file_get_contents($this->tempDir.'/var/unsubscribed.json'), true);
|
||||
self::assertCount(1, $data);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user