Skip to content

Commit aaf44a9

Browse files
new tests
1 parent 2a91bed commit aaf44a9

13 files changed

Lines changed: 800 additions & 405 deletions

.github/actions/test-types.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
[
2-
"component"
2+
"component",
3+
"sandbox"
34
]

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ config:: _install-dependencies version # Configure development environment (main
104104
test-component:
105105
(cd tests && npm install && npm run test:component)
106106

107+
test-sandbox:
108+
(cd tests && npm install && npm run test:sandbox)
109+
107110
test-performance:
108111
(cd tests && npm install && npm run test:performance)
109112

@@ -132,7 +135,7 @@ TEST_CMD := APIGEE_ACCESS_TOKEN="$(APIGEE_ACCESS_TOKEN)" \
132135
--color=yes \
133136
-n 4 \
134137
--api-name=nhs-notify-supplier \
135-
--proxy-name="$(PROXY_NAME)" \
138+
--proxy-name="nhs-notify-supplier--internal-dev--nhs-notify-supplier" \
136139
-s \
137140
--reruns 5 \
138141
--reruns-delay 5 \

package-lock.json

Lines changed: 453 additions & 396 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { expect, test } from "@playwright/test";
2+
import { sendSnsEvent } from "tests/helpers/send-sns-event";
3+
import { createPreparedV1Event } from "tests/helpers/event-fixtures";
4+
import { randomUUID } from "node:crypto";
5+
import { logger } from "tests/helpers/pino-logger";
6+
import { createValidRequestHeaders } from "tests/constants/request-headers";
7+
import getRestApiGatewayBaseUrl from "tests/helpers/aws-gateway-helper";
8+
import { SUPPLIER_LETTERS } from "tests/constants/api-constants";
9+
import {
10+
pollSupplierAllocatorLogForResolvedSpec,
11+
pollUpsertLetterLogForError,
12+
} from "tests/helpers/aws-cloudwatch-helper";
13+
14+
let baseUrl: string;
15+
16+
test.beforeAll(async () => {
17+
baseUrl = await getRestApiGatewayBaseUrl();
18+
});
19+
20+
test.describe("Event Subscription SNS Tests", () => {
21+
test("Verify that the publish event to nhs-main-supapi-eventsub topic inserts data into db", async ({
22+
request,
23+
}) => {
24+
const domainId = randomUUID();
25+
logger.info(`Testing event subscription with domainId: ${domainId}`);
26+
const preparedEvent = createPreparedV1Event({ domainId });
27+
const response = await sendSnsEvent(preparedEvent);
28+
const RETRY_DELAY_MS = 30_000;
29+
30+
expect(response.MessageId).toBeTruthy();
31+
32+
// poll supplier allocator to check if supplier has been allocated
33+
const message = await pollSupplierAllocatorLogForResolvedSpec(domainId);
34+
const supplierAllocatorLog = JSON.parse(message) as {
35+
msg?: { supplierSpec?: { supplierId?: string } };
36+
};
37+
const supplierId = supplierAllocatorLog.msg?.supplierSpec?.supplierId;
38+
39+
logger.info(
40+
`Supplier ${supplierId} allocated for domainId ${domainId} in supplier allocator lambda`,
41+
);
42+
if (!supplierId) {
43+
throw new Error("supplierId was not found in supplier allocator log");
44+
}
45+
46+
const headers = createValidRequestHeaders(supplierId);
47+
let statusCode = 0;
48+
let letterStatus: string | undefined;
49+
50+
for (let attempt = 1; attempt <= 3; attempt++) {
51+
const getLetterResponse = await request.get(
52+
`${baseUrl}/${SUPPLIER_LETTERS}/${domainId}`,
53+
{
54+
headers,
55+
},
56+
);
57+
58+
statusCode = getLetterResponse.status();
59+
const responseBody = (await getLetterResponse.json()) as {
60+
data?: { attributes?: { status?: string } };
61+
};
62+
letterStatus = responseBody.data?.attributes?.status;
63+
64+
if (statusCode === 200 && letterStatus === "PENDING") {
65+
logger.info(
66+
`Attempt ${attempt}: Received status code ${statusCode} for domainId: ${domainId}`,
67+
);
68+
break;
69+
}
70+
71+
if (attempt < 3) {
72+
logger.info(
73+
`Attempt ${attempt}: Received status code ${statusCode} for domainId: ${domainId}. Retrying after ${RETRY_DELAY_MS / 1000} seconds...`,
74+
);
75+
await new Promise((resolve) => {
76+
setTimeout(resolve, RETRY_DELAY_MS); // Wait for 30 seconds before the next attempt
77+
});
78+
}
79+
}
80+
expect(statusCode).toBe(200);
81+
expect(letterStatus).toBe("PENDING");
82+
});
83+
84+
test("Verify that the publish event with 'CANCELLED' status throws error", async ({
85+
request,
86+
}) => {
87+
const domainId = randomUUID();
88+
logger.info(`Testing event subscription with domainId: ${domainId}`);
89+
const preparedEvent = createPreparedV1Event({
90+
domainId,
91+
status: "CANCELLED",
92+
});
93+
const response = await sendSnsEvent(preparedEvent);
94+
95+
expect(response.MessageId).toBeTruthy();
96+
97+
// poll supplier allocator to check if supplier has been allocated
98+
const message = await pollSupplierAllocatorLogForResolvedSpec(domainId);
99+
const supplierAllocatorLog = JSON.parse(message) as {
100+
msg?: { supplierSpec?: { supplierId?: string } };
101+
};
102+
const supplierId = supplierAllocatorLog.msg?.supplierSpec?.supplierId;
103+
104+
logger.info(
105+
`Supplier ${supplierId} allocated for domainId ${domainId} in supplier allocator lambda`,
106+
);
107+
if (!supplierId) {
108+
throw new Error("supplierId was not found in supplier allocator log");
109+
}
110+
111+
const headers = createValidRequestHeaders(supplierId);
112+
113+
const getLetterResponse = await request.get(
114+
`${baseUrl}/${SUPPLIER_LETTERS}/${domainId}`,
115+
{
116+
headers,
117+
},
118+
);
119+
120+
expect(getLetterResponse.status()).toBe(500);
121+
await pollUpsertLetterLogForError(
122+
"Message did not match an expected schema",
123+
);
124+
});
125+
126+
test("Verify that the duplicate event throws an error", async () => {
127+
const domainId = randomUUID();
128+
logger.info(`Testing event subscription with domainId: ${domainId}`);
129+
const preparedEvent = createPreparedV1Event({
130+
domainId,
131+
status: "PREPARED",
132+
});
133+
const response = await sendSnsEvent(preparedEvent);
134+
135+
expect(response.MessageId).toBeTruthy();
136+
137+
// poll supplier allocator to check if supplier has been allocated
138+
const message = await pollSupplierAllocatorLogForResolvedSpec(domainId);
139+
const supplierAllocatorLog = JSON.parse(message) as {
140+
msg?: { supplierSpec?: { supplierId?: string } };
141+
};
142+
const supplierId = supplierAllocatorLog.msg?.supplierSpec?.supplierId;
143+
144+
logger.info(
145+
`Supplier ${supplierId} allocated for domainId ${domainId} in supplier allocator lambda`,
146+
);
147+
if (!supplierId) {
148+
throw new Error("supplierId was not found in supplier allocator log");
149+
}
150+
151+
// send same event again to simulate duplicate event
152+
const duplicateResponse = await sendSnsEvent(preparedEvent);
153+
expect(duplicateResponse.MessageId).toBeTruthy();
154+
155+
// poll supplier upsert to check if duplicate event was processed
156+
await pollUpsertLetterLogForError(
157+
`Letter with id ${domainId} already exists for supplier ${supplierId}"`,
158+
);
159+
});
160+
});

tests/constants/api-constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export const MI_ENDPOINT = "mi";
1010
export const SUPPLIERTABLENAME = `nhs-${envName}-supapi-suppliers`;
1111
export const UPSERT_LETTER_LAMBDA_ARN = `arn:aws:lambda:eu-west-2:820178564574:function:nhs-${envName}-supapi-upsertletter`;
1212
export const DATA = "data";
13+
export const EVENT_SUBSCRIPTION_TOPIC_NAME = "nhs-main-supapi-eventsub";

tests/constants/request-headers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ export function createHeaderWithNoCorrelationId(): RequestHeaders {
4242
return requestHeaders;
4343
}
4444

45-
export function createValidRequestHeaders(): RequestHeaders {
45+
export function createValidRequestHeaders(supplierId?: string): RequestHeaders {
4646
let requestHeaders: RequestHeaders;
4747
requestHeaders = {
48-
"NHSD-Supplier-ID": SUPPLIERID,
48+
"NHSD-Supplier-ID": supplierId || SUPPLIERID,
4949
"NHSD-Correlation-ID": "12345",
5050
"X-Request-ID": "requestId1",
5151
};
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import {
2+
CloudWatchLogsClient,
3+
FilterLogEventsCommand,
4+
} from "@aws-sdk/client-cloudwatch-logs";
5+
import { envName } from "tests/constants/api-constants";
6+
7+
const sleep = (ms: number) =>
8+
new Promise((resolve) => {
9+
setTimeout(resolve, ms);
10+
});
11+
12+
export async function pollSupplierAllocatorLogForResolvedSpec(
13+
domainId?: string,
14+
): Promise<string> {
15+
const intervalMs = 5000;
16+
const startTimeMs = Date.now() - 5 * 60_000;
17+
const timeoutMs = 120_000;
18+
19+
const client = new CloudWatchLogsClient({});
20+
const logGroupName = `/aws/lambda/nhs-${envName}-supapi-supplier-allocator`;
21+
const deadline = Date.now() + timeoutMs;
22+
23+
while (Date.now() < deadline) {
24+
const response = await client.send(
25+
new FilterLogEventsCommand({
26+
logGroupName,
27+
startTime: startTimeMs,
28+
interleaved: true,
29+
limit: 100,
30+
filterPattern: domainId
31+
? `"Sending message to upsert letter queue" "${domainId}"`
32+
: `"Sending message to upsert letter queue"`,
33+
}),
34+
);
35+
36+
const foundEvent = (response.events ?? []).find((event) => {
37+
const message = event.message ?? "";
38+
return (
39+
message.includes(
40+
'"description":"Sending message to upsert letter queue"',
41+
) &&
42+
(!domainId || message.includes(domainId))
43+
);
44+
});
45+
46+
if (foundEvent?.message) {
47+
return foundEvent.message;
48+
}
49+
50+
await sleep(intervalMs);
51+
}
52+
53+
throw new Error(
54+
`Timed out waiting for resolved supplier spec log in ${logGroupName}`,
55+
);
56+
}
57+
58+
export async function pollUpsertLetterLogForError(
59+
msgToCheck: string,
60+
): Promise<string> {
61+
const intervalMs = 5000;
62+
const startTimeMs = Date.now() - 5 * 60_000;
63+
const timeoutMs = 120_000;
64+
65+
const client = new CloudWatchLogsClient({});
66+
const logGroupName = `/aws/lambda/nhs-${envName}-supapi-upsertletter`;
67+
const deadline = Date.now() + timeoutMs;
68+
69+
while (Date.now() < deadline) {
70+
const response = await client.send(
71+
new FilterLogEventsCommand({
72+
logGroupName,
73+
startTime: startTimeMs,
74+
interleaved: true,
75+
limit: 100,
76+
filterPattern: `"Error processing upsert of record"`,
77+
}),
78+
);
79+
80+
const foundEvent = (response.events ?? []).find((event) => {
81+
const message = event.message ?? "";
82+
return (
83+
message.includes('"description":"Error processing upsert of record"') &&
84+
(message.includes(`"message":"${msgToCheck}`) ||
85+
message.includes(`"message": "${msgToCheck}`))
86+
);
87+
});
88+
89+
if (foundEvent?.message) {
90+
return foundEvent.message;
91+
}
92+
93+
await sleep(intervalMs);
94+
}
95+
96+
throw new Error(
97+
`Timed out waiting for resolved supplier spec log in ${logGroupName}`,
98+
);
99+
}

tests/helpers/event-fixtures.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export function createPreparedV1Event(overrides: Record<string, unknown> = {}) {
2+
const now = new Date().toISOString();
3+
4+
return {
5+
specversion: "1.0",
6+
id: (overrides.id as string) ?? "7b9a03ca-342a-4150-b56b-989109c45613",
7+
source: "/data-plane/letter-rendering/test",
8+
subject: "client/client1/letter-request/letterRequest1",
9+
type: "uk.nhs.notify.letter-rendering.letter-request.prepared.v1",
10+
time: now,
11+
dataschema:
12+
"https://notify.nhs.uk/cloudevents/schemas/letter-rendering/letter-request.prepared.1.0.0.schema.json",
13+
dataschemaversion: "1.0.0",
14+
data: {
15+
domainId:
16+
(overrides.domainId as string) ??
17+
"fe658e11-0ffc-44f4-8ad6-0fafe75bfeee",
18+
letterVariantId: "digitrials-aspiring",
19+
requestId: "request1",
20+
requestItemId: "requestItem1",
21+
requestItemPlanId: "requestItemPlan1",
22+
clientId: "client1",
23+
campaignId: "campaign1",
24+
templateId: "template1",
25+
url: (overrides.url as string) ?? "s3://letterDataBucket/letter1.pdf",
26+
sha256Hash:
27+
"3a7bd3e2360a3d29eea436fcfb7e44c735d117c8f2f1d2d1e4f6e8f7e6e8f7e6",
28+
createdAt: now,
29+
pageCount: 1,
30+
status: (overrides.status as string) ?? "PREPARED",
31+
},
32+
traceparent: "00-0af7651916cd43dd8448eb211c803191-b7ad6b7169203331-01",
33+
recordedtime: now,
34+
severitynumber: 2,
35+
severitytext: "INFO",
36+
plane: "data",
37+
};
38+
}

tests/helpers/send-sns-event.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {
2+
CreateTopicCommand,
3+
MessageAttributeValue,
4+
PublishCommand,
5+
PublishCommandOutput,
6+
} from "@aws-sdk/client-sns";
7+
import { snsClient } from "tests/helpers/aws-sns-helper";
8+
import { EVENT_SUBSCRIPTION_TOPIC_NAME } from "tests/constants/api-constants";
9+
10+
export type SnsEventMessage = Record<string, unknown> | string;
11+
12+
export async function sendSnsEvent(
13+
message: SnsEventMessage,
14+
messageAttributes?: Record<string, MessageAttributeValue>,
15+
): Promise<PublishCommandOutput> {
16+
const { TopicArn } = await snsClient.send(
17+
new CreateTopicCommand({
18+
Name: EVENT_SUBSCRIPTION_TOPIC_NAME,
19+
}),
20+
);
21+
22+
if (!TopicArn) {
23+
throw new Error(
24+
`Failed to resolve SNS topic ARN for ${EVENT_SUBSCRIPTION_TOPIC_NAME}`,
25+
);
26+
}
27+
28+
return snsClient.send(
29+
new PublishCommand({
30+
TopicArn,
31+
Message: typeof message === "string" ? message : JSON.stringify(message),
32+
...(messageAttributes ? { MessageAttributes: messageAttributes } : {}),
33+
}),
34+
);
35+
}

tests/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
"zod": "^4.1.11"
2929
},
3030
"description": "",
31+
"devDependencies": {
32+
"@aws-sdk/client-cloudwatch-logs": "^3.1003.0"
33+
},
3134
"keywords": [],
3235
"license": "ISC",
3336
"main": "index.js",

0 commit comments

Comments
 (0)