import React from 'react'; import { render, act, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; import { PersSDKProvider, usePersSDK, PersSDKContext } from '../providers/PersSDKProvider'; // Store SDK instances for event emission let mockSdkInstance: any = null; // Mock the SDK core module BEFORE any imports use it jest.mock('@explorins/pers-sdk/core', () => { const createMockEventEmitter = () => { const subscribers: Array<{ callback: (event: any) => void; options?: any }> = []; return { subscribe: jest.fn((callback: (event: any) => void, options?: any) => { subscribers.push({ callback, options }); return () => { const index = subscribers.findIndex(s => s.callback === callback); if (index > -1) subscribers.splice(index, 1); }; }), emit: (event: any) => { subscribers.forEach(({ callback, options }) => { if (!options?.domains || options.domains.includes(event.domain)) { callback(event); } }); }, _subscribers: subscribers }; }; const MockPersSDK = jest.fn().mockImplementation(() => { const events = createMockEventEmitter(); mockSdkInstance = { events, auth: { ensureValidToken: jest.fn().mockResolvedValue(true) }, users: { getCurrentUser: jest.fn().mockResolvedValue({ id: 'refreshed-user', email: 'refreshed@test.com' }) }, restoreSession: jest.fn().mockResolvedValue(null) }; return mockSdkInstance; }); return { PersSDK: MockPersSDK, DefaultAuthProvider: jest.fn().mockImplementation(() => ({ getToken: jest.fn().mockResolvedValue('mock-token') })) }; }); // Mock platform-specific providers jest.mock('../providers/react-native-http-client', () => ({ ReactNativeHttpClient: jest.fn().mockImplementation(() => ({})) })); jest.mock('../providers/react-native-auth-provider', () => ({ createReactNativeAuthProvider: jest.fn().mockReturnValue({ getToken: jest.fn().mockResolvedValue('mock-token') }) })); jest.mock('../providers/rn-dpop-provider', () => ({ ReactNativeDPoPProvider: jest.fn().mockImplementation(() => ({})) })); // Helper component to access context const TestConsumer: React.FC<{ onContext: (ctx: PersSDKContext) => void }> = ({ onContext }) => { const context = usePersSDK(); React.useEffect(() => { onContext(context); }, [context, onContext]); return
Consumer
; }; // Helper to emit events on the mock SDK const emitAuthEvent = (type: string, details: any = {}) => { if (mockSdkInstance?.events) { mockSdkInstance.events.emit({ type, domain: 'authentication', details }); } }; describe('PersSDKProvider', () => { beforeEach(() => { jest.clearAllMocks(); mockSdkInstance = null; }); describe('initialization', () => { it('should initialize with null SDK when no config provided', async () => { let capturedContext: PersSDKContext | null = null; render( { capturedContext = ctx; }} /> ); await waitFor(() => { expect(capturedContext).not.toBeNull(); }); expect(capturedContext!.sdk).toBeNull(); expect(capturedContext!.isInitialized).toBe(false); expect(capturedContext!.isAuthenticated).toBe(false); expect(capturedContext!.user).toBeNull(); }); it('should auto-initialize when config is provided', async () => { let capturedContext: PersSDKContext | null = null; render( { capturedContext = ctx; }} /> ); await waitFor(() => { expect(capturedContext?.isInitialized).toBe(true); }); expect(capturedContext!.sdk).not.toBeNull(); }); it('should prevent multiple initializations', async () => { const { PersSDK } = require('@explorins/pers-sdk/core'); let capturedContext: PersSDKContext | null = null; render( { capturedContext = ctx; }} /> ); await waitFor(() => { expect(capturedContext?.isInitialized).toBe(true); }); // Try to initialize again - should be ignored await act(async () => { await capturedContext!.initialize({ apiProjectKey: 'different-project' }); }); // Should only have been called once expect(PersSDK).toHaveBeenCalledTimes(1); }); }); describe('event handling', () => { it('should set user on session_restored event', async () => { let capturedContext: PersSDKContext | null = null; const mockUser = { id: 'user-123', email: 'test@example.com' }; render( { capturedContext = ctx; }} /> ); await waitFor(() => { expect(capturedContext?.sdk).not.toBeNull(); }); // Emit session_restored event await act(async () => { emitAuthEvent('session_restored', { user: mockUser }); }); await waitFor(() => { expect(capturedContext?.isAuthenticated).toBe(true); expect(capturedContext?.user).toEqual(mockUser); }); }); it('should set user on login_success event', async () => { let capturedContext: PersSDKContext | null = null; const mockUser = { id: 'user-456', email: 'login@example.com' }; render( { capturedContext = ctx; }} /> ); await waitFor(() => { expect(capturedContext?.sdk).not.toBeNull(); }); await act(async () => { emitAuthEvent('login_success', { user: mockUser }); }); await waitFor(() => { expect(capturedContext?.isAuthenticated).toBe(true); expect(capturedContext?.user).toEqual(mockUser); }); }); it('should clear user on logout_success event', async () => { let capturedContext: PersSDKContext | null = null; const mockUser = { id: 'user-789', email: 'logout@example.com' }; render( { capturedContext = ctx; }} /> ); await waitFor(() => { expect(capturedContext?.sdk).not.toBeNull(); }); // First, log in await act(async () => { emitAuthEvent('login_success', { user: mockUser }); }); await waitFor(() => { expect(capturedContext?.isAuthenticated).toBe(true); }); // Then logout await act(async () => { emitAuthEvent('logout_success', {}); }); await waitFor(() => { expect(capturedContext?.isAuthenticated).toBe(false); expect(capturedContext?.user).toBeNull(); }); }); it('should clear user on session_restoration_failed event', async () => { let capturedContext: PersSDKContext | null = null; render( { capturedContext = ctx; }} /> ); await waitFor(() => { expect(capturedContext?.sdk).not.toBeNull(); }); // Set initial authenticated state await act(async () => { capturedContext!.setAuthenticationState({ id: 'temp-user' } as any, true); }); await waitFor(() => { expect(capturedContext?.isAuthenticated).toBe(true); }); // Emit session_restoration_failed await act(async () => { emitAuthEvent('session_restoration_failed', { error: 'Token expired' }); }); await waitFor(() => { expect(capturedContext?.isAuthenticated).toBe(false); expect(capturedContext?.user).toBeNull(); }); }); it('should NOT respond to AUTH_FAILED code (dead code was removed)', async () => { // This test verifies that we no longer check for event.code === 'AUTH_FAILED' // which was dead code that never triggered let capturedContext: PersSDKContext | null = null; const mockUser = { id: 'user-stay', email: 'stay@example.com' }; render( { capturedContext = ctx; }} /> ); await waitFor(() => { expect(capturedContext?.sdk).not.toBeNull(); }); // Log in first await act(async () => { emitAuthEvent('login_success', { user: mockUser }); }); await waitFor(() => { expect(capturedContext?.isAuthenticated).toBe(true); }); // Emit an event with code: 'AUTH_FAILED' - this should NOT clear user // because we removed the dead code that checked for this await act(async () => { if (mockSdkInstance?.events) { mockSdkInstance.events.emit({ type: 'some_other_event', code: 'AUTH_FAILED', // This was the dead code path we removed domain: 'authentication', details: {} }); } }); // User should STILL be authenticated - the code: 'AUTH_FAILED' check is gone expect(capturedContext!.isAuthenticated).toBe(true); expect(capturedContext!.user).toEqual(mockUser); }); }); describe('usePersSDK hook', () => { it('should throw error when used outside provider', () => { // Suppress the expected error const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); const TestComponent = () => { usePersSDK(); return null; }; expect(() => render()).toThrow( 'usePersSDK must be used within a PersSDKProvider' ); spy.mockRestore(); }); }); describe('context methods', () => { it('should allow manual authentication state updates', async () => { let capturedContext: PersSDKContext | null = null; const mockUser = { id: 'manual-user', email: 'manual@example.com' }; render( { capturedContext = ctx; }} /> ); await waitFor(() => { expect(capturedContext?.isInitialized).toBe(true); }); await act(async () => { capturedContext!.setAuthenticationState(mockUser as any, true); }); await waitFor(() => { expect(capturedContext?.isAuthenticated).toBe(true); expect(capturedContext?.user).toEqual(mockUser); }); }); it('should refresh user data from SDK', async () => { let capturedContext: PersSDKContext | null = null; render( { capturedContext = ctx; }} /> ); await waitFor(() => { expect(capturedContext?.isInitialized).toBe(true); }); await act(async () => { await capturedContext!.refreshUserData(); }); await waitFor(() => { expect(capturedContext?.user).toEqual({ id: 'refreshed-user', email: 'refreshed@test.com' }); }); }); it('should throw error when refreshUserData called before init', async () => { let capturedContext: PersSDKContext | null = null; render( { capturedContext = ctx; }} /> ); await waitFor(() => { expect(capturedContext).not.toBeNull(); }); await expect(capturedContext!.refreshUserData()).rejects.toThrow( 'SDK not initialized' ); }); it('should throw error when restoreSession called before init', async () => { let capturedContext: PersSDKContext | null = null; render( { capturedContext = ctx; }} /> ); await waitFor(() => { expect(capturedContext).not.toBeNull(); }); await expect(capturedContext!.restoreSession()).rejects.toThrow( 'SDK not initialized' ); }); }); });