11import {
22 Letter ,
33 LetterAlreadyExistsError ,
4+ LetterDoesNotExistError ,
45 LetterQueueRepository ,
56} from "@internal/datastore" ;
67import { mockDeep } from "jest-mock-extended" ;
@@ -21,6 +22,7 @@ import { LetterStatus } from "../../../api-handler/src/contracts/letters";
2122const mockedDeps : jest . Mocked < Deps > = {
2223 letterQueueRepository : {
2324 putLetter : jest . fn ( ) ,
25+ deleteLetter : jest . fn ( ) ,
2426 } as unknown as LetterQueueRepository ,
2527 logger : {
2628 info : jest . fn ( ) ,
@@ -50,7 +52,7 @@ function generateLetter(status: LetterStatus, id?: string): Letter {
5052}
5153
5254beforeEach ( ( ) => {
53- jest . clearAllMocks ( ) ;
55+ jest . resetAllMocks ( ) ;
5456} ) ;
5557
5658describe ( "update-letter-queue Lambda" , ( ) => {
@@ -74,23 +76,25 @@ describe("update-letter-queue Lambda", () => {
7476 expect ( result . batchItemFailures ) . toEqual ( [ ] ) ;
7577 } ) ;
7678
77- it ( "does not publish updates " , async ( ) => {
79+ it ( "deletes letters that are no longer pending " , async ( ) => {
7880 const handler = createHandler ( mockedDeps ) ;
7981 const oldLetter = generateLetter ( "PENDING" ) ;
80- const newLetter = generateLetter ( "PENDING " ) ;
82+ const newLetter = generateLetter ( "ACCEPTED " ) ;
8183
8284 const testData = generateKinesisEvent ( [
8385 generateModifyRecord ( oldLetter , newLetter ) ,
8486 ] ) ;
8587 const result = await handler ( testData , mockDeep < Context > ( ) , jest . fn ( ) ) ;
8688
87- expect ( mockedDeps . letterQueueRepository . putLetter ) . not . toHaveBeenCalled ( ) ;
89+ expect (
90+ mockedDeps . letterQueueRepository . deleteLetter ,
91+ ) . toHaveBeenCalledWith ( "supplier1" , "1" ) ;
8892 expect ( result . batchItemFailures ) . toEqual ( [ ] ) ;
8993 } ) ;
9094
9195 it ( "does not publish non-PENDING letters" , async ( ) => {
9296 const handler = createHandler ( mockedDeps ) ;
93- const newLetter = generateLetter ( "PRINTED " ) ;
97+ const newLetter = generateLetter ( "ACCEPTED " ) ;
9498
9599 const testData = generateKinesisEvent ( [ generateInsertRecord ( newLetter ) ] ) ;
96100 const result = await handler ( testData , mockDeep < Context > ( ) , jest . fn ( ) ) ;
@@ -99,6 +103,22 @@ describe("update-letter-queue Lambda", () => {
99103 expect ( result . batchItemFailures ) . toEqual ( [ ] ) ;
100104 } ) ;
101105
106+ it ( "does not delete letters that are still PENDING" , async ( ) => {
107+ const handler = createHandler ( mockedDeps ) ;
108+ const oldLetter = generateLetter ( "PENDING" ) ;
109+ const newLetter = generateLetter ( "PENDING" ) ;
110+
111+ const testData = generateKinesisEvent ( [
112+ generateModifyRecord ( oldLetter , newLetter ) ,
113+ ] ) ;
114+ const result = await handler ( testData , mockDeep < Context > ( ) , jest . fn ( ) ) ;
115+
116+ expect (
117+ mockedDeps . letterQueueRepository . deleteLetter ,
118+ ) . not . toHaveBeenCalled ( ) ;
119+ expect ( result . batchItemFailures ) . toEqual ( [ ] ) ;
120+ } ) ;
121+
102122 it ( "handles empty Records array" , async ( ) => {
103123 const handler = createHandler ( mockedDeps ) ;
104124 const testData = { Records : [ ] } as unknown as KinesisStreamEvent ;
@@ -116,11 +136,10 @@ describe("update-letter-queue Lambda", () => {
116136 const newLetter = { id : "1" , status : "PENDING" } as Letter ;
117137
118138 const testData = generateKinesisEvent ( [ generateInsertRecord ( newLetter ) ] ) ;
119- await expect (
120- handler ( testData , mockDeep < Context > ( ) , jest . fn ( ) ) ,
121- ) . rejects . toThrow ( ) ;
139+ const result = await handler ( testData , mockDeep < Context > ( ) , jest . fn ( ) ) ;
122140
123141 expect ( mockedDeps . letterQueueRepository . putLetter ) . not . toHaveBeenCalled ( ) ;
142+ expect ( result . batchItemFailures ) . toEqual ( [ { itemIdentifier : "seq-0" } ] ) ;
124143 } ) ;
125144
126145 it ( "returns on the first failure" , async ( ) => {
@@ -143,7 +162,7 @@ describe("update-letter-queue Lambda", () => {
143162 expect ( result . batchItemFailures ) . toEqual ( [ { itemIdentifier : "seq-0" } ] ) ;
144163 } ) ;
145164
146- it ( "does not treat a replayed event as a failure" , async ( ) => {
165+ it ( "does not treat a replayed insert as a failure" , async ( ) => {
147166 const handler = createHandler ( mockedDeps ) ;
148167 const newLetter1 = generateLetter ( "PENDING" , "1" ) ;
149168 const newLetter2 = generateLetter ( "PENDING" , "2" ) ;
@@ -160,6 +179,25 @@ describe("update-letter-queue Lambda", () => {
160179 expect ( result . batchItemFailures ) . toEqual ( [ ] ) ;
161180 } ) ;
162181
182+ it ( "does not treat a replayed delete as a failure" , async ( ) => {
183+ const handler = createHandler ( mockedDeps ) ;
184+ const oldLetter1 = generateLetter ( "PENDING" , "1" ) ;
185+ const oldLetter2 = generateLetter ( "PENDING" , "2" ) ;
186+ const newLetter1 = generateLetter ( "ACCEPTED" , "1" ) ;
187+ const newLetter2 = generateLetter ( "ACCEPTED" , "2" ) ;
188+ ( mockedDeps . letterQueueRepository . putLetter as jest . Mock )
189+ . mockRejectedValueOnce ( new LetterDoesNotExistError ( "supplier1" , "1" ) )
190+ . mockResolvedValueOnce ( { } ) ;
191+
192+ const testData = generateKinesisEvent ( [
193+ generateModifyRecord ( oldLetter1 , newLetter1 ) ,
194+ generateModifyRecord ( oldLetter2 , newLetter2 ) ,
195+ ] ) ;
196+ const result = await handler ( testData , mockDeep < Context > ( ) , jest . fn ( ) ) ;
197+
198+ expect ( result . batchItemFailures ) . toEqual ( [ ] ) ;
199+ } ) ;
200+
163201 it ( "throws error when Kinesis payload cannot be parsed as JSON" , async ( ) => {
164202 const handler = createHandler ( mockedDeps ) ;
165203 const invalidJsonPayload = "not valid json {{{" ;
@@ -191,11 +229,12 @@ describe("update-letter-queue Lambda", () => {
191229 describe ( "Metrics" , ( ) => {
192230 it ( "emits success metrics when all letters are processed successfully" , async ( ) => {
193231 const handler = createHandler ( mockedDeps ) ;
194- const newLetter1 = generateLetter ( "PENDING" , "1" ) ;
232+ const oldLetter1 = generateLetter ( "PENDING" , "1" ) ;
233+ const newLetter1 = generateLetter ( "ACCEPTED" , "1" ) ;
195234 const newLetter2 = generateLetter ( "PENDING" , "2" ) ;
196235
197236 const testData = generateKinesisEvent ( [
198- generateInsertRecord ( newLetter1 ) ,
237+ generateModifyRecord ( oldLetter1 , newLetter1 ) ,
199238 generateInsertRecord ( newLetter2 ) ,
200239 ] ) ;
201240 await handler ( testData , mockDeep < Context > ( ) , jest . fn ( ) ) ;
@@ -204,7 +243,7 @@ describe("update-letter-queue Lambda", () => {
204243 assertFailureMetricLogged ( 0 ) ;
205244 } ) ;
206245
207- it ( "emits failure metrics when a letter fails to process " , async ( ) => {
246+ it ( "emits failure metrics when a letter fails to be inserted " , async ( ) => {
208247 const handler = createHandler ( mockedDeps ) ;
209248 const newLetter1 = generateLetter ( "PENDING" , "1" ) ;
210249 const newLetter2 = generateLetter ( "PENDING" , "2" ) ;
@@ -222,10 +261,31 @@ describe("update-letter-queue Lambda", () => {
222261 assertFailureMetricLogged ( 1 ) ;
223262 } ) ;
224263
225- it ( "does not count a reprocessed event as a success or failure" , async ( ) => {
264+ it ( "emits failure metrics when a letter fails to be deleted" , async ( ) => {
265+ const handler = createHandler ( mockedDeps ) ;
266+ const oldLetter1 = generateLetter ( "PENDING" , "1" ) ;
267+ const oldLetter2 = generateLetter ( "PENDING" , "2" ) ;
268+ const newLetter1 = generateLetter ( "ACCEPTED" , "1" ) ;
269+ const newLetter2 = generateLetter ( "ACCEPTED" , "2" ) ;
270+ ( mockedDeps . letterQueueRepository . deleteLetter as jest . Mock )
271+ . mockResolvedValueOnce ( { } )
272+ . mockRejectedValueOnce ( new Error ( "DynamoDB error" ) ) ;
273+
274+ const testData = generateKinesisEvent ( [
275+ generateModifyRecord ( oldLetter1 , newLetter1 ) ,
276+ generateModifyRecord ( oldLetter2 , newLetter2 ) ,
277+ ] ) ;
278+ await handler ( testData , mockDeep < Context > ( ) , jest . fn ( ) ) ;
279+
280+ assertSuccessMetricLogged ( 1 ) ;
281+ assertFailureMetricLogged ( 1 ) ;
282+ } ) ;
283+
284+ it ( "does not count a replayed insert as a success or failure" , async ( ) => {
226285 const handler = createHandler ( mockedDeps ) ;
227286 const newLetter1 = generateLetter ( "PENDING" , "1" ) ;
228287 const newLetter2 = generateLetter ( "PENDING" , "2" ) ;
288+
229289 ( mockedDeps . letterQueueRepository . putLetter as jest . Mock )
230290 . mockRejectedValueOnce ( new LetterAlreadyExistsError ( "supplier1" , "1" ) )
231291 . mockResolvedValueOnce ( { } ) ;
@@ -240,6 +300,26 @@ describe("update-letter-queue Lambda", () => {
240300 assertFailureMetricLogged ( 0 ) ;
241301 } ) ;
242302
303+ it ( "does not count a replayed delete as a success or failure" , async ( ) => {
304+ const handler = createHandler ( mockedDeps ) ;
305+ const oldLetter1 = generateLetter ( "PENDING" , "1" ) ;
306+ const oldLetter2 = generateLetter ( "PENDING" , "2" ) ;
307+ const newLetter1 = generateLetter ( "ACCEPTED" , "1" ) ;
308+ const newLetter2 = generateLetter ( "ACCEPTED" , "2" ) ;
309+ ( mockedDeps . letterQueueRepository . deleteLetter as jest . Mock )
310+ . mockRejectedValueOnce ( new LetterDoesNotExistError ( "supplier1" , "1" ) )
311+ . mockResolvedValueOnce ( { } ) ;
312+
313+ const testData = generateKinesisEvent ( [
314+ generateModifyRecord ( oldLetter1 , newLetter1 ) ,
315+ generateModifyRecord ( oldLetter2 , newLetter2 ) ,
316+ ] ) ;
317+ await handler ( testData , mockDeep < Context > ( ) , jest . fn ( ) ) ;
318+
319+ assertSuccessMetricLogged ( 1 ) ;
320+ assertFailureMetricLogged ( 0 ) ;
321+ } ) ;
322+
243323 it ( "emits zero success metrics when no pending letters are in the batch" , async ( ) => {
244324 const handler = createHandler ( mockedDeps ) ;
245325 const newLetter = generateLetter ( "PRINTED" ) ;
0 commit comments