Files
adam-acs/tests/property/provisioning_code.property.spec.ts
T
Kevin L'HereecandCopilot d1c65dbf1e 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>
2026-05-29 16:46:37 +02:00

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