createMock(FirewallStore::class); $tenant = $this->createStub(TenantContextInterface::class); $tenant->method('requireIdentifier')->willReturn('tenant-a'); $identity = $this->createStub(IdentityContextInterface::class); $identity->method('hasPermission')->willReturn(true); $store->expects(self::once()) ->method('queryTenantLogs') ->with( 'tenant-a', self::callback(static fn(array $filters): bool => $filters['eventType'] === FirewallLogObject::EVENT_AUTH_FAILURE && $filters['from'] instanceof \DateTimeImmutable ), 25, 10 ) ->willReturn(['items' => [], 'total' => 0, 'limit' => 25, 'offset' => 10]); $service = new TenantFirewallLogService(new FirewallLogService($store), $tenant, $identity); self::assertSame(0, $service->query([ 'eventType' => FirewallLogObject::EVENT_AUTH_FAILURE, 'from' => '2026-08-01T00:00:00+00:00', ], 25, 10)['total']); } #[TestDox('System log reads may filter one tenant without changing ownership')] public function testSystemQuery(): void { $store = $this->createMock(FirewallStore::class); $identity = $this->createStub(IdentityContextInterface::class); $identity->method('hasPermission')->willReturn(true); $store->expects(self::once()) ->method('querySystemLogs') ->with('tenant-a', self::isArray(), 50, 0) ->willReturn(['items' => [], 'total' => 0, 'limit' => 50, 'offset' => 0]); $service = new SystemFirewallLogService(new FirewallLogService($store), $identity); self::assertSame(0, $service->query('tenant-a', [], 50, 0)['total']); } #[TestDox('Log queries reject invalid filters before database access')] public function testValidation(): void { $store = $this->createMock(FirewallStore::class); $store->expects(self::never())->method('queryTenantLogs'); $query = new FirewallLogService($store); $this->expectException(\InvalidArgumentException::class); $query->tenant('tenant-a', ['ipAddress' => 'not-an-ip'], 50, 0); } #[TestDox('Log boundaries enforce dedicated read 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('queryTenantLogs'); $store->expects(self::never())->method('querySystemLogs'); try { (new TenantFirewallLogService(new FirewallLogService($store), $tenant, $identity))->query([]); self::fail('Tenant log read should be rejected.'); } catch (\RuntimeException $error) { self::assertStringContainsString(TenantFirewallLogService::PERMISSION_READ, $error->getMessage()); } $this->expectExceptionMessage(SystemFirewallLogService::PERMISSION_READ); (new SystemFirewallLogService(new FirewallLogService($store), $identity))->query(null, []); } }