/* eslint-disable @typescript-eslint/no-unused-vars */ /** * Unit tests for unified-session.ts * * Tests the UnifiedSessionManager implementation that mimics * official WhatsApp Web client telemetry behavior. */ import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals' import { createUnifiedSessionManager, extractServerTime, shouldEnableUnifiedSession, UnifiedSessionManager, type UnifiedSessionOptions } from '../../Utils/unified-session.js' import type { BinaryNode } from '../../WABinary/types.js' // TimeMs constants for testing (matching unified-session.ts) const TimeMs = { Second: 1_000, Minute: 60_000, Hour: 3_600_000, Day: 86_400_000, Week: 604_800_000 } as const // Mock the prometheus metrics to avoid side effects jest.mock('../../Utils/prometheus-metrics.js', () => ({ metrics: { socketEvents: { inc: jest.fn() }, errors: { inc: jest.fn() }, circuitBreakerTrips: { inc: jest.fn() } } })) // Mock logger const createMockLogger = () => ({ trace: jest.fn(), debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), level: 'debug' }) // Type for mock sendNode function type MockSendNode = jest.Mock<(node: BinaryNode) => Promise> describe('UnifiedSessionManager', () => { let manager: UnifiedSessionManager let mockLogger: ReturnType let mockSendNode: MockSendNode beforeEach(() => { mockLogger = createMockLogger() mockSendNode = jest.fn<(node: BinaryNode) => Promise>().mockResolvedValue(undefined) manager = createUnifiedSessionManager({ enabled: true, logger: mockLogger as any, enableCircuitBreaker: false, // Disable for simpler testing sendNode: mockSendNode }) }) afterEach(() => { manager.destroy() jest.clearAllMocks() }) describe('TimeMs constants', () => { it('should have correct time values', () => { expect(TimeMs.Second).toBe(1_000) expect(TimeMs.Minute).toBe(60_000) expect(TimeMs.Hour).toBe(3_600_000) expect(TimeMs.Day).toBe(86_400_000) expect(TimeMs.Week).toBe(604_800_000) }) it('should have readonly constants (enforced by TypeScript)', () => { // TypeScript's 'as const' ensures compile-time immutability // At runtime, we verify the values are what we expect expect(TimeMs.Second).toBe(1_000) expect(TimeMs.Week).toBe(604_800_000) // Verify the object structure is correct expect(Object.keys(TimeMs)).toEqual(['Second', 'Minute', 'Hour', 'Day', 'Week']) }) }) describe('initialization', () => { it('should initialize with default state', () => { const state = manager.getState() expect(state.isInitialized).toBe(true) expect(state.serverTimeOffset).toBe(0) expect(state.lastSentTime).toBe(0) expect(state.sendCount).toBe(0) }) it('should create with factory function', () => { const newManager = createUnifiedSessionManager({ enabled: true }) expect(newManager.getState().isInitialized).toBe(true) newManager.destroy() }) it('should respect enabled option', async () => { const disabledManager = createUnifiedSessionManager({ enabled: false, sendNode: mockSendNode }) await disabledManager.send('login') expect(mockSendNode).not.toHaveBeenCalled() disabledManager.destroy() }) }) describe('getSessionId', () => { it('should return a string', () => { const sessionId = manager.getSessionId() expect(typeof sessionId).toBe('string') }) it('should return a numeric value as string', () => { const sessionId = manager.getSessionId() const parsed = parseInt(sessionId, 10) expect(isNaN(parsed)).toBe(false) expect(parsed).toBeGreaterThanOrEqual(0) }) it('should return value within week range', () => { const sessionId = manager.getSessionId() const value = parseInt(sessionId, 10) // Session ID should be modulo 7 days (in ms) expect(value).toBeLessThan(TimeMs.Week) }) it('should change with server time offset', () => { const id1 = manager.getSessionId() // Update offset by 1 hour manager.updateServerTimeOffset(Math.floor(Date.now() / 1000) + 3600) const id2 = manager.getSessionId() // IDs should be different due to offset expect(id1).not.toBe(id2) }) }) describe('updateServerTimeOffset', () => { it('should update offset from string timestamp', () => { const serverTime = Math.floor(Date.now() / 1000) + 100 manager.updateServerTimeOffset(serverTime.toString()) const state = manager.getState() // Offset should be approximately 100 seconds (100,000ms) expect(Math.abs(state.serverTimeOffset - 100_000)).toBeLessThan(1000) }) it('should update offset from number timestamp', () => { const serverTime = Math.floor(Date.now() / 1000) - 50 manager.updateServerTimeOffset(serverTime) const state = manager.getState() // Offset should be approximately -50 seconds (-50,000ms) expect(Math.abs(state.serverTimeOffset + 50_000)).toBeLessThan(1000) }) it('should ignore undefined value', () => { manager.updateServerTimeOffset(undefined) expect(manager.getState().serverTimeOffset).toBe(0) }) it('should ignore invalid values', () => { manager.updateServerTimeOffset('invalid') expect(manager.getState().serverTimeOffset).toBe(0) manager.updateServerTimeOffset(-1) expect(manager.getState().serverTimeOffset).toBe(0) manager.updateServerTimeOffset(0) expect(manager.getState().serverTimeOffset).toBe(0) }) it('should only update on significant change (>1 second)', () => { const serverTime = Math.floor(Date.now() / 1000) + 100 manager.updateServerTimeOffset(serverTime) const offset1 = manager.getState().serverTimeOffset // Small change (< 1 second) should not update manager.updateServerTimeOffset(serverTime + 0.5) const offset2 = manager.getState().serverTimeOffset expect(offset1).toBe(offset2) }) }) describe('send', () => { it('should send unified_session node', async () => { await manager.send('login') expect(mockSendNode).toHaveBeenCalledTimes(1) const sentNode = mockSendNode.mock.calls[0]?.[0] expect(sentNode).toBeDefined() expect(sentNode!.tag).toBe('ib') expect(Array.isArray(sentNode!.content)).toBe(true) const content = sentNode!.content as BinaryNode[] expect(content).toHaveLength(1) expect(content[0]!.tag).toBe('unified_session') expect(typeof content[0]!.attrs.id).toBe('string') }) it('should update state after send', async () => { expect(manager.getState().sendCount).toBe(0) await manager.send('login') const state = manager.getState() expect(state.sendCount).toBe(1) expect(state.lastSentTime).toBeGreaterThan(0) }) it('should rate limit sends (except login)', async () => { // First send should work await manager.send('presence') expect(mockSendNode).toHaveBeenCalledTimes(1) // Immediate second send should be skipped (rate limited) await manager.send('presence') expect(mockSendNode).toHaveBeenCalledTimes(1) }) it('should not rate limit login trigger', async () => { await manager.send('login') expect(mockSendNode).toHaveBeenCalledTimes(1) // Login should always send, even immediately after await manager.send('login') expect(mockSendNode).toHaveBeenCalledTimes(2) }) it('should not throw on send failure', async () => { mockSendNode.mockRejectedValue(new Error('Network error')) // Should not throw await expect(manager.send('login')).resolves.toBeUndefined() // Reset mock for subsequent tests mockSendNode.mockResolvedValue(undefined) }) it('should log warning on send failure', async () => { mockSendNode.mockRejectedValue(new Error('Network error')) await manager.send('login') expect(mockLogger.warn).toHaveBeenCalled() // Reset mock for subsequent tests mockSendNode.mockResolvedValue(undefined) }) }) describe('reset', () => { it('should reset all state', async () => { // Modify state manager.updateServerTimeOffset(Math.floor(Date.now() / 1000) + 100) await manager.send('login') const stateBefore = manager.getState() expect(stateBefore.serverTimeOffset).not.toBe(0) expect(stateBefore.sendCount).toBe(1) // Reset manager.reset() const stateAfter = manager.getState() expect(stateAfter.serverTimeOffset).toBe(0) expect(stateAfter.sendCount).toBe(0) expect(stateAfter.lastSentTime).toBe(0) expect(stateAfter.isInitialized).toBe(true) }) }) describe('destroy', () => { it('should mark as not initialized', () => { manager.destroy() expect(manager.getState().isInitialized).toBe(false) }) }) }) describe('extractServerTime', () => { it('should extract time from string attribute', () => { const node = { tag: 'test', attrs: { t: '1234567890' } } expect(extractServerTime(node as any)).toBe(1234567890) }) it('should extract time from number attribute', () => { const node = { tag: 'test', attrs: { t: 1234567890 } } expect(extractServerTime(node as any)).toBe(1234567890) }) it('should return undefined for missing attribute', () => { const node = { tag: 'test', attrs: {} } expect(extractServerTime(node as any)).toBeUndefined() }) it('should return undefined for invalid values', () => { expect(extractServerTime({ tag: 'test', attrs: { t: 'invalid' } } as any)).toBeUndefined() expect(extractServerTime({ tag: 'test', attrs: { t: 0 } } as any)).toBeUndefined() expect(extractServerTime({ tag: 'test', attrs: { t: -1 } } as any)).toBeUndefined() }) }) describe('shouldEnableUnifiedSession', () => { const originalEnv = process.env.BAILEYS_UNIFIED_SESSION_ENABLED afterEach(() => { if (originalEnv === undefined) { delete process.env.BAILEYS_UNIFIED_SESSION_ENABLED } else { process.env.BAILEYS_UNIFIED_SESSION_ENABLED = originalEnv } }) it('should return true by default', () => { delete process.env.BAILEYS_UNIFIED_SESSION_ENABLED expect(shouldEnableUnifiedSession()).toBe(true) }) it('should respect environment variable true', () => { process.env.BAILEYS_UNIFIED_SESSION_ENABLED = 'true' expect(shouldEnableUnifiedSession()).toBe(true) process.env.BAILEYS_UNIFIED_SESSION_ENABLED = '1' expect(shouldEnableUnifiedSession()).toBe(true) process.env.BAILEYS_UNIFIED_SESSION_ENABLED = 'TRUE' expect(shouldEnableUnifiedSession()).toBe(true) }) it('should respect environment variable false', () => { process.env.BAILEYS_UNIFIED_SESSION_ENABLED = 'false' expect(shouldEnableUnifiedSession()).toBe(false) process.env.BAILEYS_UNIFIED_SESSION_ENABLED = '0' expect(shouldEnableUnifiedSession()).toBe(false) process.env.BAILEYS_UNIFIED_SESSION_ENABLED = '' expect(shouldEnableUnifiedSession()).toBe(false) }) }) describe('UnifiedSessionManager with CircuitBreaker', () => { let manager: UnifiedSessionManager let mockSendNode: MockSendNode beforeEach(() => { mockSendNode = jest.fn<(node: BinaryNode) => Promise>().mockResolvedValue(undefined) manager = createUnifiedSessionManager({ enabled: true, logger: createMockLogger() as any, enableCircuitBreaker: true, sendNode: mockSendNode }) }) afterEach(() => { manager.destroy() }) it('should work with circuit breaker enabled', async () => { await manager.send('login') expect(mockSendNode).toHaveBeenCalledTimes(1) }) it('should handle circuit breaker failures gracefully', async () => { // Make send fail multiple times to trigger circuit breaker let callCount = 0 mockSendNode.mockImplementation(async () => { callCount++ if (callCount <= 3) { throw new Error(`Fail ${callCount}`) } }) // These should not throw even with failures await expect(manager.send('login')).resolves.toBeUndefined() await expect(manager.send('login')).resolves.toBeUndefined() await expect(manager.send('login')).resolves.toBeUndefined() }) })