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) }) })