diff --git a/packages/twenty-front/src/modules/auth/hooks/__mocks__/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/__mocks__/useAuth.ts
new file mode 100644
index 000000000..48e752679
--- /dev/null
+++ b/packages/twenty-front/src/modules/auth/hooks/__mocks__/useAuth.ts
@@ -0,0 +1,111 @@
+import {
+ ChallengeDocument,
+ SignUpDocument,
+ VerifyDocument,
+} from '~/generated/graphql';
+
+export const queries = {
+ challenge: ChallengeDocument,
+ verify: VerifyDocument,
+ signup: SignUpDocument,
+};
+
+export const email = 'test@test.com';
+export const password = 'testing';
+export const token =
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
+
+export const variables = {
+ challenge: {
+ email,
+ password,
+ },
+ verify: { loginToken: token },
+ signup: {},
+};
+
+export const results = {
+ challenge: {
+ loginToken: {
+ token,
+ expiresAt: '2022-01-01',
+ },
+ },
+ verify: {
+ user: {
+ id: 'id',
+ firstName: 'firstName',
+ lastName: 'lastName',
+ email: 'email',
+ canImpersonate: 'canImpersonate',
+ supportUserHash: 'supportUserHash',
+ workspaceMember: {
+ id: 'id',
+ name: {
+ firstName: 'firstName',
+ lastName: 'lastName',
+ },
+ colorScheme: 'colorScheme',
+ avatarUrl: 'avatarUrl',
+ locale: 'locale',
+ },
+ defaultWorkspace: {
+ id: 'id',
+ displayName: 'displayName',
+ logo: 'logo',
+ domainName: 'domainName',
+ inviteHash: 'inviteHash',
+ allowImpersonation: true,
+ subscriptionStatus: 'subscriptionStatus',
+ featureFlags: {
+ id: 'id',
+ key: 'key',
+ value: 'value',
+ workspaceId: 'workspaceId',
+ },
+ },
+ },
+ tokens: {
+ accessToken: { token, expiresAt: 'expiresAt' },
+ refreshToken: { token, expiresAt: 'expiresAt' },
+ },
+ signup: {},
+ },
+ signUp: { loginToken: { token, expiresAt: 'expiresAt' } },
+};
+
+export const mocks = [
+ {
+ request: {
+ query: queries.challenge,
+ variables: variables.challenge,
+ },
+ result: jest.fn(() => ({
+ data: {
+ challenge: results.challenge,
+ },
+ })),
+ },
+ {
+ request: {
+ query: queries.verify,
+ variables: variables.verify,
+ },
+ result: jest.fn(() => ({
+ data: {
+ verify: results.verify,
+ },
+ })),
+ },
+ {
+ request: {
+ query: queries.signup,
+ variables: variables.challenge,
+ },
+ result: jest.fn(() => ({
+ data: {
+ signUp: results.signUp,
+ },
+ })),
+ },
+];
diff --git a/packages/twenty-front/src/modules/auth/hooks/__test__/useAuth.test.tsx b/packages/twenty-front/src/modules/auth/hooks/__test__/useAuth.test.tsx
new file mode 100644
index 000000000..25bfda751
--- /dev/null
+++ b/packages/twenty-front/src/modules/auth/hooks/__test__/useAuth.test.tsx
@@ -0,0 +1,147 @@
+import { ReactNode } from 'react';
+import { useApolloClient } from '@apollo/client';
+import { MockedProvider } from '@apollo/client/testing';
+import { expect } from '@storybook/test';
+import { act, renderHook } from '@testing-library/react';
+import { RecoilRoot, useRecoilValue } from 'recoil';
+
+import { useAuth } from '@/auth/hooks/useAuth';
+import { authProvidersState } from '@/client-config/states/authProvidersState';
+import { billingState } from '@/client-config/states/billingState';
+import { isDebugModeState } from '@/client-config/states/isDebugModeState';
+import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
+import { supportChatState } from '@/client-config/states/supportChatState';
+import { telemetryState } from '@/client-config/states/telemetryState';
+import { iconsState } from '@/ui/display/icon/states/iconsState';
+
+import { email, mocks, password, results, token } from '../__mocks__/useAuth';
+
+const Wrapper = ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+);
+
+const renderHooks = () => {
+ const { result } = renderHook(
+ () => {
+ return useAuth();
+ },
+ {
+ wrapper: Wrapper,
+ },
+ );
+ return { result };
+};
+
+describe('useAuth', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return challenge object', async () => {
+ const { result } = renderHooks();
+
+ await act(async () => {
+ expect(await result.current.challenge(email, password)).toStrictEqual(
+ results.challenge,
+ );
+ });
+
+ expect(mocks[0].result).toHaveBeenCalled();
+ });
+
+ it('should verify user', async () => {
+ const { result } = renderHooks();
+
+ await act(async () => {
+ await result.current.verify(token);
+ });
+
+ expect(mocks[1].result).toHaveBeenCalled();
+ });
+
+ it('should handle credential sign-in', async () => {
+ const { result } = renderHooks();
+
+ await act(async () => {
+ await result.current.signInWithCredentials(email, password);
+ });
+
+ expect(mocks[0].result).toHaveBeenCalled();
+ expect(mocks[1].result).toHaveBeenCalled();
+ });
+
+ it('should handle sign-out', async () => {
+ const { result } = renderHook(
+ () => {
+ const client = useApolloClient();
+ const icons = useRecoilValue(iconsState);
+ const authProviders = useRecoilValue(authProvidersState);
+ const billing = useRecoilValue(billingState);
+ const isSignInPrefilled = useRecoilValue(isSignInPrefilledState);
+ const supportChat = useRecoilValue(supportChatState);
+ const telemetry = useRecoilValue(telemetryState);
+ const isDebugMode = useRecoilValue(isDebugModeState);
+ return {
+ ...useAuth(),
+ client,
+ state: {
+ icons,
+ authProviders,
+ billing,
+ isSignInPrefilled,
+ supportChat,
+ telemetry,
+ isDebugMode,
+ },
+ };
+ },
+ {
+ wrapper: Wrapper,
+ },
+ );
+
+ const { signOut, client } = result.current;
+
+ await act(async () => {
+ await signOut();
+ });
+
+ expect(sessionStorage.length).toBe(0);
+ expect(client.cache.extract()).toEqual({});
+
+ const { state } = result.current;
+
+ expect(state.icons).toEqual({});
+ expect(state.authProviders).toEqual({
+ google: false,
+ magicLink: false,
+ password: true,
+ });
+ expect(state.billing).toBeNull();
+ expect(state.isSignInPrefilled).toBe(false);
+ expect(state.supportChat).toEqual({
+ supportDriver: 'none',
+ supportFrontChatId: null,
+ });
+ expect(state.telemetry).toEqual({
+ enabled: true,
+ anonymizationEnabled: true,
+ });
+ expect(state.isDebugMode).toBe(false);
+ });
+
+ it('should handle credential sign-up', async () => {
+ const { result } = renderHooks();
+
+ await act(async () => {
+ const res = await result.current.signUpWithCredentials(email, password);
+ expect(res).toHaveProperty('user');
+ expect(res).toHaveProperty('workspaceMember');
+ expect(res).toHaveProperty('workspace');
+ });
+
+ expect(mocks[2].result).toHaveBeenCalled();
+ });
+});
diff --git a/packages/twenty-front/src/modules/auth/hooks/__test__/useIsLogged.test.ts b/packages/twenty-front/src/modules/auth/hooks/__test__/useIsLogged.test.ts
new file mode 100644
index 000000000..2674f26d9
--- /dev/null
+++ b/packages/twenty-front/src/modules/auth/hooks/__test__/useIsLogged.test.ts
@@ -0,0 +1,47 @@
+import { act } from 'react-dom/test-utils';
+import { renderHook } from '@testing-library/react';
+import { RecoilRoot, useSetRecoilState } from 'recoil';
+
+import { useIsLogged } from '@/auth/hooks/useIsLogged';
+import { tokenPairState } from '@/auth/states/tokenPairState';
+
+const renderHooks = () => {
+ const { result } = renderHook(
+ () => {
+ const isLogged = useIsLogged();
+ const setTokenPair = useSetRecoilState(tokenPairState);
+
+ return {
+ isLogged,
+ setTokenPair,
+ };
+ },
+ {
+ wrapper: RecoilRoot,
+ },
+ );
+ return { result };
+};
+
+describe('useIsLogged', () => {
+ it('should return correct value', async () => {
+ const { result } = renderHooks();
+
+ expect(result.current.isLogged).toBe(false);
+
+ await act(async () => {
+ result.current.setTokenPair({
+ accessToken: {
+ expiresAt: '',
+ token: 'testToken',
+ },
+ refreshToken: {
+ expiresAt: '',
+ token: 'testToken',
+ },
+ });
+ });
+
+ expect(result.current.isLogged).toBe(true);
+ });
+});
diff --git a/packages/twenty-front/src/modules/auth/hooks/__test__/useOnboardingStatus.test.ts b/packages/twenty-front/src/modules/auth/hooks/__test__/useOnboardingStatus.test.ts
new file mode 100644
index 000000000..14349aaad
--- /dev/null
+++ b/packages/twenty-front/src/modules/auth/hooks/__test__/useOnboardingStatus.test.ts
@@ -0,0 +1,195 @@
+import { act } from 'react-dom/test-utils';
+import { renderHook } from '@testing-library/react';
+import { RecoilRoot, useSetRecoilState } from 'recoil';
+
+import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
+import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
+import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
+import { isVerifyPendingState } from '@/auth/states/isVerifyPendingState';
+import { tokenPairState } from '@/auth/states/tokenPairState';
+import { billingState } from '@/client-config/states/billingState';
+
+const tokenPair = {
+ accessToken: { token: 'accessToken', expiresAt: 'expiresAt' },
+ refreshToken: { token: 'refreshToken', expiresAt: 'expiresAt' },
+};
+const billing = {
+ billingUrl: 'testing.com',
+ isBillingEnabled: true,
+};
+const currentWorkspace = {
+ displayName: 'testing',
+ id: '1',
+ allowImpersonation: true,
+};
+const currentWorkspaceMember = {
+ id: '1',
+ locale: '',
+ name: {
+ firstName: '',
+ lastName: '',
+ },
+};
+
+const renderHooks = () => {
+ const { result } = renderHook(
+ () => {
+ const onboardingStatus = useOnboardingStatus();
+ const setBilling = useSetRecoilState(billingState);
+ const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
+ const setCurrentWorkspaceMember = useSetRecoilState(
+ currentWorkspaceMemberState,
+ );
+ const setTokenPair = useSetRecoilState(tokenPairState);
+ const setVerifyPending = useSetRecoilState(isVerifyPendingState);
+
+ return {
+ onboardingStatus,
+ setBilling,
+ setCurrentWorkspace,
+ setCurrentWorkspaceMember,
+ setTokenPair,
+ setVerifyPending,
+ };
+ },
+ {
+ wrapper: RecoilRoot,
+ },
+ );
+ return { result };
+};
+
+describe('useOnboardingStatus', () => {
+ it('should return "ongoing_user_creation" when user is not logged in', async () => {
+ const { result } = renderHooks();
+
+ expect(result.current.onboardingStatus).toBe('ongoing_user_creation');
+ });
+
+ it('should return undefined when currentWorkspaceMember in undefined', async () => {
+ const { result } = renderHooks();
+
+ act(() => {
+ result.current.setTokenPair(tokenPair);
+ });
+
+ expect(result.current.onboardingStatus).toBe(undefined);
+ });
+
+ it('should return "incomplete"', async () => {
+ const { result } = renderHooks();
+ const {
+ setTokenPair,
+ setBilling,
+ setCurrentWorkspace,
+ setCurrentWorkspaceMember,
+ } = result.current;
+
+ act(() => {
+ setTokenPair(tokenPair);
+ setBilling(billing);
+ setCurrentWorkspace({
+ ...currentWorkspace,
+ subscriptionStatus: 'incomplete',
+ });
+ setCurrentWorkspaceMember(currentWorkspaceMember);
+ });
+
+ expect(result.current.onboardingStatus).toBe('incomplete');
+ });
+
+ it('should return "canceled"', async () => {
+ const { result } = renderHooks();
+ const {
+ setTokenPair,
+ setBilling,
+ setCurrentWorkspace,
+ setCurrentWorkspaceMember,
+ } = result.current;
+
+ act(() => {
+ setTokenPair(tokenPair);
+ setBilling(billing);
+ setCurrentWorkspace({
+ ...currentWorkspace,
+ subscriptionStatus: 'canceled',
+ });
+ setCurrentWorkspaceMember(currentWorkspaceMember);
+ });
+
+ expect(result.current.onboardingStatus).toBe('canceled');
+ });
+
+ it('should return "ongoing_workspace_creation"', async () => {
+ const { result } = renderHooks();
+ const {
+ setTokenPair,
+ setBilling,
+ setCurrentWorkspace,
+ setCurrentWorkspaceMember,
+ } = result.current;
+
+ act(() => {
+ setTokenPair(tokenPair);
+ setBilling(billing);
+ setCurrentWorkspace({
+ ...currentWorkspace,
+ displayName: '',
+ subscriptionStatus: 'completed',
+ });
+ setCurrentWorkspaceMember(currentWorkspaceMember);
+ });
+
+ expect(result.current.onboardingStatus).toBe('ongoing_workspace_creation');
+ });
+
+ it('should return "ongoing_profile_creation"', async () => {
+ const { result } = renderHooks();
+ const {
+ setTokenPair,
+ setBilling,
+ setCurrentWorkspace,
+ setCurrentWorkspaceMember,
+ } = result.current;
+
+ act(() => {
+ setTokenPair(tokenPair);
+ setBilling(billing);
+ setCurrentWorkspace({
+ ...currentWorkspace,
+ subscriptionStatus: 'completed',
+ });
+ setCurrentWorkspaceMember(currentWorkspaceMember);
+ });
+
+ expect(result.current.onboardingStatus).toBe('ongoing_profile_creation');
+ });
+
+ it('should return "completed"', async () => {
+ const { result } = renderHooks();
+ const {
+ setTokenPair,
+ setBilling,
+ setCurrentWorkspace,
+ setCurrentWorkspaceMember,
+ } = result.current;
+
+ act(() => {
+ setTokenPair(tokenPair);
+ setBilling(billing);
+ setCurrentWorkspace({
+ ...currentWorkspace,
+ subscriptionStatus: 'completed',
+ });
+ setCurrentWorkspaceMember({
+ ...currentWorkspaceMember,
+ name: {
+ firstName: 'John',
+ lastName: 'Doe',
+ },
+ });
+ });
+
+ expect(result.current.onboardingStatus).toBe('completed');
+ });
+});
diff --git a/packages/twenty-front/src/modules/auth/services/__tests__/AuthService.test.ts b/packages/twenty-front/src/modules/auth/services/__tests__/AuthService.test.ts
new file mode 100644
index 000000000..50a21a2be
--- /dev/null
+++ b/packages/twenty-front/src/modules/auth/services/__tests__/AuthService.test.ts
@@ -0,0 +1,33 @@
+import { act } from 'react-dom/test-utils';
+import { enableFetchMocks } from 'jest-fetch-mock';
+
+import { renewToken } from '@/auth/services/AuthService';
+
+enableFetchMocks();
+
+const tokens = {
+ accessToken: {
+ token: 'accessToken',
+ expiresAt: 'expiresAt',
+ },
+ refreshToken: {
+ token: 'refreshToken',
+ expiresAt: 'expiresAt',
+ },
+};
+
+describe('AuthService', () => {
+ it('should renewToken', async () => {
+ fetchMock.mockResponse(() =>
+ Promise.resolve({
+ body: JSON.stringify({
+ data: { renewToken: { tokens } },
+ }),
+ }),
+ );
+ await act(async () => {
+ const res = await renewToken('http://localhost:3000', tokens);
+ expect(res).toEqual(tokens);
+ });
+ });
+});
diff --git a/packages/twenty-front/src/modules/auth/utils/__test__/passwordRegex.test.ts b/packages/twenty-front/src/modules/auth/utils/__test__/passwordRegex.test.ts
new file mode 100644
index 000000000..adf01551c
--- /dev/null
+++ b/packages/twenty-front/src/modules/auth/utils/__test__/passwordRegex.test.ts
@@ -0,0 +1,11 @@
+import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex';
+
+describe('PASSWORD_REGEX', () => {
+ it('should match passwords with at least 8 characters', () => {
+ const validPassword = 'password123';
+ const invalidPassword = '1234567';
+
+ expect(PASSWORD_REGEX.test(validPassword)).toBe(true);
+ expect(PASSWORD_REGEX.test(invalidPassword)).toBe(false);
+ });
+});
diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx
new file mode 100644
index 000000000..80cc60a60
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx
@@ -0,0 +1,119 @@
+import { act } from 'react-dom/test-utils';
+import { MemoryRouter } from 'react-router-dom';
+import { renderHook } from '@testing-library/react';
+import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil';
+
+import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
+import { commandMenuCommandsState } from '@/command-menu/states/commandMenuCommandsState';
+import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
+import { CommandType } from '@/command-menu/types/Command';
+
+const Wrapper = ({ children }: { children: React.ReactNode }) => (
+
+
+ {children}
+
+
+);
+
+const renderHooks = () => {
+ const { result } = renderHook(
+ () => {
+ const commandMenu = useCommandMenu();
+ const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
+ const [commandMenuCommands, setCommandMenuCommands] = useRecoilState(
+ commandMenuCommandsState,
+ );
+
+ return {
+ commandMenu,
+ isCommandMenuOpened,
+ commandMenuCommands,
+ setCommandMenuCommands,
+ };
+ },
+ {
+ wrapper: Wrapper,
+ },
+ );
+ return { result };
+};
+
+describe('useCommandMenu', () => {
+ it('should open and close the command menu', () => {
+ const { result } = renderHooks();
+
+ act(() => {
+ result.current.commandMenu.openCommandMenu();
+ });
+
+ expect(result.current.isCommandMenuOpened).toBe(true);
+
+ act(() => {
+ result.current.commandMenu.closeCommandMenu();
+ });
+
+ expect(result.current.isCommandMenuOpened).toBe(false);
+ });
+
+ it('should toggle the command menu', () => {
+ const { result } = renderHooks();
+
+ expect(result.current.isCommandMenuOpened).toBe(false);
+
+ act(() => {
+ result.current.commandMenu.toggleCommandMenu();
+ });
+
+ expect(result.current.isCommandMenuOpened).toBe(true);
+
+ act(() => {
+ result.current.commandMenu.toggleCommandMenu();
+ });
+
+ expect(result.current.isCommandMenuOpened).toBe(false);
+ });
+
+ it('should add commands to the menu', () => {
+ const { result } = renderHooks();
+
+ expect(
+ result.current.commandMenuCommands.find((cmd) => cmd.label === 'Test'),
+ ).toBeUndefined();
+
+ act(() => {
+ result.current.commandMenu.addToCommandMenu([
+ { label: 'Test', id: 'test', to: '/test', type: CommandType.Navigate },
+ ]);
+ });
+
+ expect(
+ result.current.commandMenuCommands.find((cmd) => cmd.label === 'Test'),
+ ).toBeDefined();
+ });
+
+ it('onItemClick', () => {
+ const { result } = renderHooks();
+ const onClickMock = jest.fn();
+
+ act(() => {
+ result.current.commandMenu.onItemClick(onClickMock, '/test');
+ });
+
+ expect(result.current.isCommandMenuOpened).toBe(true);
+ expect(onClickMock).toHaveBeenCalledTimes(1);
+ });
+
+ it('should setToIntitialCommandMenu command menu', () => {
+ const { result } = renderHooks();
+
+ act(() => {
+ result.current.commandMenu.setToIntitialCommandMenu();
+ });
+
+ expect(result.current.commandMenuCommands.length).toBe(5);
+ });
+});