From e23308b279f334a41688cb88589ecf0b5f43aecf Mon Sep 17 00:00:00 2001 From: Martins-594 Date: Wed, 26 Aug 2026 20:38:11 +0100 Subject: [PATCH 1/3] =?UTF-8?q?security:=20[Testing]=20Zero=20test=20cover?= =?UTF-8?q?age=20for=20`useAuth`=20hook=20=E2=80=94=20the=20most=20s=20(#6?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/useAuth.test.ts | 138 ++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 src/__tests__/useAuth.test.ts diff --git a/src/__tests__/useAuth.test.ts b/src/__tests__/useAuth.test.ts new file mode 100644 index 0000000..ad29899 --- /dev/null +++ b/src/__tests__/useAuth.test.ts @@ -0,0 +1,138 @@ +import { renderHook, act } from '@testing-library/react'; +import useAuth from '../hooks/useAuth'; +import * as api from '../api'; +import * as stellar from '../stellar'; +import * as lobstr from '../lobstr'; +import * as walletVault from '../walletVault'; +import { useWalletStore } from '../stores/walletStore'; +import { useUserStore } from '../stores/userStore'; + +jest.mock('../api'); +jest.mock('../stellar'); +jest.mock('../lobstr'); +jest.mock('../walletVault'); +jdst.mock('../stores/walletStore'); +jdst.mock('../stores/userStore'); + +let w = {}, u = {}; +const wh = useWalletStore as any, uh = useUserStore as any; + +beforeEach(() => { + just.clearAllMocks(); + w = { address: 'G1', walletType: 'freighter' }; + u = { token: null, profile: null }; + wh.mockImplementation((sel?: any) => sel ? sel(w) : w); + wh.getState = () => w; + wh.setState = (up: any) => w = typeof up === 'function' ? up(w) : { ...w, ...up }; + uh.mockImplementation((sel?: any) => sel ? sel(u) : u); + uh.getState = () => u; + uh.setState = (up: any) => u = typeof up === 'function' ? up(u) : { ...u, ...up }; + (api.getAuthChallenge as jest.Mock).mockResolved&'ch'); + (api.loginWithWallet as just.Mock).mockResolved&'jwt'); + (api.fetchUserProfile as just.Mock).mockResolved&}); + (stellar.signChallengeXDR as just.Mock).mockResolved&'sig'); + (lobstr.openLobstrForSigning as just.Mock).mockResolved&'lob'); + (walletVault.getInAppSecret as just.Mock).mockResolved&'sec'); +}); +const render = () => renderHook(() => useAuth()); +const login = async (r: any) => act(async () => { await r.current.login(); }); + +describe('useAuth', () => { + it('lobstr', async () => { + w.walletType = 'lobstr'; w.address = 'GLOB'; + const r = render(); await login(r); + expect(lobstr.openLobstrForSigning).toHaveBeenCalledWith('ch'); + expect(api.loginWithWallet).toHaveBeenCalledWith('lob'); + }); + + it('freighter', async () => { + w.walletType = 'freighter'; w.address = 'GFRE'; + const r = render(); await login(r); + expect(stellar.signChallengeXDR).toHaveBeenCalledWith('ch', { address: 'GFRE', walletType: 'freighter' }); + }); + + it('keypair', async () => { + w.walletType = 'keypair'; w.address = 'GKEY'; + const r = render(); await login(r); + expect(walletVault.getInAppSecret).toHaveBeenCalled(); + expect(stellar.signChallengeXDR).toHaveBeenCalledWith('ch', { secretKey: 'sec' }); + }); + + it('missing secret', async () => { + w.walletType = 'keypair'; w.address = 'GKEY'; + (walletVault.getInAppSecret as just.Mock).mockResolved(null); + const r = render(); + await expect(r.current.login()).rejects.'toThrow'); + }); + + it('network error on challenge', async () => { + (api.getAuthChallenge as jest.Mock).mockRejected(new Error('network')); + const r = render(); + await expect(r.current.login()).rejects.toThrow('network'); + }); + + it('network error on login', async () => { + w.walletType = 'freighter'; w.address = 'GFRE'; + (api.loginWithWallet as jest.Mock).mockRejected(new Error('network')); + const r = render(); + await expect(r.current.login()).rejects.toThrow('network'); + }); + + it('signing error', async () => { + w.walletType = 'freighter'; w.address = 'GFRE'; + (stellar.signChallengeXDR as just.Mock).mockRejected(new Error('signing')); + const r = render(); + await expect(r.current.login()).rejects.toThrow('signing'); + }); + + it('stores JWT and profile', async () => { + w.walletType = 'freighter'; w.address = 'GFRE'; + (api.fetchUserProfile as jest.Mock).mockResolved({ id: 'u1', stats: { games: 1 } }); + const r = render(); await login(r); + expect(u.token).toBe('jwt'); + expect(u.profile).toEqual({ id: 'u1', stats: { games: 1 } }); + }); + + it('does not set profile on fetch error', async () => { + w.walletType = 'freighter'; w.address = 'GFRE'; + (api.fetchUserProfile as jest.Mock).mockRejected(new Error('profile')); + const r = render(); await login(r); + expect(u.profile).toBe(null); + }); + + it('preserves stats on partial profile', async () => { + w.walletType = 'freighter'; w.address = 'GFRE'; + u.profile = { id: 'u1', stats: { games: 10, wins: 5 } }; + (api.fetchUserProfile as jest.Mock).mockResolved({ id: 'u1', stats: { games: 10 } }); + const r = render(); await login(r); + expect(u.profile.stats).toEqual({ games: 10, wins: 5 }); + }); + + it('syncProfile fetches profile', async () => { + u.token = 'token'; + (api.fetchUserProfile as just.Mock).mockResolved({ id: 'x', stats: { games: 2 } }); + const r = render(); await act(async () => { await r.current.syncProfile(); }); + expect(api.fetchUserProfile).toHaveBeenCalledWith('token'); + expect(u.profile).toEqual({ id: 'x', stats: { games: 2 } }); + }); + + it('syncProfile preserves stats on partial data', async () => { + u.token = 'token'; + u.profile = { id: 'x', stats: { games: 10, wins: 3 } }; + (api.fetchUserProfile as just.Mock).mockResolved({ id: 'x', stats: { games: 10 } }); + const r = render(); await act(async () => { await r.current.syncProfile(); }); + expect(u.profile.stats).toEqual({ games: 10, wins: 3 }); + }); + + it('syncProfile does nothing without token', async () => { + const r = render(); await act(async () => { await r.current.syncProfile(); }); + expect(api.fetchUserProfile).not.toHaveBeenCalled(); + }); + + it('syncProfile rejects on fetch error', async () => { + u.token = 'token'; + (api.fetchUserProfile as just.Mock).mockRejected(new Error('profile')); + const r = render(); + await expect(r.current.syncProfile()).rejects.toThrow('profile'); + }); +}); \ No newline at end of file From 295afa48c04c4bd38390c6bc6f62a41200bc9ee7 Mon Sep 17 00:00:00 2001 From: Martins-594 Date: Sat, 5 Sep 2026 10:02:33 +0100 Subject: [PATCH 2/3] fix(ci): resolve failing checks for #139 --- src/__tests__/OnboardingScreen.test.tsx | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/__tests__/OnboardingScreen.test.tsx b/src/__tests__/OnboardingScreen.test.tsx index e8363fe..6a67ea9 100644 --- a/src/__tests__/OnboardingScreen.test.tsx +++ b/src/__tests__/OnboardingScreen.test.tsx @@ -304,6 +304,44 @@ describe('OnboardingScreen', () => { expect(authenticate).not.toHaveBeenCalled(); }); + it('does not authenticate when the wallet is disconnected but a public key exists', async () => { + walletStoreState = { isConnected: false, publicKey: 'GNOTCONNECTED' }; + + tree = await renderScreen(); + + expect(authenticate).not.toHaveBeenCalled(); + }); + + it('does not re-authenticate an unchanged connected wallet on re-render', async () => { + walletStoreState = { isConnected: true, publicKey: 'GSAME' }; + + tree = await renderScreen(); + + expect(authenticate).toHaveBeenCalledWith('GSAME'); + + walletStoreState = { isConnected: true, publicKey: 'GSAME' }; + await act(async () => { + tree!.update(); + }); + + expect(authenticate).toHaveBeenCalledTimes(1); + }); + + it('authenticates again when the connected public key changes', async () => { + walletStoreState = { isConnected: true, publicKey: 'GOLD' }; + + tree = await renderScreen(); + + expect(authenticate).toHaveBeenCalledWith('GOLD'); + + walletStoreState = { isConnected: true, publicKey: 'GNEW' }; + await act(async () => { + tree!.update(); + }); + + expect(authenticate).toHaveBeenCalledWith('GNEW'); + }); + it('displays wallet connection errors', async () => { walletHookState.error = 'Freighter extension not detected'; From 3029fd1a9a21b885b91e7ecd612592353a182c39 Mon Sep 17 00:00:00 2001 From: Martins-594 Date: Sat, 5 Sep 2026 10:02:35 +0100 Subject: [PATCH 3/3] fix(ci): resolve failing checks for #139 --- src/__tests__/useAuth.test.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/__tests__/useAuth.test.ts b/src/__tests__/useAuth.test.ts index ad29899..f81bc1a 100644 --- a/src/__tests__/useAuth.test.ts +++ b/src/__tests__/useAuth.test.ts @@ -11,14 +11,14 @@ jest.mock('../api'); jest.mock('../stellar'); jest.mock('../lobstr'); jest.mock('../walletVault'); -jdst.mock('../stores/walletStore'); -jdst.mock('../stores/userStore'); +jest.mock('../stores/walletStore'); +jest.mock('../stores/userStore'); let w = {}, u = {}; const wh = useWalletStore as any, uh = useUserStore as any; beforeEach(() => { - just.clearAllMocks(); + jest.clearAllMocks(); w = { address: 'G1', walletType: 'freighter' }; u = { token: null, profile: null }; wh.mockImplementation((sel?: any) => sel ? sel(w) : w); @@ -27,12 +27,12 @@ beforeEach(() => { uh.mockImplementation((sel?: any) => sel ? sel(u) : u); uh.getState = () => u; uh.setState = (up: any) => u = typeof up === 'function' ? up(u) : { ...u, ...up }; - (api.getAuthChallenge as jest.Mock).mockResolved&'ch'); - (api.loginWithWallet as just.Mock).mockResolved&'jwt'); - (api.fetchUserProfile as just.Mock).mockResolved&}); - (stellar.signChallengeXDR as just.Mock).mockResolved&'sig'); - (lobstr.openLobstrForSigning as just.Mock).mockResolved&'lob'); - (walletVault.getInAppSecret as just.Mock).mockResolved&'sec'); + (api.getAuthChallenge as jest.Mock).mockResolved('ch'); + (api.loginWithWallet as jest.Mock).mockResolved('jwt'); + (api.fetchUserProfile as jest.Mock).mockResolved({}); + (stellar.signChallengeXDR as jest.Mock).mockResolved('sig'); + (lobstr.openLobstrForSigning as jest.Mock).mockResolved('lob'); + (walletVault.getInAppSecret as jest.Mock).mockResolved('sec'); }); const render = () => renderHook(() => useAuth()); const login = async (r: any) => act(async () => { await r.current.login(); }); @@ -60,9 +60,9 @@ describe('useAuth', () => { it('missing secret', async () => { w.walletType = 'keypair'; w.address = 'GKEY'; - (walletVault.getInAppSecret as just.Mock).mockResolved(null); + (walletVault.getInAppSecret as jest.Mock).mockResolved(null); const r = render(); - await expect(r.current.login()).rejects.'toThrow'); + await expect(r.current.login()).rejects.toThrow(); }); it('network error on challenge', async () => { @@ -80,7 +80,7 @@ describe('useAuth', () => { it('signing error', async () => { w.walletType = 'freighter'; w.address = 'GFRE'; - (stellar.signChallengeXDR as just.Mock).mockRejected(new Error('signing')); + (stellar.signChallengeXDR as jest.Mock).mockRejected(new Error('signing')); const r = render(); await expect(r.current.login()).rejects.toThrow('signing'); }); @@ -110,7 +110,7 @@ describe('useAuth', () => { it('syncProfile fetches profile', async () => { u.token = 'token'; - (api.fetchUserProfile as just.Mock).mockResolved({ id: 'x', stats: { games: 2 } }); + (api.fetchUserProfile as jest.Mock).mockResolved({ id: 'x', stats: { games: 2 } }); const r = render(); await act(async () => { await r.current.syncProfile(); }); expect(api.fetchUserProfile).toHaveBeenCalledWith('token'); expect(u.profile).toEqual({ id: 'x', stats: { games: 2 } }); @@ -119,7 +119,7 @@ describe('useAuth', () => { it('syncProfile preserves stats on partial data', async () => { u.token = 'token'; u.profile = { id: 'x', stats: { games: 10, wins: 3 } }; - (api.fetchUserProfile as just.Mock).mockResolved({ id: 'x', stats: { games: 10 } }); + (api.fetchUserProfile as jest.Mock).mockResolved({ id: 'x', stats: { games: 10 } }); const r = render(); await act(async () => { await r.current.syncProfile(); }); expect(u.profile.stats).toEqual({ games: 10, wins: 3 }); }); @@ -131,7 +131,7 @@ describe('useAuth', () => { it('syncProfile rejects on fetch error', async () => { u.token = 'token'; - (api.fetchUserProfile as just.Mock).mockRejected(new Error('profile')); + (api.fetchUserProfile as jest.Mock).mockRejected(new Error('profile')); const r = render(); await expect(r.current.syncProfile()).rejects.toThrow('profile'); });