getName()); self::assertSame('203.0.113.10', $blocked->getIpAddress()); self::assertSame('Repeated abuse', $blocked->getReason()); self::assertSame(SecurityEventSeverity::CRITICAL, $blocked->getSeverity()); self::assertSame(IpAllowedEvent::class, $allowed->getName()); self::assertSame('203.0.113.11', $allowed->getIpAddress()); self::assertSame(SecurityEventSeverity::INFO, $allowed->getSeverity()); self::assertSame(DeviceBlockedEvent::class, $device->getName()); self::assertSame('device-a', $device->getDeviceFingerprint()); self::assertSame(SecurityEventSeverity::CRITICAL, $device->getSeverity()); } #[Test] #[TestDox('Firewall settings events capture the complete configuration transition')] public function constructsSettingsEvent(): void { $event = new FirewallSettingsUpdatedEvent( 'Tighten controls', ['maxAuthFailures' => 5], ['maxAuthFailures' => 3], 'tenant-a', 'operator-a', ); self::assertSame(FirewallSettingsUpdatedEvent::class, $event->getName()); self::assertSame('Tighten controls', $event->getChangeReason()); self::assertSame(['maxAuthFailures' => 5], $event->getPrevious()); self::assertSame(['maxAuthFailures' => 3], $event->getCurrent()); self::assertSame('tenant-a', $event->getTenantId()); self::assertSame('operator-a', $event->getIdentityId()); self::assertSame('manual', $event->getChangeOrigin()); self::assertSame(SecurityEventSeverity::INFO, $event->getSeverity()); } #[Test] #[TestDox('Firewall policy events reject missing subject and ownership context')] public function rejectsIncompletePolicyContext(): void { foreach ([ static fn() => new IpBlockedEvent(''), static fn() => new IpAllowedEvent(''), static fn() => new DeviceBlockedEvent(''), static fn() => new FirewallSettingsUpdatedEvent('', [], [], 'tenant-a'), static fn() => new FirewallSettingsUpdatedEvent('Reason', [], [], ''), ] as $construction) { try { $construction(); self::fail('Incomplete firewall policy context was accepted.'); } catch (\InvalidArgumentException) { $this->addToAssertionCount(1); } } } }