Files
adam-acs/tests/property/configuration_overlap.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

186 lines
6.2 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import * as fc from 'fast-check'
import ConfigurationService, { ValidationError } from '../../app/services/configuration_service.js'
import ConfigurationModel from '../../app/models/configuration.js'
import type { Configuration, ConfigurationCriteria } from '../../app/domain/configuration/configuration.js'
import { PolicyType } from '../../app/domain/configuration/configuration.js'
/**
* **Validates: Requirements 3.4**
*
* Property 7: Same-priority overlap rejected
* Two configurations with the same priority and overlapping criteria are always rejected with 409.
*/
vi.mock('../../app/models/configuration.js', () => ({
default: {
findAllEnabled: vi.fn(),
},
}))
// Arbitrary: generates a positive integer priority
const priorityArb = fc.integer({ min: 1, max: 10000 })
// Arbitrary: generates a non-empty string suitable for criteria fields (no glob chars for exact overlap)
const criteriaValueArb = fc
.array(
fc.constantFrom(
...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.split('')
),
{ minLength: 1, maxLength: 20 }
)
.map((chars) => chars.join(''))
// Arbitrary: generates a valid configuration identifier
const identifierArb = fc
.array(
fc.constantFrom(
...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split('')
),
{ minLength: 1, maxLength: 20 }
)
.map((chars) => {
const first = chars[0].replace(/[-_]/, 'A')
return first + chars.slice(1).join('')
})
// Arbitrary: generates overlapping criteria (same productClass and same manufacturer)
const overlappingCriteriaArb = fc.record({
manufacturer: criteriaValueArb,
productClass: criteriaValueArb,
})
// Build a minimal Configuration object for mocking findAllEnabled results
function buildExistingConfig(
identifier: string,
priority: number,
criteria: ConfigurationCriteria
): Configuration {
return {
identifier,
name: `Config ${identifier}`,
version: '1.0.0',
priority,
policy: PolicyType.MAINTAIN,
enabled: true,
tags: [],
criteria,
inventory: [],
desiredParameters: [],
createdAt: new Date(),
updatedAt: new Date(),
}
}
describe('Property 7: Same-priority overlap rejected', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('detectOverlap throws ValidationError with statusCode 409 when same priority and overlapping criteria', async () => {
await fc.assert(
fc.asyncProperty(
priorityArb,
overlappingCriteriaArb,
identifierArb,
async (priority, criteria, existingId) => {
vi.clearAllMocks()
// Mock findAllEnabled to return an existing config with the same priority and overlapping criteria
const existingConfig = buildExistingConfig(existingId, priority, criteria)
vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([existingConfig])
const service = new ConfigurationService()
// Call detectOverlap with the same criteria and same priority
try {
await service.detectOverlap(criteria, priority)
// If no error thrown, the property is violated
expect.fail('Expected ValidationError to be thrown')
} catch (error) {
expect(error).toBeInstanceOf(ValidationError)
expect((error as ValidationError).statusCode).toBe(409)
}
}
),
{ numRuns: 100 }
)
})
it('detectOverlap throws 409 with conflicting identifier listed in the error message', async () => {
await fc.assert(
fc.asyncProperty(
priorityArb,
overlappingCriteriaArb,
identifierArb,
async (priority, criteria, existingId) => {
vi.clearAllMocks()
const existingConfig = buildExistingConfig(existingId, priority, criteria)
vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([existingConfig])
const service = new ConfigurationService()
try {
await service.detectOverlap(criteria, priority)
expect.fail('Expected ValidationError to be thrown')
} catch (error) {
expect(error).toBeInstanceOf(ValidationError)
const validationError = error as ValidationError
expect(validationError.statusCode).toBe(409)
expect(validationError.message).toContain(existingId)
}
}
),
{ numRuns: 100 }
)
})
it('detectOverlap does NOT throw 409 when same criteria but different priority', async () => {
await fc.assert(
fc.asyncProperty(
priorityArb,
overlappingCriteriaArb,
identifierArb,
fc.integer({ min: 1, max: 5000 }),
async (priority, criteria, existingId, offset) => {
vi.clearAllMocks()
// Ensure different priority by adding an offset
const differentPriority = priority + offset + 1
const existingConfig = buildExistingConfig(existingId, differentPriority, criteria)
vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([existingConfig])
const service = new ConfigurationService()
// Should NOT throw — returns a warning instead
const result = await service.detectOverlap(criteria, priority)
expect(result).not.toBeNull()
expect(result!.type).toBe('overlap_different_priority')
}
),
{ numRuns: 100 }
)
})
it('detectOverlap excludes the configuration being updated (excludeId)', async () => {
await fc.assert(
fc.asyncProperty(
priorityArb,
overlappingCriteriaArb,
identifierArb,
async (priority, criteria, configId) => {
vi.clearAllMocks()
// The existing config has the same id as the one being updated
const existingConfig = buildExistingConfig(configId, priority, criteria)
vi.mocked(ConfigurationModel.findAllEnabled).mockResolvedValue([existingConfig])
const service = new ConfigurationService()
// Should NOT throw because we exclude the config being updated
const result = await service.detectOverlap(criteria, priority, configId)
expect(result).toBeNull()
}
),
{ numRuns: 100 }
)
})
})