Files
server/tests/php/Integration/Stores/FirewallStoreTest.php
T
2026-07-30 22:11:27 -04:00

165 lines
6.0 KiB
PHP

<?php
declare(strict_types=1);
namespace KTXT\Integration\Stores;
use KTXC\Context\TenantContextInterface;
use KTXC\Db\DataStore;
use KTXC\Models\Firewall\FirewallRuleObject;
use KTXC\Service\FirewallService;
use KTXC\Stores\FirewallStore;
use KTXF\Event\EventDispatcherInterface;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase;
#[AllowMockObjectsWithoutExpectations]
class FirewallStoreTest extends TestCase
{
private DataStore $dataStore;
private FirewallStore $store;
private bool $databaseAvailable = false;
protected function setUp(): void
{
$system = require dirname(__DIR__, 4).'/config/system.php';
$database = $system['database'];
$database['database'] = sprintf('ktrix_firewall_test_%d', getmypid());
$this->dataStore = new DataStore($database);
try {
$this->dataStore->getDatabase()->drop();
$this->databaseAvailable = true;
} catch (\MongoDB\Driver\Exception\Exception $error) {
self::markTestSkipped('MongoDB is unavailable: '.$error->getMessage());
}
$this->store = new FirewallStore($this->dataStore);
}
protected function tearDown(): void
{
if ($this->databaseAvailable) {
$this->dataStore->getDatabase()->drop();
}
}
#[TestDox('Applicable rules contain system rules and only the requested tenant rules')]
public function testApplicableScopes(): void
{
$tenantA = $this->rule('tenant-a', FirewallRuleObject::SCOPE_TENANT, 'tenant-a');
$tenantB = $this->rule('tenant-b', FirewallRuleObject::SCOPE_TENANT, 'tenant-b');
$system = $this->rule('system', FirewallRuleObject::SCOPE_SYSTEM);
$expiredSystem = $this->rule('expired-system', FirewallRuleObject::SCOPE_SYSTEM)
->setExpiresAt(new \DateTimeImmutable('-1 minute'));
$disabledSystem = $this->rule('disabled-system', FirewallRuleObject::SCOPE_SYSTEM)
->setEnabled(false);
foreach ([$tenantA, $tenantB, $system, $expiredSystem, $disabledSystem] as $rule) {
$this->store->depositRule($rule);
}
$rules = $this->store->listApplicableRules('tenant-a');
$reasons = array_map(static fn(FirewallRuleObject $rule): ?string => $rule->getReason(), $rules);
sort($reasons);
self::assertSame(['system', 'tenant-a'], $reasons);
}
#[TestDox('Tenant rule listings cannot expose system or other tenant rules')]
public function testTenantListingIsolation(): void
{
$this->store->depositRule($this->rule('tenant-a', FirewallRuleObject::SCOPE_TENANT, 'tenant-a'));
$this->store->depositRule($this->rule('tenant-b', FirewallRuleObject::SCOPE_TENANT, 'tenant-b'));
$this->store->depositRule($this->rule('system', FirewallRuleObject::SCOPE_SYSTEM));
$rules = $this->store->listRules('tenant-a');
self::assertCount(1, $rules);
self::assertSame('tenant-a', $rules[0]->getReason());
self::assertSame(FirewallRuleObject::SCOPE_TENANT, $rules[0]->getScope());
}
#[TestDox('Expired exact IP rules do not suppress a replacement block')]
public function testExpiredBlockReplacement(): void
{
$expired = $this->rule('expired', FirewallRuleObject::SCOPE_TENANT, 'tenant-a')
->setExpiresAt(new \DateTimeImmutable('-1 minute'));
$this->store->depositRule($expired);
self::assertNull($this->store->findExactIpRule(
'tenant-a',
'203.0.113.10',
FirewallRuleObject::ACTION_BLOCK
));
$tenantContext = $this->createMock(TenantContextInterface::class);
$tenantContext->method('identifier')->willReturn('tenant-a');
$tenantContext->method('configuration')->willReturn(null);
$events = $this->createMock(EventDispatcherInterface::class);
$service = new FirewallService($this->store, $tenantContext, $events);
$replacement = $service->blockIp('203.0.113.10', durationSeconds: 300);
self::assertFalse($replacement->isExpired());
self::assertNotSame($expired->getId(), $replacement->getId());
self::assertSame(
$replacement->getId(),
$this->store->findExactIpRule(
'tenant-a',
'203.0.113.10',
FirewallRuleObject::ACTION_BLOCK
)?->getId()
);
self::assertCount(2, $this->store->listRules('tenant-a', false));
}
#[TestDox('Exact IP lookups respect tenant and system scope')]
public function testExactLookupScope(): void
{
$tenant = $this->rule('tenant', FirewallRuleObject::SCOPE_TENANT, 'tenant-a');
$system = $this->rule('system', FirewallRuleObject::SCOPE_SYSTEM);
$this->store->depositRule($tenant);
$this->store->depositRule($system);
self::assertSame(
$tenant->getId(),
$this->store->findExactIpRule(
'tenant-a',
'203.0.113.10',
FirewallRuleObject::ACTION_BLOCK
)?->getId()
);
self::assertSame(
$system->getId(),
$this->store->findExactIpRule(
null,
'203.0.113.10',
FirewallRuleObject::ACTION_BLOCK,
FirewallRuleObject::SCOPE_SYSTEM
)?->getId()
);
self::assertNull($this->store->findExactIpRule(
'tenant-b',
'203.0.113.10',
FirewallRuleObject::ACTION_BLOCK
));
}
private function rule(
string $reason,
string $scope,
?string $tenantId = null
): FirewallRuleObject {
return (new FirewallRuleObject())
->setScope($scope)
->setTenantId($tenantId)
->setType(FirewallRuleObject::TYPE_IP)
->setAction(FirewallRuleObject::ACTION_BLOCK)
->setValue('203.0.113.10')
->setReason($reason)
->setCreatedAt(new \DateTimeImmutable())
->setEnabled(true);
}
}