Files
adam-acs/tests/unit/services/cwmp/main_state_machine.integration.spec.ts
T

402 lines
19 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([{
identifier: 'BM632w-standard',
name: 'BM632w Standard',
version: '1.0.1',
priority: 1,
policy: 'maintain',
enabled: true,
criteria: { manufacturer: 'Huawei*', productClass: 'BM632w' },
inventory: [{ path: 'InternetGatewayDevice.ManagementServer.' }],
desiredParameters: [
{ name: 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', value: '300', type: 'unsignedInt' },
],
firmwareTarget: null,
schedule: null,
tags: [],
}]),
findOne: vi.fn().mockResolvedValue(null),
},
}))
// Mock FirmwareCatalogService
vi.mock('../../../../app/services/firmware_catalog_service.js', () => ({
default: class { async resolveUpgradePath() { return [] } },
FirmwareCatalogService: class { async resolveUpgradePath() { return [] } },
}))
// Mock schedule_window
vi.mock('../../../../app/utils/schedule_window.js', () => ({
isWithinSchedule: vi.fn().mockReturnValue(true),
}))
// 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 { DeviceParameter, DeviceProperty } from '../../../../app/domain/device/device_parameter.js'
import { CWMPTaskQueue } from '../../../../app/services/cwmp/task_queue.js'
import { ParameterType } from '../../../../app/domain/shared/types.js'
import { reclassifyDeviceFromCatalog } from '../../../../app/services/parameter_writability.js'
import { buildProvisioningCode } from '../../../../app/utils/provisioning_code.js'
import logger from '@adonisjs/core/services/logger'
/**
* Simulates two full Inform sessions from the same BM632w device.
*
* Session 1: Device is NEW (not in DB)
* → GPN test on Inform paths → classify → Critical reads → ConfigEval (GPN+GPV+SPV) → ProvCode → Done
*
* Session 2: Device is KNOWN (already classified, already compliant)
* → Skip GPN (already classified) → Critical reads → ConfigEval (skip) → Done
*/
describe('Two-session scenario: same device, first contact then reconnect', () => {
let device: Device
beforeEach(() => {
vi.clearAllMocks()
})
function makeNewDevice(): Device {
const d = new Device(new DeviceId('Huawei Technologies Co., Ltd.', '202BC1', 'BM632w', '000000'))
d.setDataModel('tr-098')
// Simulate Inform parameters (stored by handleInform before state machine starts)
const informParams = [
{ Name: 'InternetGatewayDevice.DeviceInfo.HardwareVersion', Value: { _: '40501' } },
{ Name: 'InternetGatewayDevice.DeviceInfo.SoftwareVersion', Value: { _: 'V100R001' } },
{ Name: 'InternetGatewayDevice.ManagementServer.ConnectionRequestURL', Value: { _: 'http://172.20.0.5:37547/' } },
{ Name: 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', Value: { _: '60' } },
]
d.updateFromValues(informParams)
d.setTmpParamValues(informParams)
return d
}
it('SESSION 1: new device — full GPN + classify + critical reads + config eval + SPV + provCode', async () => {
device = makeNewDevice()
// gpnStatus = 'unknown', no parameters (only properties from Inform) → GPN test will run
expect(device.gpnStatus).toBe('unknown')
expect(device.getParameters().length).toBe(0) // all stored as properties initially
const taskQueue = new CWMPTaskQueue()
const sm = new MainStateMachine(device, 'session-1', taskQueue)
console.log('\n=== SESSION 1: New device (first contact) ===')
// Step 1: start → IdleState extracts Inform paths → sends GPN
const step1 = await sm.start()
console.log(`1. [${sm.getCurrentStateName()}] → GPN sent`)
expect(step1).toContain('GetParameterNames')
expect(sm.getCurrentStateName()).toBe('discovering-parameters')
// Step 2: First GPN response (DeviceInfo.) → still discovering (ManagementServer. pending)
await sm.handleResponse(true, {
ParameterList: {
ParameterInfoStruct: [
{ Name: 'InternetGatewayDevice.DeviceInfo.HardwareVersion', Writable: '0' },
{ Name: 'InternetGatewayDevice.DeviceInfo.SoftwareVersion', Writable: '0' },
],
},
})
console.log(`2. [${sm.getCurrentStateName()}] → First GPN response (DeviceInfo.), next GPN pending`)
expect(sm.getCurrentStateName()).toBe('discovering-parameters')
// Step 3: Second GPN response (ManagementServer.) → classify → transition to critical-reads → GPV
const step3 = await sm.handleResponse(true, {
ParameterList: {
ParameterInfoStruct: [
{ Name: 'InternetGatewayDevice.ManagementServer.ConnectionRequestURL', Writable: '0' },
{ Name: 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', Writable: '1' },
],
},
})
console.log(`3. [${sm.getCurrentStateName()}] → Second GPN response (ManagementServer.), GPV (critical reads) sent`)
expect(device.gpnStatus).toBe('supported')
expect(step3).toContain('GetParameterValues')
expect(sm.getCurrentStateName()).toBe('critical-reads')
// Step 4: Critical reads GPV response → update device → transition to config-eval
const step4 = await sm.handleResponse(true, {
ParameterList: {
ParameterValueStruct: [
{ Name: 'InternetGatewayDevice.DeviceInfo.HardwareVersion', Value: { _: '40501' } },
{ Name: 'InternetGatewayDevice.DeviceInfo.SoftwareVersion', Value: { _: 'V100R001' } },
{ Name: 'InternetGatewayDevice.DeviceInfo.ModelName', Value: { _: 'BM632w' } },
{ Name: 'InternetGatewayDevice.ManagementServer.ConnectionRequestURL', Value: { _: 'http://172.20.0.5:37547/' } },
],
},
})
console.log(`4. [${sm.getCurrentStateName()}] → Critical reads processed, ConfigEval started`)
expect(sm.getCurrentStateName()).toBe('configuration-evaluation')
// Step 5: ConfigEval sends inventory GPN
expect(step4).toContain('GetParameterNames')
console.log(`5. [${sm.getCurrentStateName()}] → Inventory GPN sent`)
// Step 6: Inventory GPN response → discover leaves → sends GPV
const step6 = await sm.handleResponse(true, {
ParameterList: {
ParameterInfoStruct: [
{ Name: 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', Writable: '1' },
{ Name: 'InternetGatewayDevice.ManagementServer.ConnectionRequestURL', Writable: '0' },
],
},
})
console.log(`6. [${sm.getCurrentStateName()}] → Inventory GPN response → sends GPV for leaves`)
expect(step6).toContain('GetParameterValues')
// Step 7: Inventory GPV response → update values → sends SPV for diff
const step7 = await sm.handleResponse(true, {
ParameterList: {
ParameterValueStruct: [
{ Name: 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', Value: { _: '60' } },
{ Name: 'InternetGatewayDevice.ManagementServer.ConnectionRequestURL', Value: { _: 'http://172.20.0.5:37547/' } },
],
},
})
console.log(`7. [${sm.getCurrentStateName()}] → GPV values saved → SPV sent (PeriodicInformInterval 60→300)`)
expect(step7).toContain('SetParameterValues')
expect(step7).toContain('300')
// Step 8: SPV response → transition to compliance-update → sends ProvisioningCode
const step8 = await sm.handleResponse(true, { Status: '0' })
console.log(`8. [${sm.getCurrentStateName()}] → SPV success → ProvisioningCode sent`)
expect(step8).toContain('SetParameterValues')
expect(step8).toContain('ProvisioningCode')
// Step 9: ProvCode response → executing-commands → operational → done
const step9 = await sm.handleResponse(true, { Status: '0' })
console.log(`9. [${sm.getCurrentStateName()}] → ProvCode done → session complete`)
expect(sm.isCompleted()).toBe(true)
expect(sm.getCurrentStateName()).toBe('operational')
console.log('\n✅ Session 1 complete: device classified + configured')
console.log(` gpnStatus: ${device.gpnStatus}`)
console.log(` parameters: ${device.getParameters().length}`)
console.log(` properties: ${device.getProperties().length}`)
})
it('SESSION 2: known device (already classified + compliant) — minimal session', async () => {
// Simulate a device loaded from DB after session 1
device = makeNewDevice()
device.setGpnStatus('supported')
// Simulate that organizeParameters was called in session 1: writable params are parameters
device.setParameter(new DeviceParameter(
'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', '300', ParameterType.UNSIGNED_INT
))
// ProvisioningCode already set from session 1
const config = { identifier: 'BM632w-standard', version: '1.0.1', desiredParameters: [{ name: 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', value: '300', type: 'unsignedInt' }], firmwareTarget: null }
const provCode = buildProvisioningCode(config as any)
device.setProperty(new DeviceProperty('InternetGatewayDevice.DeviceInfo.ProvisioningCode', provCode, ParameterType.STRING))
expect(device.gpnStatus).toBe('supported')
expect(device.getParameters().length).toBeGreaterThan(0)
const taskQueue = new CWMPTaskQueue()
const sm = new MainStateMachine(device, 'session-2', taskQueue)
console.log('\n=== SESSION 2: Known device (already classified + compliant) ===')
// Step 1: start → IdleState sees gpnStatus=supported + has parameters → SKIP GPN → critical reads GPV
const step1 = await sm.start()
console.log(`1. [${sm.getCurrentStateName()}] → GPN SKIPPED (already classified) → GPV (critical reads) sent`)
expect(step1).toContain('GetParameterValues')
expect(step1).not.toContain('GetParameterNames')
expect(sm.getCurrentStateName()).toBe('critical-reads')
// Step 2: Critical reads GPV response → config eval
const step2 = await sm.handleResponse(true, {
ParameterList: {
ParameterValueStruct: [
{ Name: 'InternetGatewayDevice.DeviceInfo.HardwareVersion', Value: { _: '40501' } },
{ Name: 'InternetGatewayDevice.DeviceInfo.SoftwareVersion', Value: { _: 'V100R001' } },
{ Name: 'InternetGatewayDevice.DeviceInfo.ModelName', Value: { _: 'BM632w' } },
{ Name: 'InternetGatewayDevice.ManagementServer.ConnectionRequestURL', Value: { _: 'http://172.20.0.5:37547/' } },
],
},
})
console.log(`2. [${sm.getCurrentStateName()}] → Critical reads done → ConfigEval: already compliant, no SPV needed`)
// ConfigEval should find isAlreadyCompliant=true (value matches, provCode matches)
// → skips inventory, skips SPV, skips provCode → goes to operational
expect(sm.isCompleted()).toBe(true)
expect(sm.getCurrentStateName()).toBe('operational')
console.log(`3. [${sm.getCurrentStateName()}] → Session complete (no GPN, no SPV, no ProvCode)`)
// Verify reclassifyDeviceFromCatalog was NOT called (device already classified)
expect(vi.mocked(reclassifyDeviceFromCatalog)).not.toHaveBeenCalled()
console.log('\n✅ Session 2 complete: minimal session (GPV only)')
console.log(` Total RPCs: 1 (GetParameterValues for critical reads)`)
console.log(` Skipped: GPN test, inventory, SPV, ProvisioningCode`)
})
})
import DeviceModel from '../../../../app/models/device.js'
describe('GPN unsupported → catalog classification + persistence', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('GPN fault triggers catalog reclassification and persists to DB', async () => {
// Simulate a Livebox 4 (tr-181) device with Inform params
const device = new Device(new DeviceId('Sagemcom', 'F8B7FF', 'Livebox 4', 'LB4-TEST-001'))
device.setDataModel('tr-181')
const informParams = [
{ Name: 'Device.DeviceInfo.HardwareVersion', Value: { _: 'v3.0' } },
{ Name: 'Device.DeviceInfo.SoftwareVersion', Value: { _: 'SR50-5.0.0' } },
{ Name: 'Device.ManagementServer.ConnectionRequestURL', Value: { _: 'http://cpe:7547/' } },
{ Name: 'Device.ManagementServer.PeriodicInformInterval', Value: { _: '60' } },
]
device.updateFromValues(informParams)
device.setTmpParamValues(informParams)
// gpnStatus = 'unknown' → will attempt GPN
const taskQueue = new CWMPTaskQueue()
const sm = new MainStateMachine(device, 'session-gpn-fail', taskQueue)
// Step 1: start → sends GPN for Device.DeviceInfo. (first unclassified path)
const step1 = await sm.start()
expect(step1).toContain('GetParameterNames')
expect(sm.getCurrentStateName()).toBe('discovering-parameters')
// Step 2: GPN Fault → mark unsupported + catalog classify + persist
const step2 = await sm.handleResponse(false, {
FaultCode: 9003,
FaultString: 'Method not supported',
})
// Verify: reclassifyDeviceFromCatalog was called
expect(vi.mocked(reclassifyDeviceFromCatalog)).toHaveBeenCalledWith(device)
// Verify: DeviceModel.createOrUpdate was called to persist the classification
expect(vi.mocked(DeviceModel.createOrUpdate)).toHaveBeenCalledWith(
expect.objectContaining({
gpnStatus: 'unsupported',
gpnCircuitBreaker: expect.objectContaining({ failureCount: 1 }),
})
)
// Verify: device is marked as unsupported
expect(device.gpnStatus).toBe('unsupported')
expect(device.gpnCircuitBreaker.failureCount).toBe(1)
// Verify: transitioned to critical-reads (sends GPV)
expect(step2).toContain('GetParameterValues')
expect(sm.getCurrentStateName()).toBe('critical-reads')
})
it('subsequent session with same params skips GPN (all classified)', async () => {
// Simulate device loaded from DB after first session (already classified)
const device = new Device(new DeviceId('Sagemcom', 'F8B7FF', 'Livebox 4', 'LB4-TEST-001'))
device.setDataModel('tr-181')
device.setGpnStatus('unsupported')
// After catalog classification, PeriodicInformInterval is a parameter (writable)
device.setParameter(new DeviceParameter(
'Device.ManagementServer.PeriodicInformInterval', '300', ParameterType.UNSIGNED_INT
))
// Read-only params are properties
device.setProperty(new DeviceProperty('Device.DeviceInfo.HardwareVersion', 'v3.0', ParameterType.STRING))
device.setProperty(new DeviceProperty('Device.DeviceInfo.SoftwareVersion', 'SR50-5.0.0', ParameterType.STRING))
device.setProperty(new DeviceProperty('Device.ManagementServer.ConnectionRequestURL', 'http://cpe:7547/', ParameterType.STRING))
// Inform arrives with same params (all already classified)
const informParams = [
{ Name: 'Device.DeviceInfo.HardwareVersion', Value: { _: 'v3.0' } },
{ Name: 'Device.DeviceInfo.SoftwareVersion', Value: { _: 'SR50-5.0.0' } },
{ Name: 'Device.ManagementServer.ConnectionRequestURL', Value: { _: 'http://cpe:7547/' } },
{ Name: 'Device.ManagementServer.PeriodicInformInterval', Value: { _: '300' } },
]
device.updateFromValues(informParams)
device.setTmpParamValues(informParams)
const taskQueue = new CWMPTaskQueue()
const sm = new MainStateMachine(device, 'session-2', taskQueue)
// Start → all params classified → skip GPN → directly to critical-reads (GPV)
const step1 = await sm.start()
expect(step1).toContain('GetParameterValues')
expect(step1).not.toContain('GetParameterNames')
expect(sm.getCurrentStateName()).toBe('critical-reads')
// reclassifyDeviceFromCatalog should NOT have been called
expect(vi.mocked(reclassifyDeviceFromCatalog)).not.toHaveBeenCalled()
})
it('session with NEW param in Inform (not yet in properties) triggers catalog classification', async () => {
// Device already classified from previous session
const device = new Device(new DeviceId('Sagemcom', 'F8B7FF', 'Livebox 4', 'LB4-TEST-001'))
device.setDataModel('tr-181')
device.setGpnStatus('unsupported')
device.setGpnCircuitBreaker({ failureCount: 1, lastFailureDate: new Date(), state: 'closed', cooldownUntil: null })
// Existing classified params
device.setParameter(new DeviceParameter('Device.ManagementServer.PeriodicInformInterval', '300', ParameterType.UNSIGNED_INT))
device.setProperty(new DeviceProperty('Device.DeviceInfo.HardwareVersion', 'v3.0', ParameterType.STRING))
device.setProperty(new DeviceProperty('Device.ManagementServer.ConnectionRequestURL', 'http://cpe:7547/', ParameterType.STRING))
// New Inform arrives with a NEW param that is NOT in _parameters/_properties
// (simulating a param that wasn't in the previous Inform and wasn't stored by updateFromValues)
const informParams = [
{ Name: 'Device.DeviceInfo.HardwareVersion', Value: { _: 'v3.0' } },
{ Name: 'Device.ManagementServer.ConnectionRequestURL', Value: { _: 'http://cpe:7547/' } },
{ Name: 'Device.ManagementServer.PeriodicInformInterval', Value: { _: '300' } },
{ Name: 'Device.WANDevice.1.ExternalIPAddress', Value: { _: '192.168.1.1' } }, // NEW - not in device yet
]
// Only set tmpParamValues WITHOUT updateFromValues for the new param
device.setTmpParamValues(informParams)
const taskQueue = new CWMPTaskQueue()
const sm = new MainStateMachine(device, 'session-new-param', taskQueue)
// Start → detects 1 unclassified param (WANDevice not in _parameters/_properties)
// GPN unsupported → catalog classify
const step1 = await sm.start()
// Should have called catalog to classify the new param
expect(vi.mocked(reclassifyDeviceFromCatalog)).toHaveBeenCalledWith(device)
// Should go to critical-reads (GPV)
expect(step1).toContain('GetParameterValues')
expect(sm.getCurrentStateName()).toBe('critical-reads')
})
})