Files
adam-acs/tests/unit/services/cwmp/task_queue.spec.ts
T
2026-06-08 16:35:33 +02:00

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)
})
})