Skip to content

Commit c68acc2

Browse files
merge feature/CCM-12997 into my branch
2 parents c232df7 + 10ec08f commit c68acc2

30 files changed

Lines changed: 1559 additions & 176 deletions

File tree

.devcontainer/devcontainer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@
8383
},
8484
"mounts": [
8585
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
86-
"source=${localEnv:HOME}/.aws,target=/home/vscode/.aws,type=bind,consistency=cached"
86+
"source=${localEnv:HOME}/.aws,target=/home/vscode/.aws,type=bind,consistency=cached",
87+
"source=${localEnv:HOME}/.npmrc,target=/home/vscode/.npmrc,type=bind,consistency=cached"
8788
],
8889
"name": "Devcontainer",
8990
"postCreateCommand": "scripts/devcontainer/postcreatecommand.sh"

.github/workflows/stage-1-commit.yaml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ jobs:
7272
needs: detect-terraform-changes
7373
if: needs.detect-terraform-changes.outputs.terraform_changed == 'true'
7474
permissions:
75-
contents: write
75+
contents: write
7676
steps:
7777
- name: "Checkout code"
7878
uses: actions/checkout@v5
@@ -164,8 +164,6 @@ jobs:
164164
registry-url: 'https://npm.pkg.github.com'
165165
- name: "Setup ASDF"
166166
uses: asdf-vm/actions/setup@v4
167-
- name: "Perform Setup"
168-
uses: ./.github/actions/setup
169167
- name: "Trivy Scan"
170168
uses: ./.github/actions/trivy
171169
count-lines-of-code:
@@ -288,7 +286,7 @@ jobs:
288286
uses: actions/setup-node@v4
289287
with:
290288
node-version: ${{ inputs.nodejs_version }}
291-
registry-url: 'https://npm.pkg.github.com'
289+
registry-url: "https://npm.pkg.github.com"
292290

293291
- name: check if local version differs from latest published version
294292
id: check-version

.github/workflows/stage-2-test.yaml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ env:
3838

3939
permissions:
4040
id-token: write # This is required for requesting the JWT
41-
contents: read # This is required for actions/checkout
41+
contents: read # This is required for actions/checkout
4242
packages: read # This is required for downloading from GitHub Package Registry
4343

4444
jobs:
@@ -49,6 +49,11 @@ jobs:
4949
steps:
5050
- name: "Checkout code"
5151
uses: actions/checkout@v5
52+
- name: Setup NodeJS
53+
uses: actions/setup-node@v4
54+
with:
55+
node-version: ${{ inputs.nodejs_version }}
56+
registry-url: "https://npm.pkg.github.com"
5257
- name: "Cache node_modules"
5358
uses: actions/cache@v4
5459
with:
@@ -73,6 +78,11 @@ jobs:
7378
steps:
7479
- name: "Checkout code"
7580
uses: actions/checkout@v5
81+
- name: Setup NodeJS
82+
uses: actions/setup-node@v4
83+
with:
84+
node-version: ${{ inputs.nodejs_version }}
85+
registry-url: "https://npm.pkg.github.com"
7686
- name: "Cache node_modules"
7787
uses: actions/cache@v4
7888
with:
@@ -142,6 +152,11 @@ jobs:
142152
steps:
143153
- name: "Checkout code"
144154
uses: actions/checkout@v5
155+
- name: Setup NodeJS
156+
uses: actions/setup-node@v4
157+
with:
158+
node-version: ${{ inputs.nodejs_version }}
159+
registry-url: "https://npm.pkg.github.com"
145160
- name: "Cache node_modules"
146161
uses: actions/cache@v4
147162
with:
@@ -168,6 +183,11 @@ jobs:
168183
steps:
169184
- name: "Checkout code"
170185
uses: actions/checkout@v5
186+
- name: Setup NodeJS
187+
uses: actions/setup-node@v4
188+
with:
189+
node-version: ${{ inputs.nodejs_version }}
190+
registry-url: "https://npm.pkg.github.com"
171191
- name: "Cache node_modules"
172192
uses: actions/cache@v4
173193
with:

infrastructure/terraform/components/api/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ No requirements.
2424
| <a name="input_group"></a> [group](#input\_group) | The group variables are being inherited from (often synonmous with account short-name) | `string` | n/a | yes |
2525
| <a name="input_kms_deletion_window"></a> [kms\_deletion\_window](#input\_kms\_deletion\_window) | When a kms key is deleted, how long should it wait in the pending deletion state? | `string` | `"30"` | no |
2626
| <a name="input_letter_table_ttl_hours"></a> [letter\_table\_ttl\_hours](#input\_letter\_table\_ttl\_hours) | Number of hours to set as TTL on letters table | `number` | `24` | no |
27+
| <a name="input_letter_variant_map"></a> [letter\_variant\_map](#input\_letter\_variant\_map) | n/a | `map(object({ supplierId = string, specId = string }))` | <pre>{<br/> "lv1": {<br/> "specId": "spec1",<br/> "supplierId": "supplier1"<br/> },<br/> "lv2": {<br/> "specId": "spec2",<br/> "supplierId": "supplier1"<br/> },<br/> "lv3": {<br/> "specId": "spec3",<br/> "supplierId": "supplier2"<br/> }<br/>}</pre> | no |
2728
| <a name="input_log_level"></a> [log\_level](#input\_log\_level) | The log level to be used in lambda functions within the component. Any log with a lower severity than the configured value will not be logged: https://docs.python.org/3/library/logging.html#levels | `string` | `"INFO"` | no |
2829
| <a name="input_log_retention_in_days"></a> [log\_retention\_in\_days](#input\_log\_retention\_in\_days) | The retention period in days for the Cloudwatch Logs events to be retained, default of 0 is indefinite | `number` | `0` | no |
2930
| <a name="input_manually_configure_mtls_truststore"></a> [manually\_configure\_mtls\_truststore](#input\_manually\_configure\_mtls\_truststore) | Manually manage the truststore used for API Gateway mTLS (e.g. for prod environment) | `bool` | `false` | no |

infrastructure/terraform/components/api/module_lambda_upsert_letter.tf

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module "upsert_letter" {
22
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-lambda.zip"
33

4-
function_name = "upsert-letter"
4+
function_name = "upsert_letter"
55
description = "Update or Insert the letter data in the letters table"
66

77
aws_account_id = var.aws_account_id
@@ -22,7 +22,7 @@ module "upsert_letter" {
2222
function_code_base_path = local.aws_lambda_functions_dir_path
2323
function_code_dir = "upsert-letter/dist"
2424
function_include_common = true
25-
handler_function_name = "handler"
25+
handler_function_name = "upsertLetterHandler"
2626
runtime = "nodejs22.x"
2727
memory = 128
2828
timeout = 29
@@ -35,7 +35,9 @@ module "upsert_letter" {
3535
log_destination_arn = local.destination_arn
3636
log_subscription_role_arn = local.acct.log_subscription_role_arn
3737

38-
lambda_env_vars = merge(local.common_lambda_env_vars, {})
38+
lambda_env_vars = merge(local.common_lambda_env_vars, {
39+
VARIANT_MAP = jsonencode(var.letter_variant_map)
40+
})
3941
}
4042

4143
data "aws_iam_policy_document" "upsert_letter_lambda" {
@@ -58,7 +60,10 @@ data "aws_iam_policy_document" "upsert_letter_lambda" {
5860
effect = "Allow"
5961

6062
actions = [
61-
"dynamodb:PutItem"
63+
"dynamodb:PutItem",
64+
"dynamodb:GetItem",
65+
"dynamodb:Query",
66+
"dynamodb:UpdateItem"
6267
]
6368

6469
resources = [

infrastructure/terraform/components/api/variables.tf

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,12 @@ variable "disable_gateway_execute_endpoint" {
146146
description = "Disable the execution endpoint for the API Gateway"
147147
default = true
148148
}
149+
150+
variable "letter_variant_map" {
151+
type = map(object({ supplierId = string, specId = string }))
152+
default = {
153+
"lv1" = { supplierId = "supplier1", specId = "spec1" },
154+
"lv2" = { supplierId = "supplier1", specId = "spec2" },
155+
"lv3" = { supplierId = "supplier2", specId = "spec3" }
156+
}
157+
}

internal/datastore/src/__test__/letter-repository.test.ts

Lines changed: 80 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,34 @@ import {
77
setupDynamoDBContainer,
88
} from "./db";
99
import { LetterRepository } from "../letter-repository";
10-
import { Letter } from "../types";
10+
import { InsertLetter, Letter, UpdateLetter } from "../types";
1111
import { LogStream, createTestLogger } from "./logs";
12-
import { LetterDto } from "../../../../lambdas/api-handler/src/contracts/letters";
1312

1413
function createLetter(
1514
supplierId: string,
1615
letterId: string,
1716
status: Letter["status"] = "PENDING",
18-
): Omit<Letter, "ttl" | "supplierStatus" | "supplierStatusSk"> {
17+
): InsertLetter {
18+
const now = new Date().toISOString();
1919
return {
2020
id: letterId,
2121
supplierId,
2222
specificationId: "specification1",
2323
groupId: "group1",
2424
url: `s3://bucket/${letterId}.pdf`,
2525
status,
26-
createdAt: new Date().toISOString(),
27-
updatedAt: new Date().toISOString(),
26+
createdAt: now,
27+
updatedAt: now,
28+
source: "/data-plane/letter-rendering/pdf",
29+
subject: `client/1/letter-request/${letterId}`,
2830
};
2931
}
3032

33+
function assertDateBetween(date: number, before: number, after: number) {
34+
expect(date).toBeGreaterThanOrEqual(before);
35+
expect(date).toBeLessThanOrEqual(after);
36+
}
37+
3138
// Database tests can take longer, especially with setup and teardown
3239
jest.setTimeout(30_000);
3340

@@ -66,18 +73,46 @@ describe("LetterRepository", () => {
6673
expect(letter.status).toBe(status);
6774
}
6875

76+
function assertTtl(ttl: number, before: number, after: number) {
77+
const expectedLower = Math.floor(
78+
before / 1000 + 60 * 60 * db.config.lettersTtlHours,
79+
);
80+
const expectedUpper = Math.floor(
81+
after / 1000 + 60 * 60 * db.config.lettersTtlHours,
82+
);
83+
expect(ttl).toBeGreaterThanOrEqual(expectedLower);
84+
expect(ttl).toBeLessThanOrEqual(expectedUpper);
85+
}
86+
6987
test("adds a letter to the database", async () => {
7088
const supplierId = "supplier1";
7189
const letterId = "letter1";
7290

91+
const before = Date.now();
92+
7393
await letterRepository.putLetter(createLetter(supplierId, letterId));
7494

95+
const after = Date.now();
96+
7597
const letter = await letterRepository.getLetterById(supplierId, letterId);
7698
expect(letter).toBeDefined();
7799
expect(letter.id).toBe(letterId);
78100
expect(letter.supplierId).toBe(supplierId);
101+
assertDateBetween(new Date(letter.createdAt).valueOf(), before, after);
102+
assertDateBetween(new Date(letter.updatedAt).valueOf(), before, after);
103+
assertDateBetween(
104+
new Date(letter.supplierStatusSk).valueOf(),
105+
before,
106+
after,
107+
);
108+
expect(letter.supplierStatus).toBe("supplier1#PENDING");
109+
expect(letter.url).toBe("s3://bucket/letter1.pdf");
110+
expect(letter.specificationId).toBe("specification1");
111+
expect(letter.groupId).toBe("group1");
79112
expect(letter.reasonCode).toBeUndefined();
80113
expect(letter.reasonText).toBeUndefined();
114+
expect(letter.subject).toBe(`client/1/letter-request/${letterId}`);
115+
assertTtl(letter.ttl, before, after);
81116
});
82117

83118
test("fetches a letter by id", async () => {
@@ -90,6 +125,9 @@ describe("LetterRepository", () => {
90125
specificationId: "specification1",
91126
groupId: "group1",
92127
status: "PENDING",
128+
url: "s3://bucket/letter1.pdf",
129+
source: "/data-plane/letter-rendering/pdf",
130+
subject: "client/1/letter-request/letter1",
93131
}),
94132
);
95133
});
@@ -122,18 +160,18 @@ describe("LetterRepository", () => {
122160
});
123161

124162
test("updates a letter's status in the database", async () => {
125-
const letter = createLetter("supplier1", "letter1", "PENDING");
163+
const letter = createLetter("supplier1", "letter1");
126164
await letterRepository.putLetter(letter);
127165
await checkLetterStatus("supplier1", "letter1", "PENDING");
128166

129-
const letterDto: LetterDto = {
167+
const updateLetter: UpdateLetter = {
130168
id: "letter1",
131169
supplierId: "supplier1",
132170
status: "REJECTED",
133171
reasonCode: "R01",
134172
reasonText: "Reason text",
135173
};
136-
await letterRepository.updateLetterStatus(letterDto);
174+
await letterRepository.updateLetterStatus(updateLetter);
137175

138176
const updatedLetter = await letterRepository.getLetterById(
139177
"supplier1",
@@ -147,9 +185,7 @@ describe("LetterRepository", () => {
147185
test("updates a letter's updatedAt date", async () => {
148186
jest.useFakeTimers();
149187
jest.setSystemTime(new Date(2020, 1, 1));
150-
await letterRepository.putLetter(
151-
createLetter("supplier1", "letter1", "PENDING"),
152-
);
188+
await letterRepository.putLetter(createLetter("supplier1", "letter1"));
153189
const originalLetter = await letterRepository.getLetterById(
154190
"supplier1",
155191
"letter1",
@@ -159,7 +195,7 @@ describe("LetterRepository", () => {
159195
// Month is zero-indexed in JavaScript Date
160196
// Day is one-indexed
161197
jest.setSystemTime(new Date(2020, 1, 2));
162-
const letterDto: LetterDto = {
198+
const letterDto: UpdateLetter = {
163199
id: "letter1",
164200
supplierId: "supplier1",
165201
status: "DELIVERED",
@@ -175,13 +211,13 @@ describe("LetterRepository", () => {
175211
});
176212

177213
test("can't update a letter that does not exist", async () => {
178-
const letterDto: LetterDto = {
214+
const updateLetter: UpdateLetter = {
179215
id: "letter1",
180216
supplierId: "supplier1",
181217
status: "DELIVERED",
182218
};
183219
await expect(
184-
letterRepository.updateLetterStatus(letterDto),
220+
letterRepository.updateLetterStatus(updateLetter),
185221
).rejects.toThrow(
186222
"Letter with id letter1 not found for supplier supplier1",
187223
);
@@ -193,13 +229,13 @@ describe("LetterRepository", () => {
193229
lettersTableName: "nonexistent-table",
194230
});
195231

196-
const letterDto: LetterDto = {
232+
const updateLetter: UpdateLetter = {
197233
id: "letter1",
198234
supplierId: "supplier1",
199235
status: "DELIVERED",
200236
};
201237
await expect(
202-
misconfiguredRepository.updateLetterStatus(letterDto),
238+
misconfiguredRepository.updateLetterStatus(updateLetter),
203239
).rejects.toThrow("Cannot do operations on a non-existent table");
204240
});
205241

@@ -238,12 +274,12 @@ describe("LetterRepository", () => {
238274
);
239275
expect(pendingLetters.letters).toHaveLength(2);
240276

241-
const letterDto: LetterDto = {
277+
const updateLetter: UpdateLetter = {
242278
id: "letter1",
243279
supplierId: "supplier1",
244280
status: "DELIVERED",
245281
};
246-
await letterRepository.updateLetterStatus(letterDto);
282+
await letterRepository.updateLetterStatus(updateLetter);
247283
const remainingLetters = await letterRepository.getLettersByStatus(
248284
"supplier1",
249285
"PENDING",
@@ -313,7 +349,7 @@ describe("LetterRepository", () => {
313349
url: "s3://bucket/invalid-letter.pdf",
314350
status: "PENDING",
315351
supplierStatus: "supplier1#PENDING",
316-
supplierStatusSk: Date.now().toString(),
352+
supplierStatusSk: new Date().toISOString(),
317353
createdAt: new Date().toISOString(),
318354
updatedAt: new Date().toISOString(),
319355
},
@@ -405,15 +441,36 @@ describe("LetterRepository", () => {
405441
});
406442

407443
test("should batch write letters to the database", async () => {
408-
const letters = [
444+
const before = Date.now();
445+
446+
await letterRepository.putLetterBatch([
409447
createLetter("supplier1", "letter1"),
410448
createLetter("supplier1", "letter2"),
411449
createLetter("supplier1", "letter3"),
412-
];
450+
]);
413451

414-
await letterRepository.putLetterBatch(letters);
452+
const after = Date.now();
453+
454+
const letter = await letterRepository.getLetterById("supplier1", "letter1");
455+
expect(letter).toBeDefined();
456+
expect(letter.id).toBe("letter1");
457+
expect(letter.supplierId).toBe("supplier1");
458+
assertDateBetween(new Date(letter.createdAt).valueOf(), before, after);
459+
assertDateBetween(new Date(letter.updatedAt).valueOf(), before, after);
460+
assertDateBetween(
461+
new Date(letter.supplierStatusSk).valueOf(),
462+
before,
463+
after,
464+
);
465+
expect(letter.supplierStatus).toBe("supplier1#PENDING");
466+
expect(letter.url).toBe("s3://bucket/letter1.pdf");
467+
expect(letter.specificationId).toBe("specification1");
468+
expect(letter.groupId).toBe("group1");
469+
expect(letter.reasonCode).toBeUndefined();
470+
expect(letter.reasonText).toBeUndefined();
471+
expect(letter.subject).toBe("client/1/letter-request/letter1");
472+
assertTtl(letter.ttl, before, after);
415473

416-
await checkLetterStatus("supplier1", "letter1", "PENDING");
417474
await checkLetterStatus("supplier1", "letter2", "PENDING");
418475
await checkLetterStatus("supplier1", "letter3", "PENDING");
419476
});

0 commit comments

Comments
 (0)