Skip to content

Commit d7ec73f

Browse files
letter queue tests
1 parent 3820d92 commit d7ec73f

9 files changed

Lines changed: 267 additions & 5 deletions

File tree

tests/component-tests/apiGateway-tests/testCases/update-letter-status.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ export type PatchMessageResponseBody = {
2828
};
2929
};
3030

31-
export function patchRequestHeaders(): RequestHeaders {
31+
export function patchRequestHeaders(supplierId?: string): RequestHeaders {
3232
return {
3333
headerauth1: process.env.HEADERAUTH || "",
34-
"NHSD-Supplier-ID": SUPPLIERID,
34+
"NHSD-Supplier-ID": supplierId ?? SUPPLIERID,
3535
"NHSD-Correlation-ID": "12344",
3636
"X-Request-ID": "requestId1",
3737
};
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { expect, test } from "@playwright/test";
2+
import { randomUUID } from "node:crypto";
3+
import {
4+
createPreparedEventBatchWithSameDomainId,
5+
createPreparedV1Event,
6+
} from "tests/helpers/event-fixtures";
7+
import { logger } from "tests/helpers/pino-logger";
8+
import { sendSnsBatchEvent, sendSnsEvent } from "tests/helpers/send-sns-event";
9+
import { supplierIdFromSupplierAllocatorLog } from "tests/helpers/aws-cloudwatch-helper";
10+
import getRestApiGatewayBaseUrl from "tests/helpers/aws-gateway-helper";
11+
import { SUPPLIER_LETTERS } from "tests/constants/api-constants";
12+
import { supplierDataSetup } from "tests/helpers/suppliers-setup-helper";
13+
import { checkLetterQueueTable } from "tests/helpers/generate-fetch-test-data";
14+
import {
15+
patchRequestHeaders,
16+
patchValidRequestBody,
17+
} from "../apiGateway-tests/testCases/update-letter-status";
18+
19+
let baseUrl: string;
20+
21+
test.beforeAll(async () => {
22+
baseUrl = await getRestApiGatewayBaseUrl();
23+
});
24+
25+
test.describe("Letter Queue Tests", () => {
26+
test.setTimeout(180_000);
27+
28+
test("Verify that the letter queue operation inserts data into the letter queue table for pending letters", async ({
29+
request,
30+
}) => {
31+
const letterId = randomUUID();
32+
const status = "ACCEPTED";
33+
34+
logger.info(`Sending event with domainId: ${letterId}`);
35+
const preparedEvent = createPreparedV1Event({ domainId: letterId });
36+
const response = await sendSnsEvent(preparedEvent);
37+
38+
expect(response.MessageId).toBeTruthy();
39+
40+
const supplierId = await supplierIdFromSupplierAllocatorLog(letterId);
41+
42+
await supplierDataSetup(supplierId);
43+
44+
const [letterQueue, count] = await checkLetterQueueTable(
45+
supplierId,
46+
letterId,
47+
);
48+
expect(letterQueue).toBe(true);
49+
expect(count).toBe(1);
50+
51+
// update the letter status to ACCEPTED and verify if letter queue table is cleaned up
52+
53+
const headers = patchRequestHeaders(supplierId);
54+
const body = patchValidRequestBody(letterId, status);
55+
56+
const patchResponse = await request.patch(
57+
`${baseUrl}/${SUPPLIER_LETTERS}/${letterId}`,
58+
{
59+
headers,
60+
data: body,
61+
},
62+
);
63+
expect(patchResponse.status()).toBe(202);
64+
logger.info(
65+
`Updated letter status to ${status} for letterId ${letterId}, now polling letter queue table for cleanup confirmation`,
66+
);
67+
68+
const [letterQueueAfterUpdate, countAfterUpdate] =
69+
await checkLetterQueueTable(supplierId, letterId, true);
70+
expect(letterQueueAfterUpdate).toBe(true);
71+
expect(countAfterUpdate).toBeUndefined();
72+
});
73+
74+
test("Verify if the only one entry is inserted in the letter queue table for a batch of events with the same letterId", async () => {
75+
const letterId = randomUUID();
76+
const eventBatch = createPreparedEventBatchWithSameDomainId({
77+
domainId: letterId,
78+
});
79+
logger.info(
80+
`Sending batch event with ${eventBatch.length} events ${letterId}`,
81+
);
82+
const response = await sendSnsBatchEvent(
83+
eventBatch.map((event) => ({
84+
id: event.id,
85+
message: event,
86+
})),
87+
);
88+
expect(response.Successful).toBeTruthy();
89+
90+
const supplierId = await supplierIdFromSupplierAllocatorLog(letterId);
91+
92+
logger.info(
93+
`Verifying if only one entry is inserted in the letter queue table for the batch of events with same letterId ${letterId}`,
94+
);
95+
const [letterExists, itemCount] = await checkLetterQueueTable(
96+
supplierId,
97+
letterId,
98+
);
99+
expect(letterExists).toBe(true);
100+
expect(itemCount).toBe(1);
101+
});
102+
});

tests/config/main.config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getReporters } from "./reporters";
55

66
const localConfig: PlaywrightTestConfig = {
77
...baseConfig,
8-
globalSetup: path.resolve(__dirname, "./global-setup.ts"),
8+
//globalSetup: path.resolve(__dirname, "./global-setup.ts"),
99
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
1010
reporter: getReporters("api-test"),
1111
projects: [
@@ -20,6 +20,11 @@ const localConfig: PlaywrightTestConfig = {
2020
testMatch: "**/*.spec.ts",
2121
dependencies: ["apiGateway-tests"],
2222
},
23+
{
24+
name: "letterQueue-tests",
25+
testDir: path.resolve(__dirname, "../component-tests/letterQueue-tests"),
26+
testMatch: "**/*.spec.ts",
27+
},
2328
],
2429
};
2530

tests/constants/api-constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ export const AWS_ACCOUNT_ID = process.env.AWS_ACCOUNT_ID ?? "820178564574";
1515
export const EVENT_SUBSCRIPTION_TOPIC_ARN =
1616
process.env.EVENT_SUBSCRIPTION_TOPIC_ARN ??
1717
`arn:aws:sns:${AWS_REGION}:${AWS_ACCOUNT_ID}:${EVENT_SUBSCRIPTION_TOPIC_NAME}`;
18+
export const LETTERQUEUE_TABLENAME = `nhs-${envName}-supapi-letter-queue`;

tests/helpers/aws-cloudwatch-helper.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
FilterLogEventsCommand,
44
} from "@aws-sdk/client-cloudwatch-logs";
55
import { AWS_REGION, envName } from "tests/constants/api-constants";
6+
import { logger } from "./pino-logger";
67

78
const sleep = (ms: number) =>
89
new Promise((resolve) => {
@@ -97,3 +98,22 @@ export async function pollUpsertLetterLogForError(
9798
`Timed out waiting for upsert letter error log in ${logGroupName}`,
9899
);
99100
}
101+
102+
export async function supplierIdFromSupplierAllocatorLog(
103+
domainId: string,
104+
): Promise<string> {
105+
const message = await pollSupplierAllocatorLogForResolvedSpec(domainId);
106+
const supplierAllocatorLog = JSON.parse(message) as {
107+
msg?: { supplierSpec?: { supplierId?: string } };
108+
};
109+
const supplierId = supplierAllocatorLog.msg?.supplierSpec?.supplierId;
110+
111+
logger.info(
112+
`Supplier ${supplierId} allocated for domainId ${domainId} in supplier allocator lambda`,
113+
);
114+
115+
if (!supplierId) {
116+
throw new Error("supplierId was not found in supplier allocator log");
117+
}
118+
return supplierId;
119+
}

tests/helpers/event-fixtures.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { randomUUID } from "node:crypto";
2+
13
export function createPreparedV1Event(overrides: Record<string, unknown> = {}) {
24
const now = new Date().toISOString();
35

@@ -36,3 +38,13 @@ export function createPreparedV1Event(overrides: Record<string, unknown> = {}) {
3638
plane: "data",
3739
};
3840
}
41+
42+
export function createPreparedEventBatchWithSameDomainId(
43+
overrides: Record<string, unknown> = {},
44+
) {
45+
return [
46+
createPreparedV1Event({ id: randomUUID(), ...overrides }),
47+
createPreparedV1Event({ id: randomUUID(), ...overrides }),
48+
createPreparedV1Event({ id: randomUUID(), ...overrides }),
49+
];
50+
}

tests/helpers/generate-fetch-test-data.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import {
66
QueryCommand,
77
} from "@aws-sdk/lib-dynamodb";
88
import {
9+
LETTERQUEUE_TABLENAME,
910
LETTERSTABLENAME,
1011
SUPPLIERTABLENAME,
1112
envName,
1213
} from "../constants/api-constants";
1314
import { createSupplierData, runCreateLetter } from "./pnpm-helpers";
15+
import { logger } from "./pino-logger";
1416

1517
const ddb = new DynamoDBClient({});
1618
const docClient = DynamoDBDocumentClient.from(ddb);
@@ -146,8 +148,7 @@ export async function checkSupplierExists(
146148

147149
const { Items } = await docClient.send(new QueryCommand(params));
148150
return Items !== undefined && Items.length > 0;
149-
} catch (error) {
150-
console.error("Error checking supplier existence:", error);
151+
} catch {
151152
return false;
152153
}
153154
}
@@ -162,3 +163,50 @@ export async function createSupplierEntry(supplierId: string): Promise<void> {
162163
status: "ENABLED",
163164
});
164165
}
166+
167+
export async function checkLetterQueueTable(
168+
supplierId: string,
169+
letterId: string,
170+
deleteAfterCheck?: boolean,
171+
): Promise<[boolean, number?]> {
172+
const MAX_ATTEMPTS = 5;
173+
const RETRY_DELAY_MS = 10_000;
174+
try {
175+
const params = {
176+
TableName: LETTERQUEUE_TABLENAME,
177+
KeyConditionExpression:
178+
"supplierId = :supplierId AND letterId = :letterId",
179+
ExpressionAttributeValues: {
180+
":supplierId": supplierId,
181+
":letterId": letterId,
182+
},
183+
};
184+
185+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
186+
const { Items } = await docClient.send(new QueryCommand(params));
187+
if (!deleteAfterCheck && Items !== undefined && Items.length > 0) {
188+
logger.info(
189+
`Queried letter queue table to verify existence for letterId ${letterId} and found items, confirming existence`,
190+
);
191+
return [true, Items.length];
192+
}
193+
if (deleteAfterCheck && Items?.length === 0) {
194+
logger.info(
195+
`Queried letter queue table to verify deletion for letterId ${letterId} and found no items, confirming deletion`,
196+
);
197+
return [true];
198+
}
199+
if (attempt < MAX_ATTEMPTS) {
200+
logger.info(
201+
`Retrying letter queue query for supplierId ${supplierId} and letterId ${letterId} in ${RETRY_DELAY_MS}ms`,
202+
);
203+
await delay(RETRY_DELAY_MS);
204+
}
205+
}
206+
207+
return [false];
208+
} catch (error) {
209+
logger.error({ supplierId, letterId, error }, "Letter queue query failed");
210+
return [false];
211+
}
212+
}

tests/helpers/poll-for-letters-helper.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,48 @@ export async function pollForLettersInDb(
4747
}
4848
return { letterStatus, statusCode };
4949
}
50+
51+
export async function pollForLettersInLetterQueue(
52+
request: APIRequestContext,
53+
supplierId: string,
54+
domainId: string,
55+
baseUrl: string,
56+
): Promise<{ letterStatus: string | undefined; statusCode: number }> {
57+
const headers = createValidRequestHeaders(supplierId);
58+
let statusCode = 0;
59+
let letterStatus: string | undefined;
60+
const RETRY_DELAY_MS = 10_000;
61+
const MAX_ATTEMPTS = 5;
62+
63+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
64+
const getLetterResponse = await request.get(
65+
`${baseUrl}/${SUPPLIER_LETTERS}/${domainId}`,
66+
{
67+
headers,
68+
},
69+
);
70+
71+
statusCode = getLetterResponse.status();
72+
const responseBody = (await getLetterResponse.json()) as {
73+
data?: { attributes?: { status?: string } };
74+
};
75+
letterStatus = responseBody.data?.attributes?.status;
76+
77+
if (statusCode === 200 && letterStatus === "PENDING") {
78+
logger.info(
79+
`Attempt ${attempt}: Received status code ${statusCode} for domainId: ${domainId}`,
80+
);
81+
break;
82+
}
83+
84+
if (attempt < MAX_ATTEMPTS) {
85+
logger.info(
86+
`Attempt ${attempt}: Received status code ${statusCode} for domainId: ${domainId}. Retrying after ${RETRY_DELAY_MS / 1000} seconds...`,
87+
);
88+
await new Promise((resolve) => {
89+
setTimeout(resolve, RETRY_DELAY_MS); // Wait for 10 seconds before the next attempt
90+
});
91+
}
92+
}
93+
return { letterStatus, statusCode };
94+
}

tests/helpers/send-sns-event.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import {
22
MessageAttributeValue,
3+
PublishBatchCommand,
4+
PublishBatchCommandOutput,
5+
PublishBatchRequestEntry,
36
PublishCommand,
47
PublishCommandOutput,
58
} from "@aws-sdk/client-sns";
69
import { EVENT_SUBSCRIPTION_TOPIC_ARN } from "tests/constants/api-constants";
710
import { snsClient } from "tests/helpers/aws-sns-helper";
811

912
export type SnsEventMessage = Record<string, unknown> | string;
13+
export type SnsBatchEventEntry = {
14+
id?: string;
15+
message: SnsEventMessage;
16+
messageAttributes?: Record<string, MessageAttributeValue>;
17+
};
1018

1119
export async function sendSnsEvent(
1220
message: SnsEventMessage,
@@ -20,3 +28,24 @@ export async function sendSnsEvent(
2028
}),
2129
);
2230
}
31+
32+
export async function sendSnsBatchEvent(
33+
messages: SnsBatchEventEntry[],
34+
): Promise<PublishBatchCommandOutput> {
35+
const publishBatchRequestEntries: PublishBatchRequestEntry[] = messages.map(
36+
({ id, message, messageAttributes }, index) => ({
37+
Id: id ?? `message-${index + 1}`,
38+
Message: typeof message === "string" ? message : JSON.stringify(message),
39+
...(messageAttributes && {
40+
MessageAttributes: messageAttributes,
41+
}),
42+
}),
43+
);
44+
45+
const command = new PublishBatchCommand({
46+
TopicArn: EVENT_SUBSCRIPTION_TOPIC_ARN,
47+
PublishBatchRequestEntries: publishBatchRequestEntries,
48+
});
49+
50+
return snsClient.send(command);
51+
}

0 commit comments

Comments
 (0)