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.
178 lines
5.9 KiB
PHP
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());
|
|
}
|
|
}
|