diff --git a/tests/unit/services/cwmp/two_session_scenario.spec.ts b/tests/unit/services/cwmp/two_session_scenario.spec.ts index 37dcf66..555463f 100644 --- a/tests/unit/services/cwmp/two_session_scenario.spec.ts +++ b/tests/unit/services/cwmp/two_session_scenario.spec.ts @@ -265,3 +265,136 @@ describe('Two-session scenario: same device, first contact then reconnect', () = 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') + }) +}) +