b06c18d38e
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
157 lines
6.2 KiB
PHP
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);
|
|
}
|
|
}
|