220 lines
8.1 KiB
TypeScript
220 lines
8.1 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest'
|
|
|
|
// Mock logger + DeviceModel
|
|
vi.mock('@adonisjs/core/services/logger', () => ({
|
|
default: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
}))
|
|
|
|
vi.mock('#models/device', () => ({
|
|
default: {
|
|
updateOne: vi.fn().mockResolvedValue({ modifiedCount: 1 }),
|
|
createOrUpdate: vi.fn().mockResolvedValue({}),
|
|
},
|
|
}))
|
|
|
|
// Mock ConfigurationEngine
|
|
vi.mock('../../../../app/services/configuration_engine.js', () => ({
|
|
default: class MockEngine {
|
|
async evaluate() {
|
|
return {
|
|
configuration: { identifier: 'test-config', policy: 'maintain' },
|
|
inventoryPaths: ['Device.DeviceInfo.', 'Device.ManagementServer.'],
|
|
parameterDiffs: [{ name: 'Device.X', targetValue: '42', type: 'xsd:string' }],
|
|
firmwareMigrationPath: [],
|
|
skipFirmware: false,
|
|
isAlreadyCompliant: false,
|
|
}
|
|
}
|
|
},
|
|
}))
|
|
|
|
// Mock provisioning code
|
|
vi.mock('../../../../app/utils/provisioning_code.js', () => ({
|
|
buildProvisioningCode: () => 'test-prov-code-123',
|
|
}))
|
|
|
|
import {
|
|
ConfigurationEvaluationState,
|
|
ComplianceUpdateState,
|
|
} from '../../../../app/services/cwmp/configuration_evaluation_state.js'
|
|
import { Device } from '../../../../app/domain/device/device.js'
|
|
import { DeviceId } from '../../../../app/domain/device/device_id.js'
|
|
import { CWMPTaskQueue } from '../../../../app/services/cwmp/task_queue.js'
|
|
import { DeviceActionExecutor } from '../../../../app/services/cwmp/device_action_executor.js'
|
|
|
|
function makeContext(gpnStatus: 'supported' | 'unsupported' = 'supported') {
|
|
const device = new Device(new DeviceId('Mfr', 'OUI', 'PC', 'SN'))
|
|
device.setGpnStatus(gpnStatus)
|
|
const taskQueue = new CWMPTaskQueue()
|
|
return {
|
|
device,
|
|
session: 'sess-1',
|
|
message: undefined as { success: boolean; data: any } | undefined,
|
|
taskQueue,
|
|
services: { deviceAction: new DeviceActionExecutor(taskQueue) },
|
|
}
|
|
}
|
|
|
|
describe('ConfigurationEvaluationState', () => {
|
|
it('has name "configuration-evaluation"', () => {
|
|
const state = new ConfigurationEvaluationState()
|
|
expect(state.name).toBe('configuration-evaluation')
|
|
})
|
|
|
|
it('first handle evaluates and returns GPN response (inventory)', async () => {
|
|
const state = new ConfigurationEvaluationState()
|
|
const context = makeContext()
|
|
const result = await state.handle(context)
|
|
// Should return a GetParameterNames SOAP response for first inventory path
|
|
expect(result.response).toContain('GetParameterNames')
|
|
expect(result.response).toContain('Device.DeviceInfo.')
|
|
})
|
|
|
|
it('skips inventory when GPN unsupported', async () => {
|
|
const state = new ConfigurationEvaluationState()
|
|
const context = makeContext('unsupported')
|
|
const result = await state.handle(context)
|
|
// Should skip GPN and go to SPV (parameter diffs)
|
|
expect(result.response).toContain('SetParameterValues')
|
|
expect(result.response).toContain('Device.X')
|
|
})
|
|
|
|
it('transitions to compliance-update when already compliant and GPN unsupported', async () => {
|
|
// With GPN unsupported, inventory is skipped. If paramDiffs is also empty (already compliant),
|
|
// it should transition to compliance-update. But our mock always returns paramDiffs.
|
|
// So this test just verifies the second call returns SPV for the diffs.
|
|
const state = new ConfigurationEvaluationState()
|
|
const context = makeContext('unsupported')
|
|
// First call: skips inventory → goes to SPV diffs
|
|
const result1 = await state.handle(context)
|
|
expect(result1.response).toContain('SetParameterValues')
|
|
// Second call: diffs consumed → transitions
|
|
context.message = { success: true, data: { Status: 0 } }
|
|
const result2 = await state.handle(context)
|
|
expect(result2.nextState).toBeDefined()
|
|
expect(result2.nextState!.name).toBe('compliance-update')
|
|
})
|
|
})
|
|
|
|
describe('ConfigurationEvaluationState - full happy path', () => {
|
|
it('inventory GPN → GPV → SPV diffs → compliance-update', async () => {
|
|
const state = new ConfigurationEvaluationState()
|
|
const context = makeContext('supported')
|
|
|
|
// Step 1: First call → GPN for inventory path (Device.DeviceInfo.)
|
|
const r1 = await state.handle(context)
|
|
expect(r1.response).toContain('GetParameterNames')
|
|
expect(r1.response).toContain('Device.DeviceInfo.')
|
|
|
|
// Step 2: GPN response for DeviceInfo → sends next GPN for ManagementServer.
|
|
context.message = {
|
|
success: true,
|
|
data: {
|
|
ParameterList: {
|
|
ParameterInfoStruct: [{ Name: 'Device.DeviceInfo.Manufacturer', Writable: '0' }],
|
|
},
|
|
},
|
|
}
|
|
const r2 = await state.handle(context)
|
|
expect(r2.response).toContain('GetParameterNames')
|
|
expect(r2.response).toContain('Device.ManagementServer.')
|
|
|
|
// Step 3: GPN response for ManagementServer → extract leaves → GPV
|
|
context.message = {
|
|
success: true,
|
|
data: {
|
|
ParameterList: {
|
|
ParameterInfoStruct: [{ Name: 'Device.ManagementServer.PeriodicInformInterval', Writable: '1' }],
|
|
},
|
|
},
|
|
}
|
|
const r3 = await state.handle(context)
|
|
expect(r3.response).toContain('GetParameterValues')
|
|
|
|
// Step 4: GPV response → values saved → SPV diffs
|
|
context.message = {
|
|
success: true,
|
|
data: { ParameterList: { ParameterValueStruct: [{ Name: 'Device.ManagementServer.PeriodicInformInterval', Value: { _: '60' } }] } },
|
|
}
|
|
const r4 = await state.handle(context)
|
|
expect(r4.response).toContain('SetParameterValues')
|
|
expect(r4.response).toContain('Device.X')
|
|
|
|
// Step 5: SPV response → no firmware → transitions to compliance-update
|
|
context.message = { success: true, data: { Status: 0 } }
|
|
const r5 = await state.handle(context)
|
|
expect(r5.nextState).toBeDefined()
|
|
expect(r5.nextState!.name).toBe('compliance-update')
|
|
})
|
|
})
|
|
|
|
describe('ComplianceUpdateState', () => {
|
|
it('has name "compliance-update"', () => {
|
|
const engineResult = {
|
|
configuration: { identifier: 'cfg-1', policy: 'maintain' },
|
|
inventoryPaths: [],
|
|
parameterDiffs: [],
|
|
firmwareMigrationPath: [],
|
|
skipFirmware: false,
|
|
isAlreadyCompliant: true,
|
|
}
|
|
const state = new ComplianceUpdateState(engineResult as any)
|
|
expect(state.name).toBe('compliance-update')
|
|
})
|
|
|
|
it('first call writes provisioning code and returns SPV', async () => {
|
|
const engineResult = {
|
|
configuration: { identifier: 'cfg-1', policy: 'maintain' },
|
|
inventoryPaths: [],
|
|
parameterDiffs: [],
|
|
firmwareMigrationPath: [],
|
|
skipFirmware: false,
|
|
isAlreadyCompliant: true,
|
|
}
|
|
const state = new ComplianceUpdateState(engineResult as any)
|
|
const context = makeContext('supported')
|
|
const result = await state.handle(context)
|
|
// Should write provisioning code via SPV
|
|
expect(result.response).toContain('SetParameterValues')
|
|
expect(result.response).toContain('ProvisioningCode')
|
|
})
|
|
|
|
it('second call transitions to executing-commands', async () => {
|
|
const engineResult = {
|
|
configuration: { identifier: 'cfg-1', policy: 'maintain' },
|
|
inventoryPaths: [],
|
|
parameterDiffs: [],
|
|
firmwareMigrationPath: [],
|
|
skipFirmware: false,
|
|
isAlreadyCompliant: true,
|
|
}
|
|
const state = new ComplianceUpdateState(engineResult as any)
|
|
const context = makeContext('supported')
|
|
|
|
// First call: writes provisioning code
|
|
await state.handle(context)
|
|
|
|
// Second call: transitions to executing-commands
|
|
context.message = { success: true, data: { Status: 0 } }
|
|
const result2 = await state.handle(context)
|
|
expect(result2.nextState).toBeDefined()
|
|
expect(result2.nextState!.name).toBe('executing-commands')
|
|
})
|
|
|
|
it('without configuration transitions directly to executing-commands', async () => {
|
|
const engineResult = {
|
|
configuration: null,
|
|
inventoryPaths: [],
|
|
parameterDiffs: [],
|
|
firmwareMigrationPath: [],
|
|
skipFirmware: false,
|
|
isAlreadyCompliant: true,
|
|
}
|
|
const state = new ComplianceUpdateState(engineResult as any)
|
|
const context = makeContext('supported')
|
|
const result = await state.handle(context)
|
|
expect(result.nextState).toBeDefined()
|
|
expect(result.nextState!.name).toBe('executing-commands')
|
|
})
|
|
})
|