- 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>
153 lines
5.1 KiB
TypeScript
153 lines
5.1 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import * as fc from 'fast-check'
|
|
import {
|
|
computeContentHash,
|
|
buildProvisioningCode,
|
|
parseProvisioningCode,
|
|
} from '../../app/utils/provisioning_code.js'
|
|
import { PolicyType } from '../../app/domain/configuration/configuration.js'
|
|
import type { Configuration, DesiredParameter } from '../../app/domain/configuration/configuration.js'
|
|
|
|
/**
|
|
* **Validates: Requirements 6.2**
|
|
*
|
|
* Property 11: ProvisioningCode format correctness
|
|
*/
|
|
|
|
// --- Arbitraries ---
|
|
|
|
const parameterTypeArb = fc.constantFrom('string', 'int', 'unsignedInt', 'boolean', 'dateTime') as fc.Arbitrary<DesiredParameter['type']>
|
|
|
|
const desiredParameterArb: fc.Arbitrary<DesiredParameter> = fc.record({
|
|
name: fc.stringMatching(/^[A-Za-z][A-Za-z0-9.]{1,50}$/),
|
|
value: fc.oneof(
|
|
fc.string({ minLength: 0, maxLength: 20 }),
|
|
fc.integer(),
|
|
fc.boolean()
|
|
),
|
|
type: parameterTypeArb,
|
|
})
|
|
|
|
const firmwareTargetArb = fc.record({
|
|
productClass: fc.string({ minLength: 1, maxLength: 20 }),
|
|
targetVersion: fc.stringMatching(/^[0-9]+\.[0-9]+\.[0-9]+$/),
|
|
})
|
|
|
|
/** Generates a valid Configuration with random identifier, version, and desired state. */
|
|
function configurationArb(): fc.Arbitrary<Configuration> {
|
|
return fc.record({
|
|
identifier: fc.stringMatching(/^[A-Za-z0-9][A-Za-z0-9_-]{0,30}$/),
|
|
name: fc.string({ minLength: 1, maxLength: 50 }),
|
|
version: fc.stringMatching(/^[0-9]+\.[0-9]+\.[0-9]+$/),
|
|
priority: fc.integer({ min: 1, max: 1000 }),
|
|
policy: fc.constantFrom(PolicyType.ONCE, PolicyType.MAINTAIN),
|
|
enabled: fc.boolean(),
|
|
tags: fc.array(fc.string({ minLength: 1, maxLength: 10 }), { maxLength: 5 }),
|
|
criteria: fc.record({
|
|
manufacturer: fc.option(fc.string({ minLength: 1, maxLength: 20 }), { nil: undefined }),
|
|
productClass: fc.option(fc.string({ minLength: 1, maxLength: 20 }), { nil: undefined }),
|
|
}),
|
|
inventory: fc.array(
|
|
fc.record({ path: fc.stringMatching(/^[A-Za-z][A-Za-z0-9.]{1,30}\.$/) }),
|
|
{ maxLength: 5 }
|
|
),
|
|
desiredParameters: fc.array(desiredParameterArb, { maxLength: 10 }),
|
|
firmwareTarget: fc.option(firmwareTargetArb, { nil: undefined }),
|
|
schedule: fc.constant(undefined),
|
|
createdAt: fc.date(),
|
|
updatedAt: fc.date(),
|
|
}) as fc.Arbitrary<Configuration>
|
|
}
|
|
|
|
describe('Property 11: ProvisioningCode format correctness', () => {
|
|
it('buildProvisioningCode output always matches the expected regex', () => {
|
|
fc.assert(
|
|
fc.property(configurationArb(), (config) => {
|
|
const code = buildProvisioningCode(config)
|
|
expect(code).toMatch(/^(.+)\(v(.+)\)-([a-f0-9]{12})$/)
|
|
}),
|
|
{ numRuns: 200 }
|
|
)
|
|
})
|
|
|
|
it('parseProvisioningCode(buildProvisioningCode(config)) round-trips correctly', () => {
|
|
fc.assert(
|
|
fc.property(configurationArb(), (config) => {
|
|
const code = buildProvisioningCode(config)
|
|
const parsed = parseProvisioningCode(code)
|
|
|
|
expect(parsed).not.toBeNull()
|
|
expect(parsed!.identifier).toBe(config.identifier)
|
|
expect(parsed!.version).toBe(config.version)
|
|
expect(parsed!.hash).toBe(computeContentHash(config))
|
|
}),
|
|
{ numRuns: 200 }
|
|
)
|
|
})
|
|
|
|
it('computeContentHash is deterministic (same config → same hash)', () => {
|
|
fc.assert(
|
|
fc.property(configurationArb(), (config) => {
|
|
const hash1 = computeContentHash(config)
|
|
const hash2 = computeContentHash(config)
|
|
expect(hash1).toBe(hash2)
|
|
}),
|
|
{ numRuns: 200 }
|
|
)
|
|
})
|
|
|
|
it('computeContentHash produces exactly 12 hex characters', () => {
|
|
fc.assert(
|
|
fc.property(configurationArb(), (config) => {
|
|
const hash = computeContentHash(config)
|
|
expect(hash).toMatch(/^[a-f0-9]{12}$/)
|
|
}),
|
|
{ numRuns: 200 }
|
|
)
|
|
})
|
|
|
|
it('changing desiredParameters changes the hash', () => {
|
|
fc.assert(
|
|
fc.property(
|
|
configurationArb(),
|
|
desiredParameterArb,
|
|
(config, extraParam) => {
|
|
// Ensure the extra param has a different name from all existing ones
|
|
const existingNames = new Set(config.desiredParameters.map((p) => p.name))
|
|
if (existingNames.has(extraParam.name)) return // skip this case
|
|
|
|
const hash1 = computeContentHash(config)
|
|
const modified = {
|
|
...config,
|
|
desiredParameters: [...config.desiredParameters, extraParam],
|
|
}
|
|
const hash2 = computeContentHash(modified)
|
|
expect(hash1).not.toBe(hash2)
|
|
}
|
|
),
|
|
{ numRuns: 200 }
|
|
)
|
|
})
|
|
|
|
it('parseProvisioningCode returns null for invalid strings', () => {
|
|
const invalidStringsArb = fc.oneof(
|
|
fc.constant(''),
|
|
fc.constant('no-parens-here'),
|
|
fc.constant('id(v1.0)-tooshort'),
|
|
fc.constant('id(1.0)-a1b2c3d4e5f6'), // missing 'v' prefix
|
|
fc.constant('id(v1.0)-ZZZZZZZZZZZZ'), // uppercase not hex
|
|
// Random strings that don't match the pattern
|
|
fc.string({ minLength: 0, maxLength: 50 }).filter(
|
|
(s) => !/^(.+)\(v(.+)\)-([a-f0-9]{12})$/.test(s)
|
|
)
|
|
)
|
|
|
|
fc.assert(
|
|
fc.property(invalidStringsArb, (input) => {
|
|
expect(parseProvisioningCode(input)).toBeNull()
|
|
}),
|
|
{ numRuns: 200 }
|
|
)
|
|
})
|
|
})
|