Skip to content

Commit 71f7a41

Browse files
bhansell1alexnuttall
authored andcommitted
CCM-10547: add cmapaignid to template and implement client feature flag
1 parent a70ebb8 commit 71f7a41

File tree

21 files changed

+401
-33
lines changed

21 files changed

+401
-33
lines changed

frontend/src/__tests__/app/request-proof-of-template/page.test.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,25 @@ import {
1414
NHS_APP_TEMPLATE,
1515
SMS_TEMPLATE,
1616
} from '../../helpers';
17+
import { serverIsFeatureEnabled } from '@utils/server-features';
1718
import content from '@content/content';
1819

1920
const { pageTitle } = content.components.requestProof;
2021

2122
jest.mock('@utils/form-actions');
2223
jest.mock('next/navigation');
2324
jest.mock('@forms/RequestProof/RequestProof');
25+
jest.mock('@utils/server-features');
2426

2527
const getTemplateMock = jest.mocked(getTemplate);
2628
const redirectMock = jest.mocked(redirect);
29+
const serverIsFeatureEnabledMock = jest.mocked(serverIsFeatureEnabled);
2730

2831
describe('RequestProofPage', () => {
29-
beforeEach(jest.resetAllMocks);
32+
beforeEach(() => {
33+
jest.resetAllMocks();
34+
serverIsFeatureEnabledMock.mockResolvedValueOnce(true);
35+
});
3036

3137
test('should load page', async () => {
3238
const state = {
@@ -104,4 +110,17 @@ describe('RequestProofPage', () => {
104110
expect(redirectMock).toHaveBeenCalledWith('/invalid-template', 'replace');
105111
}
106112
);
113+
114+
test('should forbid user from requesting a proof when client does not have feature enabled', async () => {
115+
serverIsFeatureEnabledMock.mockReset();
116+
serverIsFeatureEnabledMock.mockResolvedValueOnce(false);
117+
118+
await RequestProofPage({
119+
params: Promise.resolve({
120+
templateId: 'template-id',
121+
}),
122+
});
123+
124+
expect(redirectMock).toHaveBeenCalledWith('/invalid-template', 'replace');
125+
});
107126
});
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { serverIsFeatureEnabled } from '@utils/server-features';
2+
import { getSessionServer } from '@utils/amplify-utils';
3+
import { ClientConfiguration } from 'nhs-notify-backend-client';
4+
import { mock } from 'jest-mock-extended';
5+
6+
jest.mock('@utils/amplify-utils');
7+
jest.mock('nhs-notify-backend-client');
8+
9+
const getSessionServerMock = jest.mocked(getSessionServer);
10+
const clientConfigurationMock = jest.mocked(ClientConfiguration.fetch);
11+
12+
describe('serverIsFeatureEnabled', () => {
13+
beforeAll(() => {
14+
jest.resetAllMocks();
15+
});
16+
17+
it('should return false when no accessToken', async () => {
18+
getSessionServerMock.mockResolvedValueOnce({
19+
accessToken: undefined,
20+
userSub: undefined,
21+
});
22+
23+
const enabled = await serverIsFeatureEnabled('proofing');
24+
25+
expect(enabled).toEqual(false);
26+
});
27+
28+
it('should return false when no client', async () => {
29+
getSessionServerMock.mockResolvedValueOnce({
30+
accessToken: 'token',
31+
userSub: undefined,
32+
});
33+
34+
const enabled = await serverIsFeatureEnabled('proofing');
35+
36+
expect(enabled).toEqual(false);
37+
38+
expect(clientConfigurationMock).toHaveBeenCalledWith('token');
39+
});
40+
41+
it('should return false when feature is not enabled', async () => {
42+
const featureEnabled = jest.fn(() => false);
43+
44+
clientConfigurationMock.mockResolvedValueOnce(
45+
mock<ClientConfiguration>({
46+
featureEnabled,
47+
})
48+
);
49+
50+
getSessionServerMock.mockResolvedValueOnce({
51+
accessToken: 'token',
52+
userSub: undefined,
53+
});
54+
55+
const enabled = await serverIsFeatureEnabled('proofing');
56+
57+
expect(enabled).toEqual(false);
58+
59+
expect(clientConfigurationMock).toHaveBeenCalledWith('token');
60+
61+
expect(featureEnabled).toHaveBeenCalledWith('proofing');
62+
});
63+
64+
it('should return true when feature is enabled', async () => {
65+
const featureEnabled = jest.fn(() => true);
66+
67+
clientConfigurationMock.mockResolvedValueOnce(
68+
mock<ClientConfiguration>({
69+
featureEnabled,
70+
})
71+
);
72+
73+
getSessionServerMock.mockResolvedValueOnce({
74+
accessToken: 'token',
75+
userSub: undefined,
76+
});
77+
78+
const enabled = await serverIsFeatureEnabled('proofing');
79+
80+
expect(enabled).toEqual(true);
81+
82+
expect(clientConfigurationMock).toHaveBeenCalledWith('token');
83+
84+
expect(featureEnabled).toHaveBeenCalledWith('proofing');
85+
});
86+
});

frontend/src/app/request-proof-of-template/[templateId]/page.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
} from 'nhs-notify-web-template-management-utils';
1010
import { getTemplate } from '@utils/form-actions';
1111
import content from '@content/content';
12+
import { serverIsFeatureEnabled } from '@utils/server-features';
13+
1214
const { pageTitle } = content.components.requestProof;
1315

1416
export async function generateMetadata(): Promise<Metadata> {
@@ -20,6 +22,13 @@ export async function generateMetadata(): Promise<Metadata> {
2022
const RequestProofPage = async (props: PageProps) => {
2123
const { templateId } = await props.params;
2224

25+
const proofingEnabled = await serverIsFeatureEnabled('proofing');
26+
27+
if (!proofingEnabled) {
28+
// Note: replace as part of CCM-?????
29+
return redirect('/invalid-template', RedirectType.replace);
30+
}
31+
2332
const template = await getTemplate(templateId);
2433

2534
const validatedTemplate = validateLetterTemplate(template);

frontend/src/hooks/use-text-input.hook.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { useState } from 'react';
33
type InputValue = HTMLTextAreaElement | HTMLInputElement | HTMLSelectElement;
44

55
export const useTextInput = <T extends InputValue, S extends string = string>(
6-
initialState: string
6+
initialState: S
77
): [S, React.ChangeEventHandler<T>] => {
8-
const [value, setValue] = useState<S>(initialState as S);
8+
const [value, setValue] = useState<S>(initialState);
99

1010
const handleChange: React.ChangeEventHandler<T> = (e) => {
1111
setValue(e.target.value as S);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use server';
2+
3+
import { cache } from 'react';
4+
import { ClientConfiguration, Features } from 'nhs-notify-backend-client';
5+
import { getSessionServer } from './amplify-utils';
6+
7+
/*
8+
* Caches at the request context level. Not a global cache.
9+
*/
10+
const fetchClient = cache(async (accessToken: string) =>
11+
ClientConfiguration.fetch(accessToken)
12+
);
13+
14+
/**
15+
* Server-Side
16+
*
17+
* Fetches client configuration to check whether a specific feature is enabled
18+
* @param {string} feature keyof Features
19+
* @returns {Promise<Boolean>} boolean
20+
*/
21+
export async function serverIsFeatureEnabled(
22+
feature: keyof Features
23+
): Promise<boolean> {
24+
const { accessToken } = await getSessionServer();
25+
26+
if (!accessToken) return false;
27+
28+
const client = await fetchClient(accessToken);
29+
30+
return client?.featureEnabled(feature) || false;
31+
}

lambdas/backend-api/src/__tests__/templates/api/create-letter.test.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { createHandler } from '@backend-api/templates/api/create-letter';
22
import { mock } from 'jest-mock-extended';
3-
import { CreateUpdateTemplate, TemplateDto } from 'nhs-notify-backend-client';
3+
import {
4+
CreateUpdateTemplate,
5+
ClientConfiguration,
6+
TemplateDto,
7+
} from 'nhs-notify-backend-client';
48
import {
59
APIGatewayProxyEvent,
610
APIGatewayProxyResult,
@@ -12,12 +16,16 @@ import {
1216
} from 'nhs-notify-web-template-management-test-helper-utils';
1317
import { TemplateClient } from '@backend-api/templates/app/template-client';
1418

19+
jest.mock('nhs-notify-backend-client/src/client-configuration-client');
20+
1521
const setup = () => {
1622
const templateClient = mock<TemplateClient>();
1723

24+
const clientConfigurationFetch = jest.mocked(ClientConfiguration.fetch);
25+
1826
const handler = createHandler({ templateClient });
1927

20-
return { handler, mocks: { templateClient } };
28+
return { handler, mocks: { templateClient, clientConfigurationFetch } };
2129
};
2230

2331
const userId = '8B892046';
@@ -42,6 +50,12 @@ describe('create-letter', () => {
4250
test('successfully handles multipart form input and forwards PDF and CSV', async () => {
4351
const { handler, mocks } = setup();
4452

53+
mocks.clientConfigurationFetch.mockResolvedValueOnce(
54+
mock<ClientConfiguration>({
55+
campaignId: 'campaignId',
56+
})
57+
);
58+
4559
const pdfFilename = 'template.pdf';
4660
const csvFilename = 'data.csv';
4761
const pdfType = 'application/pdf';
@@ -72,6 +86,7 @@ describe('create-letter', () => {
7286
body: multipart.toString('base64'),
7387
headers: {
7488
'Content-Type': contentType,
89+
Authorization: 'example',
7590
},
7691
requestContext: { authorizer: { user: userId, clientId } },
7792
});
@@ -115,8 +130,11 @@ describe('create-letter', () => {
115130
initialTemplate,
116131
{ userId, clientId },
117132
new File([pdf], pdfFilename, { type: pdfType }),
118-
new File([csv], csvFilename, { type: csvType })
133+
new File([csv], csvFilename, { type: csvType }),
134+
'campaignId'
119135
);
136+
137+
expect(mocks.clientConfigurationFetch).toHaveBeenCalledWith('example');
120138
});
121139

122140
test('successfully handles multipart form input without test data', async () => {
@@ -252,6 +270,7 @@ describe('create-letter', () => {
252270
initialTemplate,
253271
{ userId, clientId: undefined },
254272
new File([pdf], pdfFilename, { type: pdfType }),
273+
undefined,
255274
undefined
256275
);
257276
});
@@ -433,6 +452,7 @@ describe('create-letter', () => {
433452
initialTemplate,
434453
{ userId, clientId },
435454
new File([pdf], pdfFilename, { type: pdfType }),
455+
undefined,
436456
undefined
437457
);
438458
});

lambdas/backend-api/src/__tests__/templates/api/create.test.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
import type { APIGatewayProxyEvent, Context } from 'aws-lambda';
22
import { mock } from 'jest-mock-extended';
3-
import { TemplateDto, CreateUpdateTemplate } from 'nhs-notify-backend-client';
3+
import {
4+
TemplateDto,
5+
CreateUpdateTemplate,
6+
ClientConfiguration,
7+
} from 'nhs-notify-backend-client';
48
import { createHandler } from '@backend-api/templates/api/create';
59
import { TemplateClient } from '@backend-api/templates/app/template-client';
610

11+
jest.mock('nhs-notify-backend-client/src/client-configuration-client');
12+
713
const setup = () => {
814
const templateClient = mock<TemplateClient>();
915

16+
const clientConfigurationFetch = jest.mocked(ClientConfiguration.fetch);
17+
1018
const handler = createHandler({ templateClient });
1119

12-
return { handler, mocks: { templateClient } };
20+
return { handler, mocks: { templateClient, clientConfigurationFetch } };
1321
};
1422

1523
describe('Template API - Create', () => {
@@ -112,6 +120,12 @@ describe('Template API - Create', () => {
112120
test('should return template', async () => {
113121
const { handler, mocks } = setup();
114122

123+
mocks.clientConfigurationFetch.mockResolvedValueOnce(
124+
mock<ClientConfiguration>({
125+
campaignId: 'campaignId',
126+
})
127+
);
128+
115129
const create: CreateUpdateTemplate = {
116130
name: 'updated-name',
117131
message: 'message',

0 commit comments

Comments
 (0)