Add two-session scenario test demonstrating the full flow. Fix OperationalState to also skip reclassify when already classified. Session 1: GPN test → classify → critical reads → inventory GPN→GPV→SPV → ProvCode. Session 2: skip GPN + skip all (1 GPV only). All 811 tests pass. Build status: Success
This commit is contained in:
1 parent
ffbed94dcc
commit
25d69bd03f
2 files changed
+269
-2
No files matched your search
@@ -457,8 +457,10 @@ export class OperationalState extends DeviceState {
|
||||
async handle(context: DeviceContext): Promise<DeviceStateResult> {
|
||||
const { device } = context
|
||||
|
||||
// Consolidated reclassification (Phase 11)
|
||||
reclassifyDeviceFromCatalog(device)
|
||||
// Consolidated reclassification — only if not already classified
|
||||
if (device.gpnStatus !== 'supported' && device.getParameters().length === 0) {
|
||||
reclassifyDeviceFromCatalog(device)
|
||||
}
|
||||
|
||||
// Persist device with operational status
|
||||
device.operationalStatus = 'operational'
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
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)
|
||||
d.updateFromValues([
|
||||
{ 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' } },
|
||||
])
|
||||
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`)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user