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 const desiredParameterArb: fc.Arbitrary = 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 { 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 } 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 } ) }) })