Skip to content

Commit

Permalink
(GAP-1984) Adds Navbar for authenticated users (#71)
Browse files Browse the repository at this point in the history
* initial

* import

* amend import

* add a couple of mocks

* rm

* add logout tests

* assert on axios

* add back button, rm breadcrumbs

* run prettier

* rm

* setup auth context, mock next-config in all tests

* hide sign out when OL disabled

* useauth

* fix predicate

* get ol from runtimeConfig

* refactor env vars - use useContext

* update env example

* update env example

* update env example
  • Loading branch information
john-tco authored Nov 9, 2023
1 parent 4ec9d1b commit 63237c3
Show file tree
Hide file tree
Showing 32 changed files with 471 additions and 334 deletions.
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,7 @@ APPLICANT_HOST=http://localhost:3001/apply/applicant
USER_SERVICE_HOST=http://localhost:8082
USER_TOKEN_NAME=user-service-token
USER_TOKEN_SECRET=secret
ONE_LOGIN_ENABLED=true
ONE_LOGIN_ENABLED=true
SESSION_COOKIE_NAME=session_id
ADMIN_BACKEND_HOST=http:localhost:8081
V2_LOGOUT_URL=http://localhost:8082/v2/logout
15 changes: 3 additions & 12 deletions __tests__/pages/api/confirm-email.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,7 @@ import {
generateSignedApiKey,
} from '../../../src/service/api-key-service';
import * as nookies from 'nookies';
import {
maxAgeForCookies,
notificationRoutes,
} from '../../../src/utils/constants';

jest.mock('next/config', () => {
return jest.fn().mockImplementation(() => {
return { serverRuntimeConfig: {} };
});
});
import { notificationRoutes } from '../../../src/utils/constants';

jest.mock('../../../src/service/api-key-service');

Expand Down Expand Up @@ -52,7 +43,7 @@ describe('Confirm email and set cookie', () => {
handler(req, res);
expect(res.redirect).toHaveBeenCalledTimes(1);
expect(res.redirect).toHaveBeenCalledWith(
notificationRoutes['manageNotifications']
notificationRoutes['manageNotifications'],
);
});

Expand All @@ -75,7 +66,7 @@ describe('Confirm email and set cookie', () => {
maxAge: 2 * 60 * 60,
path: '/',
httpOnly: true,
}
},
);
});
});
74 changes: 74 additions & 0 deletions __tests__/pages/api/logout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import '@testing-library/jest-dom';
import { merge } from 'lodash';
import Logout from '../../../pages/api/logout';
import { getSessionIdFromCookies } from '../../../src/utils/session';
import axios from 'axios';

jest.mock('../../../src/utils/session');
jest.mock('axios');

const mockedRedirect = jest.fn();
const mockedSetHeader = jest.fn();
const mockedSend = jest.fn();

const req = (overrides = {}) =>
merge(
{
headers: {
referer: `/referer`,
},
cookies: { sessionCookieName: 'testSessionId' },
},
overrides,
);

const res = (overrides = {}) =>
merge(
{
redirect: mockedRedirect,
setHeader: mockedSetHeader,
send: mockedSend,
},
overrides,
);

describe('Logout page', () => {
beforeEach(() => {
jest.clearAllMocks();
process.env.ONE_LOGIN_ENABLED = 'false';
process.env.LOGOUT = 'http://localhost:8082/logout';
});

it('Should clear back-end authentication session if there is session_id cookie available', async () => {
(getSessionIdFromCookies as jest.Mock).mockReturnValue('testSessionId');
await Logout(req(), res());

expect(axios.delete).toHaveBeenCalledTimes(1);
});

it('Should NOT try to clear back-end authentication session if session_id cookie not available', async () => {
(getSessionIdFromCookies as jest.Mock).mockReturnValue('');
await Logout(req(), res());

expect(axios.delete).toHaveBeenCalledTimes(0);
});

it('Should clear session_id cookie', async () => {
await Logout(req(), res());

expect(mockedSetHeader).toHaveBeenCalledTimes(1);
});

it('Should redirect to login page', async () => {
process.env.V2_LOGOUT_URL = 'http://localhost:8082/logout';
process.env.LOGOUT_URL = 'http://localhost:8082/logout';

await Logout(req(), res());

expect(mockedRedirect).toHaveBeenNthCalledWith(
1,
302,
'http://localhost:8082/logout',
);
});
});
6 changes: 0 additions & 6 deletions __tests__/pages/api/notification-signup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@ import { notificationRoutes } from '../../../src/utils/constants';
import { decrypt } from '../../../src/utils/encryption';
import nookies from 'nookies';

jest.mock('next/config', () => {
return jest.fn().mockImplementation(() => {
return { serverRuntimeConfig: {} };
});
});

jest.mock('../../../src/service/api-key-service');
jest.mock('../../../src/utils/encryption');
jest.mock('nookies');
Expand Down
6 changes: 0 additions & 6 deletions __tests__/pages/api/unsubscribe.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@ jest.mock('../../../src/utils/jwt', () => ({
})),
}));

jest.mock('next/config', () => {
return jest.fn().mockImplementation(() => {
return { serverRuntimeConfig: {} };
});
});

const req = {
body: {
email: '[email protected]',
Expand Down
1 change: 0 additions & 1 deletion __tests__/pages/info/privacy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ jest.mock('next/router', () => ({
return jest.fn();
},
}));

let props, component;
beforeAll(async () => {
props = {
Expand Down
1 change: 0 additions & 1 deletion __tests__/pages/info/terms-and-conditions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ jest.mock('next/router', () => ({
return jest.fn();
},
}));

let props, component;
beforeAll(async () => {
props = {
Expand Down
9 changes: 3 additions & 6 deletions __tests__/pages/newsletter/confirmation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,9 @@ import { parseBody } from 'next/dist/server/api-utils/node';

jest.mock('next/dist/server/api-utils/node');

jest.mock('next/router', () => {
return {
useRouter: jest.fn(),
};
});

jest.mock('next/router', () => ({
useRouter: jest.fn(),
}));
jest.mock('../../../src/service/gov_notify_service', () => ({
sendEmail: jest.fn(),
}));
Expand Down
22 changes: 8 additions & 14 deletions __tests__/pages/notifications/check-email.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,14 @@ import CheckEmail, {
import { notificationRoutes } from '../../../src/utils/constants';
import cookieExistsAndContainsValidJwt from '../../../src/utils/cookieAndJwtChecker';

jest.mock('next/router', () => {
return {
useRouter: jest.fn(),
};
});

jest.mock('nookies', () => {
return {
get: jest.fn(),
set: jest.fn(),
destroy: jest.fn(),
};
});

jest.mock('next/router', () => ({
useRouter: jest.fn(),
}));
jest.mock('nookies', () => ({
get: jest.fn(),
set: jest.fn(),
destroy: jest.fn(),
}));
jest.mock('../../../src/utils/cookieAndJwtChecker');

const mockQuery = {
Expand Down
6 changes: 0 additions & 6 deletions __tests__/pages/notifications/deleteSearch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,6 @@ jest.mock('../../../src/service/saved_search_service');
const encryptedEmail = 'test-encrypted-email-string';
const decryptedEmail = 'test-decrypted-email-string';

jest.mock('next/config', () => {
return jest.fn().mockImplementation(() => {
return { serverRuntimeConfig: {} };
});
});

jest.mock('next/router', () => {
return {
useRouter: jest.fn(),
Expand Down
6 changes: 0 additions & 6 deletions __tests__/pages/notifications/email-confirmation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@ jest.mock('../../../src/utils/encryption');

const encryptedEmail = 'test-encrypted-email-string';

jest.mock('next/config', () => {
return jest.fn().mockImplementation(() => {
return { serverRuntimeConfig: {} };
});
});

jest.mock('next/router', () => {
return {
useRouter: jest.fn(),
Expand Down
6 changes: 0 additions & 6 deletions __tests__/pages/notifications/unsubscribe.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@ jest.mock('../../../src/service/api-key-service');
const encryptedEmail = 'test-encrypted-email-string';
const decryptedEmail = 'test-decrypted-email-string';

jest.mock('next/config', () => {
return jest.fn().mockImplementation(() => {
return { serverRuntimeConfig: {} };
});
});

jest.mock('next/router', () => {
return {
useRouter: jest.fn(),
Expand Down
1 change: 0 additions & 1 deletion __tests__/pages/save-search/email.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ describe('getServerSideProps', () => {
afterEach(() => {
jest.clearAllMocks();
});

it('should return expected props for a GET request', async () => {
const context = {
req: {
Expand Down
1 change: 0 additions & 1 deletion __tests__/pages/save-search/notifications.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import SignupSavedSearch, {
import { RouterContext } from 'next/dist/shared/lib/router-context.js';
import { parseBody } from 'next/dist/server/api-utils/node';
jest.mock('next/dist/server/api-utils/node');

describe('Rendering serverside props', () => {
const queryWithNoErrors = {
req: { method: 'GET' },
Expand Down
1 change: 1 addition & 0 deletions __tests__/pages/unsubscribe/[id].test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jest.mock('../../../pages/service-error/index.page', () => ({
jest.mock('../../../src/utils/encryption', () => ({
decrypt: jest.fn(),
}));

jest.mock(
'../../../src/service/newsletter/newsletter-subscription-service',
() => ({
Expand Down
5 changes: 0 additions & 5 deletions __tests__/service/subscription_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,6 @@ jest.mock('../../src/utils/axios', () => {
};
});

jest.mock('next/config', () => {
return jest.fn().mockImplementation(() => {
return { serverRuntimeConfig: {} };
});
});
const subscriptionService = SubscriptionService.getInstance();
const instance = axios.create();

Expand Down
1 change: 1 addition & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ module.exports = {
sassOptions: {
includePaths: [path.join(__dirname, 'styles')],
},

output: 'standalone',
};
53 changes: 48 additions & 5 deletions pages/_app.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
import Script from 'next/script';
import nookies from 'nookies';
import React, { useEffect } from 'react';
import React, { createContext, useContext, useEffect } from 'react';
import TagManager from 'react-gtm-module';
import '../src/lib/ie11_nodelist_polyfill';
import '../styles/globals.scss';
import { checkUserLoggedIn } from '../src/service';
import { getJwtFromCookies } from '../src/utils/jwt';
import App from 'next/app';

const MyApp = ({ Component, pageProps }) => {
let cookies = nookies.get({});
export const AuthContext = createContext({
isUserLoggedIn: false,
});
export const AppContext = createContext({
applicantUrl: null,
oneLoginEnabled: null,
});

export const useAuth = () => useContext(AuthContext);
export const useAppContext = () => useContext(AppContext);

const MyApp = ({
Component,
pageProps,
props: { isUserLoggedIn, applicantUrl, oneLoginEnabled },
}) => {
const cookies = nookies.get({});

useEffect(() => {
if (cookies.design_system_cookies_policy === 'true') {
Expand All @@ -28,10 +46,35 @@ const MyApp = ({ Component, pageProps }) => {
return (
<>
<Script src="/javascript/govuk.js" strategy="beforeInteractive" />

<Component {...pageProps} />
<AppContext.Provider value={{ applicantUrl, oneLoginEnabled }}>
<AuthContext.Provider value={{ isUserLoggedIn }}>
<Component {...pageProps} />
</AuthContext.Provider>
</AppContext.Provider>
</>
);
};

MyApp.getInitialProps = async (context) => {
const ctx = await App.getInitialProps(context);
let oneLoginEnabled = null;
let applicantUrl = null;

if (process?.env) {
oneLoginEnabled = process.env.ONE_LOGIN_ENABLED;
applicantUrl = process.env.APPLY_FOR_A_GRANT_APPLICANT_URL;
}
try {
const { jwt } = getJwtFromCookies(context.ctx.req);
const isUserLoggedIn = await checkUserLoggedIn(jwt);

return { ...ctx, props: { isUserLoggedIn, applicantUrl, oneLoginEnabled } };
} catch (err) {
return {
...ctx,
props: { isUserLoggedIn: false, applicantUrl, oneLoginEnabled },
};
}
};

export default MyApp;
29 changes: 29 additions & 0 deletions pages/api/logout/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { getSessionIdFromCookies } from '../../../src/utils/session';
import axios from 'axios';

const Logout = async (req: NextApiRequest, res: NextApiResponse) => {
const sessionCookie = getSessionIdFromCookies(req);
if (sessionCookie) await logoutAdmin(sessionCookie);

res.setHeader(
'Set-Cookie',
`session_id=deleted; Path=/; secure; HttpOnly; SameSite=Strict; expires=Thu, 01 Jan 2003 00:00:00 GMT`,
);
res.redirect(302, process.env.V2_LOGOUT_URL);
};

const axiosSessionConfig = (sessionId: string) => ({
withCredentials: true,
headers: {
Cookie: `SESSION=${sessionId};`,
},
});

const logoutAdmin = async (sessionCookie: string) =>
axios.delete(
`${process.env.ADMIN_BACKEND_HOST}/logout`,
axiosSessionConfig(sessionCookie),
);

export default Logout;
Loading

0 comments on commit 63237c3

Please sign in to comment.