chore: initial commit — AdonisJS v7 TR-069 ACS
- Docker infrastructure (Dockerfile, docker-compose.yml, genieacs-sim) - AdonisJS v7 app with Inertia.js + Vue 3 - MongoDB integration - CWMP/TR-069 state machine engine - Configuration engine with compliance tracking - Branding: ADAM ACS (favicon, navbar, locales) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
commit
d1c65dbf1e
164 files changed
+29127
No files matched your search
@@ -0,0 +1,42 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { matchGlob } from '../../../app/utils/glob_match.js'
|
||||
|
||||
describe('matchGlob', () => {
|
||||
it('matches exact strings', () => {
|
||||
expect(matchGlob('Sagemcom', 'Sagemcom')).toBe(true)
|
||||
expect(matchGlob('Sagemcom', 'sagemcom')).toBe(false)
|
||||
})
|
||||
|
||||
it('matches * as any sequence of characters', () => {
|
||||
expect(matchGlob('Livebox*', 'Livebox5')).toBe(true)
|
||||
expect(matchGlob('Livebox*', 'Livebox')).toBe(true)
|
||||
expect(matchGlob('Livebox*', 'LiveboxPro')).toBe(true)
|
||||
expect(matchGlob('*box', 'Livebox')).toBe(true)
|
||||
expect(matchGlob('*box*', 'Livebox5')).toBe(true)
|
||||
})
|
||||
|
||||
it('matches ? as exactly one character', () => {
|
||||
expect(matchGlob('BM63?w', 'BM632w')).toBe(true)
|
||||
expect(matchGlob('BM63?w', 'BM63w')).toBe(false)
|
||||
expect(matchGlob('BM63?w', 'BM6312w')).toBe(false)
|
||||
})
|
||||
|
||||
it('is case-sensitive', () => {
|
||||
expect(matchGlob('Livebox*', 'livebox5')).toBe(false)
|
||||
expect(matchGlob('livebox*', 'Livebox5')).toBe(false)
|
||||
})
|
||||
|
||||
it('escapes regex special characters in pattern', () => {
|
||||
expect(matchGlob('device.info', 'device.info')).toBe(true)
|
||||
expect(matchGlob('device.info', 'deviceXinfo')).toBe(false)
|
||||
expect(matchGlob('a+b', 'a+b')).toBe(true)
|
||||
expect(matchGlob('a+b', 'aab')).toBe(false)
|
||||
})
|
||||
|
||||
it('handles empty pattern and value', () => {
|
||||
expect(matchGlob('', '')).toBe(true)
|
||||
expect(matchGlob('*', '')).toBe(true)
|
||||
expect(matchGlob('?', '')).toBe(false)
|
||||
expect(matchGlob('', 'something')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,89 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
isParameterCoveredByInventory,
|
||||
suggestInventoryPath,
|
||||
} from '../../../app/utils/inventory_coverage.js'
|
||||
|
||||
describe('isParameterCoveredByInventory', () => {
|
||||
it('returns true for subtree match (path ending with dot)', () => {
|
||||
const inventoryPaths = [{ path: 'InternetGatewayDevice.ManagementServer.' }]
|
||||
expect(
|
||||
isParameterCoveredByInventory(
|
||||
'InternetGatewayDevice.ManagementServer.PeriodicInformInterval',
|
||||
inventoryPaths
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('returns true for exact match (path without trailing dot)', () => {
|
||||
const inventoryPaths = [
|
||||
{ path: 'InternetGatewayDevice.ManagementServer.PeriodicInformInterval' },
|
||||
]
|
||||
expect(
|
||||
isParameterCoveredByInventory(
|
||||
'InternetGatewayDevice.ManagementServer.PeriodicInformInterval',
|
||||
inventoryPaths
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when parameter is not covered', () => {
|
||||
const inventoryPaths = [{ path: 'InternetGatewayDevice.DeviceInfo.' }]
|
||||
expect(
|
||||
isParameterCoveredByInventory(
|
||||
'InternetGatewayDevice.ManagementServer.PeriodicInformInterval',
|
||||
inventoryPaths
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for empty inventory', () => {
|
||||
expect(
|
||||
isParameterCoveredByInventory(
|
||||
'InternetGatewayDevice.ManagementServer.PeriodicInformInterval',
|
||||
[]
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('handles multiple inventory paths', () => {
|
||||
const inventoryPaths = [
|
||||
{ path: 'InternetGatewayDevice.DeviceInfo.' },
|
||||
{ path: 'InternetGatewayDevice.ManagementServer.' },
|
||||
]
|
||||
expect(
|
||||
isParameterCoveredByInventory(
|
||||
'InternetGatewayDevice.ManagementServer.PeriodicInformInterval',
|
||||
inventoryPaths
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('does not match partial path names without trailing dot', () => {
|
||||
const inventoryPaths = [{ path: 'InternetGatewayDevice.Man' }]
|
||||
expect(
|
||||
isParameterCoveredByInventory(
|
||||
'InternetGatewayDevice.ManagementServer.PeriodicInformInterval',
|
||||
inventoryPaths
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('suggestInventoryPath', () => {
|
||||
it('removes the leaf name and adds trailing dot', () => {
|
||||
expect(
|
||||
suggestInventoryPath('InternetGatewayDevice.ManagementServer.PeriodicInformInterval')
|
||||
).toBe('InternetGatewayDevice.ManagementServer.')
|
||||
})
|
||||
|
||||
it('handles deeply nested paths', () => {
|
||||
expect(
|
||||
suggestInventoryPath('InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.Name')
|
||||
).toBe('InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.')
|
||||
})
|
||||
|
||||
it('handles single-level path', () => {
|
||||
expect(suggestInventoryPath('ParameterName')).toBe('.')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,119 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
computeContentHash,
|
||||
buildProvisioningCode,
|
||||
parseProvisioningCode,
|
||||
} from '../../../app/utils/provisioning_code.js'
|
||||
import type { Configuration } from '../../../app/domain/configuration/configuration.js'
|
||||
import { PolicyType } from '../../../app/domain/configuration/configuration.js'
|
||||
|
||||
function makeConfig(overrides: Partial<Configuration> = {}): Configuration {
|
||||
return {
|
||||
identifier: 'BM632w-standard',
|
||||
name: 'Configuration standard BM632w',
|
||||
version: '1.0.1',
|
||||
priority: 100,
|
||||
policy: PolicyType.ONCE,
|
||||
enabled: true,
|
||||
tags: [],
|
||||
criteria: { productClass: 'BM632w' },
|
||||
inventory: [],
|
||||
desiredParameters: [
|
||||
{ name: 'Device.Param.B', value: 'hello', type: 'string' },
|
||||
{ name: 'Device.Param.A', value: 42, type: 'int' },
|
||||
],
|
||||
firmwareTarget: { productClass: 'BM632w', targetVersion: '2.1.0' },
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('computeContentHash', () => {
|
||||
it('returns a 12-character hex string', () => {
|
||||
const hash = computeContentHash(makeConfig())
|
||||
expect(hash).toMatch(/^[a-f0-9]{12}$/)
|
||||
})
|
||||
|
||||
it('is deterministic for the same config', () => {
|
||||
const config = makeConfig()
|
||||
expect(computeContentHash(config)).toBe(computeContentHash(config))
|
||||
})
|
||||
|
||||
it('sorts desiredParameters by name for consistency', () => {
|
||||
const config1 = makeConfig({
|
||||
desiredParameters: [
|
||||
{ name: 'B.Param', value: 1, type: 'int' },
|
||||
{ name: 'A.Param', value: 2, type: 'int' },
|
||||
],
|
||||
})
|
||||
const config2 = makeConfig({
|
||||
desiredParameters: [
|
||||
{ name: 'A.Param', value: 2, type: 'int' },
|
||||
{ name: 'B.Param', value: 1, type: 'int' },
|
||||
],
|
||||
})
|
||||
expect(computeContentHash(config1)).toBe(computeContentHash(config2))
|
||||
})
|
||||
|
||||
it('changes when desiredParameters change', () => {
|
||||
const config1 = makeConfig({
|
||||
desiredParameters: [{ name: 'A.Param', value: 1, type: 'int' }],
|
||||
})
|
||||
const config2 = makeConfig({
|
||||
desiredParameters: [{ name: 'A.Param', value: 2, type: 'int' }],
|
||||
})
|
||||
expect(computeContentHash(config1)).not.toBe(computeContentHash(config2))
|
||||
})
|
||||
|
||||
it('changes when firmwareTarget changes', () => {
|
||||
const config1 = makeConfig({
|
||||
firmwareTarget: { productClass: 'BM632w', targetVersion: '2.0.0' },
|
||||
})
|
||||
const config2 = makeConfig({
|
||||
firmwareTarget: { productClass: 'BM632w', targetVersion: '3.0.0' },
|
||||
})
|
||||
expect(computeContentHash(config1)).not.toBe(computeContentHash(config2))
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildProvisioningCode', () => {
|
||||
it('returns the correct format', () => {
|
||||
const config = makeConfig()
|
||||
const code = buildProvisioningCode(config)
|
||||
expect(code).toMatch(/^BM632w-standard\(v1\.0\.1\)-[a-f0-9]{12}$/)
|
||||
})
|
||||
|
||||
it('matches the regex pattern from the spec', () => {
|
||||
const config = makeConfig()
|
||||
const code = buildProvisioningCode(config)
|
||||
expect(code).toMatch(/^(.+)\(v(.+)\)-([a-f0-9]{12})$/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseProvisioningCode', () => {
|
||||
it('parses a valid provisioning code', () => {
|
||||
const result = parseProvisioningCode('BM632w-standard(v1.0.1)-a1b2c3d4e5f6')
|
||||
expect(result).toEqual({
|
||||
identifier: 'BM632w-standard',
|
||||
version: '1.0.1',
|
||||
hash: 'a1b2c3d4e5f6',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns null for invalid format', () => {
|
||||
expect(parseProvisioningCode('invalid')).toBeNull()
|
||||
expect(parseProvisioningCode('')).toBeNull()
|
||||
expect(parseProvisioningCode('id(v1.0)-short')).toBeNull() // hash too short
|
||||
})
|
||||
|
||||
it('round-trips with buildProvisioningCode', () => {
|
||||
const config = makeConfig()
|
||||
const code = buildProvisioningCode(config)
|
||||
const parsed = parseProvisioningCode(code)
|
||||
expect(parsed).not.toBeNull()
|
||||
expect(parsed!.identifier).toBe('BM632w-standard')
|
||||
expect(parsed!.version).toBe('1.0.1')
|
||||
expect(parsed!.hash).toMatch(/^[a-f0-9]{12}$/)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,100 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { isWithinSchedule } from '../../../app/utils/schedule_window.js'
|
||||
import type { ScheduleWindow } from '../../../app/domain/configuration/configuration.js'
|
||||
|
||||
describe('isWithinSchedule', () => {
|
||||
it('returns true when schedule is undefined', () => {
|
||||
expect(isWithinSchedule(undefined)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns true when within a normal window (non-midnight-spanning)', () => {
|
||||
const schedule: ScheduleWindow = {
|
||||
daysOfWeek: [1, 2, 3, 4, 5], // Mon-Fri
|
||||
startHour: 2,
|
||||
endHour: 6,
|
||||
timezone: 'UTC',
|
||||
}
|
||||
// Wednesday at 03:00 UTC
|
||||
const now = new Date('2025-01-08T03:00:00Z')
|
||||
expect(isWithinSchedule(schedule, now)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when outside a normal window', () => {
|
||||
const schedule: ScheduleWindow = {
|
||||
daysOfWeek: [1, 2, 3, 4, 5],
|
||||
startHour: 2,
|
||||
endHour: 6,
|
||||
timezone: 'UTC',
|
||||
}
|
||||
// Wednesday at 10:00 UTC
|
||||
const now = new Date('2025-01-08T10:00:00Z')
|
||||
expect(isWithinSchedule(schedule, now)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false when at the endHour boundary (exclusive)', () => {
|
||||
const schedule: ScheduleWindow = {
|
||||
daysOfWeek: [1, 2, 3, 4, 5],
|
||||
startHour: 2,
|
||||
endHour: 6,
|
||||
timezone: 'UTC',
|
||||
}
|
||||
// Wednesday at 06:00 UTC (endHour is exclusive)
|
||||
const now = new Date('2025-01-08T06:00:00Z')
|
||||
expect(isWithinSchedule(schedule, now)).toBe(false)
|
||||
})
|
||||
|
||||
it('handles midnight-spanning window (startHour > endHour)', () => {
|
||||
const schedule: ScheduleWindow = {
|
||||
daysOfWeek: [0, 1, 2, 3, 4, 5, 6],
|
||||
startHour: 22,
|
||||
endHour: 6,
|
||||
timezone: 'UTC',
|
||||
}
|
||||
// At 23:00 UTC - should be within
|
||||
expect(isWithinSchedule(schedule, new Date('2025-01-08T23:00:00Z'))).toBe(true)
|
||||
// At 03:00 UTC - should be within
|
||||
expect(isWithinSchedule(schedule, new Date('2025-01-08T03:00:00Z'))).toBe(true)
|
||||
// At 10:00 UTC - should be outside
|
||||
expect(isWithinSchedule(schedule, new Date('2025-01-08T10:00:00Z'))).toBe(false)
|
||||
})
|
||||
|
||||
it('filters by day of week', () => {
|
||||
const schedule: ScheduleWindow = {
|
||||
daysOfWeek: [1, 2, 3, 4, 5], // Mon-Fri only
|
||||
startHour: 0,
|
||||
endHour: 23,
|
||||
timezone: 'UTC',
|
||||
}
|
||||
// Saturday 2025-01-11 at 10:00 UTC
|
||||
const saturday = new Date('2025-01-11T10:00:00Z')
|
||||
expect(isWithinSchedule(schedule, saturday)).toBe(false)
|
||||
|
||||
// Monday 2025-01-06 at 10:00 UTC
|
||||
const monday = new Date('2025-01-06T10:00:00Z')
|
||||
expect(isWithinSchedule(schedule, monday)).toBe(true)
|
||||
})
|
||||
|
||||
it('handles IANA timezone conversion', () => {
|
||||
const schedule: ScheduleWindow = {
|
||||
daysOfWeek: [0, 1, 2, 3, 4, 5, 6],
|
||||
startHour: 2,
|
||||
endHour: 6,
|
||||
timezone: 'Europe/Paris', // UTC+1 in winter
|
||||
}
|
||||
// 03:00 UTC = 04:00 Paris time → within [2, 6)
|
||||
expect(isWithinSchedule(schedule, new Date('2025-01-08T03:00:00Z'))).toBe(true)
|
||||
// 00:00 UTC = 01:00 Paris time → outside [2, 6)
|
||||
expect(isWithinSchedule(schedule, new Date('2025-01-08T00:00:00Z'))).toBe(false)
|
||||
})
|
||||
|
||||
it('returns true when daysOfWeek is empty (no day restriction)', () => {
|
||||
const schedule: ScheduleWindow = {
|
||||
daysOfWeek: [],
|
||||
startHour: 2,
|
||||
endHour: 6,
|
||||
timezone: 'UTC',
|
||||
}
|
||||
// Any day at 03:00 UTC
|
||||
expect(isWithinSchedule(schedule, new Date('2025-01-08T03:00:00Z'))).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user