import { describe, it, expect, vi, beforeEach } from 'vitest' // Mock logger vi.mock('@adonisjs/core/services/logger', () => ({ default: { info: vi.fn(), debug: vi.fn(), warn: vi.fn(), error: vi.fn() }, })) // Mock DeviceModel vi.mock('../../../../app/models/device.js', () => ({ default: { createOrUpdate: vi.fn().mockResolvedValue({}), findByDeviceId: vi.fn().mockResolvedValue(null), updateOne: vi.fn().mockResolvedValue({}), }, })) // Mock ConfigurationModel vi.mock('#models/configuration', () => ({ default: { find: vi.fn().mockResolvedValue([]), findOne: vi.fn().mockResolvedValue(null), }, })) // Mock FirmwareCatalogService vi.mock('../../../../app/services/firmware_catalog_service.js', () => { const fn = vi.fn().mockResolvedValue([]) return { default: class { resolveUpgradePath = fn }, FirmwareCatalogService: class { resolveUpgradePath = fn }, __mockResolveUpgradePath: fn, } }) // Mock schedule_window vi.mock('../../../../app/utils/schedule_window.js', () => ({ isWithinSchedule: vi.fn().mockReturnValue(true), })) // Mock parameter_writability vi.mock('../../../../app/services/parameter_writability.js', () => ({ reclassifyDeviceFromCatalog: vi.fn(), })) // Mock CommandModel vi.mock('../../../../app/models/command.js', () => ({ default: { getCollection: vi.fn().mockReturnValue({ find: vi.fn().mockReturnValue({ sort: vi.fn().mockReturnValue({ toArray: vi.fn().mockResolvedValue([]) }) }), countDocuments: vi.fn().mockResolvedValue(0), }), updateCommand: vi.fn().mockResolvedValue({}), }, })) import { ConfigurationEvaluationState, FirmwareUpgradeState, ComplianceUpdateState } from '../../../../app/services/cwmp/configuration_evaluation_state.js' import { Device } from '../../../../app/domain/device/device.js' import { DeviceId } from '../../../../app/domain/device/device_id.js' import { DeviceParameter } from '../../../../app/domain/device/device_parameter.js' import { CWMPTaskQueue, ResultStatus } from '../../../../app/services/cwmp/task_queue.js' import { DeviceActionExecutor } from '../../../../app/services/cwmp/device_action_executor.js' import { ParameterType } from '../../../../app/domain/shared/types.js' import ConfigurationModel from '#models/configuration' function makeDevice(): Device { const device = new Device(new DeviceId('Sagemcom', 'F8B7FF', 'Livebox 4', 'LB4-TEST-001')) device.setDataModel('tr-181') device.setGpnStatus('unsupported') return device } function makeContext(device: Device) { const taskQueue = new CWMPTaskQueue() return { device, session: 'test-session', taskQueue, message: undefined as any, services: { deviceAction: new DeviceActionExecutor(taskQueue) }, } } function makeConfig(overrides: any = {}) { return { identifier: 'test-config', name: 'Test Config', version: '1.0.0', priority: 1, policy: 'maintain', enabled: true, criteria: { manufacturer: 'Sagemcom', productClass: 'Livebox 4' }, inventory: [{ path: 'Device.ManagementServer.' }], desiredParameters: [ { name: 'Device.ManagementServer.PeriodicInformInterval', value: '300', type: 'unsignedInt' }, ], firmwareTarget: null, schedule: null, tags: [], ...overrides, } } describe('ConfigurationEvaluationState — inventory and diffs flow', () => { beforeEach(() => { vi.clearAllMocks() }) it('sends SPV when parameterDiffs exist', async () => { const config = makeConfig() vi.mocked(ConfigurationModel.find).mockResolvedValue([config]) const device = makeDevice() // Device has the param but with wrong value device.setParameter(new DeviceParameter( 'Device.ManagementServer.PeriodicInformInterval', '60', ParameterType.UNSIGNED_INT )) const context = makeContext(device) const state = new ConfigurationEvaluationState() const result = await state.handle(context as any) // Since gpnStatus=unsupported, inventory GPN is skipped // Should produce SPV for the diff expect(result.response).toContain('SetParameterValues') expect(result.response).toContain('PeriodicInformInterval') expect(result.response).toContain('300') }) it('skips inventory GPN when device gpnStatus is unsupported', async () => { const config = makeConfig({ desiredParameters: [] }) vi.mocked(ConfigurationModel.find).mockResolvedValue([config]) const device = makeDevice() const context = makeContext(device) const state = new ConfigurationEvaluationState() const result = await state.handle(context as any) // No diffs, inventory skipped → should go to compliance-update expect(result.nextState).toBeDefined() // Should NOT send GPN expect(result.response || '').not.toContain('GetParameterNames') }) it('handles evaluation error gracefully with PartialOperationalState', async () => { // Force evaluate to throw vi.mocked(ConfigurationModel.find).mockRejectedValue(new Error('DB connection lost')) const device = makeDevice() const context = makeContext(device) const state = new ConfigurationEvaluationState() const result = await state.handle(context as any) // Should transition to PartialOperationalState expect(result.nextState).toBeDefined() expect((result.nextState as any).name).toBe('partial_operational') }) }) describe('FirmwareUpgradeState', () => { beforeEach(() => { vi.clearAllMocks() }) it('sends Download RPC for firmware step', async () => { const device = makeDevice() const context = makeContext(device) const steps = [ { fromVersion: '4.0', toVersion: '5.0', downloadUrl: 'http://fw.example.com/5.0.bin', fileSize: 1024, checksum: 'abc' }, ] const engineResult = { configuration: makeConfig(), isAlreadyCompliant: false } as any const state = new FirmwareUpgradeState(steps, 0, engineResult) const result = await state.handle(context as any) expect(result.response).toContain('Download') expect(result.response).toContain('http://fw.example.com/5.0.bin') expect(result.response).toContain('1 Firmware Upgrade Image') }) it('transitions to ComplianceUpdateState when no more steps', async () => { const device = makeDevice() const context = makeContext(device) const engineResult = { configuration: makeConfig(), isAlreadyCompliant: false } as any const state = new FirmwareUpgradeState([], 0, engineResult) // empty steps const result = await state.handle(context as any) expect(result.nextState).toBeDefined() expect((result.nextState as any).name).toBe('compliance-update') }) }) describe('ComplianceUpdateState — provCode write when different', () => { beforeEach(() => { vi.clearAllMocks() }) it('sends SPV for provisioning code when device has different value', async () => { const device = makeDevice() // Set a DIFFERENT provisioning code on the device device.setParameter(new DeviceParameter( 'Device.DeviceInfo.ProvisioningCode', 'old-code', ParameterType.STRING )) const context = makeContext(device) const engineResult = { configuration: makeConfig(), isAlreadyCompliant: true, } as any const state = new ComplianceUpdateState(engineResult) const result = await state.handle(context as any) // Should produce an SPV for the provisioning code expect(result.response).toContain('SetParameterValues') expect(result.response).toContain('ProvisioningCode') }) it('transitions to ExecutingCommandsState when no configuration', async () => { const device = makeDevice() const context = makeContext(device) const engineResult = { configuration: null, isAlreadyCompliant: true } as any const state = new ComplianceUpdateState(engineResult) const result = await state.handle(context as any) expect(result.nextState).toBeDefined() expect((result.nextState as any).name).toBe('executing-commands') }) }) describe('ConfigurationEvaluationState — inventory GPN phase (device supports GPN)', () => { beforeEach(() => { vi.clearAllMocks() }) it('sends GPN for inventory paths when device supports GPN', async () => { const config = makeConfig({ desiredParameters: [{ name: 'Device.ManagementServer.PeriodicInformInterval', value: '300', type: 'unsignedInt' }], inventory: [{ path: 'Device.ManagementServer.' }], }) vi.mocked(ConfigurationModel.find).mockResolvedValue([config]) const device = makeDevice() device.setGpnStatus('supported') // Device supports GPN // Set a different value so isAlreadyCompliant = false (triggers inventory) device.setParameter(new DeviceParameter( 'Device.ManagementServer.PeriodicInformInterval', '60', ParameterType.UNSIGNED_INT )) const context = makeContext(device) const state = new ConfigurationEvaluationState() const result = await state.handle(context as any) // Should send GetParameterNames for the inventory path expect(result.response).toBeDefined() expect(result.response).toContain('GetParameterNames') expect(result.response).toContain('Device.ManagementServer.') }) it('GPN response processed inline — extracts leaf names and sends GPV', async () => { const config = makeConfig({ desiredParameters: [{ name: 'Device.X', value: '1', type: 'string' }], inventory: [{ path: 'Device.ManagementServer.' }], }) vi.mocked(ConfigurationModel.find).mockResolvedValue([config]) const device = makeDevice() device.setGpnStatus('supported') const context = makeContext(device) const state = new ConfigurationEvaluationState() // First call: sends GPN await state.handle(context as any) // Second call with GPN response data → processes inline, extracts leaves, sends GPV context.message = { success: true, data: { ParameterList: { ParameterInfoStruct: [ { Name: 'Device.ManagementServer.PeriodicInformInterval', Writable: '1' }, { Name: 'Device.ManagementServer.ConnectionRequestURL', Writable: '0' }, { Name: 'Device.ManagementServer.', Writable: '0' }, // non-leaf, filtered ], }, }, } const result2 = await state.handle(context as any) expect(result2.response).toContain('GetParameterValues') expect(result2.response).toContain('Device.ManagementServer.PeriodicInformInterval') }) it('GPN failure processed inline — records failure and skips inventory', async () => { const config = makeConfig({ desiredParameters: [{ name: 'Device.X', value: '1', type: 'string' }], inventory: [{ path: 'Device.ManagementServer.' }], }) vi.mocked(ConfigurationModel.find).mockResolvedValue([config]) const device = makeDevice() device.setGpnStatus('supported') const context = makeContext(device) const state = new ConfigurationEvaluationState() await state.handle(context as any) // GPN fault response context.message = { success: false, data: { FaultCode: 9001 } } await state.handle(context as any) // Device should have recorded GPN failure expect(device.gpnCircuitBreaker.failureCount).toBeGreaterThanOrEqual(1) }) it('inventory GPN response then sends SPV for diffs after inventory completes', async () => { const config = makeConfig({ desiredParameters: [{ name: 'Device.ManagementServer.PeriodicInformInterval', value: '300', type: 'unsignedInt' }], inventory: [{ path: 'Device.ManagementServer.' }], }) vi.mocked(ConfigurationModel.find).mockResolvedValue([config]) const device = makeDevice() device.setGpnStatus('supported') device.setParameter(new DeviceParameter( 'Device.ManagementServer.PeriodicInformInterval', '60', ParameterType.UNSIGNED_INT )) const context = makeContext(device) const state = new ConfigurationEvaluationState() // First call: sends GPN for inventory const result1 = await state.handle(context as any) expect(result1.response).toContain('GetParameterNames') // Second call: GPN response received, inventory done, should send SPV for diffs context.message = { success: true, data: {} } const result2 = await state.handle(context as any) // After inventory finishes (no more paths), should send SPV for the diff expect(result2.response).toContain('SetParameterValues') }) }) describe('FirmwareUpgradeState — error handling', () => { beforeEach(() => { vi.clearAllMocks() }) it('returns isComplete on download error', async () => { const device = makeDevice() const taskQueue = new CWMPTaskQueue() const context = { device, session: 'test-session', taskQueue, services: { deviceAction: { applyDownload: vi.fn().mockRejectedValue(new Error('Network timeout')), }, }, } const steps = [{ fromVersion: '4.0', toVersion: '5.0', downloadUrl: 'http://fw.bin', fileSize: 1024, checksum: '' }] const engineResult = { configuration: makeConfig() } as any const state = new FirmwareUpgradeState(steps, 0, engineResult) const result = await state.handle(context as any) expect(result.isComplete).toBe(true) }) }) describe('ComplianceUpdateState — second call after provCode write', () => { beforeEach(() => { vi.clearAllMocks() }) it('transitions to ExecutingCommandsState on second call', async () => { const device = makeDevice() // Device has a DIFFERENT provCode so first call sends SPV device.setParameter(new DeviceParameter( 'Device.DeviceInfo.ProvisioningCode', 'old-code', ParameterType.STRING )) const context = makeContext(device) const engineResult = { configuration: makeConfig(), isAlreadyCompliant: true } as any const state = new ComplianceUpdateState(engineResult) // First call: sends SPV for provisioning code const result1 = await state.handle(context as any) expect(result1.response).toContain('SetParameterValues') // Execute the provCode task callback (covers the anonymous callback at line ~285) const taskCallbacks = (context.taskQueue as any).taskCallbacks as Map const taskId = Array.from(taskCallbacks.keys()).pop() if (taskId) { await context.taskQueue.executeTaskCallback(taskId, ResultStatus.Success, {}) } // Second call: after SPV response, transitions to ExecutingCommandsState const result2 = await state.handle(context as any) expect(result2.nextState).toBeDefined() expect((result2.nextState as any).name).toBe('executing-commands') }) }) describe('ConfigurationEvaluationState — phase 2 GPV callback and SPV callback', () => { beforeEach(() => { vi.clearAllMocks() }) it('inventory phase 2 GPV response processed inline', async () => { const config = makeConfig({ desiredParameters: [{ name: 'Device.X', value: '1', type: 'string' }], inventory: [{ path: 'Device.ManagementServer.' }], }) vi.mocked(ConfigurationModel.find).mockResolvedValue([config]) const device = makeDevice() device.setGpnStatus('supported') const context = makeContext(device) const state = new ConfigurationEvaluationState() // First call: sends GPN await state.handle(context as any) // Second call: GPN response → extracts leaves → sends GPV context.message = { success: true, data: { ParameterList: { ParameterInfoStruct: [{ Name: 'Device.ManagementServer.X', Writable: '1' }] } }, } const result2 = await state.handle(context as any) expect(result2.response).toContain('GetParameterValues') // Third call: GPV response → updates device → proceeds to SPV context.message = { success: true, data: { ParameterList: { ParameterValueStruct: [{ Name: 'Device.ManagementServer.X', Value: { _: '42', '$xsi:type': 'xsd:string' } }] } }, } const result3 = await state.handle(context as any) // Should now apply diffs (SPV) since Device.X doesn't match desired '1' expect(result3.response || result3.nextState).toBeDefined() }) it('SPV callback is invoked after applying diffs', async () => { const config = makeConfig({ desiredParameters: [{ name: 'Device.ManagementServer.PeriodicInformInterval', value: '300', type: 'unsignedInt' }], inventory: [], }) vi.mocked(ConfigurationModel.find).mockResolvedValue([config]) const device = makeDevice() device.setGpnStatus('unsupported') device.setParameter(new DeviceParameter( 'Device.ManagementServer.PeriodicInformInterval', '60', ParameterType.UNSIGNED_INT )) const context = makeContext(device) const state = new ConfigurationEvaluationState() const result = await state.handle(context as any) expect(result.response).toContain('SetParameterValues') // Execute the SPV callback (covers line ~147 `() => Promise.resolve('')`) const taskCallbacks = (context.taskQueue as any).taskCallbacks as Map const spvTaskId = Array.from(taskCallbacks.keys()).pop() await context.taskQueue.executeTaskCallback(spvTaskId!, ResultStatus.Success, {}) }) }) describe('FirmwareUpgradeState — download callback', () => { beforeEach(() => { vi.clearAllMocks() }) it('download callback is invoked on success', async () => { const device = makeDevice() const context = makeContext(device) const steps = [{ fromVersion: '4.0', toVersion: '5.0', downloadUrl: 'http://fw.bin', fileSize: 1024, checksum: '' }] const engineResult = { configuration: makeConfig() } as any const state = new FirmwareUpgradeState(steps, 0, engineResult) const result = await state.handle(context as any) expect(result.response).toContain('Download') // Execute the download callback (covers line ~206 `async () => ''`) const taskCallbacks = (context.taskQueue as any).taskCallbacks as Map const dlTaskId = Array.from(taskCallbacks.keys()).pop() if (dlTaskId) { await context.taskQueue.executeTaskCallback(dlTaskId, ResultStatus.Success, {}) } }) }) describe('ConfigurationEvaluationState — inventory phase 2 GPV saves values on device', () => { beforeEach(() => { vi.clearAllMocks() }) it('GPV response in phase 2 updates device values inline', async () => { const config = makeConfig({ desiredParameters: [{ name: 'Device.X', value: '1', type: 'string' }], inventory: [{ path: 'Device.ManagementServer.' }], }) vi.mocked(ConfigurationModel.find).mockResolvedValue([config]) const device = makeDevice() device.setGpnStatus('supported') const context = makeContext(device) const state = new ConfigurationEvaluationState() // Step 1: first handle → sends GPN for inventory await state.handle(context as any) // Step 2: GPN response inline → extracts leaf names → sends GPV context.message = { success: true, data: { ParameterList: { ParameterInfoStruct: [ { Name: 'Device.ManagementServer.PeriodicInformInterval', Writable: '1' }, { Name: 'Device.ManagementServer.ConnectionRequestURL', Writable: '0' }, ], }, }, } const result2 = await state.handle(context as any) expect(result2.response).toContain('GetParameterValues') // Before GPV response — device has no value for PeriodicInformInterval const before = device.getParameters().find((p) => p.name === 'Device.ManagementServer.PeriodicInformInterval') expect(before?.value ?? null).toBeNull() // Step 3: GPV response inline → updates device values context.message = { success: true, data: { ParameterList: { ParameterValueStruct: [ { Name: 'Device.ManagementServer.PeriodicInformInterval', Value: { _: '3600', $: { 'xsi:type': 'xsd:unsignedInt' } } }, { Name: 'Device.ManagementServer.ConnectionRequestURL', Value: { _: 'http://cpe/cr', $: { 'xsi:type': 'xsd:string' } } }, ], }, }, } await state.handle(context as any) // After GPV response — device must have the values saved const after = device.getProperty('Device.ManagementServer.PeriodicInformInterval') expect(after).not.toBeNull() expect(after!.value).toBe('3600') }) }) describe('ConfigurationEvaluationState — firmware upgrade transition (lines 154-158)', () => { beforeEach(() => { vi.clearAllMocks() }) it('transitions to FirmwareUpgradeState when firmwareMigrationPath has steps', async () => { // Directly test by creating state with pre-set internal state const config = makeConfig({ inventory: [] }) vi.mocked(ConfigurationModel.find).mockResolvedValue([config]) const device = makeDevice() device.setGpnStatus('unsupported') const context = makeContext(device) const state = new ConfigurationEvaluationState() // Manually override the engine result after initialization // First, trigger initialization await state.handle(context as any) // The state already ran - but if already compliant it transitions. // Instead, set up the engineResult with firmware steps directly: ;(state as any).engineResult = { configuration: config, inventoryPaths: [], parameterDiffs: [], firmwareMigrationPath: [{ fromVersion: '4.0', toVersion: '5.0', downloadUrl: 'http://fw.bin', fileSize: 1024, checksum: '' }], isAlreadyCompliant: false, skipFirmware: false, } ;(state as any).inventoryQueue = [] ;(state as any).paramDiffs = [] // Call handle again - should hit firmware transition branch const result = await state.handle(context as any) expect(result.nextState).toBeDefined() expect((result.nextState as any).name).toBe('firmware-upgrade') }) })