Files
adam-acs/tests/unit/services/references/reference_engine_full.spec.ts
T
Kevin L'Hereec 76e75132d5
Build & Deploy / build (push) Successful in 34s
Build & Deploy / docker (push) Successful in 1m19s
Build & Deploy / deploy (push) Successful in 37s
test: raise business-logic unit test coverage across core services
Adds targeted unit tests to cover previously untested branches/functions
in the business-critical (non-protocol) services:

- device_service.ts: 35.8% -> 98.76% (CRUD, executeCommand, refreshDevice,
  calculatePagination, IPv4/IPv6 Connection Request fallback chain)
- domain/device/device.ts: 76.7% -> 96.59% (GPN circuit breaker state
  machine, session lifecycle, canExecuteCommands, detectDataModelFromParams)
- services/references/reference_engine.ts: 78.66% -> 100% (provider
  loading edge cases, rejection handling, singleton lifecycle)
- services/configuration_engine.ts: 70.96% -> 98.7% (matchesCriteria
  branches, shouldApplyOnce policy variants, dryRun, complianceReport)
- services/cwmp/state_machine_manager.ts: 70.9% -> 87.27% (getHashIdForRemoteIp,
  ensureSessionForRemote IP fallback, task callback error handling)
- services/cwmp/main_state_machine.ts: 57.79% -> 94.29% (ExecutingCommandsState
  full command-type dispatch, MAX_STATE_TRANSITIONS guard, error->
  PartialOperationalState transitions)

Also fixes:
- reference_engine_full.spec.ts previously depended on the real
  config/references.json (production config, wassup provider legitimately
  disabled there) instead of a test fixture — now mocks config_loader
  for deterministic, isolated assertions
- main_state_machine.spec.ts CommandModel.getCollection mock returned a
  new object per call instead of a shared instance, breaking
  setupCommandMock() for the new command-dispatch tests

Overall project coverage (excluding protocol/): 72.76% -> 82.34% statements.
Full suite: 1005/1005 tests passing.
2026-07-22 11:45:50 +02:00

286 lines
8.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import type { DeviceRecord } from '../../../../app/domain/device/device_record.js'
import type { InformContext, ReferencesConfig } from '../../../../app/services/references/types.js'
import { DeviceId } from '../../../../app/domain/device/device_id.js'
// This suite exercises ReferenceEngine's provider-loading and dispatch logic in
// isolation from config/references.json (which reflects production settings and
// may legitimately have providers disabled, e.g. wassup when its mock is
// unreachable — see commit 1fc9715). A mutable test fixture (reset per test)
// keeps this suite deterministic regardless of that file's contents, while
// still allowing individual tests to exercise other provider types/configs.
const defaultTestConfig: ReferencesConfig = {
providers: [
{
name: 'wassup',
type: 'wassup',
enabled: true,
url: 'http://wassup-mock:9999',
timeout: 3000,
trigger: ['BOOT', 'BOOTSTRAP'],
resolveBy: 'ip',
options: {
service: 'KRMSCO',
requestedProperties: ['sai', 'spr', 'clo', 'puid', 'utz'],
},
},
],
}
let testConfig: ReferencesConfig = defaultTestConfig
vi.mock('../../../../app/services/references/config_loader.js', () => ({
loadReferencesConfig: vi.fn(() => testConfig),
reloadReferencesConfig: vi.fn(() => testConfig),
}))
// Import AFTER mocking config_loader so ReferenceEngine picks up the test fixture.
const { ReferenceEngine, getReferenceEngine, resetReferenceEngine } =
await import('../../../../app/services/references/reference_engine.js')
beforeEach(() => {
testConfig = defaultTestConfig
resetReferenceEngine()
})
function makeDevice(): DeviceRecord {
return {
identifier: 'test-001',
deviceId: new DeviceId('Sagemcom', 'F8B7FF', 'LB6', 'SN001'),
properties: [],
parameters: [],
}
}
describe('ReferenceEngine (full)', () => {
it('loads providers from config', () => {
const engine = new ReferenceEngine()
const names = engine.getProviderNames()
expect(names).toContain('wassup')
})
it('getProviderConfigs returns all configs', () => {
const engine = new ReferenceEngine()
const configs = engine.getProviderConfigs()
expect(configs.length).toBeGreaterThan(0)
expect(configs[0].name).toBe('wassup')
})
it('resolveAll returns empty when no events match triggers', async () => {
const engine = new ReferenceEngine()
const result = await engine.resolveAll(makeDevice(), {
events: ['6 CONNECTION REQUEST'],
remoteIp: '10.0.0.1',
headers: {},
})
// CONNECTION_REQUEST is not in the wassup trigger config (only BOOT, BOOTSTRAP)
expect(result).toEqual([])
})
it('resolveAll triggers on BOOT event', async () => {
const engine = new ReferenceEngine()
// This will attempt to call the real wassup-mock (which may not be running)
// We just verify it doesn't crash and returns a result (error or ok)
const result = await engine.resolveAll(makeDevice(), {
events: ['1 BOOT'],
remoteIp: '10.0.0.1',
headers: {},
})
// Either resolved or error (depending on whether mock is up)
expect(result.length).toBeLessThanOrEqual(1)
if (result.length > 0) {
expect(['ok', 'unknown', 'error']).toContain(result[0].status)
expect(result[0].provider).toBe('wassup')
}
})
it('resolveAll triggers on numeric event "0" (BOOTSTRAP)', async () => {
const engine = new ReferenceEngine()
const result = await engine.resolveAll(makeDevice(), {
events: ['0'],
remoteIp: '1.2.3.4',
headers: {},
})
expect(result.length).toBeLessThanOrEqual(1)
})
it('resolveOne returns null for unknown provider', async () => {
const engine = new ReferenceEngine()
const result = await engine.resolveOne('nonexistent', makeDevice(), {
events: [],
remoteIp: '',
headers: {},
})
expect(result).toBeNull()
})
it('resolveOne calls the named provider', async () => {
const engine = new ReferenceEngine()
const result = await engine.resolveOne('wassup', makeDevice(), {
events: [],
remoteIp: '10.0.0.1',
headers: {},
})
// Will get an error or unknown since mock may not be running, but provider is called
if (result) {
expect(result.provider).toBe('wassup')
expect(result.resolvedBy).toContain('ip:10.0.0.1')
}
})
})
describe('ReferenceEngine — provider loading edge cases', () => {
it('skips a disabled provider entirely (never registered)', () => {
testConfig = {
providers: [
{
name: 'wassup',
type: 'wassup',
enabled: false,
trigger: ['BOOT'],
resolveBy: 'ip',
},
],
}
const engine = new ReferenceEngine()
expect(engine.getProviderNames()).toEqual([])
// Config is still tracked even if the provider itself isn't instantiated
expect(engine.getProviderConfigs()).toHaveLength(1)
})
it('does not register a provider for unimplemented type "http-json"', () => {
testConfig = {
providers: [
{ name: 'crm', type: 'http-json', enabled: true, trigger: ['BOOT'], resolveBy: 'ip' },
],
}
const engine = new ReferenceEngine()
expect(engine.getProviderNames()).toEqual([])
})
it('does not register a provider for unimplemented type "http-properties"', () => {
testConfig = {
providers: [
{ name: 'noc', type: 'http-properties', enabled: true, trigger: ['BOOT'], resolveBy: 'ip' },
],
}
const engine = new ReferenceEngine()
expect(engine.getProviderNames()).toEqual([])
})
it('does not register a provider for unimplemented type "static"', () => {
testConfig = {
providers: [
{ name: 'fixed', type: 'static', enabled: true, trigger: ['BOOT'], resolveBy: 'ip' },
],
}
const engine = new ReferenceEngine()
expect(engine.getProviderNames()).toEqual([])
})
it('does not register a provider for an unknown/unrecognized type', () => {
testConfig = {
providers: [
{
name: 'weird',
type: 'unknown-type' as any,
enabled: true,
trigger: ['BOOT'],
resolveBy: 'ip',
},
],
}
const engine = new ReferenceEngine()
expect(engine.getProviderNames()).toEqual([])
})
it('handles an empty provider list gracefully', () => {
testConfig = { providers: [] }
const engine = new ReferenceEngine()
expect(engine.getProviderNames()).toEqual([])
expect(engine.getProviderConfigs()).toEqual([])
})
})
describe('ReferenceEngine.getApplicableProviders — orphan provider (config removed)', () => {
it('excludes a provider whose config entry is no longer present', async () => {
const engine = new ReferenceEngine()
// Simulate a stale in-memory provider whose config was removed at runtime
;(engine as any).configs = []
const result = await engine.resolveAll(makeDevice(), {
events: ['1 BOOT'],
remoteIp: '10.0.0.1',
headers: {},
})
expect(result).toEqual([])
})
})
describe('ReferenceEngine.resolveAll — provider rejection handling', () => {
it('logs and skips a provider whose resolve() promise rejects', async () => {
const engine = new ReferenceEngine()
const provider = (engine as any).providers[0]
vi.spyOn(provider, 'resolve').mockRejectedValue(new Error('boom'))
const result = await engine.resolveAll(makeDevice(), {
events: ['1 BOOT'],
remoteIp: '10.0.0.1',
headers: {},
})
expect(result).toEqual([])
})
it('excludes a fulfilled-but-null resolution from the results', async () => {
const engine = new ReferenceEngine()
const provider = (engine as any).providers[0]
vi.spyOn(provider, 'resolve').mockResolvedValue(null)
const result = await engine.resolveAll(makeDevice(), {
events: ['1 BOOT'],
remoteIp: '10.0.0.1',
headers: {},
})
expect(result).toEqual([])
})
it('includes a fulfilled resolution in the results', async () => {
const engine = new ReferenceEngine()
const provider = (engine as any).providers[0]
const resolved = {
provider: 'wassup',
resolvedAt: new Date(),
resolvedBy: 'ip:10.0.0.1',
status: 'ok' as const,
properties: { sai: 'value' },
}
vi.spyOn(provider, 'resolve').mockResolvedValue(resolved)
const result = await engine.resolveAll(makeDevice(), {
events: ['1 BOOT'],
remoteIp: '10.0.0.1',
headers: {},
})
expect(result).toEqual([resolved])
})
})
describe('getReferenceEngine / resetReferenceEngine — singleton lifecycle', () => {
it('returns the same instance on repeated calls', () => {
const a = getReferenceEngine()
const b = getReferenceEngine()
expect(a).toBe(b)
})
it('resetReferenceEngine forces a new instance to be created next call', () => {
const a = getReferenceEngine()
resetReferenceEngine()
const b = getReferenceEngine()
expect(a).not.toBe(b)
})
})