Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion src/store/bitpay-id/bitpay-id.effects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Tests for bitpay-id.effects.ts
*
* Covers:
* - startFetchSession (success + failure)
* - startFetchSession (success + failure + stale-cookie clear when paired)
* - startBitPayIdStoreInit (dispatches SUCCESS_INITIALIZE_STORE)
* - startBitPayIdAnalyticsInit (Braze merge branch, no-op when user is falsy)
* - checkLoginWithPasskey (no email, passkey false, passkey true, error 1001, other error)
Expand All @@ -14,6 +14,7 @@
*/

import configureTestStore from '@test/store';
import {clearAllCookiesEverywhere} from '../../utils/cookieAuth';
import {Network} from '../../constants';
import {BitPayIdActionTypes} from './bitpay-id.types';
import {
Expand Down Expand Up @@ -256,6 +257,69 @@ describe('startFetchSession', () => {
// failedFetchSession sets fetchSessionStatus to 'failed'
expect(store.getState().BITPAY_ID.fetchSessionStatus).toBe('failed');
});

it('clears cookies and re-fetches when a paired user has a dead session', async () => {
(MockAuthApi.fetchSession as jest.Mock)
.mockResolvedValueOnce(makeSession({isAuthenticated: false}))
.mockResolvedValueOnce(
makeSession({csrfToken: 'fresh-token', isAuthenticated: false}),
);

const store = baseStore();
await store.dispatch(startFetchSession());

expect(clearAllCookiesEverywhere).toHaveBeenCalledTimes(1);
expect(MockAuthApi.fetchSession).toHaveBeenCalledTimes(2);
// The stored csrfToken must come from the session fetched after the clear.
expect(store.getState().BITPAY_ID.session.csrfToken).toBe('fresh-token');
});

it('does NOT clear cookies when the user is not paired', async () => {
(MockAuthApi.fetchSession as jest.Mock).mockResolvedValueOnce(
makeSession({isAuthenticated: false}),
);

const store = configureTestStore({
BITPAY_ID: {
session: makeSession(),
apiToken: {[Network.mainnet]: ''},
},
APP: {network: Network.mainnet},
});
await store.dispatch(startFetchSession());

expect(clearAllCookiesEverywhere).not.toHaveBeenCalled();
expect(MockAuthApi.fetchSession).toHaveBeenCalledTimes(1);
});

it('does NOT clear cookies when the session is authenticated', async () => {
(MockAuthApi.fetchSession as jest.Mock).mockResolvedValueOnce(
makeSession({isAuthenticated: true}),
);

const store = baseStore();
await store.dispatch(startFetchSession());

expect(clearAllCookiesEverywhere).not.toHaveBeenCalled();
expect(MockAuthApi.fetchSession).toHaveBeenCalledTimes(1);
});

it('still re-fetches when clearing cookies throws', async () => {
(clearAllCookiesEverywhere as jest.Mock).mockRejectedValueOnce(
new Error('cookie store unavailable'),
);
(MockAuthApi.fetchSession as jest.Mock)
.mockResolvedValueOnce(makeSession({isAuthenticated: false}))
.mockResolvedValueOnce(
makeSession({csrfToken: 'fresh-token', isAuthenticated: false}),
);

const store = baseStore();
await store.dispatch(startFetchSession());

expect(MockAuthApi.fetchSession).toHaveBeenCalledTimes(2);
expect(store.getState().BITPAY_ID.session.csrfToken).toBe('fresh-token');
});
});

// ---------------------------------------------------------------------------
Expand Down
21 changes: 19 additions & 2 deletions src/store/bitpay-id/bitpay-id.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,27 @@ export const startBitPayIdStoreInit =
export const startFetchSession =
(): Effect<Promise<void>> => async (dispatch, getState) => {
try {
const {APP} = getState();
const {APP, BITPAY_ID} = getState();
dispatch(BitPayIdActions.updateFetchSessionStatus('loading'));

const session = await AuthApi.fetchSession(APP.network);
let session = await AuthApi.fetchSession(APP.network);

// A paired user with a dead web session leaves only stale cookies on
// disk. Drop them, then re-fetch: the csrfToken just read is bound to the
// cookie being cleared, so storing it would orphan every consumer.
if (!session.isAuthenticated && BITPAY_ID.apiToken[APP.network]) {
try {
await clearAllCookiesEverywhere();
} catch (err: any) {
const errMsg =
err instanceof Error ? err.message : JSON.stringify(err);
logManager.debug(
'[startFetchSession] An error occurred while clearing cookies.',
errMsg,
);
}
session = await AuthApi.fetchSession(APP.network);
}

dispatch(BitPayIdActions.successFetchSession(session));
} catch {
Expand Down
Loading