Przeglądaj źródła

tests: improve coverage on newly created code

Nicolas Meienberger 2 lat temu
rodzic
commit
cbf1240512

+ 109 - 0
src/client/hooks/__tests__/useLocale.test.ts

@@ -0,0 +1,109 @@
+import nookies from 'nookies';
+import { getTRPCMock } from '@/client/mocks/getTrpcMock';
+import { server } from '@/client/mocks/server';
+import { renderHook, waitFor } from '../../../../tests/test-utils';
+import { useLocale } from '../useLocale';
+
+beforeEach(() => {
+  nookies.destroy(null, 'locale');
+});
+
+describe('test: useLocale()', () => {
+  describe('test: locale', () => {
+    it('should return users locale if logged in', async () => {
+      // arrange
+      const locale = 'fr-FR';
+      // @ts-expect-error - we're mocking the trpc context
+      server.use(getTRPCMock({ path: ['auth', 'me'], response: { locale } }));
+
+      // act
+      const { result } = renderHook(() => useLocale());
+
+      // assert
+      await waitFor(() => {
+        expect(result.current.locale).toEqual(locale);
+      });
+    });
+
+    it('should return cookie locale if not logged in', async () => {
+      // arrange
+      const locale = 'fr-FR';
+      nookies.set(null, 'locale', locale);
+      server.use(getTRPCMock({ path: ['auth', 'me'], response: null }));
+
+      // act
+      const { result } = renderHook(() => useLocale());
+
+      // assert
+      await waitFor(() => {
+        expect(result.current.locale).toEqual(locale);
+      });
+    });
+
+    it('should return browser locale if not logged in and no cookie', async () => {
+      // arrange
+      const locale = 'fr-FR';
+      jest.spyOn(window.navigator, 'language', 'get').mockReturnValueOnce(locale);
+      server.use(getTRPCMock({ path: ['auth', 'me'], response: null }));
+
+      // act
+      const { result } = renderHook(() => useLocale());
+
+      // assert
+      await waitFor(() => {
+        expect(result.current.locale).toEqual(locale);
+      });
+    });
+
+    it('should default to english if no locale is found', async () => {
+      // arrange
+      server.use(getTRPCMock({ path: ['auth', 'me'], response: null }));
+      // @ts-expect-error - we're mocking window.navigator
+      jest.spyOn(window.navigator, 'language', 'get').mockReturnValueOnce(undefined);
+
+      // act
+      const { result } = renderHook(() => useLocale());
+
+      // assert
+      await waitFor(() => {
+        expect(result.current.locale).toEqual('en');
+      });
+    });
+  });
+
+  describe('test: changeLocale()', () => {
+    it('should set the locale in the cookie', async () => {
+      // arrange
+      const locale = 'fr-FR';
+      const { result } = renderHook(() => useLocale());
+
+      // act
+      result.current.changeLocale(locale);
+
+      // assert
+      await waitFor(() => {
+        expect(nookies.get(null)).toEqual({ locale: 'fr-FR' });
+      });
+    });
+
+    it('should update the locale in the user profile when logged in', async () => {
+      // arrange
+      const locale = 'en';
+      // @ts-expect-error - we're mocking the trpc context
+      server.use(getTRPCMock({ path: ['auth', 'me'], response: { locale: 'fr-FR' } }));
+      server.use(getTRPCMock({ path: ['auth', 'changeLocale'], type: 'mutation', response: true }));
+      const { result } = renderHook(() => useLocale());
+      await waitFor(() => {
+        expect(result.current.locale).toEqual('fr-FR');
+      });
+
+      // act
+      result.current.changeLocale(locale);
+
+      // assert
+      await waitFor(() => {
+        expect(nookies.get(null)).toEqual({ locale });
+      });
+    });
+  });
+});

+ 1 - 0
src/client/messages/fr-FR.json

@@ -0,0 +1 @@
+{}

+ 3 - 8
src/client/modules/Auth/containers/LoginContainer/LoginContainer.tsx

@@ -2,6 +2,7 @@ import React, { useState } from 'react';
 import { toast } from 'react-hot-toast';
 import { toast } from 'react-hot-toast';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import { useTranslations } from 'next-intl';
 import { useTranslations } from 'next-intl';
+import type { MessageKey } from '@/server/utils/errors';
 import { trpc } from '../../../../utils/trpc';
 import { trpc } from '../../../../utils/trpc';
 import { AuthFormLayout } from '../../components/AuthFormLayout';
 import { AuthFormLayout } from '../../components/AuthFormLayout';
 import { LoginForm } from '../../components/LoginForm';
 import { LoginForm } from '../../components/LoginForm';
@@ -16,10 +17,7 @@ export const LoginContainer: React.FC = () => {
   const utils = trpc.useContext();
   const utils = trpc.useContext();
   const login = trpc.auth.login.useMutation({
   const login = trpc.auth.login.useMutation({
     onError: (e) => {
     onError: (e) => {
-      let toastMessage = e.message;
-      if (e.data?.translatedError) {
-        toastMessage = t(e.data.translatedError);
-      }
+      const toastMessage = t(e.data?.translatedError || (e.message as MessageKey));
       toast.error(toastMessage);
       toast.error(toastMessage);
     },
     },
     onSuccess: (data) => {
     onSuccess: (data) => {
@@ -34,10 +32,7 @@ export const LoginContainer: React.FC = () => {
 
 
   const verifyTotp = trpc.auth.verifyTotp.useMutation({
   const verifyTotp = trpc.auth.verifyTotp.useMutation({
     onError: (e) => {
     onError: (e) => {
-      let toastMessage = e.message;
-      if (e.data?.translatedError) {
-        toastMessage = t(e.data.translatedError);
-      }
+      const toastMessage = t(e.data?.translatedError || (e.message as MessageKey));
       toast.error(toastMessage);
       toast.error(toastMessage);
     },
     },
     onSuccess: () => {
     onSuccess: () => {

+ 2 - 4
src/client/modules/Auth/containers/RegisterContainer/RegisterContainer.tsx

@@ -3,6 +3,7 @@ import React from 'react';
 import { toast } from 'react-hot-toast';
 import { toast } from 'react-hot-toast';
 import { useLocale } from '@/client/hooks/useLocale';
 import { useLocale } from '@/client/hooks/useLocale';
 import { useTranslations } from 'next-intl';
 import { useTranslations } from 'next-intl';
+import type { MessageKey } from '@/server/utils/errors';
 import { trpc } from '../../../../utils/trpc';
 import { trpc } from '../../../../utils/trpc';
 import { AuthFormLayout } from '../../components/AuthFormLayout';
 import { AuthFormLayout } from '../../components/AuthFormLayout';
 import { RegisterForm } from '../../components/RegisterForm';
 import { RegisterForm } from '../../components/RegisterForm';
@@ -16,10 +17,7 @@ export const RegisterContainer: React.FC = () => {
   const utils = trpc.useContext();
   const utils = trpc.useContext();
   const register = trpc.auth.register.useMutation({
   const register = trpc.auth.register.useMutation({
     onError: (e) => {
     onError: (e) => {
-      let toastMessage = e.message;
-      if (e.data?.translatedError) {
-        toastMessage = t(e.data.translatedError);
-      }
+      const toastMessage = t(e.data?.translatedError || (e.message as MessageKey));
       toast.error(toastMessage);
       toast.error(toastMessage);
     },
     },
     onSuccess: () => {
     onSuccess: () => {

+ 2 - 4
src/client/modules/Auth/containers/ResetPasswordContainer/ResetPasswordContainer.tsx

@@ -2,6 +2,7 @@ import { useRouter } from 'next/router';
 import React from 'react';
 import React from 'react';
 import { toast } from 'react-hot-toast';
 import { toast } from 'react-hot-toast';
 import { useTranslations } from 'next-intl';
 import { useTranslations } from 'next-intl';
+import type { MessageKey } from '@/server/utils/errors';
 import { Button } from '../../../../components/ui/Button';
 import { Button } from '../../../../components/ui/Button';
 import { trpc } from '../../../../utils/trpc';
 import { trpc } from '../../../../utils/trpc';
 import { AuthFormLayout } from '../../components/AuthFormLayout';
 import { AuthFormLayout } from '../../components/AuthFormLayout';
@@ -22,10 +23,7 @@ export const ResetPasswordContainer: React.FC<Props> = ({ isRequested }) => {
       utils.auth.checkPasswordChangeRequest.invalidate();
       utils.auth.checkPasswordChangeRequest.invalidate();
     },
     },
     onError: (e) => {
     onError: (e) => {
-      let toastMessage = e.message;
-      if (e.data?.translatedError) {
-        toastMessage = t(e.data.translatedError);
-      }
+      const toastMessage = t(e.data?.translatedError || (e.message as MessageKey));
       toast.error(toastMessage);
       toast.error(toastMessage);
     },
     },
   });
   });

+ 61 - 2
src/client/utils/__tests__/page-helpers.test.ts

@@ -1,6 +1,10 @@
-import { getAuthedPageProps } from '../page-helpers';
+import nookies from 'nookies';
+import merge from 'lodash.merge';
+import { getAuthedPageProps, getMessagesPageProps } from '../page-helpers';
+import englishMessages from '../../messages/en.json';
+import frenchMessages from '../../messages/fr-FR.json';
 
 
-describe('getAuthedPageProps', () => {
+describe('test: getAuthedPageProps()', () => {
   it('should redirect to /login if there is no user id in session', async () => {
   it('should redirect to /login if there is no user id in session', async () => {
     // arrange
     // arrange
     const ctx = { req: { session: {} } };
     const ctx = { req: { session: {} } };
@@ -26,3 +30,58 @@ describe('getAuthedPageProps', () => {
     expect(props).toEqual({});
     expect(props).toEqual({});
   });
   });
 });
 });
+
+describe('test: getMessagesPageProps()', () => {
+  beforeEach(() => {
+    nookies.destroy(null, 'locale');
+  });
+
+  it('should return correct messages if the locale is in the session', async () => {
+    // arrange
+    const ctx = { req: { session: { locale: 'fr' }, headers: {} } };
+
+    // act
+    // @ts-expect-error - we're passing in a partial context
+    const { props } = await getMessagesPageProps(ctx);
+
+    // assert
+    expect(props.messages).toEqual(merge(frenchMessages, englishMessages));
+  });
+
+  it('should return correct messages if the locale in the cookie', async () => {
+    // arrange
+    const ctx = { req: { session: {}, headers: {} } };
+    nookies.set(null, 'locale', 'fr-FR');
+
+    // act
+    // @ts-expect-error - we're passing in a partial context
+    const { props } = await getMessagesPageProps(ctx);
+
+    // assert
+    expect(props.messages).toEqual(merge(frenchMessages, englishMessages));
+  });
+
+  it('should return correct messages if the locale is detected from the browser', async () => {
+    // arrange
+    const ctx = { req: { session: {}, headers: { 'accept-language': 'fr-FR' } } };
+
+    // act
+    // @ts-expect-error - we're passing in a partial context
+    const { props } = await getMessagesPageProps(ctx);
+
+    // assert
+    expect(props.messages).toEqual(merge(frenchMessages, englishMessages));
+  });
+
+  it('should default to english messages if the locale is not found', async () => {
+    // arrange
+    const ctx = { req: { session: {}, headers: {} } };
+
+    // act
+    // @ts-expect-error - we're passing in a partial context
+    const { props } = await getMessagesPageProps(ctx);
+
+    // assert
+    expect(props.messages).toEqual(englishMessages);
+  });
+});

+ 36 - 1
src/server/routers/auth/auth.router.test.ts

@@ -84,7 +84,7 @@ describe('Test: register', () => {
 
 
     // act
     // act
     try {
     try {
-      await caller.register({ username: 'test@test.com', password: '123' });
+      await caller.register({ username: 'test@test.com', password: '123', locale: 'en' });
     } catch (e) {
     } catch (e) {
       error = e as { code: string };
       error = e as { code: string };
     }
     }
@@ -327,3 +327,38 @@ describe('Test: resetPassword', () => {
     expect(error?.code).not.toBe('UNAUTHORIZED');
     expect(error?.code).not.toBe('UNAUTHORIZED');
   });
   });
 });
 });
+
+describe('Test: changeLocale', () => {
+  it('should not be accessible without an account', async () => {
+    // arrange
+    const caller = authRouter.createCaller(fromPartial({ req: { session: {} } }));
+    let error;
+
+    // act
+    try {
+      await caller.changeLocale({ locale: 'en' });
+    } catch (e) {
+      error = e as { code: string };
+    }
+
+    // assert
+    expect(error?.code).toBe('UNAUTHORIZED');
+  });
+
+  it('should be accessible with an account', async () => {
+    // arrange
+    await createUser({ id: 122, locale: 'en' }, db);
+    const caller = authRouter.createCaller(fromPartial({ req: { session: { userId: 122 } } }));
+    let error;
+
+    // act
+    try {
+      await caller.changeLocale({ locale: 'fr-FR' });
+    } catch (e) {
+      error = e as { code: string };
+    }
+
+    // assert
+    expect(error?.code).not.toBe('UNAUTHORIZED');
+  });
+});

+ 34 - 0
src/server/services/auth/auth.service.test.ts

@@ -687,3 +687,37 @@ describe('Test: changePassword', () => {
     expect(sessions).toHaveLength(0);
     expect(sessions).toHaveLength(0);
   });
   });
 });
 });
+
+describe('test: changeLocale()', () => {
+  it('should change the locale of the user', async () => {
+    // arrange
+    const email = faker.internet.email();
+    const user = await createUser({ email }, database);
+    const locale = 'fr-FR';
+
+    // act
+    await AuthService.changeLocale({ userId: user.id, locale });
+
+    // assert
+    const updatedUser = await getUserById(user.id, database);
+    expect(updatedUser?.locale).toBe(locale);
+  });
+
+  it('should throw if the user does not exist', async () => {
+    // arrange
+    const locale = 'fr-FR';
+
+    // act & assert
+    await expect(AuthService.changeLocale({ userId: 1, locale })).rejects.toThrowError('server-messages.errors.user-not-found');
+  });
+
+  it('should throw if the locale is invalid', async () => {
+    // arrange
+    const email = faker.internet.email();
+    const user = await createUser({ email }, database);
+    const locale = 'invalid';
+
+    // act & assert
+    await expect(AuthService.changeLocale({ userId: user.id, locale })).rejects.toThrowError('server-messages.errors.invalid-locale');
+  });
+});