Files
Classeo/backend/tests/Unit/Shared/Infrastructure/Monitoring/MetricsControllerTest.php
Mathias STRASSER 9ccad77bf0 feat: Messaging asynchrone fiable avec retry, dead-letter et métriques
Les événements métier (emails d'invitation, reset password, activation)
bloquaient la réponse API en étant traités de manière synchrone. Ce commit
route ces événements vers un transport AMQP asynchrone avec un worker
dédié, garantissant des réponses API rapides et une gestion robuste des
échecs.

Le retry utilise une stratégie Fibonacci (1s, 1s, 2s, 3s, 5s, 8s, 13s)
qui offre un bon compromis entre réactivité et protection des services
externes. Les messages qui épuisent leurs tentatives arrivent dans une
dead-letter queue Doctrine avec alerte email à l'admin.

La commande console CreateTestActivationTokenCommand détecte désormais
les comptes déjà actifs et génère un token de réinitialisation de mot
de passe au lieu d'un token d'activation, évitant une erreur bloquante
lors de la ré-invitation par un admin.
2026-02-08 21:38:20 +01:00

178 lines
5.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Unit\Shared\Infrastructure\Monitoring;
use App\Shared\Infrastructure\Monitoring\HealthMetricsCollectorInterface;
use App\Shared\Infrastructure\Monitoring\MessengerMetricsCollectorInterface;
use App\Shared\Infrastructure\Monitoring\MetricsController;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Prometheus\CollectorRegistry;
use Prometheus\MetricFamilySamples;
use Prometheus\RenderTextFormat;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Stub for HealthMetricsCollectorInterface that doesn't make real connections.
*/
final class HealthMetricsCollectorStub implements HealthMetricsCollectorInterface
{
private bool $collected = false;
public function collect(): void
{
$this->collected = true;
}
public function wasCollectCalled(): bool
{
return $this->collected;
}
}
/**
* @see Story 1.8 - T3.3: Expose /metrics endpoint in backend
*/
#[CoversClass(MetricsController::class)]
final class MetricsControllerTest extends TestCase
{
private function createController(
?CollectorRegistry $registry = null,
string $appEnv = 'dev',
): MetricsController {
$registry ??= $this->createMock(CollectorRegistry::class);
$registry->method('getMetricFamilySamples')->willReturn([]);
$healthMetrics = new HealthMetricsCollectorStub();
$messengerMetrics = $this->createMock(MessengerMetricsCollectorInterface::class);
return new MetricsController($registry, $healthMetrics, $messengerMetrics, $appEnv);
}
#[Test]
public function itReturnsMetricsWithCorrectContentType(): void
{
$controller = $this->createController();
$request = Request::create('/metrics');
$response = $controller($request);
self::assertSame(Response::HTTP_OK, $response->getStatusCode());
self::assertSame(RenderTextFormat::MIME_TYPE, $response->headers->get('Content-Type'));
}
#[Test]
public function itRendersMetricsFromRegistry(): void
{
$sample = new MetricFamilySamples([
'name' => 'test_counter',
'type' => 'counter',
'help' => 'A test counter',
'labelNames' => [],
'samples' => [
[
'name' => 'test_counter',
'labelNames' => [],
'labelValues' => [],
'value' => 42,
],
],
]);
$registry = $this->createMock(CollectorRegistry::class);
$registry->method('getMetricFamilySamples')->willReturn([$sample]);
$healthMetrics = new HealthMetricsCollectorStub();
$messengerMetrics = $this->createMock(MessengerMetricsCollectorInterface::class);
$controller = new MetricsController($registry, $healthMetrics, $messengerMetrics);
$request = Request::create('/metrics');
$response = $controller($request);
$content = $response->getContent();
self::assertStringContainsString('test_counter', $content);
self::assertStringContainsString('42', $content);
}
#[Test]
public function itReturnsEmptyResponseWhenNoMetrics(): void
{
$controller = $this->createController();
$request = Request::create('/metrics');
$response = $controller($request);
self::assertSame(Response::HTTP_OK, $response->getStatusCode());
self::assertIsString($response->getContent());
}
#[Test]
public function itAllowsInternalIpInProduction(): void
{
$controller = $this->createController(appEnv: 'prod');
$request = Request::create('/metrics', server: ['REMOTE_ADDR' => '172.18.0.5']);
$response = $controller($request);
self::assertSame(Response::HTTP_OK, $response->getStatusCode());
}
#[Test]
public function itBlocksExternalIpInProduction(): void
{
$registry = $this->createMock(CollectorRegistry::class);
$healthMetrics = new HealthMetricsCollectorStub();
$messengerMetrics = $this->createMock(MessengerMetricsCollectorInterface::class);
$controller = new MetricsController($registry, $healthMetrics, $messengerMetrics, 'prod');
$request = Request::create('/metrics', server: ['REMOTE_ADDR' => '8.8.8.8']);
$this->expectException(AccessDeniedHttpException::class);
$this->expectExceptionMessage('Metrics endpoint is restricted to internal networks.');
$controller($request);
}
#[Test]
public function itAllowsAnyIpInDev(): void
{
$controller = $this->createController(appEnv: 'dev');
$request = Request::create('/metrics', server: ['REMOTE_ADDR' => '8.8.8.8']);
$response = $controller($request);
self::assertSame(Response::HTTP_OK, $response->getStatusCode());
}
#[Test]
public function itAllowsLocalhostInProduction(): void
{
$controller = $this->createController(appEnv: 'prod');
$request = Request::create('/metrics', server: ['REMOTE_ADDR' => '127.0.0.1']);
$response = $controller($request);
self::assertSame(Response::HTTP_OK, $response->getStatusCode());
}
#[Test]
public function itCollectsHealthMetricsBeforeRendering(): void
{
$registry = $this->createMock(CollectorRegistry::class);
$registry->method('getMetricFamilySamples')->willReturn([]);
$healthMetrics = new HealthMetricsCollectorStub();
$messengerMetrics = $this->createMock(MessengerMetricsCollectorInterface::class);
$controller = new MetricsController($registry, $healthMetrics, $messengerMetrics);
$request = Request::create('/metrics');
$controller($request);
self::assertTrue($healthMetrics->wasCollectCalled());
}
}