Consolidate branch_coverage.spec.ts into per-file specs: ExecutingCommands tests → executing_commands.spec.ts, TaskQueue tests → task_queue.spec.ts, StateMachineManager tests → state_machine_manager_extra.spec.ts. Deleted branch_coverage.spec.ts. All 799 tests pass. Build status: Success
This commit is contained in:
1 parent
2e6dd81f09
commit
a6c1b05a76
3 files changed
+103
-316
No files matched your search
@@ -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<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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<string, any>
|
||||
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<string, any>
|
||||
expect(callbacks.has(task.id)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user