- TR-069 CWMP state machine hardening (14 phases): shared event utility, multi-event routing, GPN circuit breaker, IdleState GPN guard, expanded GPN fault codes, CriticalReadsState, ConfigEval GPN guard, task queue race condition fix, explicit error handling, terminal states (OperationalState/PartialOperationalState), consolidated reclassify, crash pattern detection, removed enqueueImmediate, state flow wiring - Fix SOAP Fault not processed by state machine (soap-env:Fault detection) - Fix GPN circuit breaker not persisting after session drops - Fix CriticalReadsState GPV response parsing (xsi:type normalization) - Fix CriticalReadsState not processing GPV response values - Skip SPV when device is already compliant (ProvisioningCode check) - Add Device.DeviceInfo.ModelName to critical reads GPV - Fix TypeError on DeviceDetail parameters view (null guards) - Log empty 204 responses in exchange trace UI - TR-181 catalog: type corrections and new entries (+6640 lines) - SequenceViewer: replace session-nav buttons with compact dropdown select - SequenceViewer: fix CPE/ACS lifeline headers wrapping to 2 lines - Add 20 unit tests for state machine fixes (SOAP Fault, GPN circuit breaker, CriticalReads, stale session, ConfigEval/ComplianceUpdate skips) Generated with [Continue](https://continue.dev) Co-Authored-By: Continue <noreply@continue.dev>
192 lines
7.1 KiB
TypeScript
192 lines
7.1 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
import { CWMPTaskQueue, TaskStatus, ResultStatus, TaskResult } 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)
|
|
const task = queue.getTaskForDevice(deviceId)
|
|
// 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', {})
|
|
const t2 = queue.addCWMPTask(deviceId, 'factory_reset', {})
|
|
// 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')
|
|
})
|
|
})
|
|
})
|