d919b70a2e
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
96 lines
4.0 KiB
PHP
96 lines
4.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace KTXT\Unit\Service;
|
|
|
|
use KTXC\Context\IdentityContextInterface;
|
|
use KTXC\Context\TenantContextInterface;
|
|
use KTXC\Models\Firewall\FirewallLogObject;
|
|
use KTXC\Service\FirewallLogService;
|
|
use KTXC\Service\SystemFirewallLogService;
|
|
use KTXC\Service\TenantFirewallLogService;
|
|
use KTXC\Stores\FirewallStore;
|
|
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
|
|
use PHPUnit\Framework\Attributes\TestDox;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
#[AllowMockObjectsWithoutExpectations]
|
|
final class FirewallLogServicesTest extends TestCase
|
|
{
|
|
#[TestDox('Tenant log reads derive tenant ownership and validated filters')]
|
|
public function testTenantQuery(): void
|
|
{
|
|
$store = $this->createMock(FirewallStore::class);
|
|
$tenant = $this->createStub(TenantContextInterface::class);
|
|
$tenant->method('requireIdentifier')->willReturn('tenant-a');
|
|
$identity = $this->createStub(IdentityContextInterface::class);
|
|
$identity->method('hasPermission')->willReturn(true);
|
|
$store->expects(self::once())
|
|
->method('queryTenantLogs')
|
|
->with(
|
|
'tenant-a',
|
|
self::callback(static fn(array $filters): bool =>
|
|
$filters['eventType'] === FirewallLogObject::EVENT_AUTH_FAILURE
|
|
&& $filters['from'] instanceof \DateTimeImmutable
|
|
),
|
|
25,
|
|
10
|
|
)
|
|
->willReturn(['items' => [], 'total' => 0, 'limit' => 25, 'offset' => 10]);
|
|
$service = new TenantFirewallLogService(new FirewallLogService($store), $tenant, $identity);
|
|
|
|
self::assertSame(0, $service->query([
|
|
'eventType' => FirewallLogObject::EVENT_AUTH_FAILURE,
|
|
'from' => '2026-08-01T00:00:00+00:00',
|
|
], 25, 10)['total']);
|
|
}
|
|
|
|
#[TestDox('System log reads may filter one tenant without changing ownership')]
|
|
public function testSystemQuery(): void
|
|
{
|
|
$store = $this->createMock(FirewallStore::class);
|
|
$identity = $this->createStub(IdentityContextInterface::class);
|
|
$identity->method('hasPermission')->willReturn(true);
|
|
$store->expects(self::once())
|
|
->method('querySystemLogs')
|
|
->with('tenant-a', self::isArray(), 50, 0)
|
|
->willReturn(['items' => [], 'total' => 0, 'limit' => 50, 'offset' => 0]);
|
|
$service = new SystemFirewallLogService(new FirewallLogService($store), $identity);
|
|
|
|
self::assertSame(0, $service->query('tenant-a', [], 50, 0)['total']);
|
|
}
|
|
|
|
#[TestDox('Log queries reject invalid filters before database access')]
|
|
public function testValidation(): void
|
|
{
|
|
$store = $this->createMock(FirewallStore::class);
|
|
$store->expects(self::never())->method('queryTenantLogs');
|
|
$query = new FirewallLogService($store);
|
|
$this->expectException(\InvalidArgumentException::class);
|
|
|
|
$query->tenant('tenant-a', ['ipAddress' => 'not-an-ip'], 50, 0);
|
|
}
|
|
|
|
#[TestDox('Log boundaries enforce dedicated read permissions')]
|
|
public function testPermissions(): void
|
|
{
|
|
$store = $this->createMock(FirewallStore::class);
|
|
$identity = $this->createStub(IdentityContextInterface::class);
|
|
$identity->method('hasPermission')->willReturn(false);
|
|
$tenant = $this->createStub(TenantContextInterface::class);
|
|
$store->expects(self::never())->method('queryTenantLogs');
|
|
$store->expects(self::never())->method('querySystemLogs');
|
|
|
|
try {
|
|
(new TenantFirewallLogService(new FirewallLogService($store), $tenant, $identity))->query([]);
|
|
self::fail('Tenant log read should be rejected.');
|
|
} catch (\RuntimeException $error) {
|
|
self::assertStringContainsString(TenantFirewallLogService::PERMISSION_READ, $error->getMessage());
|
|
}
|
|
|
|
$this->expectExceptionMessage(SystemFirewallLogService::PERMISSION_READ);
|
|
(new SystemFirewallLogService(new FirewallLogService($store), $identity))->query(null, []);
|
|
}
|
|
}
|