- Fix state machine start timing: don't call start() in startStateMachine, let the CPE's first empty body trigger the state machine via continue(). This ensures GPN is sent at the correct point in the TR-069 session. - Add unit tests for reclassifyDeviceFromCatalog (10 tests) - Add unit tests for MainStateMachine transitions and GPN callbacks (11 tests) - Test both scenarios: Huawei (GPN supported) and Livebox (GPN unsupported + fallback) - Fix tr069_trace_service test path assertion (config/ → data/) - Update steering doc with Livebox bootstrap command
318 lines
13 KiB
TypeScript
318 lines
13 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
// Mock AdonisJS logger before imports
|
|
vi.mock('@adonisjs/core/services/logger', () => ({
|
|
default: {
|
|
info: vi.fn(),
|
|
debug: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
},
|
|
}))
|
|
|
|
// Mock DeviceModel to avoid MongoDB dependency
|
|
vi.mock('../../../app/models/device.js', () => ({
|
|
default: {
|
|
createOrUpdate: vi.fn().mockResolvedValue({}),
|
|
findByDeviceId: vi.fn().mockResolvedValue(null),
|
|
},
|
|
}))
|
|
|
|
// 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),
|
|
}),
|
|
},
|
|
}))
|
|
|
|
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 { DeviceProperty, DeviceParameter } from '../../../app/domain/device/device_parameter.js'
|
|
import { CWMPTaskQueue } from '../../../app/services/cwmp/task_queue.js'
|
|
import { ParameterType } from '../../../app/domain/shared/types.js'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function makeTr181Device(): Device {
|
|
const device = new Device(new DeviceId('Sagemcom', 'F8B7FF', 'Livebox 4', 'LB4-TEST-001'))
|
|
device.setDataModel('tr-181')
|
|
return device
|
|
}
|
|
|
|
function makeTr098Device(): Device {
|
|
const device = new Device(new DeviceId('Huawei Technologies Co., Ltd.', '202BC1', 'BM632w', '000000'))
|
|
device.setDataModel('tr-098')
|
|
return device
|
|
}
|
|
|
|
function makeUnknownDevice(): Device {
|
|
const device = new Device(new DeviceId('Unknown', '000000', 'Generic', 'SN001'))
|
|
device.setDataModel('unknown')
|
|
return device
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MainStateMachine — Initial state
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('MainStateMachine — initial state', () => {
|
|
it('starts in idle state', () => {
|
|
const device = makeTr181Device()
|
|
const taskQueue = new CWMPTaskQueue()
|
|
const sm = new MainStateMachine(device, 'session-1', taskQueue)
|
|
|
|
expect(sm.getCurrentStateName()).toBe('idle')
|
|
expect(sm.isCompleted()).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MainStateMachine — TR-181 device flow (GPN on Device.ManagementServer.)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('MainStateMachine — TR-181 device', () => {
|
|
let device: Device
|
|
let taskQueue: CWMPTaskQueue
|
|
let sm: MainStateMachine
|
|
|
|
beforeEach(() => {
|
|
device = makeTr181Device()
|
|
taskQueue = new CWMPTaskQueue()
|
|
sm = new MainStateMachine(device, 'session-1', taskQueue)
|
|
})
|
|
|
|
it('transitions to discovering-parameters and sends GPN for Device.ManagementServer.', async () => {
|
|
const response = await sm.start()
|
|
|
|
expect(sm.getCurrentStateName()).toBe('discovering-parameters')
|
|
// Response should be a SOAP envelope with GetParameterNames
|
|
expect(response).toContain('GetParameterNames')
|
|
expect(response).toContain('Device.ManagementServer.')
|
|
})
|
|
|
|
it('advances past discovering-parameters after receiving a response', async () => {
|
|
await sm.start() // sends GPN, state = discovering-parameters
|
|
|
|
// Simulate any response (success or failure) — state machine advances
|
|
await sm.handleResponse(true, {})
|
|
|
|
// Should have moved past discovering-parameters (no more paths in queue)
|
|
expect(sm.getCurrentStateName()).not.toBe('discovering-parameters')
|
|
expect(sm.getCurrentStateName()).not.toBe('idle')
|
|
})
|
|
|
|
it('GPN callback sets gpnStatus to unsupported on fault code 9000', async () => {
|
|
await sm.start()
|
|
|
|
// Get the task from the queue and execute its callback manually
|
|
const tasks = Array.from((taskQueue as any).tasks.values())
|
|
const gpnTask = tasks.find((t: any) => t.type === 'get_parameter_names')
|
|
expect(gpnTask).toBeDefined()
|
|
|
|
// Execute the callback with error
|
|
await taskQueue.executeTaskCallback(gpnTask!.id, 'error' as any, {
|
|
FaultCode: 9000,
|
|
FaultString: 'Method not supported',
|
|
})
|
|
|
|
expect(device.gpnStatus).toBe('unsupported')
|
|
})
|
|
|
|
it('GPN callback sets gpnStatus to supported on success', async () => {
|
|
await sm.start()
|
|
|
|
const tasks = Array.from((taskQueue as any).tasks.values())
|
|
const gpnTask = tasks.find((t: any) => t.type === 'get_parameter_names')
|
|
expect(gpnTask).toBeDefined()
|
|
|
|
// Execute the callback with success
|
|
await taskQueue.executeTaskCallback(gpnTask!.id, 'success' as any, {
|
|
ParameterList: {
|
|
ParameterInfoStruct: [
|
|
{ Name: 'Device.ManagementServer.URL', Writable: true },
|
|
{ Name: 'Device.ManagementServer.ParameterKey', Writable: false },
|
|
],
|
|
},
|
|
})
|
|
|
|
expect(device.gpnStatus).toBe('supported')
|
|
})
|
|
|
|
it('GPN callback organizes parameters correctly on success', async () => {
|
|
device.setProperty(new DeviceProperty('Device.ManagementServer.URL', 'http://acs', ParameterType.STRING))
|
|
device.setProperty(new DeviceProperty('Device.ManagementServer.ParameterKey', '', ParameterType.STRING))
|
|
|
|
await sm.start()
|
|
|
|
const tasks = Array.from((taskQueue as any).tasks.values())
|
|
const gpnTask = tasks.find((t: any) => t.type === 'get_parameter_names')
|
|
|
|
await taskQueue.executeTaskCallback(gpnTask!.id, 'success' as any, {
|
|
ParameterList: {
|
|
ParameterInfoStruct: [
|
|
{ Name: 'Device.ManagementServer.URL', Writable: true },
|
|
{ Name: 'Device.ManagementServer.ParameterKey', Writable: false },
|
|
],
|
|
},
|
|
})
|
|
|
|
// URL was property + writable in GPN → should be parameter now
|
|
expect(device.getParameter('Device.ManagementServer.URL')).not.toBeNull()
|
|
// ParameterKey was property + NOT writable → stays property
|
|
expect(device.getProperty('Device.ManagementServer.ParameterKey')).not.toBeNull()
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MainStateMachine — TR-098 device flow (GPN on InternetGatewayDevice.ManagementServer.)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('MainStateMachine — TR-098 device', () => {
|
|
let device: Device
|
|
let taskQueue: CWMPTaskQueue
|
|
let sm: MainStateMachine
|
|
|
|
beforeEach(() => {
|
|
device = makeTr098Device()
|
|
taskQueue = new CWMPTaskQueue()
|
|
sm = new MainStateMachine(device, 'session-1', taskQueue)
|
|
})
|
|
|
|
it('sends GPN for InternetGatewayDevice.ManagementServer.', async () => {
|
|
const response = await sm.start()
|
|
|
|
expect(sm.getCurrentStateName()).toBe('discovering-parameters')
|
|
expect(response).toContain('GetParameterNames')
|
|
expect(response).toContain('InternetGatewayDevice.ManagementServer.')
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MainStateMachine — Unknown data model
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('MainStateMachine — unknown data model', () => {
|
|
it('skips GPN and goes directly to configuration-evaluation', async () => {
|
|
const device = makeUnknownDevice()
|
|
const taskQueue = new CWMPTaskQueue()
|
|
const sm = new MainStateMachine(device, 'session-1', taskQueue)
|
|
|
|
await sm.start()
|
|
|
|
// Should skip discovering-parameters entirely
|
|
expect(sm.getCurrentStateName()).not.toBe('discovering-parameters')
|
|
expect(sm.getCurrentStateName()).not.toBe('idle')
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MainStateMachine — CompletedState reclassification
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('MainStateMachine — CompletedState reclassification', () => {
|
|
it('reclassifies writable properties on completion when GPN unsupported', async () => {
|
|
const device = makeTr181Device()
|
|
device.setGpnStatus('unsupported')
|
|
// Add writable property that should be reclassified
|
|
device.setProperty(new DeviceProperty('Device.ManagementServer.PeriodicInformInterval', '60', ParameterType.UNSIGNED_INT))
|
|
// Add read-only property that should stay
|
|
device.setProperty(new DeviceProperty('Device.DeviceInfo.Manufacturer', 'Sagemcom', ParameterType.STRING))
|
|
|
|
const taskQueue = new CWMPTaskQueue()
|
|
const sm = new MainStateMachine(device, 'session-1', taskQueue)
|
|
|
|
// Start → since dataModel is tr-181, it will try GPN
|
|
await sm.start()
|
|
|
|
// Simulate GPN failure
|
|
await sm.handleResponse(true, {})
|
|
|
|
// The state machine should eventually complete
|
|
// Let it run through (no config match → completes)
|
|
let guard = 0
|
|
while (!sm.isCompleted() && guard++ < 10) {
|
|
await sm.handleResponse(true, {})
|
|
}
|
|
|
|
// After completion, PeriodicInformInterval should be a parameter
|
|
expect(device.getParameter('Device.ManagementServer.PeriodicInformInterval')).not.toBeNull()
|
|
expect(device.getProperty('Device.ManagementServer.PeriodicInformInterval')).toBeNull()
|
|
// Manufacturer should stay as property
|
|
expect(device.getProperty('Device.DeviceInfo.Manufacturer')).not.toBeNull()
|
|
expect(device.getParameter('Device.DeviceInfo.Manufacturer')).toBeNull()
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DeviceActionExecutor — reclassification after SPV (Livebox fallback scenario)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('DeviceActionExecutor — reclassification after updateFromValues', () => {
|
|
it('reclassifies properties added via SPV callback when GPN unsupported', async () => {
|
|
const device = makeTr181Device()
|
|
device.setGpnStatus('unsupported')
|
|
// Device starts with only read-only properties (like after first Inform)
|
|
device.setProperty(new DeviceProperty('Device.DeviceInfo.Manufacturer', 'Sagemcom', ParameterType.STRING))
|
|
|
|
const taskQueue = new CWMPTaskQueue()
|
|
|
|
// Import DeviceActionExecutor
|
|
const { DeviceActionExecutor } = await import('../../../app/services/cwmp/device_action_executor.js')
|
|
const executor = new DeviceActionExecutor(taskQueue)
|
|
|
|
// Simulate SPV for ProvisioningCode (like ConfigurationEvaluationState does)
|
|
const spvXml = await executor.applySetParameterValues(
|
|
device,
|
|
[{ parameterName: 'Device.DeviceInfo.ProvisioningCode', value: 'test-code' }],
|
|
async () => ''
|
|
)
|
|
|
|
expect(spvXml).toContain('SetParameterValues')
|
|
|
|
// Get the task and execute callback with success (simulating CPE response)
|
|
const tasks = Array.from((taskQueue as any).tasks.values())
|
|
const spvTask = tasks.find((t: any) => t.type === 'set_parameter_values')
|
|
expect(spvTask).toBeDefined()
|
|
|
|
await taskQueue.executeTaskCallback(spvTask!.id, 'success' as any, { Status: 0 })
|
|
|
|
// ProvisioningCode should now be a parameter (reclassified after updateFromValues)
|
|
expect(device.getParameter('Device.DeviceInfo.ProvisioningCode')).not.toBeNull()
|
|
expect(device.getParameter('Device.DeviceInfo.ProvisioningCode')!.value).toBe('test-code')
|
|
expect(device.getProperty('Device.DeviceInfo.ProvisioningCode')).toBeNull()
|
|
})
|
|
|
|
it('does NOT reclassify when GPN is supported (live data is authoritative)', async () => {
|
|
const device = makeTr181Device()
|
|
device.setGpnStatus('supported')
|
|
device.setProperty(new DeviceProperty('Device.DeviceInfo.Manufacturer', 'Sagemcom', ParameterType.STRING))
|
|
|
|
const taskQueue = new CWMPTaskQueue()
|
|
const { DeviceActionExecutor } = await import('../../../app/services/cwmp/device_action_executor.js')
|
|
const executor = new DeviceActionExecutor(taskQueue)
|
|
|
|
// SPV for a writable param — but GPN is supported so no reclassification
|
|
await executor.applySetParameterValues(
|
|
device,
|
|
[{ parameterName: 'Device.ManagementServer.PeriodicInformInterval', value: '120' }],
|
|
async () => ''
|
|
)
|
|
|
|
const tasks = Array.from((taskQueue as any).tasks.values())
|
|
const spvTask = tasks.find((t: any) => t.type === 'set_parameter_values')
|
|
await taskQueue.executeTaskCallback(spvTask!.id, 'success' as any, { Status: 0 })
|
|
|
|
// PeriodicInformInterval was added via updateFromValues as a property
|
|
// But since GPN is supported, reclassification is NOT called
|
|
// It stays as property (the GPN response is the authority)
|
|
expect(device.getProperty('Device.ManagementServer.PeriodicInformInterval')).not.toBeNull()
|
|
expect(device.getParameter('Device.ManagementServer.PeriodicInformInterval')).toBeNull()
|
|
})
|
|
})
|