store = $this->createMock(FirewallStore::class); $this->identity = $this->createMock(IdentityContextInterface::class); $this->tenant = $this->createMock(TenantContextInterface::class); $events = $this->createMock(EventDispatcherInterface::class); $this->manager = new FirewallRuleManager( $this->store, new FirewallRuleCache($this->store), $events ); } #[TestDox('Tenant and system boundaries require their dedicated permissions')] public function testPermissions(): void { $this->identity->method('hasPermission')->willReturn(false); $this->store->expects($this->never())->method('depositRule'); try { $this->tenantService()->blockIp('203.0.113.10'); self::fail('Tenant operation should have been rejected.'); } catch (\RuntimeException $error) { self::assertStringContainsString(TenantFirewallRuleService::PERMISSION_MANAGE, $error->getMessage()); } $this->expectException(\RuntimeException::class); $this->expectExceptionMessage(SystemFirewallRuleService::PERMISSION_MANAGE); $this->systemService()->blockIp('203.0.113.10'); } #[TestDox('Read operations require their scope-specific permission')] public function testReadPermissions(): void { $this->identity->method('hasPermission')->willReturn(false); $this->store->expects(self::never())->method('queryRules'); try { $this->tenantService()->queryRules(); self::fail('Tenant read should have been rejected.'); } catch (\RuntimeException $error) { self::assertStringContainsString(TenantFirewallRuleService::PERMISSION_READ, $error->getMessage()); } $this->expectExceptionMessage(SystemFirewallRuleService::PERMISSION_READ); $this->systemService()->queryRules(); } #[TestDox('Tenant reads derive ownership and effective policy from context')] public function testTenantReads(): void { $this->allow(TenantFirewallRuleService::PERMISSION_READ); $this->tenant->method('requireIdentifier')->willReturn('tenant-a'); $this->store->expects(self::once()) ->method('queryRules') ->with(FirewallRuleObject::SCOPE_TENANT, 'tenant-a', 'active', null, null, 50, 0) ->willReturn(['items' => [], 'total' => 0, 'limit' => 50, 'offset' => 0]); $this->store->method('listSystemRules')->willReturn([]); $this->store->method('listRules')->with('tenant-a')->willReturn([]); self::assertSame(0, $this->tenantService()->queryRules()['total']); self::assertSame([], $this->tenantService()->effectivePolicy()['system']); } #[TestDox('Tenant management derives scope from tenant context')] public function testTenantScope(): void { $this->allow(TenantFirewallRuleService::PERMISSION_MANAGE); $this->tenant->method('requireIdentifier')->willReturn('tenant-a'); $this->identity->method('identifier')->willReturn('admin-a'); $this->store->method('findExactIpRule')->willReturn(null); $this->store->expects($this->once()) ->method('depositRule') ->with(self::callback(static fn(FirewallRuleObject $rule): bool => $rule->isTenantScoped() && $rule->getTenantId() === 'tenant-a' )) ->willReturnCallback(static fn(FirewallRuleObject $rule): FirewallRuleObject => $rule->setId('tenant-rule') ); $this->tenantService()->blockIp('203.0.113.10'); } #[TestDox('System management always delegates with system scope')] public function testSystemScope(): void { $this->allow(SystemFirewallRuleService::PERMISSION_MANAGE); $this->store->method('findExactIpRule')->willReturn(null); $this->store->expects($this->once()) ->method('depositRule') ->with(self::callback(static fn(FirewallRuleObject $rule): bool => $rule->isSystemScoped() && $rule->getTenantId() === null )) ->willReturnCallback(static fn(FirewallRuleObject $rule): FirewallRuleObject => $rule->setId('system-rule') ); $this->systemService()->blockIp('203.0.113.10'); } #[TestDox('Generic creation remains behind tenant and system management permissions')] public function testGenericCreationPermissions(): void { $this->identity->method('hasPermission')->willReturn(false); $this->store->expects(self::never())->method('depositRule'); try { $this->tenantService()->createRule('ip', 'block', '203.0.113.10', 'Abuse'); self::fail('Tenant creation should have been rejected.'); } catch (\RuntimeException $error) { self::assertStringContainsString(TenantFirewallRuleService::PERMISSION_MANAGE, $error->getMessage()); } $this->expectExceptionMessage(SystemFirewallRuleService::PERMISSION_MANAGE); $this->systemService()->createRule('ip', 'block', '203.0.113.10', 'Abuse'); } private function allow(string $permission): void { $this->identity->method('hasPermission')->with($permission)->willReturn(true); } private function tenantService(): TenantFirewallRuleService { return new TenantFirewallRuleService($this->manager, $this->tenant, $this->identity); } private function systemService(): SystemFirewallRuleService { return new SystemFirewallRuleService($this->manager, $this->identity); } }