createMock(FirewallStore::class); $tenant = $this->createStub(TenantContextInterface::class); $tenant->method('requireIdentifier')->willReturn('tenant-a'); $tenant->method('configuration')->willReturn( (new TenantConfiguration())->jsonDeserialize([ 'firewall' => ['enabled' => false, 'maxAuthFailures' => 8], ]) ); $identity = $this->createStub(IdentityContextInterface::class); $identity->method('hasPermission')->willReturn(true); $store->expects(self::once()) ->method('countBlockedRequests') ->with('tenant-a', self::isInstanceOf(\DateTimeImmutable::class)) ->willReturn(7); $service = new TenantFirewallStatusService( new FirewallStatusService($store), $tenant, $identity, $this->settings() ); self::assertSame(7, $service->metrics('2026-08-01T00:00:00+00:00')['blockedRequests']); self::assertFalse($service->configuration()['enabled']); self::assertSame(8, $service->configuration()['maxAuthFailures']); } #[TestDox('System metrics and maintenance status expose operational state')] public function testSystemStatus(): void { $store = $this->createMock(FirewallStore::class); $identity = $this->createStub(IdentityContextInterface::class); $identity->method('hasPermission')->willReturn(true); $store->expects(self::once()) ->method('countSystemBlockedRequests') ->with('tenant-a', null) ->willReturn(12); $store->method('maintenanceStatus')->willReturn(['status' => 'success']); $service = new SystemFirewallStatusService( new FirewallStatusService($store), $identity, $this->settings() ); self::assertSame(12, $service->metrics('tenant-a')['blockedRequests']); self::assertSame('success', $service->maintenanceStatus()['status']); } #[TestDox('Maintenance reports an explicit state before its first run')] public function testNeverRunStatus(): void { $store = $this->createStub(FirewallStore::class); self::assertSame('never_run', (new FirewallStatusService($store))->maintenanceStatus()['status']); } #[TestDox('Status reads require their dedicated permissions')] public function testPermissions(): void { $store = $this->createMock(FirewallStore::class); $identity = $this->createStub(IdentityContextInterface::class); $identity->method('hasPermission')->willReturn(false); $tenant = $this->createStub(TenantContextInterface::class); $store->expects(self::never())->method('countBlockedRequests'); $store->expects(self::never())->method('maintenanceStatus'); try { (new TenantFirewallStatusService( new FirewallStatusService($store), $tenant, $identity, $this->settings() ))->metrics(); self::fail('Tenant metrics should be rejected.'); } catch (\RuntimeException $error) { self::assertStringContainsString(TenantFirewallLogService::PERMISSION_READ, $error->getMessage()); } $this->expectExceptionMessage(SystemFirewallStatusService::PERMISSION_MAINTENANCE_READ); (new SystemFirewallStatusService( new FirewallStatusService($store), $identity, $this->settings() ))->maintenanceStatus(); } #[TestDox('Metrics reject invalid dates before querying storage')] public function testMetricValidation(): void { $store = $this->createMock(FirewallStore::class); $store->expects(self::never())->method('countSystemBlockedRequests'); $this->expectException(\InvalidArgumentException::class); (new FirewallStatusService($store))->systemMetrics(null, 'not-a-date'); } private function settings(): FirewallSettingsService { return new FirewallSettingsService( $this->createStub(TenantService::class), $this->createStub(EventDispatcherInterface::class) ); } }