fix: state machine timing + reclassification tests
- Fix state machine start timing: don't call start() in startStateMachine, let the CPE's first empty body trigger the state machine via continue(). This ensures GPN is sent at the correct point in the TR-069 session. - Add unit tests for reclassifyDeviceFromCatalog (10 tests) - Add unit tests for MainStateMachine transitions and GPN callbacks (11 tests) - Test both scenarios: Huawei (GPN supported) and Livebox (GPN unsupported + fallback) - Fix tr069_trace_service test path assertion (config/ → data/) - Update steering doc with Livebox bootstrap command
This commit is contained in:
1 parent
a5b3135396
commit
bc9ac4f125
5 files changed
+532
-10
No files matched your search
@@ -78,7 +78,18 @@ curl -s -X PUT "http://localhost:9898/api/v1/device-simulator/create-devices?noO
|
||||
}'
|
||||
```
|
||||
|
||||
### 6. Attendre l'Inform (30s max) puis vérifier
|
||||
### 6. Déclencher le bootstrap Inform
|
||||
|
||||
Le simulateur ne fait PAS d'Inform automatiquement après la création. Il faut le déclencher manuellement :
|
||||
|
||||
```bash
|
||||
sleep 3
|
||||
curl -s -X POST "http://localhost:9898/api/v1/device-simulator/device-request" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"actions":[{"macaddress":"F8:B7:FF:00:11:22","event":{"0":"BOOTSTRAP"}}]}'
|
||||
```
|
||||
|
||||
### 7. Attendre l'Inform (30s max) puis vérifier
|
||||
|
||||
```bash
|
||||
sleep 30
|
||||
@@ -99,7 +110,7 @@ print('Dans parameters (BON):', params)
|
||||
"
|
||||
```
|
||||
|
||||
### 7. Regarder les logs de reclassification
|
||||
### 8. Regarder les logs de reclassification
|
||||
|
||||
```bash
|
||||
docker compose logs app --tail 50 2>&1 | grep -i "reclassify\|Reclassify"
|
||||
@@ -107,10 +118,19 @@ docker compose logs app --tail 50 2>&1 | grep -i "reclassify\|Reclassify"
|
||||
|
||||
## Résultat attendu
|
||||
|
||||
**Sagemcom Livebox 4** (GPN unsupported, reclassification via catalogue) :
|
||||
```
|
||||
gpnStatus: unsupported
|
||||
Dans properties (MAUVAIS): []
|
||||
Dans parameters (BON): ['Device.ManagementServer.PeriodicInformInterval', 'Device.DeviceInfo.ProvisioningCode']
|
||||
properties: 7
|
||||
parameters: 2 (PeriodicInformInterval + ProvisioningCode)
|
||||
```
|
||||
|
||||
**Huawei BM632w** (GPN supported, classification via GPN response) :
|
||||
```
|
||||
gpnStatus: supported
|
||||
properties: 16
|
||||
parameters: 15
|
||||
compliance: compliant
|
||||
```
|
||||
|
||||
## Identifiants du device de test
|
||||
@@ -125,7 +145,47 @@ Dans parameters (BON): ['Device.ManagementServer.PeriodicInformInterval', 'Devic
|
||||
| MAC (simulateur) | F8:B7:FF:00:11:22 |
|
||||
| Data Model | tr-181 |
|
||||
|
||||
## API du simulateur
|
||||
## Device Huawei (genieacs-sim)
|
||||
|
||||
| Champ | Valeur |
|
||||
|-------|--------|
|
||||
| ACS identifier | `f9747826c5545b0e49db7967` |
|
||||
| Manufacturer | Huawei Technologies Co., Ltd. |
|
||||
| OUI | 202BC1 |
|
||||
| Product Class | BM632w |
|
||||
| Serial Number | 000000 |
|
||||
| Data Model | tr-098 (InternetGatewayDevice.*) |
|
||||
| Container | `adam-acs-genieacs-sim-1` |
|
||||
|
||||
### Procédure de test Huawei
|
||||
|
||||
Le Huawei est simulé par le container `genieacs-sim` (défini dans `docker-compose.yml` avec le profile `test`). Il se crée automatiquement au démarrage du container.
|
||||
|
||||
**Supprimer le device :**
|
||||
```bash
|
||||
docker stop adam-acs-genieacs-sim-1 && docker rm adam-acs-genieacs-sim-1
|
||||
curl -s -X DELETE http://localhost:8080/api/v1/devices/f9747826c5545b0e49db7967
|
||||
```
|
||||
|
||||
**Recréer le device (relancer le container) :**
|
||||
```bash
|
||||
docker compose --profile test up -d genieacs-sim
|
||||
```
|
||||
|
||||
Le container fait un Inform automatiquement au démarrage vers `http://app:8080/cwmp10`.
|
||||
|
||||
**Vérifier :**
|
||||
```bash
|
||||
sleep 15
|
||||
curl -s "http://localhost:8080/api/v1/devices/f9747826c5545b0e49db7967" | python3 -c "
|
||||
import sys,json
|
||||
d=json.load(sys.stdin)
|
||||
print('gpnStatus:', d.get('gpnStatus'))
|
||||
print('dataModel:', d.get('dataModel'))
|
||||
print('Properties:', len(d.get('properties',[])))
|
||||
print('Parameters:', len(d.get('parameters',[])))
|
||||
"
|
||||
```
|
||||
|
||||
- Base URL : `http://localhost:9898/api/v1/device-simulator`
|
||||
- Swagger : `http://localhost:9898/swagger/`
|
||||
|
||||
@@ -184,7 +184,6 @@ export class StateMachineManager extends EventEmitter {
|
||||
// Start the machine
|
||||
try {
|
||||
machineInstance.status = MachineStatus.RUNNING
|
||||
await stateMachine.start()
|
||||
machineInstance.currentState = stateMachine.getCurrentStateName()
|
||||
|
||||
if (stateMachine.isCompleted()) {
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
// Mock AdonisJS logger before imports
|
||||
vi.mock('@adonisjs/core/services/logger', () => ({
|
||||
default: {
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock DeviceModel to avoid MongoDB dependency
|
||||
vi.mock('../../../app/models/device.js', () => ({
|
||||
default: {
|
||||
createOrUpdate: vi.fn().mockResolvedValue({}),
|
||||
findByDeviceId: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
}))
|
||||
|
||||
// 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),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
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 { DeviceProperty, DeviceParameter } from '../../../app/domain/device/device_parameter.js'
|
||||
import { CWMPTaskQueue } from '../../../app/services/cwmp/task_queue.js'
|
||||
import { ParameterType } from '../../../app/domain/shared/types.js'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function makeTr181Device(): Device {
|
||||
const device = new Device(new DeviceId('Sagemcom', 'F8B7FF', 'Livebox 4', 'LB4-TEST-001'))
|
||||
device.setDataModel('tr-181')
|
||||
return device
|
||||
}
|
||||
|
||||
function makeTr098Device(): Device {
|
||||
const device = new Device(new DeviceId('Huawei Technologies Co., Ltd.', '202BC1', 'BM632w', '000000'))
|
||||
device.setDataModel('tr-098')
|
||||
return device
|
||||
}
|
||||
|
||||
function makeUnknownDevice(): Device {
|
||||
const device = new Device(new DeviceId('Unknown', '000000', 'Generic', 'SN001'))
|
||||
device.setDataModel('unknown')
|
||||
return device
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MainStateMachine — Initial state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('MainStateMachine — initial state', () => {
|
||||
it('starts in idle state', () => {
|
||||
const device = makeTr181Device()
|
||||
const taskQueue = new CWMPTaskQueue()
|
||||
const sm = new MainStateMachine(device, 'session-1', taskQueue)
|
||||
|
||||
expect(sm.getCurrentStateName()).toBe('idle')
|
||||
expect(sm.isCompleted()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MainStateMachine — TR-181 device flow (GPN on Device.ManagementServer.)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('MainStateMachine — TR-181 device', () => {
|
||||
let device: Device
|
||||
let taskQueue: CWMPTaskQueue
|
||||
let sm: MainStateMachine
|
||||
|
||||
beforeEach(() => {
|
||||
device = makeTr181Device()
|
||||
taskQueue = new CWMPTaskQueue()
|
||||
sm = new MainStateMachine(device, 'session-1', taskQueue)
|
||||
})
|
||||
|
||||
it('transitions to discovering-parameters and sends GPN for Device.ManagementServer.', async () => {
|
||||
const response = await sm.start()
|
||||
|
||||
expect(sm.getCurrentStateName()).toBe('discovering-parameters')
|
||||
// Response should be a SOAP envelope with GetParameterNames
|
||||
expect(response).toContain('GetParameterNames')
|
||||
expect(response).toContain('Device.ManagementServer.')
|
||||
})
|
||||
|
||||
it('advances past discovering-parameters after receiving a response', async () => {
|
||||
await sm.start() // sends GPN, state = discovering-parameters
|
||||
|
||||
// Simulate any response (success or failure) — state machine advances
|
||||
await sm.handleResponse(true, {})
|
||||
|
||||
// Should have moved past discovering-parameters (no more paths in queue)
|
||||
expect(sm.getCurrentStateName()).not.toBe('discovering-parameters')
|
||||
expect(sm.getCurrentStateName()).not.toBe('idle')
|
||||
})
|
||||
|
||||
it('GPN callback sets gpnStatus to unsupported on fault code 9000', async () => {
|
||||
await sm.start()
|
||||
|
||||
// Get the task from the queue and execute its callback manually
|
||||
const tasks = Array.from((taskQueue as any).tasks.values())
|
||||
const gpnTask = tasks.find((t: any) => t.type === 'get_parameter_names')
|
||||
expect(gpnTask).toBeDefined()
|
||||
|
||||
// Execute the callback with error
|
||||
await taskQueue.executeTaskCallback(gpnTask!.id, 'error' as any, {
|
||||
FaultCode: 9000,
|
||||
FaultString: 'Method not supported',
|
||||
})
|
||||
|
||||
expect(device.gpnStatus).toBe('unsupported')
|
||||
})
|
||||
|
||||
it('GPN callback sets gpnStatus to supported on success', async () => {
|
||||
await sm.start()
|
||||
|
||||
const tasks = Array.from((taskQueue as any).tasks.values())
|
||||
const gpnTask = tasks.find((t: any) => t.type === 'get_parameter_names')
|
||||
expect(gpnTask).toBeDefined()
|
||||
|
||||
// Execute the callback with success
|
||||
await taskQueue.executeTaskCallback(gpnTask!.id, 'success' as any, {
|
||||
ParameterList: {
|
||||
ParameterInfoStruct: [
|
||||
{ Name: 'Device.ManagementServer.URL', Writable: true },
|
||||
{ Name: 'Device.ManagementServer.ParameterKey', Writable: false },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(device.gpnStatus).toBe('supported')
|
||||
})
|
||||
|
||||
it('GPN callback organizes parameters correctly on success', async () => {
|
||||
device.setProperty(new DeviceProperty('Device.ManagementServer.URL', 'http://acs', ParameterType.STRING))
|
||||
device.setProperty(new DeviceProperty('Device.ManagementServer.ParameterKey', '', ParameterType.STRING))
|
||||
|
||||
await sm.start()
|
||||
|
||||
const tasks = Array.from((taskQueue as any).tasks.values())
|
||||
const gpnTask = tasks.find((t: any) => t.type === 'get_parameter_names')
|
||||
|
||||
await taskQueue.executeTaskCallback(gpnTask!.id, 'success' as any, {
|
||||
ParameterList: {
|
||||
ParameterInfoStruct: [
|
||||
{ Name: 'Device.ManagementServer.URL', Writable: true },
|
||||
{ Name: 'Device.ManagementServer.ParameterKey', Writable: false },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
// URL was property + writable in GPN → should be parameter now
|
||||
expect(device.getParameter('Device.ManagementServer.URL')).not.toBeNull()
|
||||
// ParameterKey was property + NOT writable → stays property
|
||||
expect(device.getProperty('Device.ManagementServer.ParameterKey')).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MainStateMachine — TR-098 device flow (GPN on InternetGatewayDevice.ManagementServer.)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('MainStateMachine — TR-098 device', () => {
|
||||
let device: Device
|
||||
let taskQueue: CWMPTaskQueue
|
||||
let sm: MainStateMachine
|
||||
|
||||
beforeEach(() => {
|
||||
device = makeTr098Device()
|
||||
taskQueue = new CWMPTaskQueue()
|
||||
sm = new MainStateMachine(device, 'session-1', taskQueue)
|
||||
})
|
||||
|
||||
it('sends GPN for InternetGatewayDevice.ManagementServer.', async () => {
|
||||
const response = await sm.start()
|
||||
|
||||
expect(sm.getCurrentStateName()).toBe('discovering-parameters')
|
||||
expect(response).toContain('GetParameterNames')
|
||||
expect(response).toContain('InternetGatewayDevice.ManagementServer.')
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MainStateMachine — Unknown data model
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('MainStateMachine — unknown data model', () => {
|
||||
it('skips GPN and goes directly to configuration-evaluation', async () => {
|
||||
const device = makeUnknownDevice()
|
||||
const taskQueue = new CWMPTaskQueue()
|
||||
const sm = new MainStateMachine(device, 'session-1', taskQueue)
|
||||
|
||||
await sm.start()
|
||||
|
||||
// Should skip discovering-parameters entirely
|
||||
expect(sm.getCurrentStateName()).not.toBe('discovering-parameters')
|
||||
expect(sm.getCurrentStateName()).not.toBe('idle')
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MainStateMachine — CompletedState reclassification
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('MainStateMachine — CompletedState reclassification', () => {
|
||||
it('reclassifies writable properties on completion when GPN unsupported', async () => {
|
||||
const device = makeTr181Device()
|
||||
device.setGpnStatus('unsupported')
|
||||
// Add writable property that should be reclassified
|
||||
device.setProperty(new DeviceProperty('Device.ManagementServer.PeriodicInformInterval', '60', ParameterType.UNSIGNED_INT))
|
||||
// Add read-only property that should stay
|
||||
device.setProperty(new DeviceProperty('Device.DeviceInfo.Manufacturer', 'Sagemcom', ParameterType.STRING))
|
||||
|
||||
const taskQueue = new CWMPTaskQueue()
|
||||
const sm = new MainStateMachine(device, 'session-1', taskQueue)
|
||||
|
||||
// Start → since dataModel is tr-181, it will try GPN
|
||||
await sm.start()
|
||||
|
||||
// Simulate GPN failure
|
||||
await sm.handleResponse(true, {})
|
||||
|
||||
// The state machine should eventually complete
|
||||
// Let it run through (no config match → completes)
|
||||
let guard = 0
|
||||
while (!sm.isCompleted() && guard++ < 10) {
|
||||
await sm.handleResponse(true, {})
|
||||
}
|
||||
|
||||
// After completion, PeriodicInformInterval should be a parameter
|
||||
expect(device.getParameter('Device.ManagementServer.PeriodicInformInterval')).not.toBeNull()
|
||||
expect(device.getProperty('Device.ManagementServer.PeriodicInformInterval')).toBeNull()
|
||||
// Manufacturer should stay as property
|
||||
expect(device.getProperty('Device.DeviceInfo.Manufacturer')).not.toBeNull()
|
||||
expect(device.getParameter('Device.DeviceInfo.Manufacturer')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DeviceActionExecutor — reclassification after SPV (Livebox fallback scenario)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('DeviceActionExecutor — reclassification after updateFromValues', () => {
|
||||
it('reclassifies properties added via SPV callback when GPN unsupported', async () => {
|
||||
const device = makeTr181Device()
|
||||
device.setGpnStatus('unsupported')
|
||||
// Device starts with only read-only properties (like after first Inform)
|
||||
device.setProperty(new DeviceProperty('Device.DeviceInfo.Manufacturer', 'Sagemcom', ParameterType.STRING))
|
||||
|
||||
const taskQueue = new CWMPTaskQueue()
|
||||
|
||||
// Import DeviceActionExecutor
|
||||
const { DeviceActionExecutor } = await import('../../../app/services/cwmp/device_action_executor.js')
|
||||
const executor = new DeviceActionExecutor(taskQueue)
|
||||
|
||||
// Simulate SPV for ProvisioningCode (like ConfigurationEvaluationState does)
|
||||
const spvXml = await executor.applySetParameterValues(
|
||||
device,
|
||||
[{ parameterName: 'Device.DeviceInfo.ProvisioningCode', value: 'test-code' }],
|
||||
async () => ''
|
||||
)
|
||||
|
||||
expect(spvXml).toContain('SetParameterValues')
|
||||
|
||||
// Get the task and execute callback with success (simulating CPE response)
|
||||
const tasks = Array.from((taskQueue as any).tasks.values())
|
||||
const spvTask = tasks.find((t: any) => t.type === 'set_parameter_values')
|
||||
expect(spvTask).toBeDefined()
|
||||
|
||||
await taskQueue.executeTaskCallback(spvTask!.id, 'success' as any, { Status: 0 })
|
||||
|
||||
// ProvisioningCode should now be a parameter (reclassified after updateFromValues)
|
||||
expect(device.getParameter('Device.DeviceInfo.ProvisioningCode')).not.toBeNull()
|
||||
expect(device.getParameter('Device.DeviceInfo.ProvisioningCode')!.value).toBe('test-code')
|
||||
expect(device.getProperty('Device.DeviceInfo.ProvisioningCode')).toBeNull()
|
||||
})
|
||||
|
||||
it('does NOT reclassify when GPN is supported (live data is authoritative)', async () => {
|
||||
const device = makeTr181Device()
|
||||
device.setGpnStatus('supported')
|
||||
device.setProperty(new DeviceProperty('Device.DeviceInfo.Manufacturer', 'Sagemcom', ParameterType.STRING))
|
||||
|
||||
const taskQueue = new CWMPTaskQueue()
|
||||
const { DeviceActionExecutor } = await import('../../../app/services/cwmp/device_action_executor.js')
|
||||
const executor = new DeviceActionExecutor(taskQueue)
|
||||
|
||||
// SPV for a writable param — but GPN is supported so no reclassification
|
||||
await executor.applySetParameterValues(
|
||||
device,
|
||||
[{ parameterName: 'Device.ManagementServer.PeriodicInformInterval', value: '120' }],
|
||||
async () => ''
|
||||
)
|
||||
|
||||
const tasks = Array.from((taskQueue as any).tasks.values())
|
||||
const spvTask = tasks.find((t: any) => t.type === 'set_parameter_values')
|
||||
await taskQueue.executeTaskCallback(spvTask!.id, 'success' as any, { Status: 0 })
|
||||
|
||||
// PeriodicInformInterval was added via updateFromValues as a property
|
||||
// But since GPN is supported, reclassification is NOT called
|
||||
// It stays as property (the GPN response is the authority)
|
||||
expect(device.getProperty('Device.ManagementServer.PeriodicInformInterval')).not.toBeNull()
|
||||
expect(device.getParameter('Device.ManagementServer.PeriodicInformInterval')).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,146 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { reclassifyDeviceFromCatalog } from '../../../app/services/parameter_writability.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 { ParameterType } from '../../../app/domain/shared/types.js'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function makeDevice(gpnStatus: 'supported' | 'unsupported' | 'unknown' = 'unsupported'): Device {
|
||||
const device = new Device(new DeviceId('Sagemcom', 'F8B7FF', 'Livebox 4', 'LB4-TEST-001'))
|
||||
device.setGpnStatus(gpnStatus)
|
||||
device.setDataModel('tr-181')
|
||||
return device
|
||||
}
|
||||
|
||||
function addProperty(device: Device, name: string, value: any = null): void {
|
||||
device.setProperty(new DeviceProperty(name, value, ParameterType.STRING))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// reclassifyDeviceFromCatalog
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('reclassifyDeviceFromCatalog', () => {
|
||||
it('moves writable properties to parameters (PeriodicInformInterval)', () => {
|
||||
const device = makeDevice()
|
||||
addProperty(device, 'Device.ManagementServer.PeriodicInformInterval', '30')
|
||||
|
||||
const count = reclassifyDeviceFromCatalog(device)
|
||||
|
||||
expect(count).toBe(1)
|
||||
expect(device.getParameter('Device.ManagementServer.PeriodicInformInterval')).not.toBeNull()
|
||||
expect(device.getProperty('Device.ManagementServer.PeriodicInformInterval')).toBeNull()
|
||||
})
|
||||
|
||||
it('moves writable properties to parameters (ProvisioningCode)', () => {
|
||||
const device = makeDevice()
|
||||
addProperty(device, 'Device.DeviceInfo.ProvisioningCode', 'test-code')
|
||||
|
||||
const count = reclassifyDeviceFromCatalog(device)
|
||||
|
||||
expect(count).toBe(1)
|
||||
expect(device.getParameter('Device.DeviceInfo.ProvisioningCode')).not.toBeNull()
|
||||
expect(device.getParameter('Device.DeviceInfo.ProvisioningCode')!.value).toBe('test-code')
|
||||
expect(device.getProperty('Device.DeviceInfo.ProvisioningCode')).toBeNull()
|
||||
})
|
||||
|
||||
it('does NOT move read-only properties (Manufacturer)', () => {
|
||||
const device = makeDevice()
|
||||
addProperty(device, 'Device.DeviceInfo.Manufacturer', 'Sagemcom')
|
||||
|
||||
const count = reclassifyDeviceFromCatalog(device)
|
||||
|
||||
expect(count).toBe(0)
|
||||
expect(device.getProperty('Device.DeviceInfo.Manufacturer')).not.toBeNull()
|
||||
expect(device.getParameter('Device.DeviceInfo.Manufacturer')).toBeNull()
|
||||
})
|
||||
|
||||
it('does NOT move properties not in catalog', () => {
|
||||
const device = makeDevice()
|
||||
addProperty(device, 'Device.Vendor.CustomParam.Unknown', 'value')
|
||||
|
||||
const count = reclassifyDeviceFromCatalog(device)
|
||||
|
||||
// Not in catalog → stays as property (default behavior for unknown paths)
|
||||
expect(count).toBe(0)
|
||||
expect(device.getProperty('Device.Vendor.CustomParam.Unknown')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('handles instance paths with numeric segments', () => {
|
||||
const device = makeDevice()
|
||||
// Device.IP.Interface.{i}.Enable is writable in catalog
|
||||
addProperty(device, 'Device.IP.Interface.1.Enable', 'true')
|
||||
|
||||
const count = reclassifyDeviceFromCatalog(device)
|
||||
|
||||
expect(count).toBe(1)
|
||||
expect(device.getParameter('Device.IP.Interface.1.Enable')).not.toBeNull()
|
||||
expect(device.getProperty('Device.IP.Interface.1.Enable')).toBeNull()
|
||||
})
|
||||
|
||||
it('reclassifies multiple properties in one call', () => {
|
||||
const device = makeDevice()
|
||||
addProperty(device, 'Device.ManagementServer.PeriodicInformInterval', '60')
|
||||
addProperty(device, 'Device.DeviceInfo.ProvisioningCode', 'code')
|
||||
addProperty(device, 'Device.DeviceInfo.Manufacturer', 'Sagemcom') // read-only, stays
|
||||
addProperty(device, 'Device.DeviceInfo.SerialNumber', 'SN123') // read-only, stays
|
||||
|
||||
const count = reclassifyDeviceFromCatalog(device)
|
||||
|
||||
expect(count).toBe(2)
|
||||
expect(device.getParameters()).toHaveLength(2)
|
||||
expect(device.getProperties()).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('preserves value and type when moving property to parameter', () => {
|
||||
const device = makeDevice()
|
||||
device.setProperty(
|
||||
new DeviceProperty('Device.ManagementServer.PeriodicInformInterval', '120', ParameterType.UNSIGNED_INT)
|
||||
)
|
||||
|
||||
reclassifyDeviceFromCatalog(device)
|
||||
|
||||
const param = device.getParameter('Device.ManagementServer.PeriodicInformInterval')
|
||||
expect(param).not.toBeNull()
|
||||
expect(param!.value).toBe('120')
|
||||
expect(param!.type).toBe(ParameterType.UNSIGNED_INT)
|
||||
})
|
||||
|
||||
it('returns 0 when device has no properties', () => {
|
||||
const device = makeDevice()
|
||||
|
||||
const count = reclassifyDeviceFromCatalog(device)
|
||||
|
||||
expect(count).toBe(0)
|
||||
})
|
||||
|
||||
it('does not touch existing parameters', () => {
|
||||
const device = makeDevice()
|
||||
device.setParameter(new DeviceParameter('Device.ManagementServer.URL', 'http://acs', ParameterType.STRING))
|
||||
addProperty(device, 'Device.DeviceInfo.Manufacturer', 'Sagemcom')
|
||||
|
||||
reclassifyDeviceFromCatalog(device)
|
||||
|
||||
expect(device.getParameter('Device.ManagementServer.URL')!.value).toBe('http://acs')
|
||||
expect(device.getParameters()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('works with InternetGatewayDevice paths (TR-098) — not in TR-181 catalog', () => {
|
||||
const device = new Device(new DeviceId('Huawei', '202BC1', 'BM632w', '000000'))
|
||||
device.setGpnStatus('unsupported')
|
||||
device.setDataModel('tr-098')
|
||||
addProperty(device, 'InternetGatewayDevice.DeviceInfo.ProvisioningCode', 'code')
|
||||
addProperty(device, 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval', '60')
|
||||
|
||||
const count = reclassifyDeviceFromCatalog(device)
|
||||
|
||||
// TR-098 paths are NOT in the TR-181 catalog → nothing reclassified
|
||||
expect(count).toBe(0)
|
||||
expect(device.getProperties()).toHaveLength(2)
|
||||
expect(device.getParameters()).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
@@ -108,19 +108,19 @@ describe('Tr069TraceService', () => {
|
||||
expect(fsPromisesMocks.writeFile).toHaveBeenCalledTimes(2)
|
||||
expect(fsPromisesMocks.writeFile).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.stringContaining('config/tr069-trace.json.tmp'),
|
||||
expect.stringContaining('tr069-trace.json.tmp'),
|
||||
expect.stringContaining(baseEntry.deviceId),
|
||||
'utf-8'
|
||||
)
|
||||
expect(fsPromisesMocks.writeFile).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.stringContaining('config/tr069-trace.json.tmp'),
|
||||
expect.stringContaining('tr069-trace.json.tmp'),
|
||||
JSON.stringify({ entries: [] }, null, 2),
|
||||
'utf-8'
|
||||
)
|
||||
expect(fsMocks.renameSync).toHaveBeenCalledWith(
|
||||
expect.stringContaining('config/tr069-trace.json.tmp'),
|
||||
expect.stringContaining('config/tr069-trace.json')
|
||||
expect.stringContaining('tr069-trace.json.tmp'),
|
||||
expect.stringContaining('tr069-trace.json')
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user