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