diff --git a/packages/manager/apps/procedures/package.json b/packages/manager/apps/procedures/package.json index dfa68d879aae..9332d9df586c 100644 --- a/packages/manager/apps/procedures/package.json +++ b/packages/manager/apps/procedures/package.json @@ -45,6 +45,7 @@ "@tanstack/react-query-devtools": "^5.51.21", "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^14.1.2", + "@testing-library/react-hooks": "^8.0.1", "@vitejs/plugin-react": "^4.2.1", "@vitest/coverage-v8": "^1.2.0", "element-internals-polyfill": "^1.3.10", diff --git a/packages/manager/apps/procedures/src/components/Loading/Loading.test.tsx b/packages/manager/apps/procedures/src/components/Loading/Loading.test.tsx new file mode 100644 index 000000000000..b7e45d524463 --- /dev/null +++ b/packages/manager/apps/procedures/src/components/Loading/Loading.test.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import Loading from './Loading'; + +describe('Loading', () => { + it('should render with the correct layout', () => { + const { container } = render(); + + expect( + container.querySelector('.flex.justify-center.items-center'), + ).toBeInTheDocument(); + expect(container.querySelector('.w-28')).toBeInTheDocument(); + }); +}); diff --git a/packages/manager/apps/procedures/src/components/Loading/SkeletonLoading.test.tsx b/packages/manager/apps/procedures/src/components/Loading/SkeletonLoading.test.tsx new file mode 100644 index 000000000000..5f189d99f748 --- /dev/null +++ b/packages/manager/apps/procedures/src/components/Loading/SkeletonLoading.test.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { SkeletonLoading } from './SkeletonLoading'; + +describe('SkeletonLoading Component', () => { + it('should render the correct number of skeleton loaders', () => { + const { container } = render(); + + const skeletons = container.querySelectorAll('osds-skeleton'); + expect(skeletons).toHaveLength(7); + }); + + it('should have the correct wrapper classes', () => { + const { container } = render(); + const wrapper = container.firstChild; + expect(wrapper).toHaveClass('w-[100%]', 'p-10'); + }); +}); diff --git a/packages/manager/apps/procedures/src/context/User/modals/ExpiredSessionModal.test.tsx b/packages/manager/apps/procedures/src/context/User/modals/ExpiredSessionModal.test.tsx new file mode 100644 index 000000000000..0a6e622be107 --- /dev/null +++ b/packages/manager/apps/procedures/src/context/User/modals/ExpiredSessionModal.test.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { vi } from 'vitest'; +import { ExpiredSessionModal } from './ExpiredSessionModal'; + +const onCloseMock = vi.fn(); + +describe('ExpiredSessionModal', () => { + it('should render the modal with the correct content', () => { + render(); + + expect( + screen.getByText('account-disable-2fa-session-modal-expired-title'), + ).toBeInTheDocument(); + + expect( + screen.getByText('account-disable-2fa-session-modal-expired-message'), + ).toBeInTheDocument(); + + expect( + screen.getByText('account-disable-2fa-session-modal-expired-auth-button'), + ).toBeInTheDocument(); + }); + + it('should call onClose when the button is clicked', () => { + render(); + + const button = screen.getByText( + 'account-disable-2fa-session-modal-expired-auth-button', + ); + fireEvent.click(button); + + expect(onCloseMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/manager/apps/procedures/src/context/User/modals/SessionModals.test.tsx b/packages/manager/apps/procedures/src/context/User/modals/SessionModals.test.tsx new file mode 100644 index 000000000000..f5abce322bdf --- /dev/null +++ b/packages/manager/apps/procedures/src/context/User/modals/SessionModals.test.tsx @@ -0,0 +1,151 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { vi } from 'vitest'; +import { useSessionModal } from '../useSessionModal'; +import userContext, { User } from '../context'; +import { getRedirectLoginUrl } from '@/utils/url-builder'; +import { SessionModals } from './SessionModals'; + +vi.mock('../useSessionModal', () => ({ + useSessionModal: vi.fn(), +})); + +vi.mock('@/utils/url-builder', () => ({ + getRedirectLoginUrl: vi.fn(), +})); + +vi.mock('./ExpiredSessionModal', () => ({ + ExpiredSessionModal: ({ onClose }: { onClose: () => void }) => ( +
+ +
+ ), +})); + +vi.mock('./WarningSessionModal', () => ({ + WarningSessionModal: ({ onClose }: { onClose: () => void }) => ( +
+ +
+ ), +})); + +const mockUser: Partial = { + legalForm: 'administration', + email: 'test@example.com', + language: 'en', + subsidiary: 'FR', +}; + +describe('SessionModals', () => { + const mockSetShowExpiredModal = vi.fn(); + const mockSetShowWarningModal = vi.fn(); + + beforeEach(() => { + (useSessionModal as any).mockReturnValue({ + setShowExpiredModal: mockSetShowExpiredModal, + setShowWarningModal: mockSetShowWarningModal, + showExpiredModal: false, + showWarningModal: false, + }); + + (getRedirectLoginUrl as any).mockReturnValue('https://login.url'); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should not display any modals by default', () => { + render( + + + , + ); + + expect( + screen.queryByTestId('expired-session-modal'), + ).not.toBeInTheDocument(); + expect( + screen.queryByTestId('warning-session-modal'), + ).not.toBeInTheDocument(); + }); + + it('should display the ExpiredSessionModal when showExpiredModal is true', () => { + (useSessionModal as any).mockReturnValue({ + setShowExpiredModal: mockSetShowExpiredModal, + setShowWarningModal: mockSetShowWarningModal, + showExpiredModal: true, + showWarningModal: false, + }); + + render( + + + , + ); + + expect(screen.getByTestId('expired-session-modal')).toBeInTheDocument(); + }); + + it('should close the ExpiredSessionModal and redirect when the close button is clicked', async () => { + (useSessionModal as any).mockReturnValue({ + setShowExpiredModal: mockSetShowExpiredModal, + setShowWarningModal: mockSetShowWarningModal, + showExpiredModal: true, + showWarningModal: false, + }); + + render( + + + , + ); + + const closeButton = screen.getByText('Close Expired Modal'); + fireEvent.click(closeButton); + + await vi.waitFor(() => { + expect(mockSetShowExpiredModal).toHaveBeenCalledWith(false); + expect(getRedirectLoginUrl).toHaveBeenCalledWith(mockUser); + }); + }); + + it('should display the WarningSessionModal when showWarningModal is true', () => { + (useSessionModal as any).mockReturnValue({ + setShowExpiredModal: mockSetShowExpiredModal, + setShowWarningModal: mockSetShowWarningModal, + showExpiredModal: false, + showWarningModal: true, + }); + + render( + + + , + ); + + expect(screen.getByTestId('warning-session-modal')).toBeInTheDocument(); + }); + + it('should close the WarningSessionModal when the close button is clicked', () => { + (useSessionModal as any).mockReturnValue({ + setShowExpiredModal: mockSetShowExpiredModal, + setShowWarningModal: mockSetShowWarningModal, + showExpiredModal: false, + showWarningModal: true, + }); + + render( + + + , + ); + + const closeButton = screen.getByText('Close Warning Modal'); + fireEvent.click(closeButton); + + expect(mockSetShowWarningModal).toHaveBeenCalledWith(false); + }); +}); diff --git a/packages/manager/apps/procedures/src/context/User/modals/SessionModals.tsx b/packages/manager/apps/procedures/src/context/User/modals/SessionModals.tsx index c416ccb96f80..95c079d0f70d 100644 --- a/packages/manager/apps/procedures/src/context/User/modals/SessionModals.tsx +++ b/packages/manager/apps/procedures/src/context/User/modals/SessionModals.tsx @@ -6,7 +6,9 @@ import { WarningSessionModal } from './WarningSessionModal'; import userContext from '../context'; export const SessionModals: FunctionComponent = () => { - const { user } = useContext(userContext); + const context = useContext(userContext); + + const { user } = context; const { setShowExpiredModal, setShowWarningModal, diff --git a/packages/manager/apps/procedures/src/context/User/modals/WarningSessionModal.test.tsx b/packages/manager/apps/procedures/src/context/User/modals/WarningSessionModal.test.tsx new file mode 100644 index 000000000000..5a746c785dee --- /dev/null +++ b/packages/manager/apps/procedures/src/context/User/modals/WarningSessionModal.test.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { vi } from 'vitest'; +import { WarningSessionModal } from './WarningSessionModal'; + +const onCloseMock = vi.fn(); + +describe('WarningSessionModal', () => { + it('should render the modal with the correct content', () => { + render(); + + expect( + screen.getByText('account-disable-2fa-session-modal-warning-message'), + ).toBeInTheDocument(); + + expect( + screen.getByText('account-disable-2fa-session-modal-warning-ok-button'), + ).toBeInTheDocument(); + }); + + it('should call onClose when the button is clicked', () => { + render(); + + const button = screen.getByText( + 'account-disable-2fa-session-modal-warning-ok-button', + ); + fireEvent.click(button); + + expect(onCloseMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/manager/apps/procedures/src/context/User/useSessionModal.test.tsx b/packages/manager/apps/procedures/src/context/User/useSessionModal.test.tsx new file mode 100644 index 000000000000..fce6f89fc8fe --- /dev/null +++ b/packages/manager/apps/procedures/src/context/User/useSessionModal.test.tsx @@ -0,0 +1,137 @@ +import { vi } from 'vitest'; +import { renderHook } from '@testing-library/react-hooks'; +import { useSessionModal } from '@/context/User/useSessionModal'; +import { useFetchServerTime } from '@/data/hooks/useUtils'; +import { User } from '@/context/User/context'; + +vi.mock('@/data/hooks/useUtils', () => ({ + useFetchServerTime: vi.fn(), +})); + +const fakeUrl = 'http://example.com/login'; + +vi.mock('@/utils/url-builder', () => ({ + getRedirectLoginUrl: vi.fn(() => fakeUrl), +})); + +const assignMock = vi.fn(); + +describe('useSessionModal', () => { + const mockUser: Partial = { + exp: 0.002, + iat: 0, + }; + + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + vi.setSystemTime(0); + Object.defineProperty(window, 'location', { + value: { + ...window.location, + assign: assignMock, + }, + writable: true, + }); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('should initialize modal states as false', () => { + (useFetchServerTime as jest.Mock).mockReturnValue({ + data: 0, + isError: false, + isFetched: false, + }); + + const { result } = renderHook(() => useSessionModal(mockUser as User, 0.5)); + + expect(result.current.showWarningModal).toBe(false); + expect(result.current.showExpiredModal).toBe(false); + }); + + it('should redirect in expected url when the user is undefined', async () => { + (useFetchServerTime as jest.Mock).mockReturnValue({ + data: undefined, + isError: true, + isFetched: true, + }); + + renderHook(() => useSessionModal(undefined, 0.5)); + + await vi.waitFor(() => { + expect(assignMock).toHaveBeenCalledWith(fakeUrl); + }); + }); + + it('should redirect in expected url when remaining Expire Time is reached', async () => { + const serverTime = 0; + mockUser.exp = -1; + (useFetchServerTime as jest.Mock).mockReturnValue({ + data: serverTime, + isError: false, + isFetched: true, + }); + + renderHook(() => useSessionModal(mockUser as User, 0.5)); + + await vi.waitFor(() => { + expect(assignMock).toHaveBeenCalledWith(fakeUrl); + }); + }); + + it('should show the warning modal when warning time is reached', async () => { + mockUser.exp = 0.1; + const serverTime = 0; + + (useFetchServerTime as jest.Mock).mockReturnValue({ + data: serverTime, + isError: false, + isFetched: true, + }); + + const { result } = renderHook(() => useSessionModal(mockUser as User, 0.1)); + + await vi.waitFor(() => { + expect(result.current.showWarningModal).toBe(true); + expect(result.current.showExpiredModal).toBe(false); + }); + }); + + it('should show the expired modal when expired time is reached', async () => { + mockUser.exp = 0.1; + const serverTime = 0; + + (useFetchServerTime as jest.Mock).mockReturnValue({ + data: serverTime, + isError: false, + isFetched: true, + }); + + const { result } = renderHook(() => useSessionModal(mockUser as User, 0.5)); + + await vi.waitFor(() => { + expect(result.current.showExpiredModal).toBe(true); + }); + }); + + it('should show the expired modal and warning modal when times are reached', async () => { + mockUser.exp = 1; + const serverTime = 0.5; + + (useFetchServerTime as jest.Mock).mockReturnValue({ + data: serverTime, + isError: false, + isFetched: true, + }); + + const { result } = renderHook(() => useSessionModal(mockUser as User, 0.5)); + + await vi.waitFor(() => { + expect(result.current.showWarningModal).toBe(true); + expect(result.current.showExpiredModal).toBe(true); + }); + }); +}); diff --git a/packages/manager/apps/procedures/src/context/User/useUser.test.tsx b/packages/manager/apps/procedures/src/context/User/useUser.test.tsx new file mode 100644 index 000000000000..7b2e4afe3611 --- /dev/null +++ b/packages/manager/apps/procedures/src/context/User/useUser.test.tsx @@ -0,0 +1,34 @@ +import React, { FunctionComponent } from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import userContext, { UserContext } from '@/context/User/context'; +import useUserContext from './useUser'; + +const mockContextValue: UserContext = { + user: { + legalForm: 'administration', + email: 'johndoe@example.com', + language: 'en', + subsidiary: 'FR', + iss: 'issuer.example.com', + sub: '12345', + exp: 1700000000, + nbf: 1699990000, + iat: 1699980000, + }, +}; + +const { Provider } = userContext; + +const wrapper: FunctionComponent = ({ children }: any) => ( + {children} +); + +describe('useUserContext', () => { + it('should return the user context', () => { + const { result } = renderHook(() => useUserContext(), { + wrapper, + }); + + expect(result.current.user).toEqual(mockContextValue.user); + }); +}); diff --git a/packages/manager/apps/procedures/src/pages/disableMFA/create/confirm/Confirm.page.tsx b/packages/manager/apps/procedures/src/pages/disableMFA/create/confirm/Confirm.page.tsx deleted file mode 100644 index 2ff7c60e2583..000000000000 --- a/packages/manager/apps/procedures/src/pages/disableMFA/create/confirm/Confirm.page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export default function ConfirmCreateRequest() { - return

Confirm request creation

; -} diff --git a/packages/manager/apps/procedures/src/pages/disableMFA/error/Error.page.test.tsx b/packages/manager/apps/procedures/src/pages/disableMFA/error/Error.page.test.tsx new file mode 100644 index 000000000000..c2a97fe76c47 --- /dev/null +++ b/packages/manager/apps/procedures/src/pages/disableMFA/error/Error.page.test.tsx @@ -0,0 +1,19 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import ErrorPage from './Error.page'; + +describe('ErrorPage Component', () => { + it('should render all text elements with the correct translation keys', () => { + render(); + + expect( + screen.getByText('account-disable-2fa-error-validation'), + ).toBeInTheDocument(); + expect( + screen.getByText('account-disable-2fa-error-verification'), + ).toBeInTheDocument(); + expect( + screen.getByText('account-disable-2fa-error-suggestion'), + ).toBeInTheDocument(); + }); +}); diff --git a/packages/manager/apps/procedures/src/pages/disableMFA/error/Error.page.tsx b/packages/manager/apps/procedures/src/pages/disableMFA/error/Error.page.tsx index f58975067611..44e1e0601d82 100644 --- a/packages/manager/apps/procedures/src/pages/disableMFA/error/Error.page.tsx +++ b/packages/manager/apps/procedures/src/pages/disableMFA/error/Error.page.tsx @@ -7,7 +7,7 @@ import { } from '@ovhcloud/ods-common-theming'; import { ODS_TEXT_SIZE } from '@ovhcloud/ods-components'; -export default function SeeRequest() { +export default function ErrorPage() { const { t } = useTranslation('account-disable-2fa/error'); return ( diff --git a/packages/manager/apps/procedures/src/pages/disableMFA/see/See.page.test.tsx b/packages/manager/apps/procedures/src/pages/disableMFA/see/See.page.test.tsx new file mode 100644 index 000000000000..a04451705c33 --- /dev/null +++ b/packages/manager/apps/procedures/src/pages/disableMFA/see/See.page.test.tsx @@ -0,0 +1,20 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import SeeRequest from './See.page'; + +describe('SeeRequest Component', () => { + it('should render all text elements with the correct translation keys', () => { + render(); + + expect( + screen.getByText('account-disable-2fa-see-title'), + ).toBeInTheDocument(); + + expect( + screen.getByText('account-disable-2fa-see-description1'), + ).toBeInTheDocument(); + expect( + screen.getByText('account-disable-2fa-see-description2'), + ).toBeInTheDocument(); + }); +}); diff --git a/packages/manager/apps/procedures/src/routes/routes.tsx b/packages/manager/apps/procedures/src/routes/routes.tsx index a81c18f1c4c0..7c1ae3635e30 100644 --- a/packages/manager/apps/procedures/src/routes/routes.tsx +++ b/packages/manager/apps/procedures/src/routes/routes.tsx @@ -33,12 +33,6 @@ export const Routes: any = [ import('@/pages/disableMFA/create/Create.page'), ), children: [ - { - path: 'confirm', - ...lazyRouteConfig(() => - import('@/pages/disableMFA/create/confirm/Confirm.page'), - ), - }, { path: '', ...lazyRouteConfig(() =>