Skip to content

Commit eb8f0f2

Browse files
authored
CCM-13857 Skip replayed letter event (#495)
* Skip replayed letter event * Fix tests * Revert change to PR * Fix order of arguments
1 parent 82395aa commit eb8f0f2

File tree

8 files changed

+102
-75
lines changed

8 files changed

+102
-75
lines changed

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { LetterRepository } from "../letter-repository";
1010
import { InsertLetter, Letter, UpdateLetter } from "../types";
1111
import { LogStream, createTestLogger } from "./logs";
12+
import { LetterAlreadyExistsError } from "../letter-already-exists-error";
1213

1314
function createLetter(
1415
supplierId: string,
@@ -149,9 +150,7 @@ describe("LetterRepository", () => {
149150
await letterRepository.putLetter(createLetter("supplier1", "letter1"));
150151
await expect(
151152
letterRepository.putLetter(createLetter("supplier1", "letter1")),
152-
).rejects.toThrow(
153-
"Letter with id letter1 already exists for supplier supplier1",
154-
);
153+
).rejects.toThrow(LetterAlreadyExistsError);
155154
});
156155

157156
test("rethrows errors from DynamoDB when creating a letter", async () => {

internal/datastore/src/letter-repository.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
LetterSchemaBase,
1919
UpdateLetter,
2020
} from "./types";
21+
import { LetterAlreadyExistsError } from "./letter-already-exists-error";
2122

2223
export type PagingOptions = Partial<{
2324
exclusiveStartKey: Record<string, any>;
@@ -62,9 +63,7 @@ export class LetterRepository {
6263
error instanceof Error &&
6364
error.name === "ConditionalCheckFailedException"
6465
) {
65-
throw new Error(
66-
`Letter with id ${letter.id} already exists for supplier ${letter.supplierId}`,
67-
);
66+
throw new LetterAlreadyExistsError(letter.supplierId, letter.id);
6867
}
6968
throw error;
7069
}

lambdas/upsert-letter/jest.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ export const baseJestConfig = {
99
},
1010
],
1111
},
12+
transformIgnorePatterns: [
13+
"node_modules/(?!(@nhsdigital/nhs-notify-event-schemas-supplier-config)/)",
14+
],
1215

1316
// Automatically clear mock calls, instances, contexts and results before every test
1417
clearMocks: true,

lambdas/upsert-letter/src/handler/__tests__/upsert-handler.test.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { SQSEvent, SQSRecord } from "aws-lambda";
22
import pino from "pino";
3-
import { LetterRepository } from "internal/datastore/src";
3+
import {
4+
LetterAlreadyExistsError,
5+
LetterRepository,
6+
} from "@internal/datastore";
47
import { LetterRequestPreparedEventV2 } from "@nhsdigital/nhs-notify-event-schemas-letter-rendering";
58
import { LetterRequestPreparedEvent } from "@nhsdigital/nhs-notify-event-schemas-letter-rendering-v1";
69
import {
@@ -199,7 +202,11 @@ describe("createUpsertLetterHandler", () => {
199202
putLetter: jest.fn(),
200203
updateLetterStatus: jest.fn(),
201204
} as unknown as LetterRepository,
202-
logger: { error: jest.fn(), info: jest.fn() } as unknown as pino.Logger,
205+
logger: {
206+
error: jest.fn(),
207+
warn: jest.fn(),
208+
info: jest.fn(),
209+
} as unknown as pino.Logger,
203210
env: {
204211
LETTERS_TABLE_NAME: "LETTERS_TABLE_NAME",
205212
LETTER_TTL_HOURS: 12_960,
@@ -301,6 +308,31 @@ describe("createUpsertLetterHandler", () => {
301308
);
302309
});
303310

311+
it("does not treat a replayed insert as a failure", async () => {
312+
const v1message = {
313+
letterEvent: createPreparedV1Event(),
314+
supplierSpec: {
315+
supplierId: "supplier1",
316+
specId: "spec1",
317+
priority: 10,
318+
billingId: "billing1",
319+
},
320+
};
321+
const evt: SQSEvent = createSQSEvent([
322+
createSqsRecord("msg2", JSON.stringify(v1message)),
323+
]);
324+
(mockedDeps.letterRepo.putLetter as jest.Mock).mockRejectedValue(
325+
new LetterAlreadyExistsError("supplier1", "letter1"),
326+
);
327+
328+
const result = await createUpsertLetterHandler(mockedDeps)(
329+
evt,
330+
{} as any,
331+
{} as any,
332+
);
333+
expect(result!.batchItemFailures).toEqual([]);
334+
});
335+
304336
test("unknown supplier has metric emitted with 'unknown' supplier dimension", async () => {
305337
const letterEvent = createSupplierStatusChangeEventWithoutSupplier();
306338

lambdas/upsert-letter/src/handler/upsert-handler.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { SQSBatchItemFailure, SQSEvent, SQSHandler } from "aws-lambda";
2-
import { InsertLetter, UpdateLetter } from "@internal/datastore";
2+
import {
3+
InsertLetter,
4+
LetterAlreadyExistsError,
5+
UpdateLetter,
6+
} from "@internal/datastore";
37
import {
48
$LetterRequestPreparedEvent,
59
LetterRequestPreparedEvent,
@@ -69,14 +73,26 @@ function getOperationFromType(type: string): UpsertOperation {
6973
supplierSpec.priority,
7074
supplierSpec.billingId, // use billingId for now
7175
);
72-
await deps.letterRepo.putLetter(letterToInsert);
76+
try {
77+
await deps.letterRepo.putLetter(letterToInsert);
7378

74-
deps.logger.info({
75-
description: "Inserted letter",
76-
eventId: preparedRequest.id,
77-
letterId: letterToInsert.id,
78-
supplierId: letterToInsert.supplierId,
79-
});
79+
deps.logger.info({
80+
description: "Inserted letter",
81+
eventId: preparedRequest.id,
82+
letterId: letterToInsert.id,
83+
supplierId: letterToInsert.supplierId,
84+
});
85+
} catch (error) {
86+
if (error instanceof LetterAlreadyExistsError) {
87+
deps.logger.warn({
88+
description: "Letter already exists",
89+
supplierId: letterToInsert.supplierId,
90+
letterId: letterToInsert.id,
91+
});
92+
} else {
93+
throw error;
94+
}
95+
}
8096
},
8197
};
8298
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { SUPPLIER_LETTERS, envName } from "tests/constants/api-constants";
99
import {
1010
pollSupplierAllocatorLogForResolvedSpec,
1111
pollUpsertLetterLogForError,
12+
pollUpsertLetterLogForWarning,
1213
} from "tests/helpers/aws-cloudwatch-helper";
1314
import { supplierDataSetup } from "tests/helpers/suppliers-setup-helper";
1415
import { pollForLetterStatus } from "tests/helpers/poll-for-letters-helper";
@@ -133,8 +134,6 @@ test.describe("Event Subscription SNS Tests", () => {
133134
expect(duplicateResponse.MessageId).toBeTruthy();
134135

135136
// poll supplier upsert to check if duplicate event was processed
136-
await pollUpsertLetterLogForError(
137-
`Letter with id ${domainId} already exists for supplier ${supplierId}"`,
138-
);
137+
await pollUpsertLetterLogForWarning("Letter already exists", domainId);
139138
});
140139
});

tests/component-tests/letterQueue-tests/queue-operations.spec.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
import { logger } from "tests/helpers/pino-logger";
88
import { sendSnsBatchEvent, sendSnsEvent } from "tests/helpers/send-sns-event";
99
import {
10-
pollUpsertLetterLogForError,
10+
pollUpsertLetterLogForWarning,
1111
supplierIdFromSupplierAllocatorLog,
1212
} from "tests/helpers/aws-cloudwatch-helper";
1313
import getRestApiGatewayBaseUrl from "tests/helpers/aws-gateway-helper";
@@ -102,9 +102,6 @@ test.describe("Letter Queue Tests", () => {
102102
expect(letterExists).toBe(true);
103103
expect(itemCount).toBe(1);
104104

105-
await pollUpsertLetterLogForError(
106-
`Letter with id ${letterId} already exists for supplier ${supplierId}"`,
107-
letterId,
108-
);
105+
await pollUpsertLetterLogForWarning("Letter already exists", letterId);
109106
});
110107
});

tests/helpers/aws-cloudwatch-helper.ts

Lines changed: 33 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@ const sleep = (ms: number) =>
1010
setTimeout(resolve, ms);
1111
});
1212

13-
export async function pollSupplierAllocatorLogForResolvedSpec(
14-
domainId: string,
13+
async function pollLambdaLog(
14+
lambdaName: string,
15+
filterPatterns: string[],
16+
extraPatterns?: string[],
1517
): Promise<string> {
1618
const intervalMs = 5000;
1719
const startTimeMs = Date.now() - 5 * 60_000;
1820
const timeoutMs = 120_000;
1921

2022
const client = new CloudWatchLogsClient({ region: AWS_REGION });
21-
const logGroupName = `/aws/lambda/nhs-${envName}-supapi-supplier-allocator`;
23+
const logGroupName = `/aws/lambda/nhs-${envName}-supapi-${lambdaName}`;
2224
const deadline = Date.now() + timeoutMs;
2325

2426
while (Date.now() < deadline) {
@@ -28,18 +30,15 @@ export async function pollSupplierAllocatorLogForResolvedSpec(
2830
startTime: startTimeMs,
2931
interleaved: true,
3032
limit: 100,
31-
filterPattern: `"Sending message to upsert letter queue" "${domainId}"`,
33+
filterPattern: filterPatterns.join(" "),
3234
}),
3335
);
3436

3537
const foundEvent = (response.events ?? []).find((event) => {
3638
const message = event.message ?? "";
37-
return (
38-
message.includes(
39-
'"description":"Sending message to upsert letter queue"',
40-
) &&
41-
(!domainId || message.includes(domainId))
42-
);
39+
return extraPatterns
40+
? extraPatterns.some((pattern) => message.includes(pattern))
41+
: true;
4342
});
4443
if (foundEvent?.message) {
4544
return foundEvent.message;
@@ -48,55 +47,38 @@ export async function pollSupplierAllocatorLogForResolvedSpec(
4847
await sleep(intervalMs);
4948
}
5049

51-
throw new Error(
52-
`Timed out waiting for resolved supplier spec log in ${logGroupName}`,
53-
);
50+
throw new Error(`Timed out waiting for resolved log in ${logGroupName}`);
51+
}
52+
53+
export async function pollSupplierAllocatorLogForResolvedSpec(
54+
domainId: string,
55+
): Promise<string> {
56+
return pollLambdaLog("supplier-allocator", [
57+
'"Sending message to upsert letter queue"',
58+
`"${domainId}"`,
59+
]);
5460
}
5561

5662
export async function pollUpsertLetterLogForError(
5763
msgToCheck: string,
5864
domainId?: string,
5965
): Promise<string> {
60-
const intervalMs = 5000;
61-
const startTimeMs = Date.now() - 5 * 60_000;
62-
const timeoutMs = 120_000;
63-
64-
const client = new CloudWatchLogsClient({ region: AWS_REGION });
65-
const logGroupName = `/aws/lambda/nhs-${envName}-supapi-upsertletter`;
66-
const deadline = Date.now() + timeoutMs;
67-
68-
while (Date.now() < deadline) {
69-
const response = await client.send(
70-
new FilterLogEventsCommand({
71-
logGroupName,
72-
startTime: startTimeMs,
73-
interleaved: true,
74-
limit: 100,
75-
filterPattern: domainId
76-
? `"Error processing upsert of record" "${domainId}"`
77-
: `"Error processing upsert of record"`,
78-
}),
79-
);
80-
81-
const foundEvent = (response.events ?? []).find((event) => {
82-
const message = event.message ?? "";
83-
return (
84-
message.includes('"description":"Error processing upsert of record"') &&
85-
(message.includes(`"message":"${msgToCheck}`) ||
86-
message.includes(`"message": "${msgToCheck}`))
87-
);
88-
});
89-
90-
if (foundEvent?.message) {
91-
return foundEvent.message;
92-
}
93-
94-
await sleep(intervalMs);
66+
const filterPatterns = ['"Error processing upsert of record"'];
67+
if (domainId) {
68+
filterPatterns.push(`"${domainId}"`);
9569
}
70+
return pollLambdaLog("upsertletter", filterPatterns, [
71+
`"message": "${msgToCheck}`,
72+
`"message":"${msgToCheck}`,
73+
]);
74+
}
9675

97-
throw new Error(
98-
`Timed out waiting for upsert letter error log in ${logGroupName}`,
99-
);
76+
export async function pollUpsertLetterLogForWarning(
77+
description: string,
78+
domainId: string,
79+
): Promise<string> {
80+
const filterPatterns = ['"WARN"', `"${domainId}"`, `"${description}"`];
81+
return pollLambdaLog("upsertletter", filterPatterns);
10082
}
10183

10284
export async function supplierIdFromSupplierAllocatorLog(

0 commit comments

Comments
 (0)