feat: make event state constructor-only and immutable

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-08-05 22:46:33 -04:00
parent ba4deccea9
commit 52afd35d6f
8 changed files with 266 additions and 193 deletions
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace KTXT\Unit\Event;
use KTXF\Event\Event;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase;
final class EventTest extends TestCase
{
#[Test]
#[TestDox('Event data and context are supplied during construction')]
public function constructsCompleteEventState(): void
{
$event = new Event(
'test.event',
['nested' => ['value' => 'original']],
'tenant-a',
'identity-a',
);
$copy = $event->getData();
$copy['nested']['value'] = 'changed';
self::assertSame('original', $event->get('nested')['value']);
self::assertSame('tenant-a', $event->getTenantId());
self::assertSame('identity-a', $event->getIdentityId());
}
#[Test]
#[TestDox('Event data rejects mutable object references')]
public function rejectsMutablePayloadValues(): void
{
$this->expectException(\InvalidArgumentException::class);
new Event('test.event', ['mutable' => new \stdClass()]);
}
}
+26 -4
View File
@@ -34,13 +34,35 @@ final class SecurityEventTest extends TestCase
}
#[Test]
#[TestDox('Explicit severity can override the event type default')]
#[TestDox('Construction can override the event type default severity')]
public function allowsSeverityOverride(): void
{
$event = new SecurityEvent(SecurityEvent::AUTH_FAILURE);
$event->setSeverity(SecurityEvent::SEVERITY_CRITICAL);
$event = new SecurityEvent(
SecurityEvent::AUTH_FAILURE,
severity: SecurityEvent::SEVERITY_CRITICAL,
);
self::assertSame(SecurityEvent::SEVERITY_CRITICAL, $event->getSeverity());
}
#[Test]
#[TestDox('Event state exposes no mutation methods')]
public function exposesNoMutationMethods(): void
{
foreach ([
'set',
'setTenantId',
'setIdentityId',
'setIpAddress',
'setDeviceFingerprint',
'setUserAgent',
'setRequestPath',
'setRequestMethod',
'setUserId',
'setReason',
'setSeverity',
] as $method) {
self::assertFalse(method_exists(SecurityEvent::class, $method));
}
}
}
+25 -18
View File
@@ -215,9 +215,9 @@ class FirewallServiceTest extends TestCase
null,
'tenant-rule',
FirewallRuleObject::SCOPE_TENANT,
'Tenant block'
'Tenant block',
'tenant-a',
);
$event->setTenantId('tenant-a');
$this->service->logSecurityEvent($event);
}
@@ -275,9 +275,9 @@ class FirewallServiceTest extends TestCase
'203.0.113.10',
101,
60,
'/login'
'/login',
'tenant-a',
);
$event->setTenantId('tenant-a');
$this->service->logSecurityEvent($event);
}
@@ -300,11 +300,11 @@ class FirewallServiceTest extends TestCase
\KTXC\Security\Event\SecurityEvent::SUSPICIOUS_ACTIVITY,
'203.0.113.20',
null,
['detector' => 'payload-signature']
['detector' => 'payload-signature'],
tenantId: 'tenant-a',
requestPath: '/admin',
requestMethod: 'POST',
);
$event->setTenantId('tenant-a')
->setRequestPath('/admin')
->setRequestMethod('POST');
$this->service->logSecurityEvent($event);
}
@@ -329,9 +329,9 @@ class FirewallServiceTest extends TestCase
'ruleId' => 'rule-123',
'ruleScope' => FirewallRuleObject::SCOPE_SYSTEM,
'origin' => FirewallRuleManager::ORIGIN_MANUAL,
]
],
identityId: 'operator',
);
$event->setIdentityId('operator');
$this->service->logSecurityEvent($event);
}
@@ -351,9 +351,10 @@ class FirewallServiceTest extends TestCase
->willReturnArgument(0);
$event = new \KTXC\Security\Event\SecurityEvent(
\KTXC\Security\Event\SecurityEvent::FIREWALL_SETTINGS_UPDATED,
['changeReason' => 'Tighten controls']
['changeReason' => 'Tighten controls'],
tenantId: 'tenant-a',
identityId: 'operator',
);
$event->setTenantId('tenant-a')->setIdentityId('operator');
$this->service->logSecurityEvent($event);
}
@@ -376,8 +377,10 @@ class FirewallServiceTest extends TestCase
->willReturn(4);
$this->events->expects($this->never())->method('dispatch');
$event = \KTXC\Security\Event\SecurityEvent::authFailure('203.0.113.10');
$event->setTenantId('tenant-a');
$event = \KTXC\Security\Event\SecurityEvent::authFailure(
'203.0.113.10',
tenantId: 'tenant-a',
);
$this->service->handleAuthFailure($event);
self::assertSame(8, $this->currentConfiguration->firewall()->maxAuthFailures());
@@ -400,8 +403,10 @@ class FirewallServiceTest extends TestCase
->with('tenant-a', '203.0.113.10', 300)
->willReturn(0);
$event = \KTXC\Security\Event\SecurityEvent::authFailure('203.0.113.10');
$event->setTenantId('tenant-a');
$event = \KTXC\Security\Event\SecurityEvent::authFailure(
'203.0.113.10',
tenantId: 'tenant-a',
);
$this->service->handleAuthFailure($event);
}
@@ -454,8 +459,10 @@ class FirewallServiceTest extends TestCase
}
});
$event = \KTXC\Security\Event\SecurityEvent::authFailure('203.0.113.10');
$event->setTenantId('tenant-event');
$event = \KTXC\Security\Event\SecurityEvent::authFailure(
'203.0.113.10',
tenantId: 'tenant-event',
);
$this->service->handleAuthFailure($event);
self::assertSame(['tenant-event', 'tenant-event', 'tenant-event'], $publishedTenants);