store = $this->createMock(FirewallStore::class); $this->tenantContext = $this->createMock(TenantContextInterface::class); $this->events = $this->createMock(EventDispatcherInterface::class); $this->currentTenant = 'tenant-a'; $this->tenantContext->method('identifier')->willReturnCallback( fn(): string => $this->currentTenant ); $this->tenantContext->method('configuration')->willReturn(null); $this->service = new FirewallService($this->store, $this->tenantContext, $this->events); } #[TestDox('System blocks cannot be overridden by tenant allows')] public function testSystemBlockPrecedence(): void { $systemBlock = $this->rule( 'system-block', FirewallRuleObject::SCOPE_SYSTEM, FirewallRuleObject::ACTION_BLOCK, null ); $tenantAllow = $this->rule( 'tenant-allow', FirewallRuleObject::SCOPE_TENANT, FirewallRuleObject::ACTION_ALLOW, 'tenant-a' ); $this->store->expects($this->once()) ->method('listApplicableRules') ->with('tenant-a') ->willReturn([$tenantAllow, $systemBlock]); $this->events->expects($this->once())->method('dispatch'); $result = $this->service->analyze('203.0.113.10'); self::assertTrue($result->isBlocked()); self::assertSame('system-block', $result->ruleId); } #[TestDox('Tenant blocks override system allows')] public function testTenantBlockPrecedence(): void { $systemAllow = $this->rule( 'system-allow', FirewallRuleObject::SCOPE_SYSTEM, FirewallRuleObject::ACTION_ALLOW, null ); $tenantBlock = $this->rule( 'tenant-block', FirewallRuleObject::SCOPE_TENANT, FirewallRuleObject::ACTION_BLOCK, 'tenant-a' ); $this->store->method('listApplicableRules')->willReturn([$systemAllow, $tenantBlock]); $this->events->expects($this->once())->method('dispatch'); $result = $this->service->analyze('203.0.113.10'); self::assertTrue($result->isBlocked()); self::assertSame('tenant-block', $result->ruleId); } #[TestDox('Rule caches are isolated by tenant')] public function testTenantCacheIsolation(): void { $this->store->expects($this->exactly(2)) ->method('listApplicableRules') ->willReturnCallback(static fn(string $tenantId): array => [ (new FirewallRuleObject()) ->setId($tenantId) ->setScope(FirewallRuleObject::SCOPE_TENANT) ->setTenantId($tenantId) ->setType(FirewallRuleObject::TYPE_IP) ->setAction(FirewallRuleObject::ACTION_BLOCK) ->setValue('203.0.113.10'), ]); self::assertSame('tenant-a', $this->service->analyze('203.0.113.10')->ruleId); $this->currentTenant = 'tenant-b'; self::assertSame('tenant-b', $this->service->analyze('203.0.113.10')->ruleId); } #[TestDox('New IP blocks are explicitly tenant-scoped')] public function testIpBlockScope(): void { $this->store->method('findExactIpRule')->willReturn(null); $this->store->expects($this->once()) ->method('depositRule') ->with(self::callback(static function (FirewallRuleObject $rule): bool { return $rule->getScope() === FirewallRuleObject::SCOPE_TENANT && $rule->getTenantId() === 'tenant-a'; })) ->willReturnArgument(0); $rule = $this->service->blockIp('203.0.113.10'); self::assertSame(FirewallRuleObject::SCOPE_TENANT, $rule->getScope()); self::assertSame('tenant-a', $rule->getTenantId()); } private function rule( string $id, string $scope, string $action, ?string $tenantId ): FirewallRuleObject { return (new FirewallRuleObject()) ->setId($id) ->setScope($scope) ->setTenantId($tenantId) ->setType(FirewallRuleObject::TYPE_IP) ->setAction($action) ->setValue('203.0.113.10') ->setReason($id); } }