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:
56
tests/Controller/CspReportControllerTest.php
Normal file
56
tests/Controller/CspReportControllerTest.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
class CspReportControllerTest extends WebTestCase
|
||||
{
|
||||
public function testInvalidPayloadReturnsBadRequest(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->request('POST', '/my-csp-report', [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
], '');
|
||||
|
||||
self::assertResponseStatusCodeSame(400);
|
||||
}
|
||||
|
||||
public function testBrowserExtensionViolationIsIgnored(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$payload = json_encode([
|
||||
'csp-report' => [
|
||||
'source-file' => 'chrome-extension://abc',
|
||||
'blocked-uri' => 'inline',
|
||||
'document-uri' => 'https://e-cosplay.fr/',
|
||||
'violated-directive' => 'script-src',
|
||||
],
|
||||
]);
|
||||
|
||||
$client->request('POST', '/my-csp-report', [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
], $payload);
|
||||
|
||||
self::assertResponseStatusCodeSame(204);
|
||||
}
|
||||
|
||||
public function testRealViolationIsProcessed(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$payload = json_encode([
|
||||
'csp-report' => [
|
||||
'source-file' => 'https://evil.com/script.js',
|
||||
'blocked-uri' => 'https://evil.com',
|
||||
'document-uri' => 'https://e-cosplay.fr/page',
|
||||
'violated-directive' => 'script-src',
|
||||
],
|
||||
]);
|
||||
|
||||
$client->request('POST', '/my-csp-report', [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
], $payload);
|
||||
|
||||
self::assertResponseStatusCodeSame(204);
|
||||
}
|
||||
}
|
||||
17
tests/Controller/EmailTrackingControllerTest.php
Normal file
17
tests/Controller/EmailTrackingControllerTest.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
class EmailTrackingControllerTest extends WebTestCase
|
||||
{
|
||||
public function testTrackReturnsImageResponse(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->request('GET', '/track/nonexistent-id/logo.jpg');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertResponseHeaderSame('Content-Type', 'image/jpeg');
|
||||
}
|
||||
}
|
||||
16
tests/Controller/HomeControllerTest.php
Normal file
16
tests/Controller/HomeControllerTest.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
class HomeControllerTest extends WebTestCase
|
||||
{
|
||||
public function testIndexReturnsSuccess(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->request('GET', '/');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
}
|
||||
24
tests/Controller/RedirectControllerTest.php
Normal file
24
tests/Controller/RedirectControllerTest.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
class RedirectControllerTest extends WebTestCase
|
||||
{
|
||||
public function testNoUrlRedirectsToHome(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->request('GET', '/external-redirect');
|
||||
|
||||
self::assertResponseRedirects('/');
|
||||
}
|
||||
|
||||
public function testWithUrlRendersWarningPage(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->request('GET', '/external-redirect?redirUrl=https://example.com');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
}
|
||||
16
tests/Controller/RegistrationControllerTest.php
Normal file
16
tests/Controller/RegistrationControllerTest.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
class RegistrationControllerTest extends WebTestCase
|
||||
{
|
||||
public function testRegistrationPageReturnsSuccess(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->request('GET', '/inscription');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
}
|
||||
35
tests/Controller/RobotsControllerTest.php
Normal file
35
tests/Controller/RobotsControllerTest.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
class RobotsControllerTest extends WebTestCase
|
||||
{
|
||||
public function testRobotsReturnsPlainText(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->request('GET', '/robots.txt');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertResponseHeaderSame('Content-Type', 'text/plain');
|
||||
}
|
||||
|
||||
public function testRobotsContainsSitemap(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->request('GET', '/robots.txt');
|
||||
|
||||
self::assertStringContainsString('Sitemap:', $client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testRobotsContainsDisallowRules(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->request('GET', '/robots.txt');
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
self::assertStringContainsString('Disallow: /external-redirect', $content);
|
||||
self::assertStringContainsString('Disallow: /my-csp-report', $content);
|
||||
}
|
||||
}
|
||||
24
tests/Controller/SearchControllerTest.php
Normal file
24
tests/Controller/SearchControllerTest.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
class SearchControllerTest extends WebTestCase
|
||||
{
|
||||
public function testSearchPageReturnsSuccess(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->request('GET', '/search');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testSearchWithQueryParam(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->request('GET', '/search?q=cosplay');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
}
|
||||
24
tests/Controller/SecurityControllerTest.php
Normal file
24
tests/Controller/SecurityControllerTest.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
class SecurityControllerTest extends WebTestCase
|
||||
{
|
||||
public function testLoginPageReturnsSuccess(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->request('GET', '/connexion');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testLogoutThrowsLogicException(): void
|
||||
{
|
||||
$this->expectException(\LogicException::class);
|
||||
|
||||
$controller = new \App\Controller\SecurityController();
|
||||
$controller->logout();
|
||||
}
|
||||
}
|
||||
35
tests/Controller/SitemapControllerTest.php
Normal file
35
tests/Controller/SitemapControllerTest.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
class SitemapControllerTest extends WebTestCase
|
||||
{
|
||||
public function testSitemapIndexReturnsXml(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->request('GET', '/sitemap.xml');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertResponseHeaderSame('Content-Type', 'text/xml');
|
||||
}
|
||||
|
||||
public function testSitemapMainReturnsXml(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->request('GET', '/sitemap-main.xml');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertResponseHeaderSame('Content-Type', 'text/xml');
|
||||
}
|
||||
|
||||
public function testSitemapEventsReturnsXml(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->request('GET', '/sitemap-events-1.xml');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertResponseHeaderSame('Content-Type', 'text/xml');
|
||||
}
|
||||
}
|
||||
34
tests/Controller/UnsubscribeControllerTest.php
Normal file
34
tests/Controller/UnsubscribeControllerTest.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
class UnsubscribeControllerTest extends WebTestCase
|
||||
{
|
||||
public function testInvalidTokenReturns404(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->request('GET', '/unsubscribe/invalid-base64');
|
||||
|
||||
self::assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
public function testValidTokenShowsUnsubscribePage(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$token = base64_encode('user@example.com');
|
||||
$client->request('GET', '/unsubscribe/'.$token);
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testPostConfirmsUnsubscribe(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$token = base64_encode('user@example.com');
|
||||
$client->request('POST', '/unsubscribe/'.$token);
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
}
|
||||
42
tests/Entity/EmailTrackingTest.php
Normal file
42
tests/Entity/EmailTrackingTest.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Entity\EmailTracking;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class EmailTrackingTest extends TestCase
|
||||
{
|
||||
public function testConstructorSetsFields(): void
|
||||
{
|
||||
$tracking = new EmailTracking('msg-123', 'user@example.com', 'Hello');
|
||||
|
||||
self::assertSame('msg-123', $tracking->getMessageId());
|
||||
self::assertSame('user@example.com', $tracking->getRecipient());
|
||||
self::assertSame('Hello', $tracking->getSubject());
|
||||
self::assertSame('sent', $tracking->getState());
|
||||
self::assertInstanceOf(\DateTimeImmutable::class, $tracking->getSentAt());
|
||||
self::assertNull($tracking->getOpenedAt());
|
||||
self::assertNull($tracking->getId());
|
||||
}
|
||||
|
||||
public function testMarkAsOpenedTransitionsState(): void
|
||||
{
|
||||
$tracking = new EmailTracking('msg-123', 'user@example.com', 'Hello');
|
||||
$tracking->markAsOpened();
|
||||
|
||||
self::assertSame('opened', $tracking->getState());
|
||||
self::assertInstanceOf(\DateTimeImmutable::class, $tracking->getOpenedAt());
|
||||
}
|
||||
|
||||
public function testMarkAsOpenedDoesNotTransitionTwice(): void
|
||||
{
|
||||
$tracking = new EmailTracking('msg-123', 'user@example.com', 'Hello');
|
||||
$tracking->markAsOpened();
|
||||
$firstOpenedAt = $tracking->getOpenedAt();
|
||||
|
||||
$tracking->markAsOpened();
|
||||
|
||||
self::assertSame($firstOpenedAt, $tracking->getOpenedAt());
|
||||
}
|
||||
}
|
||||
49
tests/Entity/MessengerLogTest.php
Normal file
49
tests/Entity/MessengerLogTest.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Entity\MessengerLog;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class MessengerLogTest extends TestCase
|
||||
{
|
||||
public function testConstructorSetsFields(): void
|
||||
{
|
||||
$log = new MessengerLog(
|
||||
messageClass: 'App\\Message\\TestMessage',
|
||||
messageBody: 'serialized-body',
|
||||
errorMessage: 'Something failed',
|
||||
stackTrace: '#0 trace',
|
||||
transportName: 'async',
|
||||
retryCount: 2,
|
||||
);
|
||||
|
||||
self::assertSame('App\\Message\\TestMessage', $log->getMessageClass());
|
||||
self::assertSame('serialized-body', $log->getMessageBody());
|
||||
self::assertSame('failed', $log->getStatus());
|
||||
self::assertSame('Something failed', $log->getErrorMessage());
|
||||
self::assertSame('#0 trace', $log->getStackTrace());
|
||||
self::assertSame('async', $log->getTransportName());
|
||||
self::assertSame(2, $log->getRetryCount());
|
||||
self::assertInstanceOf(\DateTimeImmutable::class, $log->getCreatedAt());
|
||||
self::assertInstanceOf(\DateTimeImmutable::class, $log->getFailedAt());
|
||||
self::assertNull($log->getId());
|
||||
}
|
||||
|
||||
public function testMarkAsResolved(): void
|
||||
{
|
||||
$log = new MessengerLog('Class', null, 'error');
|
||||
$log->markAsResolved();
|
||||
|
||||
self::assertSame('resolved', $log->getStatus());
|
||||
}
|
||||
|
||||
public function testSetStatus(): void
|
||||
{
|
||||
$log = new MessengerLog('Class', null, 'error');
|
||||
$result = $log->setStatus('retrying');
|
||||
|
||||
self::assertSame('retrying', $log->getStatus());
|
||||
self::assertSame($log, $result);
|
||||
}
|
||||
}
|
||||
66
tests/Entity/UserTest.php
Normal file
66
tests/Entity/UserTest.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Entity\User;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class UserTest extends TestCase
|
||||
{
|
||||
public function testNewUserHasCreatedAt(): void
|
||||
{
|
||||
$user = new User();
|
||||
|
||||
self::assertInstanceOf(\DateTimeImmutable::class, $user->getCreatedAt());
|
||||
}
|
||||
|
||||
public function testGetRolesAlwaysIncludesRoleUser(): void
|
||||
{
|
||||
$user = new User();
|
||||
|
||||
self::assertContains('ROLE_USER', $user->getRoles());
|
||||
}
|
||||
|
||||
public function testSetRolesDeduplicatesRoleUser(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->setRoles(['ROLE_ADMIN', 'ROLE_USER']);
|
||||
|
||||
$roles = $user->getRoles();
|
||||
self::assertCount(2, $roles);
|
||||
self::assertContains('ROLE_ADMIN', $roles);
|
||||
self::assertContains('ROLE_USER', $roles);
|
||||
}
|
||||
|
||||
public function testGetUserIdentifierReturnsEmail(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->setEmail('test@example.com');
|
||||
|
||||
self::assertSame('test@example.com', $user->getUserIdentifier());
|
||||
}
|
||||
|
||||
public function testFluentSetters(): void
|
||||
{
|
||||
$user = new User();
|
||||
|
||||
$result = $user->setEmail('a@b.com')
|
||||
->setFirstName('John')
|
||||
->setLastName('Doe')
|
||||
->setPassword('hashed');
|
||||
|
||||
self::assertSame($user, $result);
|
||||
self::assertSame('a@b.com', $user->getEmail());
|
||||
self::assertSame('John', $user->getFirstName());
|
||||
self::assertSame('Doe', $user->getLastName());
|
||||
self::assertSame('hashed', $user->getPassword());
|
||||
}
|
||||
|
||||
public function testEraseCredentialsDoesNotThrow(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->eraseCredentials();
|
||||
|
||||
self::assertNull($user->getId());
|
||||
}
|
||||
}
|
||||
37
tests/Enum/CacheKeyTest.php
Normal file
37
tests/Enum/CacheKeyTest.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Enum;
|
||||
|
||||
use App\Enum\CacheKey;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class CacheKeyTest extends TestCase
|
||||
{
|
||||
public function testResolveWithParameters(): void
|
||||
{
|
||||
self::assertSame('user_profile_42', CacheKey::USER_PROFILE->resolve(42));
|
||||
self::assertSame('event_detail_10', CacheKey::EVENT_DETAIL->resolve(10));
|
||||
self::assertSame('search_symfony', CacheKey::SEARCH_RESULTS->resolve('symfony'));
|
||||
}
|
||||
|
||||
public function testResolveWithoutParameters(): void
|
||||
{
|
||||
self::assertSame('event_list', CacheKey::EVENT_LIST->resolve());
|
||||
self::assertSame('home_page', CacheKey::HOME_PAGE->resolve());
|
||||
}
|
||||
|
||||
public function testTtlReturnsPositiveIntegers(): void
|
||||
{
|
||||
foreach (CacheKey::cases() as $case) {
|
||||
self::assertGreaterThan(0, $case->ttl(), "TTL for {$case->name} should be positive");
|
||||
}
|
||||
}
|
||||
|
||||
public function testSpecificTtlValues(): void
|
||||
{
|
||||
self::assertSame(1800, CacheKey::USER_PROFILE->ttl());
|
||||
self::assertSame(300, CacheKey::EVENT_LIST->ttl());
|
||||
self::assertSame(60, CacheKey::EVENT_TICKETS->ttl());
|
||||
self::assertSame(3600, CacheKey::SITEMAP_MAIN->ttl());
|
||||
}
|
||||
}
|
||||
62
tests/EventSubscriber/MessengerFailureSubscriberTest.php
Normal file
62
tests/EventSubscriber/MessengerFailureSubscriberTest.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\EventSubscriber;
|
||||
|
||||
use App\EventSubscriber\MessengerFailureSubscriber;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||
|
||||
class MessengerFailureSubscriberTest extends TestCase
|
||||
{
|
||||
public function testGetSubscribedEvents(): void
|
||||
{
|
||||
$events = MessengerFailureSubscriber::getSubscribedEvents();
|
||||
|
||||
self::assertArrayHasKey(WorkerMessageFailedEvent::class, $events);
|
||||
self::assertSame('onMessageFailed', $events[WorkerMessageFailedEvent::class]);
|
||||
}
|
||||
|
||||
public function testOnMessageFailedPersistsLogAndSendsEmail(): void
|
||||
{
|
||||
$em = $this->createMock(EntityManagerInterface::class);
|
||||
$mailer = $this->createMock(MailerInterface::class);
|
||||
|
||||
$em->expects(self::once())->method('persist');
|
||||
$em->expects(self::once())->method('flush');
|
||||
$mailer->expects(self::once())->method('send');
|
||||
|
||||
$subscriber = new MessengerFailureSubscriber($em, $mailer);
|
||||
|
||||
$message = new \stdClass();
|
||||
$envelope = new Envelope($message);
|
||||
$exception = new \RuntimeException('Test failure');
|
||||
|
||||
$event = new WorkerMessageFailedEvent($envelope, 'async', $exception);
|
||||
|
||||
$subscriber->onMessageFailed($event);
|
||||
}
|
||||
|
||||
public function testOnMessageFailedHandlesMailerException(): void
|
||||
{
|
||||
$em = $this->createMock(EntityManagerInterface::class);
|
||||
$mailer = $this->createMock(MailerInterface::class);
|
||||
|
||||
$em->expects(self::once())->method('persist');
|
||||
$em->expects(self::once())->method('flush');
|
||||
$mailer->method('send')->willThrowException(new \RuntimeException('mail failed'));
|
||||
|
||||
$subscriber = new MessengerFailureSubscriber($em, $mailer);
|
||||
|
||||
$message = new \stdClass();
|
||||
$envelope = new Envelope($message);
|
||||
$exception = new \RuntimeException('Test failure');
|
||||
$event = new WorkerMessageFailedEvent($envelope, 'async', $exception);
|
||||
|
||||
$subscriber->onMessageFailed($event);
|
||||
|
||||
self::assertTrue(true);
|
||||
}
|
||||
}
|
||||
25
tests/Message/MeilisearchMessageTest.php
Normal file
25
tests/Message/MeilisearchMessageTest.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Message;
|
||||
|
||||
use App\Message\MeilisearchMessage;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class MeilisearchMessageTest extends TestCase
|
||||
{
|
||||
public function testConstructorSetsProperties(): void
|
||||
{
|
||||
$message = new MeilisearchMessage('addDocuments', 'events', ['documents' => [['id' => 1]]]);
|
||||
|
||||
self::assertSame('addDocuments', $message->action);
|
||||
self::assertSame('events', $message->index);
|
||||
self::assertSame(['documents' => [['id' => 1]]], $message->payload);
|
||||
}
|
||||
|
||||
public function testDefaultPayloadIsEmpty(): void
|
||||
{
|
||||
$message = new MeilisearchMessage('deleteIndex', 'events');
|
||||
|
||||
self::assertSame([], $message->payload);
|
||||
}
|
||||
}
|
||||
94
tests/MessageHandler/MeilisearchMessageHandlerTest.php
Normal file
94
tests/MessageHandler/MeilisearchMessageHandlerTest.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\MessageHandler;
|
||||
|
||||
use App\Message\MeilisearchMessage;
|
||||
use App\MessageHandler\MeilisearchMessageHandler;
|
||||
use App\Service\MeilisearchService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class MeilisearchMessageHandlerTest extends TestCase
|
||||
{
|
||||
private MeilisearchService $meilisearch;
|
||||
private MeilisearchMessageHandler $handler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->meilisearch = $this->createMock(MeilisearchService::class);
|
||||
$this->handler = new MeilisearchMessageHandler($this->meilisearch);
|
||||
}
|
||||
|
||||
public function testHandleCreateIndex(): void
|
||||
{
|
||||
$this->meilisearch->expects(self::once())
|
||||
->method('createIndex')
|
||||
->with('events', 'uid');
|
||||
|
||||
($this->handler)(new MeilisearchMessage('createIndex', 'events', ['primaryKey' => 'uid']));
|
||||
}
|
||||
|
||||
public function testHandleDeleteIndex(): void
|
||||
{
|
||||
$this->meilisearch->expects(self::once())
|
||||
->method('deleteIndex')
|
||||
->with('events');
|
||||
|
||||
($this->handler)(new MeilisearchMessage('deleteIndex', 'events'));
|
||||
}
|
||||
|
||||
public function testHandleAddDocuments(): void
|
||||
{
|
||||
$docs = [['id' => 1]];
|
||||
$this->meilisearch->expects(self::once())
|
||||
->method('addDocuments')
|
||||
->with('events', $docs);
|
||||
|
||||
($this->handler)(new MeilisearchMessage('addDocuments', 'events', ['documents' => $docs]));
|
||||
}
|
||||
|
||||
public function testHandleUpdateDocuments(): void
|
||||
{
|
||||
$docs = [['id' => 1, 'title' => 'Updated']];
|
||||
$this->meilisearch->expects(self::once())
|
||||
->method('updateDocuments')
|
||||
->with('events', $docs);
|
||||
|
||||
($this->handler)(new MeilisearchMessage('updateDocuments', 'events', ['documents' => $docs]));
|
||||
}
|
||||
|
||||
public function testHandleDeleteDocument(): void
|
||||
{
|
||||
$this->meilisearch->expects(self::once())
|
||||
->method('deleteDocument')
|
||||
->with('events', 42);
|
||||
|
||||
($this->handler)(new MeilisearchMessage('deleteDocument', 'events', ['documentId' => 42]));
|
||||
}
|
||||
|
||||
public function testHandleDeleteDocuments(): void
|
||||
{
|
||||
$this->meilisearch->expects(self::once())
|
||||
->method('deleteDocuments')
|
||||
->with('events', [1, 2, 3]);
|
||||
|
||||
($this->handler)(new MeilisearchMessage('deleteDocuments', 'events', ['ids' => [1, 2, 3]]));
|
||||
}
|
||||
|
||||
public function testHandleUpdateSettings(): void
|
||||
{
|
||||
$settings = ['searchableAttributes' => ['title']];
|
||||
$this->meilisearch->expects(self::once())
|
||||
->method('updateSettings')
|
||||
->with('events', $settings);
|
||||
|
||||
($this->handler)(new MeilisearchMessage('updateSettings', 'events', ['settings' => $settings]));
|
||||
}
|
||||
|
||||
public function testUnknownActionThrowsException(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Unknown action: invalid');
|
||||
|
||||
($this->handler)(new MeilisearchMessage('invalid', 'events'));
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
90
tests/Twig/ViteAssetExtensionTest.php
Normal file
90
tests/Twig/ViteAssetExtensionTest.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Twig;
|
||||
|
||||
use App\Twig\ViteAssetExtension;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
|
||||
class ViteAssetExtensionTest extends TestCase
|
||||
{
|
||||
private CacheItemPoolInterface $cache;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->cache = $this->createMock(CacheItemPoolInterface::class);
|
||||
$_ENV['VITE_LOAD'] = '1';
|
||||
}
|
||||
|
||||
private function createExtension(string $manifestPath = '/tmp/nonexistent'): ViteAssetExtension
|
||||
{
|
||||
return new ViteAssetExtension($manifestPath, $this->cache);
|
||||
}
|
||||
|
||||
public function testGetFunctionsReturnsExpectedNames(): void
|
||||
{
|
||||
$extension = $this->createExtension();
|
||||
$functions = $extension->getFunctions();
|
||||
|
||||
$names = array_map(fn ($f) => $f->getName(), $functions);
|
||||
|
||||
self::assertContains('vite_asset', $names);
|
||||
self::assertContains('isMobile', $names);
|
||||
self::assertContains('vite_favicons', $names);
|
||||
}
|
||||
|
||||
public function testAssetDevReturnsScriptTags(): void
|
||||
{
|
||||
$_ENV['VITE_LOAD'] = '0';
|
||||
$extension = $this->createExtension();
|
||||
|
||||
$html = $extension->asset('app.js', []);
|
||||
|
||||
self::assertStringContainsString('localhost:5173', $html);
|
||||
self::assertStringContainsString('app.js', $html);
|
||||
}
|
||||
|
||||
public function testAssetProdUsesManifest(): void
|
||||
{
|
||||
$manifest = [
|
||||
'app.js' => [
|
||||
'file' => 'assets/app.abc123.js',
|
||||
'css' => ['assets/app.def456.css'],
|
||||
],
|
||||
];
|
||||
|
||||
$item = $this->createMock(CacheItemInterface::class);
|
||||
$item->method('isHit')->willReturn(true);
|
||||
$item->method('get')->willReturn($manifest);
|
||||
$this->cache->method('getItem')->willReturn($item);
|
||||
|
||||
$extension = $this->createExtension();
|
||||
$html = $extension->assetProd('app.js');
|
||||
|
||||
self::assertStringContainsString('assets/app.abc123.js', $html);
|
||||
self::assertStringContainsString('assets/app.def456.css', $html);
|
||||
}
|
||||
|
||||
public function testAssetProdHandlesMissingManifest(): void
|
||||
{
|
||||
$item = $this->createMock(CacheItemInterface::class);
|
||||
$item->method('isHit')->willReturn(false);
|
||||
$this->cache->method('getItem')->willReturn($item);
|
||||
|
||||
$extension = $this->createExtension('/tmp/nonexistent_manifest.json');
|
||||
$html = $extension->assetProd('missing.js');
|
||||
|
||||
self::assertStringContainsString('script', $html);
|
||||
}
|
||||
|
||||
public function testFaviconsDevReturnsFaviconLink(): void
|
||||
{
|
||||
$_ENV['VITE_LOAD'] = '0';
|
||||
$extension = $this->createExtension();
|
||||
|
||||
$html = $extension->favicons();
|
||||
|
||||
self::assertStringContainsString('favicon.ico', $html);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user