151 lines
5.7 KiB
TypeScript
151 lines
5.7 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest'
|
|
|
|
vi.mock('@adonisjs/core/services/logger', () => ({
|
|
default: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
}))
|
|
vi.mock('#models/device', () => ({
|
|
default: {
|
|
createOrUpdate: vi.fn().mockResolvedValue({}),
|
|
updateOne: vi.fn().mockResolvedValue({}),
|
|
},
|
|
}))
|
|
vi.mock('../../../../app/models/command.js', () => ({
|
|
default: {
|
|
getCollection: () => ({
|
|
find: vi.fn().mockReturnValue({
|
|
sort: vi.fn().mockReturnValue({ toArray: vi.fn().mockResolvedValue([]) }),
|
|
}),
|
|
}),
|
|
updateCommand: vi.fn().mockResolvedValue({}),
|
|
},
|
|
}))
|
|
|
|
import {
|
|
MainStateMachine,
|
|
ExecutingCommandsState,
|
|
} 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(dataModel: string = 'tr-181') {
|
|
const d = new Device(new DeviceId('Mfr', 'OUI', 'PC', 'SN'))
|
|
d.setDataModel(dataModel as any)
|
|
return d
|
|
}
|
|
|
|
describe('MainStateMachine', () => {
|
|
it('starts in idle state', () => {
|
|
const sm = new MainStateMachine(makeDevice(), 'sess-1', new CWMPTaskQueue())
|
|
expect(sm.getCurrentStateName()).toBe('idle')
|
|
expect(sm.isCompleted()).toBe(false)
|
|
})
|
|
|
|
it('start triggers state execution and returns response', async () => {
|
|
const sm = new MainStateMachine(makeDevice('tr-181'), 'sess-1', new CWMPTaskQueue())
|
|
const response = await sm.start()
|
|
// idle → discovering-parameters → sends GPN for Device.ManagementServer.
|
|
expect(response).toContain('GetParameterNames')
|
|
expect(sm.getCurrentStateName()).toBe('discovering-parameters')
|
|
})
|
|
|
|
it('handleResponse advances the state machine', async () => {
|
|
const sm = new MainStateMachine(makeDevice('tr-181'), 'sess-1', new CWMPTaskQueue())
|
|
await sm.start()
|
|
// Simulate GPN success response
|
|
await sm.handleResponse(true, {
|
|
ParameterList: {
|
|
ParameterInfoStruct: [{ Name: 'Device.ManagementServer.URL', Writable: '1' }],
|
|
},
|
|
})
|
|
// After GPN success, should transition to critical-reads
|
|
expect(sm.getCurrentStateName()).toBe('critical-reads')
|
|
})
|
|
|
|
it('with unknown data model, skips to critical-reads', async () => {
|
|
const sm = new MainStateMachine(makeDevice('unknown'), 'sess-1', new CWMPTaskQueue())
|
|
await sm.start()
|
|
// idle → directly to critical-reads (no GPN for unknown model)
|
|
expect(sm.getCurrentStateName()).toBe('critical-reads')
|
|
})
|
|
|
|
it('with tr-098, sends GPN for InternetGatewayDevice.ManagementServer.', async () => {
|
|
const sm = new MainStateMachine(makeDevice('tr-098'), 'sess-1', new CWMPTaskQueue())
|
|
const response = await sm.start()
|
|
expect(response).toContain('InternetGatewayDevice.ManagementServer.')
|
|
})
|
|
|
|
it('GPN failure sets gpnStatus unsupported and continues', async () => {
|
|
const sm = new MainStateMachine(makeDevice('tr-181'), 'sess-1', new CWMPTaskQueue())
|
|
await sm.start()
|
|
// Simulate GPN fault (9001 = method not supported)
|
|
await sm.handleResponse(false, { FaultCode: 9001, FaultString: 'Method not supported' })
|
|
// Should have transitioned past discovering-parameters
|
|
expect(sm.getCurrentStateName()).not.toBe('discovering-parameters')
|
|
})
|
|
|
|
it('full lifecycle: idle → discovering → critical-reads → config-eval → compliance → executing → operational', async () => {
|
|
const sm = new MainStateMachine(makeDevice('tr-181'), 'sess-1', new CWMPTaskQueue())
|
|
// Step 1: start → GPN
|
|
await sm.start()
|
|
expect(sm.getCurrentStateName()).toBe('discovering-parameters')
|
|
|
|
// Step 2: GPN response → critical-reads
|
|
await sm.handleResponse(true, { ParameterList: { ParameterInfoStruct: [] } })
|
|
expect(sm.getCurrentStateName()).toBe('critical-reads')
|
|
|
|
// Step 3+: critical reads will send GPV, after response moves to config eval
|
|
// Simulate multiple responses to drain the state machine
|
|
let guard = 0
|
|
while (!sm.isCompleted() && guard++ < 20) {
|
|
await sm.handleResponse(true, {
|
|
Status: 0,
|
|
ParameterList: { ParameterInfoStruct: [], ParameterValueStruct: [] },
|
|
})
|
|
}
|
|
expect(sm.isCompleted()).toBe(true)
|
|
})
|
|
|
|
it('error in state transitions to completed gracefully', async () => {
|
|
const sm = new MainStateMachine(makeDevice('tr-181'), 'sess-1', new CWMPTaskQueue())
|
|
await sm.start()
|
|
// Force an unexpected error by passing malformed data many times
|
|
for (let i = 0; i < 20; i++) {
|
|
await sm.handleResponse(false, { error: 'unexpected' })
|
|
if (sm.isCompleted()) break
|
|
}
|
|
// Should eventually complete (safety guard or error handling)
|
|
expect(sm.isCompleted()).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('ExecutingCommandsState', () => {
|
|
it('returns isComplete when no commands found (includeNI=true)', async () => {
|
|
const state = new ExecutingCommandsState(true)
|
|
const device = makeDevice()
|
|
const context = {
|
|
device,
|
|
session: 'sess-1',
|
|
taskQueue: new CWMPTaskQueue(),
|
|
services: { deviceAction: { getParameterValues: vi.fn() } as any },
|
|
}
|
|
const result = await state.handle(context)
|
|
// With includeNI=true and no commands, transitions to OperationalState
|
|
expect(result.nextState).toBeDefined()
|
|
expect(result.nextState!.name).toBe('operational')
|
|
})
|
|
|
|
it('returns isComplete when no commands found (includeNI=false)', async () => {
|
|
const state = new ExecutingCommandsState(false)
|
|
const device = makeDevice()
|
|
const context = {
|
|
device,
|
|
session: 'sess-1',
|
|
taskQueue: new CWMPTaskQueue(),
|
|
services: { deviceAction: {} as any },
|
|
}
|
|
const result = await state.handle(context)
|
|
expect(result.isComplete).toBe(true)
|
|
})
|
|
})
|