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.
286 lines
8.9 KiB
TypeScript
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)
|
|
})
|
|
})
|