diff --git a/.github/actions/create-lines-of-code-report/action.yaml b/.github/actions/create-lines-of-code-report/action.yaml
index e3554ae4..86396f7a 100644
--- a/.github/actions/create-lines-of-code-report/action.yaml
+++ b/.github/actions/create-lines-of-code-report/action.yaml
@@ -32,7 +32,7 @@ runs:
run: zip lines-of-code-report.json.zip lines-of-code-report.json
- name: "Upload CLOC report as an artefact"
if: ${{ !env.ACT }}
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: lines-of-code-report.json.zip
path: ./lines-of-code-report.json.zip
diff --git a/.github/actions/scan-dependencies/action.yaml b/.github/actions/scan-dependencies/action.yaml
index c85a8db3..1000df14 100644
--- a/.github/actions/scan-dependencies/action.yaml
+++ b/.github/actions/scan-dependencies/action.yaml
@@ -32,7 +32,7 @@ runs:
run: zip sbom-repository-report.json.zip sbom-repository-report.json
- name: "Upload SBOM report as an artefact"
if: ${{ !env.ACT }}
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: sbom-repository-report.json.zip
path: ./sbom-repository-report.json.zip
@@ -47,7 +47,7 @@ runs:
run: zip vulnerabilities-repository-report.json.zip vulnerabilities-repository-report.json
- name: "Upload vulnerabilities report as an artefact"
if: ${{ !env.ACT }}
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: vulnerabilities-repository-report.json.zip
path: ./vulnerabilities-repository-report.json.zip
diff --git a/.github/workflows/stage-4-acceptance.yaml b/.github/workflows/stage-4-acceptance.yaml
index 60cfce86..b171974b 100644
--- a/.github/workflows/stage-4-acceptance.yaml
+++ b/.github/workflows/stage-4-acceptance.yaml
@@ -40,37 +40,9 @@ permissions:
contents: read # This is required for actions/checkout
jobs:
- environment-set-up:
- name: "Environment set up"
- runs-on: ubuntu-latest
- environment: dev
- timeout-minutes: 15
- steps:
- - name: "Checkout code"
- uses: actions/checkout@v4
- - name: "Repo setup"
- run: |
- npm ci
- - name: "Generate dependencies"
- run: |
- npm run generate-dependencies --workspaces --if-present
- - name: Configure AWS credentials
- uses: aws-actions/configure-aws-credentials@v4
- with:
- role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ASSUME_ROLE_NAME }}
- role-session-name: deployInfra
- aws-region: ${{ env.AWS_REGION }}
- - name: "Create Amplify sandbox"
- run: |
- ./scripts/create_amplify_sandbox.sh
- - uses: actions/upload-artifact@v4
- with:
- name: amplify_outputs.json
- path: frontend/amplify_outputs.json
-
sandbox-set-up:
name: "Sandbox set up"
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
environment: dev
timeout-minutes: 15
steps:
@@ -92,11 +64,10 @@ jobs:
with:
name: sandbox_tf_outputs.json
path: sandbox_tf_outputs.json
-
test-security:
name: "Security test"
- runs-on: ubuntu-latest
- needs: [environment-set-up, sandbox-set-up]
+ runs-on: ubuntu-22.04
+ needs: [sandbox-set-up]
timeout-minutes: 10
steps:
- name: "Checkout code"
@@ -109,17 +80,19 @@ jobs:
echo "Nothing to save"
test-accessibility:
name: "Accessibility test"
- runs-on: ubuntu-latest
- needs: [environment-set-up, sandbox-set-up]
+ runs-on: ubuntu-22.04
+ needs: [sandbox-set-up]
environment: dev
+ env:
+ INCLUDE_AUTH_PAGES: 'true'
timeout-minutes: 10
steps:
- name: "Checkout code"
uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
- name: amplify_outputs.json
- path: frontend/
+ name: sandbox_tf_outputs.json
+ path: ./
- name: "Repo setup"
run: |
npm ci
@@ -133,7 +106,9 @@ jobs:
role-session-name: deployInfra
aws-region: eu-west-2
- name: "Run accessibility test"
- run: make test-accessibility
+ run: |
+ npm run create-amplify-outputs file
+ make test-accessibility
- name: Archive accessibility results
uses: actions/upload-artifact@v4
with:
@@ -141,17 +116,19 @@ jobs:
path: ".reports/accessibility"
test-ui-component:
name: "UI Component test"
- runs-on: ubuntu-latest
- needs: [environment-set-up, sandbox-set-up]
+ runs-on: ubuntu-22.04
+ needs: [sandbox-set-up]
environment: dev
+ env:
+ INCLUDE_AUTH_PAGES: 'true'
timeout-minutes: 10
steps:
- name: "Checkout code"
uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
- name: amplify_outputs.json
- path: frontend/
+ name: sandbox_tf_outputs.json
+ path: ./
- name: "Repo setup"
run: |
npm ci
@@ -168,45 +145,19 @@ jobs:
aws-region: eu-west-2
- name: "Run ui component test"
run: |
+ npm run create-amplify-outputs file
cd tests/test-team
npm run test:local-ui
- name: Archive component test results
+ if: success() || failure()
uses: actions/upload-artifact@v4
with:
name: component test report
path: "tests/test-team/playwright-report"
- environment-tear-down:
- name: "Environment tear down"
- if: success() || failure()
- runs-on: ubuntu-latest
- needs: [test-accessibility, test-ui-component]
- environment: dev
- steps:
- - name: "Checkout code"
- uses: actions/checkout@v4
- - uses: actions/download-artifact@v4
- with:
- name: amplify_outputs.json
- path: frontend/
- - name: "Repo setup"
- run: |
- npm ci
- - name: "Generate dependencies"
- run: |
- npm run generate-dependencies --workspaces --if-present
- - name: Configure AWS credentials
- uses: aws-actions/configure-aws-credentials@v4
- with:
- role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ASSUME_ROLE_NAME }}
- role-session-name: deployInfra
- aws-region: eu-west-2
- - name: "Destroy Amplify sandbox"
- run: |
- (cd frontend && npm run destroy-sandbox -- --identifier "wf-${GITHUB_RUN_ID}")
sandbox-tear-down:
name: "Sandbox tear down"
if: success() || failure()
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
needs: [test-accessibility, test-ui-component]
environment: dev
steps:
diff --git a/.gitignore b/.gitignore
index 2203489d..83c7a273 100644
--- a/.gitignore
+++ b/.gitignore
@@ -82,3 +82,4 @@ sandbox_cognito_auth_token.json
frontend/public/testing
.vscode/launch.json
+auth.json
diff --git a/amplify.yml b/amplify.yml
index ceedba94..8964028a 100644
--- a/amplify.yml
+++ b/amplify.yml
@@ -10,8 +10,8 @@ applications:
- nvm use 20.13.1
- npm ci --cache .npm --prefer-offline
- npm run generate-dependencies --workspaces --if-present
+ - npm run create-amplify-outputs env
- cd frontend
- - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
frontend:
phases:
build:
diff --git a/frontend/.env.template b/frontend/.env.template
new file mode 100644
index 00000000..1045ec70
--- /dev/null
+++ b/frontend/.env.template
@@ -0,0 +1,2 @@
+# Includes auth pages when building web frontend in production mode.
+INCLUDE_AUTH_PAGES=''
diff --git a/frontend/amplify/backend.ts b/frontend/amplify/backend.ts
index aefe1484..4f36ee13 100644
--- a/frontend/amplify/backend.ts
+++ b/frontend/amplify/backend.ts
@@ -1,28 +1 @@
-import { defineBackend } from '@aws-amplify/backend';
-import { PolicyStatement, Effect } from 'aws-cdk-lib/aws-iam';
-import { auth } from './auth/resource';
-import { data } from './data/resource';
-import { sendEmail } from './functions/send-email/resource';
-
-const backend = defineBackend({
- auth,
- data,
- sendEmail,
-});
-
-const sendEmailLambda = backend.sendEmail.resources.lambda;
-
-const attachPolicy = new PolicyStatement({
- sid: 'AmplifySendEmail',
- effect: Effect.ALLOW,
- actions: ['ses:SendRawEmail'],
- resources: [`arn:aws:ses:eu-west-2:${process.env.ACCOUNT_ID}:identity/*`],
-});
-
-sendEmailLambda.addToRolePolicy(attachPolicy);
-
-backend.data.resources.cfnResources.amplifyDynamoDbTables.TemplateStorage.timeToLiveAttribute =
- {
- attributeName: 'ttl',
- enabled: true,
- };
+/* eslint-disable unicorn/no-empty-file */
diff --git a/frontend/next.config.js b/frontend/next.config.js
index 3f4fb117..ead9269b 100644
--- a/frontend/next.config.js
+++ b/frontend/next.config.js
@@ -1,17 +1,21 @@
/** @type {import('next').NextConfig} */
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
+const amplifyConfig = require('./amplify_outputs.json');
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? '/templates';
const domain = process.env.NOTIFY_DOMAIN_NAME ?? 'localhost:3000';
const nextConfig = (phase) => {
const isDevServer = phase === PHASE_DEVELOPMENT_SERVER;
+ const includeAuthPages =
+ process.env.INCLUDE_AUTH_PAGES === 'true' || isDevServer;
return {
basePath,
env: {
basePath,
+ BACKEND_API_URL: amplifyConfig?.meta?.backend_api_url,
},
experimental: {
@@ -32,7 +36,7 @@ const nextConfig = (phase) => {
},
async rewrites() {
- if (isDevServer) {
+ if (includeAuthPages) {
return [
{
source: '/auth/signout',
@@ -52,7 +56,7 @@ const nextConfig = (phase) => {
// pages with e.g. .dev.tsx extension are only included when running locally
pageExtensions: ['ts', 'tsx', 'js', 'jsx'].flatMap((extension) => {
- return isDevServer ? [`dev.${extension}`, extension] : [extension];
+ return includeAuthPages ? [`dev.${extension}`, extension] : [extension];
}),
};
};
diff --git a/frontend/package.json b/frontend/package.json
index 6fbce8a9..ae84675f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -6,9 +6,9 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
- "lint": "next lint --dir .",
- "lint:fix": "next lint --dir . --fix",
- "test:unit": "jest",
+ "lint": "npm run mock-amplify-outputs && next lint --dir .",
+ "lint:fix": "npm run lint -- --fix",
+ "test:unit": "npm run mock-amplify-outputs && jest",
"app:start": "pm2 start npm -- start",
"app:wait": "wait-on -l http://localhost:3000/templates/create-and-submit-templates",
"app:stop": "pm2 kill",
@@ -29,6 +29,7 @@
"next": "14.2.13",
"nhs-notify-web-template-management-amplify": "*",
"nhs-notify-web-template-management-utils": "*",
+ "nhs-notify-backend-client": "*",
"nhsuk-frontend": "^8.3.0",
"nhsuk-react-components": "^4.1.1",
"path": "^0.12.7",
diff --git a/frontend/src/__tests__/components/forms/DeleteTemplate/server-action.test.ts b/frontend/src/__tests__/components/forms/DeleteTemplate/server-action.test.ts
index 41e159f6..abb8f1d9 100644
--- a/frontend/src/__tests__/components/forms/DeleteTemplate/server-action.test.ts
+++ b/frontend/src/__tests__/components/forms/DeleteTemplate/server-action.test.ts
@@ -29,13 +29,10 @@ test('calls form action and redirects', async () => {
await deleteTemplateAction(mockTemplate);
- expect(mockSaveTemplate).toHaveBeenCalledWith(
- {
- ...mockTemplate,
- templateStatus: TemplateStatus.DELETED,
- },
- 1_643_619_600
- );
+ expect(mockSaveTemplate).toHaveBeenCalledWith({
+ ...mockTemplate,
+ templateStatus: TemplateStatus.DELETED,
+ });
expect(mockRedirect).toHaveBeenCalledWith(
'/manage-templates',
diff --git a/frontend/src/__tests__/components/forms/SubmitTemplate/server-action.test.ts b/frontend/src/__tests__/components/forms/SubmitTemplate/server-action.test.ts
index 344f16ba..439b48c3 100644
--- a/frontend/src/__tests__/components/forms/SubmitTemplate/server-action.test.ts
+++ b/frontend/src/__tests__/components/forms/SubmitTemplate/server-action.test.ts
@@ -92,12 +92,7 @@ describe('submitTemplate', () => {
await submitTemplate('submit-route', formData);
- expect(sendEmailMock).toHaveBeenCalledWith(
- mockNhsAppTemplate.id,
- mockNhsAppTemplate.name,
- mockNhsAppTemplate.message,
- null
- );
+ expect(sendEmailMock).toHaveBeenCalledWith(mockNhsAppTemplate.id);
expect(redirectMock).toHaveBeenCalledWith('/submit-route/1', 'push');
});
@@ -120,11 +115,6 @@ describe('submitTemplate', () => {
await submitTemplate('submit-route', formData);
- expect(sendEmailMock).toHaveBeenCalledWith(
- mockEmailTemplate.id,
- mockEmailTemplate.name,
- mockEmailTemplate.message,
- mockEmailTemplate.subject
- );
+ expect(sendEmailMock).toHaveBeenCalledWith(mockEmailTemplate.id);
});
});
diff --git a/frontend/src/__tests__/components/molecules/__snapshots__/AuthLink.test.tsx.snap b/frontend/src/__tests__/components/molecules/__snapshots__/AuthLink.test.tsx.snap
index 2c3c5b50..7662aa01 100644
--- a/frontend/src/__tests__/components/molecules/__snapshots__/AuthLink.test.tsx.snap
+++ b/frontend/src/__tests__/components/molecules/__snapshots__/AuthLink.test.tsx.snap
@@ -42,7 +42,7 @@ exports[`AuthLink renders Log out link when authStatus is authenticated 1`] = `
>
Log out
diff --git a/frontend/src/__tests__/utils/amplify-utils.test.ts b/frontend/src/__tests__/utils/amplify-utils.test.ts
index 714fdbe0..c68eed4c 100644
--- a/frontend/src/__tests__/utils/amplify-utils.test.ts
+++ b/frontend/src/__tests__/utils/amplify-utils.test.ts
@@ -1,33 +1,79 @@
/**
* @jest-environment node
*/
-import { getAmplifyBackendClient } from '@utils/amplify-utils';
+import {
+ getAmplifyBackendClient,
+ getAccessTokenServer,
+} from '@utils/amplify-utils';
import { generateServerClientUsingCookies } from '@aws-amplify/adapter-nextjs/api';
+import { fetchAuthSession } from 'aws-amplify/auth/server';
import nextHeaders from 'next/headers';
+jest.mock('aws-amplify/auth/server');
jest.mock('@aws-amplify/adapter-nextjs/api');
+jest.mock('next/headers', () => ({
+ cookies: () => ({
+ getAll: jest.fn(),
+ }),
+}));
jest.mock('@/amplify_outputs.json', () => ({
name: 'mockConfig',
}));
-jest.mock('next/headers', () => ({
- cookies: () => {},
-}));
-test('getAmplifyBackendClient', () => {
- // arrange
- const generateServerClientUsingCookiesMock = jest.mocked(
- generateServerClientUsingCookies
- );
- const cookiesSpy = jest.spyOn(nextHeaders, 'cookies');
-
- // act
- getAmplifyBackendClient();
-
- // assert
- expect(generateServerClientUsingCookiesMock).toHaveBeenCalledTimes(1);
- expect(generateServerClientUsingCookiesMock).toHaveBeenCalledWith({
- config: { name: 'mockConfig' },
- cookies: cookiesSpy,
- authMode: 'iam',
+const fetchAuthSessionMock = jest.mocked(fetchAuthSession);
+
+describe('amplify-utils', () => {
+ test('getAmplifyBackendClient', () => {
+ // arrange
+ const generateServerClientUsingCookiesMock = jest.mocked(
+ generateServerClientUsingCookies
+ );
+ const cookiesSpy = jest.spyOn(nextHeaders, 'cookies');
+
+ // act
+ getAmplifyBackendClient();
+
+ // assert
+ expect(generateServerClientUsingCookiesMock).toHaveBeenCalledTimes(1);
+ expect(generateServerClientUsingCookiesMock).toHaveBeenCalledWith({
+ config: { name: 'mockConfig' },
+ cookies: cookiesSpy,
+ authMode: 'iam',
+ });
+ });
+
+ test('getAccessTokenServer - should return the auth token', async () => {
+ fetchAuthSessionMock.mockResolvedValue({
+ tokens: {
+ accessToken: {
+ toString: () => 'mockSub',
+ payload: {
+ sub: 'mockSub',
+ },
+ },
+ },
+ });
+
+ const result = await getAccessTokenServer();
+
+ expect(result).toEqual('mockSub');
+ });
+
+ test('getAccessTokenServer - should return undefined when no auth session', async () => {
+ fetchAuthSessionMock.mockResolvedValue({});
+
+ const result = await getAccessTokenServer();
+
+ expect(result).toBeUndefined();
+ });
+
+ test('getAccessTokenServer - should return undefined an error occurs', async () => {
+ fetchAuthSessionMock.mockImplementationOnce(() => {
+ throw new Error('JWT Expired');
+ });
+
+ const result = await getAccessTokenServer();
+
+ expect(result).toBeUndefined();
});
});
diff --git a/frontend/src/__tests__/utils/form-actions.test.ts b/frontend/src/__tests__/utils/form-actions.test.ts
index 2808bf02..68a6b2ed 100644
--- a/frontend/src/__tests__/utils/form-actions.test.ts
+++ b/frontend/src/__tests__/utils/form-actions.test.ts
@@ -7,7 +7,6 @@ import {
TemplateType,
TemplateStatus,
} from 'nhs-notify-web-template-management-utils';
-import { logger } from 'nhs-notify-web-template-management-utils/logger';
import {
createTemplate,
saveTemplate,
@@ -15,402 +14,351 @@ import {
sendEmail,
getTemplates,
} from '@utils/form-actions';
-import { getAmplifyBackendClient } from '@utils/amplify-utils';
+import { getAccessTokenServer } from '@utils/amplify-utils';
import { mockDeep } from 'jest-mock-extended';
-import type { Template } from 'nhs-notify-web-template-management-utils';
-
-jest.mock('@aws-amplify/adapter-nextjs/data');
-jest.mock('node:crypto');
-
-const mockResponseData = {
- id: 'id',
- templateId: 'template-id',
- createdAt: 'created-at',
- updatedAt: 'updated-at',
- name: 'template-name',
- message: 'template-message',
-};
-
-const mockTemplates: Template[] = [
- {
- id: '1',
- templateType: TemplateType.NHS_APP,
- templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
- name: 'Template 1',
- message: 'Message',
- subject: 'Subject Line',
- createdAt: '2021-01-01T00:00:00.000Z',
- },
-];
+import { IBackendClient } from 'nhs-notify-backend-client/src/types/backend-client';
+
+const mockedBackendClient = mockDeep();
+const authIdTokenServerMock = jest.mocked(getAccessTokenServer);
jest.mock('@utils/amplify-utils');
+jest.mock('nhs-notify-backend-client/src/backend-api-client', () => ({
+ BackendClient: () => mockedBackendClient,
+}));
+
+describe('form-actions', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ authIdTokenServerMock.mockResolvedValueOnce('token');
+ });
-beforeEach(() => {
- jest.resetAllMocks();
-});
+ test('createTemplate', async () => {
+ const responseData = {
+ id: 'id',
+ version: 1,
+ templateType: TemplateType.NHS_APP,
+ templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
+ name: 'name',
+ message: 'message',
+ createdAt: 'today',
+ updatedAt: 'today',
+ };
-type MockSchema = ReturnType;
+ mockedBackendClient.templates.createTemplate.mockResolvedValueOnce({
+ data: responseData,
+ });
-type MockSchemaInput = Parameters>[0];
+ const createTemplateInput: Draft = {
+ templateType: TemplateType.NHS_APP,
+ templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
+ name: 'name',
+ message: 'message',
+ };
-const setup = (schema: MockSchemaInput) => {
- const mockSchema = mockDeep(schema);
+ const response = await createTemplate(createTemplateInput);
- jest.mocked(getAmplifyBackendClient).mockReturnValue(mockSchema);
-};
+ expect(mockedBackendClient.templates.createTemplate).toHaveBeenCalledWith(
+ createTemplateInput
+ );
-test('createTemplate', async () => {
- const mockCreateTemplate = jest
- .fn()
- .mockReturnValue({ data: mockResponseData });
- setup({
- models: {
- TemplateStorage: {
- create: mockCreateTemplate,
- },
- },
+ expect(response).toEqual(responseData);
});
- const createTemplateInput: Draft = {
- templateType: TemplateType.NHS_APP,
- templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
- name: 'name',
- message: 'message',
- };
-
- const response = await createTemplate(createTemplateInput);
-
- expect(mockCreateTemplate).toHaveBeenCalledWith(createTemplateInput);
- expect(response).toEqual(mockResponseData);
-});
-
-test('createTemplate - error handling', async () => {
- const mockcreateTemplate = jest.fn().mockReturnValue({
- errors: [
- {
- message: 'test-error-message',
- errorType: 'test-error-type',
- errorInfo: { error: 'test-error' },
- },
- ],
- });
- setup({
- models: {
- TemplateStorage: {
- create: mockcreateTemplate,
+ test('createTemplate - should thrown error when saving unexpectedly fails', async () => {
+ mockedBackendClient.templates.createTemplate.mockResolvedValueOnce({
+ error: {
+ code: 400,
+ message: 'Bad request',
},
- },
- });
+ });
- await expect(
- createTemplate({
+ const createTemplateInput: Draft = {
templateType: TemplateType.NHS_APP,
- } as unknown as Template)
- ).rejects.toThrow('Failed to create new template');
-});
+ templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
+ name: 'name',
+ message: 'message',
+ };
-test('saveTemplate', async () => {
- setup({
- models: {
- TemplateStorage: {
- update: jest.fn().mockReturnValue({ data: mockResponseData }),
- },
- },
- });
+ await expect(createTemplate(createTemplateInput)).rejects.toThrow(
+ 'Failed to create new template'
+ );
- const response = await saveTemplate({
- id: '0c1d3422-a2f6-44ef-969d-d513c7c9d212',
- templateType: TemplateType.NHS_APP,
- templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
- name: 'template-name',
- message: 'template-message',
+ expect(mockedBackendClient.templates.createTemplate).toHaveBeenCalledWith(
+ createTemplateInput
+ );
});
- expect(response).toEqual(mockResponseData);
-});
+ test('createTemplate - should thrown error when no token', async () => {
+ authIdTokenServerMock.mockReset();
+ authIdTokenServerMock.mockResolvedValueOnce(undefined);
-test('saveTemplate - includes TTL', async () => {
- setup({
- models: {
- TemplateStorage: {
- update: jest.fn().mockReturnValue({ data: mockResponseData }),
- },
- },
+ const createTemplateInput: Draft = {
+ templateType: TemplateType.NHS_APP,
+ templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
+ name: 'name',
+ message: 'message',
+ };
+
+ await expect(createTemplate(createTemplateInput)).rejects.toThrow(
+ 'Failed to get access token'
+ );
});
- const response = await saveTemplate(
- {
- id: '0c1d3422-a2f6-44ef-969d-d513c7c9d212',
+ test('saveTemplate', async () => {
+ const responseData = {
+ id: 'id',
+ templateType: TemplateType.NHS_APP,
+ templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
+ name: 'name',
+ message: 'message',
+ createdAt: 'today',
+ updatedAt: 'today',
+ };
+
+ mockedBackendClient.templates.updateTemplate.mockResolvedValueOnce({
+ data: responseData,
+ });
+
+ const updateTemplateInput: NHSAppTemplate = {
+ id: 'pickle',
templateType: TemplateType.NHS_APP,
templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
- name: 'template-name',
- message: 'template-message',
- },
- 10
- );
+ name: 'name',
+ message: 'message',
+ };
- expect(response).toEqual(mockResponseData);
-});
+ const response = await saveTemplate(updateTemplateInput);
-test('saveTemplate - error handling', async () => {
- setup({
- models: {
- TemplateStorage: {
- update: jest.fn().mockReturnValue({
- errors: [
- {
- message: 'test-error-message',
- errorType: 'test-error-type',
- errorInfo: { error: 'test-error' },
- },
- ],
- }),
+ expect(mockedBackendClient.templates.updateTemplate).toHaveBeenCalledWith(
+ updateTemplateInput.id,
+ updateTemplateInput
+ );
+
+ expect(response).toEqual(responseData);
+ });
+
+ test('saveTemplate - should thrown error when saving unexpectedly fails', async () => {
+ mockedBackendClient.templates.updateTemplate.mockResolvedValueOnce({
+ error: {
+ code: 400,
+ message: 'Bad request',
},
- },
+ });
+
+ const updateTemplateInput: NHSAppTemplate = {
+ id: 'pickle',
+ templateType: TemplateType.NHS_APP,
+ templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
+ name: 'name',
+ message: 'message',
+ };
+
+ await expect(saveTemplate(updateTemplateInput)).rejects.toThrow(
+ 'Failed to save template data'
+ );
+
+ expect(mockedBackendClient.templates.updateTemplate).toHaveBeenCalledWith(
+ updateTemplateInput.id,
+ updateTemplateInput
+ );
});
- await expect(
- saveTemplate({
- id: '0c1d3422-a2f6-44ef-969d-d513c7c9d212',
+ test('saveTemplate - should thrown error when no token', async () => {
+ authIdTokenServerMock.mockReset();
+ authIdTokenServerMock.mockResolvedValueOnce(undefined);
+
+ const updateTemplateInput: NHSAppTemplate = {
+ id: 'pickle',
templateType: TemplateType.NHS_APP,
templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
- name: 'template-name',
- message: 'template-message',
- })
- ).rejects.toThrow('Failed to save template data');
-});
+ name: 'name',
+ message: 'message',
+ };
-test('saveTemplate - error handling - when no data returned', async () => {
- setup({
- models: {
- TemplateStorage: {
- update: jest.fn().mockReturnValue({
- errors: undefined,
- data: undefined,
- }),
- },
- },
+ await expect(saveTemplate(updateTemplateInput)).rejects.toThrow(
+ 'Failed to get access token'
+ );
});
- await expect(
- saveTemplate({
- id: '0c1d3422-a2f6-44ef-969d-d513c7c9d212',
+ test('getTemplate', async () => {
+ const responseData = {
+ id: 'id',
templateType: TemplateType.NHS_APP,
templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
- name: 'template-name',
- message: 'template-message',
- })
- ).rejects.toThrow(
- 'Template in unknown state. No errors reported but entity returned as falsy'
- );
-});
+ name: 'name',
+ message: 'message',
+ createdAt: 'today',
+ updatedAt: 'today',
+ };
-test('getTemplate', async () => {
- setup({
- models: {
- TemplateStorage: {
- get: jest.fn().mockReturnValue({ data: mockResponseData }),
- },
- },
- });
+ mockedBackendClient.templates.getTemplate.mockResolvedValueOnce({
+ data: responseData,
+ });
- const response = await getTemplate('template-id');
+ const response = await getTemplate('id');
- expect(response).toEqual(mockResponseData);
-});
+ expect(mockedBackendClient.templates.getTemplate).toHaveBeenCalledWith(
+ 'id'
+ );
-test('getTemplate - returns undefined if template is not found', async () => {
- setup({
- models: {
- TemplateStorage: {
- get: jest.fn().mockReturnValue({
- errors: [
- {
- message: 'test-error-message',
- errorType: 'test-error-type',
- errorInfo: { error: 'test-error' },
- },
- ],
- }),
- },
- },
+ expect(response).toEqual(responseData);
});
- const response = await getTemplate('template-id');
+ test('getTemplate - should return undefined when no data', async () => {
+ mockedBackendClient.templates.getTemplate.mockResolvedValueOnce({
+ data: undefined,
+ error: {
+ code: 404,
+ message: 'Not found',
+ },
+ });
- expect(response).toBeUndefined();
-});
+ const response = await getTemplate('id');
-test('sendEmail - no errors', async () => {
- setup({
- queries: {
- sendEmail: jest.fn().mockReturnValue({}),
- },
- });
+ expect(mockedBackendClient.templates.getTemplate).toHaveBeenCalledWith(
+ 'id'
+ );
- const mockErrorLogger = jest.spyOn(logger, 'error');
- await sendEmail('template-id', 'template-name', 'template-message', null);
+ expect(response).toEqual(undefined);
+ });
- expect(mockErrorLogger).not.toHaveBeenCalled();
-});
+ test('getTemplate - should thrown error when no token', async () => {
+ authIdTokenServerMock.mockReset();
+ authIdTokenServerMock.mockResolvedValueOnce(undefined);
-test('sendEmail - errors', async () => {
- setup({
- queries: {
- sendEmail: jest.fn().mockReturnValue({ errors: ['email error'] }),
- },
+ await expect(getTemplate('id')).rejects.toThrow(
+ 'Failed to get access token'
+ );
});
- const mockErrorLogger = jest.spyOn(logger, 'error');
- await sendEmail(
- 'template-id-error',
- 'template-name',
- 'template-message',
- null
- );
-
- expect(mockErrorLogger).toHaveBeenCalledWith({
- description: 'Error sending email',
- res: {
- errors: ['email error'],
- },
+ test('getTemplates', async () => {
+ const responseData = {
+ id: 'id',
+ templateType: TemplateType.NHS_APP,
+ templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
+ name: 'name',
+ message: 'message',
+ createdAt: 'today',
+ updatedAt: 'today',
+ };
+
+ mockedBackendClient.templates.listTemplates.mockResolvedValueOnce({
+ data: [responseData],
+ });
+
+ const response = await getTemplates();
+
+ expect(mockedBackendClient.templates.listTemplates).toHaveBeenCalledWith();
+
+ expect(response).toEqual([responseData]);
});
-});
-test('getTemplates', async () => {
- setup({
- models: {
- TemplateStorage: {
- list: jest.fn().mockReturnValue({ data: mockTemplates }),
+ test('getTemplates - should return empty array when fetching unexpectedly fails', async () => {
+ mockedBackendClient.templates.listTemplates.mockResolvedValueOnce({
+ data: undefined,
+ error: {
+ code: 500,
+ message: 'Internal server error',
},
- },
- });
- const response = await getTemplates();
+ });
- expect(response).toEqual(mockTemplates);
-});
+ const response = await getTemplates();
-test('getTemplates - remove invalid templates from response', async () => {
- const templatesWithInvalidData = [
- ...mockTemplates,
- {
- id: '1',
- templateType: 'invalidType',
- templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
- name: 'Template 1',
- message: 'Message',
- subject: 'Subject Line',
- createdAt: '2021-01-01T00:00:00.000Z',
- },
- ];
- setup({
- models: {
- TemplateStorage: {
- list: jest.fn().mockReturnValue({ data: templatesWithInvalidData }),
- },
- },
+ expect(response).toEqual([]);
});
- const response = await getTemplates();
- expect(response).toEqual(mockTemplates);
-});
+ test('getTemplates - should thrown error when no token', async () => {
+ authIdTokenServerMock.mockReset();
+ authIdTokenServerMock.mockResolvedValueOnce(undefined);
-test('getTemplates - returns empty array if there are no templates/data returned', async () => {
- setup({
- models: {
- TemplateStorage: {
- list: jest.fn().mockReturnValue({ data: [] }),
- },
- },
+ await expect(getTemplates()).rejects.toThrow('Failed to get access token');
});
- const response = await getTemplates();
+ test('sendEmail', async () => {
+ mockedBackendClient.functions.sendEmail.mockResolvedValueOnce({
+ data: undefined,
+ error: undefined,
+ });
- expect(response).toEqual([]);
-});
+ const response = await sendEmail('id');
-test('getTemplates - errors', async () => {
- setup({
- models: {
- TemplateStorage: {
- list: jest.fn().mockReturnValue({
- errors: [
- {
- message: 'test-error-message',
- errorType: 'test-error-type',
- errorInfo: { error: 'test-error' },
- },
- ],
- }),
- },
- },
- });
+ expect(mockedBackendClient.functions.sendEmail).toHaveBeenCalledWith('id');
- const mockErrorLogger = jest.spyOn(logger, 'error');
- const response = await getTemplates();
+ expect(response).toEqual(undefined);
+ });
- expect(mockErrorLogger).toHaveBeenCalledWith('Failed to get templates', [
- {
- errorInfo: { error: 'test-error' },
- errorType: 'test-error-type',
- message: 'test-error-message',
- },
- ]);
+ test('sendEmail - should thrown error when no token', async () => {
+ authIdTokenServerMock.mockReset();
+ authIdTokenServerMock.mockResolvedValueOnce(undefined);
- expect(response).toEqual([]);
-});
+ await expect(sendEmail('id')).rejects.toThrow('Failed to get access token');
+ });
-test('getTemplates - order by createdAt and then id', async () => {
- const baseTemplate = {
- templateType: 'SMS',
- templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
- name: 'Template',
- message: 'Message',
- };
-
- const templates = [
- { ...baseTemplate, id: '06', createdAt: '2022-01-01T00:00:00.000Z' },
- { ...baseTemplate, id: '08', createdAt: '2020-01-01T00:00:00.000Z' },
- { ...baseTemplate, id: '05', createdAt: '2021-01-01T00:00:00.000Z' },
- { ...baseTemplate, id: '02', createdAt: '2021-01-01T00:00:00.000Z' },
- { ...baseTemplate, id: '09' },
- { ...baseTemplate, id: '10' },
- { ...baseTemplate, id: '01', createdAt: '2021-01-01T00:00:00.000Z' },
- { ...baseTemplate, id: '07' },
- { ...baseTemplate, id: '03', createdAt: '2021-01-01T00:00:00.000Z' },
- { ...baseTemplate, id: '04', createdAt: '2021-01-01T00:00:00.000Z' },
- ];
-
- // 06 is the newest, 08 is the oldest.
- // Templates without a createdAt, 07, 09 and 10, go at the end.
- // 01 - 05 all have the same createdAt.
- const expectedOrder = [
- '06',
- '01',
- '02',
- '03',
- '04',
- '05',
- '08',
- '07',
- '09',
- '10',
- ];
-
- setup({
- models: {
- TemplateStorage: {
- list: jest.fn().mockReturnValue({ data: templates }),
+ test('getTemplates - should return nothing when an error occurs', async () => {
+ mockedBackendClient.functions.sendEmail.mockResolvedValueOnce({
+ data: undefined,
+ error: {
+ code: 404,
+ message: 'Not found',
},
- },
- });
+ });
- const response = await getTemplates();
+ const response = await sendEmail('id');
- const actualOrder = [];
- for (const template of response) {
- actualOrder.push(template.id);
- }
+ expect(mockedBackendClient.functions.sendEmail).toHaveBeenCalledWith('id');
- expect(actualOrder).toEqual(expectedOrder);
+ expect(response).toEqual(undefined);
+ });
+
+ test('getTemplates - order by createdAt and then id', async () => {
+ const baseTemplate = {
+ templateType: TemplateType.SMS,
+ templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
+ name: 'Template',
+ message: 'Message',
+ updatedAt: '2021-01-01T00:00:00.000Z',
+ };
+
+ const templates = [
+ { ...baseTemplate, id: '06', createdAt: '2022-01-01T00:00:00.000Z' },
+ { ...baseTemplate, id: '08', createdAt: '2020-01-01T00:00:00.000Z' },
+ { ...baseTemplate, id: '05', createdAt: '2021-01-01T00:00:00.000Z' },
+ { ...baseTemplate, id: '02', createdAt: '2021-01-01T00:00:00.000Z' },
+ { ...baseTemplate, id: '09', createdAt: undefined as unknown as string },
+ { ...baseTemplate, id: '10', createdAt: undefined as unknown as string },
+ { ...baseTemplate, id: '01', createdAt: '2021-01-01T00:00:00.000Z' },
+ { ...baseTemplate, id: '07', createdAt: undefined as unknown as string },
+ { ...baseTemplate, id: '03', createdAt: '2021-01-01T00:00:00.000Z' },
+ { ...baseTemplate, id: '04', createdAt: '2021-01-01T00:00:00.000Z' },
+ ];
+
+ // 06 is the newest, 08 is the oldest.
+ // Templates without a createdAt, 07, 09 and 10, go at the end.
+ // 01 - 05 all have the same createdAt.
+ const expectedOrder = [
+ '06',
+ '01',
+ '02',
+ '03',
+ '04',
+ '05',
+ '08',
+ '07',
+ '09',
+ '10',
+ ];
+
+ mockedBackendClient.templates.listTemplates.mockResolvedValueOnce({
+ data: templates,
+ });
+
+ const response = await getTemplates();
+
+ const actualOrder = [];
+ for (const template of response) {
+ actualOrder.push(template.id);
+ }
+
+ expect(actualOrder).toEqual(expectedOrder);
+ });
});
diff --git a/frontend/src/app/auth/page.dev.tsx b/frontend/src/app/auth/page.dev.tsx
index 0bc3104a..09611ef2 100644
--- a/frontend/src/app/auth/page.dev.tsx
+++ b/frontend/src/app/auth/page.dev.tsx
@@ -32,7 +32,17 @@ export const Redirect = () => {
export default function Page() {
return (
Loading...
}>
-
+
diff --git a/frontend/src/app/manage-templates/page.tsx b/frontend/src/app/manage-templates/page.tsx
index 5ce9e839..b9a39f2b 100644
--- a/frontend/src/app/manage-templates/page.tsx
+++ b/frontend/src/app/manage-templates/page.tsx
@@ -1,15 +1,19 @@
-'use server';
-
import { NHSNotifyButton } from '@atoms/NHSNotifyButton/NHSNotifyButton';
import content from '@content/content';
import { ManageTemplates } from '@molecules/ManageTemplates/ManageTemplates';
-import { Template } from 'nhs-notify-web-template-management-utils';
import { getTemplates } from '@utils/form-actions';
+// Note: force this page to be dynamically rendered
+// This is because Next defaults this page as a static rendered page
+// which causes a build failure due to getTemplates attempting to get a server-side session via cookies and failing
+// The other pages which do similar thing expect a templateId parameter
+// Which informs next it needs to be dynamically rendered
+export const dynamic = 'force-dynamic';
+
const manageTemplatesContent = content.pages.manageTemplates;
export default async function ManageTemplatesPage() {
- const availableTemplateList: Template[] | [] = await getTemplates();
+ const availableTemplateList = await getTemplates();
return (
diff --git a/frontend/src/components/forms/DeleteTemplate/server-action.ts b/frontend/src/components/forms/DeleteTemplate/server-action.ts
index 11ed4972..e403fa17 100644
--- a/frontend/src/components/forms/DeleteTemplate/server-action.ts
+++ b/frontend/src/components/forms/DeleteTemplate/server-action.ts
@@ -5,30 +5,13 @@ import {
} from 'nhs-notify-web-template-management-utils';
import { saveTemplate } from '@utils/form-actions';
-// remove this when we stop using amplify backend, the API handles TTL logic
-const calculateTTL = () => {
- const currentTimeSeconds = Math.floor(Date.now() / 1000);
-
- const maxSessionLengthInSeconds = Number.parseInt(
- process.env.MAX_SESSION_LENGTH_IN_SECONDS ?? '2592000',
- 10
- ); // 30 days in seconds
-
- return currentTimeSeconds + maxSessionLengthInSeconds;
-};
-
export const deleteTemplateAction = async (
template: ChannelTemplate
): Promise
=> {
- const ttl = calculateTTL();
-
- await saveTemplate(
- {
- ...template,
- templateStatus: TemplateStatus.DELETED,
- },
- ttl
- );
+ await saveTemplate({
+ ...template,
+ templateStatus: TemplateStatus.DELETED,
+ });
redirect('/manage-templates', RedirectType.push);
};
diff --git a/frontend/src/components/forms/SubmitTemplate/server-action.ts b/frontend/src/components/forms/SubmitTemplate/server-action.ts
index 8b7120b2..c24dda71 100644
--- a/frontend/src/components/forms/SubmitTemplate/server-action.ts
+++ b/frontend/src/components/forms/SubmitTemplate/server-action.ts
@@ -32,8 +32,7 @@ export async function submitTemplate(route: string, formData: FormData) {
templateStatus: TemplateStatus.SUBMITTED,
});
- const { name, subject, message } = { subject: null, ...validatedTemplate };
- await sendEmail(templateId, name, message, subject);
+ await sendEmail(templateId);
} catch (error) {
logger.error('Failed to submit template', {
error,
diff --git a/frontend/src/components/layouts/container/container.tsx b/frontend/src/components/layouts/container/container.tsx
index 8d6e70e0..56ba1527 100644
--- a/frontend/src/components/layouts/container/container.tsx
+++ b/frontend/src/components/layouts/container/container.tsx
@@ -1,4 +1,4 @@
-export async function NHSNotifyContainer({
+export function NHSNotifyContainer({
children,
}: {
children: React.ReactNode;
diff --git a/frontend/src/components/molecules/ManageTemplates/ManageTemplates.tsx b/frontend/src/components/molecules/ManageTemplates/ManageTemplates.tsx
index 966d58ca..447c3060 100644
--- a/frontend/src/components/molecules/ManageTemplates/ManageTemplates.tsx
+++ b/frontend/src/components/molecules/ManageTemplates/ManageTemplates.tsx
@@ -14,6 +14,7 @@ import {
templateTypeDisplayMappings,
viewSubmittedTemplatePages,
} from 'nhs-notify-web-template-management-utils';
+import { TemplateDTO } from 'nhs-notify-backend-client';
const manageTemplatesContent = content.pages.manageTemplates;
@@ -28,7 +29,7 @@ const generateViewTemplateLink = (template: Template): string => {
export function ManageTemplates({
templateList,
}: {
- templateList: Template[];
+ templateList: Template[] | TemplateDTO[];
}) {
return (
diff --git a/frontend/src/content/content.ts b/frontend/src/content/content.ts
index ab7bd21c..ab758b30 100644
--- a/frontend/src/content/content.ts
+++ b/frontend/src/content/content.ts
@@ -12,7 +12,9 @@ const headerComponent = {
},
logOut: {
text: 'Log out',
- href: '/auth/signout',
+ href: `/auth/signout?redirect=${encodeURIComponent(
+ `${getBasePath()}/create-and-submit-templates`
+ )}`,
},
},
};
diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts
new file mode 100644
index 00000000..ef17e4d6
--- /dev/null
+++ b/frontend/src/middleware.ts
@@ -0,0 +1,40 @@
+import { NextResponse, type NextRequest } from 'next/server';
+import { getAccessTokenServer } from '@utils/amplify-utils';
+import { getBasePath } from '@utils/get-base-path';
+
+function isExcludedPath(path: string, excludedPaths: string[]): boolean {
+ return excludedPaths.some((excludedPath) => path.startsWith(excludedPath));
+}
+
+export async function middleware(request: NextRequest) {
+ const excludedPaths = ['/create-and-submit-templates', '/auth'];
+
+ if (isExcludedPath(request.nextUrl.pathname, excludedPaths)) {
+ return NextResponse.next();
+ }
+
+ const token = await getAccessTokenServer();
+
+ if (!token) {
+ return Response.redirect(
+ new URL(
+ `/auth?redirect=${encodeURIComponent(
+ `${getBasePath()}/${request.nextUrl.pathname}`
+ )}`,
+ request.url
+ )
+ );
+ }
+}
+
+export const config = {
+ matcher: [
+ /*
+ * Match all request paths except for the ones starting with:
+ * - _next/static (static files)
+ * - _next/image (image optimization files)
+ * - favicon.ico (favicon file)
+ */
+ '/((?!_next/static|_next/image|favicon.ico).*)',
+ ],
+};
diff --git a/frontend/src/utils/amplify-utils.ts b/frontend/src/utils/amplify-utils.ts
index 4b5af9f6..1e28a765 100644
--- a/frontend/src/utils/amplify-utils.ts
+++ b/frontend/src/utils/amplify-utils.ts
@@ -5,12 +5,31 @@
import { cookies } from 'next/headers';
import { generateServerClientUsingCookies } from '@aws-amplify/adapter-nextjs/data';
import { Schema } from 'nhs-notify-web-template-management-amplify';
+import { createServerRunner } from '@aws-amplify/adapter-nextjs';
+import { fetchAuthSession } from 'aws-amplify/auth/server';
const config = require('@/amplify_outputs.json');
+export const { runWithAmplifyServerContext } = createServerRunner({
+ config,
+});
+
export const getAmplifyBackendClient = () =>
generateServerClientUsingCookies
({
config,
cookies,
authMode: 'iam',
});
+
+export async function getAccessTokenServer(): Promise {
+ try {
+ const { tokens } = await runWithAmplifyServerContext({
+ nextServerContext: { cookies },
+ operation: fetchAuthSession,
+ });
+
+ return tokens?.accessToken?.toString();
+ } catch {
+ // no-op
+ }
+}
diff --git a/frontend/src/utils/form-actions.ts b/frontend/src/utils/form-actions.ts
index c0567358..3345eb8d 100644
--- a/frontend/src/utils/form-actions.ts
+++ b/frontend/src/utils/form-actions.ts
@@ -1,117 +1,109 @@
-/* eslint-disable array-callback-return */
-
'use server';
-import { getAmplifyBackendClient } from '@utils/amplify-utils';
-import { DbOperationError } from '@domain/errors';
+import { getAccessTokenServer } from '@utils/amplify-utils';
import {
Template,
Draft,
isTemplateValid,
- TemplateStatus,
} from 'nhs-notify-web-template-management-utils';
+import { BackendClient, TemplateDTO } from 'nhs-notify-backend-client';
import { logger } from 'nhs-notify-web-template-management-utils/logger';
export async function createTemplate(
template: Draft
-): Promise {
- const { data, errors } =
- await getAmplifyBackendClient().models.TemplateStorage.create(template);
+): Promise {
+ const token = await getAccessTokenServer();
+
+ if (!token) {
+ throw new Error('Failed to get access token');
+ }
- if (errors || !data) {
- logger.error('Failed to create template', errors, template);
+ const { data, error } =
+ await BackendClient(token).templates.createTemplate(template);
+
+ if (error) {
+ logger.error('Failed to create template', { error });
throw new Error('Failed to create new template');
}
+
return data;
}
export async function saveTemplate(
- template: Template,
- ttl?: number // remove ttl after we get rid of Amplify backend, our own API will handle the TTLs without input from the client
-): Promise {
- const { data, errors } =
- await getAmplifyBackendClient().models.TemplateStorage.update({
- ...template,
- ...(ttl && { ttl }),
- });
+ template: Template
+): Promise {
+ const token = await getAccessTokenServer();
- if (errors) {
- logger.error('Failed to save template', errors);
- throw new Error('Failed to save template data');
+ if (!token) {
+ throw new Error('Failed to get access token');
}
- if (!data) {
- throw new DbOperationError({
- message:
- 'Template in unknown state. No errors reported but entity returned as falsy',
- operation: 'update',
- });
+ const { data, error } = await BackendClient(token).templates.updateTemplate(
+ template.id,
+ template
+ );
+
+ if (error) {
+ logger.error('Failed to save template', { error });
+ throw new Error('Failed to save template data');
}
+
return data;
}
export async function getTemplate(
templateId: string
-): Promise {
- const { data, errors } =
- await getAmplifyBackendClient().models.TemplateStorage.get({
- id: templateId,
- });
+): Promise {
+ const token = await getAccessTokenServer();
- if (errors) {
- logger.error('Failed to get template', errors);
+ if (!token) {
+ throw new Error('Failed to get access token');
}
- if (!data) {
- logger.warn(`Failed to retrieve template for ID ${templateId}`);
- return undefined;
+ const { data, error } =
+ await BackendClient(token).templates.getTemplate(templateId);
+
+ if (error) {
+ logger.error('Failed to get template', { error });
}
return data;
}
-export async function sendEmail(
- templateId: string,
- templateName: string,
- templateMessage: string,
- templateSubjectLine: string | null
-) {
- const res = await getAmplifyBackendClient().queries.sendEmail({
- recipientEmail: 'england.test.cm@nhs.net',
- templateId,
- templateName,
- templateMessage,
- templateSubjectLine,
- });
-
- if (res.errors) {
+export async function sendEmail(templateId: string) {
+ const token = await getAccessTokenServer();
+
+ if (!token) {
+ throw new Error('Failed to get access token');
+ }
+
+ const { error } = await BackendClient(token).functions.sendEmail(templateId);
+
+ if (error) {
logger.error({
description: 'Error sending email',
- res,
+ error,
});
}
}
-export async function getTemplates(): Promise {
- const { data, errors } =
- await getAmplifyBackendClient().models.TemplateStorage.list();
+export async function getTemplates(): Promise {
+ const token = await getAccessTokenServer();
- if (errors) {
- logger.error('Failed to get templates', errors);
+ if (!token) {
+ throw new Error('Failed to get access token');
}
- if (!data) {
- logger.warn(`Failed to retrieve templates`);
+ const { data, error } = await BackendClient(token).templates.listTemplates();
+
+ if (error) {
+ logger.error('Failed to get templates', { error });
return [];
}
- const parsedData: Template[] = data
+ const sortedData = data
.map((template) => isTemplateValid(template))
- .filter(
- (template): template is Template =>
- template !== undefined &&
- template.templateStatus !== TemplateStatus.DELETED // when we switch over to the API we should remove this code because the API will handle the deleted filter
- )
+ .filter((template): template is Template => template !== undefined)
.sort((a, b) => {
const aCreatedAt = a.createdAt ?? '';
const bCreatedAt = b.createdAt ?? '';
@@ -122,5 +114,5 @@ export async function getTemplates(): Promise {
return aCreatedAt < bCreatedAt ? 1 : -1;
});
- return parsedData;
+ return sortedData;
}
diff --git a/frontend/src/utils/validate-template.ts b/frontend/src/utils/validate-template.ts
index 271707bc..7f9bef98 100644
--- a/frontend/src/utils/validate-template.ts
+++ b/frontend/src/utils/validate-template.ts
@@ -16,10 +16,13 @@ import {
$SubmittedSMSTemplate,
$SubmittedNHSAppTemplate,
} from 'nhs-notify-web-template-management-utils';
+import { TemplateDTO } from 'nhs-notify-backend-client';
import { logger } from 'nhs-notify-web-template-management-utils/logger';
+type TemplateUnion = Template | TemplateDTO | undefined;
+
export const validateNHSAppTemplate = (
- template: Template | undefined
+ template: TemplateUnion
): NHSAppTemplate | undefined => {
try {
return $NHSAppTemplate.parse(template);
@@ -30,7 +33,7 @@ export const validateNHSAppTemplate = (
};
export const validateSMSTemplate = (
- template: Template | undefined
+ template: TemplateUnion
): SMSTemplate | undefined => {
try {
return $SMSTemplate.parse(template);
@@ -41,7 +44,7 @@ export const validateSMSTemplate = (
};
export const validateEmailTemplate = (
- template: Template | undefined
+ template: TemplateUnion
): EmailTemplate | undefined => {
try {
return $EmailTemplate.parse(template);
@@ -52,7 +55,7 @@ export const validateEmailTemplate = (
};
export const validateSubmittedEmailTemplate = (
- template: Template | undefined
+ template: TemplateUnion
): SubmittedEmailTemplate | undefined => {
try {
return $SubmittedEmailTemplate.parse(template);
@@ -63,7 +66,7 @@ export const validateSubmittedEmailTemplate = (
};
export const validateSubmittedSMSTemplate = (
- template: Template | undefined
+ template: TemplateUnion
): SubmittedSMSTemplate | undefined => {
try {
return $SubmittedSMSTemplate.parse(template);
@@ -74,7 +77,7 @@ export const validateSubmittedSMSTemplate = (
};
export const validateSubmittedNHSAppTemplate = (
- template: Template | undefined
+ template: TemplateUnion
): SubmittedNHSAppTemplate | undefined => {
try {
return $SubmittedNHSAppTemplate.parse(template);
@@ -85,7 +88,7 @@ export const validateSubmittedNHSAppTemplate = (
};
export const validateChannelTemplate = (
- template: Template | undefined
+ template: TemplateUnion
): ChannelTemplate | undefined => {
try {
return $ChannelTemplate.parse(template);
diff --git a/infrastructure/terraform/components/app/amplify_app.tf b/infrastructure/terraform/components/app/amplify_app.tf
index 638fc58a..5b1970c3 100644
--- a/infrastructure/terraform/components/app/amplify_app.tf
+++ b/infrastructure/terraform/components/app/amplify_app.tf
@@ -35,5 +35,7 @@ resource "aws_amplify_app" "main" {
NEXT_PUBLIC_DISABLE_CONTENT = var.disable_content
AMPLIFY_MONOREPO_APP_ROOT = "frontend"
BACKEND_API_URL = module.backend_api.api_base_url
+ USER_POOL_ID = jsondecode(data.aws_ssm_parameter.cognito_config.value)["USER_POOL_ID"]
+ USER_POOL_CLIENT_ID = jsondecode(data.aws_ssm_parameter.cognito_config.value)["USER_POOL_CLIENT_ID"]
}
}
diff --git a/infrastructure/terraform/components/sandbox/aws_cognito_user_pool_client_sandbox.tf b/infrastructure/terraform/components/sandbox/aws_cognito_user_pool_client_sandbox.tf
index 7c8e9b41..919b13f5 100644
--- a/infrastructure/terraform/components/sandbox/aws_cognito_user_pool_client_sandbox.tf
+++ b/infrastructure/terraform/components/sandbox/aws_cognito_user_pool_client_sandbox.tf
@@ -4,6 +4,17 @@ resource "aws_cognito_user_pool_client" "sandbox" {
explicit_auth_flows = [
"ALLOW_USER_PASSWORD_AUTH",
- "ALLOW_REFRESH_TOKEN_AUTH"
+ "ALLOW_REFRESH_TOKEN_AUTH",
+ "ALLOW_USER_SRP_AUTH",
]
+
+ access_token_validity = 15 # 1 minutes
+ id_token_validity = 15 # 1 minutes
+ refresh_token_validity = 1 # 1 hour
+
+ token_validity_units {
+ access_token = "minutes"
+ id_token = "minutes"
+ refresh_token = "hours"
+ }
}
diff --git a/infrastructure/terraform/components/sandbox/outputs.tf b/infrastructure/terraform/components/sandbox/outputs.tf
index 255c1172..eba0cbaa 100644
--- a/infrastructure/terraform/components/sandbox/outputs.tf
+++ b/infrastructure/terraform/components/sandbox/outputs.tf
@@ -9,3 +9,7 @@ output "cognito_user_pool_id" {
output "cognito_user_pool_client_id" {
value = aws_cognito_user_pool_client.sandbox.id
}
+
+output "dynamodb_table_templates" {
+ value = module.backend_api.dynamodb_table_templates
+}
diff --git a/infrastructure/terraform/modules/backend-api/outputs.tf b/infrastructure/terraform/modules/backend-api/outputs.tf
index 4c179c12..ebda53bd 100644
--- a/infrastructure/terraform/modules/backend-api/outputs.tf
+++ b/infrastructure/terraform/modules/backend-api/outputs.tf
@@ -1,3 +1,7 @@
output "api_base_url" {
value = aws_api_gateway_stage.main.invoke_url
}
+
+output "dynamodb_table_templates" {
+ value = aws_dynamodb_table.templates.name
+}
diff --git a/infrastructure/terraform/modules/backend-api/spec.tmpl.json b/infrastructure/terraform/modules/backend-api/spec.tmpl.json
index 8309e9b6..9345499b 100644
--- a/infrastructure/terraform/modules/backend-api/spec.tmpl.json
+++ b/infrastructure/terraform/modules/backend-api/spec.tmpl.json
@@ -333,7 +333,7 @@
"authorizerUri": "arn:aws:apigateway:${AWS_REGION}:lambda:path/2015-03-31/functions/${AUTHORIZER_LAMBDA_ARN}/invocations",
"authorizerCredentials": "${APIG_EXECUTION_ROLE_ARN}",
"identitySource": "method.request.header.authorization,context.resourceId,context.httpMethod",
- "authorizerResultTtlInSeconds": 300
+ "authorizerResultTtlInSeconds": 0
}
}
},
@@ -382,8 +382,8 @@
"type": "string",
"enum": [
"NHS_APP",
- "SMS",
- "EMAIL"
+ "EMAIL",
+ "SMS"
]
},
"TemplateStatus": {
diff --git a/lambdas/authorizer/src/__tests__/index.test.ts b/lambdas/authorizer/src/__tests__/index.test.ts
index 88d578c3..bd3f6dc4 100644
--- a/lambdas/authorizer/src/__tests__/index.test.ts
+++ b/lambdas/authorizer/src/__tests__/index.test.ts
@@ -32,7 +32,7 @@ jest.mock('@aws-sdk/client-cognito-identity-provider', () => {
) {
return {
Username: undefined,
- UserAttributes: [{ Name: 'email', Value: 'email' }],
+ UserAttributes: [{ Name: 'sub', Value: 'sub' }],
};
}
@@ -48,17 +48,17 @@ jest.mock('@aws-sdk/client-cognito-identity-provider', () => {
if (
decodedJwt.iss ===
- 'https://cognito-idp.eu-west-2.amazonaws.com/user-pool-id-cognito-no-email'
+ 'https://cognito-idp.eu-west-2.amazonaws.com/user-pool-id-cognito-no-sub'
) {
return {
Username: 'username',
- UserAttributes: [{ Name: 'NOT-EMAIL', Value: 'not-email' }],
+ UserAttributes: [{ Name: 'NOT-SUB', Value: 'not-sub' }],
};
}
return {
Username: 'username',
- UserAttributes: [{ Name: 'email', Value: 'email' }],
+ UserAttributes: [{ Name: 'sub', Value: 'sub' }],
};
}
}
@@ -100,8 +100,7 @@ const allowPolicy = {
],
},
context: {
- username: 'username',
- email: 'email',
+ user: 'sub',
},
};
@@ -361,14 +360,14 @@ test.each([
expect(warnMock).toHaveBeenCalledWith('Missing user');
});
-test('returns Deny policy, when no email on Cognito UserAttributes', async () => {
- process.env.USER_POOL_ID = 'user-pool-id-cognito-no-email';
+test('returns Deny policy, when no sub on Cognito UserAttributes', async () => {
+ process.env.USER_POOL_ID = 'user-pool-id-cognito-no-sub';
const jwt = sign(
{
token_use: 'access',
client_id: 'user-pool-client-id',
- iss: 'https://cognito-idp.eu-west-2.amazonaws.com/user-pool-id-cognito-no-email',
+ iss: 'https://cognito-idp.eu-west-2.amazonaws.com/user-pool-id-cognito-no-sub',
},
'key',
{
@@ -387,7 +386,7 @@ test('returns Deny policy, when no email on Cognito UserAttributes', async () =>
);
expect(res).toEqual(denyPolicy);
- expect(warnMock).toHaveBeenCalledWith('Missing user email address');
+ expect(warnMock).toHaveBeenCalledWith('Missing user subject');
});
test('returns Allow policy on valid token', async () => {
diff --git a/lambdas/authorizer/src/index.ts b/lambdas/authorizer/src/index.ts
index ae54163a..2f46b77c 100644
--- a/lambdas/authorizer/src/index.ts
+++ b/lambdas/authorizer/src/index.ts
@@ -22,7 +22,7 @@ const $AccessToken = z.object({
const generatePolicy = (
Resource: string,
Effect: 'Allow' | 'Deny',
- context?: { username: string; email: string }
+ context?: { user: string }
) => ({
principalId: 'api-caller',
policyDocument: {
@@ -110,18 +110,15 @@ export const handler: APIGatewayRequestAuthorizerHandler = async ({
return generatePolicy(methodArn, 'Deny');
}
- const emailAddress = UserAttributes.find(
- ({ Name }) => Name === 'email'
- )?.Value;
+ const sub = UserAttributes.find(({ Name }) => Name === 'sub')?.Value;
- if (!emailAddress) {
- logger.warn('Missing user email address');
+ if (!sub) {
+ logger.warn('Missing user subject');
return generatePolicy(methodArn, 'Deny');
}
return generatePolicy(methodArn, 'Allow', {
- username: Username,
- email: emailAddress,
+ user: sub,
});
} catch (error) {
logger.error(error);
diff --git a/lambdas/backend-api/src/__tests__/templates/api/create.test.ts b/lambdas/backend-api/src/__tests__/templates/api/create.test.ts
index c32a2f2a..823ce068 100644
--- a/lambdas/backend-api/src/__tests__/templates/api/create.test.ts
+++ b/lambdas/backend-api/src/__tests__/templates/api/create.test.ts
@@ -16,9 +16,9 @@ const createMock = jest.spyOn(TemplateClient.prototype, 'createTemplate');
describe('Template API - Create', () => {
beforeEach(jest.resetAllMocks);
- test('should return 400 - Invalid request when, no email in requestContext', async () => {
+ test('should return 400 - Invalid request when, no user in requestContext', async () => {
const event = mock({
- requestContext: { authorizer: { email: undefined } },
+ requestContext: { authorizer: { user: undefined } },
body: JSON.stringify({ id: 1 }),
});
@@ -48,7 +48,7 @@ describe('Template API - Create', () => {
});
const event = mock({
- requestContext: { authorizer: { email: 'email' } },
+ requestContext: { authorizer: { user: 'sub' } },
body: undefined,
});
@@ -65,7 +65,7 @@ describe('Template API - Create', () => {
}),
});
- expect(TemplateClient).toHaveBeenCalledWith('email');
+ expect(TemplateClient).toHaveBeenCalledWith('sub');
expect(createMock).toHaveBeenCalledWith({});
});
@@ -79,7 +79,7 @@ describe('Template API - Create', () => {
});
const event = mock({
- requestContext: { authorizer: { email: 'email' } },
+ requestContext: { authorizer: { user: 'sub' } },
body: JSON.stringify({ id: 1 }),
});
@@ -93,7 +93,7 @@ describe('Template API - Create', () => {
}),
});
- expect(TemplateClient).toHaveBeenCalledWith('email');
+ expect(TemplateClient).toHaveBeenCalledWith('sub');
expect(createMock).toHaveBeenCalledWith({ id: 1 });
});
@@ -117,7 +117,7 @@ describe('Template API - Create', () => {
});
const event = mock({
- requestContext: { authorizer: { email: 'email' } },
+ requestContext: { authorizer: { user: 'sub' } },
body: JSON.stringify(create),
});
@@ -128,7 +128,7 @@ describe('Template API - Create', () => {
body: JSON.stringify({ statusCode: 201, template: response }),
});
- expect(TemplateClient).toHaveBeenCalledWith('email');
+ expect(TemplateClient).toHaveBeenCalledWith('sub');
expect(createMock).toHaveBeenCalledWith(create);
});
diff --git a/lambdas/backend-api/src/__tests__/templates/api/get.test.ts b/lambdas/backend-api/src/__tests__/templates/api/get.test.ts
index 7f7ac0e8..da56fd4d 100644
--- a/lambdas/backend-api/src/__tests__/templates/api/get.test.ts
+++ b/lambdas/backend-api/src/__tests__/templates/api/get.test.ts
@@ -15,9 +15,9 @@ const getTemplateMock = jest.spyOn(TemplateClient.prototype, 'getTemplate');
describe('Template API - Get', () => {
beforeEach(jest.resetAllMocks);
- test('should return 400 - Invalid request when, no email in requestContext', async () => {
+ test('should return 400 - Invalid request when, no user in requestContext', async () => {
const event = mock({
- requestContext: { authorizer: { email: undefined } },
+ requestContext: { authorizer: { user: undefined } },
pathParameters: { templateId: '1' },
});
@@ -36,7 +36,7 @@ describe('Template API - Get', () => {
test('should return 400 - Invalid request when, no templateId', async () => {
const event = mock({
- requestContext: { authorizer: { email: 'email' } },
+ requestContext: { authorizer: { user: 'sub' } },
pathParameters: { templateId: undefined },
});
@@ -62,7 +62,7 @@ describe('Template API - Get', () => {
});
const event = mock({
- requestContext: { authorizer: { email: 'email' } },
+ requestContext: { authorizer: { user: 'sub' } },
pathParameters: { templateId: '1' },
});
@@ -76,7 +76,7 @@ describe('Template API - Get', () => {
}),
});
- expect(TemplateClient).toHaveBeenCalledWith('email');
+ expect(TemplateClient).toHaveBeenCalledWith('sub');
expect(getTemplateMock).toHaveBeenCalledWith('1');
});
@@ -96,7 +96,7 @@ describe('Template API - Get', () => {
});
const event = mock({
- requestContext: { authorizer: { email: 'email' } },
+ requestContext: { authorizer: { user: 'sub' } },
pathParameters: { templateId: '1' },
});
@@ -107,7 +107,7 @@ describe('Template API - Get', () => {
body: JSON.stringify({ statusCode: 200, template }),
});
- expect(TemplateClient).toHaveBeenCalledWith('email');
+ expect(TemplateClient).toHaveBeenCalledWith('sub');
expect(getTemplateMock).toHaveBeenCalledWith('1');
});
});
diff --git a/lambdas/backend-api/src/__tests__/templates/api/list.test.ts b/lambdas/backend-api/src/__tests__/templates/api/list.test.ts
index 456a9f4b..641d50e2 100644
--- a/lambdas/backend-api/src/__tests__/templates/api/list.test.ts
+++ b/lambdas/backend-api/src/__tests__/templates/api/list.test.ts
@@ -15,9 +15,9 @@ const listTemplatesMock = jest.spyOn(TemplateClient.prototype, 'listTemplates');
describe('Template API - List', () => {
beforeEach(jest.resetAllMocks);
- test('should return 400 - Invalid request when, no email in requestContext', async () => {
+ test('should return 400 - Invalid request when, no user in requestContext', async () => {
const event = mock({
- requestContext: { authorizer: { email: undefined } },
+ requestContext: { authorizer: { user: undefined } },
});
const result = await handler(event, mock(), jest.fn());
@@ -42,7 +42,7 @@ describe('Template API - List', () => {
});
const event = mock({
- requestContext: { authorizer: { email: 'email' } },
+ requestContext: { authorizer: { user: 'sub' } },
pathParameters: { templateId: '1' },
});
@@ -56,7 +56,7 @@ describe('Template API - List', () => {
}),
});
- expect(TemplateClient).toHaveBeenCalledWith('email');
+ expect(TemplateClient).toHaveBeenCalledWith('sub');
expect(listTemplatesMock).toHaveBeenCalled();
});
@@ -77,7 +77,7 @@ describe('Template API - List', () => {
});
const event = mock({
- requestContext: { authorizer: { email: 'email' } },
+ requestContext: { authorizer: { user: 'sub' } },
});
const result = await handler(event, mock(), jest.fn());
@@ -87,7 +87,7 @@ describe('Template API - List', () => {
body: JSON.stringify({ statusCode: 200, templates: [template] }),
});
- expect(TemplateClient).toHaveBeenCalledWith('email');
+ expect(TemplateClient).toHaveBeenCalledWith('sub');
expect(listTemplatesMock).toHaveBeenCalled();
});
diff --git a/lambdas/backend-api/src/__tests__/templates/api/update.test.ts b/lambdas/backend-api/src/__tests__/templates/api/update.test.ts
index d2ea5898..2a046f3b 100644
--- a/lambdas/backend-api/src/__tests__/templates/api/update.test.ts
+++ b/lambdas/backend-api/src/__tests__/templates/api/update.test.ts
@@ -19,9 +19,9 @@ const updateTemplateMock = jest.spyOn(
describe('Template API - Update', () => {
beforeEach(jest.resetAllMocks);
- test('should return 400 - Invalid request when, no email in requestContext', async () => {
+ test('should return 400 - Invalid request when, no user in requestContext', async () => {
const event = mock({
- requestContext: { authorizer: { email: undefined } },
+ requestContext: { authorizer: { user: undefined } },
body: JSON.stringify({ name: 'test' }),
pathParameters: { templateId: '1-2-3' },
});
@@ -52,7 +52,7 @@ describe('Template API - Update', () => {
});
const event = mock({
- requestContext: { authorizer: { email: 'email' } },
+ requestContext: { authorizer: { user: 'sub' } },
pathParameters: { templateId: '1-2-3' },
body: undefined,
});
@@ -70,14 +70,14 @@ describe('Template API - Update', () => {
}),
});
- expect(TemplateClient).toHaveBeenCalledWith('email');
+ expect(TemplateClient).toHaveBeenCalledWith('sub');
expect(updateTemplateMock).toHaveBeenCalledWith('1-2-3', {});
});
test('should return 400 - Invalid request when, no templateId', async () => {
const event = mock({
- requestContext: { authorizer: { email: 'email' } },
+ requestContext: { authorizer: { user: 'sub' } },
body: JSON.stringify({ name: 'test' }),
pathParameters: { templateId: undefined },
});
@@ -104,7 +104,7 @@ describe('Template API - Update', () => {
});
const event = mock({
- requestContext: { authorizer: { email: 'email' } },
+ requestContext: { authorizer: { user: 'sub' } },
body: JSON.stringify({ name: 'name' }),
pathParameters: { templateId: '1-2-3' },
});
@@ -119,7 +119,7 @@ describe('Template API - Update', () => {
}),
});
- expect(TemplateClient).toHaveBeenCalledWith('email');
+ expect(TemplateClient).toHaveBeenCalledWith('sub');
expect(updateTemplateMock).toHaveBeenCalledWith('1-2-3', { name: 'name' });
});
@@ -144,7 +144,7 @@ describe('Template API - Update', () => {
});
const event = mock({
- requestContext: { authorizer: { email: 'email' } },
+ requestContext: { authorizer: { user: 'sub' } },
body: JSON.stringify(update),
pathParameters: { templateId: '1-2-3' },
});
@@ -156,7 +156,7 @@ describe('Template API - Update', () => {
body: JSON.stringify({ statusCode: 200, template: response }),
});
- expect(TemplateClient).toHaveBeenCalledWith('email');
+ expect(TemplateClient).toHaveBeenCalledWith('sub');
expect(updateTemplateMock).toHaveBeenCalledWith('1-2-3', update);
});
diff --git a/lambdas/backend-api/src/templates/api/create.ts b/lambdas/backend-api/src/templates/api/create.ts
index bb47fbc3..178eebc2 100644
--- a/lambdas/backend-api/src/templates/api/create.ts
+++ b/lambdas/backend-api/src/templates/api/create.ts
@@ -3,15 +3,15 @@ import { TemplateClient } from '@backend-api/templates/app/template-client';
import { apiFailure, apiSuccess } from './responses';
export const handler: APIGatewayProxyHandler = async (event) => {
- const email = event.requestContext.authorizer?.email;
+ const user = event.requestContext.authorizer?.user;
const dto = JSON.parse(event.body || '{}');
- if (!email) {
+ if (!user) {
return apiFailure(400, 'Invalid request');
}
- const client = new TemplateClient(email);
+ const client = new TemplateClient(user);
const { data, error } = await client.createTemplate(dto);
diff --git a/lambdas/backend-api/src/templates/api/get.ts b/lambdas/backend-api/src/templates/api/get.ts
index 4c67e568..6b76f857 100644
--- a/lambdas/backend-api/src/templates/api/get.ts
+++ b/lambdas/backend-api/src/templates/api/get.ts
@@ -3,15 +3,15 @@ import { TemplateClient } from '@backend-api/templates/app/template-client';
import { apiFailure, apiSuccess } from './responses';
export const handler: APIGatewayProxyHandler = async (event) => {
- const email = event.requestContext.authorizer?.email;
+ const user = event.requestContext.authorizer?.user;
const templateId = event.pathParameters?.templateId;
- if (!email || !templateId) {
+ if (!user || !templateId) {
return apiFailure(400, 'Invalid request');
}
- const client = new TemplateClient(email);
+ const client = new TemplateClient(user);
const { data, error } = await client.getTemplate(templateId);
diff --git a/lambdas/backend-api/src/templates/api/list.ts b/lambdas/backend-api/src/templates/api/list.ts
index 0a5f2298..18b8bec0 100644
--- a/lambdas/backend-api/src/templates/api/list.ts
+++ b/lambdas/backend-api/src/templates/api/list.ts
@@ -3,13 +3,13 @@ import { TemplateClient } from '@backend-api/templates/app/template-client';
import { apiFailure, apiSuccess } from './responses';
export const handler: APIGatewayProxyHandler = async (event) => {
- const email = event.requestContext.authorizer?.email;
+ const user = event.requestContext.authorizer?.user;
- if (!email) {
+ if (!user) {
return apiFailure(400, 'Invalid request');
}
- const client = new TemplateClient(email);
+ const client = new TemplateClient(user);
const { data, error } = await client.listTemplates();
diff --git a/lambdas/backend-api/src/templates/api/update.ts b/lambdas/backend-api/src/templates/api/update.ts
index 58134167..90959d76 100644
--- a/lambdas/backend-api/src/templates/api/update.ts
+++ b/lambdas/backend-api/src/templates/api/update.ts
@@ -3,17 +3,17 @@ import { TemplateClient } from '@backend-api/templates/app/template-client';
import { apiFailure, apiSuccess } from './responses';
export const handler: APIGatewayProxyHandler = async (event) => {
- const email = event.requestContext.authorizer?.email;
+ const user = event.requestContext.authorizer?.user;
const templateId = event.pathParameters?.templateId;
const dto = JSON.parse(event.body || '{}');
- if (!email || !templateId) {
+ if (!user || !templateId) {
return apiFailure(400, 'Invalid request');
}
- const client = new TemplateClient(email);
+ const client = new TemplateClient(user);
const { data, error } = await client.updateTemplate(templateId, dto);
diff --git a/lambdas/backend-api/src/templates/domain/template/template-repository.ts b/lambdas/backend-api/src/templates/domain/template/template-repository.ts
index 740e3df0..52d7a0dd 100644
--- a/lambdas/backend-api/src/templates/domain/template/template-repository.ts
+++ b/lambdas/backend-api/src/templates/domain/template/template-repository.ts
@@ -128,9 +128,14 @@ const update = async (
}
if (template.templateStatus === TemplateStatus.DELETED) {
+ updateExpression.push('#ttl = :ttl');
+ expressionAttributeNames = {
+ ...expressionAttributeNames,
+ '#ttl': 'ttl',
+ };
expressionAttributeValues = {
...expressionAttributeValues,
- ttl: calculateTTL(),
+ ':ttl': calculateTTL(),
};
}
@@ -203,13 +208,12 @@ const list = async (
KeyConditionExpression: '#owner = :owner',
ExpressionAttributeNames: {
'#owner': 'owner',
- '#status': 'status',
},
ExpressionAttributeValues: {
':owner': owner,
':deletedStatus': TemplateStatus.DELETED,
},
- FilterExpression: '#status <> :deletedStatus',
+ FilterExpression: 'templateStatus <> :deletedStatus',
};
const items: DatabaseTemplate[] = [];
diff --git a/lambdas/backend-client/.eslintignore b/lambdas/backend-client/.eslintignore
index 3b79a19b..07ad00a5 100644
--- a/lambdas/backend-client/.eslintignore
+++ b/lambdas/backend-client/.eslintignore
@@ -1 +1,2 @@
src/types/generated
+dist
diff --git a/lambdas/backend-client/src/types/generated/models/TemplateType.ts b/lambdas/backend-client/src/types/generated/models/TemplateType.ts
index d6010346..45738b15 100644
--- a/lambdas/backend-client/src/types/generated/models/TemplateType.ts
+++ b/lambdas/backend-client/src/types/generated/models/TemplateType.ts
@@ -4,6 +4,6 @@
/* eslint-disable */
export enum TemplateType {
NHS_APP = 'NHS_APP',
- SMS = 'SMS',
EMAIL = 'EMAIL',
+ SMS = 'SMS',
}
diff --git a/package-lock.json b/package-lock.json
index 8b46352f..2b298a9d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -38,7 +38,8 @@
"jest-html-reporter": "^3.10.2",
"jest-mock-extended": "^3.0.7",
"lcov-result-merger": "^5.0.1",
- "ts-node": "^10.9.2"
+ "ts-node": "^10.9.2",
+ "tsx": "^4.19.2"
}
},
"frontend": {
@@ -54,6 +55,7 @@
"markdown-it": "^13.0.1",
"mimetext": "^3.0.24",
"next": "14.2.13",
+ "nhs-notify-backend-client": "*",
"nhs-notify-web-template-management-amplify": "*",
"nhs-notify-web-template-management-utils": "*",
"nhsuk-frontend": "^8.3.0",
@@ -27081,6 +27083,13 @@
"is-property": "^1.0.2"
}
},
+ "node_modules/generate-password": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/generate-password/-/generate-password-1.7.1.tgz",
+ "integrity": "sha512-9bVYY+16m7W7GczRBDqXE+VVuCX+bWNrfYKC/2p2JkZukFb2sKxT6E3zZ3mJGz7GMe5iRK0A/WawSL3jQfJuNQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -36076,7 +36085,6 @@
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz",
"integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==",
- "license": "MIT",
"dependencies": {
"esbuild": "~0.23.0",
"get-tsconfig": "^4.7.5"
@@ -37357,16 +37365,575 @@
"version": "0.0.1",
"devDependencies": {
"@aws-sdk/client-appsync": "^3.650.0",
+ "@aws-sdk/client-cognito-identity-provider": "^3.650.0",
"@aws-sdk/client-dynamodb": "^3.654.0",
"@aws-sdk/lib-dynamodb": "^3.654.0",
"@playwright/test": "^1.45.1",
- "aws-amplify": "^6.6.0"
+ "aws-amplify": "^6.6.0",
+ "generate-password": "^1.7.1"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/client-cognito-identity-provider": {
+ "version": "3.650.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity-provider/-/client-cognito-identity-provider-3.650.0.tgz",
+ "integrity": "sha512-uX8XJVNIHCT0M3PpYM+cJJzhWMVQvUwh294x+WVpn22R/Bg5x1XLo9QVtWiVPhLcQCLN/kYzsz9n3uE5b7YozQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/client-sso-oidc": "3.650.0",
+ "@aws-sdk/client-sts": "3.650.0",
+ "@aws-sdk/core": "3.649.0",
+ "@aws-sdk/credential-provider-node": "3.650.0",
+ "@aws-sdk/middleware-host-header": "3.649.0",
+ "@aws-sdk/middleware-logger": "3.649.0",
+ "@aws-sdk/middleware-recursion-detection": "3.649.0",
+ "@aws-sdk/middleware-user-agent": "3.649.0",
+ "@aws-sdk/region-config-resolver": "3.649.0",
+ "@aws-sdk/types": "3.649.0",
+ "@aws-sdk/util-endpoints": "3.649.0",
+ "@aws-sdk/util-user-agent-browser": "3.649.0",
+ "@aws-sdk/util-user-agent-node": "3.649.0",
+ "@smithy/config-resolver": "^3.0.6",
+ "@smithy/core": "^2.4.1",
+ "@smithy/fetch-http-handler": "^3.2.5",
+ "@smithy/hash-node": "^3.0.4",
+ "@smithy/invalid-dependency": "^3.0.4",
+ "@smithy/middleware-content-length": "^3.0.6",
+ "@smithy/middleware-endpoint": "^3.1.1",
+ "@smithy/middleware-retry": "^3.0.16",
+ "@smithy/middleware-serde": "^3.0.4",
+ "@smithy/middleware-stack": "^3.0.4",
+ "@smithy/node-config-provider": "^3.1.5",
+ "@smithy/node-http-handler": "^3.2.0",
+ "@smithy/protocol-http": "^4.1.1",
+ "@smithy/smithy-client": "^3.3.0",
+ "@smithy/types": "^3.4.0",
+ "@smithy/url-parser": "^3.0.4",
+ "@smithy/util-base64": "^3.0.0",
+ "@smithy/util-body-length-browser": "^3.0.0",
+ "@smithy/util-body-length-node": "^3.0.0",
+ "@smithy/util-defaults-mode-browser": "^3.0.16",
+ "@smithy/util-defaults-mode-node": "^3.0.16",
+ "@smithy/util-endpoints": "^2.1.0",
+ "@smithy/util-middleware": "^3.0.4",
+ "@smithy/util-retry": "^3.0.4",
+ "@smithy/util-utf8": "^3.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/client-sso": {
+ "version": "3.650.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.650.0.tgz",
+ "integrity": "sha512-YKm14gCMChD/jlCisFlsVqB8HJujR41bl4Fup2crHwNJxhD/9LTnzwMiVVlBqlXr41Sfa6fSxczX2AMP8NM14A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.649.0",
+ "@aws-sdk/middleware-host-header": "3.649.0",
+ "@aws-sdk/middleware-logger": "3.649.0",
+ "@aws-sdk/middleware-recursion-detection": "3.649.0",
+ "@aws-sdk/middleware-user-agent": "3.649.0",
+ "@aws-sdk/region-config-resolver": "3.649.0",
+ "@aws-sdk/types": "3.649.0",
+ "@aws-sdk/util-endpoints": "3.649.0",
+ "@aws-sdk/util-user-agent-browser": "3.649.0",
+ "@aws-sdk/util-user-agent-node": "3.649.0",
+ "@smithy/config-resolver": "^3.0.6",
+ "@smithy/core": "^2.4.1",
+ "@smithy/fetch-http-handler": "^3.2.5",
+ "@smithy/hash-node": "^3.0.4",
+ "@smithy/invalid-dependency": "^3.0.4",
+ "@smithy/middleware-content-length": "^3.0.6",
+ "@smithy/middleware-endpoint": "^3.1.1",
+ "@smithy/middleware-retry": "^3.0.16",
+ "@smithy/middleware-serde": "^3.0.4",
+ "@smithy/middleware-stack": "^3.0.4",
+ "@smithy/node-config-provider": "^3.1.5",
+ "@smithy/node-http-handler": "^3.2.0",
+ "@smithy/protocol-http": "^4.1.1",
+ "@smithy/smithy-client": "^3.3.0",
+ "@smithy/types": "^3.4.0",
+ "@smithy/url-parser": "^3.0.4",
+ "@smithy/util-base64": "^3.0.0",
+ "@smithy/util-body-length-browser": "^3.0.0",
+ "@smithy/util-body-length-node": "^3.0.0",
+ "@smithy/util-defaults-mode-browser": "^3.0.16",
+ "@smithy/util-defaults-mode-node": "^3.0.16",
+ "@smithy/util-endpoints": "^2.1.0",
+ "@smithy/util-middleware": "^3.0.4",
+ "@smithy/util-retry": "^3.0.4",
+ "@smithy/util-utf8": "^3.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/client-sso-oidc": {
+ "version": "3.650.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.650.0.tgz",
+ "integrity": "sha512-6J7IS0f8ovhvbIAZaynOYP+jPX8344UlTjwHxjaXHgFvI8axu3+NslKtEEV5oHLhgzDvrKbinsu5lgE2n4Sqng==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.649.0",
+ "@aws-sdk/credential-provider-node": "3.650.0",
+ "@aws-sdk/middleware-host-header": "3.649.0",
+ "@aws-sdk/middleware-logger": "3.649.0",
+ "@aws-sdk/middleware-recursion-detection": "3.649.0",
+ "@aws-sdk/middleware-user-agent": "3.649.0",
+ "@aws-sdk/region-config-resolver": "3.649.0",
+ "@aws-sdk/types": "3.649.0",
+ "@aws-sdk/util-endpoints": "3.649.0",
+ "@aws-sdk/util-user-agent-browser": "3.649.0",
+ "@aws-sdk/util-user-agent-node": "3.649.0",
+ "@smithy/config-resolver": "^3.0.6",
+ "@smithy/core": "^2.4.1",
+ "@smithy/fetch-http-handler": "^3.2.5",
+ "@smithy/hash-node": "^3.0.4",
+ "@smithy/invalid-dependency": "^3.0.4",
+ "@smithy/middleware-content-length": "^3.0.6",
+ "@smithy/middleware-endpoint": "^3.1.1",
+ "@smithy/middleware-retry": "^3.0.16",
+ "@smithy/middleware-serde": "^3.0.4",
+ "@smithy/middleware-stack": "^3.0.4",
+ "@smithy/node-config-provider": "^3.1.5",
+ "@smithy/node-http-handler": "^3.2.0",
+ "@smithy/protocol-http": "^4.1.1",
+ "@smithy/smithy-client": "^3.3.0",
+ "@smithy/types": "^3.4.0",
+ "@smithy/url-parser": "^3.0.4",
+ "@smithy/util-base64": "^3.0.0",
+ "@smithy/util-body-length-browser": "^3.0.0",
+ "@smithy/util-body-length-node": "^3.0.0",
+ "@smithy/util-defaults-mode-browser": "^3.0.16",
+ "@smithy/util-defaults-mode-node": "^3.0.16",
+ "@smithy/util-endpoints": "^2.1.0",
+ "@smithy/util-middleware": "^3.0.4",
+ "@smithy/util-retry": "^3.0.4",
+ "@smithy/util-utf8": "^3.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "peerDependencies": {
+ "@aws-sdk/client-sts": "^3.650.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/client-sts": {
+ "version": "3.650.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.650.0.tgz",
+ "integrity": "sha512-ISK0ZQYA7O5/WYgslpWy956lUBudGC9d7eL0FFbiL0j50N80Gx3RUv22ezvZgxJWE0W3DqNr4CE19sPYn4Lw8g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/client-sso-oidc": "3.650.0",
+ "@aws-sdk/core": "3.649.0",
+ "@aws-sdk/credential-provider-node": "3.650.0",
+ "@aws-sdk/middleware-host-header": "3.649.0",
+ "@aws-sdk/middleware-logger": "3.649.0",
+ "@aws-sdk/middleware-recursion-detection": "3.649.0",
+ "@aws-sdk/middleware-user-agent": "3.649.0",
+ "@aws-sdk/region-config-resolver": "3.649.0",
+ "@aws-sdk/types": "3.649.0",
+ "@aws-sdk/util-endpoints": "3.649.0",
+ "@aws-sdk/util-user-agent-browser": "3.649.0",
+ "@aws-sdk/util-user-agent-node": "3.649.0",
+ "@smithy/config-resolver": "^3.0.6",
+ "@smithy/core": "^2.4.1",
+ "@smithy/fetch-http-handler": "^3.2.5",
+ "@smithy/hash-node": "^3.0.4",
+ "@smithy/invalid-dependency": "^3.0.4",
+ "@smithy/middleware-content-length": "^3.0.6",
+ "@smithy/middleware-endpoint": "^3.1.1",
+ "@smithy/middleware-retry": "^3.0.16",
+ "@smithy/middleware-serde": "^3.0.4",
+ "@smithy/middleware-stack": "^3.0.4",
+ "@smithy/node-config-provider": "^3.1.5",
+ "@smithy/node-http-handler": "^3.2.0",
+ "@smithy/protocol-http": "^4.1.1",
+ "@smithy/smithy-client": "^3.3.0",
+ "@smithy/types": "^3.4.0",
+ "@smithy/url-parser": "^3.0.4",
+ "@smithy/util-base64": "^3.0.0",
+ "@smithy/util-body-length-browser": "^3.0.0",
+ "@smithy/util-body-length-node": "^3.0.0",
+ "@smithy/util-defaults-mode-browser": "^3.0.16",
+ "@smithy/util-defaults-mode-node": "^3.0.16",
+ "@smithy/util-endpoints": "^2.1.0",
+ "@smithy/util-middleware": "^3.0.4",
+ "@smithy/util-retry": "^3.0.4",
+ "@smithy/util-utf8": "^3.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/core": {
+ "version": "3.649.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.649.0.tgz",
+ "integrity": "sha512-dheG/X2y25RHE7K+TlS32kcy7TgDg1OpWV44BQRoE0OBPAWmFR1D1qjjTZ7WWrdqRPKzcnDj1qED8ncyncOX8g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^2.4.1",
+ "@smithy/node-config-provider": "^3.1.5",
+ "@smithy/property-provider": "^3.1.4",
+ "@smithy/protocol-http": "^4.1.1",
+ "@smithy/signature-v4": "^4.1.1",
+ "@smithy/smithy-client": "^3.3.0",
+ "@smithy/types": "^3.4.0",
+ "@smithy/util-middleware": "^3.0.4",
+ "fast-xml-parser": "4.4.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/credential-provider-env": {
+ "version": "3.649.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.649.0.tgz",
+ "integrity": "sha512-tViwzM1dauksA3fdRjsg0T8mcHklDa8EfveyiQKK6pUJopkqV6FQx+X5QNda0t/LrdEVlFZvwHNdXqOEfc83TA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.649.0",
+ "@smithy/property-provider": "^3.1.4",
+ "@smithy/types": "^3.4.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/credential-provider-http": {
+ "version": "3.649.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.649.0.tgz",
+ "integrity": "sha512-ODAJ+AJJq6ozbns6ejGbicpsQ0dyMOpnGlg0J9J0jITQ05DKQZ581hdB8APDOZ9N8FstShP6dLZflSj8jb5fNA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.649.0",
+ "@smithy/fetch-http-handler": "^3.2.5",
+ "@smithy/node-http-handler": "^3.2.0",
+ "@smithy/property-provider": "^3.1.4",
+ "@smithy/protocol-http": "^4.1.1",
+ "@smithy/smithy-client": "^3.3.0",
+ "@smithy/types": "^3.4.0",
+ "@smithy/util-stream": "^3.1.4",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/credential-provider-ini": {
+ "version": "3.650.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.650.0.tgz",
+ "integrity": "sha512-x2M9buZxIsKuUbuDgkGHhAKYBpn0/rYdKlwuFuOhXyyAcnhvPj0lgNF2KE4ld/GF1mKr7FF/uV3G9lM6PFaYmA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/credential-provider-env": "3.649.0",
+ "@aws-sdk/credential-provider-http": "3.649.0",
+ "@aws-sdk/credential-provider-process": "3.649.0",
+ "@aws-sdk/credential-provider-sso": "3.650.0",
+ "@aws-sdk/credential-provider-web-identity": "3.649.0",
+ "@aws-sdk/types": "3.649.0",
+ "@smithy/credential-provider-imds": "^3.2.1",
+ "@smithy/property-provider": "^3.1.4",
+ "@smithy/shared-ini-file-loader": "^3.1.5",
+ "@smithy/types": "^3.4.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "peerDependencies": {
+ "@aws-sdk/client-sts": "^3.650.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/credential-provider-node": {
+ "version": "3.650.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.650.0.tgz",
+ "integrity": "sha512-uBra5YjzS/gWSekAogfqJfY6c+oKQkkou7Cjc4d/cpMNvQtF1IBdekJ7NaE1RfsDEz3uH1+Myd07YWZAJo/2Qw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/credential-provider-env": "3.649.0",
+ "@aws-sdk/credential-provider-http": "3.649.0",
+ "@aws-sdk/credential-provider-ini": "3.650.0",
+ "@aws-sdk/credential-provider-process": "3.649.0",
+ "@aws-sdk/credential-provider-sso": "3.650.0",
+ "@aws-sdk/credential-provider-web-identity": "3.649.0",
+ "@aws-sdk/types": "3.649.0",
+ "@smithy/credential-provider-imds": "^3.2.1",
+ "@smithy/property-provider": "^3.1.4",
+ "@smithy/shared-ini-file-loader": "^3.1.5",
+ "@smithy/types": "^3.4.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/credential-provider-process": {
+ "version": "3.649.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.649.0.tgz",
+ "integrity": "sha512-6VYPQpEVpU+6DDS/gLoI40ppuNM5RPIEprK30qZZxnhTr5wyrGOeJ7J7wbbwPOZ5dKwta290BiJDU2ipV8Y9BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.649.0",
+ "@smithy/property-provider": "^3.1.4",
+ "@smithy/shared-ini-file-loader": "^3.1.5",
+ "@smithy/types": "^3.4.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/credential-provider-sso": {
+ "version": "3.650.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.650.0.tgz",
+ "integrity": "sha512-069nkhcwximbvyGiAC6Fr2G+yrG/p1S3NQ5BZ2cMzB1hgUKo6TvgFK7nriYI4ljMQ+UWxqPwIdTqiUmn2iJmhg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/client-sso": "3.650.0",
+ "@aws-sdk/token-providers": "3.649.0",
+ "@aws-sdk/types": "3.649.0",
+ "@smithy/property-provider": "^3.1.4",
+ "@smithy/shared-ini-file-loader": "^3.1.5",
+ "@smithy/types": "^3.4.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/credential-provider-web-identity": {
+ "version": "3.649.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.649.0.tgz",
+ "integrity": "sha512-XVk3WsDa0g3kQFPmnCH/LaCtGY/0R2NDv7gscYZSXiBZcG/fixasglTprgWSp8zcA0t7tEIGu9suyjz8ZwhymQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.649.0",
+ "@smithy/property-provider": "^3.1.4",
+ "@smithy/types": "^3.4.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "peerDependencies": {
+ "@aws-sdk/client-sts": "^3.649.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/middleware-host-header": {
+ "version": "3.649.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.649.0.tgz",
+ "integrity": "sha512-PjAe2FocbicHVgNNwdSZ05upxIO7AgTPFtQLpnIAmoyzMcgv/zNB5fBn3uAnQSAeEPPCD+4SYVEUD1hw1ZBvEg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.649.0",
+ "@smithy/protocol-http": "^4.1.1",
+ "@smithy/types": "^3.4.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/middleware-logger": {
+ "version": "3.649.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.649.0.tgz",
+ "integrity": "sha512-qdqRx6q7lYC6KL/NT9x3ShTL0TBuxdkCczGzHzY3AnOoYUjnCDH7Vlq867O6MAvb4EnGNECFzIgtkZkQ4FhY5w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.649.0",
+ "@smithy/types": "^3.4.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/middleware-recursion-detection": {
+ "version": "3.649.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.649.0.tgz",
+ "integrity": "sha512-IPnO4wlmaLRf6IYmJW2i8gJ2+UPXX0hDRv1it7Qf8DpBW+lGyF2rnoN7NrFX0WIxdGOlJF1RcOr/HjXb2QeXfQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.649.0",
+ "@smithy/protocol-http": "^4.1.1",
+ "@smithy/types": "^3.4.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/middleware-user-agent": {
+ "version": "3.649.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.649.0.tgz",
+ "integrity": "sha512-q6sO10dnCXoxe9thobMJxekhJumzd1j6dxcE1+qJdYKHJr6yYgWbogJqrLCpWd30w0lEvnuAHK8lN2kWLdJxJw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.649.0",
+ "@aws-sdk/util-endpoints": "3.649.0",
+ "@smithy/protocol-http": "^4.1.1",
+ "@smithy/types": "^3.4.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/region-config-resolver": {
+ "version": "3.649.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.649.0.tgz",
+ "integrity": "sha512-xURBvdQXvRvca5Du8IlC5FyCj3pkw8Z75+373J3Wb+vyg8GjD14HfKk1Je1HCCQDyIE9VB/scYDcm9ri0ppePw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.649.0",
+ "@smithy/node-config-provider": "^3.1.5",
+ "@smithy/types": "^3.4.0",
+ "@smithy/util-config-provider": "^3.0.0",
+ "@smithy/util-middleware": "^3.0.4",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/token-providers": {
+ "version": "3.649.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.649.0.tgz",
+ "integrity": "sha512-ZBqr+JuXI9RiN+4DSZykMx5gxpL8Dr3exIfFhxMiwAP3DQojwl0ub8ONjMuAjq9OvmX6n+jHZL6fBnNgnNFC8w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.649.0",
+ "@smithy/property-provider": "^3.1.4",
+ "@smithy/shared-ini-file-loader": "^3.1.5",
+ "@smithy/types": "^3.4.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "peerDependencies": {
+ "@aws-sdk/client-sso-oidc": "^3.649.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/types": {
+ "version": "3.649.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.649.0.tgz",
+ "integrity": "sha512-PuPw8RysbhJNlaD2d/PzOTf8sbf4Dsn2b7hwyGh7YVG3S75yTpxSAZxrnhKsz9fStgqFmnw/jUfV/G+uQAeTVw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^3.4.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.649.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.649.0.tgz",
+ "integrity": "sha512-bZI1Wc3R/KibdDVWFxX/N4AoJFG4VJ92Dp4WYmOrVD6VPkb8jPz7ZeiYc7YwPl8NoDjYyPneBV0lEoK/V8OKAA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.649.0",
+ "@smithy/types": "^3.4.0",
+ "@smithy/util-endpoints": "^2.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/util-user-agent-browser": {
+ "version": "3.649.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.649.0.tgz",
+ "integrity": "sha512-IY43r256LhKAvdEVQO/FPdUyVpcZS5EVxh/WHVdNzuN1bNLoUK2rIzuZqVA0EGguvCxoXVmQv9m50GvG7cGktg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.649.0",
+ "@smithy/types": "^3.4.0",
+ "bowser": "^2.11.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "tests/test-team/node_modules/@aws-sdk/util-user-agent-node": {
+ "version": "3.649.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.649.0.tgz",
+ "integrity": "sha512-x5DiLpZDG/AJmCIBnE3Xhpwy35QIo3WqNiOpw6ExVs1NydbM/e90zFPSfhME0FM66D/WorigvluBxxwjxDm/GA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.649.0",
+ "@smithy/node-config-provider": "^3.1.5",
+ "@smithy/types": "^3.4.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "peerDependencies": {
+ "aws-crt": ">=1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "aws-crt": {
+ "optional": true
+ }
+ }
+ },
+ "tests/test-team/node_modules/@smithy/fetch-http-handler": {
+ "version": "3.2.9",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.9.tgz",
+ "integrity": "sha512-hYNVQOqhFQ6vOpenifFME546f0GfJn2OiQ3M0FDmuUu8V/Uiwy2wej7ZXxFBNqdx0R5DZAqWM1l6VRhGz8oE6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/protocol-http": "^4.1.4",
+ "@smithy/querystring-builder": "^3.0.7",
+ "@smithy/types": "^3.5.0",
+ "@smithy/util-base64": "^3.0.0",
+ "tslib": "^2.6.2"
}
},
"utils": {
"name": "nhs-notify-web-template-management-utils",
"version": "0.0.1",
"dependencies": {
+ "nhs-notify-backend-client": "*",
"winston": "^3.14.2",
"zod": "^3.23.8"
},
diff --git a/package.json b/package.json
index cd6c2acd..56308dcd 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"scripts": {
"build": "npm run build --workspace frontend --if-present",
"copy-files-for-testing": "mkdir -p frontend/public/testing && cp ./lambdas/backend-api/src/email/email-template.html ./frontend/public/testing/email-template.html",
+ "create-amplify-outputs": "node ./scripts/create-amplify-outputs.js",
"create-backend-sandbox": "./scripts/create_backend_sandbox.sh",
"destroy-backend-sandbox": "./scripts/destroy_backend_sandbox.sh",
"start": "npm run start --workspace frontend",
@@ -19,7 +20,9 @@
"test:unit": "npm run test:unit --workspaces",
"typecheck": "npm run typecheck --workspaces",
"lint": "npm run lint --workspaces",
- "dev": "npm run dev --workspace frontend --if-present"
+ "dev": "npm run dev --workspace frontend --if-present",
+ "create-test-user": "tsx ./tests/accessibility/create-test-user.ts",
+ "delete-test-user": "tsx ./tests/accessibility/delete-test-user.ts"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.4",
@@ -45,6 +48,7 @@
"jest-html-reporter": "^3.10.2",
"jest-mock-extended": "^3.0.7",
"lcov-result-merger": "^5.0.1",
- "ts-node": "^10.9.2"
+ "ts-node": "^10.9.2",
+ "tsx": "^4.19.2"
}
}
diff --git a/scripts/create-amplify-outputs.js b/scripts/create-amplify-outputs.js
new file mode 100644
index 00000000..47dcf2fa
--- /dev/null
+++ b/scripts/create-amplify-outputs.js
@@ -0,0 +1,51 @@
+const { writeFileSync, readFileSync } = require('node:fs');
+
+const inputType = process.argv[2];
+
+let userPoolId;
+let userPoolClientId;
+let dynamoTableName;
+let backendApiUrl;
+
+if (inputType === 'file') {
+ const outputsFileContent = JSON.parse(
+ readFileSync('./sandbox_tf_outputs.json').toString()
+ );
+
+ userPoolId = outputsFileContent.cognito_user_pool_id.value;
+
+ userPoolClientId = outputsFileContent.cognito_user_pool_client_id.value;
+
+ dynamoTableName = outputsFileContent.dynamodb_table_templates.value;
+
+ backendApiUrl = outputsFileContent.api_base_url.value;
+
+} else if (inputType === 'env') {
+ userPoolId = process.env.USER_POOL_ID ?? 'unknown-user-pool-id';
+
+ userPoolClientId =
+ process.env.USER_POOL_CLIENT_ID ?? 'unknown-user-pool-client-id';
+
+ dynamoTableName = process.env.DYNAMO_TABLE_NAME ?? 'unknown-dynamo-table-name';
+
+ backendApiUrl =
+ process.env.BACKEND_API_URL ?? 'unknown-backend-api-url';
+} else {
+ throw new Error('Unexpected input type');
+}
+
+writeFileSync(
+ './frontend/amplify_outputs.json',
+ JSON.stringify({
+ version: '1.3',
+ auth: {
+ aws_region: 'eu-west-2',
+ user_pool_id: userPoolId,
+ user_pool_client_id: userPoolClientId,
+ },
+ meta: {
+ dynamo_table_name: dynamoTableName,
+ backend_api_url: backendApiUrl,
+ }
+ }, null, 2)
+);
diff --git a/scripts/create_backend_sandbox.sh b/scripts/create_backend_sandbox.sh
index 189248f1..5f8b7879 100755
--- a/scripts/create_backend_sandbox.sh
+++ b/scripts/create_backend_sandbox.sh
@@ -68,3 +68,5 @@ trap "rm -f $(pwd)/backend_tfscaffold.tf" EXIT;
# create the outputs file
terraform output -json > ${root_dir}/sandbox_tf_outputs.json
+
+npm run create-amplify-outputs file
diff --git a/scripts/tests/accessibility.sh b/scripts/tests/accessibility.sh
index c146a7f1..45ea47c2 100755
--- a/scripts/tests/accessibility.sh
+++ b/scripts/tests/accessibility.sh
@@ -2,6 +2,10 @@
set -euo pipefail
+npm run copy-files-for-testing
+
+npm run create-test-user
+
npm run build --prefix frontend
npm run app:start --prefix frontend
@@ -9,3 +13,5 @@ npm run app:start --prefix frontend
npm run app:wait --prefix frontend
npm run test:accessibility
+
+npm run delete-test-user
diff --git a/tests/accessibility/.pa11y-ci.js b/tests/accessibility/.pa11y-ci.js
index 4a566188..ad6d49bc 100644
--- a/tests/accessibility/.pa11y-ci.js
+++ b/tests/accessibility/.pa11y-ci.js
@@ -28,6 +28,7 @@ const {
viewSubmittedNHSAppTemplatePage,
viewSubmittedTextMessageTemplatePage,
copyTemplatePage,
+ signInPageActions,
} = require('./actions');
const baseUrl = 'http://localhost:3000/templates';
@@ -78,8 +79,8 @@ module.exports = {
performCheck(emailTemplateSubmittedPage(chooseTemplateUrl)),
performCheck(viewSubmittedEmailTemplatePage(manageTemplatesUrl)),
- performCheck({ url: `${baseUrl}/invalid-template`, name: 'invalid-template'}),
- performCheck({ url: `${baseUrl}/testing/email-template.html`, name: 'email-template'})
+ performCheck({ url: `${baseUrl}/invalid-template`, actions: [...signInPageActions, 'wait for h1 to be visible'], name: 'invalid-template'}),
+ performCheck({ url: `${baseUrl}/testing/email-template.html`, actions: [...signInPageActions, 'wait for table to be visible'], name: 'email-template'})
],
defaults: {
reporters: [
diff --git a/tests/accessibility/actions/choose-a-template-type.actions.js b/tests/accessibility/actions/choose-a-template-type.actions.js
index 1f1b4570..f0830b9f 100644
--- a/tests/accessibility/actions/choose-a-template-type.actions.js
+++ b/tests/accessibility/actions/choose-a-template-type.actions.js
@@ -1,12 +1,21 @@
+const { signInPageActions } = require('./sign-in-page.actions');
+
+const pageActions = [
+ ...signInPageActions,
+ 'wait for element #choose-a-template-type-submit-button to be visible',
+];
+
const chooseATemplatePage = (url) => ({
name: 'choose-a-template',
url,
+ actions: pageActions,
});
const chooseATemplatePageError = (url) => ({
name: 'choose-a-template-error',
url,
actions: [
+ ...pageActions,
'click element #choose-a-template-type-submit-button',
'wait for element .nhsuk-error-summary__title to be visible',
],
diff --git a/tests/accessibility/actions/create-email-template.actions.js b/tests/accessibility/actions/create-email-template.actions.js
index d0d4d9a1..910b8b85 100644
--- a/tests/accessibility/actions/create-email-template.actions.js
+++ b/tests/accessibility/actions/create-email-template.actions.js
@@ -1,4 +1,7 @@
+const { signInPageActions } = require('./sign-in-page.actions');
+
const pageActions = [
+ ...signInPageActions,
'wait for element #templateType-EMAIL to be visible',
'click element #templateType-EMAIL',
'click element #choose-a-template-type-submit-button',
diff --git a/tests/accessibility/actions/create-nhs-app-template.actions.js b/tests/accessibility/actions/create-nhs-app-template.actions.js
index 6a800834..704fe0e9 100644
--- a/tests/accessibility/actions/create-nhs-app-template.actions.js
+++ b/tests/accessibility/actions/create-nhs-app-template.actions.js
@@ -1,4 +1,7 @@
+const { signInPageActions } = require('./sign-in-page.actions');
+
const pageActions = [
+ ...signInPageActions,
'wait for element #templateType-NHS_APP to be visible',
'click element #templateType-NHS_APP',
'click element #choose-a-template-type-submit-button',
diff --git a/tests/accessibility/actions/create-text-message-template.actions.js b/tests/accessibility/actions/create-text-message-template.actions.js
index f0b4641c..8d93f936 100644
--- a/tests/accessibility/actions/create-text-message-template.actions.js
+++ b/tests/accessibility/actions/create-text-message-template.actions.js
@@ -1,4 +1,7 @@
+const { signInPageActions } = require('./sign-in-page.actions');
+
const pageActions = [
+ ...signInPageActions,
'wait for element #templateType-SMS to be visible',
'click element #templateType-SMS',
'click element #choose-a-template-type-submit-button',
diff --git a/tests/accessibility/actions/index.js b/tests/accessibility/actions/index.js
index f273f35c..1299bd9b 100644
--- a/tests/accessibility/actions/index.js
+++ b/tests/accessibility/actions/index.js
@@ -20,4 +20,5 @@ module.exports = {
...require('./view-submitted-nhs-app-template.actions'),
...require('./view-submitted-text-message-template.actions'),
...require('./copy-template.actions'),
+ ...require('./sign-in-page.actions'),
};
diff --git a/tests/accessibility/actions/manage-templates.actions.js b/tests/accessibility/actions/manage-templates.actions.js
index 3656c6ce..a781aa61 100644
--- a/tests/accessibility/actions/manage-templates.actions.js
+++ b/tests/accessibility/actions/manage-templates.actions.js
@@ -1,6 +1,8 @@
+const { signInPageActions } = require('./sign-in-page.actions');
+
const pageActions = [
+ ...signInPageActions,
'wait for element #create-template-button to be visible',
- 'click element #create-template-button',
];
const manageTemplatesPage = (url) => ({
diff --git a/tests/accessibility/actions/preview-nhs-app-template.actions.js b/tests/accessibility/actions/preview-nhs-app-template.actions.js
index 8f6b1ee7..ec9f2874 100644
--- a/tests/accessibility/actions/preview-nhs-app-template.actions.js
+++ b/tests/accessibility/actions/preview-nhs-app-template.actions.js
@@ -7,6 +7,7 @@ const pageActions = [
'set field #nhsAppTemplateName to example-template-1',
'set field #nhsAppTemplateMessage to example template message',
'click element #create-nhs-app-template-submit-button',
+ 'wait for #preview-nhs-app-template-submit-button to be visible',
];
const reviewNHSAppTemplatePage = (url) => ({
@@ -20,7 +21,6 @@ const reviewNHSAppTemplateErrorPage = (url) => ({
url,
actions: [
...pageActions,
- 'wait for #preview-nhs-app-template-submit-button to be visible',
'click element #preview-nhs-app-template-submit-button',
'wait for element .nhsuk-error-summary__title to be visible',
],
diff --git a/tests/accessibility/actions/sign-in-page.actions.js b/tests/accessibility/actions/sign-in-page.actions.js
new file mode 100644
index 00000000..44f8eb6c
--- /dev/null
+++ b/tests/accessibility/actions/sign-in-page.actions.js
@@ -0,0 +1,14 @@
+const { readFileSync } = require('node:fs');
+
+const { email, password } = JSON.parse(readFileSync('./auth.json', 'utf8'));
+
+const signInPageActions = [
+ 'wait for element button[type="submit"] to be visible',
+ `set field input[type="text"] to ${email}`,
+ `set field input[type="password"] to ${password}`,
+ 'click element button[type="submit"]',
+];
+
+module.exports = {
+ signInPageActions,
+};
diff --git a/tests/accessibility/actions/view-not-yet-submitted-email-template.actions.js b/tests/accessibility/actions/view-not-yet-submitted-email-template.actions.js
index 2cfca98e..19310941 100644
--- a/tests/accessibility/actions/view-not-yet-submitted-email-template.actions.js
+++ b/tests/accessibility/actions/view-not-yet-submitted-email-template.actions.js
@@ -1,4 +1,7 @@
+const { signInPageActions } = require('./sign-in-page.actions');
+
const pageActions = [
+ ...signInPageActions,
'wait for element a[href*="preview-email-template"] to be visible',
'click element a[href*="preview-email-template"]',
'wait for element #preview-heading-message to be visible',
diff --git a/tests/accessibility/actions/view-not-yet-submitted-nhs-app-template.actions.js b/tests/accessibility/actions/view-not-yet-submitted-nhs-app-template.actions.js
index 646409d6..6b8575fd 100644
--- a/tests/accessibility/actions/view-not-yet-submitted-nhs-app-template.actions.js
+++ b/tests/accessibility/actions/view-not-yet-submitted-nhs-app-template.actions.js
@@ -1,4 +1,7 @@
+const { signInPageActions } = require('./sign-in-page.actions');
+
const pageActions = [
+ ...signInPageActions,
'wait for element a[href*="preview-nhs-app-template"] to be visible',
'click element a[href*="preview-nhs-app-template"]',
'wait for element #preview-heading-message to be visible',
diff --git a/tests/accessibility/actions/view-not-yet-submitted-text-message-template.actions.js b/tests/accessibility/actions/view-not-yet-submitted-text-message-template.actions.js
index 996ea8b7..f8d75f84 100644
--- a/tests/accessibility/actions/view-not-yet-submitted-text-message-template.actions.js
+++ b/tests/accessibility/actions/view-not-yet-submitted-text-message-template.actions.js
@@ -1,4 +1,7 @@
+const { signInPageActions } = require('./sign-in-page.actions');
+
const pageActions = [
+ ...signInPageActions,
'wait for element a[href*="preview-text-message-template"] to be visible',
'click element a[href*="preview-text-message-template"]',
'wait for element #preview-heading-message to be visible',
diff --git a/tests/accessibility/actions/view-submitted-email-template.actions.js b/tests/accessibility/actions/view-submitted-email-template.actions.js
index 47ac3353..f4379060 100644
--- a/tests/accessibility/actions/view-submitted-email-template.actions.js
+++ b/tests/accessibility/actions/view-submitted-email-template.actions.js
@@ -1,4 +1,7 @@
+const { signInPageActions } = require('./sign-in-page.actions');
+
const pageActions = [
+ ...signInPageActions,
'wait for element a[href*="view-submitted-email-template"] to be visible',
'click element a[href*="view-submitted-email-template"]',
'wait for element #preview-heading-message to be visible',
diff --git a/tests/accessibility/actions/view-submitted-nhs-app-template.actions.js b/tests/accessibility/actions/view-submitted-nhs-app-template.actions.js
index 85f59a0a..a3a92a24 100644
--- a/tests/accessibility/actions/view-submitted-nhs-app-template.actions.js
+++ b/tests/accessibility/actions/view-submitted-nhs-app-template.actions.js
@@ -1,4 +1,7 @@
+const { signInPageActions } = require('./sign-in-page.actions');
+
const pageActions = [
+ ...signInPageActions,
'wait for element a[href*="view-submitted-nhs-app-template"] to be visible',
'click element a[href*="view-submitted-nhs-app-template"]',
'wait for element #preview-heading-message to be visible',
diff --git a/tests/accessibility/actions/view-submitted-text-message-template.actions.js b/tests/accessibility/actions/view-submitted-text-message-template.actions.js
index c3540635..7e435d77 100644
--- a/tests/accessibility/actions/view-submitted-text-message-template.actions.js
+++ b/tests/accessibility/actions/view-submitted-text-message-template.actions.js
@@ -1,4 +1,7 @@
+const { signInPageActions } = require('./sign-in-page.actions');
+
const pageActions = [
+ ...signInPageActions,
'wait for element a[href*="view-submitted-text-message-template"] to be visible',
'click element a[href*="view-submitted-text-message-template"]',
'wait for element #preview-heading-message to be visible',
diff --git a/tests/accessibility/create-test-user.ts b/tests/accessibility/create-test-user.ts
new file mode 100644
index 00000000..c6cd48a6
--- /dev/null
+++ b/tests/accessibility/create-test-user.ts
@@ -0,0 +1,25 @@
+import { writeFileSync } from 'node:fs';
+import { randomUUID } from 'node:crypto';
+import { TestUserClient } from './test-user-client';
+import { generate } from 'generate-password';
+
+const generateTestUser = async () => {
+ const testEmail = `nhs-notify-automated-test-accessibility-test-${randomUUID()}@nhs.net`;
+ const testPassword = generate({
+ length: 20,
+ lowercase: true,
+ uppercase: true,
+ numbers: true,
+ symbols: true,
+ strict: true,
+ });
+ const testUserClient = new TestUserClient('./frontend');
+ await testUserClient.createTestUser(testEmail, testPassword);
+
+ writeFileSync(
+ './auth.json',
+ JSON.stringify({ email: testEmail, password: testPassword })
+ );
+};
+
+generateTestUser();
diff --git a/tests/accessibility/delete-test-user.ts b/tests/accessibility/delete-test-user.ts
new file mode 100644
index 00000000..574b4a25
--- /dev/null
+++ b/tests/accessibility/delete-test-user.ts
@@ -0,0 +1,6 @@
+import { readFileSync } from 'node:fs';
+import { TestUserClient } from './test-user-client';
+
+const { email } = JSON.parse(readFileSync('./auth.json', 'utf8'));
+
+new TestUserClient('./frontend').deleteTestUser(email);
diff --git a/tests/accessibility/test-user-client.ts b/tests/accessibility/test-user-client.ts
new file mode 100644
index 00000000..61a5b55d
--- /dev/null
+++ b/tests/accessibility/test-user-client.ts
@@ -0,0 +1,69 @@
+import {
+ AdminCreateUserCommand,
+ AdminDeleteUserCommand,
+ AdminSetUserPasswordCommand,
+ CognitoIdentityProviderClient,
+ } from '@aws-sdk/client-cognito-identity-provider';
+ import { readFileSync } from 'node:fs';
+
+ export class TestUserClient {
+ private readonly cognitoClient = new CognitoIdentityProviderClient({
+ region: 'eu-west-2',
+ });
+
+ private readonly userPoolId: string;
+
+ constructor(amplifyOutputsPathPrefix = '../..') {
+ const amplifyOutputs = JSON.parse(
+ readFileSync(`${amplifyOutputsPathPrefix}/amplify_outputs.json`, 'utf8')
+ );
+
+ this.userPoolId = amplifyOutputs.auth.user_pool_id;
+ }
+
+ async createTestUser(email: string, password: string) {
+ const res = await this.cognitoClient.send(
+ new AdminCreateUserCommand({
+ UserPoolId: this.userPoolId,
+ Username: email,
+ UserAttributes: [
+ {
+ Name: 'email',
+ Value: email,
+ },
+ {
+ Name: 'email_verified',
+ Value: 'true',
+ },
+ ],
+ MessageAction: 'SUPPRESS',
+ })
+ );
+
+ await this.cognitoClient.send(
+ new AdminSetUserPasswordCommand({
+ UserPoolId: this.userPoolId,
+ Username: email,
+ Password: password,
+ Permanent: true,
+ })
+ );
+
+ const username = res.User?.Username;
+
+ if (!username) {
+ throw new Error('Error during user creation');
+ }
+
+ return username;
+ }
+
+ async deleteTestUser(email: string) {
+ await this.cognitoClient.send(
+ new AdminDeleteUserCommand({
+ UserPoolId: this.userPoolId,
+ Username: email,
+ })
+ );
+ }
+ }
diff --git a/tests/test-team/.gitignore b/tests/test-team/.gitignore
new file mode 100644
index 00000000..5224f03a
--- /dev/null
+++ b/tests/test-team/.gitignore
@@ -0,0 +1 @@
+auth/*
diff --git a/tests/test-team/README.md b/tests/test-team/README.md
index 2aec0d80..e18b258c 100644
--- a/tests/test-team/README.md
+++ b/tests/test-team/README.md
@@ -6,3 +6,37 @@ This package includes:
- component tests, which are designed to test the behaviour on individual pages, and seed template data relevant to those pages at the start of the tests
- e2e tests, which simulate a user going through the whole app.
+
+## Tips
+
+### auth.setup.ts failing?
+
+1. Ensure you've created a sandbox environment
+2. Ensure your `frontend/amplify_outputs.json` is set correctly
+3. Ensure `INCLUDE_AUTH_PAGES=true` is set when running in production mode
+
+### Flakey-ish tests?
+
+If the frontend application is running in `development` mode I.E. by doing `npm run dev`. Then the automated tests are slower and appear to be somewhat flakier.
+
+It's best to run the tests in `production` mode by running a `npm run build && npm run start` or simply run
+
+```bash
+npm run test:local-ui
+```
+
+Which will automatically `build` and `start` the frontend application.
+
+### Need to see frontend application logs when testing?
+
+Set `stdout` to `pipe` in [local.config.ts](config/local.config.ts).
+
+### Need more debugging information?
+
+Run the following;
+
+```bash
+npx playwright show-report
+```
+
+You'll then be able to see the trace information on the failed tests.
diff --git a/tests/test-team/config/auth.setup.ts b/tests/test-team/config/auth.setup.ts
new file mode 100644
index 00000000..12118a7c
--- /dev/null
+++ b/tests/test-team/config/auth.setup.ts
@@ -0,0 +1,18 @@
+/*
+ * Playwright setting up Auth -> https://playwright.dev/docs/auth
+ */
+
+import { test as setup } from '@playwright/test';
+import { TemplateMgmtSignInPage } from '../pages/templates-mgmt-login-page';
+
+setup('authenticate setup', async ({ page }) => {
+ const loginPage = new TemplateMgmtSignInPage(page);
+
+ await loginPage.loadPage();
+
+ await loginPage.cognitoSignIn(process.env.USER_EMAIL);
+
+ await page.waitForURL('/templates/create-and-submit-templates');
+
+ await page.context().storageState({ path: 'auth/user.json' });
+});
diff --git a/tests/test-team/config/auth.teardown.ts b/tests/test-team/config/auth.teardown.ts
new file mode 100644
index 00000000..8583ae68
--- /dev/null
+++ b/tests/test-team/config/auth.teardown.ts
@@ -0,0 +1,16 @@
+/*
+ * Playwright setting up Auth -> https://playwright.dev/docs/auth
+ */
+
+import { test as teardown } from '@playwright/test';
+import { CognitoUserHelper } from '../helpers/cognito-user-helper';
+
+teardown('authenticate teardown', async () => {
+ const userHelper = new CognitoUserHelper();
+
+ const user = await userHelper.getUser(process.env.USER_EMAIL);
+
+ if (user) {
+ await userHelper.deleteUser(user.email);
+ }
+});
diff --git a/tests/test-team/config/global.setup.ts b/tests/test-team/config/global.setup.ts
index 4eaa4dbf..af438c27 100644
--- a/tests/test-team/config/global.setup.ts
+++ b/tests/test-team/config/global.setup.ts
@@ -1,11 +1,40 @@
import { FullConfig } from '@playwright/test';
-import { DatabaseTableNameHelper } from '../helpers/database-tablename-helper';
+
+import { randomUUID as uuidv4 } from 'node:crypto';
+import generate from 'generate-password';
+import { ConfigHelper } from '../helpers/config-helper';
+import { CognitoUserHelper } from '../helpers/cognito-user-helper';
async function globalSetup(config: FullConfig) {
- const tableNameHelper = new DatabaseTableNameHelper();
+ const configHelper = new ConfigHelper();
process.env.TEMPLATE_STORAGE_TABLE_NAME =
- await tableNameHelper.getTemplateStorageTableName();
+ configHelper.getTemplateStorageTableName();
+
+ process.env.COGNITO_USER_POOL_ID = configHelper.getCognitoUserPoolId();
+
+ process.env.COGNITO_USER_POOL_CLIENT_ID =
+ configHelper.getCognitoUserPoolClientId();
+
+ const cognitoUserHelper = new CognitoUserHelper();
+
+ const [temporary, password] = generate.generateMultiple(2, {
+ length: 12,
+ numbers: true,
+ uppercase: true,
+ symbols: true,
+ strict: true,
+ });
+
+ const user = await cognitoUserHelper.createUser(
+ `${uuidv4().slice(0, 5)}-playwright-nhs-notify-web-template-mmgmt@notify.nhs.uk`,
+ temporary
+ );
+
+ process.env.USER_TEMPORARY_PASSWORD = temporary;
+ process.env.USER_PASSWORD = password;
+ process.env.USER_EMAIL = user.email;
+ process.env.USER_ID = user.userId;
return config;
}
diff --git a/tests/test-team/config/local.config.ts b/tests/test-team/config/local.config.ts
index 6594f148..1b046b1c 100644
--- a/tests/test-team/config/local.config.ts
+++ b/tests/test-team/config/local.config.ts
@@ -4,9 +4,21 @@ import baseConfig from './playwright.config';
export default defineConfig({
...baseConfig,
- timeout: 10_000,
-
+ timeout: 30_000, // 30 seconds in the playwright default
+ expect: {
+ timeout: 10_000, // default is 5 seconds. After creating and previewing sometimes the load is slow on a cold start
+ },
projects: [
+ {
+ name: 'auth-setup',
+ testMatch: 'auth.setup.ts',
+ use: {
+ baseURL: 'http://localhost:3000',
+ ...devices['Desktop Chrome'],
+ headless: true,
+ screenshot: 'only-on-failure',
+ },
+ },
{
name: 'component',
testMatch: '*.component.ts',
@@ -15,7 +27,10 @@ export default defineConfig({
baseURL: 'http://localhost:3000',
...devices['Desktop Chrome'],
headless: true,
+ storageState: './auth/user.json',
},
+ dependencies: ['auth-setup'],
+ teardown: 'auth-teardown',
},
{
name: 'e2e-local',
@@ -25,9 +40,14 @@ export default defineConfig({
...devices['Desktop Chrome'],
},
},
+ {
+ name: 'auth-teardown',
+ testMatch: 'auth.teardown.ts',
+ },
],
/* Run your local dev server before starting the tests */
webServer: {
+ timeout: 2 * 60 * 1000, // 2 minutes
command: 'npm run test:start-local-app',
url: 'http://localhost:3000/templates/create-and-submit-templates',
reuseExistingServer: !process.env.CI,
diff --git a/tests/test-team/config/playwright.config.ts b/tests/test-team/config/playwright.config.ts
index e852f422..b221af08 100644
--- a/tests/test-team/config/playwright.config.ts
+++ b/tests/test-team/config/playwright.config.ts
@@ -21,8 +21,8 @@ export default defineConfig({
],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
- /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
- trace: 'on-first-retry',
+ /* Collect trace when a test fails. See https://playwright.dev/docs/trace-viewer */
+ trace: 'retain-on-failure',
},
globalSetup: './global.setup.ts',
});
diff --git a/tests/test-team/global.d.ts b/tests/test-team/global.d.ts
index 967e9dea..ed65301c 100644
--- a/tests/test-team/global.d.ts
+++ b/tests/test-team/global.d.ts
@@ -2,6 +2,11 @@ declare global {
namespace NodeJS {
interface ProcessEnv {
TEMPLATE_STORAGE_TABLE_NAME: string;
+ COGNITO_USER_POOL_ID: string;
+ USER_TEMPORARY_PASSWORD: string;
+ USER_PASSWORD: string;
+ USER_EMAIL: string;
+ USER_ID: string;
}
}
}
diff --git a/tests/test-team/helpers/cognito-user-helper.ts b/tests/test-team/helpers/cognito-user-helper.ts
new file mode 100644
index 00000000..682f990a
--- /dev/null
+++ b/tests/test-team/helpers/cognito-user-helper.ts
@@ -0,0 +1,94 @@
+import {
+ AdminCreateUserCommand,
+ AdminDeleteUserCommand,
+ AdminDisableUserCommand,
+ AdminGetUserCommand,
+ CognitoIdentityProviderClient,
+} from '@aws-sdk/client-cognito-identity-provider';
+
+export type User = {
+ email: string;
+ userId: string;
+};
+
+export class CognitoUserHelper {
+ private readonly _client: CognitoIdentityProviderClient;
+
+ constructor() {
+ this._client = new CognitoIdentityProviderClient({
+ region: 'eu-west-2',
+ });
+ }
+
+ async createUser(email: string, temporaryPassword?: string): Promise {
+ // Note: we use a unique prefix to that we don't interfere with other users.
+ const user = await this._client.send(
+ new AdminCreateUserCommand({
+ UserPoolId: process.env.COGNITO_USER_POOL_ID,
+ Username: email,
+ UserAttributes: [
+ {
+ Name: 'email',
+ Value: email,
+ },
+ {
+ Name: 'email_verified',
+ Value: 'true',
+ },
+ ],
+ MessageAction: 'SUPPRESS',
+ TemporaryPassword: temporaryPassword,
+ })
+ );
+
+ if (!user?.User?.Username) {
+ throw new Error('Unable to generate cognito user');
+ }
+
+ return {
+ email,
+ userId: String(
+ user.User.Attributes?.find((attr) => attr.Name === 'sub')?.Value
+ ),
+ };
+ }
+
+ async getUser(email: string): Promise {
+ try {
+ const user = await this._client.send(
+ new AdminGetUserCommand({
+ UserPoolId: process.env.COGNITO_USER_POOL_ID,
+ Username: email,
+ })
+ );
+
+ return {
+ email: String(
+ user.UserAttributes?.find((attr) => attr.Name === 'email')?.Value
+ ),
+ userId: String(
+ user.UserAttributes?.find((attr) => attr.Name === 'sub')?.Value
+ ),
+ };
+ } catch {
+ // no-op
+ }
+ }
+
+ async deleteUser(email: string) {
+ // Note: we must disable the user first before we can delete them
+ await this._client.send(
+ new AdminDisableUserCommand({
+ UserPoolId: process.env.COGNITO_USER_POOL_ID,
+ Username: email,
+ })
+ );
+
+ await this._client.send(
+ new AdminDeleteUserCommand({
+ UserPoolId: process.env.COGNITO_USER_POOL_ID,
+ Username: email,
+ })
+ );
+ }
+}
diff --git a/tests/test-team/helpers/config-helper.ts b/tests/test-team/helpers/config-helper.ts
new file mode 100644
index 00000000..f86e9dc1
--- /dev/null
+++ b/tests/test-team/helpers/config-helper.ts
@@ -0,0 +1,34 @@
+import * as fs from 'node:fs';
+
+type Config = {
+ auth: {
+ user_pool_id: string;
+ user_pool_client_id: string;
+ };
+ meta: {
+ dynamo_table_name: string;
+ backend_api_url: string;
+ };
+};
+
+export class ConfigHelper {
+ private _config: Config;
+
+ constructor() {
+ this._config = JSON.parse(
+ fs.readFileSync('../../frontend/amplify_outputs.json', 'utf8')
+ );
+ }
+
+ public getTemplateStorageTableName() {
+ return this._config.meta.dynamo_table_name;
+ }
+
+ public getCognitoUserPoolId() {
+ return this._config.auth.user_pool_id;
+ }
+
+ public getCognitoUserPoolClientId() {
+ return this._config.auth.user_pool_client_id;
+ }
+}
diff --git a/tests/test-team/helpers/database-tablename-helper.ts b/tests/test-team/helpers/database-tablename-helper.ts
deleted file mode 100644
index c34cbb12..00000000
--- a/tests/test-team/helpers/database-tablename-helper.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import {
- AppSyncClient,
- paginateListGraphqlApis,
- GraphqlApi,
-} from '@aws-sdk/client-appsync';
-import { Amplify } from 'aws-amplify';
-import * as fs from 'node:fs';
-
-export class DatabaseTableNameHelper {
- private readonly _appSyncClient: AppSyncClient;
-
- private _appApiId?: string;
-
- constructor() {
- this._appSyncClient = new AppSyncClient({ region: 'eu-west-2' });
- }
-
- private async listGraphqlAPIs() {
- const apiList: GraphqlApi[] = [];
-
- const paginator = paginateListGraphqlApis(
- { client: this._appSyncClient },
- {}
- );
-
- for await (const page of paginator) {
- const apiData = page.graphqlApis ?? [];
- apiList.push(...apiData);
- }
-
- return apiList;
- }
-
- private async getApiId() {
- if (this._appApiId) {
- return this._appApiId;
- }
-
- const amplifyOutput = JSON.parse(
- fs.readFileSync('../../frontend/amplify_outputs.json', 'utf8')
- );
-
- Amplify.configure(amplifyOutput);
-
- const graphqlAPIs = await this.listGraphqlAPIs();
-
- const outputUri = Amplify.getConfig().API?.GraphQL?.endpoint;
-
- const matchingGraphqlAPI = graphqlAPIs.find(
- (graphqlAPI) => graphqlAPI.uris?.GRAPHQL === outputUri
- );
-
- this._appApiId = matchingGraphqlAPI?.apiId;
-
- return this._appApiId;
- }
-
- public async getTemplateStorageTableName() {
- const appApiId = await this.getApiId();
-
- return `TemplateStorage-${appApiId}-NONE`;
- }
-}
diff --git a/tests/test-team/helpers/template-factory.ts b/tests/test-team/helpers/template-factory.ts
index bebc749c..bdb83cd4 100644
--- a/tests/test-team/helpers/template-factory.ts
+++ b/tests/test-team/helpers/template-factory.ts
@@ -30,13 +30,13 @@ export const TemplateFactory = {
}
): Template => {
return {
- __typename: 'TemplateStorage',
templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
version: 1,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
name: '',
message: '',
+ owner: process.env.USER_ID,
...template,
};
},
diff --git a/tests/test-team/helpers/template-storage-helper.ts b/tests/test-team/helpers/template-storage-helper.ts
index ee32baa0..1cdf84a9 100644
--- a/tests/test-team/helpers/template-storage-helper.ts
+++ b/tests/test-team/helpers/template-storage-helper.ts
@@ -37,6 +37,7 @@ export class TemplateStorageHelper {
new DeleteCommand({
TableName: process.env.TEMPLATE_STORAGE_TABLE_NAME,
Key: {
+ owner: process.env.USER_ID,
id,
},
})
diff --git a/tests/test-team/helpers/types.ts b/tests/test-team/helpers/types.ts
index 0c111247..5422a0c3 100644
--- a/tests/test-team/helpers/types.ts
+++ b/tests/test-team/helpers/types.ts
@@ -24,7 +24,6 @@ export enum TemplateStatus {
}
export type Template = {
- __typename: string;
createdAt: string;
updatedAt: string;
id: string;
@@ -34,4 +33,5 @@ export type Template = {
subject?: string;
templateType: TemplateType;
templateStatus: TemplateStatus;
+ owner: string;
};
diff --git a/tests/test-team/package.json b/tests/test-team/package.json
index 6752824a..f85a8da4 100644
--- a/tests/test-team/package.json
+++ b/tests/test-team/package.json
@@ -7,15 +7,17 @@
"test:local-ui": "playwright test --project component -c config/local.config.ts",
"test:local-ui-e2e": "playwright test --project e2e-local -c config/local.config.ts",
"lint": "eslint .",
- "lint:fix": "eslint . --fix",
+ "lint:fix": "npm run lint -- --fix",
"test:unit": "echo \"Unit tests not required\"",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@aws-sdk/client-appsync": "^3.650.0",
+ "@aws-sdk/client-cognito-identity-provider": "^3.650.0",
"@aws-sdk/client-dynamodb": "^3.654.0",
"@aws-sdk/lib-dynamodb": "^3.654.0",
"@playwright/test": "^1.45.1",
- "aws-amplify": "^6.6.0"
+ "aws-amplify": "^6.6.0",
+ "generate-password": "^1.7.1"
}
}
diff --git a/tests/test-team/pages/template-mgmt-base-page.ts b/tests/test-team/pages/template-mgmt-base-page.ts
index 07f3e682..471d990a 100644
--- a/tests/test-team/pages/template-mgmt-base-page.ts
+++ b/tests/test-team/pages/template-mgmt-base-page.ts
@@ -7,6 +7,8 @@ export class TemplateMgmtBasePage {
readonly loginLink: Locator;
+ readonly logoutLink: Locator;
+
readonly goBackLink: Locator;
readonly pageHeader: Locator;
@@ -32,6 +34,10 @@ export class TemplateMgmtBasePage {
.locator('[class="nhsuk-account__login--link"]')
.and(page.getByText('Log in'));
+ this.logoutLink = page
+ .locator('[class="nhsuk-account__login--link"]')
+ .and(page.getByText('Log out'));
+
this.goBackLink = page
.locator('.nhsuk-back-link__link')
.and(page.getByText('Go back'));
diff --git a/tests/test-team/pages/template-mgmt-start-page.ts b/tests/test-team/pages/template-mgmt-start-page.ts
index 70a663b1..d92103a5 100644
--- a/tests/test-team/pages/template-mgmt-start-page.ts
+++ b/tests/test-team/pages/template-mgmt-start-page.ts
@@ -30,4 +30,8 @@ export class TemplateMgmtStartPage extends TemplateMgmtBasePage {
async clickStartButton() {
await this.startButton.click();
}
+
+ async loadPage(_?: string): Promise {
+ await this.navigateToStartPage();
+ }
}
diff --git a/tests/test-team/pages/templates-mgmt-login-page.ts b/tests/test-team/pages/templates-mgmt-login-page.ts
new file mode 100644
index 00000000..b18825fe
--- /dev/null
+++ b/tests/test-team/pages/templates-mgmt-login-page.ts
@@ -0,0 +1,51 @@
+import { Locator, Page } from '@playwright/test';
+import { TemplateMgmtBasePage } from './template-mgmt-base-page';
+
+export class TemplateMgmtSignInPage extends TemplateMgmtBasePage {
+ public readonly emailInput: Locator;
+
+ public readonly passwordInput: Locator;
+
+ public readonly confirmPasswordInput: Locator;
+
+ public readonly submitButton: Locator;
+
+ public readonly errorMessage: Locator;
+
+ constructor(page: Page) {
+ super(page);
+ this.emailInput = page.locator('input[name="username"]');
+ this.passwordInput = page.locator('input[name="password"]');
+ this.confirmPasswordInput = page.locator('input[name="confirm_password"]');
+ this.submitButton = page.locator('button[type="submit"]');
+ this.errorMessage = page.locator('.amplify-alert__body');
+ }
+
+ async cognitoSignIn(email: string) {
+ await this.emailInput.fill(email);
+
+ await this.passwordInput.fill(process.env.USER_TEMPORARY_PASSWORD);
+
+ await this.clickSubmitButton();
+
+ // Note: because this is a new user Cognito forces us to update the password.
+ await this.cognitoUpdateUserPassword();
+
+ await this.clickSubmitButton();
+ }
+
+ async cognitoUpdateUserPassword() {
+ await this.passwordInput.fill(process.env.USER_PASSWORD);
+
+ await this.confirmPasswordInput.fill(process.env.USER_PASSWORD);
+ }
+
+ async clickSubmitButton() {
+ await this.submitButton.click();
+ }
+
+ async loadPage() {
+ await this.page.goto('/templates/create-and-submit-templates');
+ await super.clickLoginLink();
+ }
+}
diff --git a/tests/test-team/template-mgmt-component-tests/email/template-mgmt-create-email-page.component.ts b/tests/test-team/template-mgmt-component-tests/email/template-mgmt-create-email-page.component.ts
index 7d7d6db5..d19e495e 100644
--- a/tests/test-team/template-mgmt-component-tests/email/template-mgmt-create-email-page.component.ts
+++ b/tests/test-team/template-mgmt-component-tests/email/template-mgmt-create-email-page.component.ts
@@ -5,8 +5,8 @@ import { TemplateFactory } from '../../helpers/template-factory';
import {
assertFooterLinks,
assertGoBackLink,
+ assertLogoutLink,
assertGoBackLinkNotPresent,
- assertLoginLink,
assertNotifyBannerLink,
assertSkipToMainContent,
} from '../template-mgmt-common.steps';
@@ -72,7 +72,7 @@ test.describe('Create Email message template Page', () => {
await assertSkipToMainContent(props);
await assertNotifyBannerLink(props);
- await assertLoginLink(props);
+ await assertLogoutLink(props);
await assertFooterLinks(props);
await assertGoBackLink({
...props,
diff --git a/tests/test-team/template-mgmt-component-tests/email/template-mgmt-preview-email-page.component.ts b/tests/test-team/template-mgmt-component-tests/email/template-mgmt-preview-email-page.component.ts
index 652ff27c..6a070c01 100644
--- a/tests/test-team/template-mgmt-component-tests/email/template-mgmt-preview-email-page.component.ts
+++ b/tests/test-team/template-mgmt-component-tests/email/template-mgmt-preview-email-page.component.ts
@@ -8,7 +8,7 @@ import {
} from '../template-mgmt-preview-common.steps';
import {
assertFooterLinks,
- assertLoginLink,
+ assertLogoutLink,
assertNotifyBannerLink,
assertSkipToMainContent,
} from '../template-mgmt-common.steps';
@@ -16,13 +16,13 @@ import { TemplateType, Template, TemplateStatus } from '../../helpers/types';
const templates = {
empty: {
- __typename: 'TemplateStorage',
id: 'preview-page-invalid-email-template',
version: 1,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
templateType: TemplateType.EMAIL,
templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
+ owner: process.env.USER_ID,
} as Template,
valid: {
...TemplateFactory.createEmailTemplate('valid-email-preview-template'),
@@ -80,7 +80,7 @@ test.describe('Preview Email message template Page', () => {
await assertSkipToMainContent(props);
await assertNotifyBannerLink(props);
- await assertLoginLink(props);
+ await assertLogoutLink(props);
await assertFooterLinks(props);
await assertBackToAllTemplatesTopLink(props);
await assertBackToAllTemplatesBottomLink(props);
diff --git a/tests/test-team/template-mgmt-component-tests/email/template-mgmt-view-submitted-email-page.component.ts b/tests/test-team/template-mgmt-component-tests/email/template-mgmt-view-submitted-email-page.component.ts
index aaab07de..f8746ab7 100644
--- a/tests/test-team/template-mgmt-component-tests/email/template-mgmt-view-submitted-email-page.component.ts
+++ b/tests/test-team/template-mgmt-component-tests/email/template-mgmt-view-submitted-email-page.component.ts
@@ -5,7 +5,7 @@ import { TemplateFactory } from '../../helpers/template-factory';
import { TemplateStatus } from '../../helpers/types';
import {
assertFooterLinks,
- assertLoginLink,
+ assertLogoutLink,
assertNotifyBannerLink,
assertSkipToMainContent,
} from '../template-mgmt-common.steps';
@@ -84,7 +84,7 @@ test.describe('View submitted Email message template Page', () => {
await assertSkipToMainContent(props);
await assertNotifyBannerLink(props);
- await assertLoginLink(props);
+ await assertLogoutLink(props);
await assertFooterLinks(props);
await assertBackToAllTemplatesTopLink(props);
await assertBackToAllTemplatesBottomLink(props);
diff --git a/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-create-nhs-app-template-page.component.ts b/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-create-nhs-app-template-page.component.ts
index 10c846d8..11e7e750 100644
--- a/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-create-nhs-app-template-page.component.ts
+++ b/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-create-nhs-app-template-page.component.ts
@@ -5,8 +5,8 @@ import { TemplateStorageHelper } from '../../helpers/template-storage-helper';
import {
assertFooterLinks,
assertGoBackLink,
+ assertLogoutLink,
assertGoBackLinkNotPresent,
- assertLoginLink,
assertNotifyBannerLink,
assertSkipToMainContent,
} from '../template-mgmt-common.steps';
@@ -58,7 +58,7 @@ test.describe('Create NHS App Template Page', () => {
await assertSkipToMainContent(props);
await assertNotifyBannerLink(props);
- await assertLoginLink(props);
+ await assertLogoutLink(props);
await assertFooterLinks(props);
await assertGoBackLink({
...props,
diff --git a/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-preview-nhs-app-page.component.ts b/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-preview-nhs-app-page.component.ts
index 0f8c50d1..deec0abb 100644
--- a/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-preview-nhs-app-page.component.ts
+++ b/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-preview-nhs-app-page.component.ts
@@ -8,7 +8,7 @@ import {
} from '../template-mgmt-preview-common.steps';
import {
assertFooterLinks,
- assertLoginLink,
+ assertLogoutLink,
assertNotifyBannerLink,
assertSkipToMainContent,
} from '../template-mgmt-common.steps';
@@ -16,13 +16,13 @@ import { Template, TemplateType, TemplateStatus } from '../../helpers/types';
const templates = {
empty: {
- __typename: 'TemplateStorage',
id: 'preview-page-invalid-nhs-app-template',
version: 1,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
templateType: TemplateType.NHS_APP,
templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
+ owner: process.env.USER_ID,
} as Template,
valid: {
...TemplateFactory.createNhsAppTemplate('valid-nhs-app-preview-template'),
@@ -79,7 +79,7 @@ test.describe('Preview NHS App template Page', () => {
await assertSkipToMainContent(props);
await assertNotifyBannerLink(props);
- await assertLoginLink(props);
+ await assertLogoutLink(props);
await assertFooterLinks(props);
await assertBackToAllTemplatesTopLink(props);
await assertBackToAllTemplatesBottomLink(props);
diff --git a/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-view-submitted-nhs-app-page.component.ts b/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-view-submitted-nhs-app-page.component.ts
index 71f1dee7..c6916e33 100644
--- a/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-view-submitted-nhs-app-page.component.ts
+++ b/tests/test-team/template-mgmt-component-tests/nhs-app/template-mgmt-view-submitted-nhs-app-page.component.ts
@@ -5,7 +5,7 @@ import { TemplateFactory } from '../../helpers/template-factory';
import { TemplateStatus } from '../../helpers/types';
import {
assertFooterLinks,
- assertLoginLink,
+ assertLogoutLink,
assertNotifyBannerLink,
assertSkipToMainContent,
} from '../template-mgmt-common.steps';
@@ -78,7 +78,7 @@ test.describe('View submitted NHS App message template Page', () => {
await assertSkipToMainContent(props);
await assertNotifyBannerLink(props);
- await assertLoginLink(props);
+ await assertLogoutLink(props);
await assertFooterLinks(props);
await assertBackToAllTemplatesTopLink(props);
await assertBackToAllTemplatesBottomLink(props);
diff --git a/tests/test-team/template-mgmt-component-tests/sms/template-mgmt-create-sms-page.component.ts b/tests/test-team/template-mgmt-component-tests/sms/template-mgmt-create-sms-page.component.ts
index 23416880..40a88805 100644
--- a/tests/test-team/template-mgmt-component-tests/sms/template-mgmt-create-sms-page.component.ts
+++ b/tests/test-team/template-mgmt-component-tests/sms/template-mgmt-create-sms-page.component.ts
@@ -5,8 +5,8 @@ import { TemplateFactory } from '../../helpers/template-factory';
import {
assertFooterLinks,
assertGoBackLink,
+ assertLogoutLink,
assertGoBackLinkNotPresent,
- assertLoginLink,
assertNotifyBannerLink,
assertSkipToMainContent,
} from '../template-mgmt-common.steps';
@@ -74,7 +74,7 @@ test.describe('Create SMS message template Page', () => {
await assertSkipToMainContent(props);
await assertNotifyBannerLink(props);
- await assertLoginLink(props);
+ await assertLogoutLink(props);
await assertFooterLinks(props);
await assertGoBackLink({
...props,
diff --git a/tests/test-team/template-mgmt-component-tests/sms/template-mgmt-preview-sms-page.component.ts b/tests/test-team/template-mgmt-component-tests/sms/template-mgmt-preview-sms-page.component.ts
index 943997c9..82fe46c6 100644
--- a/tests/test-team/template-mgmt-component-tests/sms/template-mgmt-preview-sms-page.component.ts
+++ b/tests/test-team/template-mgmt-component-tests/sms/template-mgmt-preview-sms-page.component.ts
@@ -8,7 +8,7 @@ import {
} from '../template-mgmt-preview-common.steps';
import {
assertFooterLinks,
- assertLoginLink,
+ assertLogoutLink,
assertNotifyBannerLink,
assertSkipToMainContent,
} from '../template-mgmt-common.steps';
@@ -16,13 +16,13 @@ import { TemplateType, Template, TemplateStatus } from '../../helpers/types';
const templates = {
empty: {
- __typename: 'TemplateStorage',
id: 'preview-page-invalid-sms-template',
version: 1,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
templateType: TemplateType.SMS,
templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
+ owner: process.env.USER_ID,
} as Template,
valid: {
...TemplateFactory.createSmsTemplate('valid-sms-preview-template'),
@@ -79,7 +79,7 @@ test.describe('Preview SMS message template Page', () => {
await assertSkipToMainContent(props);
await assertNotifyBannerLink(props);
- await assertLoginLink(props);
+ await assertLogoutLink(props);
await assertFooterLinks(props);
await assertBackToAllTemplatesTopLink(props);
await assertBackToAllTemplatesBottomLink(props);
diff --git a/tests/test-team/template-mgmt-component-tests/sms/template-mgmt-view-submitted-sms-page.component.ts b/tests/test-team/template-mgmt-component-tests/sms/template-mgmt-view-submitted-sms-page.component.ts
index 0f19aa51..b936c286 100644
--- a/tests/test-team/template-mgmt-component-tests/sms/template-mgmt-view-submitted-sms-page.component.ts
+++ b/tests/test-team/template-mgmt-component-tests/sms/template-mgmt-view-submitted-sms-page.component.ts
@@ -5,7 +5,7 @@ import { TemplateFactory } from '../../helpers/template-factory';
import { TemplateStatus } from '../../helpers/types';
import {
assertFooterLinks,
- assertLoginLink,
+ assertLogoutLink,
assertNotifyBannerLink,
assertSkipToMainContent,
} from '../template-mgmt-common.steps';
@@ -75,7 +75,7 @@ test.describe('View submitted sms message template Page', () => {
await assertSkipToMainContent(props);
await assertNotifyBannerLink(props);
- await assertLoginLink(props);
+ await assertLogoutLink(props);
await assertFooterLinks(props);
await assertBackToAllTemplatesTopLink(props);
await assertBackToAllTemplatesBottomLink(props);
diff --git a/tests/test-team/template-mgmt-component-tests/template-mgmt-choose-page.component.ts b/tests/test-team/template-mgmt-component-tests/template-mgmt-choose-page.component.ts
index 61ff76db..71bcdc1b 100644
--- a/tests/test-team/template-mgmt-component-tests/template-mgmt-choose-page.component.ts
+++ b/tests/test-team/template-mgmt-component-tests/template-mgmt-choose-page.component.ts
@@ -3,7 +3,7 @@ import { TemplateMgmtChoosePage } from '../pages/template-mgmt-choose-page';
import {
assertFooterLinks,
assertGoBackLinkNotPresent,
- assertLoginLink,
+ assertLogoutLink,
assertNotifyBannerLink,
assertSkipToMainContent,
} from './template-mgmt-common.steps';
@@ -38,7 +38,7 @@ test.describe('Choose Template Type Page', () => {
await assertSkipToMainContent(props);
await assertNotifyBannerLink(props);
await assertFooterLinks(props);
- await assertLoginLink(props);
+ await assertLogoutLink(props);
await assertGoBackLinkNotPresent(props);
});
diff --git a/tests/test-team/template-mgmt-component-tests/template-mgmt-common.steps.ts b/tests/test-team/template-mgmt-component-tests/template-mgmt-common.steps.ts
index 022c4522..3532a2a7 100644
--- a/tests/test-team/template-mgmt-component-tests/template-mgmt-common.steps.ts
+++ b/tests/test-team/template-mgmt-component-tests/template-mgmt-common.steps.ts
@@ -37,14 +37,26 @@ export function assertNotifyBannerLink({
});
}
-export function assertLoginLink({ page, id, baseURL }: CommonStepsProps) {
+export function assertLoginLink({ page, id }: CommonStepsProps) {
return test.step('when user clicks "Log in", then user is redirected to "login page"', async () => {
await page.loadPage(id);
- await page.clickLoginLink();
+ const link = await page.loginLink.getAttribute('href');
- await expect(page.page).toHaveURL(
- `${baseURL}/auth?redirect=%2Ftemplates%2Fcreate-and-submit-templates`
+ expect(link).toBe(
+ '/auth?redirect=%2Ftemplates%2Fcreate-and-submit-templates'
+ );
+ });
+}
+
+export function assertLogoutLink({ page, id }: CommonStepsProps) {
+ return test.step('"Log out", should direct user to logout', async () => {
+ await page.loadPage(id);
+
+ const link = await page.logoutLink.getAttribute('href');
+
+ expect(link).toBe(
+ '/auth/signout?redirect=%2Ftemplates%2Fcreate-and-submit-templates'
);
});
}
diff --git a/tests/test-team/template-mgmt-component-tests/template-mgmt-copy-page.component.ts b/tests/test-team/template-mgmt-component-tests/template-mgmt-copy-page.component.ts
index 0e2c922d..ded086e6 100644
--- a/tests/test-team/template-mgmt-component-tests/template-mgmt-copy-page.component.ts
+++ b/tests/test-team/template-mgmt-component-tests/template-mgmt-copy-page.component.ts
@@ -3,7 +3,7 @@ import { TemplateMgmtCopyPage } from '../pages/template-mgmt-copy-page';
import {
assertFooterLinks,
assertGoBackLink,
- assertLoginLink,
+ assertLogoutLink,
assertNotifyBannerLink,
assertSkipToMainContent,
} from './template-mgmt-common.steps';
@@ -76,7 +76,7 @@ test.describe('Copy Template Page', () => {
await assertSkipToMainContent(props);
await assertNotifyBannerLink(props);
await assertFooterLinks(props);
- await assertLoginLink(props);
+ await assertLogoutLink(props);
await assertGoBackLink(props);
});
diff --git a/tests/test-team/template-mgmt-component-tests/template-mgmt-delete-page.component.ts b/tests/test-team/template-mgmt-component-tests/template-mgmt-delete-page.component.ts
index f8886cdb..05594920 100644
--- a/tests/test-team/template-mgmt-component-tests/template-mgmt-delete-page.component.ts
+++ b/tests/test-team/template-mgmt-component-tests/template-mgmt-delete-page.component.ts
@@ -3,7 +3,7 @@ import { TemplateMgmtDeletePage } from '../pages/template-mgmt-delete-page';
import {
assertFooterLinks,
assertGoBackLinkNotPresent,
- assertLoginLink,
+ assertLogoutLink,
assertNotifyBannerLink,
assertSkipToMainContent,
} from './template-mgmt-common.steps';
@@ -66,7 +66,7 @@ test.describe('Delete Template Page', () => {
await assertSkipToMainContent(props);
await assertNotifyBannerLink(props);
await assertFooterLinks(props);
- await assertLoginLink(props);
+ await assertLogoutLink(props);
await assertGoBackLinkNotPresent(props);
});
diff --git a/tests/test-team/template-mgmt-component-tests/template-mgmt-manage-templates-page.component.ts b/tests/test-team/template-mgmt-component-tests/template-mgmt-manage-templates-page.component.ts
index ed8540d1..6a650171 100644
--- a/tests/test-team/template-mgmt-component-tests/template-mgmt-manage-templates-page.component.ts
+++ b/tests/test-team/template-mgmt-component-tests/template-mgmt-manage-templates-page.component.ts
@@ -4,7 +4,7 @@ import { ManageTemplatesPage } from '../pages/template-mgmt-manage-templates-pag
import {
assertFooterLinks,
assertGoBackLinkNotPresent,
- assertLoginLink,
+ assertLogoutLink,
assertNotifyBannerLink,
assertSkipToMainContent,
} from './template-mgmt-common.steps';
@@ -88,7 +88,7 @@ test.describe('Manage templates page', () => {
await assertSkipToMainContent(props);
await assertNotifyBannerLink(props);
- await assertLoginLink(props);
+ await assertLogoutLink(props);
await assertFooterLinks(props);
await assertGoBackLinkNotPresent(props);
});
diff --git a/tests/test-team/template-mgmt-component-tests/template-mgmt-start-page.component.ts b/tests/test-team/template-mgmt-component-tests/template-mgmt-start-page.component.ts
index d8fad59c..553e00d6 100644
--- a/tests/test-team/template-mgmt-component-tests/template-mgmt-start-page.component.ts
+++ b/tests/test-team/template-mgmt-component-tests/template-mgmt-start-page.component.ts
@@ -1,5 +1,6 @@
import { test, expect } from '@playwright/test';
import { TemplateMgmtStartPage } from '../pages/template-mgmt-start-page';
+import { assertLogoutLink } from './template-mgmt-common.steps';
test.describe('Start Page', () => {
test('should land on start page when navigating to "/templates/create-and-submit-templates"', async ({
@@ -45,22 +46,14 @@ test.describe('Start Page', () => {
);
});
- test(
- 'should navigate to login page when "log in" link clicked',
- { tag: '@Update/CCM-4889' },
- async ({ page, baseURL }) => {
- const startPage = new TemplateMgmtStartPage(page);
-
- await startPage.navigateToStartPage();
- await startPage.clickLoginLink();
-
- await expect(page).toHaveURL(
- `${baseURL}/auth?redirect=%2Ftemplates%2Fcreate-and-submit-templates`
- );
+ test('should display logout link', async ({ page }) => {
+ const startPage = new TemplateMgmtStartPage(page);
- expect(await page.locator('h1').textContent()).toBe('404');
- }
- );
+ await assertLogoutLink({
+ page: startPage,
+ id: '/templates/create-and-submit-templates',
+ });
+ });
test('should not display "Go Back" link on page', async ({ page }) => {
const startPage = new TemplateMgmtStartPage(page);
diff --git a/tests/test-team/template-mgmt-component-tests/template-mgmt-submit-page.component.ts b/tests/test-team/template-mgmt-component-tests/template-mgmt-submit-page.component.ts
index 76138e0b..da848c47 100644
--- a/tests/test-team/template-mgmt-component-tests/template-mgmt-submit-page.component.ts
+++ b/tests/test-team/template-mgmt-component-tests/template-mgmt-submit-page.component.ts
@@ -5,7 +5,7 @@ import { TemplateFactory } from '../helpers/template-factory';
import { Template, TemplateType, TemplateStatus } from '../helpers/types';
import {
assertFooterLinks,
- assertLoginLink,
+ assertLogoutLink,
assertNotifyBannerLink,
assertSkipToMainContent,
} from './template-mgmt-common.steps';
@@ -30,13 +30,13 @@ const nhsAppFields = {
const templates = {
email: {
empty: {
- __typename: 'TemplateStorage',
id: 'submit-page-invalid-email-template',
version: 1,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
templateType: TemplateType.EMAIL,
templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
+ owner: process.env.USER_ID,
} as Template,
submit: {
...TemplateFactory.createEmailTemplate('submit-email-submit-template'),
@@ -55,13 +55,13 @@ const templates = {
},
'text-message': {
empty: {
- __typename: 'TemplateStorage',
id: 'submit-page-invalid-sms-template',
version: 1,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
templateType: TemplateType.SMS,
templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
+ owner: process.env.USER_ID,
} as Template,
submit: {
...TemplateFactory.createSmsTemplate('submit-sms-submit-template'),
@@ -78,13 +78,13 @@ const templates = {
},
'nhs-app': {
empty: {
- __typename: 'TemplateStorage',
id: 'submit-page-invalid-nhs-app-template',
version: 1,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
templateType: TemplateType.NHS_APP,
templateStatus: TemplateStatus.NOT_YET_SUBMITTED,
+ owner: process.env.USER_ID,
} as Template,
submit: {
...TemplateFactory.createNhsAppTemplate('submit-nhs-app-submit-template'),
@@ -155,7 +155,7 @@ test.describe('Submit template Page', () => {
await assertSkipToMainContent(props);
await assertNotifyBannerLink(props);
- await assertLoginLink(props);
+ await assertLogoutLink(props);
await assertFooterLinks(props);
await assertGoBackButton({
...props,
diff --git a/tests/test-team/template-mgmt-component-tests/template-mgmt-submitted-page.component.ts b/tests/test-team/template-mgmt-component-tests/template-mgmt-submitted-page.component.ts
index 91ba6e4e..2b886655 100644
--- a/tests/test-team/template-mgmt-component-tests/template-mgmt-submitted-page.component.ts
+++ b/tests/test-team/template-mgmt-component-tests/template-mgmt-submitted-page.component.ts
@@ -3,7 +3,7 @@ import { TemplateStorageHelper } from '../helpers/template-storage-helper';
import {
assertFooterLinks,
assertGoBackLink,
- assertLoginLink,
+ assertLogoutLink,
assertNotifyBannerLink,
assertSkipToMainContent,
} from './template-mgmt-common.steps';
@@ -98,7 +98,7 @@ test.describe('Template Submitted Page', () => {
await assertSkipToMainContent(props);
await assertNotifyBannerLink(props);
await assertFooterLinks(props);
- await assertLoginLink(props);
+ await assertLogoutLink(props);
await assertGoBackLink({
...props,
expectedUrl: 'templates/manage-templates',
diff --git a/utils/package.json b/utils/package.json
index 20207059..d2556369 100644
--- a/utils/package.json
+++ b/utils/package.json
@@ -14,7 +14,8 @@
},
"dependencies": {
"winston": "^3.14.2",
- "zod": "^3.23.8"
+ "zod": "^3.23.8",
+ "nhs-notify-backend-client": "*"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.4"
diff --git a/utils/src/enum.ts b/utils/src/enum.ts
index b69588e3..ffc97477 100644
--- a/utils/src/enum.ts
+++ b/utils/src/enum.ts
@@ -1,14 +1,7 @@
-export enum TemplateType {
- NHS_APP = 'NHS_APP',
- EMAIL = 'EMAIL',
- SMS = 'SMS',
-}
+import { TemplateType, TemplateStatus } from 'nhs-notify-backend-client';
-export enum TemplateStatus {
- NOT_YET_SUBMITTED = 'NOT_YET_SUBMITTED',
- SUBMITTED = 'SUBMITTED',
- DELETED = 'DELETED',
-}
+// eslint-disable-next-line unicorn/prefer-export-from
+export { TemplateType, TemplateStatus };
export const templateTypeDisplayMappings = (type: TemplateType) =>
({
diff --git a/utils/src/zod-validators.ts b/utils/src/zod-validators.ts
index c3dbae27..4dfb7bc5 100644
--- a/utils/src/zod-validators.ts
+++ b/utils/src/zod-validators.ts
@@ -7,8 +7,9 @@ export const $Template = z.object({
templateStatus: z.nativeEnum(TemplateStatus),
name: z.string(),
message: z.string(),
- subject: z.string().nullable().optional(),
+ subject: z.string().optional(),
createdAt: z.string().optional(),
+ updatedAt: z.string().optional(),
});
export const $EmailTemplate = $Template.extend({