Skip to content

Commit 4e88eb2

Browse files
committed
adding unit test
1 parent f4d848e commit 4e88eb2

File tree

21 files changed

+403
-1765
lines changed

21 files changed

+403
-1765
lines changed

infrastructure/terraform/modules/backend-api/module_lambda_send_letter_proof.tf

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ module "lambda_send_letter_proof" {
1717
DEFAULT_LETTER_SUPPLIER = local.default_letter_supplier.name
1818
SFTP_ENVIRONMENT = local.sftp_environment
1919
REGION = var.region
20-
"NODE_OPTIONS" = "--enable-source-maps",
20+
TEMPLATES_TABLE_NAME = aws_dynamodb_table.templates.name
21+
NODE_OPTIONS = "--enable-source-maps",
2122
}
2223
}
2324

@@ -105,7 +106,7 @@ data "aws_iam_policy_document" "send_letter_proof" {
105106
}
106107

107108
statement {
108-
sid = "AllowKMSAccessSQS"
109+
sid = "AllowKMSAccessGeneral"
109110
effect = "Allow"
110111

111112
actions = [
@@ -123,12 +124,12 @@ resource "aws_lambda_event_source_mapping" "trigger_send_proof" {
123124
event_source_arn = module.sftp_upload_queue.sqs_queue_arn
124125
enabled = true
125126
function_name = "${local.csi}-send-letter-proof"
126-
batch_size = 1
127+
batch_size = 10
127128
function_response_types = [
128129
"ReportBatchItemFailures"
129130
]
130131

131132
scaling_config {
132-
maximum_concurrency = 10
133+
maximum_concurrency = 2
133134
}
134135
}

lambdas/sftp-letters/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@
2727
},
2828
"dependencies": {
2929
"@aws-sdk/client-s3": "3.775.0",
30+
"@aws-sdk/client-dynamodb": "3.775.0",
31+
"@aws-sdk/lib-dynamodb": "3.775.0",
3032
"@aws-sdk/client-ssm": "3.775.0",
3133
"csv-stringify": "^6.4.4",
3234
"date-fns": "^4.1.0",
35+
"nhs-notify-entity-update-command-builder": "*",
3336
"nhs-notify-web-template-management-utils": "*",
3437
"ssh2-sftp-client": "^9.1.0",
3538
"zod": "^3.24.2"
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { mock } from 'jest-mock-extended';
2+
import { TemplateRepository } from '../../infra/template-repository';
3+
import { UserDataRepository } from '../../infra/user-data-repository';
4+
import { App } from '../../app/send';
5+
import { Batch } from '../../domain/batch';
6+
import { Logger } from 'nhs-notify-web-template-management-utils';
7+
import { SftpClient } from '../../infra/sftp-client';
8+
import { Readable } from 'node:stream';
9+
import { mockTestData } from '../helpers';
10+
11+
const sftpEnvironment = 'nhs-notify-web-template-management-main-app-api';
12+
const baseUploadDir = 'Incoming';
13+
const owner = 'owner-id';
14+
const templateId = 'template-id';
15+
const pdfVersion = 'pdf-version-id';
16+
const testDataVersion = 'test-data-version-id';
17+
18+
function setup() {
19+
const userDataRepository = mock<UserDataRepository>();
20+
const templateRepository = mock<TemplateRepository>();
21+
const batch = mock<Batch>();
22+
const logger = mock<Logger>();
23+
24+
const sftpClient = mock<SftpClient>();
25+
26+
const app = new App(
27+
userDataRepository,
28+
templateRepository,
29+
sftpEnvironment,
30+
batch,
31+
logger
32+
);
33+
34+
return {
35+
app,
36+
mocks: {
37+
userDataRepository,
38+
templateRepository,
39+
batch,
40+
logger,
41+
sftpClient,
42+
},
43+
};
44+
}
45+
46+
const standardFields = [
47+
'nhsNumber',
48+
'date',
49+
'address_line_1',
50+
'address_line_2',
51+
'address_line_3',
52+
'address_line_4',
53+
'address_line_5',
54+
'address_line_6',
55+
'address_line_7',
56+
];
57+
58+
function mockEvent(hasTestData: boolean, fields: string[]) {
59+
return {
60+
owner,
61+
templateId,
62+
pdfVersion,
63+
...(hasTestData && { testDataVersion }),
64+
fields,
65+
};
66+
}
67+
68+
describe('App', () => {
69+
test('calls dependencies to send a proofing request', async () => {
70+
const { app, mocks } = setup();
71+
72+
const fields = [...standardFields, 'fullName', 'custom1', 'custom2'];
73+
74+
const event = mockEvent(true, fields);
75+
76+
const pdf = Readable.from('data');
77+
78+
const testData = mockTestData(
79+
['custom1', 'custom2'],
80+
[
81+
{ custom1: 'short1', custom2: 'short2' },
82+
{ custom1: 'medium1', custom2: 'medium2' },
83+
{ custom1: 'long1', custom2: 'long2' },
84+
]
85+
);
86+
87+
mocks.userDataRepository.get.mockResolvedValueOnce({ testData, pdf });
88+
89+
await app.send(JSON.stringify(event), mocks.sftpClient, baseUploadDir);
90+
91+
expect(mocks.userDataRepository.get).toHaveBeenCalledTimes(1);
92+
expect(mocks.userDataRepository.get).toHaveBeenCalledWith(
93+
owner,
94+
templateId,
95+
pdfVersion,
96+
testDataVersion
97+
);
98+
99+
expect(mocks.batch.buildBatch).toHaveBeenCalledTimes(1);
100+
expect(mocks.batch.buildBatch).toHaveBeenCalledWith(templateId, fields);
101+
});
102+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export function mockTestData<const T extends string>(
2+
parameters: T[],
3+
parameterInput: [Record<T, string>, Record<T, string>, Record<T, string>]
4+
) {
5+
const headers = 'parameter,short example,medium example,long example';
6+
const rows = parameters.map((p) =>
7+
[
8+
`"${p}"`,
9+
`"${parameterInput[0][p]}"`,
10+
`"${parameterInput[1][p]}"`,
11+
`"${parameterInput[2][p]}"`,
12+
].join(',')
13+
);
14+
return [headers, ...rows].join('\n');
15+
}

lambdas/sftp-letters/src/apis/send-handler.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { SQSBatchItemFailure, SQSHandler } from 'aws-lambda';
2-
import { SftpSupplierClientRepository } from '../infra/sftp-supplier-client-repository';
3-
import { Logger, logger } from 'nhs-notify-web-template-management-utils';
4-
import { App } from '../app/send';
1+
import type { SQSBatchItemFailure, SQSHandler } from 'aws-lambda';
2+
import type { SftpSupplierClientRepository } from '../infra/sftp-supplier-client-repository';
3+
import { type Logger, logger } from 'nhs-notify-web-template-management-utils';
4+
import type { App } from '../app/send';
55

66
type Dependencies = {
77
app: App;

lambdas/sftp-letters/src/app/send.ts

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1+
import type { SftpClient } from '../infra/sftp-client';
2+
import type { UserDataRepository } from '../infra/user-data-repository';
3+
import type { Logger } from 'nhs-notify-web-template-management-utils';
4+
import type { Batch } from '../domain/batch';
5+
import type { TemplateRepository } from '../infra/template-repository';
16
import { parseTestPersonalisation } from '../domain/test-data';
27
import { serialise } from '../infra/serialise-csv';
3-
import { SftpClient } from '../infra/sftp-client';
4-
import type { UserDataRepository } from '../infra/user-data-repository';
58
import { z } from 'zod';
69
import path from 'node:path';
710
import { Readable } from 'node:stream';
8-
import { Logger } from 'nhs-notify-web-template-management-utils';
9-
import { Batch } from '../domain/batch';
10-
import { TemplateRepository } from '../infra/template-repository';
1111

1212
export function parseProofingRequest(event: string) {
1313
return z
@@ -66,43 +66,46 @@ export class App {
6666
'template,batch,records,md5'
6767
);
6868

69+
const sftpEnvDir = path.join(baseUploadDir, this.sftpEnvironment);
70+
71+
const templateDir = path.join(sftpEnvDir, 'templates', templateId);
72+
const batchDir = path.join(sftpEnvDir, 'batches', templateId);
73+
74+
const pdfName = `${templateId}.pdf`;
75+
const csvName = `${batch.id}.csv`;
76+
const manifestName = `${batch.id}_MANIFEST.csv`;
77+
78+
const pdfDestination = path.join(templateDir, pdfName);
79+
const batchDestination = path.join(batchDir, csvName);
80+
const manifestDestination = path.join(batchDir, manifestName);
81+
82+
const batchStream = Readable.from(batchCsv);
83+
const manifestStream = Readable.from(manifestCsv);
84+
85+
batchLogger.info('Updating template to SENDING_PROOF');
86+
87+
await this.templateRepository.updateToSendingProof(owner, templateId);
88+
6989
batchLogger.info('Sending PDF');
7090

71-
await sftpClient.put(
72-
userData.pdf,
73-
path.join(
74-
baseUploadDir,
75-
this.sftpEnvironment,
76-
'templates',
77-
`${templateId}.pdf`
78-
)
79-
);
91+
await Promise.all([
92+
sftpClient.mkdir(templateDir, true),
93+
sftpClient.mkdir(batchDir, true),
94+
]);
95+
96+
await sftpClient.put(userData.pdf, pdfDestination);
8097

8198
batchLogger.info('Sending batch');
8299

83-
await sftpClient.put(
84-
Readable.from(batchCsv),
85-
path.join(
86-
baseUploadDir,
87-
this.sftpEnvironment,
88-
'batches',
89-
templateId,
90-
`${batch.id}.csv`
91-
)
92-
);
100+
await sftpClient.put(batchStream, batchDestination);
93101

94102
batchLogger.info('Sending manifest');
95103

96-
await sftpClient.put(
97-
Readable.from(manifestCsv),
98-
path.join(
99-
baseUploadDir,
100-
this.sftpEnvironment,
101-
'batches',
102-
templateId,
103-
`${batch.id}_MANIFEST.csv`
104-
)
105-
);
104+
await sftpClient.put(manifestStream, manifestDestination);
105+
106+
batchLogger.info('Updating template to awaiting proof');
107+
108+
await this.templateRepository.updateToAwaitingProof(owner, templateId);
106109

107110
batchLogger.info('Sent proofing request');
108111
}

lambdas/sftp-letters/src/config/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { defaultConfigReader as reader } from 'nhs-notify-web-template-managemen
33
export function loadConfig() {
44
return {
55
csi: reader.getValue('CSI'),
6+
templatesTableName: reader.getValue('TEMPLATES_TABLE_NAME'),
67
internalBucketName: reader.getValue('INTERNAL_BUCKET_NAME'),
78
defaultSupplier: reader.getValue('DEFAULT_LETTER_SUPPLIER'),
89
sftpEnvironment: reader.getValue('SFTP_ENVIRONMENT'),

lambdas/sftp-letters/src/container-send.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,39 @@ import { logger } from 'nhs-notify-web-template-management-utils';
88
import { randomId } from './infra/ksuid-like-id';
99
import { Batch } from './domain/batch';
1010
import { TemplateRepository } from './infra/template-repository';
11+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
12+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
1113

1214
export function createContainer() {
13-
const { csi, internalBucketName, defaultSupplier, sftpEnvironment, region } =
14-
loadConfig();
15+
const {
16+
csi,
17+
internalBucketName,
18+
defaultSupplier,
19+
sftpEnvironment,
20+
region,
21+
templatesTableName,
22+
} = loadConfig();
1523

1624
const s3Client = new S3Client({ region });
1725

1826
const ssmClient = new SSMClient({ region });
1927

28+
const ddbDocClient = DynamoDBDocumentClient.from(
29+
new DynamoDBClient({ region }),
30+
{
31+
marshallOptions: { removeUndefinedValues: true },
32+
}
33+
);
34+
2035
const userDataRepository = new UserDataRepository(
2136
s3Client,
2237
internalBucketName
2338
);
2439

25-
const templateRepository = new TemplateRepository();
40+
const templateRepository = new TemplateRepository(
41+
ddbDocClient,
42+
templatesTableName
43+
);
2644

2745
const sftpSupplierClientRepository = new SftpSupplierClientRepository(
2846
csi,

lambdas/sftp-letters/src/domain/batch.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,16 @@ export class Batch {
3030
fields: string[],
3131
userTestData?: TestCustomPersonalisation
3232
): BatchDetails {
33-
const date = new Date();
33+
const date = this.getDate();
3434

35-
const id = `${templateId}-${this.getDate().getTime()}_${this.randomId()}`;
35+
const id = `${templateId}-${date.getTime()}_${this.randomId()}`;
36+
const ref = this.clientRef(date);
3637

3738
const header = `clientRef,template,${fields.join(',')}`;
3839

39-
const rows = Array.from({ length: 3 }, (_, i) =>
40-
Object.fromEntries([
41-
['clientRef', this.clientRef()],
40+
const rows = Array.from({ length: 3 }, (_, i) => {
41+
const fieldEntries = [
42+
['clientRef', ref],
4243
['template', templateId],
4344
...fields.map((field) => {
4445
const value = this.fieldValue(
@@ -49,8 +50,10 @@ export class Batch {
4950
);
5051
return [field, value];
5152
}),
52-
])
53-
);
53+
];
54+
55+
return Object.fromEntries(fieldEntries);
56+
});
5457

5558
return { id, rows, header };
5659
}
@@ -70,12 +73,10 @@ export class Batch {
7073
return userData?.[field] ?? '';
7174
}
7275

73-
private clientRef() {
74-
return [
75-
this.randomId(),
76-
this.randomId(),
77-
this.getDate().toString().slice(10),
78-
].join('_');
76+
private clientRef(date: Date) {
77+
return [this.randomId(), this.randomId(), date.toString().slice(10)].join(
78+
'_'
79+
);
7980
}
8081

8182
buildManifest(

lambdas/sftp-letters/src/infra/serialise-csv.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ export const serialise = (
55
header: string
66
): Promise<string> => {
77
const stringifier = stringify(objects, {
8-
// remove any newlines and trim
98
cast: {
109
string(value) {
1110
return value.replaceAll('\n', ' ').trim();
@@ -20,8 +19,8 @@ export const serialise = (
2019
return new Promise((resolve, reject) => {
2120
stringifier.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
2221
stringifier.on('error', (err) => reject(err));
23-
// add header on end
2422
stringifier.on('end', () =>
23+
// headers are not quoted
2524
resolve(`${header}\n${Buffer.concat(chunks).toString('utf8')}`)
2625
);
2726
});

0 commit comments

Comments
 (0)