Files
adam-acs/tests/unit/services/cwmp/critical_reads_response.spec.ts
T
Kevin L'HereecandContinue aad93911e4 feat: TR-069 CWMP state machine hardening + exchange viewer improvements
- 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>
2026-06-08 12:49:00 +00:00

165 lines
5.5 KiB
TypeScript

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 configuration evaluation (skip config eval for these tests)
vi.mock('../../../../app/services/cwmp/configuration_evaluation_state.js', () => ({
ConfigurationEvaluationState: class {
name = 'configuration-evaluation'
async handle() { return { isComplete: true, response: '' } }
},
}))
// Mock reclassify
vi.mock('../../../../app/services/parameter_writability.js', () => ({
reclassifyDeviceFromCatalog: vi.fn(),
}))
import { MainStateMachine } from '../../../../app/services/cwmp/main_state_machine.js'
import { Device } from '../../../../app/domain/device/device.js'
import { DeviceId } from '../../../../app/domain/device/device_id.js'
import { CWMPTaskQueue } from '../../../../app/services/cwmp/task_queue.js'
function makeDevice(): Device {
const device = new Device(new DeviceId('Sagemcom', 'F8B7FF', 'Livebox 4', 'LB4-TEST-001'))
device.setDataModel('tr-181')
device.setGpnStatus('unsupported') // Skip GPN, go to CriticalReads
return device
}
describe('CriticalReadsState — GPV response processing', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('processes GPV response and updates device values', async () => {
const device = makeDevice()
const taskQueue = new CWMPTaskQueue()
const sm = new MainStateMachine(device, 'session-1', taskQueue)
// Start: IdleState → CriticalReadsState → sends GPV
const gpvRequest = await sm.start()
expect(gpvRequest).toContain('GetParameterValues')
// Simulate GPVResponse with ParameterValueStruct
await sm.handleResponse(true, {
ParameterList: {
ParameterValueStruct: [
{ Name: 'Device.DeviceInfo.HardwareVersion', Value: { _: 'v3.0', '$xsi:type': 'xsd:string' } },
{ Name: 'Device.DeviceInfo.SoftwareVersion', Value: { _: 'SR50-5.0.0', '$xsi:type': 'xsd:string' } },
{ Name: 'Device.DeviceInfo.ModelName', Value: { _: 'Livebox 4', '$xsi:type': 'xsd:string' } },
],
},
})
// Device should have the values stored
const hwProp = device.getProperty('Device.DeviceInfo.HardwareVersion')
expect(hwProp).not.toBeNull()
expect(hwProp!.value).toBe('v3.0')
const swProp = device.getProperty('Device.DeviceInfo.SoftwareVersion')
expect(swProp).not.toBeNull()
expect(swProp!.value).toBe('SR50-5.0.0')
})
it('normalizes Name fields from object to string when xsi:type attribute is present', async () => {
const device = makeDevice()
const taskQueue = new CWMPTaskQueue()
const sm = new MainStateMachine(device, 'session-1', taskQueue)
await sm.start()
// Simulate GPVResponse where Name has xsi:type attribute (parsed as object by XMLParser)
await sm.handleResponse(true, {
ParameterList: {
ParameterValueStruct: [
{
Name: { _: 'Device.DeviceInfo.HardwareVersion', '$xsi:type': 'xsd:string' },
Value: { _: 'v2.1', '$xsi:type': 'xsd:string' },
},
],
},
})
// Should normalize Name._ to string and store correctly
const hwProp = device.getProperty('Device.DeviceInfo.HardwareVersion')
expect(hwProp).not.toBeNull()
expect(hwProp!.value).toBe('v2.1')
})
it('handles empty ParameterValueStruct gracefully', async () => {
const device = makeDevice()
const taskQueue = new CWMPTaskQueue()
const sm = new MainStateMachine(device, 'session-1', taskQueue)
await sm.start()
// Simulate empty GPV response
await sm.handleResponse(true, { ParameterList: { ParameterValueStruct: [] } })
// Should not throw, machine should continue
expect(sm.getCurrentStateName()).not.toBe('critical-reads')
})
it('handles missing ParameterList gracefully', async () => {
const device = makeDevice()
const taskQueue = new CWMPTaskQueue()
const sm = new MainStateMachine(device, 'session-1', taskQueue)
await sm.start()
// Simulate response with no ParameterList
await sm.handleResponse(true, {})
// Should not throw, machine should continue
expect(sm.getCurrentStateName()).not.toBe('critical-reads')
})
it('transitions to configuration-evaluation after processing', async () => {
const device = makeDevice()
const taskQueue = new CWMPTaskQueue()
const sm = new MainStateMachine(device, 'session-1', taskQueue)
await sm.start()
await sm.handleResponse(true, {
ParameterList: {
ParameterValueStruct: [
{ Name: 'Device.DeviceInfo.HardwareVersion', Value: { _: 'v1', '$xsi:type': 'xsd:string' } },
],
},
})
// Should have transitioned past critical-reads to configuration-evaluation
expect(sm.getCurrentStateName()).not.toBe('critical-reads')
expect(sm.getCurrentStateName()).toBe('configuration-evaluation')
})
})