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); + }); +});