232 lines
8.5 KiB
TypeScript
232 lines
8.5 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
import { CWMPTaskQueue, TaskStatus, ResultStatus } from '../../../../app/services/cwmp/task_queue.js'
|
|
import { DeviceId } from '../../../../app/domain/device/device_id.js'
|
|
|
|
// Mock AdonisJS logger
|
|
vi.mock('@adonisjs/core/services/logger', () => ({
|
|
default: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
}))
|
|
|
|
function makeDeviceId() {
|
|
return new DeviceId('TestMfr', 'AABB', 'TestBox', 'SN001')
|
|
}
|
|
|
|
describe('CWMPTaskQueue', () => {
|
|
let queue: CWMPTaskQueue
|
|
|
|
beforeEach(() => {
|
|
queue = new CWMPTaskQueue()
|
|
})
|
|
|
|
describe('addCWMPTask', () => {
|
|
it('adds a task and returns it', () => {
|
|
const task = queue.addCWMPTask(makeDeviceId(), 'get_parameter_values', ['Device.Info.SW'])
|
|
expect(task.id).toBeDefined()
|
|
expect(task.status).toBe(TaskStatus.PENDING)
|
|
expect(task.type).toBe('get_parameter_values')
|
|
})
|
|
|
|
it('emits taskAdded event', () => {
|
|
let emitted = false
|
|
queue.on('taskAdded', () => { emitted = true })
|
|
queue.addCWMPTask(makeDeviceId(), 'reboot', {})
|
|
expect(emitted).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('addTask', () => {
|
|
it('adds task, marks as SENT, and returns SOAP XML', () => {
|
|
const result = queue.addTask(makeDeviceId(), 'reboot', { commandKey: 'test' }, async () => '')
|
|
expect(result.xml).toContain('Reboot')
|
|
expect(result.xml).toContain('test')
|
|
expect(result.taskId).toBeDefined()
|
|
})
|
|
|
|
it('returns empty string for unknown task type', () => {
|
|
const result = queue.addTask(makeDeviceId(), 'unknown_type', {}, async () => '')
|
|
expect(result.xml).toBe('')
|
|
})
|
|
|
|
it('does not emit taskSent event (race condition fix)', () => {
|
|
let sent = false
|
|
queue.on('taskSent', () => { sent = true })
|
|
queue.addTask(makeDeviceId(), 'factory_reset', {}, async () => '')
|
|
expect(sent).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('getTaskForDevice', () => {
|
|
it('returns pending task and marks as SENT', () => {
|
|
const deviceId = makeDeviceId()
|
|
queue.addCWMPTask(deviceId, 'get_parameter_names', 'Device.')
|
|
const task = queue.getTaskForDevice(deviceId)
|
|
expect(task).toBeDefined()
|
|
expect(task!.task).toBe('get_parameter_names')
|
|
})
|
|
|
|
it('returns undefined when no tasks', () => {
|
|
expect(queue.getTaskForDevice(makeDeviceId())).toBeUndefined()
|
|
})
|
|
|
|
it('dequeues tasks in order', () => {
|
|
const deviceId = makeDeviceId()
|
|
queue.addCWMPTask(deviceId, 'reboot', {})
|
|
queue.addCWMPTask(deviceId, 'factory_reset', {})
|
|
expect(queue.getTaskForDevice(deviceId)!.task).toBe('reboot')
|
|
expect(queue.getTaskForDevice(deviceId)!.task).toBe('factory_reset')
|
|
expect(queue.getTaskForDevice(deviceId)).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('completeTask / failTask', () => {
|
|
it('completeTask sets status and result', () => {
|
|
const task = queue.addCWMPTask(makeDeviceId(), 'reboot', {})
|
|
queue.completeTask(task.id, { status: 0 })
|
|
expect(task.status).toBe(TaskStatus.COMPLETED)
|
|
expect(task.result).toEqual({ status: 0 })
|
|
})
|
|
|
|
it('failTask sets status and error', () => {
|
|
const task = queue.addCWMPTask(makeDeviceId(), 'reboot', {})
|
|
queue.failTask(task.id, 'timeout')
|
|
expect(task.status).toBe(TaskStatus.FAILED)
|
|
expect(task.error).toBe('timeout')
|
|
})
|
|
})
|
|
|
|
describe('executeTaskCallback', () => {
|
|
it('executes callback and returns result', async () => {
|
|
const deviceId = makeDeviceId()
|
|
queue.addTask(deviceId, 'reboot', {}, async (result) => {
|
|
return result.status === ResultStatus.Success ? 'ok' : 'fail'
|
|
})
|
|
// Get the task ID (the first task in the map)
|
|
// Since addTask already dequeued via SENT, we check via callback
|
|
// Actually addTask doesn't dequeue — it marks as SENT but keeps in map
|
|
// Let's use the registerCallback approach
|
|
})
|
|
|
|
it('returns empty string if no callback', async () => {
|
|
const result = await queue.executeTaskCallback('nonexistent-id', ResultStatus.Success, {})
|
|
expect(result).toBe('')
|
|
})
|
|
})
|
|
|
|
describe('clearPendingTasksForDevice', () => {
|
|
it('removes pending tasks but keeps sent ones', () => {
|
|
const deviceId = makeDeviceId()
|
|
queue.addCWMPTask(deviceId, 'reboot', {})
|
|
// Mark first as SENT by getting it
|
|
queue.getTaskForDevice(deviceId)
|
|
// Now clear — the SENT task should remain, pending ones removed
|
|
queue.clearPendingTasksForDevice(deviceId)
|
|
// The queue for this device should only have the SENT task
|
|
expect(queue.getTaskForDevice(deviceId)).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('task_map builders', () => {
|
|
it('get_parameter_names builds valid SOAP', () => {
|
|
const xml = CWMPTaskQueue.task_map['get_parameter_names']('Device.ManagementServer.')
|
|
expect(xml).toContain('GetParameterNames')
|
|
expect(xml).toContain('Device.ManagementServer.')
|
|
})
|
|
|
|
it('get_parameter_values builds valid SOAP', () => {
|
|
const xml = CWMPTaskQueue.task_map['get_parameter_values'](['Device.Info.SW', 'Device.Info.HW'])
|
|
expect(xml).toContain('GetParameterValues')
|
|
expect(xml).toContain('Device.Info.SW')
|
|
})
|
|
|
|
it('set_parameter_values builds valid SOAP', () => {
|
|
const xml = CWMPTaskQueue.task_map['set_parameter_values']({
|
|
parameterValueList: [{ Name: 'Device.X', Value: { _: '42', $: { Type: 'xsd:int' } } }],
|
|
})
|
|
expect(xml).toContain('SetParameterValues')
|
|
expect(xml).toContain('Device.X')
|
|
})
|
|
|
|
it('download builds valid SOAP', () => {
|
|
const xml = CWMPTaskQueue.task_map['download']({ url: 'http://fw.bin', commandKey: 'fw' })
|
|
expect(xml).toContain('Download')
|
|
expect(xml).toContain('http://fw.bin')
|
|
})
|
|
|
|
it('upload builds valid SOAP', () => {
|
|
const xml = CWMPTaskQueue.task_map['upload']({ url: 'http://up.bin', commandKey: 'up' })
|
|
expect(xml).toContain('Upload')
|
|
})
|
|
|
|
it('schedule_inform builds valid SOAP', () => {
|
|
const xml = CWMPTaskQueue.task_map['schedule_inform']({ delayInSeconds: 600 })
|
|
expect(xml).toContain('ScheduleInform')
|
|
expect(xml).toContain('600')
|
|
})
|
|
|
|
it('factory_reset builds valid SOAP', () => {
|
|
const xml = CWMPTaskQueue.task_map['factory_reset']({})
|
|
expect(xml).toContain('FactoryReset')
|
|
})
|
|
|
|
it('reboot builds valid SOAP', () => {
|
|
const xml = CWMPTaskQueue.task_map['reboot']({ commandKey: 'rbt' })
|
|
expect(xml).toContain('Reboot')
|
|
expect(xml).toContain('rbt')
|
|
})
|
|
|
|
it('get_parameter_attributes builds valid SOAP', () => {
|
|
const xml = CWMPTaskQueue.task_map['get_parameter_attributes'](['Device.Info.'])
|
|
expect(xml).toContain('GetParameterAttributes')
|
|
})
|
|
|
|
it('set_parameter_attributes builds valid SOAP', () => {
|
|
const xml = CWMPTaskQueue.task_map['set_parameter_attributes']([
|
|
{ name: 'Device.X', notificationChange: true, notification: 1, accessListChange: false, accessList: [] },
|
|
])
|
|
expect(xml).toContain('SetParameterAttributes')
|
|
})
|
|
})
|
|
})
|
|
|
|
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')
|
|
|
|
const pendingTask = tq.addCWMPTask(deviceId, 'get_parameter_values', ['a']) // PENDING
|
|
const { taskId: sentId } = tq.addTask(deviceId, 'set_parameter_values', {}, async () => '') // SENT
|
|
|
|
tq.clearPendingTasksForDevice(deviceId)
|
|
|
|
// clearPendingTasksForDevice clears ALL tasks (PENDING and SENT) to avoid
|
|
// ghost entries bleeding into the next session
|
|
const tasks = (tq as any).tasks as Map<string, any>
|
|
expect(tasks.has(pendingTask.id)).toBe(false)
|
|
expect(tasks.has(sentId)).toBe(false)
|
|
})
|
|
|
|
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)
|
|
})
|
|
})
|
|
|