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') }) })