Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
} from "@internal/datastore";
import { mockDeep } from "jest-mock-extended";
import pino from "pino";
import { MetricStatus } from "@internal/helpers";
import {
Context,
DynamoDBRecord,
Expand All @@ -32,9 +31,12 @@ const mockedDeps: jest.Mocked<Deps> = {
env: {} as unknown as EnvVars,
} as Deps;

function generateLetter(status: LetterStatus, id?: string): Letter {
function generateLetter(
status: LetterStatus,
overrides?: Partial<Letter>,
): Letter {
return {
id: id || "1",
id: "1",
status,
specificationId: "spec1",
supplierId: "supplier1",
Expand All @@ -48,6 +50,7 @@ function generateLetter(status: LetterStatus, id?: string): Letter {
source: "test-source",
subject: "test-subject",
billingRef: "billing-ref-1",
...overrides,
};
}

Expand Down Expand Up @@ -144,8 +147,8 @@ describe("update-letter-queue Lambda", () => {

it("returns on the first failure", async () => {
const handler = createHandler(mockedDeps);
const newLetter1 = generateLetter("PENDING", "1");
const newLetter2 = generateLetter("PENDING", "2");
const newLetter1 = generateLetter("PENDING", { id: "1" });
const newLetter2 = generateLetter("PENDING", { id: "2" });
(mockedDeps.letterQueueRepository.putLetter as jest.Mock)
.mockRejectedValueOnce({})
.mockResolvedValueOnce({});
Expand All @@ -164,8 +167,8 @@ describe("update-letter-queue Lambda", () => {

it("does not treat a replayed insert as a failure", async () => {
const handler = createHandler(mockedDeps);
const newLetter1 = generateLetter("PENDING", "1");
const newLetter2 = generateLetter("PENDING", "2");
const newLetter1 = generateLetter("PENDING", { id: "1" });
const newLetter2 = generateLetter("PENDING", { id: "2" });
(mockedDeps.letterQueueRepository.putLetter as jest.Mock)
.mockRejectedValueOnce(new LetterAlreadyExistsError("supplier1", "1"))
.mockResolvedValueOnce({});
Expand All @@ -181,10 +184,10 @@ describe("update-letter-queue Lambda", () => {

it("does not treat a replayed delete as a failure", async () => {
const handler = createHandler(mockedDeps);
const oldLetter1 = generateLetter("PENDING", "1");
const oldLetter2 = generateLetter("PENDING", "2");
const newLetter1 = generateLetter("ACCEPTED", "1");
const newLetter2 = generateLetter("ACCEPTED", "2");
const oldLetter1 = generateLetter("PENDING", { id: "1" });
const oldLetter2 = generateLetter("PENDING", { id: "2" });
const newLetter1 = generateLetter("ACCEPTED", { id: "1" });
const newLetter2 = generateLetter("ACCEPTED", { id: "2" });
(mockedDeps.letterQueueRepository.deleteLetter as jest.Mock)
.mockRejectedValueOnce(new LetterDoesNotExistError("supplier1", "1"))
.mockResolvedValueOnce({});
Expand Down Expand Up @@ -227,26 +230,51 @@ describe("update-letter-queue Lambda", () => {
});

describe("Metrics", () => {
it("emits success metrics when all letters are processed successfully", async () => {
// eslint-disable-next-line jest/expect-expect
it("logs a metric containing the delta of pending letters added/deleted", async () => {
const handler = createHandler(mockedDeps);
const oldLetter1 = generateLetter("PENDING", "1");
const newLetter1 = generateLetter("ACCEPTED", "1");
const newLetter2 = generateLetter("PENDING", "2");
const oldLetter1 = generateLetter("PENDING", { id: "1" });
const newLetter1 = generateLetter("ACCEPTED", { id: "1" });
const newLetter2 = generateLetter("PENDING", { id: "2" });
const newLetter3 = generateLetter("PENDING", { id: "3" });

const testData = generateKinesisEvent([
generateModifyRecord(oldLetter1, newLetter1),
generateInsertRecord(newLetter2),
generateInsertRecord(newLetter3),
]);
await handler(testData, mockDeep<Context>(), jest.fn());

assertSuccessMetricLogged(2);
assertFailureMetricLogged(0);
assertQueueDeltaMetricLogged("supplier1", 1);
});

it("emits failure metrics when a letter fails to be inserted", async () => {
// eslint-disable-next-line jest/expect-expect
it("breaks the metric down by supplier", async () => {
const handler = createHandler(mockedDeps);
const newLetter1 = generateLetter("PENDING", "1");
const newLetter2 = generateLetter("PENDING", "2");
const oldLetter1 = generateLetter("PENDING", { id: "1" });
const newLetter1 = generateLetter("ACCEPTED", { id: "1" });
const newLetter2 = generateLetter("PENDING", {
supplierId: "supplier2",
id: "2",
});
const newLetter3 = generateLetter("PENDING", { id: "3" });

const testData = generateKinesisEvent([
generateModifyRecord(oldLetter1, newLetter1),
generateInsertRecord(newLetter2),
generateInsertRecord(newLetter3),
]);
await handler(testData, mockDeep<Context>(), jest.fn());

assertQueueDeltaMetricLogged("supplier1", 0);
assertQueueDeltaMetricLogged("supplier2", 1);
});

// eslint-disable-next-line jest/expect-expect
it("counts a failed insert as zero", async () => {
const handler = createHandler(mockedDeps);
const newLetter1 = generateLetter("PENDING", { id: "1" });
const newLetter2 = generateLetter("PENDING", { id: "2" });
(mockedDeps.letterQueueRepository.putLetter as jest.Mock)
.mockResolvedValueOnce({})
.mockRejectedValueOnce(new Error("DynamoDB error"));
Expand All @@ -257,16 +285,16 @@ describe("update-letter-queue Lambda", () => {
]);
await handler(testData, mockDeep<Context>(), jest.fn());

assertSuccessMetricLogged(1);
assertFailureMetricLogged(1);
assertQueueDeltaMetricLogged("supplier1", 1);
});

it("emits failure metrics when a letter fails to be deleted", async () => {
// eslint-disable-next-line jest/expect-expect
it("counts a failed delete as zero", async () => {
const handler = createHandler(mockedDeps);
const oldLetter1 = generateLetter("PENDING", "1");
const oldLetter2 = generateLetter("PENDING", "2");
const newLetter1 = generateLetter("ACCEPTED", "1");
const newLetter2 = generateLetter("ACCEPTED", "2");
const oldLetter1 = generateLetter("PENDING", { id: "1" });
const oldLetter2 = generateLetter("PENDING", { id: "2" });
const newLetter1 = generateLetter("ACCEPTED", { id: "1" });
const newLetter2 = generateLetter("ACCEPTED", { id: "2" });
(mockedDeps.letterQueueRepository.deleteLetter as jest.Mock)
.mockResolvedValueOnce({})
.mockRejectedValueOnce(new Error("DynamoDB error"));
Expand All @@ -277,14 +305,14 @@ describe("update-letter-queue Lambda", () => {
]);
await handler(testData, mockDeep<Context>(), jest.fn());

assertSuccessMetricLogged(1);
assertFailureMetricLogged(1);
assertQueueDeltaMetricLogged("supplier1", -1);
});

it("does not count a replayed insert as a success or failure", async () => {
// eslint-disable-next-line jest/expect-expect
it("counts a replayed insert as zero", async () => {
const handler = createHandler(mockedDeps);
const newLetter1 = generateLetter("PENDING", "1");
const newLetter2 = generateLetter("PENDING", "2");
const newLetter1 = generateLetter("PENDING", { id: "1" });
const newLetter2 = generateLetter("PENDING", { id: "2" });

(mockedDeps.letterQueueRepository.putLetter as jest.Mock)
.mockRejectedValueOnce(new LetterAlreadyExistsError("supplier1", "1"))
Expand All @@ -296,16 +324,16 @@ describe("update-letter-queue Lambda", () => {
]);
await handler(testData, mockDeep<Context>(), jest.fn());

assertSuccessMetricLogged(1);
assertFailureMetricLogged(0);
assertQueueDeltaMetricLogged("supplier1", 1);
});

it("does not count a replayed delete as a success or failure", async () => {
// eslint-disable-next-line jest/expect-expect
it("counts a replayed delete as zero", async () => {
const handler = createHandler(mockedDeps);
const oldLetter1 = generateLetter("PENDING", "1");
const oldLetter2 = generateLetter("PENDING", "2");
const newLetter1 = generateLetter("ACCEPTED", "1");
const newLetter2 = generateLetter("ACCEPTED", "2");
const oldLetter1 = generateLetter("PENDING", { id: "1" });
const oldLetter2 = generateLetter("PENDING", { id: "2" });
const newLetter1 = generateLetter("ACCEPTED", { id: "1" });
const newLetter2 = generateLetter("ACCEPTED", { id: "2" });
(mockedDeps.letterQueueRepository.deleteLetter as jest.Mock)
.mockRejectedValueOnce(new LetterDoesNotExistError("supplier1", "1"))
.mockResolvedValueOnce({});
Expand All @@ -316,19 +344,36 @@ describe("update-letter-queue Lambda", () => {
]);
await handler(testData, mockDeep<Context>(), jest.fn());

assertSuccessMetricLogged(1);
assertFailureMetricLogged(0);
assertQueueDeltaMetricLogged("supplier1", -1);
});

it("emits zero success metrics when no pending letters are in the batch", async () => {
// eslint-disable-next-line jest/expect-expect
it("logs zero counts when no pending letters are in the batch", async () => {
const handler = createHandler(mockedDeps);
const newLetter = generateLetter("PRINTED");

const testData = generateKinesisEvent([generateInsertRecord(newLetter)]);
await handler(testData, mockDeep<Context>(), jest.fn());

assertSuccessMetricLogged(0);
assertFailureMetricLogged(0);
assertQueueDeltaMetricNotLogged();
});

it("skips records with no NewImage (e.g. DELETE events) without error", async () => {
const handler = createHandler(mockedDeps);
const deleteRecord: DynamoDBRecord = {
eventName: "REMOVE",
dynamodb: { OldImage: mapToImage(generateLetter("PENDING")) },
};

const testData = generateKinesisEvent([deleteRecord]);
const result = await handler(testData, mockDeep<Context>(), jest.fn());

expect(mockedDeps.letterQueueRepository.putLetter).not.toHaveBeenCalled();
expect(
mockedDeps.letterQueueRepository.deleteLetter,
).not.toHaveBeenCalled();
expect(result.batchItemFailures).toEqual([]);
assertQueueDeltaMetricNotLogged();
});
});
});
Expand Down Expand Up @@ -375,43 +420,42 @@ function mapToImage(oldLetter: Letter) {
);
}

function assertSuccessMetricLogged(count: number) {
function assertQueueDeltaMetricLogged(supplierId: string, delta: number) {
expect(mockedDeps.logger.info).toHaveBeenCalledWith(
expect.objectContaining({
supplier: supplierId,
_aws: expect.objectContaining({
CloudWatchMetrics: expect.arrayContaining([
expect.objectContaining({
Metrics: [
expect.objectContaining({
Name: MetricStatus.Success,
Value: count,
Name: "QueueDelta",
Value: delta,
Unit: Unit.Count,
}),
],
}),
]),
}),
success: count,
QueueDelta: delta,
}),
);
}

function assertFailureMetricLogged(count: number) {
expect(mockedDeps.logger.info).toHaveBeenCalledWith(
function assertQueueDeltaMetricNotLogged() {
expect(mockedDeps.logger.info).not.toHaveBeenCalledWith(
expect.objectContaining({
_aws: expect.objectContaining({
CloudWatchMetrics: expect.arrayContaining([
expect.objectContaining({
Metrics: [
expect.objectContaining({
Name: MetricStatus.Failure,
Value: count,
Unit: Unit.Count,
Name: "QueueDelta",
}),
],
}),
]),
}),
failure: count,
}),
);
}
Loading
Loading