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' )); } }