Skip to content

Commit

Permalink
feat(APIM-608): gov notify - file upload/link support (#1083)
Browse files Browse the repository at this point in the history
## Introduction ✏️
GOV Notify integration needs the ability to consume and send a file
buffer, in order for users to be able to download a file stored securly
by Notify.

## Resolution ✔️
- Update GOV Notify constants.
- Update GOV Notify service to conditionally add a `linkToFile`
property.
- Update `PostEmailsRequestDto` module to include `FILE`.
- Create new mock JSON response (for unit tests).

## Miscellaneous ➕
- Minor JSON indentation improvement.
- Minor documentation improvment.
- Updated GitHook lint staged commands.

---------

Co-authored-by: Abhi Markan <amarkan@ukexportfinance.gov.uk>
  • Loading branch information
ttbarnes and abhi-markan authored Dec 17, 2024
1 parent 041124c commit dddf3e6
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 34 deletions.
32 changes: 16 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"audit:fix": "npm audit --fix",
"build": "nest build -p tsconfig.build.json",
"housekeeping": "npm update --save --legacy-peer-deps && npm i --legacy-peer-deps && npm run type-check && npm run prettier:fix && npm run lint:fix && npm run validate:yml && npm run audit:fix && npm run spellcheck",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
"lint": "eslint . --ext .js,.ts",
"lint:fix": "eslint . --ext .js,.ts --fix",
"prettier": "prettier --no-error-on-unmatched-pattern --check **/*.{ts,js}",
"prettier:fix": "prettier --write **/*.{ts,js}",
"spellcheck": "cspell lint --gitignore --no-must-find-files --unique --no-progress --show-suggestions --color '**/*'",
Expand All @@ -28,8 +28,8 @@
"lint-staged": {
"**/package.json": "sort-package-json",
"**/*.{js,ts}": [
"lint:fix",
"prettier:fix"
"npm run lint:fix",
"npm run prettier:fix"
],
"**/*.md": [
"prettier --write"
Expand Down
1 change: 1 addition & 0 deletions src/constants/govuk-notify.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const GOVUK_NOTIFY = {
RESPONSE_URI: 'https://api.notifications.service.gov.uk/v2/notifications/efd12345-1234-5678-9012-ee123456789f',
TEMPLATE_ID: 'tmpl1234-1234-5678-9012-abcd12345678',
TEMPLATE_URI: 'https://api.notifications.service.gov.uk/services/abc12345-a123-4567-8901-123456789012/templates/tmpl1234-1234-5678-9012-abcd12345678',
FILE: Buffer.from('example-file.xlsx'),
},
FIELD_LENGTHS: {
TEMPLATE_ID: 36,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"id": "740e5834-3a29-46b4-9a6f-16142fde533a",
"reference": "your_reference_here",
"content": {
"subject": "SUBJECT TEXT",
"body": "MESSAGE TEXT",
"from_email": "SENDER EMAIL"
},
"uri": "https: //api.notifications.service.gov.uk/v2/notifications/740e5834-3a29-46b4-9a6f-16142fde533a",
"template": {
"id": "f33517ff-2a88-4f6e-b855-c550268ce08a",
"version": "24",
"uri": "https: //api.notifications.service.gov.uk/v2/template/f33517ff-2a88-4f6e-b855-c550268ce08a"
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"content": {
"body": "Dear John Smith,\r\n\r\nThe status of your MIA for EuroStar has been updated.\r\n\r\n* Your bank reference: EuroStar bridge\r\n* Current status: Acknowledged\r\n* Previous status: Submitted\r\n* Updated by: Joe Bloggs (Joe.Bloggs@example.com)\r\n\r\nSign in to our service for more information: \r\nhttps://www.test.service.gov.uk/\r\n\r\nWith regards,\r\n\r\nThe Digital Trade Finance Service team\r\n\r\nEmail: test@test.gov.uk\r\nPhone: +44 (0)202 123 4567\r\nOpening times: Monday to Friday, 9am to 5pm (excluding public holidays)",
"from_email": "test@notifications.service.gov.uk",
"subject": "Status update: EuroStar bridge",
"unsubscribe_link": null
"body": "Dear John Smith,\r\n\r\nThe status of your MIA for EuroStar has been updated.\r\n\r\n* Your bank reference: EuroStar bridge\r\n* Current status: Acknowledged\r\n* Previous status: Submitted\r\n* Updated by: Joe Bloggs (Joe.Bloggs@example.com)\r\n\r\nSign in to our service for more information: \r\nhttps://www.test.service.gov.uk/\r\n\r\nWith regards,\r\n\r\nThe Digital Trade Finance Service team\r\n\r\nEmail: test@test.gov.uk\r\nPhone: +44 (0)202 123 4567\r\nOpening times: Monday to Friday, 9am to 5pm (excluding public holidays)",
"from_email": "test@notifications.service.gov.uk",
"subject": "Status update: EuroStar bridge",
"unsubscribe_link": null
},
"id": "efd12345-1234-5678-9012-ee123456789f",
"reference": "tmpl1234-1234-5678-9012-abcd12345678-1713346533467",
"scheduled_for": null,
"template": {
"id": "tmpl1234-1234-5678-9012-abcd12345678",
"uri": "https://api.notifications.service.gov.uk/services/abc12345-a123-4567-8901-123456789012/templates/tmpl1234-1234-5678-9012-abcd12345678",
"version": 24
"id": "tmpl1234-1234-5678-9012-abcd12345678",
"uri": "https://api.notifications.service.gov.uk/services/abc12345-a123-4567-8901-123456789012/templates/tmpl1234-1234-5678-9012-abcd12345678",
"version": 24
},
"uri": "https://api.notifications.service.gov.uk/v2/notifications/efd12345-1234-5678-9012-ee123456789f"
}
51 changes: 46 additions & 5 deletions src/helper-modules/govuk-notify/govuk-notify.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { AxiosError, AxiosResponse } from 'axios';
import { PinoLogger } from 'nestjs-pino';
import { NotifyClient } from 'notifications-node-client';

import expectedResponse from './examples/example-response-for-send-emails.json';
import { PostEmailsRequestDto } from '../../modules/emails/dto/post-emails-request.dto';
import expectedPrepareUploadResponse from './examples/example-response-for-prepare-upload.json';
import expectedSendEmailsResponse from './examples/example-response-for-send-emails.json';
import { GovukNotifyService } from './govuk-notify.service';
jest.mock('notifications-node-client');

Expand All @@ -24,9 +26,18 @@ describe('GovukNotifyService', () => {
supplierName: valueGenerator.word(),
};

const sendEmailMethodMock = jest
.spyOn(NotifyClient.prototype, 'sendEmail')
.mockImplementation(() => Promise.resolve({ status: 201, data: expectedResponse }));
const filePersonalisation = {
...personalisation,
file: 'mock-file-buffer',
};

const mockSendEmailResponse = { status: 201, data: expectedSendEmailsResponse };
const mockPrepareUploadResponse = { status: 201, data: expectedPrepareUploadResponse };

const sendEmailMethodMock = jest.spyOn(NotifyClient.prototype, 'sendEmail').mockImplementation(() => Promise.resolve(mockSendEmailResponse));

const prepareUploadMethodMock = jest.spyOn(NotifyClient.prototype, 'prepareUpload').mockImplementation(() => Promise.resolve(mockPrepareUploadResponse));

const logger = new PinoLogger({});
logger.error = loggerError;
const service = new GovukNotifyService(logger);
Expand Down Expand Up @@ -65,10 +76,40 @@ describe('GovukNotifyService', () => {
expect(sendEmailMethodMock).toHaveBeenCalledWith(templateId, sendToEmailAddress, { personalisation, reference });
});

describe('when a file property is provided', () => {
it('calls GOV.UK Notify client prepareUpload function with the correct arguments', async () => {
const mockParams: PostEmailsRequestDto = {
sendToEmailAddress,
templateId,
personalisation: filePersonalisation,
reference,
file: 'mock-file-buffer',
};

await service.sendEmail(govUkNotifyKey, mockParams);

expect(prepareUploadMethodMock).toHaveBeenCalledTimes(1);
expect(prepareUploadMethodMock).toHaveBeenCalledWith(mockParams.file, { confirmEmailBeforeDownload: true });
});

it('calls GOV.UK Notify client sendEmail function with the correct arguments', async () => {
await service.sendEmail(govUkNotifyKey, { sendToEmailAddress, templateId, personalisation: filePersonalisation, reference });

expect(sendEmailMethodMock).toHaveBeenCalledTimes(1);

const expectedPersonalisation = {
...filePersonalisation,
linkToFile: mockPrepareUploadResponse,
};

expect(sendEmailMethodMock).toHaveBeenCalledWith(templateId, sendToEmailAddress, { personalisation: expectedPersonalisation, reference });
});
});

it('returns a 201 response from GOV.UK Notify when sending the email is successful', async () => {
const response = await service.sendEmail(govUkNotifyKey, { sendToEmailAddress, templateId, personalisation });

expect(response).toEqual({ status: 201, data: expectedResponse });
expect(response).toEqual({ status: 201, data: expectedSendEmailsResponse });
});

it.each([
Expand Down
10 changes: 9 additions & 1 deletion src/helper-modules/govuk-notify/govuk-notify.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,18 @@ export class GovukNotifyService {
async sendEmail(govUkNotifyKey: string, postEmailsRequest: PostEmailsRequestDto): Promise<PostEmailsResponseDto> {
// We create new client for each request because govUkNotifyKey (auth key) might be different.
const notifyClient = new NotifyClient(govUkNotifyKey);

const reference = postEmailsRequest.reference || `${postEmailsRequest.templateId}-${Date.now()}`;

const { personalisation } = postEmailsRequest;

if (personalisation.file) {
personalisation.linkToFile = await notifyClient.prepareUpload(personalisation.file, { confirmEmailBeforeDownload: true });
}

const notifyResponse = await notifyClient
.sendEmail(postEmailsRequest.templateId, postEmailsRequest.sendToEmailAddress, {
personalisation: postEmailsRequest.personalisation,
personalisation,
reference,
})
.then((response: any) => response)
Expand Down
16 changes: 15 additions & 1 deletion src/modules/emails/dto/post-emails-request.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class PostEmailsRequestDto {
@IsString()
@IsOptional()
@MinLength(1)
// 100 characters is arbitrary max limit, GOV.UK Notify can accept references at least 400 characters long.
// 100 characters is an arbitrary max limit. GOV.UK Notify can accept references at least 400 characters long.
@MaxLength(100)
@ApiProperty({
example: GOVUK_NOTIFY.EXAMPLES.REFERENCE,
Expand All @@ -51,4 +51,18 @@ export class PostEmailsRequestDto {
maxLength: 100,
})
readonly reference?: string | null;

@IsString()
@IsOptional()
@MinLength(1)
@MaxLength(400)
@ApiProperty({
example: GOVUK_NOTIFY.EXAMPLES.FILE,
description: 'File for GovNotify to consume and generate a link to download',
required: false,
nullable: true,
minLength: 1,
maxLength: 400,
})
readonly file?: string | null;
}

0 comments on commit dddf3e6

Please sign in to comment.