review(front): refacto url-manager (#8861)
Replace https://github.com/twentyhq/twenty/pull/8855
This commit is contained in:
@ -14,6 +14,7 @@ import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/i
|
||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||
|
||||
import { email, mocks, password, results, token } from '../__mocks__/useAuth';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
@ -83,6 +84,9 @@ describe('useAuth', () => {
|
||||
);
|
||||
const supportChat = useRecoilValue(supportChatState);
|
||||
const isDebugMode = useRecoilValue(isDebugModeState);
|
||||
const isMultiWorkspaceEnabled = useRecoilValue(
|
||||
isMultiWorkspaceEnabledState,
|
||||
);
|
||||
return {
|
||||
...useAuth(),
|
||||
client,
|
||||
@ -93,6 +97,7 @@ describe('useAuth', () => {
|
||||
isDeveloperDefaultSignInPrefilled,
|
||||
supportChat,
|
||||
isDebugMode,
|
||||
isMultiWorkspaceEnabled,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@ -4,7 +4,6 @@ import {
|
||||
snapshot_UNSTABLE,
|
||||
useGotoRecoilSnapshot,
|
||||
useRecoilCallback,
|
||||
useRecoilValue,
|
||||
useSetRecoilState,
|
||||
} from 'recoil';
|
||||
import { iconsState } from 'twenty-ui';
|
||||
@ -42,18 +41,15 @@ import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDa
|
||||
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
|
||||
import { currentUserState } from '../states/currentUserState';
|
||||
import { tokenPairState } from '../states/tokenPairState';
|
||||
import { lastAuthenticateWorkspaceState } from '@/auth/states/lastAuthenticateWorkspaceState';
|
||||
|
||||
import { urlManagerState } from '@/url-manager/states/url-manager.state';
|
||||
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
|
||||
import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
import { useIsCurrentLocationOnAWorkspaceSubdomain } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspaceSubdomain';
|
||||
import { useReadWorkspaceSubdomainFromCurrentLocation } from '@/domain-manager/hooks/useReadWorkspaceSubdomainFromCurrentLocation';
|
||||
|
||||
export const useAuth = () => {
|
||||
const setTokenPair = useSetRecoilState(tokenPairState);
|
||||
const setCurrentUser = useSetRecoilState(currentUserState);
|
||||
const urlManager = useRecoilValue(urlManagerState);
|
||||
const setLastAuthenticateWorkspaceState = useSetRecoilState(
|
||||
lastAuthenticateWorkspaceState,
|
||||
);
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
@ -68,7 +64,12 @@ export const useAuth = () => {
|
||||
const [challenge] = useChallengeMutation();
|
||||
const [signUp] = useSignUpMutation();
|
||||
const [verify] = useVerifyMutation();
|
||||
const { isTwentyWorkspaceSubdomain, getWorkspaceSubdomain } = useUrlManager();
|
||||
const { isOnAWorkspaceSubdomain } =
|
||||
useIsCurrentLocationOnAWorkspaceSubdomain();
|
||||
const { workspaceSubdomain } = useReadWorkspaceSubdomainFromCurrentLocation();
|
||||
|
||||
const { setLastAuthenticateWorkspaceDomain } =
|
||||
useLastAuthenticatedWorkspaceDomain();
|
||||
const [checkUserExistsQuery, { data: checkUserExistsData }] =
|
||||
useCheckUserExistsLazyQuery();
|
||||
|
||||
@ -101,6 +102,9 @@ export const useAuth = () => {
|
||||
const isCurrentUserLoaded = snapshot
|
||||
.getLoadable(isCurrentUserLoadedState)
|
||||
.getValue();
|
||||
const isMultiWorkspaceEnabled = snapshot
|
||||
.getLoadable(isMultiWorkspaceEnabledState)
|
||||
.getValue();
|
||||
const initialSnapshot = emptySnapshot.map(({ set }) => {
|
||||
set(iconsState, iconsValue);
|
||||
set(authProvidersState, authProvidersValue);
|
||||
@ -114,6 +118,7 @@ export const useAuth = () => {
|
||||
set(captchaProviderState, captchaProvider);
|
||||
set(clientConfigApiStatusState, clientConfigApiStatus);
|
||||
set(isCurrentUserLoadedState, isCurrentUserLoaded);
|
||||
set(isMultiWorkspaceEnabledState, isMultiWorkspaceEnabled);
|
||||
return undefined;
|
||||
});
|
||||
goToRecoilSnapshot(initialSnapshot);
|
||||
@ -212,13 +217,11 @@ export const useAuth = () => {
|
||||
const workspace = user.defaultWorkspace ?? null;
|
||||
|
||||
setCurrentWorkspace(workspace);
|
||||
if (isDefined(workspace) && isTwentyWorkspaceSubdomain) {
|
||||
setLastAuthenticateWorkspaceState({
|
||||
id: workspace.id,
|
||||
|
||||
if (isDefined(workspace) && isOnAWorkspaceSubdomain) {
|
||||
setLastAuthenticateWorkspaceDomain({
|
||||
workspaceId: workspace.id,
|
||||
subdomain: workspace.subdomain,
|
||||
cookieAttributes: {
|
||||
domain: `.${urlManager.frontDomain}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -245,12 +248,11 @@ export const useAuth = () => {
|
||||
setTokenPair,
|
||||
setCurrentUser,
|
||||
setCurrentWorkspace,
|
||||
isTwentyWorkspaceSubdomain,
|
||||
isOnAWorkspaceSubdomain,
|
||||
setCurrentWorkspaceMembers,
|
||||
setCurrentWorkspaceMember,
|
||||
setDateTimeFormat,
|
||||
setLastAuthenticateWorkspaceState,
|
||||
urlManager.frontDomain,
|
||||
setLastAuthenticateWorkspaceDomain,
|
||||
setWorkspaces,
|
||||
],
|
||||
);
|
||||
@ -340,15 +342,13 @@ export const useAuth = () => {
|
||||
params.workspacePersonalInviteToken,
|
||||
);
|
||||
}
|
||||
const subdomain = getWorkspaceSubdomain;
|
||||
|
||||
if (isDefined(subdomain)) {
|
||||
url.searchParams.set('workspaceSubdomain', subdomain);
|
||||
if (isDefined(workspaceSubdomain)) {
|
||||
url.searchParams.set('workspaceSubdomain', workspaceSubdomain);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
},
|
||||
[getWorkspaceSubdomain],
|
||||
[workspaceSubdomain],
|
||||
);
|
||||
|
||||
const handleGoogleLogin = useCallback(
|
||||
|
||||
@ -30,8 +30,8 @@ import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
|
||||
import { signInUpModeState } from '@/auth/states/signInUpModeState';
|
||||
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
|
||||
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
|
||||
import { SignInUpMode } from '@/auth/types/signInUpMode';
|
||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||
|
||||
const StyledContentContainer = styled(motion.div)`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
@ -53,8 +53,7 @@ export const SignInUpGlobalScopeForm = () => {
|
||||
const { signInWithMicrosoft } = useSignInWithMicrosoft();
|
||||
const { checkUserExists } = useAuth();
|
||||
const { readCaptchaToken } = useReadCaptchaToken();
|
||||
const { redirectToWorkspace } = useUrlManager();
|
||||
|
||||
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
||||
const setSignInUpStep = useSetRecoilState(signInUpStepState);
|
||||
const [signInUpMode, setSignInUpMode] = useRecoilState(signInUpModeState);
|
||||
|
||||
@ -97,7 +96,7 @@ export const SignInUpGlobalScopeForm = () => {
|
||||
isDefined(data?.checkUserExists.availableWorkspaces) &&
|
||||
data.checkUserExists.availableWorkspaces.length >= 1
|
||||
) {
|
||||
return redirectToWorkspace(
|
||||
return redirectToWorkspaceDomain(
|
||||
data?.checkUserExists.availableWorkspaces[0].subdomain,
|
||||
pathname,
|
||||
{
|
||||
|
||||
@ -0,0 +1,72 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useEmailPasswordResetLinkMutation } from '~/generated/graphql';
|
||||
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
|
||||
// Mocks
|
||||
jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar');
|
||||
jest.mock('~/generated/graphql');
|
||||
|
||||
describe('useHandleResetPassword', () => {
|
||||
const enqueueSnackBarMock = jest.fn();
|
||||
const emailPasswordResetLinkMock = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
(useSnackBar as jest.Mock).mockReturnValue({
|
||||
enqueueSnackBar: enqueueSnackBarMock,
|
||||
});
|
||||
(useEmailPasswordResetLinkMutation as jest.Mock).mockReturnValue([
|
||||
emailPasswordResetLinkMock,
|
||||
]);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should show error message if email is invalid', async () => {
|
||||
const { result } = renderHook(() => useHandleResetPassword());
|
||||
await act(() => result.current.handleResetPassword('')());
|
||||
|
||||
expect(enqueueSnackBarMock).toHaveBeenCalledWith('Invalid email', {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
});
|
||||
|
||||
it('should show success message if password reset link is sent', async () => {
|
||||
emailPasswordResetLinkMock.mockResolvedValue({
|
||||
data: { emailPasswordResetLink: { success: true } },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useHandleResetPassword());
|
||||
await act(() => result.current.handleResetPassword('test@example.com')());
|
||||
|
||||
expect(enqueueSnackBarMock).toHaveBeenCalledWith(
|
||||
'Password reset link has been sent to the email',
|
||||
{ variant: SnackBarVariant.Success },
|
||||
);
|
||||
});
|
||||
|
||||
it('should show error message if sending reset link fails', async () => {
|
||||
emailPasswordResetLinkMock.mockResolvedValue({
|
||||
data: { emailPasswordResetLink: { success: false } },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useHandleResetPassword());
|
||||
await act(() => result.current.handleResetPassword('test@example.com')());
|
||||
|
||||
expect(enqueueSnackBarMock).toHaveBeenCalledWith('There was some issue', {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error message in case of request error', async () => {
|
||||
const errorMessage = 'Network Error';
|
||||
emailPasswordResetLinkMock.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
const { result } = renderHook(() => useHandleResetPassword());
|
||||
await act(() => result.current.handleResetPassword('test@example.com')());
|
||||
|
||||
expect(enqueueSnackBarMock).toHaveBeenCalledWith(errorMessage, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,73 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useGetAuthorizationUrlMutation } from '~/generated/graphql';
|
||||
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar');
|
||||
jest.mock('~/generated/graphql');
|
||||
|
||||
// Helpers
|
||||
const mockEnqueueSnackBar = jest.fn();
|
||||
const mockGetAuthorizationUrlMutation = jest.fn();
|
||||
|
||||
// Mock return values
|
||||
(useSnackBar as jest.Mock).mockReturnValue({
|
||||
enqueueSnackBar: mockEnqueueSnackBar,
|
||||
});
|
||||
(useGetAuthorizationUrlMutation as jest.Mock).mockReturnValue([
|
||||
mockGetAuthorizationUrlMutation,
|
||||
]);
|
||||
|
||||
describe('useSSO', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call getAuthorizationUrlForSSO with correct parameters', async () => {
|
||||
const { result } = renderHook(() => useSSO());
|
||||
const identityProviderId = 'test-id';
|
||||
|
||||
mockGetAuthorizationUrlMutation.mockResolvedValueOnce({
|
||||
data: {
|
||||
getAuthorizationUrl: {
|
||||
authorizationURL: 'http://example.com',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await result.current.getAuthorizationUrlForSSO({ identityProviderId });
|
||||
|
||||
expect(mockGetAuthorizationUrlMutation).toHaveBeenCalledWith({
|
||||
variables: { input: { identityProviderId } },
|
||||
});
|
||||
});
|
||||
|
||||
it('should enqueue error snackbar when URL retrieval fails', async () => {
|
||||
const { result } = renderHook(() => useSSO());
|
||||
const identityProviderId = 'test-id';
|
||||
|
||||
mockGetAuthorizationUrlMutation.mockResolvedValueOnce({
|
||||
errors: [{ message: 'Error message' }],
|
||||
});
|
||||
|
||||
await result.current.redirectToSSOLoginPage(identityProviderId);
|
||||
|
||||
expect(mockEnqueueSnackBar).toHaveBeenCalledWith('Error message', {
|
||||
variant: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
it('should enqueue default error snackbar when error message is not provided', async () => {
|
||||
const { result } = renderHook(() => useSSO());
|
||||
const identityProviderId = 'test-id';
|
||||
|
||||
mockGetAuthorizationUrlMutation.mockResolvedValueOnce({ errors: [{}] });
|
||||
|
||||
await result.current.redirectToSSOLoginPage(identityProviderId);
|
||||
|
||||
expect(mockEnqueueSnackBar).toHaveBeenCalledWith('Unknown error', {
|
||||
variant: 'error',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,55 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useParams: jest.fn(),
|
||||
useSearchParams: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/auth/hooks/useAuth', () => ({
|
||||
useAuth: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useSignInWithGoogle', () => {
|
||||
it('should call signInWithGoogle with correct params', () => {
|
||||
const signInWithGoogleMock = jest.fn();
|
||||
const mockUseParams = { workspaceInviteHash: 'testHash' };
|
||||
const mockSearchParams = new URLSearchParams('inviteToken=testToken');
|
||||
|
||||
(useParams as jest.Mock).mockReturnValue(mockUseParams);
|
||||
(useSearchParams as jest.Mock).mockReturnValue([mockSearchParams]);
|
||||
(useAuth as jest.Mock).mockReturnValue({
|
||||
signInWithGoogle: signInWithGoogleMock,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useSignInWithGoogle());
|
||||
result.current.signInWithGoogle();
|
||||
|
||||
expect(signInWithGoogleMock).toHaveBeenCalledWith({
|
||||
workspaceInviteHash: 'testHash',
|
||||
workspacePersonalInviteToken: 'testToken',
|
||||
});
|
||||
});
|
||||
|
||||
it('should call signInWithGoogle with undefined invite token if not present', () => {
|
||||
const signInWithGoogleMock = jest.fn();
|
||||
const mockUseParams = { workspaceInviteHash: 'testHash' };
|
||||
const mockSearchParams = new URLSearchParams();
|
||||
|
||||
(useParams as jest.Mock).mockReturnValue(mockUseParams);
|
||||
(useSearchParams as jest.Mock).mockReturnValue([mockSearchParams]);
|
||||
(useAuth as jest.Mock).mockReturnValue({
|
||||
signInWithGoogle: signInWithGoogleMock,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useSignInWithGoogle());
|
||||
result.current.signInWithGoogle();
|
||||
|
||||
expect(signInWithGoogleMock).toHaveBeenCalledWith({
|
||||
workspaceInviteHash: 'testHash',
|
||||
workspacePersonalInviteToken: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,60 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { useSignInWithMicrosoft } from '@/auth/sign-in-up/hooks/useSignInWithMicrosoft';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useParams: jest.fn(),
|
||||
useSearchParams: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/auth/hooks/useAuth', () => ({
|
||||
useAuth: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useSignInWithMicrosoft', () => {
|
||||
it('should call signInWithMicrosoft with the correct parameters', () => {
|
||||
const workspaceInviteHashMock = 'testHash';
|
||||
const inviteTokenMock = 'testToken';
|
||||
const signInWithMicrosoftMock = jest.fn();
|
||||
|
||||
(useParams as jest.Mock).mockReturnValue({
|
||||
workspaceInviteHash: workspaceInviteHashMock,
|
||||
});
|
||||
(useSearchParams as jest.Mock).mockReturnValue([
|
||||
new URLSearchParams(`inviteToken=${inviteTokenMock}`),
|
||||
]);
|
||||
(useAuth as jest.Mock).mockReturnValue({
|
||||
signInWithMicrosoft: signInWithMicrosoftMock,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useSignInWithMicrosoft());
|
||||
result.current.signInWithMicrosoft();
|
||||
|
||||
expect(signInWithMicrosoftMock).toHaveBeenCalledWith({
|
||||
workspaceInviteHash: workspaceInviteHashMock,
|
||||
workspacePersonalInviteToken: inviteTokenMock,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing inviteToken gracefully', () => {
|
||||
const workspaceInviteHashMock = 'testHash';
|
||||
const signInWithMicrosoftMock = jest.fn();
|
||||
|
||||
(useParams as jest.Mock).mockReturnValue({
|
||||
workspaceInviteHash: workspaceInviteHashMock,
|
||||
});
|
||||
(useSearchParams as jest.Mock).mockReturnValue([new URLSearchParams('')]);
|
||||
(useAuth as jest.Mock).mockReturnValue({
|
||||
signInWithMicrosoft: signInWithMicrosoftMock,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useSignInWithMicrosoft());
|
||||
result.current.signInWithMicrosoft();
|
||||
|
||||
expect(signInWithMicrosoftMock).toHaveBeenCalledWith({
|
||||
workspaceInviteHash: workspaceInviteHashMock,
|
||||
workspacePersonalInviteToken: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,18 +0,0 @@
|
||||
import { cookieStorageEffect } from '~/utils/recoil-effects';
|
||||
import { Workspace } from '~/generated/graphql';
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const lastAuthenticateWorkspaceState = createState<
|
||||
| (Pick<Workspace, 'id' | 'subdomain'> & {
|
||||
cookieAttributes?: Cookies.CookieAttributes;
|
||||
})
|
||||
| null
|
||||
>({
|
||||
key: 'lastAuthenticateWorkspaceState',
|
||||
defaultValue: null,
|
||||
effects: [
|
||||
cookieStorageEffect('lastAuthenticateWorkspace', {
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), // 1 year
|
||||
}),
|
||||
],
|
||||
});
|
||||
Reference in New Issue
Block a user