Files
adam-acs/tests/unit/services/firmware_catalog_service.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

506 lines
16 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import FirmwareCatalogService from '../../../app/services/firmware_catalog_service.js'
import type { CreateFirmwareEntryDto, CreateMigrationRouteDto } from '../../../app/services/firmware_catalog_service.js'
// Mock the models
vi.mock('#models/firmware_entry', () => ({
default: {
createIfNotExists: vi.fn(),
findByProductClass: vi.fn(),
findByProductClassAndVersion: vi.fn(),
find: vi.fn(),
deleteByIdentifier: vi.fn(),
},
}))
vi.mock('#models/migration_route', () => ({
default: {
createIfNotExists: vi.fn(),
findByProductClass: vi.fn(),
deleteByIdentifier: vi.fn(),
},
}))
import FirmwareEntryModel from '#models/firmware_entry'
import MigrationRouteModel from '#models/migration_route'
describe('FirmwareCatalogService', () => {
let service: FirmwareCatalogService
beforeEach(() => {
service = new FirmwareCatalogService()
vi.clearAllMocks()
})
describe('createEntry', () => {
const validEntry: CreateFirmwareEntryDto = {
identifier: 'BM632w-fw-2.1.0',
productClass: 'BM632w',
version: '2.1.0',
downloadUrl: 'http://firmware.local/BM632w/2.1.0.bin',
checksum: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4',
checksumType: 'md5',
fileSize: 15728640,
}
it('creates a firmware entry with valid data', async () => {
const expected = { ...validEntry, createdAt: new Date() }
vi.mocked(FirmwareEntryModel.createIfNotExists).mockResolvedValue(expected as any)
const result = await service.createEntry(validEntry)
expect(FirmwareEntryModel.createIfNotExists).toHaveBeenCalledWith({
identifier: validEntry.identifier,
productClass: validEntry.productClass,
version: validEntry.version,
downloadUrl: validEntry.downloadUrl,
checksum: validEntry.checksum,
checksumType: validEntry.checksumType,
fileSize: validEntry.fileSize,
})
expect(result).toEqual(expected)
})
it('rejects invalid downloadUrl', async () => {
const invalidEntry = { ...validEntry, downloadUrl: 'not-a-url' }
await expect(service.createEntry(invalidEntry)).rejects.toThrow(
'downloadUrl must be a valid URL'
)
})
it('rejects non-positive fileSize', async () => {
const invalidEntry = { ...validEntry, fileSize: 0 }
await expect(service.createEntry(invalidEntry)).rejects.toThrow(
'fileSize must be a positive integer'
)
})
it('rejects negative fileSize', async () => {
const invalidEntry = { ...validEntry, fileSize: -1 }
await expect(service.createEntry(invalidEntry)).rejects.toThrow(
'fileSize must be a positive integer'
)
})
it('rejects non-integer fileSize', async () => {
const invalidEntry = { ...validEntry, fileSize: 1.5 }
await expect(service.createEntry(invalidEntry)).rejects.toThrow(
'fileSize must be a positive integer'
)
})
it('rejects invalid checksumType', async () => {
const invalidEntry = { ...validEntry, checksumType: 'sha512' as any }
await expect(service.createEntry(invalidEntry)).rejects.toThrow(
"checksumType must be 'md5' or 'sha256'"
)
})
it('rejects empty identifier', async () => {
const invalidEntry = { ...validEntry, identifier: '' }
await expect(service.createEntry(invalidEntry)).rejects.toThrow(
'identifier must be a non-empty string'
)
})
it('rejects whitespace-only identifier', async () => {
const invalidEntry = { ...validEntry, identifier: ' ' }
await expect(service.createEntry(invalidEntry)).rejects.toThrow(
'identifier must be a non-empty string'
)
})
})
describe('findEntries', () => {
it('returns entries filtered by productClass', async () => {
const entries = [
{ identifier: 'fw-1', productClass: 'BM632w', version: '1.0.0' },
]
vi.mocked(FirmwareEntryModel.findByProductClass).mockResolvedValue(entries as any)
const result = await service.findEntries('BM632w')
expect(FirmwareEntryModel.findByProductClass).toHaveBeenCalledWith('BM632w')
expect(result).toEqual(entries)
})
it('returns all entries when no productClass is specified', async () => {
const entries = [
{ identifier: 'fw-1', productClass: 'BM632w' },
{ identifier: 'fw-2', productClass: 'Livebox5' },
]
vi.mocked(FirmwareEntryModel.find).mockResolvedValue(entries as any)
const result = await service.findEntries()
expect(FirmwareEntryModel.find).toHaveBeenCalledWith({})
expect(result).toEqual(entries)
})
})
describe('deleteEntry', () => {
it('deletes an existing entry', async () => {
vi.mocked(FirmwareEntryModel.deleteByIdentifier).mockResolvedValue(true)
await expect(service.deleteEntry('fw-1')).resolves.toBeUndefined()
expect(FirmwareEntryModel.deleteByIdentifier).toHaveBeenCalledWith('fw-1')
})
it('throws when entry not found', async () => {
vi.mocked(FirmwareEntryModel.deleteByIdentifier).mockResolvedValue(false)
await expect(service.deleteEntry('nonexistent')).rejects.toThrow(
"Firmware entry 'nonexistent' not found"
)
})
})
describe('createMigrationRoute', () => {
const validRoute: CreateMigrationRouteDto = {
identifier: 'BM632w-1.x-to-2.0',
productClass: 'BM632w',
sourceVersionPattern: '1.*',
targetVersion: '2.0.0',
downloadUrl: 'http://firmware.local/BM632w/upgrade-1.x-to-2.0.bin',
priority: 10,
}
it('creates a migration route with valid data', async () => {
const expected = { ...validRoute, createdAt: new Date() }
vi.mocked(MigrationRouteModel.createIfNotExists).mockResolvedValue(expected as any)
const result = await service.createMigrationRoute(validRoute)
expect(MigrationRouteModel.createIfNotExists).toHaveBeenCalledWith({
identifier: validRoute.identifier,
productClass: validRoute.productClass,
sourceVersionPattern: validRoute.sourceVersionPattern,
targetVersion: validRoute.targetVersion,
downloadUrl: validRoute.downloadUrl,
priority: validRoute.priority,
})
expect(result).toEqual(expected)
})
it('rejects invalid downloadUrl', async () => {
const invalidRoute = { ...validRoute, downloadUrl: 'invalid' }
await expect(service.createMigrationRoute(invalidRoute)).rejects.toThrow(
'downloadUrl must be a valid URL'
)
})
it('rejects non-positive priority', async () => {
const invalidRoute = { ...validRoute, priority: 0 }
await expect(service.createMigrationRoute(invalidRoute)).rejects.toThrow(
'priority must be a positive integer'
)
})
it('rejects negative priority', async () => {
const invalidRoute = { ...validRoute, priority: -5 }
await expect(service.createMigrationRoute(invalidRoute)).rejects.toThrow(
'priority must be a positive integer'
)
})
it('rejects non-integer priority', async () => {
const invalidRoute = { ...validRoute, priority: 2.5 }
await expect(service.createMigrationRoute(invalidRoute)).rejects.toThrow(
'priority must be a positive integer'
)
})
it('rejects empty identifier', async () => {
const invalidRoute = { ...validRoute, identifier: '' }
await expect(service.createMigrationRoute(invalidRoute)).rejects.toThrow(
'identifier must be a non-empty string'
)
})
})
describe('findMigrationRoutes', () => {
it('returns routes sorted by priority descending', async () => {
const routes = [
{ identifier: 'route-1', priority: 20 },
{ identifier: 'route-2', priority: 10 },
]
vi.mocked(MigrationRouteModel.findByProductClass).mockResolvedValue(routes as any)
const result = await service.findMigrationRoutes('BM632w')
expect(MigrationRouteModel.findByProductClass).toHaveBeenCalledWith('BM632w', { priority: -1 })
expect(result).toEqual(routes)
})
})
describe('deleteMigrationRoute', () => {
it('deletes an existing route', async () => {
vi.mocked(MigrationRouteModel.deleteByIdentifier).mockResolvedValue(true)
await expect(service.deleteMigrationRoute('route-1')).resolves.toBeUndefined()
expect(MigrationRouteModel.deleteByIdentifier).toHaveBeenCalledWith('route-1')
})
it('throws when route not found', async () => {
vi.mocked(MigrationRouteModel.deleteByIdentifier).mockResolvedValue(false)
await expect(service.deleteMigrationRoute('nonexistent')).rejects.toThrow(
"Migration route 'nonexistent' not found"
)
})
})
describe('resolveUpgradePath', () => {
it('returns empty array when already at target version', async () => {
const result = await service.resolveUpgradePath('BM632w', '2.0.0', '2.0.0')
expect(result).toEqual([])
// Should not even query the database
expect(MigrationRouteModel.findByProductClass).not.toHaveBeenCalled()
})
it('resolves a direct single-step path', async () => {
vi.mocked(MigrationRouteModel.findByProductClass).mockResolvedValue([
{
identifier: 'route-1',
productClass: 'BM632w',
sourceVersionPattern: '1.0.0',
targetVersion: '2.0.0',
downloadUrl: 'http://firmware.local/BM632w/upgrade-1.0-to-2.0.bin',
priority: 10,
createdAt: new Date(),
},
])
vi.mocked(FirmwareEntryModel.findByProductClassAndVersion).mockResolvedValue({
identifier: 'BM632w-fw-2.0.0',
productClass: 'BM632w',
version: '2.0.0',
downloadUrl: 'http://firmware.local/BM632w/2.0.0.bin',
checksum: 'abc123',
checksumType: 'md5',
fileSize: 1024000,
createdAt: new Date(),
})
const result = await service.resolveUpgradePath('BM632w', '1.0.0', '2.0.0')
expect(result).toEqual([
{
fromVersion: '1.0.0',
toVersion: '2.0.0',
downloadUrl: 'http://firmware.local/BM632w/upgrade-1.0-to-2.0.bin',
fileSize: 1024000,
checksum: 'abc123',
},
])
})
it('resolves a multi-step path', async () => {
vi.mocked(MigrationRouteModel.findByProductClass).mockResolvedValue([
{
identifier: 'route-1',
productClass: 'BM632w',
sourceVersionPattern: '1.0.*',
targetVersion: '1.5.0',
downloadUrl: 'http://firmware.local/BM632w/upgrade-1.0-to-1.5.bin',
priority: 10,
createdAt: new Date(),
},
{
identifier: 'route-2',
productClass: 'BM632w',
sourceVersionPattern: '1.5.*',
targetVersion: '2.0.0',
downloadUrl: 'http://firmware.local/BM632w/upgrade-1.5-to-2.0.bin',
priority: 10,
createdAt: new Date(),
},
])
vi.mocked(FirmwareEntryModel.findByProductClassAndVersion).mockImplementation(
async (_pc: string, version: string) => {
if (version === '1.5.0') {
return {
identifier: 'BM632w-fw-1.5.0',
productClass: 'BM632w',
version: '1.5.0',
downloadUrl: 'http://firmware.local/BM632w/1.5.0.bin',
checksum: 'hash150',
checksumType: 'sha256' as const,
fileSize: 800000,
createdAt: new Date(),
}
}
if (version === '2.0.0') {
return {
identifier: 'BM632w-fw-2.0.0',
productClass: 'BM632w',
version: '2.0.0',
downloadUrl: 'http://firmware.local/BM632w/2.0.0.bin',
checksum: 'hash200',
checksumType: 'md5' as const,
fileSize: 1200000,
createdAt: new Date(),
}
}
return null
}
)
const result = await service.resolveUpgradePath('BM632w', '1.0.3', '2.0.0')
expect(result).toHaveLength(2)
expect(result[0]).toEqual({
fromVersion: '1.0.3',
toVersion: '1.5.0',
downloadUrl: 'http://firmware.local/BM632w/upgrade-1.0-to-1.5.bin',
fileSize: 800000,
checksum: 'hash150',
})
expect(result[1]).toEqual({
fromVersion: '1.5.0',
toVersion: '2.0.0',
downloadUrl: 'http://firmware.local/BM632w/upgrade-1.5-to-2.0.bin',
fileSize: 1200000,
checksum: 'hash200',
})
})
it('returns empty array when no path exists', async () => {
vi.mocked(MigrationRouteModel.findByProductClass).mockResolvedValue([
{
identifier: 'route-1',
productClass: 'BM632w',
sourceVersionPattern: '1.0.*',
targetVersion: '1.5.0',
downloadUrl: 'http://firmware.local/BM632w/upgrade-1.0-to-1.5.bin',
priority: 10,
createdAt: new Date(),
},
])
// Trying to reach 3.0.0 but no route from 1.5.0 to 3.0.0
const result = await service.resolveUpgradePath('BM632w', '1.0.0', '3.0.0')
expect(result).toEqual([])
})
it('returns empty array when cycle is detected', async () => {
vi.mocked(MigrationRouteModel.findByProductClass).mockResolvedValue([
{
identifier: 'route-1',
productClass: 'BM632w',
sourceVersionPattern: '1.0.0',
targetVersion: '1.5.0',
downloadUrl: 'http://firmware.local/BM632w/upgrade-1.0-to-1.5.bin',
priority: 10,
createdAt: new Date(),
},
{
identifier: 'route-2',
productClass: 'BM632w',
sourceVersionPattern: '1.5.0',
targetVersion: '1.0.0',
downloadUrl: 'http://firmware.local/BM632w/upgrade-1.5-to-1.0.bin',
priority: 10,
createdAt: new Date(),
},
])
const result = await service.resolveUpgradePath('BM632w', '1.0.0', '2.0.0')
expect(result).toEqual([])
})
it('selects the highest priority route when multiple apply', async () => {
vi.mocked(MigrationRouteModel.findByProductClass).mockResolvedValue([
{
identifier: 'route-low',
productClass: 'BM632w',
sourceVersionPattern: '1.0.*',
targetVersion: '1.5.0',
downloadUrl: 'http://firmware.local/BM632w/slow-path.bin',
priority: 5,
createdAt: new Date(),
},
{
identifier: 'route-high',
productClass: 'BM632w',
sourceVersionPattern: '1.*',
targetVersion: '2.0.0',
downloadUrl: 'http://firmware.local/BM632w/fast-path.bin',
priority: 20,
createdAt: new Date(),
},
])
vi.mocked(FirmwareEntryModel.findByProductClassAndVersion).mockResolvedValue({
identifier: 'BM632w-fw-2.0.0',
productClass: 'BM632w',
version: '2.0.0',
downloadUrl: 'http://firmware.local/BM632w/2.0.0.bin',
checksum: 'directhash',
checksumType: 'sha256',
fileSize: 2000000,
createdAt: new Date(),
})
const result = await service.resolveUpgradePath('BM632w', '1.0.5', '2.0.0')
expect(result).toHaveLength(1)
expect(result[0]).toEqual({
fromVersion: '1.0.5',
toVersion: '2.0.0',
downloadUrl: 'http://firmware.local/BM632w/fast-path.bin',
fileSize: 2000000,
checksum: 'directhash',
})
})
it('keeps fileSize 0 and checksum empty when no firmware entry exists', async () => {
vi.mocked(MigrationRouteModel.findByProductClass).mockResolvedValue([
{
identifier: 'route-1',
productClass: 'BM632w',
sourceVersionPattern: '1.0.0',
targetVersion: '2.0.0',
downloadUrl: 'http://firmware.local/BM632w/upgrade.bin',
priority: 10,
createdAt: new Date(),
},
])
vi.mocked(FirmwareEntryModel.findByProductClassAndVersion).mockResolvedValue(null)
const result = await service.resolveUpgradePath('BM632w', '1.0.0', '2.0.0')
expect(result).toEqual([
{
fromVersion: '1.0.0',
toVersion: '2.0.0',
downloadUrl: 'http://firmware.local/BM632w/upgrade.bin',
fileSize: 0,
checksum: '',
},
])
})
it('returns empty array when no routes exist for the productClass', async () => {
vi.mocked(MigrationRouteModel.findByProductClass).mockResolvedValue([])
const result = await service.resolveUpgradePath('BM632w', '1.0.0', '2.0.0')
expect(result).toEqual([])
})
})
})