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 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({}), }, })) // Mock ConfigurationEngine to control evaluation results vi.mock('../../../../app/services/cwmp/configuration_evaluation_state.js', () => ({ ConfigurationEvaluationState: class { name = 'configuration-evaluation' async handle() { return { isComplete: true, response: '' } } }, })) // Mock reclassify vi.mock('../../../../app/services/parameter_writability.js', () => ({ reclassifyDeviceFromCatalog: vi.fn(), })) import { MainStateMachine, OperationalState, PartialOperationalState } 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' import DeviceModel from '../../../../app/models/device.js' function makeDevice(gpnStatus: string = 'unsupported'): Device { const device = new Device(new DeviceId('Sagemcom', 'F8B7FF', 'Livebox 4', 'LB4-TEST-001')) device.setDataModel('tr-181') device.setGpnStatus(gpnStatus as any) return device } describe('MainStateMachine — additional coverage (DiscoveringParameters, GPN flows)', () => { beforeEach(() => { vi.clearAllMocks() }) it('sends GPN for tr-098 device when gpnStatus=unknown', async () => { const device = new Device(new DeviceId('Huawei', '202BC1', 'BM632w', 'SN001')) device.setDataModel('tr-098') device.setGpnStatus('unknown') const taskQueue = new CWMPTaskQueue() const sm = new MainStateMachine(device, 'sess', taskQueue) const response = await sm.start() expect(response).toContain('GetParameterNames') expect(response).toContain('InternetGatewayDevice.ManagementServer.') }) it('skips GPN for unknown dataModel and goes to critical-reads', async () => { const device = new Device(new DeviceId('Unknown', '000000', 'Generic', 'SN002')) device.setDataModel('unknown') device.setGpnStatus('unknown') const taskQueue = new CWMPTaskQueue() const sm = new MainStateMachine(device, 'sess', taskQueue) const response = await sm.start() // Should go to critical-reads (GPV) since unknown data model skips GPN expect(response).toContain('GetParameterValues') }) it('DiscoveringParametersState handles GPN success and transitions to CriticalReads', async () => { const device = makeDevice('unknown') const taskQueue = new CWMPTaskQueue() const sm = new MainStateMachine(device, 'sess', taskQueue) // Start: sends GPN await sm.start() // Simulate successful GPN response (empty parameter list) const response = await sm.handleResponse(true, { ParameterList: { ParameterInfoStruct: [] } }) // Should transition to critical-reads and send GPV expect(response).toContain('GetParameterValues') expect(sm.getCurrentStateName()).toBe('critical-reads') }) it('handles MAX_STATE_TRANSITIONS exceeded gracefully', async () => { // Create a state machine with a device that will loop const device = makeDevice('unsupported') const taskQueue = new CWMPTaskQueue() const sm = new MainStateMachine(device, 'sess', taskQueue) // The machine won't actually loop 15 times in normal flow since it completes // Just verify the machine starts and runs without crashing const response = await sm.start() expect(response).toBeDefined() }) }) describe('OperationalState', () => { beforeEach(() => { vi.clearAllMocks() }) it('persists device with operational status', async () => { const device = makeDevice() const taskQueue = new CWMPTaskQueue() const context = { device, session: 'test-session', taskQueue, services: { deviceAction: {} as any }, } const state = new OperationalState() const result = await state.handle(context as any) expect(result.isComplete).toBe(true) expect(device.operationalStatus).toBe('operational') expect(vi.mocked(DeviceModel.createOrUpdate)).toHaveBeenCalledWith( expect.objectContaining({ operationalStatus: 'operational' }) ) }) }) describe('PartialOperationalState', () => { beforeEach(() => { vi.clearAllMocks() }) it('persists device with partial_operational status and failure reason', async () => { const device = makeDevice() const taskQueue = new CWMPTaskQueue() const context = { device, session: 'test-session', taskQueue, services: { deviceAction: {} as any }, } const state = new PartialOperationalState(device, { reason: 'test_failure', blockingState: 'some-state', }) const result = await state.handle(context as any) expect(result.isComplete).toBe(true) expect(device.operationalStatus).toBe('partial_operational') expect(device.lastFailureReason).toBe('test_failure') expect(vi.mocked(DeviceModel.createOrUpdate)).toHaveBeenCalledWith( expect.objectContaining({ operationalStatus: 'partial_operational', lastFailureReason: 'test_failure', }) ) }) })