Files
server/tests/php/Unit/Controllers/FirewallControllerTest.php
T
2026-08-03 22:44:34 -04:00

203 lines
8.7 KiB
PHP

<?php
declare(strict_types=1);
namespace KTXT\Unit\Controllers;
use KTXC\Context\IdentityContextInterface;
use KTXC\Context\TenantContextInterface;
use KTXC\Controllers\FirewallController;
use KTXC\Http\Request\Request;
use KTXC\Service\FirewallRuleCache;
use KTXC\Service\FirewallRuleManager;
use KTXC\Service\FirewallSettingsService;
use KTXC\Service\FirewallStatusService;
use KTXC\Service\FirewallLogService;
use KTXC\Service\SystemFirewallLogService;
use KTXC\Service\SystemFirewallRuleService;
use KTXC\Service\SystemFirewallStatusService;
use KTXC\Service\TenantFirewallLogService;
use KTXC\Service\TenantFirewallRuleService;
use KTXC\Service\TenantFirewallStatusService;
use KTXC\Service\TenantService;
use KTXC\Stores\FirewallStore;
use KTXF\Event\EventDispatcherInterface;
use KTXF\Routing\Attributes\AuthenticatedRoute;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase;
#[AllowMockObjectsWithoutExpectations]
final class FirewallControllerTest extends TestCase
{
private FirewallStore $store;
private FirewallController $controller;
protected function setUp(): void
{
$this->store = $this->createMock(FirewallStore::class);
$tenant = $this->createMock(TenantContextInterface::class);
$tenant->method('requireIdentifier')->willReturn('tenant-a');
$identity = $this->createMock(IdentityContextInterface::class);
$identity->method('hasPermission')->willReturn(true);
$manager = new FirewallRuleManager(
$this->store,
new FirewallRuleCache($this->store),
$this->createStub(EventDispatcherInterface::class)
);
$settings = new FirewallSettingsService(
$this->createStub(TenantService::class),
$this->createStub(EventDispatcherInterface::class)
);
$this->controller = new FirewallController(
new TenantFirewallRuleService($manager, $tenant, $identity),
new SystemFirewallRuleService($manager, $identity),
new TenantFirewallLogService(new FirewallLogService($this->store), $tenant, $identity),
new SystemFirewallLogService(new FirewallLogService($this->store), $identity),
new TenantFirewallStatusService(new FirewallStatusService($this->store), $tenant, $identity, $settings),
new SystemFirewallStatusService(new FirewallStatusService($this->store), $identity, $settings)
);
}
#[TestDox('Tenant rule endpoint returns bounded paginated results')]
public function testTenantRules(): void
{
$this->store->expects(self::once())
->method('queryRules')
->with('tenant', 'tenant-a', 'active', null, null, 25, 10)
->willReturn(['items' => [], 'total' => 0, 'limit' => 25, 'offset' => 10]);
$response = $this->controller->tenantRules(limit: '25', offset: '10');
$data = json_decode($response->getContent(), true, flags: JSON_THROW_ON_ERROR);
self::assertSame(200, $response->getStatusCode());
self::assertSame(25, $data['limit']);
self::assertSame(10, $data['offset']);
}
#[TestDox('Rule endpoints reject malformed and excessive pagination')]
public function testPaginationValidation(): void
{
$this->store->expects(self::never())->method('queryRules');
self::assertSame(400, $this->controller->tenantRules(limit: 'invalid')->getStatusCode());
self::assertSame(400, $this->controller->systemRules(limit: '101')->getStatusCode());
}
#[TestDox('Metric endpoints return scoped counts and stable validation errors')]
public function testMetrics(): void
{
$this->store->method('countBlockedRequests')->willReturn(4);
$response = $this->controller->tenantMetrics();
$data = json_decode($response->getContent(), true, flags: JSON_THROW_ON_ERROR);
self::assertSame(4, $data['blockedRequests']);
self::assertSame(400, $this->controller->systemMetrics(since: 'not-a-date')->getStatusCode());
}
#[TestDox('Current-IP blocks return a structured confirmation conflict')]
public function testCurrentIpConflict(): void
{
$this->store->expects(self::never())->method('depositRule');
$response = $this->controller->createTenantRule(
new Request(server: ['REMOTE_ADDR' => '203.0.113.10']),
'ip',
'block',
'203.0.113.10',
'Suspected abuse'
);
$data = json_decode($response->getContent(), true, flags: JSON_THROW_ON_ERROR);
self::assertSame(409, $response->getStatusCode());
self::assertSame('current_ip_confirmation_required', $data['error']['code']);
}
#[TestDox('Invalid manual rules return a stable validation response')]
public function testMutationValidation(): void
{
$response = $this->controller->createSystemRule(
new Request(server: ['REMOTE_ADDR' => '203.0.113.10']),
'device',
'allow',
'device-123',
'Trusted device'
);
$data = json_decode($response->getContent(), true, flags: JSON_THROW_ON_ERROR);
self::assertSame(400, $response->getStatusCode());
self::assertSame('invalid_firewall_rule', $data['error']['code']);
}
#[TestDox('Re-enabling a current-IP block requires explicit confirmation')]
public function testEnableSafeguard(): void
{
$rule = (new \KTXC\Models\Firewall\FirewallRuleObject())
->setId('rule-123')
->setScope('tenant')
->setTenantId('tenant-a')
->setType('ip')
->setAction('block')
->setValue('203.0.113.10')
->setEnabled(false);
$this->store->method('fetchRule')->willReturn($rule);
$response = $this->controller->updateTenantRule(
new Request(server: ['REMOTE_ADDR' => '203.0.113.10']),
'rule-123',
'enable',
'Threat returned'
);
$data = json_decode($response->getContent(), true, flags: JSON_THROW_ON_ERROR);
self::assertSame(409, $response->getStatusCode());
self::assertSame('current_ip_confirmation_required', $data['error']['code']);
}
#[TestDox('Lifecycle endpoints return stable validation and not-found responses')]
public function testLifecycleResponses(): void
{
$request = new Request(server: ['REMOTE_ADDR' => '203.0.113.10']);
self::assertSame(400, $this->controller->updateSystemRule(
$request, 'rule-123', 'extend', 'More time required'
)->getStatusCode());
self::assertSame(404, $this->controller->deleteTenantRule(
'missing-rule', 'No longer required'
)->getStatusCode());
}
#[TestDox('Every rule endpoint declares its scope-specific read permission')]
public function testRoutePermissions(): void
{
$expected = [
'tenantRules' => TenantFirewallRuleService::PERMISSION_READ,
'tenantRule' => TenantFirewallRuleService::PERMISSION_READ,
'effectivePolicy' => TenantFirewallRuleService::PERMISSION_READ,
'systemRules' => SystemFirewallRuleService::PERMISSION_READ,
'systemRule' => SystemFirewallRuleService::PERMISSION_READ,
'tenantLogs' => TenantFirewallLogService::PERMISSION_READ,
'systemLogs' => SystemFirewallLogService::PERMISSION_READ,
'tenantMetrics' => TenantFirewallLogService::PERMISSION_READ,
'tenantConfiguration' => TenantFirewallStatusService::PERMISSION_SETTINGS_READ,
'systemMetrics' => SystemFirewallLogService::PERMISSION_READ,
'maintenanceStatus' => SystemFirewallStatusService::PERMISSION_MAINTENANCE_READ,
'updateTenantConfiguration' => TenantFirewallStatusService::PERMISSION_SETTINGS_MANAGE,
'updateSystemTenantConfiguration' => SystemFirewallStatusService::PERMISSION_SETTINGS_MANAGE,
'createTenantRule' => TenantFirewallRuleService::PERMISSION_MANAGE,
'createSystemRule' => SystemFirewallRuleService::PERMISSION_MANAGE,
'updateTenantRule' => TenantFirewallRuleService::PERMISSION_MANAGE,
'updateSystemRule' => SystemFirewallRuleService::PERMISSION_MANAGE,
'deleteTenantRule' => TenantFirewallRuleService::PERMISSION_MANAGE,
'deleteSystemRule' => SystemFirewallRuleService::PERMISSION_MANAGE,
];
foreach ($expected as $method => $permission) {
$attributes = (new \ReflectionMethod(FirewallController::class, $method))
->getAttributes(AuthenticatedRoute::class);
self::assertCount(1, $attributes);
self::assertSame([$permission], $attributes[0]->newInstance()->permissions);
}
}
}