- 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>
176 lines
5.3 KiB
TypeScript
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([])
|
|
})
|
|
})
|
|
})
|