diff --git a/tests/unit/services/cwmp/branch_coverage.spec.ts b/tests/unit/services/cwmp/branch_coverage.spec.ts deleted file mode 100644 index 6afd5dd..0000000 --- a/tests/unit/services/cwmp/branch_coverage.spec.ts +++ /dev/null @@ -1,316 +0,0 @@ -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 reclassify -vi.mock('../../../../app/services/parameter_writability.js', () => ({ - reclassifyDeviceFromCatalog: vi.fn(), -})) - -import { ExecutingCommandsState } from '../../../../app/services/cwmp/main_state_machine.js' -import { StateMachineManager, MachineStatus } from '../../../../app/services/cwmp/state_machine_manager.js' -import { CWMPTaskQueue, ResultStatus } from '../../../../app/services/cwmp/task_queue.js' -import { Device } from '../../../../app/domain/device/device.js' -import { DeviceId } from '../../../../app/domain/device/device_id.js' -import { DeviceParameter, DeviceProperty } from '../../../../app/domain/device/device_parameter.js' -import { DeviceActionExecutor } from '../../../../app/services/cwmp/device_action_executor.js' -import { CommandStatus, CommandType } from '../../../../app/domain/commands/command.js' -import { ModeType } from '../../../../app/domain/shared/mode.js' -import { ParameterType } from '../../../../app/domain/shared/types.js' -import CommandModel from '../../../../app/models/command.js' -import logger from '@adonisjs/core/services/logger' - -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', - message: undefined as any, - taskQueue, - services: { deviceAction: new DeviceActionExecutor(taskQueue) }, - } -} - -function setupCommandMock(commands: any[]) { - const collection = CommandModel.getCollection() - let callCount = 0 - vi.mocked(collection.find).mockImplementation(() => ({ - sort: () => ({ - toArray: () => { - const result = callCount === 0 ? commands : [] - callCount++ - return Promise.resolve(result) - }, - }), - } as any)) -} - -// ─── ExecutingCommandsState branch coverage ───────────────────────────────── - -describe('ExecutingCommandsState — uncovered branches', () => { - beforeEach(() => { vi.clearAllMocks() }) - - it('handles multiple commands in queue sequentially', async () => { - const cmd1 = { _id: 'c1', action: CommandType.GET_PARAMETER_VALUES, parameters: ['Device.A'], status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 } - const cmd2 = { _id: 'c2', action: CommandType.REBOOT, parameters: { commandKey: 'r' }, status: CommandStatus.PENDING, mode: ModeType.I, priority: 2 } - setupCommandMock([cmd1, cmd2]) - - const device = makeDevice() - const context = makeContext(device) - const state = new ExecutingCommandsState(true) - - // First handle: loads commands, sends first command - const r1 = await state.handle(context) - expect(r1.response).toContain('GetParameterValues') - - // Second handle: success response for first command, sends second - context.message = { success: true, data: {} } - const r2 = await state.handle(context) - expect(r2.response).toContain('Reboot') - - // Third handle: success response for second command → transitions to operational - context.message = { success: true, data: {} } - const r3 = await state.handle(context) - expect(r3.nextState).toBeDefined() - expect((r3.nextState as any).name).toBe('operational') - }) - - it('GET_PARAMETER_NAMES command with string params', async () => { - const cmd = { _id: 'c3', action: CommandType.GET_PARAMETER_NAMES, parameters: 'Device.ManagementServer.', status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 } - setupCommandMock([cmd]) - - const device = makeDevice() - const context = makeContext(device) - const state = new ExecutingCommandsState(true) - - const result = await state.handle(context) - expect(result.response).toContain('GetParameterNames') - }) - - it('GET_PARAMETER_NAMES command with object params', async () => { - const cmd = { _id: 'c4', action: CommandType.GET_PARAMETER_NAMES, parameters: { path: 'Device.Info.' }, status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 } - setupCommandMock([cmd]) - - const device = makeDevice() - const context = makeContext(device) - const state = new ExecutingCommandsState(true) - - const result = await state.handle(context) - expect(result.response).toContain('GetParameterNames') - }) - - it('SET_PARAMETER_VALUES with parameterValues property format', async () => { - const cmd = { _id: 'c5', action: CommandType.SET_PARAMETER_VALUES, parameters: { parameterValues: [{ name: 'Device.X', value: '1' }] }, status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 } - setupCommandMock([cmd]) - - const device = makeDevice() - const context = makeContext(device) - const state = new ExecutingCommandsState(true) - - const result = await state.handle(context) - expect(result.response).toContain('SetParameterValues') - }) - - it('SET_PARAMETER_ATTRIBUTES command', async () => { - const cmd = { _id: 'c6', action: CommandType.SET_PARAMETER_ATTRIBUTES, parameters: [{ name: 'Device.X', notificationChange: true, notification: 2, accessListChange: false, accessList: [] }], status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 } - setupCommandMock([cmd]) - - const device = makeDevice() - const context = makeContext(device) - const state = new ExecutingCommandsState(true) - - const result = await state.handle(context) - expect(result.response).toContain('SetParameterAttributes') - }) - - it('UPLOAD command', async () => { - const cmd = { _id: 'c7', action: CommandType.UPLOAD, parameters: { commandKey: 'u1', fileType: '2', url: 'http://u.bin' }, status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 } - setupCommandMock([cmd]) - - const device = makeDevice() - const context = makeContext(device) - const state = new ExecutingCommandsState(true) - - const result = await state.handle(context) - expect(result.response).toContain('Upload') - }) - - it('CommandModel.updateCommand error is caught (line 292)', async () => { - vi.mocked(CommandModel.updateCommand).mockRejectedValueOnce(new Error('DB error')) - const cmd = { _id: 'c8', action: CommandType.GET_PARAMETER_VALUES, parameters: ['Device.X'], status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 } - setupCommandMock([cmd]) - - const device = makeDevice() - const context = makeContext(device) - const state = new ExecutingCommandsState(true) - - // First handle: sends command - await state.handle(context) - // Second handle: response processing with DB error on updateCommand - vi.mocked(CommandModel.updateCommand).mockRejectedValueOnce(new Error('DB error')) - context.message = { success: true, data: {} } - await state.handle(context) - - expect(vi.mocked(logger.error)).toHaveBeenCalled() - }) - - it('returns empty response for includeNI=false when all commands done (line 304)', async () => { - const cmd = { _id: 'c9', action: CommandType.REBOOT, parameters: {}, status: CommandStatus.PENDING, mode: ModeType.I, priority: 1 } - setupCommandMock([cmd]) - - const device = makeDevice() - const context = makeContext(device) - const state = new ExecutingCommandsState(false) // includeNI = false - - await state.handle(context) - context.message = { success: true, data: {} } - const result = await state.handle(context) - - // Should return empty (not transition to Operational) - expect(result.response).toBe('') - expect(result.nextState).toBeUndefined() - }) -}) - -// ─── Task Queue branch coverage ───────────────────────────────────────────── - -describe('CWMPTaskQueue — uncovered branches', () => { - it('executeTaskCallback catches callback errors (line 212-214)', async () => { - const tq = new CWMPTaskQueue() - const deviceId = new DeviceId('X', 'Y', 'Z', 'S') - - const throwingCallback = vi.fn().mockRejectedValue(new Error('callback crash')) - const { taskId } = tq.addTask(deviceId, 'get_parameter_values', [], throwingCallback) - - const response = await tq.executeTaskCallback(taskId, ResultStatus.Success, {}) - expect(response).toBe('') - expect(vi.mocked(logger.error)).toHaveBeenCalled() - }) - - it('clearPendingTasksForDevice removes PENDING tasks and keeps SENT (lines 227-228)', () => { - const tq = new CWMPTaskQueue() - const deviceId = new DeviceId('X', 'Y', 'Z', 'S') - - // Add tasks: one will be marked SENT (via addTask), another stays PENDING - tq.addCWMPTask(deviceId, 'get_parameter_values', ['a']) // PENDING - const { taskId: sentId } = tq.addTask(deviceId, 'set_parameter_values', {}, async () => '') // SENT - - tq.clearPendingTasksForDevice(deviceId) - - // SENT task should still exist - const tasks = (tq as any).tasks as Map - expect(tasks.has(sentId)).toBe(true) - }) - - it('registerCallback adds callback for existing task (line 124)', () => { - const tq = new CWMPTaskQueue() - const deviceId = new DeviceId('X', 'Y', 'Z', 'S') - const task = tq.addCWMPTask(deviceId, 'reboot', {}) - - const cb = vi.fn().mockResolvedValue('') - tq.registerCallback(task.id, cb) - - const callbacks = (tq as any).taskCallbacks as Map - expect(callbacks.has(task.id)).toBe(true) - }) -}) - -// ─── StateMachineManager uncovered branches ──────────────────────────────── - -describe('StateMachineManager — uncovered branches', () => { - let manager: StateMachineManager - - beforeEach(() => { - vi.clearAllMocks() - ;(StateMachineManager as any).instance = null - manager = StateMachineManager.getInstance() - }) - - it('handleCWMPResponse returns empty for non-RUNNING machine', async () => { - const device = makeDevice() - const machine = await manager.startStateMachine(device, 'sess-nr', { events: ['0 BOOTSTRAP'] }) - ;(machine as any).status = MachineStatus.COMPLETED - - const result = await manager.handleCWMPResponse('sess-nr', true, {}) - expect(result).toBe('') - }) - - it('continue returns empty for non-RUNNING machine', async () => { - const device = makeDevice() - const machine = await manager.startStateMachine(device, 'sess-cnr', { events: ['0 BOOTSTRAP'] }) - ;(machine as any).status = MachineStatus.FAILED - - const result = await manager.continue('sess-cnr') - expect(result).toBe('') - }) - - it('handleCWMPResponse catches state machine errors and fails machine', async () => { - const device = makeDevice() - const machine = await manager.startStateMachine(device, 'sess-err', { events: ['0 BOOTSTRAP'] }) - - // Force handleResponse to throw - vi.spyOn(machine.stateMachine, 'handleResponse').mockRejectedValueOnce(new Error('boom')) - - const result = await manager.handleCWMPResponse('sess-err', true, {}) - expect(result).toBe('') - expect(machine.status).toBe(MachineStatus.FAILED) - }) - - it('continue catches errors and fails machine', async () => { - const device = makeDevice() - const machine = await manager.startStateMachine(device, 'sess-cerr', { events: ['0 BOOTSTRAP'] }) - - // Force handleResponse to throw - vi.spyOn(machine.stateMachine, 'handleResponse').mockRejectedValueOnce(new Error('crash')) - - const result = await manager.continue('sess-cerr') - expect(result).toBe('') - expect(machine.status).toBe(MachineStatus.FAILED) - }) - - it('ensureSessionForRemote returns false for non-RUNNING machine', async () => { - const device = makeDevice() - const machine = await manager.startStateMachine(device, 'sess-enr', { events: ['0 BOOTSTRAP'] }) - manager.bindRemoteToDevice('remote-x', device.deviceId) - ;(machine as any).status = MachineStatus.COMPLETED - - const result = manager.ensureSessionForRemote('remote-x', 'new-sess') - expect(result).toBe(false) - }) - - it('getHashIdForRemote returns undefined for invalid JSON in remoteToDevice', async () => { - ;(manager as any).remoteToDevice.set('bad-remote', 'not-valid-json{') - const result = manager.getHashIdForRemote('bad-remote') - expect(result).toBeUndefined() - }) -}) diff --git a/tests/unit/services/cwmp/state_machine_manager_extra.spec.ts b/tests/unit/services/cwmp/state_machine_manager_extra.spec.ts index fa99692..411309a 100644 --- a/tests/unit/services/cwmp/state_machine_manager_extra.spec.ts +++ b/tests/unit/services/cwmp/state_machine_manager_extra.spec.ts @@ -169,3 +169,67 @@ describe('StateMachineManager — additional coverage', () => { expect(task!.task).toBe('get_parameter_values') }) }) + +describe('StateMachineManager — error and edge case branches', () => { + let manager: StateMachineManager + + beforeEach(() => { + vi.clearAllMocks() + ;(StateMachineManager as any).instance = null + manager = StateMachineManager.getInstance() + }) + + it('handleCWMPResponse returns empty for non-RUNNING machine', async () => { + const device = makeDevice() + const machine = await manager.startStateMachine(device, 'sess-nr', { events: ['0 BOOTSTRAP'] }) + ;(machine as any).status = MachineStatus.COMPLETED + + const result = await manager.handleCWMPResponse('sess-nr', true, {}) + expect(result).toBe('') + }) + + it('continue returns empty for non-RUNNING machine', async () => { + const device = makeDevice() + const machine = await manager.startStateMachine(device, 'sess-cnr', { events: ['0 BOOTSTRAP'] }) + ;(machine as any).status = MachineStatus.FAILED + + const result = await manager.continue('sess-cnr') + expect(result).toBe('') + }) + + it('handleCWMPResponse catches state machine errors and fails machine', async () => { + const device = makeDevice() + const machine = await manager.startStateMachine(device, 'sess-err2', { events: ['0 BOOTSTRAP'] }) + vi.spyOn(machine.stateMachine, 'handleResponse').mockRejectedValueOnce(new Error('boom')) + + const result = await manager.handleCWMPResponse('sess-err2', true, {}) + expect(result).toBe('') + expect(machine.status).toBe(MachineStatus.FAILED) + }) + + it('continue catches errors and fails machine', async () => { + const device = makeDevice() + const machine = await manager.startStateMachine(device, 'sess-cerr2', { events: ['0 BOOTSTRAP'] }) + vi.spyOn(machine.stateMachine, 'handleResponse').mockRejectedValueOnce(new Error('crash')) + + const result = await manager.continue('sess-cerr2') + expect(result).toBe('') + expect(machine.status).toBe(MachineStatus.FAILED) + }) + + it('ensureSessionForRemote returns false for non-RUNNING machine', async () => { + const device = makeDevice() + const machine = await manager.startStateMachine(device, 'sess-enr2', { events: ['0 BOOTSTRAP'] }) + manager.bindRemoteToDevice('remote-xx', device.deviceId) + ;(machine as any).status = MachineStatus.COMPLETED + + const result = manager.ensureSessionForRemote('remote-xx', 'new-sess') + expect(result).toBe(false) + }) + + it('getHashIdForRemote returns undefined for invalid JSON', () => { + ;(manager as any).remoteToDevice.set('bad-remote', 'not-valid-json{') + expect(manager.getHashIdForRemote('bad-remote')).toBeUndefined() + }) +}) + diff --git a/tests/unit/services/cwmp/task_queue.spec.ts b/tests/unit/services/cwmp/task_queue.spec.ts index 846a8c1..b9053df 100644 --- a/tests/unit/services/cwmp/task_queue.spec.ts +++ b/tests/unit/services/cwmp/task_queue.spec.ts @@ -189,3 +189,42 @@ describe('CWMPTaskQueue', () => { }) }) }) + +describe('CWMPTaskQueue — branch coverage', () => { + it('executeTaskCallback catches callback errors', async () => { + const tq = new CWMPTaskQueue() + const deviceId = new DeviceId('X', 'Y', 'Z', 'S') + + const throwingCallback = vi.fn().mockRejectedValue(new Error('callback crash')) + const { taskId } = tq.addTask(deviceId, 'get_parameter_values', [], throwingCallback) + + const response = await tq.executeTaskCallback(taskId, ResultStatus.Success, {}) + expect(response).toBe('') + }) + + it('clearPendingTasksForDevice removes PENDING tasks and keeps SENT', () => { + const tq = new CWMPTaskQueue() + const deviceId = new DeviceId('X', 'Y', 'Z', 'S') + + tq.addCWMPTask(deviceId, 'get_parameter_values', ['a']) // PENDING + const { taskId: sentId } = tq.addTask(deviceId, 'set_parameter_values', {}, async () => '') // SENT + + tq.clearPendingTasksForDevice(deviceId) + + const tasks = (tq as any).tasks as Map + expect(tasks.has(sentId)).toBe(true) + }) + + it('registerCallback adds callback for existing task', () => { + const tq = new CWMPTaskQueue() + const deviceId = new DeviceId('X', 'Y', 'Z', 'S') + const task = tq.addCWMPTask(deviceId, 'reboot', {}) + + const cb = vi.fn().mockResolvedValue('') + tq.registerCallback(task.id, cb) + + const callbacks = (tq as any).taskCallbacks as Map + expect(callbacks.has(task.id)).toBe(true) + }) +}) +