diff --git a/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx b/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx index 58656698c4..00260f061a 100644 --- a/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx +++ b/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx @@ -72,7 +72,9 @@ export const VerifyEmailEffect = () => { email, ); - return enqueueSuccessSnackBar(successSnackbarParams); + enqueueSuccessSnackBar(successSnackbarParams); + + return navigate(AppPath.SignInUp); } const { loginToken, workspaceUrls } = await verifyEmailAndGetLoginToken( diff --git a/packages/twenty-front/src/modules/auth/components/__tests__/VerifyEmailEffect.test.tsx b/packages/twenty-front/src/modules/auth/components/__tests__/VerifyEmailEffect.test.tsx new file mode 100644 index 0000000000..677982459a --- /dev/null +++ b/packages/twenty-front/src/modules/auth/components/__tests__/VerifyEmailEffect.test.tsx @@ -0,0 +1,158 @@ +import { i18n } from '@lingui/core'; +import { I18nProvider } from '@lingui/react'; +import { render, waitFor } from '@testing-library/react'; +import { Provider as JotaiProvider } from 'jotai'; +import { MemoryRouter } from 'react-router-dom'; +import { SOURCE_LOCALE } from 'twenty-shared/translations'; +import { AppPath } from 'twenty-shared/types'; +import { ThemeProvider } from 'twenty-ui/theme-constants'; + +import { VerifyEmailEffect } from '@/auth/components/VerifyEmailEffect'; +import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState'; +import { + jotaiStore, + resetJotaiStore, +} from '@/ui/utilities/state/jotai/jotaiStore'; +import { dynamicActivate } from '~/utils/i18n/dynamicActivate'; + +const navigateMock = jest.fn(); +const verifyEmailAndGetWorkspaceAgnosticTokenMock = jest.fn(); +const verifyEmailAndGetLoginTokenMock = jest.fn(); +const verifyLoginTokenMock = jest.fn(); +const redirectToWorkspaceDomainMock = jest.fn(); +const enqueueSuccessSnackBarMock = jest.fn(); +const enqueueErrorSnackBarMock = jest.fn(); + +let isOnAWorkspaceValue = false; + +jest.mock('@/auth/hooks/useAuth', () => ({ + useAuth: () => ({ + verifyEmailAndGetWorkspaceAgnosticToken: + verifyEmailAndGetWorkspaceAgnosticTokenMock, + verifyEmailAndGetLoginToken: verifyEmailAndGetLoginTokenMock, + }), +})); + +jest.mock('@/auth/hooks/useVerifyLogin', () => ({ + useVerifyLogin: () => ({ verifyLoginToken: verifyLoginTokenMock }), +})); + +jest.mock('@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace', () => ({ + useIsCurrentLocationOnAWorkspace: () => ({ + isOnAWorkspace: isOnAWorkspaceValue, + }), +})); + +jest.mock('@/domain-manager/hooks/useRedirectToWorkspaceDomain', () => ({ + useRedirectToWorkspaceDomain: () => ({ + redirectToWorkspaceDomain: redirectToWorkspaceDomainMock, + }), +})); + +jest.mock('~/hooks/useNavigateApp', () => ({ + useNavigateApp: () => navigateMock, +})); + +jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar', () => ({ + useSnackBar: () => ({ + enqueueSuccessSnackBar: enqueueSuccessSnackBarMock, + enqueueErrorSnackBar: enqueueErrorSnackBarMock, + }), +})); + +// Rendered by VerifyEmailEffect in the error state; isolate it from Apollo. +jest.mock( + '@/auth/sign-in-up/hooks/useHandleResendEmailVerificationToken', + () => ({ + useHandleResendEmailVerificationToken: () => ({ + handleResendEmailVerificationToken: () => () => {}, + loading: false, + }), + }), +); + +dynamicActivate(SOURCE_LOCALE); + +const VERIFY_EMAIL_URL = + '/verify-email?email=user%40example.com&emailVerificationToken=valid-token'; + +const renderEffect = (initialEntry: string) => + render( + + + + + + + + + , + ); + +describe('VerifyEmailEffect', () => { + beforeEach(() => { + jest.clearAllMocks(); + resetJotaiStore(); + isOnAWorkspaceValue = false; + // The verification effect is gated on the client config having loaded. + jotaiStore.set(clientConfigApiStatusState.atom, { + isLoadedOnce: true, + isLoading: false, + isErrored: false, + isSaved: false, + }); + }); + + it('navigates to the SignInUp page after a successful workspace-agnostic verification on the central domain', async () => { + verifyEmailAndGetWorkspaceAgnosticTokenMock.mockResolvedValue(undefined); + + renderEffect(VERIFY_EMAIL_URL); + + await waitFor(() => { + expect(verifyEmailAndGetWorkspaceAgnosticTokenMock).toHaveBeenCalledWith( + 'valid-token', + 'user@example.com', + ); + }); + + // The workspace-agnostic flow only sets the next sign-in-up step, so the + // effect must hand off to the SignInUp page for that step to render. + await waitFor(() => { + expect(navigateMock).toHaveBeenCalledWith(AppPath.SignInUp); + }); + expect(enqueueSuccessSnackBarMock).toHaveBeenCalled(); + }); + + it('does not hand off to the SignInUp page when the verification fails', async () => { + verifyEmailAndGetWorkspaceAgnosticTokenMock.mockRejectedValue( + new Error('verification failed'), + ); + + renderEffect(VERIFY_EMAIL_URL); + + await waitFor(() => { + expect(enqueueErrorSnackBarMock).toHaveBeenCalled(); + }); + expect(navigateMock).not.toHaveBeenCalledWith(AppPath.SignInUp); + }); + + it('keeps the workspace-scoped verification path untouched when already on a workspace', async () => { + isOnAWorkspaceValue = true; + verifyEmailAndGetLoginTokenMock.mockResolvedValue({ + loginToken: { token: 'login-token' }, + workspaceUrls: { subdomainUrl: 'https://foo.twenty.com/' }, + }); + + renderEffect(VERIFY_EMAIL_URL); + + await waitFor(() => { + expect(verifyEmailAndGetLoginTokenMock).toHaveBeenCalledWith( + 'valid-token', + 'user@example.com', + ); + }); + + expect(verifyEmailAndGetWorkspaceAgnosticTokenMock).not.toHaveBeenCalled(); + expect(navigateMock).not.toHaveBeenCalledWith(AppPath.SignInUp); + }); +});