- 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>
142 lines
4.4 KiB
TypeScript
142 lines
4.4 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 ConfigurationModel
|
|
vi.mock('#models/configuration', () => ({
|
|
default: {
|
|
find: vi.fn().mockResolvedValue([]),
|
|
findByIdentifier: vi.fn().mockResolvedValue(null),
|
|
},
|
|
}))
|
|
|
|
// Mock firmware catalog service
|
|
vi.mock('../../../../app/services/firmware_catalog_service.js', () => ({
|
|
default: class {
|
|
async resolveUpgradePath() { return [] }
|
|
},
|
|
FirmwareCatalogService: class {
|
|
async resolveUpgradePath() { return [] }
|
|
},
|
|
}))
|
|
|
|
// Mock parameter_writability
|
|
vi.mock('../../../../app/services/parameter_writability.js', () => ({
|
|
reclassifyDeviceFromCatalog: vi.fn(),
|
|
}))
|
|
|
|
import { ConfigurationEvaluationState, ComplianceUpdateState } from '../../../../app/services/cwmp/configuration_evaluation_state.js'
|
|
import { Device } from '../../../../app/domain/device/device.js'
|
|
import { DeviceId } from '../../../../app/domain/device/device_id.js'
|
|
import { DeviceProperty } from '../../../../app/domain/device/device_parameter.js'
|
|
import { CWMPTaskQueue } from '../../../../app/services/cwmp/task_queue.js'
|
|
import { DeviceActionExecutor } from '../../../../app/services/cwmp/device_action_executor.js'
|
|
import { ParameterType } from '../../../../app/domain/shared/types.js'
|
|
import { buildProvisioningCode } from '../../../../app/utils/provisioning_code.js'
|
|
|
|
function makeDevice(): Device {
|
|
const device = new Device(new DeviceId('Sagemcom', 'F8B7FF', 'Livebox 4', 'LB4-TEST-001'))
|
|
device.setDataModel('tr-181')
|
|
device.setGpnStatus('unsupported')
|
|
return device
|
|
}
|
|
|
|
function makeContext(device: Device) {
|
|
const taskQueue = new CWMPTaskQueue()
|
|
return {
|
|
device,
|
|
session: 'test-session',
|
|
taskQueue,
|
|
services: {
|
|
deviceAction: new DeviceActionExecutor(taskQueue),
|
|
},
|
|
}
|
|
}
|
|
|
|
describe('ConfigurationEvaluationState — skip inventory when already compliant', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('skips inventory when isAlreadyCompliant is true (no matching config)', async () => {
|
|
const device = makeDevice()
|
|
const context = makeContext(device)
|
|
|
|
const state = new ConfigurationEvaluationState()
|
|
const result = await state.handle(context as any)
|
|
|
|
// With no configs in DB, evaluate returns isAlreadyCompliant: true
|
|
// State should transition forward without sending any GPN/GPV
|
|
// (no inventory, no diffs)
|
|
expect(result.response).toBeFalsy()
|
|
expect(result.nextState || result.isComplete).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
describe('ComplianceUpdateState — skip provCode SPV when matching', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('skips SPV when device already has matching provisioning code', async () => {
|
|
const device = makeDevice()
|
|
|
|
const mockConfig = { identifier: 'TestConfig', version: '1.0', desiredParameters: [], firmwareTarget: null }
|
|
const expectedProvCode = buildProvisioningCode(mockConfig as any)
|
|
|
|
// Set the device's ProvisioningCode to match what buildProvisioningCode produces
|
|
device.setProperty(new DeviceProperty(
|
|
'Device.DeviceInfo.ProvisioningCode',
|
|
expectedProvCode,
|
|
ParameterType.STRING
|
|
))
|
|
|
|
const context = makeContext(device)
|
|
|
|
const mockEngineResult = {
|
|
configuration: mockConfig,
|
|
inventoryPaths: [],
|
|
parameterDiffs: [],
|
|
firmwareMigrationPath: [],
|
|
isAlreadyCompliant: true,
|
|
skipFirmware: false,
|
|
}
|
|
|
|
const state = new ComplianceUpdateState(mockEngineResult as any)
|
|
const result = await state.handle(context as any)
|
|
|
|
// Should NOT produce a response (no SPV sent)
|
|
// Should transition to ExecutingCommandsState
|
|
expect(result.response).toBeFalsy()
|
|
expect(result.nextState).toBeDefined()
|
|
})
|
|
})
|