diff --git a/src/lib/influxdb/__tests__/error-metrics.test.js b/src/lib/influxdb/__tests__/error-metrics.test.js new file mode 100644 index 0000000..2996f34 --- /dev/null +++ b/src/lib/influxdb/__tests__/error-metrics.test.js @@ -0,0 +1,60 @@ +import { jest, describe, test, expect } from '@jest/globals'; +import { postErrorMetricsToInfluxdb } from '../error-metrics.js'; + +describe('error-metrics', () => { + describe('postErrorMetricsToInfluxdb', () => { + test('should resolve successfully with valid error stats', async () => { + const errorStats = { + HEALTH_API: { + total: 5, + servers: { + sense1: 3, + sense2: 2, + }, + }, + INFLUXDB_V3_WRITE: { + total: 2, + servers: { + _no_server_context: 2, + }, + }, + }; + + await expect(postErrorMetricsToInfluxdb(errorStats)).resolves.toBeUndefined(); + }); + + test('should resolve successfully with empty error stats', async () => { + const errorStats = {}; + + await expect(postErrorMetricsToInfluxdb(errorStats)).resolves.toBeUndefined(); + }); + + test('should resolve successfully with null input', async () => { + await expect(postErrorMetricsToInfluxdb(null)).resolves.toBeUndefined(); + }); + + test('should resolve successfully with undefined input', async () => { + await expect(postErrorMetricsToInfluxdb(undefined)).resolves.toBeUndefined(); + }); + + test('should resolve successfully with complex error stats', async () => { + const errorStats = { + API_TYPE_1: { + total: 100, + servers: { + server1: 25, + server2: 25, + server3: 25, + server4: 25, + }, + }, + API_TYPE_2: { + total: 0, + servers: {}, + }, + }; + + await expect(postErrorMetricsToInfluxdb(errorStats)).resolves.toBeUndefined(); + }); + }); +}); diff --git a/src/lib/influxdb/__tests__/factory.test.js b/src/lib/influxdb/__tests__/factory.test.js index eb04376..8b061bb 100644 --- a/src/lib/influxdb/__tests__/factory.test.js +++ b/src/lib/influxdb/__tests__/factory.test.js @@ -47,6 +47,81 @@ jest.unstable_mockModule('../v1/queue-metrics.js', () => ({ storeLogEventQueueMetricsV1: jest.fn(), })); +jest.unstable_mockModule('../v1/health-metrics.js', () => ({ + storeHealthMetricsV1: jest.fn(), +})); + +jest.unstable_mockModule('../v2/health-metrics.js', () => ({ + storeHealthMetricsV2: jest.fn(), +})); + +jest.unstable_mockModule('../v3/health-metrics.js', () => ({ + postHealthMetricsToInfluxdbV3: jest.fn(), +})); + +jest.unstable_mockModule('../v1/sessions.js', () => ({ + storeSessionsV1: jest.fn(), +})); + +jest.unstable_mockModule('../v2/sessions.js', () => ({ + storeSessionsV2: jest.fn(), +})); + +jest.unstable_mockModule('../v3/sessions.js', () => ({ + postProxySessionsToInfluxdbV3: jest.fn(), +})); + +jest.unstable_mockModule('../v1/butler-memory.js', () => ({ + storeButlerMemoryV1: jest.fn(), +})); + +jest.unstable_mockModule('../v2/butler-memory.js', () => ({ + storeButlerMemoryV2: jest.fn(), +})); + +jest.unstable_mockModule('../v3/butler-memory.js', () => ({ + postButlerSOSMemoryUsageToInfluxdbV3: jest.fn(), +})); + +jest.unstable_mockModule('../v1/user-events.js', () => ({ + storeUserEventV1: jest.fn(), +})); + +jest.unstable_mockModule('../v2/user-events.js', () => ({ + storeUserEventV2: jest.fn(), +})); + +jest.unstable_mockModule('../v3/user-events.js', () => ({ + postUserEventToInfluxdbV3: jest.fn(), +})); + +jest.unstable_mockModule('../v1/log-events.js', () => ({ + storeLogEventV1: jest.fn(), +})); + +jest.unstable_mockModule('../v2/log-events.js', () => ({ + storeLogEventV2: jest.fn(), +})); + +jest.unstable_mockModule('../v3/log-events.js', () => ({ + postLogEventToInfluxdbV3: jest.fn(), +})); + +jest.unstable_mockModule('../v1/event-counts.js', () => ({ + storeEventCountV1: jest.fn(), + storeRejectedEventCountV1: jest.fn(), +})); + +jest.unstable_mockModule('../v2/event-counts.js', () => ({ + storeEventCountV2: jest.fn(), + storeRejectedEventCountV2: jest.fn(), +})); + +jest.unstable_mockModule('../v3/event-counts.js', () => ({ + storeEventCountInfluxDBV3: jest.fn(), + storeRejectedEventCountInfluxDBV3: jest.fn(), +})); + describe('InfluxDB Factory', () => { let factory; let globals; @@ -54,6 +129,12 @@ describe('InfluxDB Factory', () => { let v3Impl; let v2Impl; let v1Impl; + let v3Health, v2Health, v1Health; + let v3Sessions, v2Sessions, v1Sessions; + let v3Memory, v2Memory, v1Memory; + let v3User, v2User, v1User; + let v3Log, v2Log, v1Log; + let v3EventCounts, v2EventCounts, v1EventCounts; beforeEach(async () => { jest.clearAllMocks(); @@ -63,6 +144,31 @@ describe('InfluxDB Factory', () => { v3Impl = await import('../v3/queue-metrics.js'); v2Impl = await import('../v2/queue-metrics.js'); v1Impl = await import('../v1/queue-metrics.js'); + + v3Health = await import('../v3/health-metrics.js'); + v2Health = await import('../v2/health-metrics.js'); + v1Health = await import('../v1/health-metrics.js'); + + v3Sessions = await import('../v3/sessions.js'); + v2Sessions = await import('../v2/sessions.js'); + v1Sessions = await import('../v1/sessions.js'); + + v3Memory = await import('../v3/butler-memory.js'); + v2Memory = await import('../v2/butler-memory.js'); + v1Memory = await import('../v1/butler-memory.js'); + + v3User = await import('../v3/user-events.js'); + v2User = await import('../v2/user-events.js'); + v1User = await import('../v1/user-events.js'); + + v3Log = await import('../v3/log-events.js'); + v2Log = await import('../v2/log-events.js'); + v1Log = await import('../v1/log-events.js'); + + v3EventCounts = await import('../v3/event-counts.js'); + v2EventCounts = await import('../v2/event-counts.js'); + v1EventCounts = await import('../v1/event-counts.js'); + factory = await import('../factory.js'); // Setup default mocks @@ -72,6 +178,33 @@ describe('InfluxDB Factory', () => { v2Impl.storeLogEventQueueMetricsV2.mockResolvedValue(); v1Impl.storeUserEventQueueMetricsV1.mockResolvedValue(); v1Impl.storeLogEventQueueMetricsV1.mockResolvedValue(); + + v3Health.postHealthMetricsToInfluxdbV3.mockResolvedValue(); + v2Health.storeHealthMetricsV2.mockResolvedValue(); + v1Health.storeHealthMetricsV1.mockResolvedValue(); + + v3Sessions.postProxySessionsToInfluxdbV3.mockResolvedValue(); + v2Sessions.storeSessionsV2.mockResolvedValue(); + v1Sessions.storeSessionsV1.mockResolvedValue(); + + v3Memory.postButlerSOSMemoryUsageToInfluxdbV3.mockResolvedValue(); + v2Memory.storeButlerMemoryV2.mockResolvedValue(); + v1Memory.storeButlerMemoryV1.mockResolvedValue(); + + v3User.postUserEventToInfluxdbV3.mockResolvedValue(); + v2User.storeUserEventV2.mockResolvedValue(); + v1User.storeUserEventV1.mockResolvedValue(); + + v3Log.postLogEventToInfluxdbV3.mockResolvedValue(); + v2Log.storeLogEventV2.mockResolvedValue(); + v1Log.storeLogEventV1.mockResolvedValue(); + + v3EventCounts.storeEventCountInfluxDBV3.mockResolvedValue(); + v3EventCounts.storeRejectedEventCountInfluxDBV3.mockResolvedValue(); + v2EventCounts.storeEventCountV2.mockResolvedValue(); + v2EventCounts.storeRejectedEventCountV2.mockResolvedValue(); + v1EventCounts.storeEventCountV1.mockResolvedValue(); + v1EventCounts.storeRejectedEventCountV1.mockResolvedValue(); }); describe('postUserEventQueueMetricsToInfluxdb', () => { @@ -157,4 +290,279 @@ describe('InfluxDB Factory', () => { ); }); }); + + describe('postHealthMetricsToInfluxdb', () => { + const serverName = 'test-server'; + const host = 'test-host'; + const body = { version: '1.0' }; + const serverTags = [{ name: 'env', value: 'prod' }]; + + test('should route to v3 implementation when version is 3', async () => { + utils.getInfluxDbVersion.mockReturnValue(3); + + await factory.postHealthMetricsToInfluxdb(serverName, host, body, serverTags); + + expect(v3Health.postHealthMetricsToInfluxdbV3).toHaveBeenCalledWith( + serverName, + host, + body, + serverTags + ); + expect(v2Health.storeHealthMetricsV2).not.toHaveBeenCalled(); + expect(v1Health.storeHealthMetricsV1).not.toHaveBeenCalled(); + }); + + test('should route to v2 implementation when version is 2', async () => { + utils.getInfluxDbVersion.mockReturnValue(2); + + await factory.postHealthMetricsToInfluxdb(serverName, host, body, serverTags); + + expect(v2Health.storeHealthMetricsV2).toHaveBeenCalledWith( + serverName, + host, + body, + serverTags + ); + expect(v3Health.postHealthMetricsToInfluxdbV3).not.toHaveBeenCalled(); + expect(v1Health.storeHealthMetricsV1).not.toHaveBeenCalled(); + }); + + test('should route to v1 implementation when version is 1', async () => { + utils.getInfluxDbVersion.mockReturnValue(1); + + await factory.postHealthMetricsToInfluxdb(serverName, host, body, serverTags); + + expect(v1Health.storeHealthMetricsV1).toHaveBeenCalledWith(serverTags, body); + expect(v3Health.postHealthMetricsToInfluxdbV3).not.toHaveBeenCalled(); + expect(v2Health.storeHealthMetricsV2).not.toHaveBeenCalled(); + }); + + test('should throw error for unsupported version', async () => { + utils.getInfluxDbVersion.mockReturnValue(4); + + await expect( + factory.postHealthMetricsToInfluxdb(serverName, host, body, serverTags) + ).rejects.toThrow('InfluxDB v4 not supported'); + }); + }); + + describe('postProxySessionsToInfluxdb', () => { + const userSessions = { serverName: 'test', host: 'test-host' }; + + test('should route to v3 implementation when version is 3', async () => { + utils.getInfluxDbVersion.mockReturnValue(3); + + await factory.postProxySessionsToInfluxdb(userSessions); + + expect(v3Sessions.postProxySessionsToInfluxdbV3).toHaveBeenCalledWith(userSessions); + expect(v2Sessions.storeSessionsV2).not.toHaveBeenCalled(); + expect(v1Sessions.storeSessionsV1).not.toHaveBeenCalled(); + }); + + test('should route to v2 implementation when version is 2', async () => { + utils.getInfluxDbVersion.mockReturnValue(2); + + await factory.postProxySessionsToInfluxdb(userSessions); + + expect(v2Sessions.storeSessionsV2).toHaveBeenCalledWith(userSessions); + expect(v3Sessions.postProxySessionsToInfluxdbV3).not.toHaveBeenCalled(); + expect(v1Sessions.storeSessionsV1).not.toHaveBeenCalled(); + }); + + test('should route to v1 implementation when version is 1', async () => { + utils.getInfluxDbVersion.mockReturnValue(1); + + await factory.postProxySessionsToInfluxdb(userSessions); + + expect(v1Sessions.storeSessionsV1).toHaveBeenCalledWith(userSessions); + expect(v3Sessions.postProxySessionsToInfluxdbV3).not.toHaveBeenCalled(); + expect(v2Sessions.storeSessionsV2).not.toHaveBeenCalled(); + }); + + test('should throw error for unsupported version', async () => { + utils.getInfluxDbVersion.mockReturnValue(10); + + await expect(factory.postProxySessionsToInfluxdb(userSessions)).rejects.toThrow( + 'InfluxDB v10 not supported' + ); + }); + }); + + describe('postButlerSOSMemoryUsageToInfluxdb', () => { + const memory = { heap_used: 100, heap_total: 200 }; + + test('should route to v3 implementation when version is 3', async () => { + utils.getInfluxDbVersion.mockReturnValue(3); + + await factory.postButlerSOSMemoryUsageToInfluxdb(memory); + + expect(v3Memory.postButlerSOSMemoryUsageToInfluxdbV3).toHaveBeenCalledWith(memory); + expect(v2Memory.storeButlerMemoryV2).not.toHaveBeenCalled(); + expect(v1Memory.storeButlerMemoryV1).not.toHaveBeenCalled(); + }); + + test('should route to v2 implementation when version is 2', async () => { + utils.getInfluxDbVersion.mockReturnValue(2); + + await factory.postButlerSOSMemoryUsageToInfluxdb(memory); + + expect(v2Memory.storeButlerMemoryV2).toHaveBeenCalledWith(memory); + }); + + test('should route to v1 implementation when version is 1', async () => { + utils.getInfluxDbVersion.mockReturnValue(1); + + await factory.postButlerSOSMemoryUsageToInfluxdb(memory); + + expect(v1Memory.storeButlerMemoryV1).toHaveBeenCalledWith(memory); + }); + + test('should throw error for unsupported version', async () => { + utils.getInfluxDbVersion.mockReturnValue(7); + + await expect(factory.postButlerSOSMemoryUsageToInfluxdb(memory)).rejects.toThrow( + 'InfluxDB v7 not supported' + ); + }); + }); + + describe('postUserEventToInfluxdb', () => { + const msg = { host: 'test-host', command: 'OpenApp' }; + + test('should route to v3 implementation when version is 3', async () => { + utils.getInfluxDbVersion.mockReturnValue(3); + + await factory.postUserEventToInfluxdb(msg); + + expect(v3User.postUserEventToInfluxdbV3).toHaveBeenCalledWith(msg); + }); + + test('should route to v2 implementation when version is 2', async () => { + utils.getInfluxDbVersion.mockReturnValue(2); + + await factory.postUserEventToInfluxdb(msg); + + expect(v2User.storeUserEventV2).toHaveBeenCalledWith(msg); + }); + + test('should route to v1 implementation when version is 1', async () => { + utils.getInfluxDbVersion.mockReturnValue(1); + + await factory.postUserEventToInfluxdb(msg); + + expect(v1User.storeUserEventV1).toHaveBeenCalledWith(msg); + }); + + test('should throw error for unsupported version', async () => { + utils.getInfluxDbVersion.mockReturnValue(0); + + await expect(factory.postUserEventToInfluxdb(msg)).rejects.toThrow( + 'InfluxDB v0 not supported' + ); + }); + }); + + describe('postLogEventToInfluxdb', () => { + const msg = { host: 'test-host', source: 'qseow-engine' }; + + test('should route to v3 implementation when version is 3', async () => { + utils.getInfluxDbVersion.mockReturnValue(3); + + await factory.postLogEventToInfluxdb(msg); + + expect(v3Log.postLogEventToInfluxdbV3).toHaveBeenCalledWith(msg); + }); + + test('should route to v2 implementation when version is 2', async () => { + utils.getInfluxDbVersion.mockReturnValue(2); + + await factory.postLogEventToInfluxdb(msg); + + expect(v2Log.storeLogEventV2).toHaveBeenCalledWith(msg); + }); + + test('should route to v1 implementation when version is 1', async () => { + utils.getInfluxDbVersion.mockReturnValue(1); + + await factory.postLogEventToInfluxdb(msg); + + expect(v1Log.storeLogEventV1).toHaveBeenCalledWith(msg); + }); + + test('should throw error for unsupported version', async () => { + utils.getInfluxDbVersion.mockReturnValue(-1); + + await expect(factory.postLogEventToInfluxdb(msg)).rejects.toThrow( + 'InfluxDB v-1 not supported' + ); + }); + }); + + describe('storeEventCountInfluxDB', () => { + test('should route to v3 implementation when version is 3', async () => { + utils.getInfluxDbVersion.mockReturnValue(3); + + await factory.storeEventCountInfluxDB(); + + expect(v3EventCounts.storeEventCountInfluxDBV3).toHaveBeenCalled(); + }); + + test('should route to v2 implementation when version is 2', async () => { + utils.getInfluxDbVersion.mockReturnValue(2); + + await factory.storeEventCountInfluxDB(); + + expect(v2EventCounts.storeEventCountV2).toHaveBeenCalled(); + }); + + test('should route to v1 implementation when version is 1', async () => { + utils.getInfluxDbVersion.mockReturnValue(1); + + await factory.storeEventCountInfluxDB(); + + expect(v1EventCounts.storeEventCountV1).toHaveBeenCalled(); + }); + + test('should throw error for unsupported version', async () => { + utils.getInfluxDbVersion.mockReturnValue(100); + + await expect(factory.storeEventCountInfluxDB()).rejects.toThrow( + 'InfluxDB v100 not supported' + ); + }); + }); + + describe('storeRejectedEventCountInfluxDB', () => { + test('should route to v3 implementation when version is 3', async () => { + utils.getInfluxDbVersion.mockReturnValue(3); + + await factory.storeRejectedEventCountInfluxDB(); + + expect(v3EventCounts.storeRejectedEventCountInfluxDBV3).toHaveBeenCalled(); + }); + + test('should route to v2 implementation when version is 2', async () => { + utils.getInfluxDbVersion.mockReturnValue(2); + + await factory.storeRejectedEventCountInfluxDB(); + + expect(v2EventCounts.storeRejectedEventCountV2).toHaveBeenCalled(); + }); + + test('should route to v1 implementation when version is 1', async () => { + utils.getInfluxDbVersion.mockReturnValue(1); + + await factory.storeRejectedEventCountInfluxDB(); + + expect(v1EventCounts.storeRejectedEventCountV1).toHaveBeenCalled(); + }); + + test('should throw error for unsupported version', async () => { + utils.getInfluxDbVersion.mockReturnValue(99); + + await expect(factory.storeRejectedEventCountInfluxDB()).rejects.toThrow( + 'InfluxDB v99 not supported' + ); + }); + }); }); diff --git a/src/lib/influxdb/__tests__/index.test.js b/src/lib/influxdb/__tests__/index.test.js new file mode 100644 index 0000000..45594ce --- /dev/null +++ b/src/lib/influxdb/__tests__/index.test.js @@ -0,0 +1,301 @@ +import { jest, describe, test, expect, beforeEach, afterEach } from '@jest/globals'; + +// Mock globals +const mockGlobals = { + logger: { + info: jest.fn(), + verbose: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + }, + config: { + get: jest.fn(), + has: jest.fn(), + }, +}; + +jest.unstable_mockModule('../../../globals.js', () => ({ + default: mockGlobals, +})); + +// Mock factory +const mockFactory = { + postHealthMetricsToInfluxdb: jest.fn(), + postProxySessionsToInfluxdb: jest.fn(), + postButlerSOSMemoryUsageToInfluxdb: jest.fn(), + postUserEventToInfluxdb: jest.fn(), + postLogEventToInfluxdb: jest.fn(), + storeEventCountInfluxDB: jest.fn(), + storeRejectedEventCountInfluxDB: jest.fn(), + postUserEventQueueMetricsToInfluxdb: jest.fn(), + postLogEventQueueMetricsToInfluxdb: jest.fn(), +}; + +jest.unstable_mockModule('../factory.js', () => mockFactory); + +// Mock shared utils +jest.unstable_mockModule('../shared/utils.js', () => ({ + getFormattedTime: jest.fn((time) => `formatted-${time}`), +})); + +describe('InfluxDB Index (Facade)', () => { + let indexModule; + let globals; + + beforeEach(async () => { + jest.clearAllMocks(); + + globals = (await import('../../../globals.js')).default; + indexModule = await import('../index.js'); + + // Setup default mock implementations + mockFactory.postHealthMetricsToInfluxdb.mockResolvedValue(); + mockFactory.postProxySessionsToInfluxdb.mockResolvedValue(); + mockFactory.postButlerSOSMemoryUsageToInfluxdb.mockResolvedValue(); + mockFactory.postUserEventToInfluxdb.mockResolvedValue(); + mockFactory.postLogEventToInfluxdb.mockResolvedValue(); + mockFactory.storeEventCountInfluxDB.mockResolvedValue(); + mockFactory.storeRejectedEventCountInfluxDB.mockResolvedValue(); + mockFactory.postUserEventQueueMetricsToInfluxdb.mockResolvedValue(); + mockFactory.postLogEventQueueMetricsToInfluxdb.mockResolvedValue(); + + globals.config.get.mockReturnValue(true); + }); + + describe('getFormattedTime', () => { + test('should be exported and callable', () => { + expect(indexModule.getFormattedTime).toBeDefined(); + expect(typeof indexModule.getFormattedTime).toBe('function'); + }); + + test('should format time correctly', () => { + const result = indexModule.getFormattedTime('20240101T120000'); + expect(result).toBe('formatted-20240101T120000'); + }); + }); + + describe('postHealthMetricsToInfluxdb', () => { + test('should delegate to factory', async () => { + const serverName = 'server1'; + const host = 'host1'; + const body = { version: '1.0' }; + const serverTags = [{ name: 'env', value: 'prod' }]; + + await indexModule.postHealthMetricsToInfluxdb(serverName, host, body, serverTags); + + expect(mockFactory.postHealthMetricsToInfluxdb).toHaveBeenCalledWith( + serverName, + host, + body, + serverTags + ); + }); + }); + + describe('postProxySessionsToInfluxdb', () => { + test('should delegate to factory', async () => { + const userSessions = { serverName: 'test', host: 'test-host' }; + + await indexModule.postProxySessionsToInfluxdb(userSessions); + + expect(mockFactory.postProxySessionsToInfluxdb).toHaveBeenCalledWith(userSessions); + }); + }); + + describe('postButlerSOSMemoryUsageToInfluxdb', () => { + test('should delegate to factory', async () => { + const memory = { heap_used: 100, heap_total: 200 }; + + await indexModule.postButlerSOSMemoryUsageToInfluxdb(memory); + + expect(mockFactory.postButlerSOSMemoryUsageToInfluxdb).toHaveBeenCalledWith(memory); + }); + }); + + describe('postUserEventToInfluxdb', () => { + test('should delegate to factory', async () => { + const msg = { host: 'test-host', command: 'OpenApp' }; + + await indexModule.postUserEventToInfluxdb(msg); + + expect(mockFactory.postUserEventToInfluxdb).toHaveBeenCalledWith(msg); + }); + }); + + describe('postLogEventToInfluxdb', () => { + test('should delegate to factory', async () => { + const msg = { host: 'test-host', source: 'qseow-engine' }; + + await indexModule.postLogEventToInfluxdb(msg); + + expect(mockFactory.postLogEventToInfluxdb).toHaveBeenCalledWith(msg); + }); + }); + + describe('storeEventCountInfluxDB', () => { + test('should delegate to factory', async () => { + await indexModule.storeEventCountInfluxDB('midnight', 'hour'); + + expect(mockFactory.storeEventCountInfluxDB).toHaveBeenCalled(); + }); + + test('should ignore deprecated parameters', async () => { + await indexModule.storeEventCountInfluxDB('deprecated1', 'deprecated2'); + + expect(mockFactory.storeEventCountInfluxDB).toHaveBeenCalledWith(); + }); + }); + + describe('storeRejectedEventCountInfluxDB', () => { + test('should delegate to factory', async () => { + await indexModule.storeRejectedEventCountInfluxDB('midnight', 'hour'); + + expect(mockFactory.storeRejectedEventCountInfluxDB).toHaveBeenCalled(); + }); + + test('should ignore deprecated parameters', async () => { + await indexModule.storeRejectedEventCountInfluxDB({ data: 'old' }, { data: 'old2' }); + + expect(mockFactory.storeRejectedEventCountInfluxDB).toHaveBeenCalledWith(); + }); + }); + + describe('postUserEventQueueMetricsToInfluxdb', () => { + test('should delegate to factory', async () => { + await indexModule.postUserEventQueueMetricsToInfluxdb({ some: 'data' }); + + expect(mockFactory.postUserEventQueueMetricsToInfluxdb).toHaveBeenCalled(); + }); + + test('should ignore deprecated parameter', async () => { + await indexModule.postUserEventQueueMetricsToInfluxdb({ old: 'metrics' }); + + expect(mockFactory.postUserEventQueueMetricsToInfluxdb).toHaveBeenCalledWith(); + }); + }); + + describe('postLogEventQueueMetricsToInfluxdb', () => { + test('should delegate to factory', async () => { + await indexModule.postLogEventQueueMetricsToInfluxdb({ some: 'data' }); + + expect(mockFactory.postLogEventQueueMetricsToInfluxdb).toHaveBeenCalled(); + }); + + test('should ignore deprecated parameter', async () => { + await indexModule.postLogEventQueueMetricsToInfluxdb({ old: 'metrics' }); + + expect(mockFactory.postLogEventQueueMetricsToInfluxdb).toHaveBeenCalledWith(); + }); + }); + + describe('setupUdpQueueMetricsStorage', () => { + let intervalSpy; + + beforeEach(() => { + intervalSpy = jest.spyOn(global, 'setInterval'); + }); + + afterEach(() => { + intervalSpy.mockRestore(); + }); + + test('should return empty interval IDs when InfluxDB is disabled', () => { + globals.config.get.mockImplementation((path) => { + if (path.includes('influxdbConfig.enable')) return false; + return undefined; + }); + + const result = indexModule.setupUdpQueueMetricsStorage(); + + expect(result).toEqual({ + userEvents: null, + logEvents: null, + }); + expect(globals.logger.info).toHaveBeenCalledWith( + expect.stringContaining('InfluxDB is disabled') + ); + }); + + test('should setup user event queue metrics when enabled', () => { + globals.config.get.mockImplementation((path) => { + if (path.includes('influxdbConfig.enable')) return true; + if (path.includes('userEvents.udpServerConfig.queueMetrics.influxdb.enable')) + return true; + if ( + path.includes('userEvents.udpServerConfig.queueMetrics.influxdb.writeFrequency') + ) + return 60000; + if (path.includes('logEvents.udpServerConfig.queueMetrics.influxdb.enable')) + return false; + return undefined; + }); + + const result = indexModule.setupUdpQueueMetricsStorage(); + + expect(result.userEvents).not.toBeNull(); + expect(intervalSpy).toHaveBeenCalledWith(expect.any(Function), 60000); + expect(globals.logger.info).toHaveBeenCalledWith( + expect.stringContaining('user event queue metrics') + ); + }); + + test('should setup log event queue metrics when enabled', () => { + globals.config.get.mockImplementation((path) => { + if (path.includes('influxdbConfig.enable')) return true; + if (path.includes('userEvents.udpServerConfig.queueMetrics.influxdb.enable')) + return false; + if (path.includes('logEvents.udpServerConfig.queueMetrics.influxdb.enable')) + return true; + if (path.includes('logEvents.udpServerConfig.queueMetrics.influxdb.writeFrequency')) + return 30000; + return undefined; + }); + + const result = indexModule.setupUdpQueueMetricsStorage(); + + expect(result.logEvents).not.toBeNull(); + expect(intervalSpy).toHaveBeenCalledWith(expect.any(Function), 30000); + }); + + test('should setup both metrics when both enabled', () => { + globals.config.get.mockImplementation((path) => { + if (path.includes('influxdbConfig.enable')) return true; + if (path.includes('userEvents.udpServerConfig.queueMetrics.influxdb.enable')) + return true; + if ( + path.includes('userEvents.udpServerConfig.queueMetrics.influxdb.writeFrequency') + ) + return 45000; + if (path.includes('logEvents.udpServerConfig.queueMetrics.influxdb.enable')) + return true; + if (path.includes('logEvents.udpServerConfig.queueMetrics.influxdb.writeFrequency')) + return 55000; + return undefined; + }); + + const result = indexModule.setupUdpQueueMetricsStorage(); + + expect(result.userEvents).not.toBeNull(); + expect(result.logEvents).not.toBeNull(); + expect(intervalSpy).toHaveBeenCalledTimes(2); + }); + + test('should log when metrics are disabled', () => { + globals.config.get.mockImplementation((path) => { + if (path.includes('influxdbConfig.enable')) return true; + if (path.includes('queueMetrics.influxdb.enable')) return false; + return undefined; + }); + + indexModule.setupUdpQueueMetricsStorage(); + + expect(globals.logger.info).toHaveBeenCalledWith( + expect.stringContaining('User event queue metrics storage to InfluxDB is disabled') + ); + expect(globals.logger.info).toHaveBeenCalledWith( + expect.stringContaining('Log event queue metrics storage to InfluxDB is disabled') + ); + }); + }); +}); diff --git a/src/lib/influxdb/__tests__/shared-utils.test.js b/src/lib/influxdb/__tests__/shared-utils.test.js new file mode 100644 index 0000000..67e4d92 --- /dev/null +++ b/src/lib/influxdb/__tests__/shared-utils.test.js @@ -0,0 +1,358 @@ +import { jest, describe, test, expect, beforeEach } from '@jest/globals'; + +// Mock globals +const mockGlobals = { + logger: { + info: jest.fn(), + verbose: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + }, + config: { + get: jest.fn(), + has: jest.fn(), + }, + influx: null, + appNames: [], +}; + +jest.unstable_mockModule('../../../globals.js', () => ({ + default: mockGlobals, +})); + +describe('Shared Utils - getFormattedTime', () => { + let utils; + let globals; + + beforeEach(async () => { + jest.clearAllMocks(); + globals = (await import('../../../globals.js')).default; + utils = await import('../shared/utils.js'); + }); + + test('should return empty string for null input', () => { + const result = utils.getFormattedTime(null); + expect(result).toBe(''); + }); + + test('should return empty string for undefined input', () => { + const result = utils.getFormattedTime(undefined); + expect(result).toBe(''); + }); + + test('should return empty string for empty string input', () => { + const result = utils.getFormattedTime(''); + expect(result).toBe(''); + }); + + test('should return empty string for non-string input', () => { + const result = utils.getFormattedTime(12345); + expect(result).toBe(''); + }); + + test('should return empty string for string shorter than minimum length', () => { + const result = utils.getFormattedTime('20240101T12'); + expect(result).toBe(''); + }); + + test('should return empty string for invalid date components', () => { + const result = utils.getFormattedTime('abcdXXXXTxxxxxx'); + expect(result).toBe(''); + }); + + test('should handle invalid date gracefully', () => { + // JavaScript Date constructor is lenient and converts Month 13 to January of next year + // So this doesn't actually fail - it's a valid date to JS + const result = utils.getFormattedTime('20241301T250000'); + + // The function doesn't validate date ranges, so this will return a formatted time + expect(typeof result).toBe('string'); + }); + + test('should format valid timestamp correctly', () => { + // Mock Date.now to return a known value + const mockNow = new Date('2024-01-01T13:00:00').getTime(); + jest.spyOn(Date, 'now').mockReturnValue(mockNow); + + const result = utils.getFormattedTime('20240101T120000'); + + // Should show approximately 1 hour difference + expect(result).toMatch(/\d+ days, \d+h \d+m \d+s/); + + Date.now.mockRestore(); + }); + + test('should handle timestamps with exact minimum length', () => { + const mockNow = new Date('2024-01-01T13:00:00').getTime(); + jest.spyOn(Date, 'now').mockReturnValue(mockNow); + + const result = utils.getFormattedTime('20240101T120000'); + + expect(result).not.toBe(''); + expect(result).toMatch(/\d+ days/); + + Date.now.mockRestore(); + }); + + test('should handle future timestamps', () => { + const mockNow = new Date('2024-01-01T12:00:00').getTime(); + jest.spyOn(Date, 'now').mockReturnValue(mockNow); + + // Server started in the future (edge case) + const result = utils.getFormattedTime('20250101T120000'); + + // Result might be negative or weird, but shouldn't crash + expect(typeof result).toBe('string'); + + Date.now.mockRestore(); + }); +}); + +describe('Shared Utils - processAppDocuments', () => { + let utils; + let globals; + + beforeEach(async () => { + jest.clearAllMocks(); + globals = (await import('../../../globals.js')).default; + utils = await import('../shared/utils.js'); + + globals.appNames = [ + { id: 'app-123', name: 'Sales Dashboard' }, + { id: 'app-456', name: 'HR Analytics' }, + { id: 'app-789', name: 'Finance Report' }, + ]; + }); + + test('should process empty array', async () => { + const result = await utils.processAppDocuments([], 'TEST', 'active'); + + expect(result).toEqual({ + appNames: [], + sessionAppNames: [], + }); + }); + + test('should identify session apps correctly', async () => { + const docIDs = ['SessionApp_12345', 'SessionApp_67890']; + + const result = await utils.processAppDocuments(docIDs, 'TEST', 'active'); + + expect(result.sessionAppNames).toEqual(['SessionApp_12345', 'SessionApp_67890']); + expect(result.appNames).toEqual([]); + expect(globals.logger.debug).toHaveBeenCalledWith( + expect.stringContaining('Session app is active') + ); + }); + + test('should resolve app IDs to names', async () => { + const docIDs = ['app-123', 'app-456']; + + const result = await utils.processAppDocuments(docIDs, 'TEST', 'loaded'); + + expect(result.appNames).toEqual(['HR Analytics', 'Sales Dashboard']); + expect(result.sessionAppNames).toEqual([]); + expect(globals.logger.debug).toHaveBeenCalledWith( + expect.stringContaining('App is loaded: Sales Dashboard') + ); + }); + + test('should use doc ID when app name not found', async () => { + const docIDs = ['app-unknown', 'app-123']; + + const result = await utils.processAppDocuments(docIDs, 'TEST', 'in memory'); + + expect(result.appNames).toEqual(['Sales Dashboard', 'app-unknown']); + expect(result.sessionAppNames).toEqual([]); + }); + + test('should mix session apps and regular apps', async () => { + const docIDs = ['app-123', 'SessionApp_abc', 'app-456', 'SessionApp_def', 'app-unknown']; + + const result = await utils.processAppDocuments(docIDs, 'TEST', 'active'); + + expect(result.appNames).toEqual(['HR Analytics', 'Sales Dashboard', 'app-unknown']); + expect(result.sessionAppNames).toEqual(['SessionApp_abc', 'SessionApp_def']); + }); + + test('should sort both arrays alphabetically', async () => { + const docIDs = ['app-789', 'app-123', 'app-456', 'SessionApp_z', 'SessionApp_a']; + + const result = await utils.processAppDocuments(docIDs, 'TEST', 'active'); + + expect(result.appNames).toEqual(['Finance Report', 'HR Analytics', 'Sales Dashboard']); + expect(result.sessionAppNames).toEqual(['SessionApp_a', 'SessionApp_z']); + }); + + test('should handle session app prefix at start only', async () => { + const docIDs = ['SessionApp_test', 'NotSessionApp_test', 'app-123']; + + const result = await utils.processAppDocuments(docIDs, 'TEST', 'active'); + + expect(result.sessionAppNames).toEqual(['SessionApp_test']); + expect(result.appNames).toEqual(['NotSessionApp_test', 'Sales Dashboard']); + }); + + test('should handle single document', async () => { + const docIDs = ['app-456']; + + const result = await utils.processAppDocuments(docIDs, 'TEST', 'active'); + + expect(result.appNames).toEqual(['HR Analytics']); + expect(result.sessionAppNames).toEqual([]); + }); + + test('should handle many documents efficiently', async () => { + const docIDs = Array.from({ length: 100 }, (_, i) => + i % 2 === 0 ? `SessionApp_${i}` : `app-${i}` + ); + + const result = await utils.processAppDocuments(docIDs, 'TEST', 'active'); + + expect(result.sessionAppNames.length).toBe(50); + expect(result.appNames.length).toBe(50); + // Arrays are sorted alphabetically + expect(result.sessionAppNames).toEqual(expect.arrayContaining(['SessionApp_0'])); + expect(result.appNames).toEqual(expect.arrayContaining(['app-1'])); + }); +}); + +describe('Shared Utils - applyTagsToPoint3', () => { + let utils; + let mockPoint; + + beforeEach(async () => { + jest.clearAllMocks(); + utils = await import('../shared/utils.js'); + + mockPoint = { + setTag: jest.fn().mockReturnThis(), + }; + }); + + test('should return point unchanged for null tags', () => { + const result = utils.applyTagsToPoint3(mockPoint, null); + + expect(result).toBe(mockPoint); + expect(mockPoint.setTag).not.toHaveBeenCalled(); + }); + + test('should return point unchanged for undefined tags', () => { + const result = utils.applyTagsToPoint3(mockPoint, undefined); + + expect(result).toBe(mockPoint); + expect(mockPoint.setTag).not.toHaveBeenCalled(); + }); + + test('should return point unchanged for non-object tags', () => { + const result = utils.applyTagsToPoint3(mockPoint, 'not-an-object'); + + expect(result).toBe(mockPoint); + expect(mockPoint.setTag).not.toHaveBeenCalled(); + }); + + test('should apply single tag', () => { + const tags = { env: 'production' }; + + const result = utils.applyTagsToPoint3(mockPoint, tags); + + expect(result).toBe(mockPoint); + expect(mockPoint.setTag).toHaveBeenCalledWith('env', 'production'); + expect(mockPoint.setTag).toHaveBeenCalledTimes(1); + }); + + test('should apply multiple tags', () => { + const tags = { + env: 'production', + region: 'us-east-1', + service: 'qlik-sense', + }; + + utils.applyTagsToPoint3(mockPoint, tags); + + expect(mockPoint.setTag).toHaveBeenCalledTimes(3); + expect(mockPoint.setTag).toHaveBeenCalledWith('env', 'production'); + expect(mockPoint.setTag).toHaveBeenCalledWith('region', 'us-east-1'); + expect(mockPoint.setTag).toHaveBeenCalledWith('service', 'qlik-sense'); + }); + + test('should convert non-string values to strings', () => { + const tags = { + count: 42, + enabled: true, + version: 3.14, + }; + + utils.applyTagsToPoint3(mockPoint, tags); + + expect(mockPoint.setTag).toHaveBeenCalledWith('count', '42'); + expect(mockPoint.setTag).toHaveBeenCalledWith('enabled', 'true'); + expect(mockPoint.setTag).toHaveBeenCalledWith('version', '3.14'); + }); + + test('should skip null values', () => { + const tags = { + env: 'production', + region: null, + service: 'qlik-sense', + }; + + utils.applyTagsToPoint3(mockPoint, tags); + + expect(mockPoint.setTag).toHaveBeenCalledTimes(2); + expect(mockPoint.setTag).toHaveBeenCalledWith('env', 'production'); + expect(mockPoint.setTag).toHaveBeenCalledWith('service', 'qlik-sense'); + expect(mockPoint.setTag).not.toHaveBeenCalledWith('region', expect.anything()); + }); + + test('should skip undefined values', () => { + const tags = { + env: 'production', + region: undefined, + service: 'qlik-sense', + }; + + utils.applyTagsToPoint3(mockPoint, tags); + + expect(mockPoint.setTag).toHaveBeenCalledTimes(2); + expect(mockPoint.setTag).toHaveBeenCalledWith('env', 'production'); + expect(mockPoint.setTag).toHaveBeenCalledWith('service', 'qlik-sense'); + }); + + test('should handle empty object', () => { + const tags = {}; + + const result = utils.applyTagsToPoint3(mockPoint, tags); + + expect(result).toBe(mockPoint); + expect(mockPoint.setTag).not.toHaveBeenCalled(); + }); + + test('should handle tags with special characters', () => { + const tags = { + 'tag-with-dash': 'value', + tag_with_underscore: 'value2', + 'tag.with.dot': 'value3', + }; + + utils.applyTagsToPoint3(mockPoint, tags); + + expect(mockPoint.setTag).toHaveBeenCalledTimes(3); + expect(mockPoint.setTag).toHaveBeenCalledWith('tag-with-dash', 'value'); + expect(mockPoint.setTag).toHaveBeenCalledWith('tag_with_underscore', 'value2'); + expect(mockPoint.setTag).toHaveBeenCalledWith('tag.with.dot', 'value3'); + }); + + test('should handle empty string values', () => { + const tags = { + env: '', + region: 'us-east-1', + }; + + utils.applyTagsToPoint3(mockPoint, tags); + + expect(mockPoint.setTag).toHaveBeenCalledWith('env', ''); + expect(mockPoint.setTag).toHaveBeenCalledWith('region', 'us-east-1'); + }); +});