From c8c7cb32b7a161e0a76e6fdd82c9cf760b31c1c0 Mon Sep 17 00:00:00 2001 From: ATX Bot Date: Mon, 8 Jun 2026 15:51:00 +0200 Subject: [PATCH] Increase device_action_executor.ts coverage to 94% stmts, 93% functions. Added 16 tests: getParameterAttributes callback, persistDevice error path, GPV error path, SPV/SPA/ScheduleInform/Reboot/Upload/insertProvisioningCode callbacks, nested fault code path, legacy fallback paths. All 782 tests pass. Build status: Success --- .../device_action_executor_coverage.spec.ts | 275 ++++++++++++++++++ 1 file changed, 275 insertions(+) diff --git a/tests/unit/services/cwmp/device_action_executor_coverage.spec.ts b/tests/unit/services/cwmp/device_action_executor_coverage.spec.ts index f83c460..9076272 100644 --- a/tests/unit/services/cwmp/device_action_executor_coverage.spec.ts +++ b/tests/unit/services/cwmp/device_action_executor_coverage.spec.ts @@ -254,3 +254,278 @@ describe('DeviceActionExecutor — coverage', () => { expect(xml).toContain('FactoryReset') }) }) + +describe('DeviceActionExecutor — callback execution for remaining uncovered lines', () => { + let taskQueue: CWMPTaskQueue + let executor: DeviceActionExecutor + + beforeEach(() => { + vi.clearAllMocks() + taskQueue = new CWMPTaskQueue() + executor = new DeviceActionExecutor(taskQueue) + }) + + // --- getParameterAttributes callback (lines 118-145) --- + + it('getParameterAttributes wrappedCallback processes attributes on success', async () => { + const device = makeDevice() + device.setParameter(new DeviceParameter('Device.X.Param', 'val', ParameterType.STRING)) + + await executor.getParameterAttributes(device, ['Device.X.Param'], async () => '') + + const taskCallbacks = (taskQueue as any).taskCallbacks as Map + const taskId = Array.from(taskCallbacks.keys())[0] + + await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, { + ParameterList: { + ParameterAttributeStruct: [ + { Name: 'Device.X.Param', Notification: 2, AccessList: { string: ['Subscriber'] } }, + ], + }, + }) + + // Should have updated the parameter's attributes + const param = device.getParameter('Device.X.Param') + expect(param).not.toBeNull() + }) + + it('getParameterAttributes wrappedCallback handles property attributes', async () => { + const device = makeDevice() + device.setProperty(new DeviceProperty('Device.Y.Prop', 'pval', ParameterType.STRING)) + + await executor.getParameterAttributes(device, ['Device.Y.Prop'], async () => '') + + const taskCallbacks = (taskQueue as any).taskCallbacks as Map + const taskId = Array.from(taskCallbacks.keys())[0] + + await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, { + ParameterList: { + ParameterAttributeStruct: [ + { Name: 'Device.Y.Prop', Notification: 1 }, + ], + }, + }) + + const prop = device.getProperty('Device.Y.Prop') + expect(prop).not.toBeNull() + }) + + it('getParameterAttributes callback skips entries without Name', async () => { + const device = makeDevice() + await executor.getParameterAttributes(device, ['Device.X'], async () => '') + + const taskCallbacks = (taskQueue as any).taskCallbacks as Map + const taskId = Array.from(taskCallbacks.keys())[0] + + // Entry without Name should be skipped without error + await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, { + ParameterList: { ParameterAttributeStruct: [{ Notification: 2 }] }, + }) + }) + + // --- persistDevice error path (line 81) --- + + it('persistDevice logs error when createOrUpdate fails', async () => { + vi.mocked(DeviceModel.createOrUpdate).mockRejectedValueOnce(new Error('DB down')) + const device = makeDevice() + + await executor.getParameterValues(device, ['Device.X'], async () => '') + const taskCallbacks = (taskQueue as any).taskCallbacks as Map + const taskId = Array.from(taskCallbacks.keys())[0] + + await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, { + ParameterList: { ParameterValueStruct: [{ Name: 'Device.X', Value: { _: 'v' } }] }, + }) + + expect(vi.mocked(logger.error)).toHaveBeenCalled() + }) + + // --- getParameterValues wrappedCallback error path (line 103) --- + + it('getParameterValues wrappedCallback catches errors in updateFromValues', async () => { + const device = makeDevice() + // Force updateFromValues to throw + vi.spyOn(device, 'updateFromValues').mockImplementationOnce(() => { throw new Error('parse error') }) + + await executor.getParameterValues(device, ['Device.X'], async () => '') + const taskCallbacks = (taskQueue as any).taskCallbacks as Map + const taskId = Array.from(taskCallbacks.keys())[0] + + await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, { + ParameterList: { ParameterValueStruct: [{ Name: 'Device.X', Value: { _: 'v' } }] }, + }) + + expect(vi.mocked(logger.error)).toHaveBeenCalled() + }) + + // --- applySetParameterValues callback (line 247) --- + + it('applySetParameterValues callback updates device on success', async () => { + const device = makeDevice() + const callbackResult = vi.fn().mockResolvedValue('') + + await executor.applySetParameterValues(device, [{ parameterName: 'Device.X', value: '42', type: 'int' }], callbackResult) + + const taskCallbacks = (taskQueue as any).taskCallbacks as Map + const taskId = Array.from(taskCallbacks.keys())[0] + await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, {}) + + expect(callbackResult).toHaveBeenCalledWith(true) + }) + + it('applySetParameterValues callback returns false on failure', async () => { + const device = makeDevice() + const callbackResult = vi.fn().mockResolvedValue('') + + await executor.applySetParameterValues(device, [{ parameterName: 'Device.X', value: '42' }], callbackResult) + + const taskCallbacks = (taskQueue as any).taskCallbacks as Map + const taskId = Array.from(taskCallbacks.keys())[0] + await taskQueue.executeTaskCallback(taskId!, ResultStatus.Error, { FaultCode: 9001 }) + + expect(callbackResult).toHaveBeenCalledWith(false) + }) + + // --- applySetParameterAttributes callback (lines 270-273) --- + + it('applySetParameterAttributes callback invoked on success', async () => { + const device = makeDevice() + const cb = vi.fn().mockResolvedValue('') + + await executor.applySetParameterAttributes(device, [ + { name: 'Device.X', notificationChange: true, notification: 2, accessListChange: false, accessList: [] }, + ], cb) + + const taskCallbacks = (taskQueue as any).taskCallbacks as Map + const taskId = Array.from(taskCallbacks.keys())[0] + await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, {}) + + expect(cb).toHaveBeenCalledWith(true) + }) + + // --- applyScheduleInform callback (line 289) --- + + it('applyScheduleInform callback invoked', async () => { + const device = makeDevice() + const cb = vi.fn().mockResolvedValue('') + + await executor.applyScheduleInform(device, { delayInSeconds: 600 }, cb) + + const taskCallbacks = (taskQueue as any).taskCallbacks as Map + const taskId = Array.from(taskCallbacks.keys())[0] + await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, {}) + + expect(cb).toHaveBeenCalledWith(true) + }) + + // --- applyReboot callback (line 319) --- + + it('applyReboot callback invoked on success', async () => { + const device = makeDevice() + const cb = vi.fn().mockResolvedValue('') + + await executor.applyReboot(device, { commandKey: 'r1' }, cb) + + const taskCallbacks = (taskQueue as any).taskCallbacks as Map + const taskId = Array.from(taskCallbacks.keys())[0] + await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, {}) + + expect(cb).toHaveBeenCalledWith(true) + }) + + // --- applyUpload callback (line 367) --- + + it('applyUpload returns Upload envelope and callback invoked', async () => { + const device = makeDevice() + const cb = vi.fn().mockResolvedValue('') + + const xml = await executor.applyUpload(device, { + commandKey: 'upload-1', fileType: '2 Web Content', url: 'http://upload.example.com/cfg', + }, cb) + + expect(xml).toContain('Upload') + expect(xml).toContain('http://upload.example.com/cfg') + + const taskCallbacks = (taskQueue as any).taskCallbacks as Map + const taskId = Array.from(taskCallbacks.keys())[0] + await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, {}) + + expect(cb).toHaveBeenCalledWith(true) + }) + + // --- insertProvisioningCode success callback (line 409) --- + + it('insertProvisioningCode callback clears tmpPC on success', async () => { + const device = makeDevice() + device.tmpPC = 'my-prov-code' + const cb = vi.fn().mockResolvedValue('') + + await executor.insertProvisioningCode(device, cb) + + const taskCallbacks = (taskQueue as any).taskCallbacks as Map + const taskId = Array.from(taskCallbacks.keys())[0] + await taskQueue.executeTaskCallback(taskId!, ResultStatus.Success, {}) + + expect(cb).toHaveBeenCalledWith(true) + expect(device.tmpPC).toBe('') + }) + + it('insertProvisioningCode callback returns false on failure', async () => { + const device = makeDevice() + device.tmpPC = 'prov-code' + const cb = vi.fn().mockResolvedValue('') + + await executor.insertProvisioningCode(device, cb) + + const taskCallbacks = (taskQueue as any).taskCallbacks as Map + const taskId = Array.from(taskCallbacks.keys())[0] + await taskQueue.executeTaskCallback(taskId!, ResultStatus.Error, {}) + + expect(cb).toHaveBeenCalledWith(false) + // tmpPC should NOT be cleared on failure + expect(device.tmpPC).toBe('prov-code') + }) + + // --- GPN with nested fault code path (lines 204-205) --- + + it('GPN fault with nested Detail.Fault.FaultCode marks as unsupported', async () => { + const device = makeDevice() + device.setGpnStatus('unknown') + let capturedCallback: any + vi.spyOn(taskQueue, 'addTask').mockImplementation((deviceId, type, props, cb) => { + capturedCallback = cb + return { xml: '', taskId: 'tid-nested' } + }) + + await executor.getParameterNames(device, 'Device.', async () => '') + await capturedCallback({ + status: ResultStatus.Error, + data: { Detail: { Fault: { FaultCode: '9003' } } }, + }) + + expect(device.gpnStatus).toBe('unsupported') + }) + + // --- insertProvisioningCode legacy fallback path --- + + it('insertProvisioningCode uses legacy fallback when dataModel is unknown', async () => { + const device = makeDevice('unknown') + // No Device. prefixed params, so should use InternetGatewayDevice path + device.tmpPC = 'test-pc' + + const xml = await executor.insertProvisioningCode(device, async () => '') + expect(xml).toContain('InternetGatewayDevice.DeviceInfo.ProvisioningCode') + }) + + it('insertProvisioningCode uses Device path when device has Device. params', async () => { + const device = makeDevice('unknown') + device.setProperty(new DeviceProperty('Device.DeviceInfo.X', 'val', ParameterType.STRING)) + device.tmpPC = 'test-pc' + + const xml = await executor.insertProvisioningCode(device, async () => '') + expect(xml).toContain('Device.DeviceInfo.ProvisioningCode') + }) +}) + +import logger from '@adonisjs/core/services/logger' +