- Remove SOAP from Docker stdout; route to per-device trace files only - Fix trace service paths using process.cwd() (/app/data/, /app/logs/tr069/) - Fix Docker volume permissions: dedicated /app/data & /app/logs/tr069 dirs - Add GET /api/v1/tr069-trace/:id/log endpoint (tail N lines) - Rewrite Tr069Trace.vue: clickable rows, dark log viewer, 3s auto-refresh - Add simulator-ui-env.js to public/ (remove from config/) - TR-181/InternetGatewayDevice dataModel auto-detection - Parameter writability config with W/R fallback default - Fix device_action_executor for GetParameterNames detection flow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
193 lines
7.6 KiB
TypeScript
193 lines
7.6 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
normalizePath,
|
|
resolveWritability,
|
|
} from '../../../app/services/parameter_writability.js'
|
|
import type { DeviceSnapshot } from '../../../app/domain/device/device.js'
|
|
import { OnboardingStatus } from '../../../app/domain/device/device.js'
|
|
import { DeviceId } from '../../../app/domain/device/device_id.js'
|
|
import { ParameterType } from '../../../app/domain/shared/types.js'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function makeSnapshot(
|
|
overrides: Partial<DeviceSnapshot> = {}
|
|
): DeviceSnapshot {
|
|
return {
|
|
deviceId: new DeviceId('Sagemcom', '10:6f:3f', 'Fast5360', 'SN123'),
|
|
identifier: 'abc123',
|
|
createdAt: new Date(),
|
|
lastSeen: new Date(),
|
|
onboardingStatus: OnboardingStatus.COMPLETED,
|
|
isOnline: false,
|
|
gpnStatus: 'unknown',
|
|
dataModel: 'unknown',
|
|
parameters: [],
|
|
properties: [],
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
function param(name: string) {
|
|
return { name, value: null, type: ParameterType.STRING }
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// normalizePath
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('normalizePath', () => {
|
|
it('leaves paths without numeric segments unchanged', () => {
|
|
expect(normalizePath('Device.DeviceInfo.Manufacturer')).toBe(
|
|
'Device.DeviceInfo.Manufacturer'
|
|
)
|
|
})
|
|
|
|
it('replaces a single numeric segment in the middle', () => {
|
|
expect(normalizePath('Device.IP.Interface.1.Enable')).toBe(
|
|
'Device.IP.Interface.{i}.Enable'
|
|
)
|
|
})
|
|
|
|
it('replaces a numeric segment at the end', () => {
|
|
expect(normalizePath('Device.IP.Interface.1')).toBe(
|
|
'Device.IP.Interface.{i}'
|
|
)
|
|
})
|
|
|
|
it('replaces multiple numeric segments', () => {
|
|
expect(
|
|
normalizePath('Device.IP.Interface.1.IPv4Address.2.IPAddress')
|
|
).toBe('Device.IP.Interface.{i}.IPv4Address.{i}.IPAddress')
|
|
})
|
|
|
|
it('handles large instance numbers', () => {
|
|
expect(normalizePath('Device.WiFi.AccessPoint.99.Security.WEPKey.10.WEPKey')).toBe(
|
|
'Device.WiFi.AccessPoint.{i}.Security.WEPKey.{i}.WEPKey'
|
|
)
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// resolveWritability — gpnStatus = 'supported'
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('resolveWritability — gpnStatus: supported', () => {
|
|
it('returns writable=true source=live when param is in parameters[]', () => {
|
|
const snapshot = makeSnapshot({
|
|
gpnStatus: 'supported',
|
|
parameters: [param('Device.ManagementServer.URL')],
|
|
})
|
|
expect(resolveWritability(snapshot, 'Device.ManagementServer.URL')).toEqual(
|
|
{ writable: true, source: 'live' }
|
|
)
|
|
})
|
|
|
|
it('returns writable=false source=live when param is in properties[]', () => {
|
|
const snapshot = makeSnapshot({
|
|
gpnStatus: 'supported',
|
|
properties: [param('Device.DeviceInfo.Manufacturer')],
|
|
})
|
|
expect(resolveWritability(snapshot, 'Device.DeviceInfo.Manufacturer')).toEqual(
|
|
{ writable: false, source: 'live' }
|
|
)
|
|
})
|
|
|
|
it('falls back to catalog when param is absent from both arrays', () => {
|
|
// Device.ManagementServer.URL is readWrite in TR-181 catalog
|
|
const snapshot = makeSnapshot({ gpnStatus: 'supported' })
|
|
const result = resolveWritability(snapshot, 'Device.ManagementServer.URL')
|
|
// catalog or unknown — but NOT live since arrays are empty
|
|
expect(result.source).not.toBe('live')
|
|
})
|
|
|
|
it('parameters[] takes precedence over properties[] when both have the param', () => {
|
|
const snapshot = makeSnapshot({
|
|
gpnStatus: 'supported',
|
|
parameters: [param('Device.ManagementServer.URL')],
|
|
properties: [param('Device.ManagementServer.URL')],
|
|
})
|
|
expect(resolveWritability(snapshot, 'Device.ManagementServer.URL')).toEqual(
|
|
{ writable: true, source: 'live' }
|
|
)
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// resolveWritability — gpnStatus = 'unsupported'
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('resolveWritability — gpnStatus: unsupported', () => {
|
|
it('returns catalog value for a known readOnly TR-181 parameter', () => {
|
|
// Device.DeviceInfo.Manufacturer is readOnly in TR-181
|
|
const snapshot = makeSnapshot({ gpnStatus: 'unsupported' })
|
|
const result = resolveWritability(snapshot, 'Device.DeviceInfo.Manufacturer')
|
|
expect(result).toEqual({ writable: false, source: 'catalog' })
|
|
})
|
|
|
|
it('returns catalog value for a known readWrite TR-181 parameter', () => {
|
|
// Device.ManagementServer.URL is readWrite in TR-181
|
|
const snapshot = makeSnapshot({ gpnStatus: 'unsupported' })
|
|
const result = resolveWritability(snapshot, 'Device.ManagementServer.URL')
|
|
expect(result).toEqual({ writable: true, source: 'catalog' })
|
|
})
|
|
|
|
it('returns catalog value for an instance path (numeric → {i} normalization)', () => {
|
|
// Device.IP.Interface.{i}.Enable is readWrite in TR-181
|
|
const snapshot = makeSnapshot({ gpnStatus: 'unsupported' })
|
|
const result = resolveWritability(snapshot, 'Device.IP.Interface.1.Enable')
|
|
expect(result).toEqual({ writable: true, source: 'catalog' })
|
|
})
|
|
|
|
it('returns writable=true source=default for a path absent from catalog', () => {
|
|
const snapshot = makeSnapshot({ gpnStatus: 'unsupported' })
|
|
const result = resolveWritability(snapshot, 'Device.Vendor.CustomParam.NotInCatalog')
|
|
expect(result).toEqual({ writable: true, source: 'default' })
|
|
})
|
|
|
|
it('ignores parameters[] and properties[] (does not use live data)', () => {
|
|
const snapshot = makeSnapshot({
|
|
gpnStatus: 'unsupported',
|
|
parameters: [param('Device.ManagementServer.URL')],
|
|
properties: [param('Device.DeviceInfo.Manufacturer')],
|
|
})
|
|
// Should use catalog, not live arrays
|
|
const result = resolveWritability(snapshot, 'Device.DeviceInfo.Manufacturer')
|
|
expect(result.source).toBe('catalog')
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// resolveWritability — gpnStatus = 'unknown'
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('resolveWritability — gpnStatus: unknown', () => {
|
|
it('returns catalog value for a known readOnly TR-181 parameter', () => {
|
|
const snapshot = makeSnapshot({ gpnStatus: 'unknown' })
|
|
const result = resolveWritability(snapshot, 'Device.DeviceInfo.Manufacturer')
|
|
expect(result).toEqual({ writable: false, source: 'catalog' })
|
|
})
|
|
|
|
it('returns catalog value for a known readWrite TR-181 parameter', () => {
|
|
const snapshot = makeSnapshot({ gpnStatus: 'unknown' })
|
|
const result = resolveWritability(snapshot, 'Device.ManagementServer.URL')
|
|
expect(result).toEqual({ writable: true, source: 'catalog' })
|
|
})
|
|
|
|
it('returns writable=null source=unknown for a path absent from catalog', () => {
|
|
const snapshot = makeSnapshot({ gpnStatus: 'unknown' })
|
|
const result = resolveWritability(snapshot, 'Device.Vendor.CustomParam.NotInCatalog')
|
|
expect(result).toEqual({ writable: null, source: 'unknown' })
|
|
})
|
|
|
|
it('distinguishes from unsupported: returns null not true for missing params', () => {
|
|
const snapshotUnknown = makeSnapshot({ gpnStatus: 'unknown' })
|
|
const snapshotUnsupported = makeSnapshot({ gpnStatus: 'unsupported' })
|
|
const path = 'Device.Vendor.CustomParam.NotInCatalog'
|
|
expect(resolveWritability(snapshotUnknown, path).writable).toBeNull()
|
|
expect(resolveWritability(snapshotUnsupported, path).writable).toBe(true)
|
|
})
|
|
})
|