requestsCounter = $this->createMock(Counter::class); $this->durationHistogram = $this->createMock(Histogram::class); $this->loginFailuresCounter = $this->createMock(Counter::class); $this->registry = $this->createMock(CollectorRegistry::class); $this->registry->method('getOrRegisterCounter') ->willReturnCallback(fn (string $ns, string $name) => match ($name) { 'http_requests_total' => $this->requestsCounter, 'login_failures_total' => $this->loginFailuresCounter, default => $this->createMock(Counter::class), }); $this->registry->method('getOrRegisterHistogram') ->willReturn($this->durationHistogram); $this->tenantContext = new TenantContext(); $this->collector = new MetricsCollector($this->registry, $this->tenantContext); } protected function tearDown(): void { $this->tenantContext->clear(); } #[Test] public function itRecordsRequestMetricsWithoutTenant(): void { $request = Request::create('/api/users', 'GET'); $request->attributes->set('_route', 'get_users'); $response = new Response('', 200); $kernel = $this->createMock(HttpKernelInterface::class); // Simulate request start $requestEvent = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $this->collector->onKernelRequest($requestEvent); // Expect metrics to be recorded with tenant_id="none" $this->requestsCounter->expects(self::once()) ->method('inc') ->with(['GET', 'get_users', '200', 'none']); $this->durationHistogram->expects(self::once()) ->method('observe') ->with( self::greaterThan(0), ['GET', 'get_users', 'none'], ); // Simulate request end $terminateEvent = new TerminateEvent($kernel, $request, $response); $this->collector->onKernelTerminate($terminateEvent); } #[Test] public function itRecordsRequestMetricsWithTenant(): void { $tenantConfig = new TenantConfig( tenantId: TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'), subdomain: 'ecole-alpha', databaseUrl: 'postgresql://test@localhost/test', ); $this->tenantContext->setCurrentTenant($tenantConfig); $request = Request::create('/api/users', 'POST'); $request->attributes->set('_route', 'create_user'); $response = new Response('', 201); $kernel = $this->createMock(HttpKernelInterface::class); $requestEvent = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $this->collector->onKernelRequest($requestEvent); $this->requestsCounter->expects(self::once()) ->method('inc') ->with(['POST', 'create_user', '201', 'ecole-alpha']); $terminateEvent = new TerminateEvent($kernel, $request, $response); $this->collector->onKernelTerminate($terminateEvent); } #[Test] public function itSkipsMetricsEndpoint(): void { $request = Request::create('/metrics', 'GET'); $request->attributes->set('_route', 'prometheus_metrics'); $response = new Response('', 200); $kernel = $this->createMock(HttpKernelInterface::class); $requestEvent = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $this->collector->onKernelRequest($requestEvent); // Should NOT record metrics for /metrics endpoint $this->requestsCounter->expects(self::never())->method('inc'); $this->durationHistogram->expects(self::never())->method('observe'); $terminateEvent = new TerminateEvent($kernel, $request, $response); $this->collector->onKernelTerminate($terminateEvent); } #[Test] public function itSkipsHealthEndpoint(): void { $request = Request::create('/health', 'GET'); $request->attributes->set('_route', 'health_check'); $response = new Response('', 200); $kernel = $this->createMock(HttpKernelInterface::class); $requestEvent = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $this->collector->onKernelRequest($requestEvent); $this->requestsCounter->expects(self::never())->method('inc'); $terminateEvent = new TerminateEvent($kernel, $request, $response); $this->collector->onKernelTerminate($terminateEvent); } #[Test] public function itRecordsLoginFailureWithoutTenant(): void { $this->loginFailuresCounter->expects(self::once()) ->method('inc') ->with(['none', 'invalid_credentials']); $this->collector->recordLoginFailure(); } #[Test] public function itRecordsLoginFailureWithTenant(): void { $tenantConfig = new TenantConfig( tenantId: TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'), subdomain: 'ecole-beta', databaseUrl: 'postgresql://test@localhost/test', ); $this->tenantContext->setCurrentTenant($tenantConfig); $this->loginFailuresCounter->expects(self::once()) ->method('inc') ->with(['ecole-beta', 'rate_limited']); $this->collector->recordLoginFailure('rate_limited'); } #[Test] public function itIgnoresSubrequests(): void { $request = Request::create('/api/test', 'GET'); $kernel = $this->createMock(HttpKernelInterface::class); // Subrequest should be ignored $requestEvent = new RequestEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST); $this->collector->onKernelRequest($requestEvent); $this->requestsCounter->expects(self::never())->method('inc'); $response = new Response('', 200); $terminateEvent = new TerminateEvent($kernel, $request, $response); $this->collector->onKernelTerminate($terminateEvent); } }