diff --git a/packages/common/src/store/account/sagas.test.ts b/packages/common/src/store/account/sagas.test.ts new file mode 100644 index 00000000000..c5485242c33 --- /dev/null +++ b/packages/common/src/store/account/sagas.test.ts @@ -0,0 +1,69 @@ +import { expectSaga } from 'redux-saga-test-plan' +import * as matchers from 'redux-saga-test-plan/matchers' +import { describe, it, vitest } from 'vitest' + +import { getWalletAccountSaga } from '~/api' +import { AccountUserMetadata } from '~/models' + +import { fetchAccountAsync } from './sagas' +import { fetchAccountFailed, fetchAccountSucceeded, signedIn } from './slice' + +const wallet = '0xc12f8e8a40b90e5aedf58fb729fa543e9a020cb0' + +const makeAccount = (isDeactivated: boolean) => + ({ + user: { + user_id: 1, + handle: 'test', + name: 'Test', + is_deactivated: isDeactivated + }, + playlists: [], + playlist_library: { contents: [] }, + track_save_count: 0 + }) as unknown as AccountUserMetadata + +const runFetchAccount = (account: AccountUserMetadata) => { + const sdk = { + services: { + audiusWalletClient: { + getAddresses: vitest.fn().mockResolvedValue([wallet]) + } + } + } + + return expectSaga(fetchAccountAsync, { + shouldMarkAccountAsLoading: true + }).provide([ + [matchers.getContext('audiusSdk'), vitest.fn().mockResolvedValue(sdk)], + [matchers.getContext('audiusBackendInstance'), {}], + [matchers.getContext('remoteConfigInstance'), { setUserId: vitest.fn() }], + [ + matchers.getContext('localStorage'), + { + getAudiusUserWalletOverride: vitest.fn().mockResolvedValue(null), + getItem: vitest.fn().mockResolvedValue(null), + setAudiusAccount: vitest.fn(), + setAudiusAccountUser: vitest.fn() + } + ], + [ + matchers.getContext('queryClient'), + { setQueryData: vitest.fn(), getQueryData: vitest.fn() } + ], + [matchers.call.fn(getWalletAccountSaga), account] + ]) +} + +describe('fetchAccountAsync', () => { + // Regression test: the deactivated branch used to fall through to + // fetchAccountSucceeded/signedIn, so a deactivated user was signed back in + // on any app load despite the sign-in form rejecting them. + it('does not sign in a deactivated account', async () => { + await runFetchAccount(makeAccount(true)) + .put(fetchAccountFailed({ reason: 'ACCOUNT_DEACTIVATED' })) + .not.put.actionType(fetchAccountSucceeded.type) + .not.put.actionType(signedIn.type) + .silentRun() + }) +}) diff --git a/packages/common/src/store/account/sagas.ts b/packages/common/src/store/account/sagas.ts index 1a44491a176..6b0c0dec04d 100644 --- a/packages/common/src/store/account/sagas.ts +++ b/packages/common/src/store/account/sagas.ts @@ -216,6 +216,7 @@ export function* fetchAccountAsync({ reason: 'ACCOUNT_DEACTIVATED' }) ) + return } const guestEmailFromLocalStorage = yield* call( diff --git a/packages/web/src/common/store/pages/signon/sagas.ts b/packages/web/src/common/store/pages/signon/sagas.ts index e1d83ec3728..703363e5a34 100644 --- a/packages/web/src/common/store/pages/signon/sagas.ts +++ b/packages/web/src/common/store/pages/signon/sagas.ts @@ -839,6 +839,10 @@ function* signIn(action: ReturnType) { yield* put( make(Name.SIGN_IN_WITH_DEACTIVATED_ACCOUNT, { handle: user.handle }) ) + // The hedgehog login above already persisted a session. Clear it, or a + // refresh will restore the deactivated account via fetchAccount. + const authService = yield* getContext('authService') + yield* call([authService, authService.signOut]) yield* put(signOnActions.signInFailed('Account is deactivated')) yield* put(toastActions.toast({ content: messages.deactivatedAccount })) return