feat(firewall): add audited configuration management
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXT\Unit\Service;
|
||||
|
||||
use KTXC\Models\Tenant\TenantConfiguration;
|
||||
use KTXC\Models\Tenant\TenantObject;
|
||||
use KTXC\Service\FirewallSettingsService;
|
||||
use KTXC\Service\TenantService;
|
||||
use KTXF\Event\EventDispatcherInterface;
|
||||
use KTXF\Event\SecurityEvent;
|
||||
use PHPUnit\Framework\Attributes\TestDox;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class FirewallSettingsServiceTest extends TestCase
|
||||
{
|
||||
#[TestDox('Settings updates preserve other tenant configuration and emit complete audit context')]
|
||||
public function testUpdate(): void
|
||||
{
|
||||
$tenant = (new TenantObject())
|
||||
->setId('507f1f77bcf86cd799439011')
|
||||
->setIdentifier('tenant-a')
|
||||
->setConfiguration((new TenantConfiguration())->jsonDeserialize([
|
||||
'firewall' => ['enabled' => true, 'maxAuthFailures' => 5],
|
||||
'security' => ['code' => 'preserved'],
|
||||
]));
|
||||
$tenants = $this->createMock(TenantService::class);
|
||||
$tenants->method('fetchById')->with('tenant-a')->willReturn($tenant);
|
||||
$tenants->expects(self::once())
|
||||
->method('deposit')
|
||||
->with(self::callback(static fn(TenantObject $updated): bool =>
|
||||
$updated->getConfiguration()->firewall()->maxAuthFailures() === 8
|
||||
&& $updated->getConfiguration()->security()->jsonSerialize()['code'] === 'preserved'
|
||||
))
|
||||
->willReturnArgument(0);
|
||||
$events = $this->createMock(EventDispatcherInterface::class);
|
||||
$events->expects(self::once())
|
||||
->method('dispatch')
|
||||
->with(self::callback(static fn(SecurityEvent $event): bool =>
|
||||
$event->getName() === SecurityEvent::FIREWALL_SETTINGS_UPDATED
|
||||
&& $event->getTenantId() === 'tenant-a'
|
||||
&& $event->getIdentityId() === 'admin-a'
|
||||
&& $event->get('changeReason') === 'Tighten authentication controls'
|
||||
&& $event->get('previous')['maxAuthFailures'] === 5
|
||||
&& $event->get('current')['maxAuthFailures'] === 8
|
||||
));
|
||||
|
||||
$result = (new FirewallSettingsService($tenants, $events))->update(
|
||||
'tenant-a', false, 8, 600, 7200, 'Tighten authentication controls', 'admin-a'
|
||||
);
|
||||
|
||||
self::assertSame([
|
||||
'enabled' => false,
|
||||
'maxAuthFailures' => 8,
|
||||
'authFailureWindow' => 600,
|
||||
'autoBlockDuration' => 7200,
|
||||
], $result);
|
||||
}
|
||||
|
||||
#[TestDox('Invalid settings are rejected before tenant reads or persistence')]
|
||||
public function testValidation(): void
|
||||
{
|
||||
$tenants = $this->createMock(TenantService::class);
|
||||
$tenants->expects(self::never())->method('fetchById');
|
||||
$tenants->expects(self::never())->method('deposit');
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
(new FirewallSettingsService(
|
||||
$tenants,
|
||||
$this->createStub(EventDispatcherInterface::class)
|
||||
))->update('tenant-a', true, 0, 300, 3600, 'Invalid threshold', 'admin-a');
|
||||
}
|
||||
|
||||
#[TestDox('Unknown tenants return no configuration without emitting audit events')]
|
||||
public function testMissingTenant(): void
|
||||
{
|
||||
$tenants = $this->createStub(TenantService::class);
|
||||
$events = $this->createMock(EventDispatcherInterface::class);
|
||||
$events->expects(self::never())->method('dispatch');
|
||||
|
||||
self::assertNull((new FirewallSettingsService($tenants, $events))->update(
|
||||
'missing', true, 5, 300, 3600, 'Apply defaults', 'admin-a'
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user