diff --git a/.gitleaksignore b/.gitleaksignore index cceb449a3..04b800c59 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -1,3 +1,4 @@ # SEE: https://github.com/gitleaks/gitleaks/blob/master/README.md#gitleaksignore cd9c0efec38c5d63053dd865e5d4e207c0760d91:docs/guides/Perform_static_analysis.md:generic-api-key:37 +src/__tests__/components/molecules/LoginStatus.test.tsx:jwt:23 \ No newline at end of file diff --git a/src/__tests__/app/auth/__snapshots__/page.test.tsx.snap b/src/__tests__/app/auth/__snapshots__/page.test.tsx.snap new file mode 100644 index 000000000..b58c508b1 --- /dev/null +++ b/src/__tests__/app/auth/__snapshots__/page.test.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MockAuthPage 1`] = ` + +

+ redirect +

+
+`; diff --git a/src/__tests__/app/auth/page.test.tsx b/src/__tests__/app/auth/page.test.tsx new file mode 100644 index 000000000..5ed5d93b9 --- /dev/null +++ b/src/__tests__/app/auth/page.test.tsx @@ -0,0 +1,19 @@ +import { ReactNode, ComponentType } from 'react'; +import MockAuthPage from '@app/auth/page.dev'; +import { render } from '@testing-library/react'; + +jest.mock('@aws-amplify/ui-react', () => ({ + Authenticator: { + Provider: ({ children }: { children: ReactNode }) => children, + }, + withAuthenticator: (Component: ComponentType) => Component, +})); +jest.mock('@molecules/Redirect/Redirect', () => ({ + Redirect: () =>

redirect

, +})); + +test('MockAuthPage', () => { + const container = render(); + + expect(container.asFragment()).toMatchSnapshot(); +}); diff --git a/src/__tests__/app/auth/signout/__snapshots__/page.test.tsx.snap b/src/__tests__/app/auth/signout/__snapshots__/page.test.tsx.snap new file mode 100644 index 000000000..16df702b1 --- /dev/null +++ b/src/__tests__/app/auth/signout/__snapshots__/page.test.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MockSignoutPage 1`] = ` + +

+ redirect +

+
+`; diff --git a/src/__tests__/app/auth/signout/page.test.tsx b/src/__tests__/app/auth/signout/page.test.tsx new file mode 100644 index 000000000..34ec2ceb0 --- /dev/null +++ b/src/__tests__/app/auth/signout/page.test.tsx @@ -0,0 +1,29 @@ +import { ReactNode, ComponentType } from 'react'; +import MockSignoutPage from '@app/auth/signout/page.dev'; +import { render, screen } from '@testing-library/react'; + +jest.mock('@aws-amplify/auth', () => ({ + signOut: () => Promise.resolve(), +})); + +jest.mock('@molecules/Redirect/Redirect', () => ({ + Redirect: () =>

redirect

, +})); + +jest.mock('@aws-amplify/ui-react', () => ({ + Authenticator: { + Provider: ({ children }: { children: ReactNode }) => children, + }, + withAuthenticator: (Component: ComponentType) => Component, +})); +jest.mock('@molecules/Redirect/Redirect', () => ({ + Redirect: () =>

redirect

, +})); + +test('MockSignoutPage', async () => { + const container = render(); + + await screen.findByText('redirect'); + + expect(container.asFragment()).toMatchSnapshot(); +}); diff --git a/src/__tests__/components/molecules/LoginStatus.test.tsx b/src/__tests__/components/molecules/LoginStatus.test.tsx new file mode 100644 index 000000000..fd23ef4dc --- /dev/null +++ b/src/__tests__/components/molecules/LoginStatus.test.tsx @@ -0,0 +1,28 @@ +/* eslint-disable unicorn/no-document-cookie */ +import { render } from '@testing-library/react'; +import { LoginStatus } from '@molecules/LoginStatus/LoginStatus'; + +test('LoginStatus - no cookie', () => { + document.cookie = ''; + + const container = render(); + + expect(container.asFragment()).toMatchSnapshot(); +}); + +test('LoginStatus - invalid cookie', () => { + document.cookie = 'CognitoIdentityServiceProvider.idToken=lemons'; + + const container = render(); + + expect(container.asFragment()).toMatchSnapshot(); +}); + +test('LoginStatus - valid cookie', () => { + document.cookie = + 'CognitoIdentityServiceProvider.idToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImxvY2FsaG9zdEBuaHMubmV0In0.R0rk7pjJoU07efveI4p6W-xrTM-BnP8N-pU-RYczPBA'; + + const container = render(); + + expect(container.asFragment()).toMatchSnapshot(); +}); diff --git a/src/__tests__/components/molecules/Redirect.test.tsx b/src/__tests__/components/molecules/Redirect.test.tsx new file mode 100644 index 000000000..d5de0b140 --- /dev/null +++ b/src/__tests__/components/molecules/Redirect.test.tsx @@ -0,0 +1,40 @@ +import { mockDeep } from 'jest-mock-extended'; +import { render } from '@testing-library/react'; +import { Redirect } from '@molecules/Redirect/Redirect'; +import { + useSearchParams, + ReadonlyURLSearchParams, + redirect, +} from 'next/navigation'; + +jest.mock('next/navigation', () => ({ + ...jest.requireActual('next/navigation'), + redirect: jest.fn(), + useSearchParams: jest.fn(), +})); + +test('Redirect - URL provided', () => { + const mockRedirect = jest.fn(mockDeep()); + jest.mocked(redirect).mockImplementation(mockRedirect); + + const mockSearchParams = new ReadonlyURLSearchParams({ + redirect: 'redirect', + }); + jest.mocked(useSearchParams).mockReturnValue(mockSearchParams); + + render(); + + expect(mockRedirect).toHaveBeenCalledWith('redirect'); +}); + +test('Redirect - URL not provided', () => { + const mockRedirect = jest.fn(mockDeep()); + jest.mocked(redirect).mockImplementation(mockRedirect); + + const mockSearchParams = new ReadonlyURLSearchParams({}); + jest.mocked(useSearchParams).mockReturnValue(mockSearchParams); + + render(); + + expect(mockRedirect).toHaveBeenCalledWith('/'); +}); diff --git a/src/__tests__/components/molecules/__snapshots__/LoginStatus.test.tsx.snap b/src/__tests__/components/molecules/__snapshots__/LoginStatus.test.tsx.snap new file mode 100644 index 000000000..956bb051c --- /dev/null +++ b/src/__tests__/components/molecules/__snapshots__/LoginStatus.test.tsx.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LoginStatus - invalid cookie 1`] = ` + +
  • + + Sign in + +
  • +
    +`; + +exports[`LoginStatus - no cookie 1`] = ` + +
  • + + Sign in + +
  • +
    +`; + +exports[`LoginStatus - valid cookie 1`] = ` + + +
  • + + Sign out + +
  • +
    +`; diff --git a/src/app/auth/page.dev.tsx b/src/app/auth/page.dev.tsx index 73e95c951..1a374a6f7 100644 --- a/src/app/auth/page.dev.tsx +++ b/src/app/auth/page.dev.tsx @@ -3,50 +3,24 @@ 'use client'; import { Amplify } from 'aws-amplify'; -import { Suspense, useEffect } from 'react'; -import { useSearchParams } from 'next/navigation'; +import { Suspense } from 'react'; import { Authenticator, withAuthenticator } from '@aws-amplify/ui-react'; import '@aws-amplify/ui-react/styles.css'; +import { Redirect } from '@molecules/Redirect/Redirect'; Amplify.configure(require('@/amplify_outputs.json'), { ssr: true }); -const Redirect = () => { - const searchParams = useSearchParams(); - - const redirect = searchParams.get('redirect') ?? '/'; - - useEffect(() => { - location.href = redirect; - }, [redirect]); - - if (redirect) { - return ( -

    - Redirecting to{' '} - - {redirect} - -

    - ); - } -}; - -const WrappedRedirect = () => ( - - - -); - -const MockAuthPage = () => { - return withAuthenticator(WrappedRedirect, { +const MockAuthPage = () => + withAuthenticator(Redirect, { variation: 'default', hideSignUp: true, })({}); -}; const WrappedAuthPage = () => ( - + + + ); diff --git a/src/app/auth/signout/page.dev.tsx b/src/app/auth/signout/page.dev.tsx index c554fde7b..9d3b00119 100644 --- a/src/app/auth/signout/page.dev.tsx +++ b/src/app/auth/signout/page.dev.tsx @@ -4,34 +4,13 @@ import { Amplify } from 'aws-amplify'; import { Suspense, useState, useEffect } from 'react'; -import { useSearchParams } from 'next/navigation'; import { signOut } from '@aws-amplify/auth'; import { Authenticator } from '@aws-amplify/ui-react'; +import { Redirect } from '@molecules/Redirect/Redirect'; Amplify.configure(require('@/amplify_outputs.json'), { ssr: true }); -const Redirect = () => { - const searchParams = useSearchParams(); - - const redirect = searchParams.get('redirect') ?? '/'; - - useEffect(() => { - location.href = redirect; - }, [redirect]); - - if (redirect) { - return ( -

    - Redirecting to{' '} - - {redirect} - -

    - ); - } -}; - -const MockAuthPage = () => { +const MockSignoutPage = () => { const [signedOut, setSignedOut] = useState(false); useEffect(() => { @@ -42,20 +21,15 @@ const MockAuthPage = () => { } }); - if (signedOut) { - return ( - - - - ); - } - return

    Signing out

    ; + return signedOut ? :

    Signing out

    ; }; -const WrappedAuthPage = () => ( +const WrappedSignoutPage = () => ( - + + + ); -export default WrappedAuthPage; +export default WrappedSignoutPage; diff --git a/src/components/molecules/Header/Header.tsx b/src/components/molecules/Header/Header.tsx index ca65578f6..25f658f4f 100644 --- a/src/components/molecules/Header/Header.tsx +++ b/src/components/molecules/Header/Header.tsx @@ -1,7 +1,7 @@ import Link from 'next/link'; import concatClassNames from '@utils/concat-class-names'; import content from '@content/content'; -import LoginStatus from '@molecules/LoginStatus/LoginStatus'; +import { LoginStatus } from '@molecules/LoginStatus/LoginStatus'; import styles from './Header.module.scss'; import { HeaderType } from './header.types'; diff --git a/src/components/molecules/LoginStatus/LoginStatus.tsx b/src/components/molecules/LoginStatus/LoginStatus.tsx index 32d6ca2c9..586efc160 100644 --- a/src/components/molecules/LoginStatus/LoginStatus.tsx +++ b/src/components/molecules/LoginStatus/LoginStatus.tsx @@ -9,13 +9,16 @@ import cookie from 'cookie'; import { getAuthBasePath, getBasePath } from '@utils/get-base-path'; import { usePathname } from 'next/navigation'; -const getLoggedInUser = (cookieString: string) => { - const cookies = cookie.parse(cookieString); - - if (!cookies) { - return; +const decodeCookie = (cookieValue: string) => { + try { + return jwtDecode(cookieValue); + } catch (error) { + console.error(error); } +}; +const getLoggedInUser = (cookieString: string) => { + const cookies = cookie.parse(cookieString); const idTokenCookieName = Object.keys(cookies).find( (cookieName) => cookieName.includes('CognitoIdentityServiceProvider') && @@ -27,14 +30,7 @@ const getLoggedInUser = (cookieString: string) => { } const idTokenCookieValue = cookies[idTokenCookieName]; - - if (!idTokenCookieValue) { - return; - } - - const idTokenCookieDecoded = jwtDecode( - idTokenCookieValue - ); + const idTokenCookieDecoded = decodeCookie(idTokenCookieValue); if (!idTokenCookieDecoded) { return; @@ -43,7 +39,7 @@ const getLoggedInUser = (cookieString: string) => { return idTokenCookieDecoded.email; }; -export default function LoginStatus() { +export const LoginStatus = () => { const [browserCookie, setBrowserCookie] = useState(''); const pathname = usePathname(); @@ -76,4 +72,4 @@ export default function LoginStatus() { Sign in ); -} +}; diff --git a/src/components/molecules/Redirect/Redirect.tsx b/src/components/molecules/Redirect/Redirect.tsx new file mode 100644 index 000000000..2bc0d56af --- /dev/null +++ b/src/components/molecules/Redirect/Redirect.tsx @@ -0,0 +1,12 @@ +'use client'; + +import { getBasePath } from '@utils/get-base-path'; +import { useSearchParams, redirect } from 'next/navigation'; + +export const Redirect = () => { + const searchParams = useSearchParams(); + + const redirectPath = searchParams.get('redirect') ?? '/'; + + redirect(redirectPath.replace(getBasePath(), '')); +};