import { describe, it, expect, vi, beforeEach } from 'vitest' import fc from 'fast-check' import ConfigurationEngine from '../../app/services/configuration_engine.js' import type { Configuration, ConfigurationCriteria } from '../../app/domain/configuration/configuration.js' import { PolicyType } from '../../app/domain/configuration/configuration.js' import type { Device } from '../../app/domain/device/device.js' import type { DeviceId } from '../../app/domain/device/device_id.js' import { buildProvisioningCode } from '../../app/utils/provisioning_code.js' // Mock the models and services vi.mock('#models/configuration', () => { return { default: { find: vi.fn(), findOne: vi.fn(), }, } }) vi.mock('#models/device', () => { return { default: { findOne: vi.fn(), }, } }) vi.mock('../../app/services/firmware_catalog_service.js', () => { return { default: class FirmwareCatalogService { async resolveUpgradePath() { return [] } }, } }) import ConfigurationModel from '#models/configuration' import DeviceModel from '#models/device' /** * Property 6: Highest priority Configuration wins * Validates: Requirements 3.2 * * Property: When multiple configurations match a device, the one with highest * priority is selected. If no configurations match, result is compliant. */ describe('Property 6: Highest priority Configuration wins', () => { let engine: ConfigurationEngine beforeEach(() => { engine = new ConfigurationEngine() vi.clearAllMocks() }) // Arbitrary to generate positive priority integers const priority = fc.integer({ min: 1, max: 1000 }) // Arbitrary to generate a valid device function deviceArb(): fc.Arbitrary { return fc.record({ deviceId: fc.record({ id: fc.uuid(), manufacturer: fc.string(), oui: fc.string(), productClass: fc.string(), serialNumber: fc.string(), }) as unknown as fc.Arbitrary, createdAt: fc.date(), }) as unknown as fc.Arbitrary } // Arbitrary to generate a valid configuration function configurationArb(overrides?: Partial): fc.Arbitrary { const baseArb = fc.record({ identifier: fc.string().filter((s) => s.length > 0), name: fc.string(), version: fc.stringMatching(/\d+\.\d+\.\d+/), priority: priority, policy: fc.constantFrom(PolicyType.ONCE, PolicyType.MAINTAIN), enabled: fc.constant(true), tags: fc.array(fc.string()), criteria: fc.record({ manufacturer: fc.option(fc.string(), { nil: undefined }), oui: fc.option(fc.string(), { nil: undefined }), productClass: fc.option(fc.string(), { nil: undefined }), serialNumber: fc.option(fc.string(), { nil: undefined }), }) as fc.Arbitrary, inventory: fc.array( fc.record({ path: fc.string(), }) ) as fc.Arbitrary, desiredParameters: fc.array( fc.record({ name: fc.string(), value: fc.oneof(fc.string(), fc.integer(), fc.boolean()), type: fc.constantFrom('string', 'int', 'boolean'), }) ) as fc.Arbitrary, firmwareTarget: fc.option( fc.record({ productClass: fc.string(), targetVersion: fc.stringMatching(/\d+\.\d+\.\d+/), }), { nil: undefined } ) as fc.Arbitrary, schedule: fc.constant(undefined) as fc.Arbitrary, createdAt: fc.date() as fc.Arbitrary, updatedAt: fc.date() as fc.Arbitrary, }) if (overrides) { return baseArb.map((val) => ({ ...val, ...overrides, })) as fc.Arbitrary } return baseArb as fc.Arbitrary } it('selects configuration with highest priority when multiple match', async () => { await fc.assert( fc.asyncProperty(deviceArb(), async (device) => { // Generate configurations with arbitrary priorities const numConfigs = fc.sample(fc.integer({ min: 2, max: 5 }), 1)[0] const configsArray = fc.sample(configurationArb(), numConfigs) // Ensure all have matching criteria (empty) const configurations = configsArray.map((c) => ({ ...c, criteria: {} as ConfigurationCriteria, })) vi.mocked(ConfigurationModel.find).mockResolvedValue(configurations as any) vi.mocked(DeviceModel.findOne).mockResolvedValue(device as any) const result = await engine.evaluate(device) // Find expected highest priority config const highest = configurations.reduce((max, c) => c.priority > max.priority ? c : max ) expect(result.configuration?.priority).toBe(highest.priority) expect(result.configuration?.identifier).toBe(highest.identifier) }) ) }) it('returns compliant result when no configurations match', async () => { await fc.assert( fc.asyncProperty(deviceArb(), async (device) => { vi.mocked(ConfigurationModel.find).mockResolvedValue([]) const result = await engine.evaluate(device) expect(result.configuration).toBeNull() expect(result.isAlreadyCompliant).toBe(true) expect(result.parameterDiffs).toEqual([]) expect(result.firmwareMigrationPath).toEqual([]) }) ) }) }) /** * Property 8: Diff engine computes only differing parameters * Validates: Requirements 5.1, 5.2, 5.3, 6.5 * * Property: The diff computation returns only parameters where currentValue !== targetValue. */ describe('Property 8: Diff engine computes only differing parameters', () => { let engine: ConfigurationEngine beforeEach(() => { engine = new ConfigurationEngine() vi.clearAllMocks() }) it('returns empty diffs when all desired parameters match current values', async () => { await fc.assert( fc.asyncProperty( fc.array( fc.record({ name: fc.string().filter((s) => s.length > 0), value: fc.oneof(fc.string(), fc.integer(), fc.boolean()), type: fc.constantFrom('string', 'int', 'boolean'), }), { minLength: 1, maxLength: 10 } ).map((params) => { // Deduplicate by name to avoid conflicting values for the same parameter const seen = new Set() return params.filter((p) => { if (seen.has(p.name)) return false seen.add(p.name) return true }) }).filter((params) => params.length > 0), async (desiredParams) => { const device = { deviceId: { id: 'test', manufacturer: 'Test' }, createdAt: new Date(), getParameter: (name: string) => ({ value: desiredParams.find((p) => p.name === name)?.value, }), } as any const config = { identifier: 'test', priority: 1, enabled: true, policy: PolicyType.MAINTAIN, criteria: {}, inventory: [], desiredParameters: desiredParams, createdAt: new Date(), updatedAt: new Date(), } as any vi.mocked(ConfigurationModel.find).mockResolvedValue([config]) vi.mocked(DeviceModel.findOne).mockResolvedValue(device) const result = await engine.evaluate(device) expect(result.parameterDiffs).toEqual([]) } ) ) }) it('returns diffs only for parameters that differ', async () => { const desiredParams = [ { name: 'param1', value: 'value1', type: 'string' }, { name: 'param2', value: 'value2', type: 'string' }, ] const device = { deviceId: { id: 'test', manufacturer: 'Test' }, createdAt: new Date(), getParameter: (name: string) => { if (name === 'param1') return { value: 'value1' } // Matches if (name === 'param2') return { value: 'different' } // Differs return { value: undefined } }, } as any const config = { identifier: 'test', priority: 1, enabled: true, policy: PolicyType.MAINTAIN, criteria: {}, inventory: [], desiredParameters: desiredParams, createdAt: new Date(), updatedAt: new Date(), } as any vi.mocked(ConfigurationModel.find).mockResolvedValue([config]) const engine = new ConfigurationEngine() const result = await engine.evaluate(device) expect(result.parameterDiffs).toHaveLength(1) expect(result.parameterDiffs[0].name).toBe('param2') expect(result.parameterDiffs[0].currentValue).toBe('different') expect(result.parameterDiffs[0].targetValue).toBe('value2') }) }) /** * Property 10: Policy "once" applies based on content hash comparison * Validates: Requirements 6.1, 6.3 * * Property: A configuration with policy "once" applies only if the device * doesn't have a matching provisioning code with the same content hash. */ describe('Property 10: Policy "once" applies based on content hash comparison', () => { let engine: ConfigurationEngine beforeEach(() => { engine = new ConfigurationEngine() vi.clearAllMocks() }) it('applies "once" policy if device has no provisioning code', async () => { const config = { identifier: 'config1', priority: 1, enabled: true, policy: PolicyType.ONCE, criteria: {}, inventory: [], desiredParameters: [{ name: 'param1', value: 'value1', type: 'string' }], createdAt: new Date(), updatedAt: new Date(), } as any const device = { deviceId: { id: 'test', manufacturer: 'Test' }, createdAt: new Date(), getParameter: (name: string) => ({ value: name === 'param1' ? 'different' : undefined, }), // No provisioningCode property } as any vi.mocked(ConfigurationModel.find).mockResolvedValue([config]) const result = await engine.evaluate(device) expect(result.parameterDiffs).toHaveLength(1) }) it('skips "once" policy if provisioning code matches content hash', async () => { const config = { identifier: 'config1', priority: 1, enabled: true, policy: PolicyType.ONCE, criteria: {}, inventory: [], desiredParameters: [{ name: 'param1', value: 'value1', type: 'string' }], createdAt: new Date(), updatedAt: new Date(), } as any const provCode = buildProvisioningCode(config) const device = { deviceId: { id: 'test', manufacturer: 'Test' }, createdAt: new Date(), getParameter: (name: string) => ({ value: name === 'param1' ? 'value1' : undefined, }), provisioningCode: provCode, } as any vi.mocked(ConfigurationModel.find).mockResolvedValue([config]) const result = await engine.evaluate(device) expect(result.isAlreadyCompliant).toBe(true) expect(result.parameterDiffs).toEqual([]) }) it('reapplies "once" policy if content hash changes', async () => { const configV1 = { identifier: 'config1', priority: 1, enabled: true, policy: PolicyType.ONCE, criteria: {}, inventory: [], desiredParameters: [{ name: 'param1', value: 'value1', type: 'string' }], createdAt: new Date(), updatedAt: new Date(), } as any const provCodeV1 = buildProvisioningCode(configV1) // New config version with different content const configV2 = { ...configV1, desiredParameters: [{ name: 'param1', value: 'value2', type: 'string' }], } as any const device = { deviceId: { id: 'test', manufacturer: 'Test' }, createdAt: new Date(), getParameter: (name: string) => ({ value: name === 'param1' ? 'value1' : undefined, }), provisioningCode: provCodeV1, // Has old provisioning code } as any vi.mocked(ConfigurationModel.find).mockResolvedValue([configV2]) const result = await engine.evaluate(device) // Should reapply because content hash changed expect(result.parameterDiffs).toHaveLength(1) expect(result.parameterDiffs[0].targetValue).toBe('value2') }) }) /** * Property 16: Dry-run is side-effect-free * Validates: Requirements 16.1, 16.2 * * Property: Calling dryRun() should not modify the device or database state. */ describe('Property 16: Dry-run is side-effect-free', () => { let engine: ConfigurationEngine beforeEach(() => { engine = new ConfigurationEngine() vi.clearAllMocks() }) it('dryRun returns EngineResult without modifying device', async () => { const config = { identifier: 'config1', priority: 1, enabled: true, policy: PolicyType.MAINTAIN, criteria: {}, inventory: [], desiredParameters: [{ name: 'param1', value: 'value1', type: 'string' }], createdAt: new Date(), updatedAt: new Date(), } as any const device = { deviceId: { id: 'test', manufacturer: 'Test' }, createdAt: new Date(), getParameter: (name: string) => ({ value: name === 'param1' ? 'different' : undefined, }), } as any // Make a deep copy to check it hasn't changed const deviceSnapshot = JSON.parse(JSON.stringify(device)) vi.mocked(ConfigurationModel.findOne).mockResolvedValue(config) vi.mocked(DeviceModel.findOne).mockResolvedValue(device) const result = await engine.dryRun('config1', 'test') // Verify device snapshot hasn't changed expect(JSON.stringify(device)).toBe(JSON.stringify(deviceSnapshot)) // Verify result is valid expect(result).toHaveProperty('configurationId', 'config1') expect(result).toHaveProperty('deviceIdentifier', 'test') expect(result.evaluation).toHaveProperty('parameterDiffs') }) it('dryRun does not persist any changes', async () => { const config = { identifier: 'config1', priority: 1, enabled: true, policy: PolicyType.ONCE, criteria: {}, inventory: [], desiredParameters: [{ name: 'param1', value: 'value1', type: 'string' }], createdAt: new Date(), updatedAt: new Date(), } as any const device = { deviceId: { id: 'test', manufacturer: 'Test' }, createdAt: new Date(), getParameter: (name: string) => ({ value: name === 'param1' ? 'different' : undefined, }), } as any vi.mocked(ConfigurationModel.findOne).mockResolvedValue(config) vi.mocked(DeviceModel.findOne).mockResolvedValue(device) const result = await engine.dryRun('config1', 'test') // Verify result is a DryRunResult with expected properties expect(result).toBeDefined() expect(result.configurationId).toBe('config1') expect(result.deviceIdentifier).toBe('test') // Verify no save/update calls were made (mocks should not have been called) // Since we're mocking, any DB mutations would show up in mock call counts expect(vi.mocked(ConfigurationModel.findOne)).toHaveBeenCalledTimes(1) // Only read call expect(vi.mocked(DeviceModel.findOne)).toHaveBeenCalledTimes(1) // Only read call // No mutation methods should have been called // (we didn't mock save/update, so they shouldn't exist on our mocked objects) }) })