Files
adam-acs/tests/unit/services/configuration_engine.spec.ts
T
Kevin L'HereecandCopilot aff8e14a63 feat: compliance report + auto-versioning for configurations
- Add ComplianceStatus.PENDING and configVersion to DeviceComplianceInfo
- Add ComplianceReport + ComplianceDeviceResult types to ConfigurationEngine
- Expose evaluateSpecific() as public (used by compliance + dry-run)
- Add complianceReport() method: filter matching devices, evaluate each
- Fix matchesCriteria: skip parameterCriteria entries with null/empty name
- Fix getDeviceParameterValue: fallback to plain object arrays (DeviceRecord)
- Fix findDevice: support lookup by both identifier and deviceId.id
- ConfigurationService.update: version is now auto-managed (no user input)
  - behavioral changes (criteria/params/policy/priority/…) → patch bump
  - bumpType field allows 'patch' | 'minor' | 'major'
  - cosmetic changes (name/tags) → no bump
- Add sanitizeCriteria: filter null/empty parameterCriteria names silently
- Add isBehavioralChange helper
- Add DeviceModel.resetComplianceForConfiguration call on behavioral update
- Add ConfigurationModel.distinctTags() + controller endpoint GET /tags
- Add controller endpoint GET /configurations/:id/compliance
- Add DeviceModel.getDistinctValues for inventory fields
- Fix unit tests: mock DeviceModel, remove obsolete version-validation test
- Add 14 new unit tests (engine: 5, service: 9)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01 11:32:17 +02:00

176 lines
5.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import ConfigurationEngine from '../../../app/services/configuration_engine.js'
vi.mock('#models/configuration', () => ({
default: { findOne: vi.fn(), findAllEnabled: vi.fn() },
}))
vi.mock('#models/device', () => ({
default: { getCollection: vi.fn(), findOne: vi.fn() },
}))
import ConfigurationModel from '#models/configuration'
import DeviceModel from '#models/device'
describe('ConfigurationEngine', () => {
let engine: ConfigurationEngine
beforeEach(() => {
engine = new ConfigurationEngine()
vi.clearAllMocks()
})
describe('complianceReport', () => {
it('throws when configuration is not found', async () => {
vi.mocked(ConfigurationModel.findOne).mockResolvedValue(null as any)
await expect(engine.complianceReport('missing-config')).rejects.toThrow(
'Configuration missing-config not found'
)
})
it('returns an empty report when no devices match', async () => {
vi.mocked(ConfigurationModel.findOne).mockResolvedValue({
identifier: 'config-1',
name: 'Config 1',
criteria: {},
inventory: [],
desiredParameters: [],
policy: 'maintain',
} as any)
vi.mocked(DeviceModel.getCollection).mockReturnValue({
find: () => ({ toArray: async () => [] }),
} as any)
const report = await engine.complianceReport('config-1')
expect(report).toMatchObject({
configurationId: 'config-1',
configurationName: 'Config 1',
totalDevices: 0,
compliantCount: 0,
nonCompliantCount: 0,
devices: [],
})
})
it('counts compliant and non-compliant devices', async () => {
const config = {
identifier: 'config-1',
name: 'Config 1',
criteria: {},
inventory: [],
desiredParameters: [],
policy: 'maintain',
}
const devices = [
{ identifier: 'device-1', deviceId: { serialNumber: 'SN1' } },
{ identifier: 'device-2', deviceId: { serialNumber: 'SN2' } },
]
vi.mocked(ConfigurationModel.findOne).mockResolvedValue(config as any)
vi.mocked(DeviceModel.getCollection).mockReturnValue({
find: () => ({ toArray: async () => devices }),
} as any)
vi.spyOn(engine, 'evaluateSpecific')
.mockResolvedValueOnce({
configuration: config as any,
inventoryPaths: [],
parameterDiffs: [],
firmwareMigrationPath: [],
isAlreadyCompliant: true,
skipFirmware: false,
})
.mockResolvedValueOnce({
configuration: config as any,
inventoryPaths: [],
parameterDiffs: [
{
name: 'Device.ManagementServer.URL',
currentValue: 'http://old',
targetValue: 'http://acs',
type: 'string',
},
],
firmwareMigrationPath: [],
isAlreadyCompliant: false,
skipFirmware: false,
})
const report = await engine.complianceReport('config-1')
expect(report.totalDevices).toBe(2)
expect(report.compliantCount).toBe(1)
expect(report.nonCompliantCount).toBe(1)
expect(report.devices.map((device) => device.compliant)).toEqual([true, false])
})
it('skips null parameterCriteria names when matching devices', async () => {
const config = {
identifier: 'config-1',
name: 'Config 1',
criteria: { parameterCriteria: [{ name: null as any, value: 'x' }] },
inventory: [],
desiredParameters: [],
policy: 'maintain',
}
const device = { identifier: 'device-1', deviceId: { serialNumber: 'SN1' } }
vi.mocked(ConfigurationModel.findOne).mockResolvedValue(config as any)
vi.mocked(DeviceModel.getCollection).mockReturnValue({
find: () => ({ toArray: async () => [device] }),
} as any)
vi.spyOn(engine, 'evaluateSpecific').mockResolvedValue({
configuration: config as any,
inventoryPaths: [],
parameterDiffs: [],
firmwareMigrationPath: [],
isAlreadyCompliant: true,
skipFirmware: false,
})
const report = await engine.complianceReport('config-1')
expect(report.totalDevices).toBe(1)
expect(report.compliantCount).toBe(1)
expect(engine.evaluateSpecific).toHaveBeenCalledWith(device, config)
})
})
describe('evaluateSpecific', () => {
it('falls back to plain object parameters when getParameter is unavailable', async () => {
const device = {
identifier: 'device-1',
deviceId: { serialNumber: 'SN1' },
parameters: [
{
name: 'Device.ManagementServer.URL',
value: 'http://acs',
},
],
}
const config = {
identifier: 'config-1',
name: 'Config 1',
version: '1.0.0',
priority: 100,
policy: 'maintain',
enabled: true,
tags: [],
criteria: {},
inventory: [],
desiredParameters: [
{
name: 'Device.ManagementServer.URL',
value: 'http://acs',
type: 'string',
},
],
}
const result = await engine.evaluateSpecific(device as any, config as any)
expect(result.isAlreadyCompliant).toBe(true)
expect(result.parameterDiffs).toEqual([])
})
})
})