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'
);
});
});
});