- state_machine_manager_coverage: remove duplicate assertion expecting 'idle', state can legitimately be 'critical-reads' when gpnStatus=unsupported - integration/device_model + ttl_expiration: add MongoDB connection guard (skip tests when no local DB available, timeout 2s) Generated with [Continue](https://continue.dev) Co-Authored-By: Continue <noreply@continue.dev>
202 lines
8.2 KiB
TypeScript
202 lines
8.2 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({}),
|
|
},
|
|
}))
|
|
|
|
import { StateMachineManager, MachineStatus } from '../../../../app/services/cwmp/state_machine_manager.js'
|
|
import { Device } from '../../../../app/domain/device/device.js'
|
|
import { DeviceId } from '../../../../app/domain/device/device_id.js'
|
|
import { ResultStatus } from '../../../../app/services/cwmp/task_queue.js'
|
|
import CommandModel from '../../../../app/models/command.js'
|
|
|
|
function makeDevice(gpnStatus: string = 'unsupported'): Device {
|
|
const device = new Device(new DeviceId('Sagemcom', 'F8B7FF', 'Livebox 4', 'LB4-TEST-001'))
|
|
device.setDataModel('tr-181')
|
|
device.setGpnStatus(gpnStatus as any)
|
|
return device
|
|
}
|
|
|
|
describe('StateMachineManager — coverage', () => {
|
|
let manager: StateMachineManager
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
;(StateMachineManager as any).instance = null
|
|
manager = StateMachineManager.getInstance()
|
|
})
|
|
|
|
// --- Event routing ---
|
|
|
|
it('routes BOOTSTRAP event to MainStateMachine', async () => {
|
|
const device = makeDevice()
|
|
const machine = await manager.startStateMachine(device, 'sess-1', { events: ['0 BOOTSTRAP'] })
|
|
expect(machine.status).toBe(MachineStatus.RUNNING)
|
|
// MainStateMachine has getCurrentStateName
|
|
expect(machine.stateMachine.getCurrentStateName()).toBeDefined()
|
|
})
|
|
|
|
it('routes CONNECTION_REQUEST to ImmediateCommandStateMachine', async () => {
|
|
const device = makeDevice()
|
|
const machine = await manager.startStateMachine(device, 'sess-2', { events: ['6 CONNECTION REQUEST'] })
|
|
expect(machine.status).toBe(MachineStatus.RUNNING)
|
|
expect(machine.stateMachine.getCurrentStateName()).toBe('executing-commands')
|
|
})
|
|
|
|
it('routes PERIODIC to MainStateMachine', async () => {
|
|
const device = makeDevice()
|
|
const machine = await manager.startStateMachine(device, 'sess-3', { events: ['2 PERIODIC'] })
|
|
expect(machine.status).toBe(MachineStatus.RUNNING)
|
|
expect(machine.stateMachine.getCurrentStateName()).toBe('idle')
|
|
})
|
|
|
|
it('forces MainStateMachine when NI commands exist', async () => {
|
|
// Override the mock to return count > 0
|
|
const collection = CommandModel.getCollection()
|
|
vi.mocked(collection.countDocuments).mockResolvedValueOnce(3)
|
|
const device = makeDevice()
|
|
const machine = await manager.startStateMachine(device, 'sess-4', { events: ['6 CONNECTION REQUEST'] })
|
|
// Should be MainStateMachine (idle) not ImmediateCommandStateMachine
|
|
expect(machine.stateMachine.getCurrentStateName()).toBe('idle')
|
|
})
|
|
|
|
it('preempts ImmediateCommandStateMachine on BOOT event with same session', async () => {
|
|
const device = makeDevice()
|
|
// First: create ImmediateCommandStateMachine
|
|
const machine1 = await manager.startStateMachine(device, 'sess-5', { events: ['6 CONNECTION REQUEST'] })
|
|
expect(machine1.stateMachine.getCurrentStateName()).toBe('executing-commands')
|
|
|
|
// Second: BOOT event arrives on same session (same TCP connection, new Inform)
|
|
const machine2 = await manager.startStateMachine(device, 'sess-5', { events: ['1 BOOT'] })
|
|
// Should have preempted to MainStateMachine (same machine instance, new state machine inside)
|
|
// Since gpnStatus=unsupported, the machine starts in critical-reads (or idle if GPV skipped)
|
|
const stateName = machine2.stateMachine.getCurrentStateName()
|
|
expect(stateName === 'idle' || stateName === 'critical-reads').toBe(true)
|
|
})
|
|
|
|
// --- continue() ---
|
|
|
|
it('continue() calls handleResponse on state machine', async () => {
|
|
const device = makeDevice()
|
|
await manager.startStateMachine(device, 'sess-6', { events: ['0 BOOTSTRAP'] })
|
|
// continue sends empty POST handling
|
|
const response = await manager.continue('sess-6')
|
|
// Should have produced a GPV (critical reads since gpn=unsupported)
|
|
expect(response).toContain('GetParameterValues')
|
|
})
|
|
|
|
it('continue() treats empty body as task failure when lastTaskId is set', async () => {
|
|
const device = makeDevice()
|
|
const machine = await manager.startStateMachine(device, 'sess-7', { events: ['0 BOOTSTRAP'] })
|
|
// Manually set lastTaskId to simulate in-flight task
|
|
machine.lastTaskId = 'fake-task-id'
|
|
// Add a fake task to the queue so failTask doesn't error
|
|
manager.getTaskQueue().addCWMPTask(device.deviceId, 'get_parameter_values', [])
|
|
|
|
const response = await manager.continue('sess-7')
|
|
// Should have treated as failure and advanced the state machine
|
|
expect(machine.lastTaskId).toBeUndefined()
|
|
})
|
|
|
|
it('continue() returns empty for unknown session', async () => {
|
|
const response = await manager.continue('unknown-session')
|
|
expect(response).toBe('')
|
|
})
|
|
|
|
// --- handleCWMPResponse() ---
|
|
|
|
it('handleCWMPResponse() correlates response with lastTaskId and executes callback', async () => {
|
|
const device = makeDevice()
|
|
const machine = await manager.startStateMachine(device, 'sess-8', { events: ['0 BOOTSTRAP'] })
|
|
|
|
// Start state machine (sends GPV)
|
|
await manager.continue('sess-8')
|
|
|
|
// Manually set lastTaskId
|
|
const taskId = 'test-task-123'
|
|
machine.lastTaskId = taskId
|
|
const callbackFn = vi.fn().mockResolvedValue('')
|
|
manager.getTaskQueue()['taskCallbacks'].set(taskId, callbackFn)
|
|
manager.getTaskQueue()['tasks'].set(taskId, { id: taskId, deviceId: device.deviceId, type: 'get_parameter_values', payload: [], status: 'SENT' as any, createdAt: new Date() })
|
|
|
|
// Send success response
|
|
await manager.handleCWMPResponse('sess-8', true, { ParameterList: { ParameterValueStruct: [] } })
|
|
|
|
// Callback should have been invoked
|
|
expect(callbackFn).toHaveBeenCalled()
|
|
expect(machine.lastTaskId).toBeUndefined()
|
|
})
|
|
|
|
it('handleCWMPResponse() returns empty for unknown session', async () => {
|
|
const response = await manager.handleCWMPResponse('no-such-session', true, {})
|
|
expect(response).toBe('')
|
|
})
|
|
|
|
// --- Session helpers ---
|
|
|
|
it('hasSession returns false for unknown session', () => {
|
|
expect(manager.hasSession('nonexistent')).toBe(false)
|
|
})
|
|
|
|
it('hasSession returns true for registered session', async () => {
|
|
const device = makeDevice()
|
|
await manager.startStateMachine(device, 'sess-9', { events: ['0 BOOTSTRAP'] })
|
|
expect(manager.hasSession('sess-9')).toBe(true)
|
|
})
|
|
|
|
it('ensureSessionForRemote rebinds session correctly', async () => {
|
|
const device = makeDevice()
|
|
await manager.startStateMachine(device, 'sess-10', { events: ['0 BOOTSTRAP'] })
|
|
manager.bindRemoteToDevice('remote-1', device.deviceId)
|
|
|
|
const result = manager.ensureSessionForRemote('remote-1', 'sess-10-new')
|
|
expect(result).toBe(true)
|
|
expect(manager.hasSession('sess-10-new')).toBe(true)
|
|
expect(manager.hasSession('sess-10')).toBe(false)
|
|
})
|
|
|
|
it('ensureSessionForRemote returns false for unknown remote', () => {
|
|
expect(manager.ensureSessionForRemote('unknown-remote', 'sess')).toBe(false)
|
|
})
|
|
|
|
// --- Cleanup ---
|
|
|
|
it('cleanupCompletedMachines removes old completed machines', async () => {
|
|
const device = makeDevice()
|
|
const machine = await manager.startStateMachine(device, 'sess-11', { events: ['0 BOOTSTRAP'] })
|
|
// Force complete and set old completion date
|
|
;(machine as any).status = MachineStatus.COMPLETED
|
|
;(machine as any).completedAt = new Date(Date.now() - 2 * 60 * 60 * 1000) // 2h ago
|
|
|
|
// Verify machine exists in internal map before cleanup
|
|
const machinesMap = (manager as any).machines as Map<string, any>
|
|
expect(machinesMap.has(machine.id)).toBe(true)
|
|
|
|
manager.cleanupCompletedMachines(1)
|
|
|
|
// Machine should be removed from internal map
|
|
expect(machinesMap.has(machine.id)).toBe(false)
|
|
})
|
|
})
|