Skip to content

Commit a87206d

Browse files
Add Tests for Update Letter Queue (#479)
* letter queue tests * lint issues * lint issues * fixes * lint * review fix * fix * fix
1 parent 26ac7f8 commit a87206d

File tree

10 files changed

+239
-7
lines changed

10 files changed

+239
-7
lines changed

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
};

tests/component-tests/events-tests/event-subscription.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
pollUpsertLetterLogForError,
1212
} from "tests/helpers/aws-cloudwatch-helper";
1313
import { supplierDataSetup } from "tests/helpers/suppliers-setup-helper";
14-
import { pollForLettersInDb } from "tests/helpers/poll-for-letters-helper";
14+
import { pollForLetterStatus } from "tests/helpers/poll-for-letters-helper";
1515

1616
let baseUrl: string;
1717

@@ -49,7 +49,7 @@ test.describe("Event Subscription SNS Tests", () => {
4949
await supplierDataSetup(supplierId);
5050

5151
// poll for letter to be inserted in db with status PENDING
52-
const { letterStatus, statusCode } = await pollForLettersInDb(
52+
const { letterStatus, statusCode } = await pollForLetterStatus(
5353
request,
5454
supplierId,
5555
domainId,
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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 {
10+
pollUpsertLetterLogForError,
11+
supplierIdFromSupplierAllocatorLog,
12+
} from "tests/helpers/aws-cloudwatch-helper";
13+
import getRestApiGatewayBaseUrl from "tests/helpers/aws-gateway-helper";
14+
import { SUPPLIER_LETTERS } from "tests/constants/api-constants";
15+
import { supplierDataSetup } from "tests/helpers/suppliers-setup-helper";
16+
import { checkLetterQueueTable } from "tests/helpers/generate-fetch-test-data";
17+
import {
18+
patchRequestHeaders,
19+
patchValidRequestBody,
20+
} from "../apiGateway-tests/testCases/update-letter-status";
21+
22+
let baseUrl: string;
23+
24+
test.beforeAll(async () => {
25+
baseUrl = await getRestApiGatewayBaseUrl();
26+
});
27+
28+
test.describe("Letter Queue Tests", () => {
29+
test.setTimeout(180_000);
30+
31+
test("Verify that the letter queue operation inserts data into the letter queue table for pending letters", async ({
32+
request,
33+
}) => {
34+
const letterId = randomUUID();
35+
const status = "ACCEPTED";
36+
37+
logger.info(`Sending event with domainId: ${letterId}`);
38+
const preparedEvent = createPreparedV1Event({ domainId: letterId });
39+
const response = await sendSnsEvent(preparedEvent);
40+
41+
expect(response.MessageId).toBeTruthy();
42+
43+
const supplierId = await supplierIdFromSupplierAllocatorLog(letterId);
44+
45+
await supplierDataSetup(supplierId);
46+
47+
const [letterQueue, count] = await checkLetterQueueTable(
48+
supplierId,
49+
letterId,
50+
);
51+
expect(letterQueue).toBe(true);
52+
expect(count).toBe(1);
53+
54+
// update the letter status to ACCEPTED and verify if letter queue table is cleaned up
55+
56+
const headers = patchRequestHeaders(supplierId);
57+
const body = patchValidRequestBody(letterId, status);
58+
59+
const patchResponse = await request.patch(
60+
`${baseUrl}/${SUPPLIER_LETTERS}/${letterId}`,
61+
{
62+
headers,
63+
data: body,
64+
},
65+
);
66+
expect(patchResponse.status()).toBe(202);
67+
logger.info(
68+
`Updated letter status to ${status} for letterId ${letterId}, now polling letter queue table for cleanup confirmation`,
69+
);
70+
71+
const [letterQueueAfterUpdate, countAfterUpdate] =
72+
await checkLetterQueueTable(supplierId, letterId, true);
73+
expect(letterQueueAfterUpdate).toBe(true);
74+
expect(countAfterUpdate).toBeUndefined();
75+
});
76+
77+
test("Verify if the only one entry is inserted in the letter queue table for a batch of events with the same letterId", async () => {
78+
const letterId = randomUUID();
79+
const eventBatch = createPreparedEventBatchWithSameDomainId({
80+
domainId: letterId,
81+
});
82+
logger.info(
83+
`Sending batch event with ${eventBatch.length} events ${letterId}`,
84+
);
85+
const response = await sendSnsBatchEvent(
86+
eventBatch.map((event) => ({
87+
id: event.id,
88+
message: event,
89+
})),
90+
);
91+
expect(response.Successful).toHaveLength(eventBatch.length);
92+
93+
const supplierId = await supplierIdFromSupplierAllocatorLog(letterId);
94+
95+
logger.info(
96+
`Verifying duplicate queue inserts are ignored for the batch of events with same letterId ${letterId}`,
97+
);
98+
const [letterExists, itemCount] = await checkLetterQueueTable(
99+
supplierId,
100+
letterId,
101+
);
102+
expect(letterExists).toBe(true);
103+
expect(itemCount).toBe(1);
104+
105+
await pollUpsertLetterLogForError(
106+
`Letter with id ${letterId} already exists for supplier ${supplierId}"`,
107+
letterId,
108+
);
109+
});
110+
});

tests/config/main.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 & 1 deletion
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);
@@ -147,7 +149,7 @@ export async function checkSupplierExists(
147149
const { Items } = await docClient.send(new QueryCommand(params));
148150
return Items !== undefined && Items.length > 0;
149151
} catch (error) {
150-
console.error("Error checking supplier existence:", error);
152+
logger.error({ supplierId, error }, "Supplier existence check failed");
151153
return false;
152154
}
153155
}
@@ -162,3 +164,50 @@ export async function createSupplierEntry(supplierId: string): Promise<void> {
162164
status: "ENABLED",
163165
});
164166
}
167+
168+
export async function checkLetterQueueTable(
169+
supplierId: string,
170+
letterId: string,
171+
checkForDeletedLetters?: boolean,
172+
): Promise<[boolean, number?]> {
173+
const MAX_ATTEMPTS = 5;
174+
const RETRY_DELAY_MS = 10_000;
175+
try {
176+
const params = {
177+
TableName: LETTERQUEUE_TABLENAME,
178+
KeyConditionExpression:
179+
"supplierId = :supplierId AND letterId = :letterId",
180+
ExpressionAttributeValues: {
181+
":supplierId": supplierId,
182+
":letterId": letterId,
183+
},
184+
};
185+
186+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
187+
const { Items } = await docClient.send(new QueryCommand(params));
188+
if (!checkForDeletedLetters && Items !== undefined && Items.length > 0) {
189+
logger.info(
190+
`Queried letter queue table to verify existence for letterId ${letterId} and found items, confirming existence`,
191+
);
192+
return [true, Items.length];
193+
}
194+
if (checkForDeletedLetters && Items !== undefined && Items.length === 0) {
195+
logger.info(
196+
`Queried letter queue table to verify deletion for letterId ${letterId} and found no items, confirming deletion`,
197+
);
198+
return [true];
199+
}
200+
if (attempt < MAX_ATTEMPTS) {
201+
logger.info(
202+
`Retrying letter queue query for supplierId ${supplierId} and letterId ${letterId} in ${RETRY_DELAY_MS}ms`,
203+
);
204+
await delay(RETRY_DELAY_MS);
205+
}
206+
}
207+
208+
return [false];
209+
} catch (error) {
210+
logger.error({ supplierId, letterId, error }, "Letter queue query failed");
211+
return [false];
212+
}
213+
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createValidRequestHeaders } from "tests/constants/request-headers";
33
import { SUPPLIER_LETTERS } from "tests/constants/api-constants";
44
import { logger } from "./pino-logger";
55

6-
export async function pollForLettersInDb(
6+
export async function pollForLetterStatus(
77
request: APIRequestContext,
88
supplierId: string,
99
domainId: string,
@@ -41,9 +41,10 @@ export async function pollForLettersInDb(
4141
`Attempt ${attempt}: Received status code ${statusCode} for domainId: ${domainId}. Retrying after ${RETRY_DELAY_MS / 1000} seconds...`,
4242
);
4343
await new Promise((resolve) => {
44-
setTimeout(resolve, RETRY_DELAY_MS); // Wait for 10 seconds before the next attempt
44+
setTimeout(resolve, RETRY_DELAY_MS);
4545
});
4646
}
4747
}
48+
4849
return { letterStatus, statusCode };
4950
}

tests/helpers/send-sns-event.ts

Lines changed: 34 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,29 @@ export async function sendSnsEvent(
2028
}),
2129
);
2230
}
31+
32+
export async function sendSnsBatchEvent(
33+
messages: SnsBatchEventEntry[],
34+
): Promise<PublishBatchCommandOutput> {
35+
if (messages.length > 10) {
36+
throw new Error(
37+
"SNS batch publish supports a maximum of 10 messages per batch",
38+
);
39+
}
40+
const publishBatchRequestEntries: PublishBatchRequestEntry[] = messages.map(
41+
({ id, message, messageAttributes }, index) => ({
42+
Id: id ?? `message-${index + 1}`,
43+
Message: typeof message === "string" ? message : JSON.stringify(message),
44+
...(messageAttributes && {
45+
MessageAttributes: messageAttributes,
46+
}),
47+
}),
48+
);
49+
50+
const command = new PublishBatchCommand({
51+
TopicArn: EVENT_SUBSCRIPTION_TOPIC_ARN,
52+
PublishBatchRequestEntries: publishBatchRequestEntries,
53+
});
54+
55+
return snsClient.send(command);
56+
}

0 commit comments

Comments
 (0)