import { TokenManager } from '../../managers/TokenManager'; // Mock fetch global.fetch = jest.fn(); describe('TokenManager', () => { let tokenManager: TokenManager; let mockEventHandlers: any; beforeEach(() => { mockEventHandlers = { tokenFetchStarted: jest.fn(), tokenFetchSucceeded: jest.fn(), tokenFetchFailed: jest.fn(), tokenExpiring: jest.fn(), tokenRefreshed: jest.fn(), tokenRefreshFailed: jest.fn() }; jest.useFakeTimers(); jest.clearAllMocks(); }); afterEach(() => { if (tokenManager) { tokenManager.destroy(); } jest.useRealTimers(); }); describe('initialization with token', () => { it('should initialize with provided token', () => { const expiresAt = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now tokenManager = new TokenManager({ token: 'test-token', tokenExpiresAt: expiresAt, tokenFetchRetry: { maxAttempts: 3, delay: 1000, backoff: 'exponential' }, autoRefreshToken: false, refreshBeforeExpiry: 300000, // 5 minutes baseUrl: 'https://api.test.com' }); expect(tokenManager.isTokenValid()).toBe(true); }); it('should schedule auto-refresh when configured', () => { const expiresAt = Math.floor(Date.now() / 1000) + 3600; tokenManager = new TokenManager({ token: 'test-token', tokenExpiresAt: expiresAt, tokenFetchRetry: { maxAttempts: 3, delay: 1000, backoff: 'exponential' }, autoRefreshToken: true, refreshBeforeExpiry: 300000, baseUrl: 'https://api.test.com', onTokenExpiring: jest.fn().mockResolvedValue({ token: 'refreshed-token', expiresAt: expiresAt + 3600 }) }); // Register event handlers Object.keys(mockEventHandlers).forEach(event => { tokenManager.on(event as any, mockEventHandlers[event]); }); // Timer should be scheduled expect(jest.getTimerCount()).toBeGreaterThan(0); }); }); describe('token validation', () => { beforeEach(() => { tokenManager = new TokenManager({ tokenFetchRetry: { maxAttempts: 3, delay: 1000, backoff: 'exponential' }, autoRefreshToken: false, refreshBeforeExpiry: 300000, baseUrl: 'https://api.test.com' }); }); it('should return false for missing token', () => { expect(tokenManager.isTokenValid()).toBe(false); }); it('should return true for token without expiry', () => { tokenManager = new TokenManager({ token: 'test-token', tokenFetchRetry: { maxAttempts: 3, delay: 1000, backoff: 'exponential' }, autoRefreshToken: false, refreshBeforeExpiry: 300000, baseUrl: 'https://api.test.com' }); expect(tokenManager.isTokenValid()).toBe(true); }); it('should return false for expired token', () => { const expiredTime = Math.floor(Date.now() / 1000) - 3600; // 1 hour ago tokenManager = new TokenManager({ token: 'expired-token', tokenExpiresAt: expiredTime, tokenFetchRetry: { maxAttempts: 3, delay: 1000, backoff: 'exponential' }, autoRefreshToken: false, refreshBeforeExpiry: 300000, baseUrl: 'https://api.test.com' }); expect(tokenManager.isTokenValid()).toBe(false); }); }); describe('fetchTokenFromEndpoint', () => { beforeEach(() => { tokenManager = new TokenManager({ tokenEndpoint: '/api/token', tokenFetchHeaders: { 'Custom-Header': 'value' }, tokenFetchRetry: { maxAttempts: 3, delay: 1000, backoff: 'exponential' }, autoRefreshToken: false, refreshBeforeExpiry: 300000, baseUrl: 'https://api.test.com' }); Object.keys(mockEventHandlers).forEach(event => { tokenManager.on(event as any, mockEventHandlers[event]); }); }); it('should fetch token successfully', async () => { const mockResponse = { ok: true, json: jest.fn().mockResolvedValue({ token: 'new-token', expires_at: Math.floor(Date.now() / 1000) + 3600 }) }; (fetch as jest.Mock).mockResolvedValue(mockResponse); const token = await tokenManager.getToken(); expect(token).toBe('new-token'); expect(mockEventHandlers.tokenFetchStarted).toHaveBeenCalledWith('endpoint'); expect(mockEventHandlers.tokenFetchSucceeded).toHaveBeenCalledWith({ token: 'new-token', expiresAt: expect.any(Number), source: 'endpoint' }); expect(fetch).toHaveBeenCalledWith('/api/token', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Custom-Header': 'value' } }); }); it('should handle fetch error', async () => { const mockResponse = { ok: false, status: 401, statusText: 'Unauthorized' }; (fetch as jest.Mock).mockResolvedValue(mockResponse); await expect(tokenManager.getToken()).rejects.toThrow(); expect(mockEventHandlers.tokenFetchFailed).toHaveBeenCalled(); }); it('should retry on failure', async () => { const mockFailResponse = { ok: false, status: 500, statusText: 'Server Error' }; const mockSuccessResponse = { ok: true, json: jest.fn().mockResolvedValue({ token: 'retry-token', expires_at: Math.floor(Date.now() / 1000) + 3600 }) }; (fetch as jest.Mock) .mockResolvedValueOnce(mockFailResponse) .mockResolvedValueOnce(mockSuccessResponse); const tokenPromise = tokenManager.getToken(); jest.runAllTimers(); // Fast-forward retry delays const token = await tokenPromise; expect(token).toBe('retry-token'); expect(fetch).toHaveBeenCalledTimes(2); }); }); describe('fetchTokenFromApi', () => { beforeEach(() => { tokenManager = new TokenManager({ apiKey: 'test-api-key', tokenFetchRetry: { maxAttempts: 3, delay: 1000, backoff: 'exponential' }, autoRefreshToken: false, refreshBeforeExpiry: 300000, baseUrl: 'https://api.test.com' }); Object.keys(mockEventHandlers).forEach(event => { tokenManager.on(event as any, mockEventHandlers[event]); }); }); it('should fetch token using API key', async () => { const mockResponse = { ok: true, json: jest.fn().mockResolvedValue({ token: 'api-token', expires_at: Math.floor(Date.now() / 1000) + 3600 }) }; (fetch as jest.Mock).mockResolvedValue(mockResponse); const token = await tokenManager.getToken(); expect(token).toBe('api-token'); expect(mockEventHandlers.tokenFetchStarted).toHaveBeenCalledWith('api'); expect(fetch).toHaveBeenCalledWith('https://api.test.com/stream/token', { method: 'POST', headers: { 'Authorization': 'Bearer test-api-key', 'Content-Type': 'application/json', 'User-Agent': 'WioEX-Stream-SDK/1.5.0' } }); }); }); describe('token refresh', () => { beforeEach(() => { const mockRefreshCallback = jest.fn().mockResolvedValue({ token: 'refreshed-token', expiresAt: Math.floor(Date.now() / 1000) + 3600 }); tokenManager = new TokenManager({ token: 'current-token', tokenExpiresAt: Math.floor(Date.now() / 1000) + 300, // Expires in 5 minutes tokenFetchRetry: { maxAttempts: 3, delay: 1000, backoff: 'exponential' }, autoRefreshToken: true, refreshBeforeExpiry: 600000, // 10 minutes baseUrl: 'https://api.test.com', onTokenExpiring: mockRefreshCallback }); Object.keys(mockEventHandlers).forEach(event => { tokenManager.on(event as any, mockEventHandlers[event]); }); }); it('should refresh token automatically', async () => { // Fast-forward to trigger refresh jest.runAllTimers(); expect(mockEventHandlers.tokenExpiring).toHaveBeenCalled(); expect(mockEventHandlers.tokenRefreshed).toHaveBeenCalledWith({ token: 'refreshed-token', expiresAt: expect.any(Number) }); }); it('should handle refresh failure', async () => { const mockRefreshCallback = jest.fn().mockRejectedValue(new Error('Refresh failed')); tokenManager = new TokenManager({ token: 'current-token', tokenExpiresAt: Math.floor(Date.now() / 1000) + 300, tokenFetchRetry: { maxAttempts: 3, delay: 1000, backoff: 'exponential' }, autoRefreshToken: true, refreshBeforeExpiry: 600000, baseUrl: 'https://api.test.com', onTokenExpiring: mockRefreshCallback }); Object.keys(mockEventHandlers).forEach(event => { tokenManager.on(event as any, mockEventHandlers[event]); }); jest.runAllTimers(); expect(mockEventHandlers.tokenRefreshFailed).toHaveBeenCalledWith( expect.objectContaining({ message: 'Refresh failed' }) ); }); }); describe('getToken', () => { it('should return current valid token', async () => { tokenManager = new TokenManager({ token: 'valid-token', tokenExpiresAt: Math.floor(Date.now() / 1000) + 3600, tokenFetchRetry: { maxAttempts: 3, delay: 1000, backoff: 'exponential' }, autoRefreshToken: false, refreshBeforeExpiry: 300000, baseUrl: 'https://api.test.com' }); const token = await tokenManager.getToken(); expect(token).toBe('valid-token'); }); it('should fetch new token when current is invalid', async () => { tokenManager = new TokenManager({ tokenEndpoint: '/api/token', tokenFetchRetry: { maxAttempts: 3, delay: 1000, backoff: 'exponential' }, autoRefreshToken: false, refreshBeforeExpiry: 300000, baseUrl: 'https://api.test.com' }); const mockResponse = { ok: true, json: jest.fn().mockResolvedValue({ token: 'fetched-token', expires_at: Math.floor(Date.now() / 1000) + 3600 }) }; (fetch as jest.Mock).mockResolvedValue(mockResponse); const token = await tokenManager.getToken(); expect(token).toBe('fetched-token'); }); it('should throw error when unable to get token', async () => { tokenManager = new TokenManager({ tokenFetchRetry: { maxAttempts: 3, delay: 1000, backoff: 'exponential' }, autoRefreshToken: false, refreshBeforeExpiry: 300000, baseUrl: 'https://api.test.com' }); await expect(tokenManager.getToken()).rejects.toThrow('No token source configured'); }); }); describe('cleanup', () => { it('should clean up resources on destroy', () => { tokenManager = new TokenManager({ token: 'test-token', tokenExpiresAt: Math.floor(Date.now() / 1000) + 3600, tokenFetchRetry: { maxAttempts: 3, delay: 1000, backoff: 'exponential' }, autoRefreshToken: true, refreshBeforeExpiry: 300000, baseUrl: 'https://api.test.com', onTokenExpiring: jest.fn() }); const initialTimerCount = jest.getTimerCount(); tokenManager.destroy(); expect(jest.getTimerCount()).toBeLessThan(initialTimerCount); expect(tokenManager.isTokenValid()).toBe(false); }); }); });