import { AuthService } from '../auth.service'; // Mock fetch global.fetch = jest.fn(); describe('AuthService', () => { let authService: AuthService; const mockFetch = fetch as jest.MockedFunction; beforeEach(() => { authService = new AuthService('https://api.example.com'); mockFetch.mockClear(); }); afterEach(() => { jest.clearAllMocks(); }); describe('createSession', () => { it('should create a session successfully', async () => { const mockResponse = { status: 'success', message: 'Session created successfully', data: { token: 'mock-jwt-token', expires_at: new Date(Date.now() + 3600000).toISOString(), // 1 hour from now session_id: 'mock-session-id', }, }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockResponse, } as Response); const result = await authService.createSession('pk_test_key', 'https://example.com'); expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/api/widgets/session/create/', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ widget_key: 'pk_test_key', origin: 'https://example.com', }), }); expect(result).toEqual(mockResponse.data); // The token should be stored and valid after successful session creation expect(await authService.getToken()).toBe('mock-jwt-token'); expect(authService.isTokenValid()).toBe(true); }); it('should accept flat session JSON (production API shape)', async () => { const flat = { token: 'flat-token', expires_at: new Date(Date.now() + 3600000).toISOString(), session_id: 'sid', }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => flat, } as Response); const result = await authService.createSession('pk_test_key', 'https://example.com'); expect(result).toEqual(flat); expect(await authService.getToken()).toBe('flat-token'); }); it('should handle API errors', async () => { const mockError = { error: 'Invalid widget key', }; mockFetch.mockResolvedValueOnce({ ok: false, json: async () => mockError, } as Response); await expect(authService.createSession('pk_invalid_key', 'https://example.com')).rejects.toThrow('Invalid widget key'); }); it('should handle network errors', async () => { mockFetch.mockRejectedValueOnce(new Error('Network error')); await expect(authService.createSession('pk_test_key', 'https://example.com')).rejects.toThrow('Network error'); }); }); describe('token management', () => { it('should return undefined for invalid token', async () => { expect(await authService.getToken()).toBeUndefined(); expect(authService.isTokenValid()).toBe(false); }); it('should clear session', async () => { // Set a mock token const authServiceWithPrivate = authService as unknown as { sessionToken?: string; sessionExpiresAt?: Date }; authServiceWithPrivate.sessionToken = 'mock-token'; authServiceWithPrivate.sessionExpiresAt = new Date(Date.now() + 3600000); authService.clearSession(); expect(await authService.getToken()).toBeUndefined(); expect(authService.isTokenValid()).toBe(false); }); it('should return auth header when token is valid', async () => { const authServiceWithPrivate = authService as unknown as { sessionToken?: string; sessionExpiresAt?: Date }; authServiceWithPrivate.sessionToken = 'mock-token'; authServiceWithPrivate.sessionExpiresAt = new Date(Date.now() + 3600000); expect(await authService.getAuthHeader()).toEqual({ Authorization: 'Bearer mock-token', }); }); it('should return empty object when token is invalid', async () => { expect(await authService.getAuthHeader()).toEqual({}); }); it('should return undefined when token is expired', async () => { // Set a mock token with past expiration const authServiceWithPrivate = authService as unknown as { sessionToken?: string; sessionExpiresAt?: Date }; authServiceWithPrivate.sessionToken = 'mock-token'; authServiceWithPrivate.sessionExpiresAt = new Date(Date.now() - 3600000); expect(await authService.getToken()).toBeUndefined(); expect(authService.isTokenValid()).toBe(false); }); }); describe('refreshSessionIfNeeded', () => { it('should not refresh if token is valid', async () => { const authServiceWithPrivate = authService as unknown as { sessionToken?: string; sessionExpiresAt?: Date }; authServiceWithPrivate.sessionToken = 'mock-token'; authServiceWithPrivate.sessionExpiresAt = new Date(Date.now() + 3600000); const result = await authService.refreshSessionIfNeeded('pk_test_key', 'https://example.com'); expect(result).toBe(true); expect(mockFetch).not.toHaveBeenCalled(); }); it('should refresh if token is invalid', async () => { const mockResponse = { status: 'success', message: 'Session created successfully', data: { token: 'new-mock-token', expires_at: new Date(Date.now() + 3600000).toISOString(), // 1 hour from now session_id: 'new-session-id', }, }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockResponse, } as Response); const result = await authService.refreshSessionIfNeeded('pk_test_key', 'https://example.com'); expect(result).toBe(true); expect(mockFetch).toHaveBeenCalled(); expect(await authService.getToken()).toBe('new-mock-token'); }); it('should return false if refresh fails', async () => { mockFetch.mockRejectedValueOnce(new Error('Network error')); const result = await authService.refreshSessionIfNeeded('pk_test_key', 'https://example.com'); expect(result).toBe(false); }); }); describe('chat ID management', () => { it('should set and get chat ID', () => { const chatId = 'test-chat-id-123'; authService.setChatId(chatId); expect(authService.getChatId()).toBe(chatId); }); it('should return undefined for chat ID when not set', () => { expect(authService.getChatId()).toBeUndefined(); }); it('should return chat ID header when chat ID is set', () => { const chatId = 'test-chat-id-123'; authService.setChatId(chatId); expect(authService.getChatIdHeader()).toEqual({ 'X-BCX-Chat-ID': chatId, }); }); it('should return empty object for chat ID header when not set', () => { expect(authService.getChatIdHeader()).toEqual({}); }); it('should return all headers including chat ID', async () => { const authServiceWithPrivate = authService as unknown as { sessionToken?: string; sessionExpiresAt?: Date }; authServiceWithPrivate.sessionToken = 'mock-token'; authServiceWithPrivate.sessionExpiresAt = new Date(Date.now() + 3600000); const chatId = 'test-chat-id-123'; authService.setChatId(chatId); expect(await authService.getAllHeaders()).toEqual({ 'Authorization': 'Bearer mock-token', 'X-BCX-Chat-ID': chatId, }); }); it('should clear chat ID when clearing session', () => { const chatId = 'test-chat-id-123'; authService.setChatId(chatId); expect(authService.getChatId()).toBe(chatId); authService.clearSession(); expect(authService.getChatId()).toBeUndefined(); }); }); describe('automatic token refresh', () => { it('should automatically refresh token when expired in getToken()', async () => { // Set expired token const authServiceWithPrivate = authService as unknown as { sessionToken?: string; sessionExpiresAt?: Date; widgetKey?: string; origin?: string; }; authServiceWithPrivate.sessionToken = 'expired-token'; authServiceWithPrivate.sessionExpiresAt = new Date(Date.now() - 3600000); // 1 hour ago authServiceWithPrivate.widgetKey = 'pk_test_key'; authServiceWithPrivate.origin = 'https://example.com'; // Mock refresh response const mockResponse = { status: 'success', message: 'Session created successfully', data: { token: 'new-refreshed-token', expires_at: new Date(Date.now() + 3600000).toISOString(), // 1 hour from now session_id: 'new-session-id', }, }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockResponse, } as Response); // getToken() should automatically refresh const token = await authService.getToken(); expect(token).toBe('new-refreshed-token'); expect(mockFetch).toHaveBeenCalled(); expect(authService.isTokenValid()).toBe(true); }); it('should not refresh if token is still valid', async () => { // Set valid token const authServiceWithPrivate = authService as unknown as { sessionToken?: string; sessionExpiresAt?: Date; widgetKey?: string; origin?: string; }; authServiceWithPrivate.sessionToken = 'valid-token'; authServiceWithPrivate.sessionExpiresAt = new Date(Date.now() + 3600000); // 1 hour from now authServiceWithPrivate.widgetKey = 'pk_test_key'; authServiceWithPrivate.origin = 'https://example.com'; const token = await authService.getToken(); expect(token).toBe('valid-token'); expect(mockFetch).not.toHaveBeenCalled(); }); it('should return undefined if refresh fails', async () => { // Set expired token const authServiceWithPrivate = authService as unknown as { sessionToken?: string; sessionExpiresAt?: Date; widgetKey?: string; origin?: string; }; authServiceWithPrivate.sessionToken = 'expired-token'; authServiceWithPrivate.sessionExpiresAt = new Date(Date.now() - 3600000); authServiceWithPrivate.widgetKey = 'pk_test_key'; authServiceWithPrivate.origin = 'https://example.com'; // Mock failed refresh mockFetch.mockRejectedValueOnce(new Error('Network error')); const token = await authService.getToken(); expect(token).toBeUndefined(); expect(mockFetch).toHaveBeenCalled(); }); it('should prevent concurrent refresh attempts', async () => { // Set expired token const authServiceWithPrivate = authService as unknown as { sessionToken?: string; sessionExpiresAt?: Date; widgetKey?: string; origin?: string; }; authServiceWithPrivate.sessionToken = 'expired-token'; authServiceWithPrivate.sessionExpiresAt = new Date(Date.now() - 3600000); authServiceWithPrivate.widgetKey = 'pk_test_key'; authServiceWithPrivate.origin = 'https://example.com'; // Mock slow refresh response const mockResponse = { status: 'success', message: 'Session created successfully', data: { token: 'new-refreshed-token', expires_at: new Date(Date.now() + 3600000).toISOString(), session_id: 'new-session-id', }, }; let resolveFetch: (value: Response) => void; const slowFetchPromise = new Promise(resolve => { resolveFetch = resolve; }); mockFetch.mockReturnValueOnce(slowFetchPromise as Promise); // Start multiple concurrent getToken() calls const tokenPromises = [ authService.getToken(), authService.getToken(), authService.getToken(), ]; // Resolve fetch after a delay setTimeout(() => { resolveFetch!({ ok: true, json: async () => mockResponse, } as Response); }, 100); const tokens = await Promise.all(tokenPromises); // All should return the same refreshed token expect(tokens.every(t => t === 'new-refreshed-token')).toBe(true); // Fetch should only be called once (not 3 times) expect(mockFetch).toHaveBeenCalledTimes(1); }); }); });