dataStore = new DataStore($database); try { $this->dataStore->getDatabase()->drop(); $this->databaseAvailable = true; } catch (\MongoDB\Driver\Exception\Exception $error) { self::markTestSkipped('MongoDB is unavailable: '.$error->getMessage()); } $this->store = new FirewallStore($this->dataStore); } protected function tearDown(): void { if ($this->databaseAvailable) { $this->dataStore->getDatabase()->drop(); } } #[TestDox('Applicable rules contain system rules and only the requested tenant rules')] public function testApplicableScopes(): void { $tenantA = $this->rule('tenant-a', FirewallRuleObject::SCOPE_TENANT, 'tenant-a'); $tenantB = $this->rule('tenant-b', FirewallRuleObject::SCOPE_TENANT, 'tenant-b'); $system = $this->rule('system', FirewallRuleObject::SCOPE_SYSTEM); $expiredSystem = $this->rule('expired-system', FirewallRuleObject::SCOPE_SYSTEM) ->setExpiresAt(new \DateTimeImmutable('-1 minute')); $disabledSystem = $this->rule('disabled-system', FirewallRuleObject::SCOPE_SYSTEM) ->setEnabled(false); foreach ([$tenantA, $tenantB, $system, $expiredSystem, $disabledSystem] as $rule) { $this->store->depositRule($rule); } $rules = $this->store->listApplicableRules('tenant-a'); $reasons = array_map(static fn(FirewallRuleObject $rule): ?string => $rule->getReason(), $rules); sort($reasons); self::assertSame(['system', 'tenant-a'], $reasons); } #[TestDox('Tenant rule listings cannot expose system or other tenant rules')] public function testTenantListingIsolation(): void { $this->store->depositRule($this->rule('tenant-a', FirewallRuleObject::SCOPE_TENANT, 'tenant-a')); $this->store->depositRule($this->rule('tenant-b', FirewallRuleObject::SCOPE_TENANT, 'tenant-b')); $this->store->depositRule($this->rule('system', FirewallRuleObject::SCOPE_SYSTEM)); $rules = $this->store->listRules('tenant-a'); self::assertCount(1, $rules); self::assertSame('tenant-a', $rules[0]->getReason()); self::assertSame(FirewallRuleObject::SCOPE_TENANT, $rules[0]->getScope()); } #[TestDox('Expired exact IP rules do not suppress a replacement block')] public function testExpiredBlockReplacement(): void { $expired = $this->rule('expired', FirewallRuleObject::SCOPE_TENANT, 'tenant-a') ->setExpiresAt(new \DateTimeImmutable('-1 minute')); $this->store->depositRule($expired); self::assertNull($this->store->findExactIpRule( 'tenant-a', '203.0.113.10', FirewallRuleObject::ACTION_BLOCK )); $tenantContext = $this->createMock(TenantContextInterface::class); $tenantContext->method('identifier')->willReturn('tenant-a'); $tenantContext->method('configuration')->willReturn(null); $events = $this->createMock(EventDispatcherInterface::class); $service = new FirewallService($this->store, $tenantContext, $events); $replacement = $service->blockIp('203.0.113.10', durationSeconds: 300); self::assertFalse($replacement->isExpired()); self::assertNotSame($expired->getId(), $replacement->getId()); self::assertSame( $replacement->getId(), $this->store->findExactIpRule( 'tenant-a', '203.0.113.10', FirewallRuleObject::ACTION_BLOCK )?->getId() ); self::assertCount(2, $this->store->listRules('tenant-a', false)); } #[TestDox('Exact IP lookups respect tenant and system scope')] public function testExactLookupScope(): void { $tenant = $this->rule('tenant', FirewallRuleObject::SCOPE_TENANT, 'tenant-a'); $system = $this->rule('system', FirewallRuleObject::SCOPE_SYSTEM); $this->store->depositRule($tenant); $this->store->depositRule($system); self::assertSame( $tenant->getId(), $this->store->findExactIpRule( 'tenant-a', '203.0.113.10', FirewallRuleObject::ACTION_BLOCK )?->getId() ); self::assertSame( $system->getId(), $this->store->findExactIpRule( null, '203.0.113.10', FirewallRuleObject::ACTION_BLOCK, FirewallRuleObject::SCOPE_SYSTEM )?->getId() ); self::assertNull($this->store->findExactIpRule( 'tenant-b', '203.0.113.10', FirewallRuleObject::ACTION_BLOCK )); } private function rule( string $reason, string $scope, ?string $tenantId = null ): FirewallRuleObject { return (new FirewallRuleObject()) ->setScope($scope) ->setTenantId($tenantId) ->setType(FirewallRuleObject::TYPE_IP) ->setAction(FirewallRuleObject::ACTION_BLOCK) ->setValue('203.0.113.10') ->setReason($reason) ->setCreatedAt(new \DateTimeImmutable()) ->setEnabled(true); } }