import { describe, it, expect, vi, beforeEach } from 'vitest' // Mock AdonisJS logger before imports vi.mock('@adonisjs/core/services/logger', () => ({ default: { info: vi.fn(), debug: vi.fn(), warn: vi.fn(), error: vi.fn(), }, })) // Mock DeviceModel to avoid MongoDB dependency vi.mock('../../../app/models/device.js', () => ({ default: { createOrUpdate: vi.fn().mockResolvedValue({}), findByDeviceId: vi.fn().mockResolvedValue(null), }, })) // 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), }), }, })) import { MainStateMachine } from '../../../app/services/cwmp/main_state_machine.js' import { Device } from '../../../app/domain/device/device.js' import { DeviceId } from '../../../app/domain/device/device_id.js' import { DeviceProperty } from '../../../app/domain/device/device_parameter.js' import { CWMPTaskQueue } from '../../../app/services/cwmp/task_queue.js' import { ParameterType } from '../../../app/domain/shared/types.js' // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function makeTr181Device(): Device { const device = new Device(new DeviceId('Sagemcom', 'F8B7FF', 'Livebox 4', 'LB4-TEST-001')) device.setDataModel('tr-181') return device } function makeTr098Device(): Device { const device = new Device(new DeviceId('Huawei Technologies Co., Ltd.', '202BC1', 'BM632w', '000000')) device.setDataModel('tr-098') return device } function makeUnknownDevice(): Device { const device = new Device(new DeviceId('Unknown', '000000', 'Generic', 'SN001')) device.setDataModel('unknown') return device } // --------------------------------------------------------------------------- // MainStateMachine — Initial state // --------------------------------------------------------------------------- describe('MainStateMachine — initial state', () => { it('starts in idle state', () => { const device = makeTr181Device() const taskQueue = new CWMPTaskQueue() const sm = new MainStateMachine(device, 'session-1', taskQueue) expect(sm.getCurrentStateName()).toBe('idle') expect(sm.isCompleted()).toBe(false) }) }) // --------------------------------------------------------------------------- // MainStateMachine — TR-181 device flow (GPN on Device.ManagementServer.) // --------------------------------------------------------------------------- describe('MainStateMachine — TR-181 device', () => { let device: Device let taskQueue: CWMPTaskQueue let sm: MainStateMachine beforeEach(() => { device = makeTr181Device() taskQueue = new CWMPTaskQueue() sm = new MainStateMachine(device, 'session-1', taskQueue) }) it('transitions to discovering-parameters and sends GPN for Device.ManagementServer.', async () => { const response = await sm.start() expect(sm.getCurrentStateName()).toBe('discovering-parameters') // Response should be a SOAP envelope with GetParameterNames expect(response).toContain('GetParameterNames') expect(response).toContain('Device.ManagementServer.') }) it('advances past discovering-parameters after receiving a response', async () => { await sm.start() // sends GPN, state = discovering-parameters // Simulate any response (success or failure) — state machine advances await sm.handleResponse(true, {}) // Should have moved past discovering-parameters (now in critical-reads) expect(sm.getCurrentStateName()).not.toBe('discovering-parameters') expect(sm.getCurrentStateName()).not.toBe('idle') }) it('GPN callback sets gpnStatus to unsupported on fault code 9000', async () => { await sm.start() // Get the task from the queue and execute its callback manually const tasks: any[] = Array.from((taskQueue as any).tasks.values()) const gpnTask = tasks.find((t: any) => t.type === 'get_parameter_names') expect(gpnTask).toBeDefined() // Execute the callback with error await taskQueue.executeTaskCallback(gpnTask!.id, 'error' as any, { FaultCode: 9000, FaultString: 'Method not supported', }) expect(device.gpnStatus).toBe('unsupported') }) it('GPN callback sets gpnStatus to supported on success', async () => { await sm.start() const tasks: any[] = Array.from((taskQueue as any).tasks.values()) const gpnTask = tasks.find((t: any) => t.type === 'get_parameter_names') expect(gpnTask).toBeDefined() // Execute the callback with success await taskQueue.executeTaskCallback(gpnTask!.id, 'success' as any, { ParameterList: { ParameterInfoStruct: [ { Name: 'Device.ManagementServer.URL', Writable: true }, { Name: 'Device.ManagementServer.ParameterKey', Writable: false }, ], }, }) expect(device.gpnStatus).toBe('supported') }) it('GPN callback organizes parameters correctly on success', async () => { device.setProperty(new DeviceProperty('Device.ManagementServer.URL', 'http://acs', ParameterType.STRING)) device.setProperty(new DeviceProperty('Device.ManagementServer.ParameterKey', '', ParameterType.STRING)) await sm.start() const tasks: any[] = Array.from((taskQueue as any).tasks.values()) const gpnTask = tasks.find((t: any) => t.type === 'get_parameter_names') await taskQueue.executeTaskCallback(gpnTask!.id, 'success' as any, { ParameterList: { ParameterInfoStruct: [ { Name: 'Device.ManagementServer.URL', Writable: true }, { Name: 'Device.ManagementServer.ParameterKey', Writable: false }, ], }, }) // URL was property + writable in GPN → should be parameter now expect(device.getParameter('Device.ManagementServer.URL')).not.toBeNull() // ParameterKey was property + NOT writable → stays property expect(device.getProperty('Device.ManagementServer.ParameterKey')).not.toBeNull() }) }) // --------------------------------------------------------------------------- // MainStateMachine — TR-098 device flow (GPN on InternetGatewayDevice.ManagementServer.) // --------------------------------------------------------------------------- describe('MainStateMachine — TR-098 device', () => { let device: Device let taskQueue: CWMPTaskQueue let sm: MainStateMachine beforeEach(() => { device = makeTr098Device() taskQueue = new CWMPTaskQueue() sm = new MainStateMachine(device, 'session-1', taskQueue) }) it('sends GPN for InternetGatewayDevice.ManagementServer.', async () => { const response = await sm.start() expect(sm.getCurrentStateName()).toBe('discovering-parameters') expect(response).toContain('GetParameterNames') expect(response).toContain('InternetGatewayDevice.ManagementServer.') }) }) // --------------------------------------------------------------------------- // MainStateMachine — Unknown data model // --------------------------------------------------------------------------- describe('MainStateMachine — unknown data model', () => { it('skips GPN and goes directly to critical-reads', async () => { const device = makeUnknownDevice() const taskQueue = new CWMPTaskQueue() const sm = new MainStateMachine(device, 'session-1', taskQueue) await sm.start() // Should skip discovering-parameters and go to critical-reads expect(sm.getCurrentStateName()).not.toBe('discovering-parameters') expect(sm.getCurrentStateName()).not.toBe('idle') expect(sm.getCurrentStateName()).toBe('critical-reads') }) }) // --------------------------------------------------------------------------- // MainStateMachine — CompletedState reclassification // --------------------------------------------------------------------------- describe('MainStateMachine — CompletedState reclassification', () => { it('reclassifies writable properties on completion when GPN unsupported', async () => { const device = makeTr181Device() device.setGpnStatus('unsupported') // Add writable property that should be reclassified device.setProperty(new DeviceProperty('Device.ManagementServer.PeriodicInformInterval', '60', ParameterType.UNSIGNED_INT)) // Add read-only property that should stay device.setProperty(new DeviceProperty('Device.DeviceInfo.Manufacturer', 'Sagemcom', ParameterType.STRING)) const taskQueue = new CWMPTaskQueue() const sm = new MainStateMachine(device, 'session-1', taskQueue) // Start → since dataModel is tr-181, it will try GPN await sm.start() // Simulate GPN failure await sm.handleResponse(true, {}) // The state machine should eventually complete // Let it run through (no config match → completes) let guard = 0 while (!sm.isCompleted() && guard++ < 10) { await sm.handleResponse(true, {}) } // After completion, PeriodicInformInterval should be a parameter expect(device.getParameter('Device.ManagementServer.PeriodicInformInterval')).not.toBeNull() expect(device.getProperty('Device.ManagementServer.PeriodicInformInterval')).toBeNull() // Manufacturer should stay as property expect(device.getProperty('Device.DeviceInfo.Manufacturer')).not.toBeNull() expect(device.getParameter('Device.DeviceInfo.Manufacturer')).toBeNull() }) }) // --------------------------------------------------------------------------- // DeviceActionExecutor — reclassification after SPV (Livebox fallback scenario) // --------------------------------------------------------------------------- describe('DeviceActionExecutor — reclassification after updateFromValues', () => { it('SPV callback updates device values but does not reclassify (consolidated to terminal state)', async () => { const device = makeTr181Device() device.setGpnStatus('unsupported') // Device starts with only read-only properties (like after first Inform) device.setProperty(new DeviceProperty('Device.DeviceInfo.Manufacturer', 'Sagemcom', ParameterType.STRING)) const taskQueue = new CWMPTaskQueue() // Import DeviceActionExecutor const { DeviceActionExecutor } = await import('../../../app/services/cwmp/device_action_executor.js') const executor = new DeviceActionExecutor(taskQueue) // Simulate SPV for ProvisioningCode (like ConfigurationEvaluationState does) const spvXml = await executor.applySetParameterValues( device, [{ parameterName: 'Device.DeviceInfo.ProvisioningCode', value: 'test-code' }], async () => '' ) expect(spvXml).toContain('SetParameterValues') // Get the task and execute callback with success (simulating CPE response) const tasks: any[] = Array.from((taskQueue as any).tasks.values()) const spvTask = tasks.find((t: any) => t.type === 'set_parameter_values') expect(spvTask).toBeDefined() await taskQueue.executeTaskCallback(spvTask!.id, 'success' as any, { Status: 0 }) // ProvisioningCode is stored as a property (reclassification happens later in OperationalState) expect(device.getProperty('Device.DeviceInfo.ProvisioningCode')).not.toBeNull() expect(device.getProperty('Device.DeviceInfo.ProvisioningCode')!.value).toBe('test-code') }) it('does NOT reclassify when GPN is supported (live data is authoritative)', async () => { const device = makeTr181Device() device.setGpnStatus('supported') device.setProperty(new DeviceProperty('Device.DeviceInfo.Manufacturer', 'Sagemcom', ParameterType.STRING)) const taskQueue = new CWMPTaskQueue() const { DeviceActionExecutor } = await import('../../../app/services/cwmp/device_action_executor.js') const executor = new DeviceActionExecutor(taskQueue) // SPV for a writable param — but GPN is supported so no reclassification await executor.applySetParameterValues( device, [{ parameterName: 'Device.ManagementServer.PeriodicInformInterval', value: '120' }], async () => '' ) const tasks: any[] = Array.from((taskQueue as any).tasks.values()) const spvTask = tasks.find((t: any) => t.type === 'set_parameter_values') await taskQueue.executeTaskCallback(spvTask!.id, 'success' as any, { Status: 0 }) // PeriodicInformInterval was added via updateFromValues as a property // But since GPN is supported, reclassification is NOT called // It stays as property (the GPN response is the authority) expect(device.getProperty('Device.ManagementServer.PeriodicInformInterval')).not.toBeNull() expect(device.getParameter('Device.ManagementServer.PeriodicInformInterval')).toBeNull() }) })