402 lines
19 KiB
TypeScript
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')
|
|
})
|
|
})
|
|
|
|
|