diff --git a/lambdas/core-notifier-lambda/src/__tests__/apis/sqs-handler.test.ts b/lambdas/core-notifier-lambda/src/__tests__/apis/sqs-handler.test.ts index 473ab88b0..cb3892e5c 100644 --- a/lambdas/core-notifier-lambda/src/__tests__/apis/sqs-handler.test.ts +++ b/lambdas/core-notifier-lambda/src/__tests__/apis/sqs-handler.test.ts @@ -5,10 +5,10 @@ import { NotifyMessageProcessor } from 'app/notify-message-processor'; import { ISenderManagement } from 'sender-management'; import { SqsHandlerDependencies, createHandler } from 'apis/sqs-handler'; import { parseSqsRecord } from 'app/parse-sqs-message'; -import { InvalidPdmResourceAvailableEvent } from 'domain/invalid-pdm-resource-available-event'; import { RequestNotifyError } from 'domain/request-notify-error'; import { validPdmEvent, validSender } from '__tests__/constants'; import { + InvalidEvent, MessageRequestRejected, MessageRequestSkipped, MessageRequestSubmitted, @@ -195,14 +195,14 @@ describe('createHandler', () => { }); }); - describe('when parseSqsRecord throws InvalidPdmResourceAvailableEvent', () => { + describe('when parseSqsRecord throws InvalidEvent', () => { it('marks the message as failed for retry', async () => { const sqsEvent = createSqsEvent(1); const handler = createHandler(dependencies); const { messageId } = sqsEvent.Records[0]; mockParseSqsRecord.mockImplementationOnce(() => { - throw new InvalidPdmResourceAvailableEvent(messageId); + throw new InvalidEvent('Some validation errors'); }); const result = await handler(sqsEvent); @@ -211,7 +211,7 @@ describe('createHandler', () => { batchItemFailures: [{ itemIdentifier: messageId }], }); expect(mockLogger.warn).toHaveBeenCalledWith({ - error: 'Unable to parse PDMResourceAvailable event from SQS message', + error: 'Unable to parse event', description: 'Failed processing message', messageId, senderId: undefined, diff --git a/lambdas/core-notifier-lambda/src/__tests__/app/parse-sqs-message.test.ts b/lambdas/core-notifier-lambda/src/__tests__/app/parse-sqs-message.test.ts index a99eba2cd..d9040da98 100644 --- a/lambdas/core-notifier-lambda/src/__tests__/app/parse-sqs-message.test.ts +++ b/lambdas/core-notifier-lambda/src/__tests__/app/parse-sqs-message.test.ts @@ -2,10 +2,11 @@ import type { SQSRecord } from 'aws-lambda'; import { mock } from 'jest-mock-extended'; import { Logger } from 'utils'; import { parseSqsRecord } from 'app/parse-sqs-message'; -import { InvalidPdmResourceAvailableEvent } from 'domain/invalid-pdm-resource-available-event'; import { validPdmEvent } from '__tests__/constants'; +import { InvalidEvent } from 'digital-letters-events'; const mockLogger = mock(); +const mockChildLogger = mock(); describe('parseSqsRecord', () => { const messageId = 'test-message-id-123'; @@ -27,6 +28,10 @@ describe('parseSqsRecord', () => { awsRegion: 'eu-west-2', }); + beforeAll(() => { + mockLogger.child.mockReturnValue(mockChildLogger); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -38,13 +43,12 @@ describe('parseSqsRecord', () => { const result = parseSqsRecord(sqsRecord, mockLogger); expect(result).toEqual(validPdmEvent); - expect(mockLogger.info).toHaveBeenCalledWith({ + expect(mockLogger.child).toHaveBeenCalledWith({ messageId }); + expect(mockChildLogger.info).toHaveBeenCalledWith({ description: 'Parsing SQS Record', - messageId, }); - expect(mockLogger.info).toHaveBeenCalledWith({ + expect(mockChildLogger.info).toHaveBeenCalledWith({ description: 'Parsed valid PDMResourceAvailable Event', - messageId, messageReference: validPdmEvent.data.messageReference, senderId: validPdmEvent.data.senderId, resourceId: validPdmEvent.data.resourceId, @@ -53,20 +57,16 @@ describe('parseSqsRecord', () => { }); describe('when SQS record contains an invalid PDMResourceAvailable event', () => { - it('logs error and throws InvalidPdmResourceAvailableEvent', () => { + it('logs error and throws InvalidEvent', () => { const invalidEvent = { ...validPdmEvent, data: {} }; const sqsRecord = createSqsRecord(invalidEvent); - expect(() => parseSqsRecord(sqsRecord, mockLogger)).toThrow( - InvalidPdmResourceAvailableEvent, - ); - - expect(mockLogger.error).toHaveBeenCalledWith( + expect(() => parseSqsRecord(sqsRecord, mockLogger)).toThrow(InvalidEvent); + expect(mockLogger.child).toHaveBeenCalledWith({ messageId }); + expect(mockChildLogger.error).toHaveBeenCalledWith( expect.objectContaining({ - description: - 'The SQS message does not contain a valid PDMResourceAvailable event', - messageId, - error: expect.any(Array), + description: 'Error parsing PDMResourceAvailable event', + err: expect.any(Array), }), ); }); @@ -92,9 +92,8 @@ describe('parseSqsRecord', () => { }; expect(() => parseSqsRecord(sqsRecord, mockLogger)).toThrow(SyntaxError); - expect(mockLogger.info).toHaveBeenCalledWith({ + expect(mockChildLogger.info).toHaveBeenCalledWith({ description: 'Parsing SQS Record', - messageId, }); }); }); diff --git a/lambdas/core-notifier-lambda/src/apis/sqs-handler.ts b/lambdas/core-notifier-lambda/src/apis/sqs-handler.ts index 5d72d880e..8316c6edb 100644 --- a/lambdas/core-notifier-lambda/src/apis/sqs-handler.ts +++ b/lambdas/core-notifier-lambda/src/apis/sqs-handler.ts @@ -10,6 +10,9 @@ import { MessageRequestSkipped, MessageRequestSubmitted, PDMResourceAvailable, + validateMessageRequestRejected, + validateMessageRequestSkipped, + validateMessageRequestSubmitted, } from 'digital-letters-events'; import { mapPdmEventToMessageRequestRejected, @@ -17,9 +20,6 @@ import { mapPdmEventToMessageRequestSubmitted, mapPdmEventToSingleMessageRequest, } from 'domain/mapper'; -import messageRequestSubmittedValidator from 'digital-letters-events/MessageRequestSubmitted.js'; -import messageRequestRejectedValidator from 'digital-letters-events/MessageRequestRejected.js'; -import messageRequestSkippedValidator from 'digital-letters-events/MessageRequestSkipped.js'; import { parseSqsRecord } from 'app/parse-sqs-message'; import type { NotifyMessageProcessor } from 'app/notify-message-processor'; @@ -186,17 +186,17 @@ export const createHandler = ({ submittedEvents.length > 0 && eventPublisher.sendEvents( submittedEvents, - messageRequestSubmittedValidator, + validateMessageRequestSubmitted, ), skippedEvents.length > 0 && eventPublisher.sendEvents( skippedEvents, - messageRequestSkippedValidator, + validateMessageRequestSkipped, ), rejectedEvents.length > 0 && eventPublisher.sendEvents( rejectedEvents, - messageRequestRejectedValidator, + validateMessageRequestRejected, ), ].filter(Boolean), ); diff --git a/lambdas/core-notifier-lambda/src/app/parse-sqs-message.ts b/lambdas/core-notifier-lambda/src/app/parse-sqs-message.ts index ce78a3b06..522fce530 100644 --- a/lambdas/core-notifier-lambda/src/app/parse-sqs-message.ts +++ b/lambdas/core-notifier-lambda/src/app/parse-sqs-message.ts @@ -1,34 +1,26 @@ import type { SQSRecord } from 'aws-lambda'; import { Logger } from 'utils'; -import { PDMResourceAvailable } from 'digital-letters-events'; -import { InvalidPdmResourceAvailableEvent } from 'domain/invalid-pdm-resource-available-event'; -import messagePDMResourceAvailableValidator from 'digital-letters-events/PDMResourceAvailable.js'; +import { + PDMResourceAvailable, + validatePDMResourceAvailable, +} from 'digital-letters-events'; export const parseSqsRecord = ( sqsRecord: SQSRecord, logger: Logger, ): PDMResourceAvailable => { - logger.info({ + const childLogger = logger.child({ messageId: sqsRecord.messageId }); + childLogger.info({ description: 'Parsing SQS Record', - messageId: sqsRecord.messageId, }); const sqsEventBody = JSON.parse(sqsRecord.body); const sqsEventDetail = sqsEventBody.detail; - if (!messagePDMResourceAvailableValidator(sqsEventDetail)) { - logger.error({ - error: messagePDMResourceAvailableValidator.errors, - description: - 'The SQS message does not contain a valid PDMResourceAvailable event', - messageId: sqsRecord.messageId, - }); - throw new InvalidPdmResourceAvailableEvent(sqsRecord.messageId); - } + validatePDMResourceAvailable(sqsEventDetail, childLogger); - logger.info({ + childLogger.info({ description: 'Parsed valid PDMResourceAvailable Event', - messageId: sqsRecord.messageId, messageReference: sqsEventDetail.data.messageReference, senderId: sqsEventDetail.data.senderId, resourceId: sqsEventDetail.data.resourceId, diff --git a/lambdas/core-notifier-lambda/src/domain/invalid-pdm-resource-available-event.ts b/lambdas/core-notifier-lambda/src/domain/invalid-pdm-resource-available-event.ts deleted file mode 100644 index f803a3f9a..000000000 --- a/lambdas/core-notifier-lambda/src/domain/invalid-pdm-resource-available-event.ts +++ /dev/null @@ -1,8 +0,0 @@ -export class InvalidPdmResourceAvailableEvent extends Error { - readonly sqsMessageId: string; - - constructor(sqsMessageId: string) { - super('Unable to parse PDMResourceAvailable event from SQS message'); - this.sqsMessageId = sqsMessageId; - } -} diff --git a/lambdas/file-scanner-lambda/src/__tests__/apis/sqs-handler.test.ts b/lambdas/file-scanner-lambda/src/__tests__/apis/sqs-handler.test.ts index 6fb343295..0f961678e 100644 --- a/lambdas/file-scanner-lambda/src/__tests__/apis/sqs-handler.test.ts +++ b/lambdas/file-scanner-lambda/src/__tests__/apis/sqs-handler.test.ts @@ -345,7 +345,7 @@ describe('SQS Handler', () => { ); expect(mockLogger.warn).toHaveBeenCalledWith( expect.objectContaining({ - description: 'Error parsing queue entry', + description: 'Error parsing SQS record', }), ); }); @@ -405,7 +405,7 @@ describe('SQS Handler', () => { expect(mockFileScanner.scanFile).not.toHaveBeenCalled(); expect(mockLogger.warn).toHaveBeenCalledWith( expect.objectContaining({ - description: 'Error parsing queue entry', + description: 'Error parsing SQS record', }), ); expect(mockLogger.warn).toHaveBeenCalledWith( diff --git a/lambdas/file-scanner-lambda/src/apis/sqs-handler.ts b/lambdas/file-scanner-lambda/src/apis/sqs-handler.ts index d8b7b7062..7f438f5d3 100644 --- a/lambdas/file-scanner-lambda/src/apis/sqs-handler.ts +++ b/lambdas/file-scanner-lambda/src/apis/sqs-handler.ts @@ -4,8 +4,7 @@ import type { SQSBatchResponse, SQSEvent, } from 'aws-lambda'; -import { ItemDequeued } from 'digital-letters-events'; -import itemDequeuedValidator from 'digital-letters-events/ItemDequeued.js'; +import { ItemDequeued, validateItemDequeued } from 'digital-letters-events'; import { EventPublisher, Logger } from 'utils'; export interface HandlerDependencies { @@ -27,15 +26,7 @@ function validateRecord( const sqsEventBody = JSON.parse(body); const sqsEventDetail = sqsEventBody.detail; - const isEventValid = itemDequeuedValidator(sqsEventDetail); - if (!isEventValid) { - logger.warn({ - err: itemDequeuedValidator.errors, - description: 'Error parsing queue entry', - }); - - return null; - } + validateItemDequeued(sqsEventDetail, logger); return { messageId, event: sqsEventDetail }; } catch (error) { diff --git a/lambdas/move-scanned-files-lambda/src/__tests__/domain/mapper.test.ts b/lambdas/move-scanned-files-lambda/src/__tests__/domain/mapper.test.ts index 549b641c9..40826e9f6 100644 --- a/lambdas/move-scanned-files-lambda/src/__tests__/domain/mapper.test.ts +++ b/lambdas/move-scanned-files-lambda/src/__tests__/domain/mapper.test.ts @@ -1,6 +1,10 @@ +import { + validateFileQuarantined, + validateFileSafe, +} from 'digital-letters-events'; import { createFileQuarantinedEvent, createFileSafeEvent } from 'domain/mapper'; -import fileSafeValidator from 'digital-letters-events/FileSafe.js'; -import fileQuarantinedValidator from 'digital-letters-events/FileQuarantined.js'; +import { mock } from 'jest-mock-extended'; +import { Logger } from 'utils'; // Mock randomUUID to make tests deterministic jest.mock('node:crypto', () => ({ @@ -22,6 +26,7 @@ describe('mapper', () => { }); describe('createFileSafeEvent', () => { + const mockLogger = mock(); it('creates a FileSafe event with correct structure', () => { const messageReference = 'msg-ref-123'; const senderId = 'sender-456'; @@ -52,11 +57,7 @@ describe('mapper', () => { recordedtime: '2024-01-15T10:30:00.000Z', severitynumber: 2, }); - const isValid = fileSafeValidator(result); - if (!isValid) { - throw new Error(JSON.stringify(fileSafeValidator.errors, null, 2)); - } - expect(isValid).toBe(true); + expect(() => validateFileSafe(result, mockLogger)).not.toThrow(); }); it('handles different input values correctly', () => { @@ -80,6 +81,8 @@ describe('mapper', () => { }); describe('createFileQuarantinedEvent', () => { + const mockLogger = mock(); + it('creates a FileQuarantined event with correct structure', () => { const messageReference = 'msg-ref-789'; const senderId = 'sender-012'; @@ -111,13 +114,7 @@ describe('mapper', () => { recordedtime: '2024-01-15T10:30:00.000Z', severitynumber: 2, }); - const isValid = fileQuarantinedValidator(result); - if (!isValid) { - throw new Error( - JSON.stringify(fileQuarantinedValidator.errors, null, 2), - ); - } - expect(isValid).toBe(true); + expect(() => validateFileQuarantined(result, mockLogger)).not.toThrow(); }); }); }); diff --git a/lambdas/move-scanned-files-lambda/src/apis/sqs-handler.ts b/lambdas/move-scanned-files-lambda/src/apis/sqs-handler.ts index f142e35f6..11a2b9399 100644 --- a/lambdas/move-scanned-files-lambda/src/apis/sqs-handler.ts +++ b/lambdas/move-scanned-files-lambda/src/apis/sqs-handler.ts @@ -6,9 +6,12 @@ import type { SQSRecord, } from 'aws-lambda'; import { EventPublisher, Logger } from 'utils'; -import { FileQuarantined, FileSafe } from 'digital-letters-events'; -import fileSafeValidator from 'digital-letters-events/FileSafe.js'; -import fileQuarantinedValidator from 'digital-letters-events/FileQuarantined.js'; +import { + FileQuarantined, + FileSafe, + validateFileQuarantined, + validateFileSafe, +} from 'digital-letters-events'; import { parseSqsRecord } from 'app/parse-sqs-message'; import { MoveFileHandler } from 'app/move-file-handler'; @@ -69,14 +72,11 @@ export const createHandler = ({ await Promise.all( [ fileSafeEvents.length > 0 && - eventPublisher.sendEvents( - fileSafeEvents, - fileSafeValidator, - ), + eventPublisher.sendEvents(fileSafeEvents, validateFileSafe), fileQuarantinedEvents.length > 0 && eventPublisher.sendEvents( fileQuarantinedEvents, - fileQuarantinedValidator, + validateFileQuarantined, ), ].filter(Boolean), ); diff --git a/lambdas/pdm-poll-lambda/src/__tests__/apis/sqs-handler.test.ts b/lambdas/pdm-poll-lambda/src/__tests__/apis/sqs-handler.test.ts index abed2de86..271cfdbd1 100644 --- a/lambdas/pdm-poll-lambda/src/__tests__/apis/sqs-handler.test.ts +++ b/lambdas/pdm-poll-lambda/src/__tests__/apis/sqs-handler.test.ts @@ -296,13 +296,13 @@ describe('SQS Handler', () => { const result = await handler(event); - expect(logger.warn).toHaveBeenCalledWith({ + expect(logger.error).toHaveBeenCalledWith({ err: expect.arrayContaining([ expect.objectContaining({ instancePath: '/source', }), ]), - description: 'Error parsing queue entry', + description: 'Error parsing PDMResourceSubmitted event', }); expect(logger.info).toHaveBeenCalledWith( @@ -323,13 +323,13 @@ describe('SQS Handler', () => { const result = await handler(event); - expect(logger.warn).toHaveBeenCalledWith({ + expect(logger.error).toHaveBeenCalledWith({ err: expect.arrayContaining([ expect.objectContaining({ instancePath: '/source', }), ]), - description: 'Error parsing queue entry', + description: 'Error parsing PDMResourceUnavailable event', }); expect(logger.info).toHaveBeenCalledWith( diff --git a/lambdas/pdm-poll-lambda/src/apis/sqs-handler.ts b/lambdas/pdm-poll-lambda/src/apis/sqs-handler.ts index 092327f00..dc83e6b7d 100644 --- a/lambdas/pdm-poll-lambda/src/apis/sqs-handler.ts +++ b/lambdas/pdm-poll-lambda/src/apis/sqs-handler.ts @@ -9,11 +9,11 @@ import { PDMResourceRetriesExceeded, PDMResourceSubmitted, PDMResourceUnavailable, + validatePDMResourceAvailable, + validatePDMResourceRetriesExceeded, + validatePDMResourceSubmitted, + validatePDMResourceUnavailable, } from 'digital-letters-events'; -import pdmResourceAvailableValidator from 'digital-letters-events/PDMResourceAvailable.js'; -import pdmResourceSubmittedValidator from 'digital-letters-events/PDMResourceSubmitted.js'; -import pdmResourceUnavailableValidator from 'digital-letters-events/PDMResourceUnavailable.js'; -import pdmResourceRetriesExceededValidator from 'digital-letters-events/PDMResourceRetriesExceeded.js'; import { randomUUID } from 'node:crypto'; import { EventPublisher, Logger } from 'utils'; @@ -43,28 +43,12 @@ function validateRecord( sqsEventDetail.type === 'uk.nhs.notify.digital.letters.pdm.resource.submitted.v1' ) { - const isEventValid = pdmResourceSubmittedValidator(sqsEventDetail); - if (!isEventValid) { - logger.warn({ - err: pdmResourceSubmittedValidator.errors, - description: 'Error parsing queue entry', - }); - - return null; - } + validatePDMResourceSubmitted(sqsEventDetail, logger); return { messageId, event: sqsEventDetail }; } - const isEventValid = pdmResourceUnavailableValidator(sqsEventDetail); - if (!isEventValid) { - logger.warn({ - err: pdmResourceUnavailableValidator.errors, - description: 'Error parsing queue entry', - }); - - return null; - } + validatePDMResourceUnavailable(sqsEventDetail, logger); return { messageId, event: sqsEventDetail }; } catch (error) { @@ -211,17 +195,17 @@ export const createHandler = ({ availableEvents.length > 0 && eventPublisher.sendEvents( availableEvents, - pdmResourceAvailableValidator, + validatePDMResourceAvailable, ), unavailableEvents.length > 0 && eventPublisher.sendEvents( unavailableEvents, - pdmResourceUnavailableValidator, + validatePDMResourceUnavailable, ), retriesExceededEvents.length > 0 && eventPublisher.sendEvents( retriesExceededEvents, - pdmResourceRetriesExceededValidator, + validatePDMResourceRetriesExceeded, ), ].filter(Boolean), ); diff --git a/lambdas/pdm-uploader-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts b/lambdas/pdm-uploader-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts index e561fe080..5d37d31bc 100644 --- a/lambdas/pdm-uploader-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts +++ b/lambdas/pdm-uploader-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts @@ -225,7 +225,7 @@ describe('sqs-trigger-lambda', () => { expect(mockEventPublisher.sendEvents).not.toHaveBeenCalled(); expect(mockLogger.error).toHaveBeenCalledWith( expect.objectContaining({ - description: 'Error parsing queue entry', + description: 'Error parsing MESHInboxMessageDownloaded event', }), ); }); diff --git a/lambdas/pdm-uploader-lambda/src/apis/sqs-trigger-lambda.ts b/lambdas/pdm-uploader-lambda/src/apis/sqs-trigger-lambda.ts index 4f3232cab..38f402dcb 100644 --- a/lambdas/pdm-uploader-lambda/src/apis/sqs-trigger-lambda.ts +++ b/lambdas/pdm-uploader-lambda/src/apis/sqs-trigger-lambda.ts @@ -9,12 +9,12 @@ import type { UploadToPdmOutcome, UploadToPdmResult, } from 'app/upload-to-pdm'; -import messageDownloadedValidator from 'digital-letters-events/MESHInboxMessageDownloaded.js'; -import pdmResourceSubmittedValidator from 'digital-letters-events/PDMResourceSubmitted.js'; -import pdmResourceSubmissionRejectedValidator from 'digital-letters-events/PDMResourceSubmissionRejected.js'; import { MESHInboxMessageDownloaded, PDMResourceSubmitted, + validateMESHInboxMessageDownloaded, + validatePDMResourceSubmissionRejected, + validatePDMResourceSubmitted, } from 'digital-letters-events'; import { EventPublisher, Logger } from 'utils'; @@ -42,14 +42,7 @@ function validateRecord( const sqsEventBody = JSON.parse(body); const sqsEventDetail = sqsEventBody.detail; - const isEventValid = messageDownloadedValidator(sqsEventDetail); - if (!isEventValid) { - logger.error({ - err: messageDownloadedValidator.errors, - description: 'Error parsing queue entry', - }); - return null; - } + validateMESHInboxMessageDownloaded(sqsEventDetail, logger); return { messageId, event: sqsEventDetail }; } catch (error) { @@ -157,7 +150,7 @@ async function publishSuccessfulEvents( resourceId, }, })), - pdmResourceSubmittedValidator, + validatePDMResourceSubmitted, ); if (submittedFailedEvents.length > 0) { logger.warn({ @@ -199,7 +192,7 @@ async function publishFailedEvents( reasonCode: 'DL_PDMV_001', }, })), - pdmResourceSubmissionRejectedValidator, + validatePDMResourceSubmissionRejected, ); if (rejectedFailedEvents.length > 0) { logger.warn({ diff --git a/lambdas/print-analyser/src/__tests__/apis/sqs-handler.test.ts b/lambdas/print-analyser/src/__tests__/apis/sqs-handler.test.ts index 442a53129..800803e40 100644 --- a/lambdas/print-analyser/src/__tests__/apis/sqs-handler.test.ts +++ b/lambdas/print-analyser/src/__tests__/apis/sqs-handler.test.ts @@ -6,6 +6,8 @@ import { fileSafeEvent, fivePagePdf, recordEvent } from '__tests__/test-data'; import { FileSafe } from 'digital-letters-events'; const logger = mock(); +const mockChildLogger = mock(); +logger.child.mockReturnValue(mockChildLogger); const eventPublisher = mock(); jest.mock('node:crypto', () => ({ @@ -112,14 +114,16 @@ describe('SQS Handler', () => { const result = await handler(event); - expect(logger.warn).toHaveBeenCalledWith({ + expect(logger.child).toHaveBeenCalledWith({ + messageReference: fileSafeEvent.data.messageReference, + }); + expect(mockChildLogger.error).toHaveBeenCalledWith({ err: expect.arrayContaining([ expect.objectContaining({ instancePath: '/source', }), ]), - description: 'Error parsing print analyser queue entry', - messageReference: invalidFileSafeEvent.data.messageReference, + description: 'Error parsing FileSafe event', }); expect(logger.info).toHaveBeenCalledWith( @@ -137,14 +141,13 @@ describe('SQS Handler', () => { const result = await handler(event); - expect(logger.warn).toHaveBeenCalledWith({ + expect(mockChildLogger.error).toHaveBeenCalledWith({ err: expect.arrayContaining([ expect.objectContaining({ message: `must have required property 'specversion'`, }), ]), - description: 'Error parsing print analyser queue entry', - messageReference: 'not present', + description: 'Error parsing FileSafe event', }); expect(logger.info).toHaveBeenCalledWith( diff --git a/lambdas/print-analyser/src/apis/sqs-handler.ts b/lambdas/print-analyser/src/apis/sqs-handler.ts index 54bf51502..5299a2645 100644 --- a/lambdas/print-analyser/src/apis/sqs-handler.ts +++ b/lambdas/print-analyser/src/apis/sqs-handler.ts @@ -5,9 +5,12 @@ import type { } from 'aws-lambda'; import { createHash, randomUUID } from 'node:crypto'; import { PDFDocument } from 'pdf-lib'; -import { FileSafe, PDFAnalysed } from 'digital-letters-events'; -import fileSafeValidator from 'digital-letters-events/FileSafe.js'; -import pdfAnalysedValidator from 'digital-letters-events/PDFAnalysed.js'; +import { + FileSafe, + PDFAnalysed, + validateFileSafe, + validatePDFAnalysed, +} from 'digital-letters-events'; import { EventPublisher, Logger, getS3ObjectBufferFromUri } from 'utils'; export interface HandlerDependencies { @@ -33,17 +36,11 @@ function validateRecord( const sqsEventBody = JSON.parse(body); const sqsEventDetail = sqsEventBody.detail; - const isEventValid = fileSafeValidator(sqsEventDetail); - if (!isEventValid) { - logger.warn({ - err: fileSafeValidator.errors, - description: 'Error parsing print analyser queue entry', - messageReference: - sqsEventDetail?.data?.messageReference || 'not present', - }); + const messageReference = + sqsEventDetail?.data?.messageReference || 'not present'; + const childLogger = logger.child({ messageReference }); - return null; - } + validateFileSafe(sqsEventDetail, childLogger); return { messageId, event: sqsEventDetail }; } catch (error) { @@ -133,7 +130,7 @@ export const createHandler = ({ }), ); - await eventPublisher.sendEvents(validEvents, pdfAnalysedValidator); + await eventPublisher.sendEvents(validEvents, validatePDFAnalysed); const processedItemCount = receivedItemCount - batchItemFailures.length; logger.info( diff --git a/lambdas/print-sender-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts b/lambdas/print-sender-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts index d67793df7..c0d17cd8a 100644 --- a/lambdas/print-sender-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts +++ b/lambdas/print-sender-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts @@ -49,6 +49,7 @@ const createValidEvent = (overrides = {}): PDFAnalysed => ({ describe('sqs-trigger-lambda', () => { let mockPrintSender: jest.Mocked; let mockLogger: jest.Mocked; + let mockChildLogger: jest.Mocked; let handler: any; beforeEach(() => { @@ -56,10 +57,17 @@ describe('sqs-trigger-lambda', () => { send: jest.fn(), } as unknown as jest.Mocked; + mockChildLogger = { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + } as unknown as jest.Mocked; + mockLogger = { info: jest.fn(), error: jest.fn(), warn: jest.fn(), + child: jest.fn().mockReturnValue(mockChildLogger), } as unknown as jest.Mocked; handler = createHandler({ @@ -110,10 +118,9 @@ describe('sqs-trigger-lambda', () => { const result = await handler(sqsEvent); expect(result.batchItemFailures).toEqual([{ itemIdentifier: 'message-1' }]); - expect(mockLogger.error).toHaveBeenCalledWith( + expect(mockChildLogger.error).toHaveBeenCalledWith( expect.objectContaining({ - description: 'Error parsing print sender queue entry', - messageReference: 'not present', + description: 'Error parsing PDFAnalysed event', }), ); expect(mockPrintSender.send).not.toHaveBeenCalled(); diff --git a/lambdas/print-sender-lambda/src/__tests__/app/print-sender.test.ts b/lambdas/print-sender-lambda/src/__tests__/app/print-sender.test.ts index 529e571e2..639298964 100644 --- a/lambdas/print-sender-lambda/src/__tests__/app/print-sender.test.ts +++ b/lambdas/print-sender-lambda/src/__tests__/app/print-sender.test.ts @@ -119,10 +119,7 @@ describe('PrintSender', () => { mockEventPublisher.sendEvents.mockImplementation( async (events, validator) => { - const isValid = validator(events[0]); - if (!isValid) { - throw new Error('Event validation failed'); - } + validator(events[0], mockLogger); return []; }, ); @@ -160,12 +157,11 @@ describe('PrintSender', () => { const [[events, eventValidator]] = mockEventPublisher.sendEvents.mock.calls; const event = events[0] as LetterRequestPreparedEvent; - const validationResult = eventValidator(event); + expect(() => eventValidator(event, mockLogger)).not.toThrow(); expect(event.source).toBe( '/data-plane/digital-letters/staging-account/staging', ); - expect(validationResult).toBe(true); }); }); }); diff --git a/lambdas/print-sender-lambda/src/apis/sqs-trigger-lambda.ts b/lambdas/print-sender-lambda/src/apis/sqs-trigger-lambda.ts index a9936123b..f06751e65 100644 --- a/lambdas/print-sender-lambda/src/apis/sqs-trigger-lambda.ts +++ b/lambdas/print-sender-lambda/src/apis/sqs-trigger-lambda.ts @@ -5,8 +5,7 @@ import type { } from 'aws-lambda'; import type { PrintSender, PrintSenderOutcome } from 'app/print-sender'; import { Logger } from 'utils'; -import pdfAnalysedValidator from 'digital-letters-events/PDFAnalysed.js'; -import { PDFAnalysed } from 'digital-letters-events'; +import { validatePDFAnalysed } from 'digital-letters-events'; interface PrintSenderHandlerDependencies { printSender: PrintSender; @@ -27,20 +26,13 @@ export const createHandler = ({ const sqsEventBody = JSON.parse(body); const sqsEventDetail = sqsEventBody.detail; - const isEventValid = pdfAnalysedValidator(sqsEventDetail); - if (!isEventValid) { - logger.error({ - err: pdfAnalysedValidator.errors, - description: 'Error parsing print sender queue entry', - messageReference: - sqsEventDetail?.data?.messageReference || 'not present', - }); - batchItemFailures.push({ itemIdentifier: messageId }); - return; - } - const pdfAnalysedEvent: PDFAnalysed = sqsEventDetail; + const messageReference = + sqsEventDetail?.data?.messageReference || 'not present'; + const childLogger = logger.child({ messageReference }); + + validatePDFAnalysed(sqsEventDetail, childLogger); - const result = await printSender.send(pdfAnalysedEvent); + const result = await printSender.send(sqsEventDetail); if (result === 'failed') { batchItemFailures.push({ itemIdentifier: messageId }); diff --git a/lambdas/print-sender-lambda/src/app/print-sender.ts b/lambdas/print-sender-lambda/src/app/print-sender.ts index 2de2c563e..5772d5cdf 100644 --- a/lambdas/print-sender-lambda/src/app/print-sender.ts +++ b/lambdas/print-sender-lambda/src/app/print-sender.ts @@ -1,4 +1,4 @@ -import { PDFAnalysed } from 'digital-letters-events'; +import { InvalidEvent, PDFAnalysed } from 'digital-letters-events'; import { EventPublisher, Logger } from 'utils'; import { randomUUID } from 'node:crypto'; import { @@ -53,7 +53,16 @@ export class PrintSender { await this.eventPublisher.sendEvents( [letterPreparedEvent], - (event) => $LetterRequestPreparedEvent.safeParse(event).success, + (event, logger) => { + const parseResult = $LetterRequestPreparedEvent.safeParse(event); + if (!parseResult.success) { + logger.warn({ + err: parseResult.error, + description: 'Error parsing LetterRequestPreparedEvent event', + }); + throw new InvalidEvent('Invalid LetterRequestPreparedEvent'); + } + }, ); } catch (error) { this.logger.error({ diff --git a/lambdas/print-status-handler/src/apis/sqs-handler.ts b/lambdas/print-status-handler/src/apis/sqs-handler.ts index 47a15ed3f..d75d7a678 100644 --- a/lambdas/print-status-handler/src/apis/sqs-handler.ts +++ b/lambdas/print-status-handler/src/apis/sqs-handler.ts @@ -9,8 +9,10 @@ import { $LetterEvent, LetterEvent, } from '@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-events'; -import { PrintLetterTransitioned } from 'digital-letters-events'; -import printLetterTransitionedValidator from 'digital-letters-events/PrintLetterTransitioned.js'; +import { + PrintLetterTransitioned, + validatePrintLetterTransitioned, +} from 'digital-letters-events'; import { EventPublisher, Logger } from 'utils'; export interface HandlerDependencies { @@ -157,7 +159,7 @@ export const createHandler = ({ await eventPublisher.sendEvents( validEvents, - printLetterTransitionedValidator, + validatePrintLetterTransitioned, ); const processedItemCount = receivedItemCount - batchItemFailures.length; diff --git a/lambdas/report-generator/src/__tests__/apis/sqs-trigger-lambda.test.ts b/lambdas/report-generator/src/__tests__/apis/sqs-trigger-lambda.test.ts index b0eee1767..7cb243548 100644 --- a/lambdas/report-generator/src/__tests__/apis/sqs-trigger-lambda.test.ts +++ b/lambdas/report-generator/src/__tests__/apis/sqs-trigger-lambda.test.ts @@ -200,7 +200,7 @@ describe('sqs-trigger-lambda', () => { expect(response.batchItemFailures).toEqual([{ itemIdentifier: 'msg-1' }]); expect(mockLogger.error).toHaveBeenCalledWith( expect.objectContaining({ - description: 'Error parsing queue entry', + description: 'Error parsing GenerateReport event', }), ); expect(mockReportGenerator.generate).not.toHaveBeenCalled(); diff --git a/lambdas/report-generator/src/apis/sqs-trigger-lambda.ts b/lambdas/report-generator/src/apis/sqs-trigger-lambda.ts index 61536adf1..14c321f96 100644 --- a/lambdas/report-generator/src/apis/sqs-trigger-lambda.ts +++ b/lambdas/report-generator/src/apis/sqs-trigger-lambda.ts @@ -9,9 +9,12 @@ import type { ReportGeneratorOutcome, ReportGeneratorResult, } from 'app/report-generator'; -import generateReportValidator from 'digital-letters-events/GenerateReport.js'; -import reportGeneratedValidator from 'digital-letters-events/ReportGenerated.js'; -import { GenerateReport, ReportGenerated } from 'digital-letters-events'; +import { + GenerateReport, + ReportGenerated, + validateGenerateReport, + validateReportGenerated, +} from 'digital-letters-events'; import { EventPublisher, Logger } from 'utils'; interface ProcessingResult { @@ -38,14 +41,7 @@ function validateRecord( const sqsEventBody = JSON.parse(body); const sqsEventDetail = sqsEventBody.detail; - const isEventValid = generateReportValidator(sqsEventDetail); - if (!isEventValid) { - logger.error({ - err: generateReportValidator.errors, - description: 'Error parsing queue entry', - }); - return null; - } + validateGenerateReport(sqsEventDetail, logger); return { messageId, event: sqsEventDetail }; } catch (error) { @@ -149,7 +145,7 @@ async function publishSuccessfulEvents( const submittedFailedEvents = await eventPublisher.sendEvents( reportGeneratedEvents, - reportGeneratedValidator, + validateReportGenerated, ); return submittedFailedEvents; diff --git a/lambdas/report-scheduler/src/__tests__/apis/scheduled-event-handler.test.ts b/lambdas/report-scheduler/src/__tests__/apis/scheduled-event-handler.test.ts index 5788adeef..fe8cd4fa0 100644 --- a/lambdas/report-scheduler/src/__tests__/apis/scheduled-event-handler.test.ts +++ b/lambdas/report-scheduler/src/__tests__/apis/scheduled-event-handler.test.ts @@ -1,12 +1,12 @@ -import { EventPublisher, Sender } from 'utils'; +import { EventPublisher, Logger, Sender } from 'utils'; import { ISenderManagement } from 'sender-management'; -import { GenerateReport } from 'digital-letters-events'; +import { GenerateReport, validateGenerateReport } from 'digital-letters-events'; import { createHandler } from 'apis/scheduled-event-handler'; -import GenerateReportValidator from 'digital-letters-events/GenerateReport.js'; describe('scheduled-event-handler', () => { let mockSenderManagement: jest.Mocked; let mockEventPublisher: jest.Mocked; + let mockLogger: jest.Mocked; beforeEach(() => { mockSenderManagement = { @@ -17,6 +17,13 @@ describe('scheduled-event-handler', () => { sendEvents: jest.fn(), } as unknown as jest.Mocked; + mockLogger = { + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + child: jest.fn().mockReturnThis(), + } as unknown as jest.Mocked; + jest.useFakeTimers(); }); @@ -101,9 +108,7 @@ describe('scheduled-event-handler', () => { expect(event.time).toBe('2024-01-15T12:00:00.000Z'); expect(event.severitynumber).toBe(2); - const isEventValid = GenerateReportValidator(event); - expect(GenerateReportValidator.errors).toBeNull(); - expect(isEventValid).toBe(true); + expect(() => validateGenerateReport(event, mockLogger)).not.toThrow(); }); it('should handle empty sender list', async () => { diff --git a/lambdas/report-scheduler/src/apis/scheduled-event-handler.ts b/lambdas/report-scheduler/src/apis/scheduled-event-handler.ts index 801fd41a3..6e85a0dc2 100644 --- a/lambdas/report-scheduler/src/apis/scheduled-event-handler.ts +++ b/lambdas/report-scheduler/src/apis/scheduled-event-handler.ts @@ -1,7 +1,6 @@ import { EventPublisher } from 'utils'; import { ISenderManagement } from 'sender-management'; -import { GenerateReport } from 'digital-letters-events'; -import GenerateReportValidator from 'digital-letters-events/GenerateReport.js'; +import { GenerateReport, validateGenerateReport } from 'digital-letters-events'; import { randomUUID } from 'node:crypto'; export type CreateHandlerDependencies = { @@ -37,7 +36,7 @@ export const createHandler = ({ recordedtime: new Date().toISOString(), severitynumber: 2, })), - GenerateReportValidator, + validateGenerateReport, ); }; }; diff --git a/lambdas/ttl-create-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts b/lambdas/ttl-create-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts index e2320758b..f31d41cc2 100644 --- a/lambdas/ttl-create-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts +++ b/lambdas/ttl-create-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts @@ -1,8 +1,7 @@ import { messageDownloadedEvent } from '__tests__/data'; import { createHandler } from 'apis/sqs-trigger-lambda'; import type { SQSEvent } from 'aws-lambda'; -import { ItemEnqueued } from 'digital-letters-events'; -import itemEnqueuedValidator from 'digital-letters-events/ItemEnqueued.js'; +import { ItemEnqueued, validateItemEnqueued } from 'digital-letters-events'; import { randomUUID } from 'node:crypto'; jest.mock('node:crypto', () => ({ @@ -60,12 +59,14 @@ describe('createHandler', () => { expect(createTtl.send).toHaveBeenCalledWith(messageDownloadedEvent); expect(eventPublisher.sendEvents).toHaveBeenCalledWith( [itemEnqueuedEvent], - itemEnqueuedValidator, + validateItemEnqueued, ); const publishedEvent = eventPublisher.sendEvents.mock.lastCall?.[0]; expect(publishedEvent).toHaveLength(1); - expect(itemEnqueuedValidator(publishedEvent?.[0])).toBeTruthy(); + expect(() => + validateItemEnqueued(publishedEvent?.[0], logger), + ).not.toThrow(); expect(logger.info).toHaveBeenCalledWith({ description: 'Processed SQS Event.', @@ -85,7 +86,9 @@ describe('createHandler', () => { expect(res.batchItemFailures).toEqual([{ itemIdentifier: 'msg2' }]); expect(logger.error).toHaveBeenCalledWith( expect.objectContaining({ - description: expect.stringContaining('parsing ttl queue entry'), + description: expect.stringContaining( + 'Error parsing MESHInboxMessageDownloaded event', + ), }), ); expect(logger.info).toHaveBeenCalledWith({ @@ -178,7 +181,7 @@ describe('createHandler', () => { expect(createTtl.send).toHaveBeenCalledTimes(3); expect(eventPublisher.sendEvents).toHaveBeenCalledWith( [itemEnqueuedEvent, itemEnqueuedEvent, itemEnqueuedEvent], - itemEnqueuedValidator, + validateItemEnqueued, ); expect(logger.info).toHaveBeenCalledWith({ description: 'Processed SQS Event.', @@ -205,7 +208,7 @@ describe('createHandler', () => { expect(res.batchItemFailures).toEqual([]); expect(eventPublisher.sendEvents).toHaveBeenCalledWith( [itemEnqueuedEvent, itemEnqueuedEvent], - itemEnqueuedValidator, + validateItemEnqueued, ); expect(logger.warn).toHaveBeenCalledWith({ description: 'Some events failed to publish', @@ -228,7 +231,7 @@ describe('createHandler', () => { expect(res.batchItemFailures).toEqual([]); expect(eventPublisher.sendEvents).toHaveBeenCalledWith( [itemEnqueuedEvent], - itemEnqueuedValidator, + validateItemEnqueued, ); expect(logger.warn).toHaveBeenCalledWith({ err: publishError, @@ -277,7 +280,7 @@ describe('createHandler', () => { ]); expect(eventPublisher.sendEvents).toHaveBeenCalledWith( [itemEnqueuedEvent], - itemEnqueuedValidator, + validateItemEnqueued, ); expect(logger.info).toHaveBeenCalledWith({ description: 'Processed SQS Event.', diff --git a/lambdas/ttl-create-lambda/src/apis/sqs-trigger-lambda.ts b/lambdas/ttl-create-lambda/src/apis/sqs-trigger-lambda.ts index 5c9a91ab5..1ed224098 100644 --- a/lambdas/ttl-create-lambda/src/apis/sqs-trigger-lambda.ts +++ b/lambdas/ttl-create-lambda/src/apis/sqs-trigger-lambda.ts @@ -6,11 +6,11 @@ import type { import { randomUUID } from 'node:crypto'; import type { CreateTtl, CreateTtlOutcome } from 'app/create-ttl'; import { EventPublisher, Logger } from 'utils'; -import itemEnqueuedValidator from 'digital-letters-events/ItemEnqueued.js'; -import messageDownloadedValidator from 'digital-letters-events/MESHInboxMessageDownloaded.js'; import { ItemEnqueued, MESHInboxMessageDownloaded, + validateItemEnqueued, + validateMESHInboxMessageDownloaded, } from 'digital-letters-events'; interface ProcessingResult { @@ -37,27 +37,16 @@ export const createHandler = ({ try { const sqsEventBody = JSON.parse(body); const sqsEventDetail = sqsEventBody.detail; + validateMESHInboxMessageDownloaded(sqsEventDetail, logger); - const isEventValid = messageDownloadedValidator(sqsEventDetail); - if (!isEventValid) { - logger.error({ - err: messageDownloadedValidator.errors, - description: 'Error parsing ttl queue entry', - }); - batchItemFailures.push({ itemIdentifier: messageId }); - return { result: 'failed' }; - } - const messageDownloadedEvent: MESHInboxMessageDownloaded = - sqsEventDetail; - - const result = await createTtl.send(messageDownloadedEvent); + const result = await createTtl.send(sqsEventDetail); if (result === 'failed') { batchItemFailures.push({ itemIdentifier: messageId }); return { result: 'failed' }; } - return { result, item: messageDownloadedEvent }; + return { result, item: sqsEventDetail }; } catch (error) { logger.error({ err: error, @@ -113,7 +102,7 @@ export const createHandler = ({ messageUri: event.data.messageUri, }, })), - itemEnqueuedValidator, + validateItemEnqueued, ); if (failedEvents.length > 0) { logger.warn({ diff --git a/lambdas/ttl-handle-expiry-lambda/src/__tests__/apis/dynamodb-stream-handler.test.ts b/lambdas/ttl-handle-expiry-lambda/src/__tests__/apis/dynamodb-stream-handler.test.ts index 7ef9c53c6..ef3bee90b 100644 --- a/lambdas/ttl-handle-expiry-lambda/src/__tests__/apis/dynamodb-stream-handler.test.ts +++ b/lambdas/ttl-handle-expiry-lambda/src/__tests__/apis/dynamodb-stream-handler.test.ts @@ -3,7 +3,7 @@ import { EventPublisher, Logger } from 'utils'; import { mock } from 'jest-mock-extended'; import { createHandler } from 'apis/dynamodb-stream-handler'; import { Dlq } from 'app/dlq'; -import itemDequeuedValidator from 'digital-letters-events/ItemDequeued.js'; +import { validateItemDequeued } from 'digital-letters-events'; const logger = mock(); const eventPublisher = mock(); @@ -123,12 +123,14 @@ describe('createHandler', () => { }), }), ], - itemDequeuedValidator, + validateItemDequeued, ); const publishedEvent = eventPublisher.sendEvents.mock.lastCall?.[0]; expect(publishedEvent).toHaveLength(1); - expect(itemDequeuedValidator(publishedEvent?.[0])).toBeTruthy(); + expect(() => + validateItemDequeued(publishedEvent?.[0], logger), + ).not.toThrow(); expect(result).toEqual({}); }); @@ -247,10 +249,10 @@ describe('createHandler', () => { const result = await handler(mockInvalidEvent); - expect(logger.warn).toHaveBeenCalledWith( + expect(logger.error).toHaveBeenCalledWith( expect.objectContaining({ err: expect.any(Object), - description: 'Error parsing ttl item event', + description: 'Error parsing MESHInboxMessageDownloaded event', }), ); @@ -396,7 +398,7 @@ describe('createHandler', () => { }), }), ], - itemDequeuedValidator, + validateItemDequeued, ); expect(result).toEqual({}); }); diff --git a/lambdas/ttl-handle-expiry-lambda/src/apis/dynamodb-stream-handler.ts b/lambdas/ttl-handle-expiry-lambda/src/apis/dynamodb-stream-handler.ts index a3ba0f7bb..ead29c79e 100644 --- a/lambdas/ttl-handle-expiry-lambda/src/apis/dynamodb-stream-handler.ts +++ b/lambdas/ttl-handle-expiry-lambda/src/apis/dynamodb-stream-handler.ts @@ -5,12 +5,11 @@ import type { DynamoDBRecord, DynamoDBStreamEvent, } from 'aws-lambda'; -import type { +import { ItemDequeued, - MESHInboxMessageDownloaded, + validateItemDequeued, + validateMESHInboxMessageDownloaded, } from 'digital-letters-events'; -import itemDequeuedValidator from 'digital-letters-events/ItemDequeued.js'; -import messageDownloadedValidator from 'digital-letters-events/MESHInboxMessageDownloaded.js'; import { randomUUID } from 'node:crypto'; import { $TtlDynamodbRecord, EventPublisher, Logger } from 'utils'; @@ -20,10 +19,6 @@ export type CreateHandlerDependencies = { logger: Logger; }; -const eventValidator = messageDownloadedValidator as ( - d: unknown, -) => d is MESHInboxMessageDownloaded; - export const createHandler = ({ dlq, eventPublisher, @@ -68,19 +63,8 @@ export const createHandler = ({ return; } - let itemEvent: MESHInboxMessageDownloaded; - if (eventValidator(item.event)) { - itemEvent = item.event; - } else { - logger.warn({ - err: messageDownloadedValidator.errors, - description: 'Error parsing ttl item event', - }); - - failures.push(record); - - return; - } + const itemEvent = item.event; + validateMESHInboxMessageDownloaded(itemEvent, logger); if (item.withdrawn) { logger.info({ @@ -108,7 +92,7 @@ export const createHandler = ({ }, }, ], - itemDequeuedValidator, + validateItemDequeued, ); } } catch (error) { diff --git a/src/cloudevents/domains/digital-letters/2025-10-draft/defs/print.schema.yaml b/src/cloudevents/domains/digital-letters/2025-10-draft/defs/print.schema.yaml index 2ced7c8f7..2a1b80232 100644 --- a/src/cloudevents/domains/digital-letters/2025-10-draft/defs/print.schema.yaml +++ b/src/cloudevents/domains/digital-letters/2025-10-draft/defs/print.schema.yaml @@ -52,7 +52,6 @@ properties: examples: - "s3://my-bucket/path/to/my-object" createdAt: - title: "Created At DateTime" description: "Timestamp when the letter was created (RFC 3339)." examples: [ "2025-10-01T10:15:30.000Z" diff --git a/src/digital-letters-events/.gitignore b/src/digital-letters-events/.gitignore index 808d18e93..e26513150 100644 --- a/src/digital-letters-events/.gitignore +++ b/src/digital-letters-events/.gitignore @@ -1,3 +1,4 @@ validators types models +guard-functions diff --git a/src/digital-letters-events/errors/InvalidEvent.ts b/src/digital-letters-events/errors/InvalidEvent.ts new file mode 100644 index 000000000..597c2e3c0 --- /dev/null +++ b/src/digital-letters-events/errors/InvalidEvent.ts @@ -0,0 +1,8 @@ +export class InvalidEvent extends Error { + readonly errors: any; + + constructor(errors: any) { + super('Unable to parse event'); + this.errors = errors; + } +} diff --git a/src/digital-letters-events/errors/index.ts b/src/digital-letters-events/errors/index.ts new file mode 100644 index 000000000..efe5bb906 --- /dev/null +++ b/src/digital-letters-events/errors/index.ts @@ -0,0 +1 @@ +export * from './InvalidEvent'; diff --git a/src/digital-letters-events/index.ts b/src/digital-letters-events/index.ts new file mode 100644 index 000000000..c79fa396c --- /dev/null +++ b/src/digital-letters-events/index.ts @@ -0,0 +1,3 @@ +export * from './types'; +export * from './guard-functions'; +export * from './errors'; diff --git a/src/digital-letters-events/package.json b/src/digital-letters-events/package.json index 7b8327cef..c482fa68e 100644 --- a/src/digital-letters-events/package.json +++ b/src/digital-letters-events/package.json @@ -10,7 +10,7 @@ }, "exports": { ".": { - "types": "./types/index.d.ts" + "default": "./index.ts" }, "./*.js": { "default": "./validators/*.js", diff --git a/src/digital-letters-events/tsconfig.json b/src/digital-letters-events/tsconfig.json index c2099752d..0a4341bab 100644 --- a/src/digital-letters-events/tsconfig.json +++ b/src/digital-letters-events/tsconfig.json @@ -2,7 +2,8 @@ "compilerOptions": { "allowJs": true, "isolatedModules": true, - "outDir": "dist" + "outDir": "dist", + "rootDir": "." }, "exclude": [ "node_modules" @@ -10,6 +11,9 @@ "extends": "@tsconfig/node22/tsconfig.json", "include": [ "validators/**/*", - "types/**/*" + "types/**/*", + "guard-functions/**/*", + "index.ts", + "errors/**/*" ] } diff --git a/src/typescript-schema-generator/jest.config.ts b/src/typescript-schema-generator/jest.config.ts index a02be289f..8bc05484c 100644 --- a/src/typescript-schema-generator/jest.config.ts +++ b/src/typescript-schema-generator/jest.config.ts @@ -4,6 +4,7 @@ const config = { ...baseJestConfig, coveragePathIgnorePatterns: [ ...(baseJestConfig.coveragePathIgnorePatterns ?? []), + 'generate-guard-functions-cli.ts', 'src/generate-types-cli.ts', 'src/generate-validators-cli.ts', ], diff --git a/src/typescript-schema-generator/package.json b/src/typescript-schema-generator/package.json index 5059875e6..77a841fb1 100644 --- a/src/typescript-schema-generator/package.json +++ b/src/typescript-schema-generator/package.json @@ -18,7 +18,9 @@ "name": "typescript-schema-generator", "private": true, "scripts": { - "generate-dependencies": "npm run generate-types && npm run generate-validators", + "clean": "rm -rf ../digital-letters-events/guard-functions ../digital-letters-events/validators ../digital-letters-events/types", + "generate-dependencies": "npm run clean && npm run generate-types && npm run generate-validators && npm run generate-guard-functions", + "generate-guard-functions": "tsx src/generate-guard-functions-cli.ts", "generate-types": "tsx src/generate-types-cli.ts", "generate-validators": "tsx src/generate-validators-cli.ts", "lint": "eslint src", diff --git a/src/typescript-schema-generator/src/__tests__/generate-guard-functions.test.ts b/src/typescript-schema-generator/src/__tests__/generate-guard-functions.test.ts new file mode 100644 index 000000000..ab8418fca --- /dev/null +++ b/src/typescript-schema-generator/src/__tests__/generate-guard-functions.test.ts @@ -0,0 +1,66 @@ +/* eslint-disable security/detect-non-literal-fs-filename */ + +import { destinationPackageName } from 'file-utils'; +import { generateGuardFunctions } from 'generate-guard-functions'; +import mockFs from 'mock-fs'; +import { readFileSync, readdirSync } from 'node:fs'; +import path from 'node:path'; +import { eventSchemasDir } from 'utils'; + +jest.mock('json-schema-to-typescript'); + +describe('generate-guard-functions', () => { + const outputDir = path.resolve( + __dirname, + '..', + '..', + '..', + destinationPackageName, + 'guard-functions', + ); + + beforeEach(() => { + mockFs({ + [eventSchemasDir]: { + 'one.flattened.schema.json': '{"title": "One"}', + 'two.flattened.schema.json': '{"title": "Two"}', + 'three.flattened.schema.json': '{"title": "Three"}', + }, + }); + + jest.spyOn(console, 'log').mockImplementation(() => {}); + jest.spyOn(console, 'group').mockImplementation(() => {}); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('should generate a guard function file for each schema', async () => { + await generateGuardFunctions(); + + const typeDeclarationFiles = readdirSync(outputDir); + + expect(typeDeclarationFiles.length).toBe(4); + expect(typeDeclarationFiles).toEqual( + expect.arrayContaining([ + 'index.ts', + 'OneGuard.ts', + 'TwoGuard.ts', + 'ThreeGuard.ts', + ]), + ); + }); + + it('should create an index file exporting all generated guard function', async () => { + await generateGuardFunctions(); + + const indexFileContents = readFileSync( + path.join(outputDir, 'index.ts'), + 'utf8', + ); + expect(indexFileContents).toContain("export * from './OneGuard';"); + expect(indexFileContents).toContain("export * from './TwoGuard';"); + expect(indexFileContents).toContain("export * from './ThreeGuard';"); + }); +}); diff --git a/src/typescript-schema-generator/src/__tests__/generate-types.test.ts b/src/typescript-schema-generator/src/__tests__/generate-types.test.ts index 153fc005f..918e07604 100644 --- a/src/typescript-schema-generator/src/__tests__/generate-types.test.ts +++ b/src/typescript-schema-generator/src/__tests__/generate-types.test.ts @@ -46,12 +46,7 @@ describe('generate-types', () => { expect(typeDeclarationFiles.length).toBe(4); expect(typeDeclarationFiles).toEqual( - expect.arrayContaining([ - 'index.d.ts', - 'One.d.ts', - 'Two.d.ts', - 'Three.d.ts', - ]), + expect.arrayContaining(['index.ts', 'One.ts', 'Two.ts', 'Three.ts']), ); }); @@ -59,7 +54,7 @@ describe('generate-types', () => { await generateTypes(); const indexFileContents = readFileSync( - path.join(outputDir, 'index.d.ts'), + path.join(outputDir, 'index.ts'), 'utf8', ); expect(indexFileContents).toContain("export * from './One';"); diff --git a/src/typescript-schema-generator/src/generate-guard-functions-cli.ts b/src/typescript-schema-generator/src/generate-guard-functions-cli.ts new file mode 100644 index 000000000..692d3d925 --- /dev/null +++ b/src/typescript-schema-generator/src/generate-guard-functions-cli.ts @@ -0,0 +1,8 @@ +/* eslint-disable no-console */ + +import { generateGuardFunctions } from 'generate-guard-functions'; + +generateGuardFunctions().catch((error) => { + console.error('Error generating guard functions:', error); + throw error; +}); diff --git a/src/typescript-schema-generator/src/generate-guard-functions.ts b/src/typescript-schema-generator/src/generate-guard-functions.ts new file mode 100644 index 000000000..6cff242a6 --- /dev/null +++ b/src/typescript-schema-generator/src/generate-guard-functions.ts @@ -0,0 +1,53 @@ +/* eslint-disable no-console */ +import { createOutputDir, writeFile } from 'file-utils'; +import path from 'node:path'; +import { writeFileSync } from 'node:fs'; +import { eventSchemasDir, listEventSchemas, loadSchema } from 'utils'; + +export async function generateGuardFunctions() { + const eventSchemaFilenames = listEventSchemas(); + const outputDir = createOutputDir('guard-functions'); + console.log(`Output directory created at ${outputDir}`); + + console.group('Writing guard functions:'); + const indexLines: string[] = []; + for (const eventSchemaFilename of eventSchemaFilenames) { + const eventSchemaPath = path.join(eventSchemasDir, eventSchemaFilename); + const eventSchema = loadSchema(eventSchemaPath); + const typeName = eventSchema.title; + + const validatorVariableName = `event${typeName}Validator`; + + const guardFunction = `import ${validatorVariableName} from 'digital-letters-events/${typeName}.js'; +import { type ${typeName} } from '../types'; +import { InvalidEvent } from '../errors'; +import { Logger } from 'utils'; +import { ValidateFunction } from 'ajv'; + +const validator = ${validatorVariableName} as unknown as ValidateFunction; + +export function validate${typeName}( + event: unknown, + logger: Logger, +): asserts event is ${typeName} { + if (!validator(event)) { + logger.error({ + err: validator.errors, + description: 'Error parsing ${typeName} event', + }); + throw new InvalidEvent(validator.errors); + } +} +`; + + const typeDeclarationFilename = `${typeName}Guard.ts`; + writeFile(outputDir, typeDeclarationFilename, guardFunction); + console.log(typeDeclarationFilename); + + indexLines.push(`export * from './${typeName}Guard';`); + } + console.groupEnd(); + + writeFileSync(path.join(outputDir, 'index.ts'), `${indexLines.join('\n')}\n`); + console.log('index.ts file written'); +} diff --git a/src/typescript-schema-generator/src/generate-types.ts b/src/typescript-schema-generator/src/generate-types.ts index 81cc10a2f..d6384b839 100644 --- a/src/typescript-schema-generator/src/generate-types.ts +++ b/src/typescript-schema-generator/src/generate-types.ts @@ -1,6 +1,7 @@ /* eslint-disable no-console */ -import { createOutputDir, writeFile, writeTypesIndex } from 'file-utils'; +import { createOutputDir, writeFile } from 'file-utils'; import { compile } from 'json-schema-to-typescript'; +import { writeFileSync } from 'node:fs'; import path from 'node:path'; import { eventSchemasDir, listEventSchemas, loadSchema } from 'utils'; @@ -21,7 +22,7 @@ export async function generateTypes() { }); // Write a .d.ts file named after the schema title or file. - const typeDeclarationFilename = `${typeName}.d.ts`; + const typeDeclarationFilename = `${typeName}.ts`; writeFile(outputDir, typeDeclarationFilename, eventTs); console.log(typeDeclarationFilename); @@ -30,6 +31,6 @@ export async function generateTypes() { } console.groupEnd(); - writeTypesIndex(outputDir, indexLines); - console.log('index.d.ts file written'); + writeFileSync(path.join(outputDir, 'index.ts'), `${indexLines.join('\n')}\n`); + console.log('index.ts file written'); } diff --git a/tests/playwright/digital-letters-component-tests/core-notify.component.spec.ts b/tests/playwright/digital-letters-component-tests/core-notify.component.spec.ts index fb26d8032..de9e137fd 100644 --- a/tests/playwright/digital-letters-component-tests/core-notify.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/core-notify.component.spec.ts @@ -9,8 +9,10 @@ import { SENDER_ID_THAT_TRIGGERS_ERROR_IN_NOTIFY_SANDBOX, SENDER_ID_VALID_FOR_NOTIFY_SANDBOX, } from 'constants/tests-constants'; -import { PDMResourceAvailable } from 'digital-letters-events'; -import messagePDMResourceAvailableValidator from 'digital-letters-events/PDMResourceAvailable.js'; +import { + PDMResourceAvailable, + validatePDMResourceAvailable, +} from 'digital-letters-events'; import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import eventPublisher from 'helpers/event-bus-helpers'; import expectToPassEventually from 'helpers/expectations'; @@ -63,7 +65,7 @@ test.describe('Digital Letters - Core Notify', () => { }, }, ], - messagePDMResourceAvailableValidator, + validatePDMResourceAvailable, ); // Verify the event is processed and a message appears in the Lambda logs @@ -116,7 +118,7 @@ test.describe('Digital Letters - Core Notify', () => { }, }, ], - messagePDMResourceAvailableValidator, + validatePDMResourceAvailable, ); // Verify the event is processed and a message appears in the Lambda logs @@ -168,7 +170,7 @@ test.describe('Digital Letters - Core Notify', () => { }, }, ], - messagePDMResourceAvailableValidator, + validatePDMResourceAvailable, ); // Verify the event is published in the event bus @@ -206,7 +208,7 @@ test.describe('Digital Letters - Core Notify', () => { }, }, ], - messagePDMResourceAvailableValidator, + validatePDMResourceAvailable, ); await Promise.all([ diff --git a/tests/playwright/digital-letters-component-tests/file-scanner.component.spec.ts b/tests/playwright/digital-letters-component-tests/file-scanner.component.spec.ts index 48b277fb7..a1c7e5836 100644 --- a/tests/playwright/digital-letters-component-tests/file-scanner.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/file-scanner.component.spec.ts @@ -5,7 +5,6 @@ import { PREFIX_DL_FILES, REGION, } from 'constants/backend-constants'; -import itemDequeuedValidator from 'digital-letters-events/ItemDequeued.js'; import eventPublisher from 'helpers/event-bus-helpers'; import expectToPassEventually from 'helpers/expectations'; import { expectMessageContainingString, purgeQueue } from 'helpers/sqs-helpers'; @@ -15,6 +14,7 @@ import { getS3ObjectMetadata, putDataS3, } from 'utils'; +import { validateItemDequeued } from 'digital-letters-events'; const DOCUMENT_REFERENCE_BUCKET = `nhs-${process.env.AWS_ACCOUNT_ID}-${REGION}-${ENV}-dl-pii-data`; const UNSCANNED_FILES_BUCKET = `nhs-${process.env.AWS_ACCOUNT_ID}-${REGION}-main-acct-digi-unscanned-files`; @@ -77,7 +77,7 @@ test('should extract PDF from DocumentReference and store in unscanned bucket wi }, }, ], - itemDequeuedValidator, + validateItemDequeued, ); await expectToPassEventually(async () => { @@ -143,7 +143,7 @@ test('should handle validation errors by sending messages to DLQ', async () => { }, }, ], - itemDequeuedValidator, + validateItemDequeued, ); // Verify the file was NOT processed successfully diff --git a/tests/playwright/digital-letters-component-tests/mesh-acknowledge.component.spec.ts b/tests/playwright/digital-letters-component-tests/mesh-acknowledge.component.spec.ts index 8fe63d32b..a55e148bb 100644 --- a/tests/playwright/digital-letters-component-tests/mesh-acknowledge.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/mesh-acknowledge.component.spec.ts @@ -5,8 +5,10 @@ import { NON_PII_S3_BUCKET_NAME, } from 'constants/backend-constants'; import { SENDER_ID_SKIPS_NOTIFY } from 'constants/tests-constants'; -import { MESHInboxMessageDownloaded } from 'digital-letters-events'; -import messageDownloadedValidator from 'digital-letters-events/MESHInboxMessageDownloaded.js'; +import { + MESHInboxMessageDownloaded, + validateMESHInboxMessageDownloaded, +} from 'digital-letters-events'; import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import eventPublisher from 'helpers/event-bus-helpers'; import expectToPassEventually from 'helpers/expectations'; @@ -68,7 +70,7 @@ test.describe('Digital Letters - Mesh Acknowledger', () => { }, }, ], - messageDownloadedValidator, + validateMESHInboxMessageDownloaded, ); // The mailbox ID matches the Mock MESH config in SSM. @@ -126,7 +128,7 @@ test.describe('Digital Letters - Mesh Acknowledger', () => { }, }, ], - messageDownloadedValidator, + validateMESHInboxMessageDownloaded, ); await expectMessageContainingString( diff --git a/tests/playwright/digital-letters-component-tests/mesh-poll-download.component.spec.ts b/tests/playwright/digital-letters-component-tests/mesh-poll-download.component.spec.ts index 430b8512b..7fde42e51 100644 --- a/tests/playwright/digital-letters-component-tests/mesh-poll-download.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/mesh-poll-download.component.spec.ts @@ -13,8 +13,8 @@ import { invokeLambda } from 'helpers/lambda-helpers'; import { downloadFromS3, uploadToS3 } from 'helpers/s3-helpers'; import { expectMessageContainingString } from 'helpers/sqs-helpers'; import { v4 as uuidv4 } from 'uuid'; -import messageMessageReceived from 'digital-letters-events/MESHInboxMessageReceived.js'; import { SENDER_ID_SKIPS_NOTIFY } from 'constants/tests-constants'; +import { validateMESHInboxMessageReceived } from 'digital-letters-events'; test.describe('Digital Letters - MESH Poll and Download', () => { const senderId = SENDER_ID_SKIPS_NOTIFY; @@ -142,7 +142,7 @@ test.describe('Digital Letters - MESH Poll and Download', () => { }, }, ], - messageMessageReceived, + validateMESHInboxMessageReceived, ); await expectMessageContainingString( diff --git a/tests/playwright/digital-letters-component-tests/pdm-poll.component.spec.ts b/tests/playwright/digital-letters-component-tests/pdm-poll.component.spec.ts index 0d0d04548..8d31cfb8d 100644 --- a/tests/playwright/digital-letters-component-tests/pdm-poll.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/pdm-poll.component.spec.ts @@ -4,8 +4,10 @@ import { PDM_POLL_DLQ_NAME, PDM_POLL_LAMBDA_LOG_GROUP_NAME, } from 'constants/backend-constants'; -import pdmResourceSubmittedValidator from 'digital-letters-events/PDMResourceSubmitted.js'; -import pdmResourceUnavailableValidator from 'digital-letters-events/PDMResourceUnavailable.js'; +import { + validatePDMResourceSubmitted, + validatePDMResourceUnavailable, +} from 'digital-letters-events'; import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import eventPublisher from 'helpers/event-bus-helpers'; import expectToPassEventually from 'helpers/expectations'; @@ -65,7 +67,7 @@ test.describe('PDM Poll', () => { }, }, ], - pdmResourceSubmittedValidator, + validatePDMResourceSubmitted, ); await expectToPassEventually(async () => { @@ -102,7 +104,7 @@ test.describe('PDM Poll', () => { }, }, ], - pdmResourceSubmittedValidator, + validatePDMResourceSubmitted, ); await expectToPassEventually(async () => { @@ -141,7 +143,7 @@ test.describe('PDM Poll', () => { }, }, ], - pdmResourceUnavailableValidator, + validatePDMResourceUnavailable, ); await expectToPassEventually(async () => { @@ -179,7 +181,7 @@ test.describe('PDM Poll', () => { }, }, ], - pdmResourceUnavailableValidator, + validatePDMResourceUnavailable, ); await expectToPassEventually(async () => { @@ -216,7 +218,7 @@ test.describe('PDM Poll', () => { }, }, ], - pdmResourceUnavailableValidator, + validatePDMResourceUnavailable, ); await expectToPassEventually(async () => { diff --git a/tests/playwright/digital-letters-component-tests/pdm-uploader.component.spec.ts b/tests/playwright/digital-letters-component-tests/pdm-uploader.component.spec.ts index e5ba36dcd..5b2993eb2 100644 --- a/tests/playwright/digital-letters-component-tests/pdm-uploader.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/pdm-uploader.component.spec.ts @@ -5,7 +5,6 @@ import { PDM_UPLOADER_DLQ_NAME, PDM_UPLOADER_LAMBDA_LOG_GROUP_NAME, } from 'constants/backend-constants'; -import messageDownloadedValidator from 'digital-letters-events/MESHInboxMessageDownloaded.js'; import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import eventPublisher from 'helpers/event-bus-helpers'; import expectToPassEventually from 'helpers/expectations'; @@ -13,6 +12,7 @@ import { expectMessageContainingString, purgeQueue } from 'helpers/sqs-helpers'; import { v4 as uuidv4 } from 'uuid'; import { SENDER_ID_SKIPS_NOTIFY } from 'constants/tests-constants'; import { putDataS3 } from 'utils'; +import { validateMESHInboxMessageDownloaded } from 'digital-letters-events'; const pdmRequest = { resourceType: 'DocumentReference', @@ -84,7 +84,7 @@ test.describe('Digital Letters - Upload to PDM', () => { }, }, ], - messageDownloadedValidator, + validateMESHInboxMessageDownloaded, ); await expectToPassEventually(async () => { @@ -150,7 +150,7 @@ test.describe('Digital Letters - Upload to PDM', () => { }, }, ], - messageDownloadedValidator, + validateMESHInboxMessageDownloaded, ); await expectToPassEventually(async () => { diff --git a/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts b/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts index 7638fe3bb..46720477c 100644 --- a/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts @@ -10,8 +10,7 @@ import eventPublisher from 'helpers/event-bus-helpers'; import expectToPassEventually from 'helpers/expectations'; import { fivePagePdf } from 'helpers/pdf-helpers'; import { v4 as uuidv4 } from 'uuid'; -import fileSafeValidator from 'digital-letters-events/FileSafe.js'; -import { FileSafe } from 'digital-letters-events'; +import { FileSafe, validateFileSafe } from 'digital-letters-events'; import { expectMessageContainingString, purgeQueue } from 'helpers/sqs-helpers'; import { putFileS3 } from 'utils'; @@ -64,7 +63,7 @@ test.describe('Print analyser', () => { }, }; - await eventPublisher.sendEvents([event], fileSafeValidator); + await eventPublisher.sendEvents([event], validateFileSafe); await expectToPassEventually(async () => { const eventLogEntry = await getLogsFromCloudwatch( @@ -104,9 +103,9 @@ test.describe('Print analyser', () => { const eventLogEntry = await getLogsFromCloudwatch( PRINT_ANALYSER_LAMBDA_LOG_GROUP_NAME, [ - '$.message.description = "Error parsing print analyser queue entry"', + '$.message.description = "Error parsing FileSafe event"', `$.message.err[0].message = "must have required property 'senderId'"`, - `$.message.messageReference = "${messageReference}"`, + `$.messageReference = "${messageReference}"`, ], ); diff --git a/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts b/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts index 4afa53674..b761c54ab 100644 --- a/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts @@ -4,8 +4,7 @@ import { PRINT_SENDER_DLQ_NAME, PRINT_SENDER_LAMBDA_LOG_GROUP_NAME, } from 'constants/backend-constants'; -import { PDFAnalysed } from 'digital-letters-events'; -import pdfAnalysedValidator from 'digital-letters-events/PDFAnalysed.js'; +import { PDFAnalysed, validatePDFAnalysed } from 'digital-letters-events'; import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import eventPublisher from 'helpers/event-bus-helpers'; import expectToPassEventually from 'helpers/expectations'; @@ -52,7 +51,7 @@ test.describe('Digital Letters - Print Sender', () => { }, }, ], - pdfAnalysedValidator, + validatePDFAnalysed, ); // Verify letter prepared event published @@ -103,9 +102,9 @@ test.describe('Digital Letters - Print Sender', () => { const eventLogEntry = await getLogsFromCloudwatch( PRINT_SENDER_LAMBDA_LOG_GROUP_NAME, [ - '$.message.description = "Error parsing print sender queue entry"', + '$.message.description = "Error parsing PDFAnalysed event"', `$.message.err[0].message = "must have required property 'senderId'"`, - `$.message.messageReference = "${messageReference}"`, + `$.messageReference = "${messageReference}"`, ], ); diff --git a/tests/playwright/digital-letters-component-tests/send-reports-trust.component.spec.ts b/tests/playwright/digital-letters-component-tests/send-reports-trust.component.spec.ts index 5c5bd417b..2a8311680 100644 --- a/tests/playwright/digital-letters-component-tests/send-reports-trust.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/send-reports-trust.component.spec.ts @@ -11,8 +11,8 @@ import expectToPassEventually from 'helpers/expectations'; import { downloadFromS3, uploadToS3 } from 'helpers/s3-helpers'; import { expectMessageContainingString } from 'helpers/sqs-helpers'; import { v4 as uuidv4 } from 'uuid'; -import reportGenerated from 'digital-letters-events/ReportGenerated.js'; import { SENDER_ID_SKIPS_NOTIFY } from 'constants/tests-constants'; +import { validateReportGenerated } from 'digital-letters-events'; test.describe('Digital Letters - Send reports to Trust', () => { const senderId = SENDER_ID_SKIPS_NOTIFY; @@ -44,7 +44,7 @@ test.describe('Digital Letters - Send reports to Trust', () => { }, }, ], - reportGenerated, + validateReportGenerated, ); } diff --git a/tests/playwright/digital-letters-component-tests/ttl-create.component.spec.ts b/tests/playwright/digital-letters-component-tests/ttl-create.component.spec.ts index 42d626ef2..6a5d35fa2 100644 --- a/tests/playwright/digital-letters-component-tests/ttl-create.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/ttl-create.component.spec.ts @@ -8,8 +8,10 @@ import { SENDER_ID_SKIPS_NOTIFY, SENDER_ID_VALID_FOR_NOTIFY_SANDBOX, } from 'constants/tests-constants'; -import { MESHInboxMessageDownloaded } from 'digital-letters-events'; -import messageDownloadedValidator from 'digital-letters-events/MESHInboxMessageDownloaded.js'; +import { + MESHInboxMessageDownloaded, + validateMESHInboxMessageDownloaded, +} from 'digital-letters-events'; import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import { getTtl } from 'helpers/dynamodb-helpers'; import eventPublisher from 'helpers/event-bus-helpers'; @@ -61,7 +63,7 @@ test.describe('Digital Letters - Create TTL', () => { }, }, ], - messageDownloadedValidator, + validateMESHInboxMessageDownloaded, ); // Verify TTL created @@ -102,7 +104,7 @@ test.describe('Digital Letters - Create TTL', () => { }, }, ], - messageDownloadedValidator, + validateMESHInboxMessageDownloaded, ); // Verify TTL created @@ -154,7 +156,7 @@ test.describe('Digital Letters - Create TTL', () => { const eventLogEntry = await getLogsFromCloudwatch( CREATE_TTL_LAMBDA_LOG_GROUP_NAME, [ - '$.message.description = "Error parsing ttl queue entry"', + '$.message.description = "Error parsing MESHInboxMessageDownloaded event"', `$.message.err[0].params.additionalProperty = "${unexpectedField}"`, ], ); @@ -185,7 +187,7 @@ test.describe('Digital Letters - Create TTL', () => { }, }, ], - messageDownloadedValidator, + validateMESHInboxMessageDownloaded, ); await Promise.all([ diff --git a/tests/playwright/helpers/report-helpers.ts b/tests/playwright/helpers/report-helpers.ts index 5b5fa25c9..6e02b8b29 100644 --- a/tests/playwright/helpers/report-helpers.ts +++ b/tests/playwright/helpers/report-helpers.ts @@ -19,16 +19,16 @@ import { PDMResourceRetriesExceeded, PDMResourceSubmissionRejected, PrintLetterTransitioned, + validateDigitalLetterRead, + validateFileQuarantined, + validateGenerateReport, + validateItemDequeued, + validateMessageRequestRejected, + validateMessageRequestSkipped, + validatePDMResourceRetriesExceeded, + validatePDMResourceSubmissionRejected, + validatePrintLetterTransitioned, } from 'digital-letters-events'; -import generateReportValidator from 'digital-letters-events/GenerateReport.js'; -import digitalLetterReadValidator from 'digital-letters-events/DigitalLetterRead.js'; -import itemDequeuedValidator from 'digital-letters-events/ItemDequeued.js'; -import messageRequestSkippedValidator from 'digital-letters-events/MessageRequestSkipped.js'; -import printLetterTransitionedValidator from 'digital-letters-events/PrintLetterTransitioned.js'; -import pdmResourceSubmissionRejectedValidator from 'digital-letters-events/PDMResourceSubmissionRejected.js'; -import pdmResourceRetriesExceededValidator from 'digital-letters-events/PDMResourceRetriesExceeded.js'; -import messageRequestRejectedValidator from 'digital-letters-events/MessageRequestRejected.js'; -import fileQuarantinedValidator from 'digital-letters-events/FileQuarantined.js'; import { QueryExecutionState, getQueryState, @@ -142,7 +142,7 @@ export function publishEventForScenario(scenario: ReportScenario) { scenario.senderId, ), ], - digitalLetterReadValidator, + validateDigitalLetterRead, ); break; } @@ -156,7 +156,7 @@ export function publishEventForScenario(scenario: ReportScenario) { scenario.senderId, ), ], - itemDequeuedValidator, + validateItemDequeued, ); break; } @@ -170,7 +170,7 @@ export function publishEventForScenario(scenario: ReportScenario) { scenario.senderId, ), ], - pdmResourceSubmissionRejectedValidator, + validatePDMResourceSubmissionRejected, ); break; } @@ -184,7 +184,7 @@ export function publishEventForScenario(scenario: ReportScenario) { scenario.senderId, ), ], - pdmResourceRetriesExceededValidator, + validatePDMResourceRetriesExceeded, ); break; } @@ -198,7 +198,7 @@ export function publishEventForScenario(scenario: ReportScenario) { scenario.senderId, ), ], - messageRequestRejectedValidator, + validateMessageRequestRejected, ); break; } @@ -217,7 +217,7 @@ export function publishEventForScenario(scenario: ReportScenario) { scenario.senderId, ), ], - fileQuarantinedValidator, + validateFileQuarantined, ); } else { eventPublisher.sendEvents( @@ -232,7 +232,7 @@ export function publishEventForScenario(scenario: ReportScenario) { scenario.expectedReason, ), ], - printLetterTransitionedValidator, + validatePrintLetterTransitioned, ); } break; @@ -276,7 +276,7 @@ export async function publishGenerateReport( }, }, ], - generateReportValidator, + validateGenerateReport, ); } @@ -310,7 +310,7 @@ export async function publishEventNotInReports(senderId: string) { }, }, ], - messageRequestSkippedValidator, + validateMessageRequestSkipped, ); } diff --git a/utils/utils/src/__tests__/event-publisher/event-publisher.test.ts b/utils/utils/src/__tests__/event-publisher/event-publisher.test.ts index cb6f44e6b..34b081e82 100644 --- a/utils/utils/src/__tests__/event-publisher/event-publisher.test.ts +++ b/utils/utils/src/__tests__/event-publisher/event-publisher.test.ts @@ -101,7 +101,9 @@ describe('Event Publishing', () => { }); const publisher = new EventPublisher(testConfig); - const result = await publisher.sendEvents(events, () => false); + const result = await publisher.sendEvents(events, () => { + throw new Error('Some validation error'); + }); expect(result).toEqual([]); expect(eventBridgeMock.calls()).toHaveLength(0); @@ -186,7 +188,9 @@ describe('Event Publishing', () => { }); const publisher = new EventPublisher(testConfig); - const result = await publisher.sendEvents(events, () => false); + const result = await publisher.sendEvents(events, () => { + throw new Error('Some validation error'); + }); expect(result).toEqual([event]); expect(eventBridgeMock.calls()).toHaveLength(0); @@ -197,7 +201,9 @@ describe('Event Publishing', () => { sqsMock.on(SendMessageBatchCommand).rejects(new Error('DLQ error')); const publisher = new EventPublisher(testConfig); - const result = await publisher.sendEvents(events, () => false); + const result = await publisher.sendEvents(events, () => { + throw new Error('Some validation error'); + }); expect(result).toEqual(events); expect(eventBridgeMock.calls()).toHaveLength(0); @@ -359,14 +365,14 @@ describe('Event Publishing', () => { }); const publisher = new EventPublisher(testConfig); - const result = await publisher.sendEvents( - allEvents, - (e) => - !( - e.id.includes('22222222-2222-2222-2222') || - e.id.includes('33333333-3333-3333-3333') - ), - ); + const result = await publisher.sendEvents(allEvents, (e) => { + if ( + e.id.includes('22222222-2222-2222-2222') || + e.id.includes('33333333-3333-3333-3333') + ) { + throw new Error('Some validation error'); + } + }); expect(result).toHaveLength( invalidAndDlqError.length + eventBridgeAndDlqError.length, diff --git a/utils/utils/src/event-publisher/event-publisher.ts b/utils/utils/src/event-publisher/event-publisher.ts index e7b6b45f6..e4fdc20c9 100644 --- a/utils/utils/src/event-publisher/event-publisher.ts +++ b/utils/utils/src/event-publisher/event-publisher.ts @@ -20,7 +20,7 @@ export interface EventPublisherDependencies { type PublishableEvent = { id: string; source: string; type: string }; -type EventValidationFunction = { (event: T): boolean; errors?: any[] }; +type EventValidationFunction = (event: T, logger: Logger) => void; export class EventPublisher { private readonly eventBridge: EventBridgeClient; @@ -201,16 +201,11 @@ export class EventPublisher { const invalidEvents: T[] = []; for (const event of events) { - const isEventValid = eventValidator(event); - if (isEventValid) { + try { + eventValidator(event, this.logger); validEvents.push(event); - } else { + } catch { invalidEvents.push(event); - - this.logger.info({ - description: 'Error parsing event', - error: eventValidator.errors, - }); } } diff --git a/utils/utils/src/logger.ts b/utils/utils/src/logger.ts index d1c1a9106..894fcba4b 100644 --- a/utils/utils/src/logger.ts +++ b/utils/utils/src/logger.ts @@ -4,7 +4,7 @@ const { combine, errors, json, timestamp } = winston.format; export const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', - format: combine(timestamp(), json(), errors({ stack: true, cause: true })), + format: combine(errors({ stack: true, cause: true }), timestamp(), json()), transports: [ new winston.transports.Stream({ stream: process.stdout,