Files
server/tests/php/Unit/Service/FirewallRuleServicesTest.php
T
2026-08-03 22:27:20 -04:00

157 lines
6.2 KiB
PHP

<?php
declare(strict_types=1);
namespace KTXT\Unit\Service;
use KTXC\Context\IdentityContextInterface;
use KTXC\Context\TenantContextInterface;
use KTXC\Models\Firewall\FirewallRuleObject;
use KTXC\Service\FirewallRuleCache;
use KTXC\Service\FirewallRuleManager;
use KTXC\Service\SystemFirewallRuleService;
use KTXC\Service\TenantFirewallRuleService;
use KTXC\Stores\FirewallStore;
use KTXF\Event\EventDispatcherInterface;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
#[AllowMockObjectsWithoutExpectations]
class FirewallRuleServicesTest extends TestCase
{
private FirewallStore&MockObject $store;
private IdentityContextInterface&MockObject $identity;
private TenantContextInterface&MockObject $tenant;
private FirewallRuleManager $manager;
protected function setUp(): void
{
$this->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'
))
->willReturnArgument(0);
$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
))
->willReturnArgument(0);
$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);
}
}