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 { 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) }) })