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()); } }