66} from "@internal/datastore" ;
77import { mockDeep } from "jest-mock-extended" ;
88import pino from "pino" ;
9- import { MetricStatus } from "@internal/helpers" ;
109import {
1110 Context ,
1211 DynamoDBRecord ,
@@ -32,9 +31,12 @@ const mockedDeps: jest.Mocked<Deps> = {
3231 env : { } as unknown as EnvVars ,
3332} as Deps ;
3433
35- function generateLetter ( status : LetterStatus , id ?: string ) : Letter {
34+ function generateLetter (
35+ status : LetterStatus ,
36+ overrides ?: Partial < Letter > ,
37+ ) : Letter {
3638 return {
37- id : id || "1" ,
39+ id : "1" ,
3840 status,
3941 specificationId : "spec1" ,
4042 supplierId : "supplier1" ,
@@ -50,6 +52,7 @@ function generateLetter(status: LetterStatus, id?: string): Letter {
5052 billingRef : "billing-ref-1" ,
5153 specificationBillingId : "billing1" ,
5254 priority : 2 ,
55+ ...overrides ,
5356 } ;
5457}
5558
@@ -147,8 +150,8 @@ describe("update-letter-queue Lambda", () => {
147150
148151 it ( "returns on the first failure" , async ( ) => {
149152 const handler = createHandler ( mockedDeps ) ;
150- const newLetter1 = generateLetter ( "PENDING" , "1" ) ;
151- const newLetter2 = generateLetter ( "PENDING" , "2" ) ;
153+ const newLetter1 = generateLetter ( "PENDING" , { id : "1" } ) ;
154+ const newLetter2 = generateLetter ( "PENDING" , { id : "2" } ) ;
152155 ( mockedDeps . letterQueueRepository . putLetter as jest . Mock )
153156 . mockRejectedValueOnce ( { } )
154157 . mockResolvedValueOnce ( { } ) ;
@@ -167,8 +170,8 @@ describe("update-letter-queue Lambda", () => {
167170
168171 it ( "does not treat a replayed insert as a failure" , async ( ) => {
169172 const handler = createHandler ( mockedDeps ) ;
170- const newLetter1 = generateLetter ( "PENDING" , "1" ) ;
171- const newLetter2 = generateLetter ( "PENDING" , "2" ) ;
173+ const newLetter1 = generateLetter ( "PENDING" , { id : "1" } ) ;
174+ const newLetter2 = generateLetter ( "PENDING" , { id : "2" } ) ;
172175 ( mockedDeps . letterQueueRepository . putLetter as jest . Mock )
173176 . mockRejectedValueOnce ( new LetterAlreadyExistsError ( "supplier1" , "1" ) )
174177 . mockResolvedValueOnce ( { } ) ;
@@ -184,10 +187,10 @@ describe("update-letter-queue Lambda", () => {
184187
185188 it ( "does not treat a replayed delete as a failure" , async ( ) => {
186189 const handler = createHandler ( mockedDeps ) ;
187- const oldLetter1 = generateLetter ( "PENDING" , "1" ) ;
188- const oldLetter2 = generateLetter ( "PENDING" , "2" ) ;
189- const newLetter1 = generateLetter ( "ACCEPTED" , "1" ) ;
190- const newLetter2 = generateLetter ( "ACCEPTED" , "2" ) ;
190+ const oldLetter1 = generateLetter ( "PENDING" , { id : "1" } ) ;
191+ const oldLetter2 = generateLetter ( "PENDING" , { id : "2" } ) ;
192+ const newLetter1 = generateLetter ( "ACCEPTED" , { id : "1" } ) ;
193+ const newLetter2 = generateLetter ( "ACCEPTED" , { id : "2" } ) ;
191194 ( mockedDeps . letterQueueRepository . deleteLetter as jest . Mock )
192195 . mockRejectedValueOnce ( new LetterDoesNotExistError ( "supplier1" , "1" ) )
193196 . mockResolvedValueOnce ( { } ) ;
@@ -230,26 +233,51 @@ describe("update-letter-queue Lambda", () => {
230233 } ) ;
231234
232235 describe ( "Metrics" , ( ) => {
233- it ( "emits success metrics when all letters are processed successfully" , async ( ) => {
236+ // eslint-disable-next-line jest/expect-expect
237+ it ( "logs a metric containing the delta of pending letters added/deleted" , async ( ) => {
234238 const handler = createHandler ( mockedDeps ) ;
235- const oldLetter1 = generateLetter ( "PENDING" , "1" ) ;
236- const newLetter1 = generateLetter ( "ACCEPTED" , "1" ) ;
237- const newLetter2 = generateLetter ( "PENDING" , "2" ) ;
239+ const oldLetter1 = generateLetter ( "PENDING" , { id : "1" } ) ;
240+ const newLetter1 = generateLetter ( "ACCEPTED" , { id : "1" } ) ;
241+ const newLetter2 = generateLetter ( "PENDING" , { id : "2" } ) ;
242+ const newLetter3 = generateLetter ( "PENDING" , { id : "3" } ) ;
238243
239244 const testData = generateKinesisEvent ( [
240245 generateModifyRecord ( oldLetter1 , newLetter1 ) ,
241246 generateInsertRecord ( newLetter2 ) ,
247+ generateInsertRecord ( newLetter3 ) ,
242248 ] ) ;
243249 await handler ( testData , mockDeep < Context > ( ) , jest . fn ( ) ) ;
244250
245- assertSuccessMetricLogged ( 2 ) ;
246- assertFailureMetricLogged ( 0 ) ;
251+ assertQueueDeltaMetricLogged ( "supplier1" , 1 ) ;
247252 } ) ;
248253
249- it ( "emits failure metrics when a letter fails to be inserted" , async ( ) => {
254+ // eslint-disable-next-line jest/expect-expect
255+ it ( "breaks the metric down by supplier" , async ( ) => {
250256 const handler = createHandler ( mockedDeps ) ;
251- const newLetter1 = generateLetter ( "PENDING" , "1" ) ;
252- const newLetter2 = generateLetter ( "PENDING" , "2" ) ;
257+ const oldLetter1 = generateLetter ( "PENDING" , { id : "1" } ) ;
258+ const newLetter1 = generateLetter ( "ACCEPTED" , { id : "1" } ) ;
259+ const newLetter2 = generateLetter ( "PENDING" , {
260+ supplierId : "supplier2" ,
261+ id : "2" ,
262+ } ) ;
263+ const newLetter3 = generateLetter ( "PENDING" , { id : "3" } ) ;
264+
265+ const testData = generateKinesisEvent ( [
266+ generateModifyRecord ( oldLetter1 , newLetter1 ) ,
267+ generateInsertRecord ( newLetter2 ) ,
268+ generateInsertRecord ( newLetter3 ) ,
269+ ] ) ;
270+ await handler ( testData , mockDeep < Context > ( ) , jest . fn ( ) ) ;
271+
272+ assertQueueDeltaMetricLogged ( "supplier1" , 0 ) ;
273+ assertQueueDeltaMetricLogged ( "supplier2" , 1 ) ;
274+ } ) ;
275+
276+ // eslint-disable-next-line jest/expect-expect
277+ it ( "counts a failed insert as zero" , async ( ) => {
278+ const handler = createHandler ( mockedDeps ) ;
279+ const newLetter1 = generateLetter ( "PENDING" , { id : "1" } ) ;
280+ const newLetter2 = generateLetter ( "PENDING" , { id : "2" } ) ;
253281 ( mockedDeps . letterQueueRepository . putLetter as jest . Mock )
254282 . mockResolvedValueOnce ( { } )
255283 . mockRejectedValueOnce ( new Error ( "DynamoDB error" ) ) ;
@@ -260,16 +288,16 @@ describe("update-letter-queue Lambda", () => {
260288 ] ) ;
261289 await handler ( testData , mockDeep < Context > ( ) , jest . fn ( ) ) ;
262290
263- assertSuccessMetricLogged ( 1 ) ;
264- assertFailureMetricLogged ( 1 ) ;
291+ assertQueueDeltaMetricLogged ( "supplier1" , 1 ) ;
265292 } ) ;
266293
267- it ( "emits failure metrics when a letter fails to be deleted" , async ( ) => {
294+ // eslint-disable-next-line jest/expect-expect
295+ it ( "counts a failed delete as zero" , async ( ) => {
268296 const handler = createHandler ( mockedDeps ) ;
269- const oldLetter1 = generateLetter ( "PENDING" , "1" ) ;
270- const oldLetter2 = generateLetter ( "PENDING" , "2" ) ;
271- const newLetter1 = generateLetter ( "ACCEPTED" , "1" ) ;
272- const newLetter2 = generateLetter ( "ACCEPTED" , "2" ) ;
297+ const oldLetter1 = generateLetter ( "PENDING" , { id : "1" } ) ;
298+ const oldLetter2 = generateLetter ( "PENDING" , { id : "2" } ) ;
299+ const newLetter1 = generateLetter ( "ACCEPTED" , { id : "1" } ) ;
300+ const newLetter2 = generateLetter ( "ACCEPTED" , { id : "2" } ) ;
273301 ( mockedDeps . letterQueueRepository . deleteLetter as jest . Mock )
274302 . mockResolvedValueOnce ( { } )
275303 . mockRejectedValueOnce ( new Error ( "DynamoDB error" ) ) ;
@@ -280,14 +308,14 @@ describe("update-letter-queue Lambda", () => {
280308 ] ) ;
281309 await handler ( testData , mockDeep < Context > ( ) , jest . fn ( ) ) ;
282310
283- assertSuccessMetricLogged ( 1 ) ;
284- assertFailureMetricLogged ( 1 ) ;
311+ assertQueueDeltaMetricLogged ( "supplier1" , - 1 ) ;
285312 } ) ;
286313
287- it ( "does not count a replayed insert as a success or failure" , async ( ) => {
314+ // eslint-disable-next-line jest/expect-expect
315+ it ( "counts a replayed insert as zero" , async ( ) => {
288316 const handler = createHandler ( mockedDeps ) ;
289- const newLetter1 = generateLetter ( "PENDING" , "1" ) ;
290- const newLetter2 = generateLetter ( "PENDING" , "2" ) ;
317+ const newLetter1 = generateLetter ( "PENDING" , { id : "1" } ) ;
318+ const newLetter2 = generateLetter ( "PENDING" , { id : "2" } ) ;
291319
292320 ( mockedDeps . letterQueueRepository . putLetter as jest . Mock )
293321 . mockRejectedValueOnce ( new LetterAlreadyExistsError ( "supplier1" , "1" ) )
@@ -299,16 +327,16 @@ describe("update-letter-queue Lambda", () => {
299327 ] ) ;
300328 await handler ( testData , mockDeep < Context > ( ) , jest . fn ( ) ) ;
301329
302- assertSuccessMetricLogged ( 1 ) ;
303- assertFailureMetricLogged ( 0 ) ;
330+ assertQueueDeltaMetricLogged ( "supplier1" , 1 ) ;
304331 } ) ;
305332
306- it ( "does not count a replayed delete as a success or failure" , async ( ) => {
333+ // eslint-disable-next-line jest/expect-expect
334+ it ( "counts a replayed delete as zero" , async ( ) => {
307335 const handler = createHandler ( mockedDeps ) ;
308- const oldLetter1 = generateLetter ( "PENDING" , "1" ) ;
309- const oldLetter2 = generateLetter ( "PENDING" , "2" ) ;
310- const newLetter1 = generateLetter ( "ACCEPTED" , "1" ) ;
311- const newLetter2 = generateLetter ( "ACCEPTED" , "2" ) ;
336+ const oldLetter1 = generateLetter ( "PENDING" , { id : "1" } ) ;
337+ const oldLetter2 = generateLetter ( "PENDING" , { id : "2" } ) ;
338+ const newLetter1 = generateLetter ( "ACCEPTED" , { id : "1" } ) ;
339+ const newLetter2 = generateLetter ( "ACCEPTED" , { id : "2" } ) ;
312340 ( mockedDeps . letterQueueRepository . deleteLetter as jest . Mock )
313341 . mockRejectedValueOnce ( new LetterDoesNotExistError ( "supplier1" , "1" ) )
314342 . mockResolvedValueOnce ( { } ) ;
@@ -319,19 +347,36 @@ describe("update-letter-queue Lambda", () => {
319347 ] ) ;
320348 await handler ( testData , mockDeep < Context > ( ) , jest . fn ( ) ) ;
321349
322- assertSuccessMetricLogged ( 1 ) ;
323- assertFailureMetricLogged ( 0 ) ;
350+ assertQueueDeltaMetricLogged ( "supplier1" , - 1 ) ;
324351 } ) ;
325352
326- it ( "emits zero success metrics when no pending letters are in the batch" , async ( ) => {
353+ // eslint-disable-next-line jest/expect-expect
354+ it ( "logs zero counts when no pending letters are in the batch" , async ( ) => {
327355 const handler = createHandler ( mockedDeps ) ;
328356 const newLetter = generateLetter ( "PRINTED" ) ;
329357
330358 const testData = generateKinesisEvent ( [ generateInsertRecord ( newLetter ) ] ) ;
331359 await handler ( testData , mockDeep < Context > ( ) , jest . fn ( ) ) ;
332360
333- assertSuccessMetricLogged ( 0 ) ;
334- assertFailureMetricLogged ( 0 ) ;
361+ assertQueueDeltaMetricNotLogged ( ) ;
362+ } ) ;
363+
364+ it ( "skips records with no NewImage (e.g. DELETE events) without error" , async ( ) => {
365+ const handler = createHandler ( mockedDeps ) ;
366+ const deleteRecord : DynamoDBRecord = {
367+ eventName : "REMOVE" ,
368+ dynamodb : { OldImage : mapToImage ( generateLetter ( "PENDING" ) ) } ,
369+ } ;
370+
371+ const testData = generateKinesisEvent ( [ deleteRecord ] ) ;
372+ const result = await handler ( testData , mockDeep < Context > ( ) , jest . fn ( ) ) ;
373+
374+ expect ( mockedDeps . letterQueueRepository . putLetter ) . not . toHaveBeenCalled ( ) ;
375+ expect (
376+ mockedDeps . letterQueueRepository . deleteLetter ,
377+ ) . not . toHaveBeenCalled ( ) ;
378+ expect ( result . batchItemFailures ) . toEqual ( [ ] ) ;
379+ assertQueueDeltaMetricNotLogged ( ) ;
335380 } ) ;
336381 } ) ;
337382} ) ;
@@ -378,43 +423,42 @@ function mapToImage(oldLetter: Letter) {
378423 ) ;
379424}
380425
381- function assertSuccessMetricLogged ( count : number ) {
426+ function assertQueueDeltaMetricLogged ( supplierId : string , delta : number ) {
382427 expect ( mockedDeps . logger . info ) . toHaveBeenCalledWith (
383428 expect . objectContaining ( {
429+ supplier : supplierId ,
384430 _aws : expect . objectContaining ( {
385431 CloudWatchMetrics : expect . arrayContaining ( [
386432 expect . objectContaining ( {
387433 Metrics : [
388434 expect . objectContaining ( {
389- Name : MetricStatus . Success ,
390- Value : count ,
435+ Name : "QueueDelta" ,
436+ Value : delta ,
437+ Unit : Unit . Count ,
391438 } ) ,
392439 ] ,
393440 } ) ,
394441 ] ) ,
395442 } ) ,
396- success : count ,
443+ QueueDelta : delta ,
397444 } ) ,
398445 ) ;
399446}
400447
401- function assertFailureMetricLogged ( count : number ) {
402- expect ( mockedDeps . logger . info ) . toHaveBeenCalledWith (
448+ function assertQueueDeltaMetricNotLogged ( ) {
449+ expect ( mockedDeps . logger . info ) . not . toHaveBeenCalledWith (
403450 expect . objectContaining ( {
404451 _aws : expect . objectContaining ( {
405452 CloudWatchMetrics : expect . arrayContaining ( [
406453 expect . objectContaining ( {
407454 Metrics : [
408455 expect . objectContaining ( {
409- Name : MetricStatus . Failure ,
410- Value : count ,
411- Unit : Unit . Count ,
456+ Name : "QueueDelta" ,
412457 } ) ,
413458 ] ,
414459 } ) ,
415460 ] ) ,
416461 } ) ,
417- failure : count ,
418462 } ) ,
419463 ) ;
420464}
0 commit comments