- 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>
260 lines
8.4 KiB
TypeScript
260 lines
8.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import fc from 'fast-check'
|
|
import { MigrationRoute } from '../../app/domain/firmware/firmware_catalog.js'
|
|
|
|
/**
|
|
* Property 13: Firmware migration path resolution
|
|
* Validates: Requirements 7.3
|
|
*
|
|
* Verifies that:
|
|
* 1. If currentVersion === targetVersion, result is always empty array
|
|
* 2. If a direct route exists from current to target, path has exactly 1 step
|
|
* 3. Path steps are contiguous: step[i].toVersion === step[i+1].fromVersion
|
|
* 4. No version appears twice in the path (no cycles)
|
|
* 5. First step starts from currentVersion, last step ends at targetVersion (when path exists)
|
|
*/
|
|
|
|
vi.mock('../../app/models/migration_route.js', () => ({
|
|
default: {
|
|
findByProductClass: vi.fn(),
|
|
},
|
|
}))
|
|
|
|
vi.mock('../../app/models/firmware_entry.js', () => ({
|
|
default: {
|
|
findByProductClassAndVersion: vi.fn(),
|
|
},
|
|
}))
|
|
|
|
import MigrationRouteModel from '../../app/models/migration_route.js'
|
|
import FirmwareEntryModel from '../../app/models/firmware_entry.js'
|
|
import FirmwareCatalogService from '../../app/services/firmware_catalog_service.js'
|
|
|
|
describe('Property 13: Firmware migration path resolution', () => {
|
|
let service: FirmwareCatalogService
|
|
|
|
beforeEach(() => {
|
|
service = new FirmwareCatalogService()
|
|
vi.mocked(MigrationRouteModel.findByProductClass).mockReset()
|
|
vi.mocked(FirmwareEntryModel.findByProductClassAndVersion).mockReset()
|
|
vi.mocked(FirmwareEntryModel.findByProductClassAndVersion).mockResolvedValue(null)
|
|
})
|
|
|
|
/**
|
|
* Arbitrary for version strings (e.g., "1.0.0", "2.3.1")
|
|
*/
|
|
const versionArb = fc
|
|
.tuple(
|
|
fc.integer({ min: 0, max: 99 }),
|
|
fc.integer({ min: 0, max: 99 }),
|
|
fc.integer({ min: 0, max: 99 })
|
|
)
|
|
.map(([major, minor, patch]) => `${major}.${minor}.${patch}`)
|
|
|
|
/**
|
|
* Arbitrary for productClass strings
|
|
*/
|
|
const productClassArb = fc.stringMatching(/^[A-Za-z][A-Za-z0-9]{2,10}$/)
|
|
|
|
/**
|
|
* Helper to create a MigrationRoute object
|
|
*/
|
|
function makeRoute(
|
|
productClass: string,
|
|
sourceVersionPattern: string,
|
|
targetVersion: string,
|
|
priority: number = 10
|
|
): MigrationRoute {
|
|
return {
|
|
identifier: `${productClass}-${sourceVersionPattern}-to-${targetVersion}`,
|
|
productClass,
|
|
sourceVersionPattern,
|
|
targetVersion,
|
|
downloadUrl: `http://firmware.local/${productClass}/${targetVersion}.bin`,
|
|
priority,
|
|
createdAt: new Date(),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to set up mocks for a given set of routes
|
|
*/
|
|
function setupMocks(routes: MigrationRoute[]) {
|
|
vi.mocked(MigrationRouteModel.findByProductClass).mockImplementation(
|
|
async (pc: string) => routes.filter((r) => r.productClass === pc)
|
|
)
|
|
}
|
|
|
|
it('1. If currentVersion === targetVersion, result is always empty array', async () => {
|
|
await fc.assert(
|
|
fc.asyncProperty(productClassArb, versionArb, async (productClass, version) => {
|
|
setupMocks([])
|
|
const result = await service.resolveUpgradePath(productClass, version, version)
|
|
expect(result).toEqual([])
|
|
})
|
|
)
|
|
})
|
|
|
|
it('2. If a direct route exists from current to target, path has exactly 1 step', async () => {
|
|
await fc.assert(
|
|
fc.asyncProperty(
|
|
productClassArb,
|
|
versionArb,
|
|
versionArb,
|
|
fc.integer({ min: 1, max: 100 }),
|
|
async (productClass, currentVersion, targetVersion, priority) => {
|
|
fc.pre(currentVersion !== targetVersion)
|
|
|
|
const route = makeRoute(productClass, currentVersion, targetVersion, priority)
|
|
setupMocks([route])
|
|
|
|
const result = await service.resolveUpgradePath(
|
|
productClass,
|
|
currentVersion,
|
|
targetVersion
|
|
)
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].fromVersion).toBe(currentVersion)
|
|
expect(result[0].toVersion).toBe(targetVersion)
|
|
}
|
|
)
|
|
)
|
|
})
|
|
|
|
it('3. Path steps are contiguous: step[i].toVersion === step[i+1].fromVersion', async () => {
|
|
/**
|
|
* Generate a chain of distinct versions and create routes forming a linear path.
|
|
*/
|
|
const distinctVersionsArb = fc
|
|
.array(versionArb, { minLength: 3, maxLength: 6 })
|
|
.map((versions) => [...new Set(versions)])
|
|
.filter((unique) => unique.length >= 3)
|
|
|
|
await fc.assert(
|
|
fc.asyncProperty(productClassArb, distinctVersionsArb, async (productClass, versionList) => {
|
|
// Create a chain of routes: v0→v1, v1→v2, ..., v(n-2)→v(n-1)
|
|
const routes: MigrationRoute[] = []
|
|
for (let i = 0; i < versionList.length - 1; i++) {
|
|
routes.push(makeRoute(productClass, versionList[i], versionList[i + 1], 10))
|
|
}
|
|
|
|
setupMocks(routes)
|
|
|
|
const currentVersion = versionList[0]
|
|
const targetVersion = versionList[versionList.length - 1]
|
|
|
|
const result = await service.resolveUpgradePath(
|
|
productClass,
|
|
currentVersion,
|
|
targetVersion
|
|
)
|
|
|
|
// Path should be found and contiguous
|
|
expect(result.length).toBeGreaterThan(0)
|
|
for (let i = 0; i < result.length - 1; i++) {
|
|
expect(result[i].toVersion).toBe(result[i + 1].fromVersion)
|
|
}
|
|
})
|
|
)
|
|
})
|
|
|
|
it('4. No version appears twice in the path (no cycles)', async () => {
|
|
const distinctVersionsArb = fc
|
|
.array(versionArb, { minLength: 2, maxLength: 6 })
|
|
.map((versions) => [...new Set(versions)])
|
|
.filter((unique) => unique.length >= 2)
|
|
|
|
await fc.assert(
|
|
fc.asyncProperty(productClassArb, distinctVersionsArb, async (productClass, versionList) => {
|
|
// Create routes forming a chain
|
|
const routes: MigrationRoute[] = []
|
|
for (let i = 0; i < versionList.length - 1; i++) {
|
|
routes.push(makeRoute(productClass, versionList[i], versionList[i + 1], 10))
|
|
}
|
|
|
|
setupMocks(routes)
|
|
|
|
const currentVersion = versionList[0]
|
|
const targetVersion = versionList[versionList.length - 1]
|
|
|
|
const result = await service.resolveUpgradePath(
|
|
productClass,
|
|
currentVersion,
|
|
targetVersion
|
|
)
|
|
|
|
// Collect all versions in the path
|
|
if (result.length > 0) {
|
|
const allVersions: string[] = [result[0].fromVersion]
|
|
for (const step of result) {
|
|
allVersions.push(step.toVersion)
|
|
}
|
|
// No duplicates
|
|
const uniqueVersions = new Set(allVersions)
|
|
expect(uniqueVersions.size).toBe(allVersions.length)
|
|
}
|
|
})
|
|
)
|
|
})
|
|
|
|
it('5. First step starts from currentVersion, last step ends at targetVersion (when path exists)', async () => {
|
|
const distinctVersionsArb = fc
|
|
.array(versionArb, { minLength: 2, maxLength: 6 })
|
|
.map((versions) => [...new Set(versions)])
|
|
.filter((unique) => unique.length >= 2)
|
|
|
|
await fc.assert(
|
|
fc.asyncProperty(productClassArb, distinctVersionsArb, async (productClass, versionList) => {
|
|
// Create routes forming a chain
|
|
const routes: MigrationRoute[] = []
|
|
for (let i = 0; i < versionList.length - 1; i++) {
|
|
routes.push(makeRoute(productClass, versionList[i], versionList[i + 1], 10))
|
|
}
|
|
|
|
setupMocks(routes)
|
|
|
|
const currentVersion = versionList[0]
|
|
const targetVersion = versionList[versionList.length - 1]
|
|
|
|
const result = await service.resolveUpgradePath(
|
|
productClass,
|
|
currentVersion,
|
|
targetVersion
|
|
)
|
|
|
|
if (result.length > 0) {
|
|
expect(result[0].fromVersion).toBe(currentVersion)
|
|
expect(result[result.length - 1].toVersion).toBe(targetVersion)
|
|
}
|
|
})
|
|
)
|
|
})
|
|
|
|
it('cycle detection: routes forming a cycle return empty path', async () => {
|
|
await fc.assert(
|
|
fc.asyncProperty(
|
|
productClassArb,
|
|
versionArb,
|
|
versionArb,
|
|
versionArb,
|
|
async (productClass, v1, v2, target) => {
|
|
// Ensure all three are distinct
|
|
fc.pre(v1 !== v2 && v2 !== target && v1 !== target)
|
|
|
|
// Create a cycle: v1→v2, v2→v1 (cycle), no route to target
|
|
const routes: MigrationRoute[] = [
|
|
makeRoute(productClass, v1, v2, 10),
|
|
makeRoute(productClass, v2, v1, 10),
|
|
]
|
|
|
|
setupMocks(routes)
|
|
|
|
const result = await service.resolveUpgradePath(productClass, v1, target)
|
|
// Should return empty (either no path to target, or cycle detected)
|
|
expect(result).toEqual([])
|
|
}
|
|
)
|
|
)
|
|
})
|
|
})
|