Add 17 branch coverage tests: ExecutingCommandsState (multi-command, GPN/SPV/SPA/Upload param formats, updateCommand error, includeNI=false), TaskQueue (callback error, clearPendingTasks, registerCallback), StateMachineManager (non-RUNNING paths, error handling, invalid JSON). All 799 tests pass. Build status: Success

This commit is contained in:
ATX Bot committed 2026-06-08 15:53:34 +02:00
1 parent c8c7cb32b7
commit 71c6e7e0e3
1 file changed
+316
@@ -0,0 +1,316 @@
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<string, any>
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<string, any>
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()
})
})