UNPKG

@reown/appkit-controllers

Version:

The full stack toolkit to build onchain app UX.

914 lines (913 loc) • 43.6 kB
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { NetworkUtil, SafeLocalStorage, SafeLocalStorageKeys } from '@reown/appkit-common'; import { ApiController, BlockchainApiController, ChainController } from '@reown/appkit-controllers'; import { ReownAuthentication } from '@reown/appkit-controllers/features'; import { extendedMainnet, mockChainControllerState, mockSession } from '@reown/appkit-controllers/testing'; vi.useFakeTimers({ now: new Date('2024-12-05T16:02:32.905Z') }); vi.stubGlobal('localStorage', { getItem: vi.fn(), setItem: vi.fn(), removeItem: vi.fn() }); const mocks = { mockFetchResponse: (response, ok = true) => { return { ok, json: async () => response, text: async () => (typeof response === 'string' ? response : JSON.stringify(response)), headers: { get: () => 'application/json' } }; }, createMockJWT: (payload) => { // Create a mock JWT token with proper format (header.payload.signature) const header = { alg: 'HS256', typ: 'JWT' }; const encodedHeader = btoa(JSON.stringify(header)); const encodedPayload = btoa(JSON.stringify(payload)); const signature = 'mock_signature'; return `${encodedHeader}.${encodedPayload}.${signature}`; } }; describe.each([ { namespace: 'eip155', id: 1, address: '0x1234567890abcdef1234567890abcdef12345678' }, { namespace: 'solana', id: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', address: '2VqKhjZ766ZN3uBtBpb7Ls3cN4HrocP1rzxzekhVEgpU' } ])('ReownAuthentication - $namespace', ({ namespace, id, address }) => { let siwx; let mockJWT; beforeAll(() => { global.fetch = vi.fn(); // Mock document.location properly Object.defineProperty(global, 'document', { value: { location: { host: 'mocked.com', href: 'http://mocked.com/' } }, writable: true }); }); beforeEach(() => { siwx = new ReownAuthentication(); // Create a shared mock JWT for all tests mockJWT = mocks.createMockJWT({ aud: 'test-audience', iss: 'test-issuer', exp: Math.floor(Date.now() / 1000) + 3600, projectIdKey: 'test-project-id', sub: 'test-subject', address: address, chainId: namespace === 'eip155' ? id : id.toString(), chainNamespace: namespace, caip2Network: `${namespace}:${id}`, uri: 'http://mocked.com/', domain: 'mocked.com', projectUuid: 'test-project-uuid', profileUuid: 'test-profile-uuid', nonce: 'mock_nonce' }); }); afterAll(() => { vi.clearAllMocks(); }); describe('createMessage', () => { beforeEach(() => { vi.spyOn(ChainController, 'getAllRequestedCaipNetworks').mockReturnValue([ { id: 1, name: 'Ethereum Mainnet', chainNamespace: 'eip155', caipNetworkId: `${namespace}:${id}` }, { id: 2, name: 'Solana', chainNamespace: 'solana', caipNetworkId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' } ]); }); it('creates a message', async () => { const fetchSpy = vi.spyOn(global, 'fetch'); const setItemSpy = vi.spyOn(localStorage, 'setItem'); fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse({ token: 'mock_token', nonce: 'mock_nonce' })); const message = await siwx.createMessage({ accountAddress: address, chainId: `${namespace}:${id}` }); expect(message).toEqual({ accountAddress: address, chainId: `${namespace}:${id}`, domain: 'mocked.com', expirationTime: undefined, issuedAt: '2024-12-05T16:02:32.905Z', nonce: 'mock_nonce', notBefore: undefined, requestId: undefined, resources: undefined, statement: undefined, toString: expect.any(Function), uri: 'http://mocked.com/', version: '1' }); const networkName = NetworkUtil.getNetworkNameByCaipNetworkId(ChainController.getAllRequestedCaipNetworks(), `${namespace}:${id}`); expect(message.toString()) .toBe(`mocked.com wants you to sign in with your ${networkName} account: ${address} URI: http://mocked.com/ Version: 1 Chain ID: ${namespace}:${id} Nonce: mock_nonce Issued At: 2024-12-05T16:02:32.905Z`); expect(fetchSpy).toHaveBeenCalledWith(new URL('https://api.web3modal.org/auth/v1/nonce?projectId=&st=appkit&sv=html-wagmi-undefined'), { body: undefined, headers: undefined, method: 'GET' }); expect(setItemSpy).toHaveBeenCalledWith('@appkit/siwx-nonce-token', 'mock_token'); }); it('should throw an text error if response is not json', async () => { const fetchSpy = vi.spyOn(global, 'fetch'); fetchSpy.mockResolvedValueOnce({ ok: false, headers: { get: () => 'text/plain' }, text: async () => 'mock_error' }); await expect(siwx.createMessage({ accountAddress: address, chainId: `${namespace}:${id}` })).rejects.toThrowError('mock_error'); }); it('should use default domain and uri if document is not available', async () => { const documentSpy = vi.spyOn(global, 'document', 'get').mockReturnValue(undefined); siwx = new ReownAuthentication(); vi.spyOn(global, 'fetch').mockResolvedValueOnce(mocks.mockFetchResponse({ token: 'mock_token', nonce: 'mock_nonce' })); const message = await siwx.createMessage({ accountAddress: address, chainId: `${namespace}:${id}` }); expect(message.domain).toBe('Unknown Domain'); expect(message.uri).toBe('Unknown URI'); documentSpy.mockRestore(); }); }); describe('addSession', () => { it('adds a session', async () => { const fetchSpy = vi.spyOn(global, 'fetch'); const setItemSpy = vi.spyOn(localStorage, 'setItem'); vi.spyOn(BlockchainApiController.state, 'clientId', 'get').mockReturnValueOnce(null); vi.spyOn(localStorage, 'getItem').mockReturnValueOnce('mock_nonce_token'); fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse({ token: mockJWT })); const session = mockSession({ data: { accountAddress: address, chainId: `${namespace}:${id}` } }); await siwx.addSession(session); expect(fetchSpy).toHaveBeenCalledWith(new URL('https://api.web3modal.org/auth/v1/authenticate?projectId=&st=appkit&sv=html-wagmi-undefined'), { body: `{"data":{"domain":"example.com","accountAddress":"${address}","statement":"This is a statement","chainId":"${namespace}:${id}","uri":"siwx://example.com","version":"1","nonce":"123"},"message":"Hello AppKit!","signature":"0x3c70e0a2d87f677dc0c3faf98fdf6313e99a3d9191bb79f7ecfce0c2cf46b7b33fd4c4bb83bca82fe872e35963382027d0d18018342d7dc36a675918cb73e9061c","clientId":null}`, headers: { 'x-nonce-jwt': 'Bearer mock_nonce_token' }, method: 'POST' }); expect(setItemSpy).toHaveBeenCalledWith('@appkit/siwx-auth-token', mockJWT); }); it('should use correct client id', async () => { const fetchSpy = vi.spyOn(global, 'fetch'); vi.spyOn(localStorage, 'getItem').mockReturnValueOnce('mock_nonce_token'); vi.spyOn(BlockchainApiController.state, 'clientId', 'get').mockReturnValueOnce('mock_client_id'); fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse({ token: mockJWT })); const session = mockSession({ data: { accountAddress: address, chainId: `${namespace}:${id}` } }); await siwx.addSession(session); expect(fetchSpy).toHaveBeenCalledWith(new URL('https://api.web3modal.org/auth/v1/authenticate?projectId=&st=appkit&sv=html-wagmi-undefined'), { body: `{"data":{"domain":"example.com","accountAddress":"${address}","statement":"This is a statement","chainId":"${namespace}:${id}","uri":"siwx://example.com","version":"1","nonce":"123"},"message":"Hello AppKit!","signature":"0x3c70e0a2d87f677dc0c3faf98fdf6313e99a3d9191bb79f7ecfce0c2cf46b7b33fd4c4bb83bca82fe872e35963382027d0d18018342d7dc36a675918cb73e9061c","clientId":"mock_client_id"}`, headers: { 'x-nonce-jwt': 'Bearer mock_nonce_token' }, method: 'POST' }); }); it.each([ { walletInfo: { name: 'mock_wallet_name', icon: 'mock_wallet_icon' }, expectedBody: `{"data":{"domain":"example.com","accountAddress":"${address}","statement":"This is a statement","chainId":"${namespace}:${id}","uri":"siwx://example.com","version":"1","nonce":"123"},"message":"Hello AppKit!","signature":"0x3c70e0a2d87f677dc0c3faf98fdf6313e99a3d9191bb79f7ecfce0c2cf46b7b33fd4c4bb83bca82fe872e35963382027d0d18018342d7dc36a675918cb73e9061c","walletInfo":{"type":"unknown","name":"mock_wallet_name","icon":"mock_wallet_icon"}}` }, { walletInfo: { type: 'ANNOUNCED', name: 'mock_wallet_name', icon: 'mock_wallet_icon' }, expectedBody: `{"data":{"domain":"example.com","accountAddress":"${address}","statement":"This is a statement","chainId":"${namespace}:${id}","uri":"siwx://example.com","version":"1","nonce":"123"},"message":"Hello AppKit!","signature":"0x3c70e0a2d87f677dc0c3faf98fdf6313e99a3d9191bb79f7ecfce0c2cf46b7b33fd4c4bb83bca82fe872e35963382027d0d18018342d7dc36a675918cb73e9061c","walletInfo":{"type":"extension","name":"mock_wallet_name","icon":"mock_wallet_icon"}}` }, { walletInfo: { type: 'WALLET_CONNECT', name: 'mock_wallet_name', icon: 'mock_wallet_icon' }, expectedBody: `{"data":{"domain":"example.com","accountAddress":"${address}","statement":"This is a statement","chainId":"${namespace}:${id}","uri":"siwx://example.com","version":"1","nonce":"123"},"message":"Hello AppKit!","signature":"0x3c70e0a2d87f677dc0c3faf98fdf6313e99a3d9191bb79f7ecfce0c2cf46b7b33fd4c4bb83bca82fe872e35963382027d0d18018342d7dc36a675918cb73e9061c","walletInfo":{"type":"walletconnect","name":"mock_wallet_name","icon":"mock_wallet_icon"}}` }, { walletInfo: { type: 'AUTH', name: 'AUTH', social: 'google', identifier: 'mock_identifier' }, expectedBody: `{"data":{"domain":"example.com","accountAddress":"${address}","statement":"This is a statement","chainId":"${namespace}:${id}","uri":"siwx://example.com","version":"1","nonce":"123"},"message":"Hello AppKit!","signature":"0x3c70e0a2d87f677dc0c3faf98fdf6313e99a3d9191bb79f7ecfce0c2cf46b7b33fd4c4bb83bca82fe872e35963382027d0d18018342d7dc36a675918cb73e9061c","walletInfo":{"type":"social","social":"google","identifier":"mock_identifier"}}` } ])('should use correct wallet info', async ({ walletInfo, expectedBody }) => { const fetchSpy = vi.spyOn(global, 'fetch'); vi.spyOn(localStorage, 'getItem').mockReturnValueOnce('mock_nonce_token'); vi.spyOn(ChainController, 'getAccountData').mockReturnValueOnce({ connectedWalletInfo: walletInfo }); fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse({ token: mockJWT })); const session = mockSession({ data: { accountAddress: address, chainId: `${namespace}:${id}` } }); await siwx.addSession(session); expect(fetchSpy).toHaveBeenCalledWith(new URL('https://api.web3modal.org/auth/v1/authenticate?projectId=&st=appkit&sv=html-wagmi-undefined'), { body: expectedBody, headers: { 'x-nonce-jwt': 'Bearer mock_nonce_token' }, method: 'POST' }); }); }); describe('getSessions', () => { beforeEach(() => { vi.spyOn(localStorage, 'getItem').mockReturnValue(mockJWT); }); afterEach(() => { vi.spyOn(localStorage, 'getItem').mockRestore(); }); it('gets sessions', async () => { const fetchSpy = vi.spyOn(global, 'fetch'); fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse({ address, chainId: id, caip2Network: `${namespace}:${id}` })); const sessions = await siwx.getSessions(`${namespace}:${id}`, address); expect(sessions).toEqual([ { data: { accountAddress: address, chainId: `${namespace}:${id}` }, message: '', signature: '' } ]); expect(fetchSpy).toHaveBeenCalledWith(new URL('https://api.web3modal.org/auth/v1/me?projectId=&st=appkit&sv=html-wagmi-undefined'), { body: undefined, headers: { Authorization: `Bearer ${mockJWT}` }, method: 'GET' }); }); it('gets sessions when address is not lowercased', async () => { const fetchSpy = vi.spyOn(global, 'fetch'); fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse({ address, chainId: id, caip2Network: `${namespace}:${id}` })); const sessions = await siwx.getSessions(`${namespace}:${id}`, address); expect(sessions).toEqual([ { data: { accountAddress: address, chainId: `${namespace}:${id}` }, message: '', signature: '' } ]); }); it('returns empty array if session is not found', async () => { const fetchSpy = vi.spyOn(global, 'fetch'); fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse({ address: 'different_address', chainId: id })); await expect(siwx.getSessions(`${namespace}:${id}`, address)).resolves.toEqual([]); fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse({ address, chainId: 2 })); await expect(siwx.getSessions(`${namespace}:${id}`, address)).resolves.toEqual([]); fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse(null)); await expect(siwx.getSessions(`${namespace}:${id}`, address)).resolves.toEqual([]); fetchSpy.mockRejectedValueOnce(new Error()); await expect(siwx.getSessions(`${namespace}:${id}`, address)).resolves.toEqual([]); }); it('should not request session if no auth token is set', async () => { vi.spyOn(localStorage, 'getItem').mockReturnValueOnce(null); const fetchSpy = vi.spyOn(global, 'fetch'); await expect(siwx.getSessions(`${namespace}:${id}`, address)).resolves.toEqual([]); expect(fetchSpy).not.toHaveBeenCalled(); }); }); describe('revokeSession', () => { it('revokes a session', async () => { const removeItemSpy = vi.spyOn(localStorage, 'removeItem'); await siwx.revokeSession(`${namespace}:${id}`, '0x1234567890abcdef1234567890abcdef12345678'); expect(removeItemSpy).toHaveBeenCalledWith('@appkit/siwx-auth-token'); }); }); describe('setSessions', () => { it('clears storage token if sessions are empty', async () => { const removeItemSpy = vi.spyOn(localStorage, 'removeItem'); await siwx.setSessions([]); expect(removeItemSpy).toHaveBeenCalledWith('@appkit/siwx-auth-token'); }); it('adds a session with default first item', async () => { const addSessionSpy = vi.spyOn(siwx, 'addSession'); addSessionSpy.mockResolvedValueOnce(); const session = mockSession({ data: { accountAddress: address, chainId: `${namespace}:${id}` } }); await siwx.setSessions([session]); expect(addSessionSpy).toHaveBeenCalledWith(session); }); it('should use the correct session if there are multiple sessions', async () => { const addSessionSpy = vi.spyOn(siwx, 'addSession'); addSessionSpy.mockResolvedValueOnce(); mockChainControllerState({ activeCaipNetwork: { ...extendedMainnet, id: 2, caipNetworkId: 'eip155:2' } }); const session = mockSession({ data: { accountAddress: address, chainId: `${namespace}:${id}` } }); const session2 = mockSession({ data: { accountAddress: address, chainId: 'eip155:2' } }); await siwx.setSessions([session, session2]); expect(addSessionSpy).toHaveBeenCalledWith(session2); }); }); describe('getRequired', () => { it('should return true for getRequired() by default', () => { expect(siwx.getRequired()).toBe(true); }); it('should return false for getRequired()', () => { siwx = new ReownAuthentication({ required: false }); expect(siwx.getRequired()).toBe(false); }); }); describe('events', () => { it('should register event listeners with on() method', () => { const callback = vi.fn(); const unsubscribe = siwx.on('sessionChanged', callback); expect(unsubscribe).toBeInstanceOf(Function); }); it('should emit session-changed event when a session is added', async () => { const callback = vi.fn(); siwx.on('sessionChanged', callback); vi.spyOn(localStorage, 'getItem').mockReturnValueOnce('mock_nonce_token'); vi.spyOn(global, 'fetch').mockResolvedValueOnce(mocks.mockFetchResponse({ token: mockJWT })); const session = mockSession(); await siwx.addSession(session); expect(callback).toHaveBeenCalledWith(session); }); it('should emit session-changed event when sessions are retrieved', async () => { const callback = vi.fn(); siwx.on('sessionChanged', callback); vi.spyOn(localStorage, 'getItem').mockReturnValueOnce(mockJWT); vi.spyOn(global, 'fetch').mockResolvedValueOnce(mocks.mockFetchResponse({ address: address, chainId: id, caip2Network: `${namespace}:${id}` })); await siwx.getSessions(`${namespace}:${id}`, address); expect(callback).toHaveBeenCalledWith({ data: { accountAddress: address, chainId: `${namespace}:${id}` }, message: '', signature: '' }); }); it('should emit session-changed event with undefined when a session is revoked', async () => { const callback = vi.fn(); siwx.on('sessionChanged', callback); await siwx.revokeSession(`${namespace}:${id}`, address); expect(callback).toHaveBeenCalledWith(undefined); }); it('should emit session-changed event with undefined when sessions are cleared', async () => { const callback = vi.fn(); siwx.on('sessionChanged', callback); await siwx.setSessions([]); expect(callback).toHaveBeenCalledWith(undefined); }); it('should properly unsubscribe listener when unsubscribe function is called', async () => { const callback = vi.fn(); const unsubscribe = siwx.on('sessionChanged', callback); unsubscribe(); vi.spyOn(localStorage, 'getItem').mockReturnValueOnce('mock_nonce_token'); vi.spyOn(global, 'fetch').mockResolvedValueOnce(mocks.mockFetchResponse({ token: mockJWT })); await siwx.addSession(mockSession()); expect(callback).not.toHaveBeenCalled(); }); it('should remove all listeners when removeAllListeners is called', async () => { const callback1 = vi.fn(); const callback2 = vi.fn(); siwx.on('sessionChanged', callback1); siwx.on('sessionChanged', callback2); siwx.removeAllListeners(); vi.spyOn(localStorage, 'getItem').mockReturnValueOnce('mock_nonce_token'); vi.spyOn(global, 'fetch').mockResolvedValueOnce(mocks.mockFetchResponse({ token: mockJWT })); await siwx.addSession(mockSession()); expect(callback1).not.toHaveBeenCalled(); expect(callback2).not.toHaveBeenCalled(); }); }); describe('getSessionAccount', () => { it('should throw an error if not authenticated', async () => { vi.spyOn(localStorage, 'getItem').mockReturnValueOnce(null); await expect(siwx.getSessionAccount()).rejects.toThrow('Not authenticated'); }); it('should return session account data when authenticated', async () => { vi.spyOn(localStorage, 'getItem').mockReturnValueOnce(mockJWT); const fetchSpy = vi.spyOn(global, 'fetch'); const mockAccountData = { address: '0x1234567890abcdef1234567890abcdef12345678', chainId: 1, aud: 'test-aud', iss: 'test-iss', exp: 1000000000, projectIdKey: 'test-project-id-key', sub: 'test-sub', uri: 'http://mocked.com/', domain: 'mocked.com', projectUuid: 'test-project-uuid', profileUuid: 'test-profile-uuid', nonce: 'test-nonce', appKitAccount: { uuid: 'test-uuid', caip2_chain: 'eip155:1', address: '0x1234567890abcdef1234567890abcdef12345678', profile_uuid: 'test-profile-uuid', created_at: '2023-01-01T00:00:00.000Z', is_main_account: true, verification_status: null, connection_method: null, metadata: {}, last_signed_in_at: '2023-01-01T00:00:00.000Z', signed_up_at: '2023-01-01T00:00:00.000Z', updated_at: '2023-01-01T00:00:00.000Z' } }; fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse(mockAccountData)); const result = await siwx.getSessionAccount(); expect(result).toEqual(mockAccountData); expect(fetchSpy).toHaveBeenCalledWith(new URL('https://api.web3modal.org/auth/v1/me?projectId=&st=appkit&sv=html-wagmi-undefined&includeAppKitAccount=true'), { body: undefined, headers: { Authorization: `Bearer ${mockJWT}` }, method: 'GET' }); }); it('should handle request errors when fetching session account', async () => { vi.spyOn(localStorage, 'getItem').mockReturnValueOnce(mockJWT); const fetchSpy = vi.spyOn(global, 'fetch'); // Simulate a non-JSON response error fetchSpy.mockResolvedValueOnce({ headers: { get: () => 'text/plain' }, text: async () => 'Error fetching session account' }); await expect(siwx.getSessionAccount()).rejects.toThrow('Error fetching session account'); }); }); describe('setSessionAccountMetadata', () => { it('should throw an error if not authenticated', async () => { vi.spyOn(localStorage, 'getItem').mockReturnValueOnce(null); await expect(siwx.setSessionAccountMetadata({ test: 'value' })).rejects.toThrow('Not authenticated'); }); it('should send metadata to API when authenticated', async () => { vi.spyOn(localStorage, 'getItem').mockReturnValueOnce(mockJWT); const fetchSpy = vi.spyOn(global, 'fetch'); const metadata = { displayName: 'Test User', avatar: 'https://example.com/avatar.png', customField: 'custom value' }; fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse({ success: true })); await siwx.setSessionAccountMetadata(metadata); expect(fetchSpy).toHaveBeenCalledWith(new URL('https://api.web3modal.org/auth/v1/account-metadata?projectId=&st=appkit&sv=html-wagmi-undefined'), { body: JSON.stringify({ metadata }), headers: { Authorization: `Bearer ${mockJWT}` }, method: 'PUT' }); }); it('should handle request errors when updating metadata', async () => { vi.spyOn(localStorage, 'getItem').mockReturnValueOnce(mockJWT); const fetchSpy = vi.spyOn(global, 'fetch'); // Simulate a non-JSON response error fetchSpy.mockResolvedValueOnce({ headers: { get: () => 'text/plain' }, text: async () => 'Error updating metadata' }); await expect(siwx.setSessionAccountMetadata({ test: 'value' })).rejects.toThrow('Error updating metadata'); }); it('should pass empty object as metadata if none provided', async () => { vi.spyOn(localStorage, 'getItem').mockReturnValueOnce(mockJWT); const fetchSpy = vi.spyOn(global, 'fetch'); fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse({ success: true })); await siwx.setSessionAccountMetadata(); expect(fetchSpy).toHaveBeenCalledWith(new URL('https://api.web3modal.org/auth/v1/account-metadata?projectId=&st=appkit&sv=html-wagmi-undefined'), { body: JSON.stringify({ metadata: null }), headers: { Authorization: `Bearer ${mockJWT}` }, method: 'PUT' }); }); }); }); describe('Constructor with custom parameters', () => { it('should use custom storage keys when provided', () => { const customAuth = new ReownAuthentication({ localAuthStorageKey: '@custom/auth-token', localNonceStorageKey: '@custom/nonce-token', required: false }); expect(customAuth.getRequired()).toBe(false); // Test that custom keys are used internally (we can verify through the behavior) vi.spyOn(SafeLocalStorage, 'getItem'); customAuth['getStorageToken']('@custom/auth-token'); expect(SafeLocalStorage.getItem).toHaveBeenCalledWith('@custom/auth-token'); }); it('should use default values when no parameters provided', () => { const defaultAuth = new ReownAuthentication(); expect(defaultAuth.getRequired()).toBe(true); }); }); describe('Email OTP functionality', () => { let siwx; beforeEach(() => { siwx = new ReownAuthentication(); }); describe('requestEmailOtp', () => { it('should request email OTP and set otpUuid', async () => { const fetchSpy = vi.spyOn(global, 'fetch'); const mockUuid = 'mock-otp-uuid-123'; fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse({ uuid: mockUuid })); const result = await siwx.requestEmailOtp({ email: 'test@example.com', account: 'eip155:1:0x1234567890abcdef1234567890abcdef12345678' }); expect(result).toEqual({ uuid: mockUuid }); expect(fetchSpy).toHaveBeenCalledWith(new URL('https://api.web3modal.org/auth/v1/otp?projectId=&st=appkit&sv=html-wagmi-undefined'), { method: 'POST', body: JSON.stringify({ email: 'test@example.com', account: 'eip155:1:0x1234567890abcdef1234567890abcdef12345678' }), headers: undefined }); // Verify that otpUuid is set internally expect(siwx['otpUuid']).toBe(mockUuid); // Verify that messenger resources are updated expect(siwx['messenger'].resources).toEqual(['email:test@example.com']); }); it('should handle null uuid response', async () => { const fetchSpy = vi.spyOn(global, 'fetch'); fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse({ uuid: null })); const result = await siwx.requestEmailOtp({ email: 'test@example.com', account: 'eip155:1:0x1234567890abcdef1234567890abcdef12345678' }); expect(result).toEqual({ uuid: null }); expect(siwx['otpUuid']).toBe(null); }); it('should handle API errors when requesting OTP', async () => { const fetchSpy = vi.spyOn(global, 'fetch'); fetchSpy.mockResolvedValueOnce({ ok: false, text: async () => 'OTP request failed' }); await expect(siwx.requestEmailOtp({ email: 'test@example.com', account: 'eip155:1:0x1234567890abcdef1234567890abcdef12345678' })).rejects.toThrow('OTP request failed'); }); }); describe('confirmEmailOtp', () => { beforeEach(() => { // Set otpUuid to simulate having requested OTP siwx['otpUuid'] = 'mock-otp-uuid-123'; }); it('should confirm email OTP with correct headers', async () => { const fetchSpy = vi.spyOn(global, 'fetch'); fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse(null)); await siwx.confirmEmailOtp({ code: '123456' }); expect(fetchSpy).toHaveBeenCalledWith(new URL('https://api.web3modal.org/auth/v1/otp?projectId=&st=appkit&sv=html-wagmi-undefined'), { method: 'PUT', body: JSON.stringify({ code: '123456' }), headers: { 'x-otp': 'mock-otp-uuid-123' } }); }); it('should not include OTP header when otpUuid is null', async () => { siwx['otpUuid'] = null; const fetchSpy = vi.spyOn(global, 'fetch'); fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse(null)); await siwx.confirmEmailOtp({ code: '123456' }); expect(fetchSpy).toHaveBeenCalledWith(new URL('https://api.web3modal.org/auth/v1/otp?projectId=&st=appkit&sv=html-wagmi-undefined'), { method: 'PUT', body: JSON.stringify({ code: '123456' }), headers: {} }); }); it('should handle API errors when confirming OTP', async () => { const fetchSpy = vi.spyOn(global, 'fetch'); fetchSpy.mockResolvedValueOnce({ ok: false, text: async () => 'Invalid OTP code' }); await expect(siwx.confirmEmailOtp({ code: '123456' })).rejects.toThrow('Invalid OTP code'); }); }); it('should clear otpUuid when session is added', async () => { // Set initial otpUuid siwx['otpUuid'] = 'mock-otp-uuid-123'; const fetchSpy = vi.spyOn(global, 'fetch'); vi.spyOn(localStorage, 'getItem').mockReturnValueOnce('mock_nonce_token'); const mockJWT = mocks.createMockJWT({ aud: 'test-audience', iss: 'test-issuer', exp: Math.floor(Date.now() / 1000) + 3600, projectIdKey: 'test-project-id', sub: 'test-subject', address: '0x1234567890abcdef1234567890abcdef12345678', chainId: 1, chainNamespace: 'eip155', caip2Network: 'eip155:1', uri: 'http://mocked.com/', domain: 'mocked.com', projectUuid: 'test-project-uuid', profileUuid: 'test-profile-uuid', nonce: 'mock_nonce' }); fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse({ token: mockJWT })); const session = mockSession({ data: { accountAddress: '0x1234567890abcdef1234567890abcdef12345678', chainId: 'eip155:1' } }); await siwx.addSession(session); // Verify otpUuid is cleared expect(siwx['otpUuid']).toBe(null); }); }); describe('JWT Decode functionality (tested indirectly)', () => { let siwx; beforeEach(() => { siwx = new ReownAuthentication(); }); it('should decode valid JWT token correctly through addSession', async () => { const payload = { aud: 'test-audience', iss: 'test-issuer', exp: 1234567890, projectIdKey: 'test-project-id', sub: 'test-subject', address: '0x1234567890abcdef1234567890abcdef12345678', chainId: 1, chainNamespace: 'eip155', caip2Network: 'eip155:1', uri: 'http://test.com/', domain: 'test.com', projectUuid: 'test-project-uuid', profileUuid: 'test-profile-uuid', nonce: 'test-nonce', email: 'test@example.com' }; const mockJWT = mocks.createMockJWT(payload); const setAccountPropSpy = vi.spyOn(ChainController, 'setAccountProp'); const fetchSpy = vi.spyOn(global, 'fetch'); vi.spyOn(localStorage, 'getItem').mockReturnValueOnce('mock_nonce_token'); fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse({ token: mockJWT })); const session = mockSession({ data: { accountAddress: '0x1234567890abcdef1234567890abcdef12345678', chainId: 'eip155:1' } }); await siwx.addSession(session); // Verify that the JWT was decoded correctly by checking if setAppKitAccountUser was called expect(setAccountPropSpy).toHaveBeenCalledWith('user', { email: 'test@example.com' }, 'eip155'); setAccountPropSpy.mockRestore(); }); it('should handle invalid JWT format through addSession', async () => { const fetchSpy = vi.spyOn(global, 'fetch'); vi.spyOn(localStorage, 'getItem').mockReturnValueOnce('mock_nonce_token'); fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse({ token: 'invalid-token' })); const session = mockSession({ data: { accountAddress: '0x1234567890abcdef1234567890abcdef12345678', chainId: 'eip155:1' } }); await expect(siwx.addSession(session)).rejects.toThrow('Invalid token'); }); it('should handle JWT with missing parts through addSession', async () => { const fetchSpy = vi.spyOn(global, 'fetch'); vi.spyOn(localStorage, 'getItem').mockReturnValueOnce('mock_nonce_token'); fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse({ token: 'header.payload' })); const session = mockSession({ data: { accountAddress: '0x1234567890abcdef1234567890abcdef12345678', chainId: 'eip155:1' } }); await expect(siwx.addSession(session)).rejects.toThrow('Invalid token'); }); it('should handle JWT with malformed payload through addSession', async () => { const fetchSpy = vi.spyOn(global, 'fetch'); vi.spyOn(localStorage, 'getItem').mockReturnValueOnce('mock_nonce_token'); // Create a token with malformed base64 payload const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' })); const malformedPayload = 'invalid-base64-!!!'; const signature = 'signature'; const malformedToken = `${header}.${malformedPayload}.${signature}`; fetchSpy.mockResolvedValueOnce(mocks.mockFetchResponse({ token: malformedToken })); const session = mockSession({ data: { accountAddress: '0x1234567890abcdef1234567890abcdef12345678', chainId: 'eip155:1' } }); await expect(siwx.addSession(session)).rejects.toThrow(); }); }); describe('setAppKitAccountUser functionality', () => { let siwx; let setAccountPropSpy; beforeEach(() => { siwx = new ReownAuthentication(); setAccountPropSpy = vi.spyOn(ChainController, 'setAccountProp'); }); afterEach(() => { setAccountPropSpy.mockRestore(); }); it('should set user email for all chain namespaces when email exists', () => { const mockSession = { aud: 'test-audience', iss: 'test-issuer', exp: 1234567890, projectIdKey: 'test-project-id', sub: 'test-subject', address: '0x1234567890abcdef1234567890abcdef12345678', chainId: 1, chainNamespace: 'eip155', caip2Network: 'eip155:1', uri: 'http://test.com/', domain: 'test.com', projectUuid: 'test-project-uuid', profileUuid: 'test-profile-uuid', nonce: 'test-nonce', email: 'test@example.com' }; siwx['setAppKitAccountUser'](mockSession); // Should be called for each chain namespace in AppKitConstantsUtil.CHAIN expect(setAccountPropSpy).toHaveBeenCalledWith('user', { email: 'test@example.com' }, 'eip155'); expect(setAccountPropSpy).toHaveBeenCalledWith('user', { email: 'test@example.com' }, 'solana'); expect(setAccountPropSpy).toHaveBeenCalledWith('user', { email: 'test@example.com' }, 'bip122'); }); it('should not set user when email is undefined', () => { const mockSession = { aud: 'test-audience', iss: 'test-issuer', exp: 1234567890, projectIdKey: 'test-project-id', sub: 'test-subject', address: '0x1234567890abcdef1234567890abcdef12345678', chainId: 1, chainNamespace: 'eip155', caip2Network: 'eip155:1', uri: 'http://test.com/', domain: 'test.com', projectUuid: 'test-project-uuid', profileUuid: 'test-profile-uuid', nonce: 'test-nonce' // No email property }; siwx['setAppKitAccountUser'](mockSession); expect(setAccountPropSpy).not.toHaveBeenCalled(); }); it('should not set user when email is empty string', () => { const mockSession = { aud: 'test-audience', iss: 'test-issuer', exp: 1234567890, projectIdKey: 'test-project-id', sub: 'test-subject', address: '0x1234567890abcdef1234567890abcdef12345678', chainId: 1, chainNamespace: 'eip155', caip2Network: 'eip155:1', uri: 'http://test.com/', domain: 'test.com', projectUuid: 'test-project-uuid', profileUuid: 'test-profile-uuid', nonce: 'test-nonce', email: '' }; siwx['setAppKitAccountUser'](mockSession); expect(setAccountPropSpy).not.toHaveBeenCalled(); }); }); describe('Edge cases and error handling', () => { let siwx; beforeEach(() => { siwx = new ReownAuthentication(); }); it('should handle non-JSON response correctly', async () => { const fetchSpy = vi.spyOn(global, 'fetch'); fetchSpy.mockResolvedValueOnce({ ok: true, headers: { get: () => 'text/plain' }, json: async () => { throw new Error('Not JSON'); } }); const result = await siwx['request']({ method: 'GET', key: 'nonce' }); expect(result).toBe(null); }); it('should handle fetch errors gracefully', async () => { const fetchSpy = vi.spyOn(global, 'fetch'); fetchSpy.mockRejectedValueOnce(new Error('Network error')); await expect(siwx['request']({ method: 'GET', key: 'nonce' })).rejects.toThrow('Network error'); }); it('should handle missing SDK properties', () => { const originalGetSdkProperties = ApiController._getSdkProperties; ApiController._getSdkProperties = vi.fn().mockReturnValue({ projectId: '', st: '', sv: '' }); const result = siwx['getSDKProperties'](); expect(result).toEqual({ projectId: '', st: '', sv: '' }); ApiController._getSdkProperties = originalGetSdkProperties; }); it('should handle wallet info when connectedWalletInfo is undefined', () => { vi.spyOn(ChainController, 'getAccountData').mockReturnValue(undefined); const walletInfo = siwx['getWalletInfo'](); expect(walletInfo).toBeUndefined(); }); it('should handle wallet info with missing properties', () => { vi.spyOn(ChainController, 'getAccountData').mockReturnValue({ connectedWalletInfo: { type: 'unknown-type' // Missing name and icon } }); const walletInfo = siwx['getWalletInfo'](); expect(walletInfo).toEqual({ type: 'unknown', name: undefined, icon: undefined }); }); it('should handle clearStorageTokens correctly', () => { const removeItemSpy = vi.spyOn(SafeLocalStorage, 'removeItem'); const emitSpy = vi.spyOn(siwx, 'emit'); // Set initial state siwx['otpUuid'] = 'test-uuid'; siwx['clearStorageTokens'](); expect(siwx['otpUuid']).toBe(null); expect(removeItemSpy).toHaveBeenCalledWith(SafeLocalStorageKeys.SIWX_AUTH_TOKEN); expect(removeItemSpy).toHaveBeenCalledWith(SafeLocalStorageKeys.SIWX_NONCE_TOKEN); expect(emitSpy).toHaveBeenCalledWith('sessionChanged', undefined); }); }); //# sourceMappingURL=ReownAuthentication.test.js.map