Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
"devDependencies": [
"jest.config.ts",
"jest.setup.ts",
"**/__tests__/**"
"**/__tests__/**",
"**/*.dev.[jt]s?(x)"
]
}
],
Expand Down
6 changes: 6 additions & 0 deletions frontend/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ const nextConfig = (phase) => {
permanent: false,
basePath: false,
},
{
source: `${basePath}/auth/signout`,
destination: '/auth/signout',
basePath: false,
permanent: false,
},
];
},

Expand Down
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
"@types/jest": "^29.5.14",
"@types/js-cookie": "^3.0.6",
"@types/jsonwebtoken": "^9.0.8",
"@types/markdown-it": "^13.0.1",
"@types/node": "^22.8.1",
Expand All @@ -52,6 +53,7 @@
"constructs": "^10.3.0",
"eslint-config-next": "^15.1.7",
"jest-mock-extended": "^3.0.7",
"js-cookie": "^3.0.5",
"mochawesome": "^7.1.3",
"pa11y-ci": "^3.1.0",
"pa11y-ci-reporter-html": "^7.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import {
csrfServerAction,
} from '@molecules/NHSNotifyFormWrapper/NHSNotifyFormWrapper';
import { render } from '@testing-library/react';
import { verifyCsrfTokenFull } from '@utils/csrf-utils';
import { redirect } from 'next/navigation';
import { verifyFormCsrfToken } from '@utils/csrf-utils';

jest.mock('next/navigation');

jest.mock('@utils/csrf-utils', () => ({
verifyCsrfTokenFull: jest.fn(),
verifyFormCsrfToken: jest.fn(),
}));

test('Renders back button', () => {
Expand All @@ -27,8 +30,9 @@ describe('csrfServerAction', () => {
expect(action).toEqual('/action');
});

test('server action', async () => {
const mockAction = jest.fn(() => 'response');
test('server action with valid csrf check', async () => {
jest.mocked(verifyFormCsrfToken).mockResolvedValueOnce(true);
const mockAction = jest.fn();
const action = csrfServerAction(mockAction);

if (typeof action === 'string') {
Expand All @@ -38,7 +42,25 @@ describe('csrfServerAction', () => {
const mockFormData = mockDeep<FormData>();
await action(mockFormData);

expect(verifyCsrfTokenFull).toHaveBeenCalledWith(mockFormData);
expect(verifyFormCsrfToken).toHaveBeenCalledWith(mockFormData);
expect(mockAction).toHaveBeenCalledWith(mockFormData);
});

test('server action with failed csrf check', async () => {
jest.mocked(verifyFormCsrfToken).mockResolvedValueOnce(false);

const mockAction = jest.fn();
const action = csrfServerAction(mockAction);

if (typeof action === 'string') {
throw new TypeError('Expected server action');
}

const mockFormData = mockDeep<FormData>();
await action(mockFormData);

expect(verifyFormCsrfToken).toHaveBeenCalledWith(mockFormData);
expect(redirect).toHaveBeenCalledWith('/auth/signout');
expect(mockAction).not.toHaveBeenCalled();
});
});
7 changes: 6 additions & 1 deletion frontend/src/__tests__/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ afterAll(() => {
});

describe('middleware function', () => {
it('If route is not registered in midleware, respond with 404', async () => {
it('If route is not registered in middleware, respond with 404', async () => {
const url = new URL('https://url.com/manage-templates/does-not-exist');
const request = new NextRequest(url);
const response = await middleware(request);
Expand All @@ -31,13 +31,18 @@ describe('middleware function', () => {
it('if request path is protected, and no access token is obtained, redirect to auth page', async () => {
const url = new URL('https://url.com/manage-templates');
const request = new NextRequest(url);
request.cookies.set('csrf_token', 'some-csrf-value');

const response = await middleware(request);

expect(getTokenMock).toHaveBeenCalledWith({ forceRefresh: true });

expect(response.status).toBe(307);
expect(response.headers.get('location')).toBe(
'https://url.com/auth?redirect=%2Ftemplates%2Fmanage-templates'
);
expect(response.headers.get('Content-Type')).toBe('text/html');
expect(response.cookies.get('csrf_token')?.value).toEqual('');
});

it('if request path is protected, and access token is obtained, respond with CSP', async () => {
Expand Down
57 changes: 49 additions & 8 deletions frontend/src/__tests__/utils/amplify-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/**
* @jest-environment node
*/
import { getAccessTokenServer } from '@utils/amplify-utils';
import { sign } from 'jsonwebtoken';
import { fetchAuthSession } from 'aws-amplify/auth/server';
import { getAccessTokenServer, getSessionId } from '../../utils/amplify-utils';

jest.mock('aws-amplify/auth/server');
jest.mock('@aws-amplify/adapter-nextjs/api');
Expand All @@ -19,24 +20,22 @@ const fetchAuthSessionMock = jest.mocked(fetchAuthSession);

describe('amplify-utils', () => {
test('getAccessTokenServer - should return the auth token', async () => {
fetchAuthSessionMock.mockResolvedValue({
fetchAuthSessionMock.mockResolvedValueOnce({
tokens: {
accessToken: {
toString: () => 'mockSub',
payload: {
sub: 'mockSub',
},
toString: () => 'mockToken',
payload: {},
},
},
});

const result = await getAccessTokenServer();

expect(result).toEqual('mockSub');
expect(result).toEqual('mockToken');
});

test('getAccessTokenServer - should return undefined when no auth session', async () => {
fetchAuthSessionMock.mockResolvedValue({});
fetchAuthSessionMock.mockResolvedValueOnce({});

const result = await getAccessTokenServer();

Expand All @@ -52,4 +51,46 @@ describe('amplify-utils', () => {

expect(result).toBeUndefined();
});

describe('getSessionId', () => {
test('returns void when access token not found', async () => {
fetchAuthSessionMock.mockResolvedValueOnce({});

await expect(getSessionId()).resolves.toBeUndefined();
});

test('errors when session ID not found', async () => {
fetchAuthSessionMock.mockResolvedValueOnce({
tokens: {
accessToken: {
toString: () => sign({}, 'key'),
payload: {},
},
},
});

await expect(getSessionId()).resolves.toBeUndefined();
});

test('returns session id', async () => {
fetchAuthSessionMock.mockResolvedValueOnce({
tokens: {
accessToken: {
toString: () =>
sign(
{
origin_jti: 'jti',
},
'key'
),
payload: {},
},
},
});

const sessionId = await getSessionId();

expect(sessionId).toEqual('jti');
});
});
});
Loading
Loading