diff --git a/lambdas/print-status-handler/package.json b/lambdas/print-status-handler/package.json index 5db2c2d8c..a06248b1d 100644 --- a/lambdas/print-status-handler/package.json +++ b/lambdas/print-status-handler/package.json @@ -1,11 +1,11 @@ { "dependencies": { - "@nhsdigital/nhs-notify-event-schemas-supplier-api": "1.0.17", "digital-letters-events": "^0.0.1", "utils": "^0.0.1", "zod": "^4.1.12" }, "devDependencies": { + "@nhsdigital/nhs-notify-event-schemas-supplier-api": "^1.0.17", "@tsconfig/node22": "^22.0.2", "@types/aws-lambda": "^8.10.155", "@types/jest": "^29.5.14", diff --git a/lambdas/print-status-handler/src/__tests__/apis/sqs-handler.test.ts b/lambdas/print-status-handler/src/__tests__/apis/sqs-handler.test.ts index 07f7c4af8..56f2d6ca2 100644 --- a/lambdas/print-status-handler/src/__tests__/apis/sqs-handler.test.ts +++ b/lambdas/print-status-handler/src/__tests__/apis/sqs-handler.test.ts @@ -7,6 +7,7 @@ import { failedLetterEvent, recordEvent, } from '__tests__/test-data'; +import { LetterStatusChangeEvent } from '@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-events'; const logger = mock(); const eventPublisher = mock(); @@ -37,11 +38,16 @@ describe('SQS Handler', () => { expect(eventPublisher.sendEvents).toHaveBeenCalledWith( [ { - ...acceptedLetterEvent, id: '550e8400-e29b-41d4-a716-446655440001', time: '2023-06-20T12:00:00.250Z', recordedtime: '2023-06-20T12:00:00.250Z', plane: 'data', + specversion: '1.0', + datacontenttype: 'application/json', + traceparent: + '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01', + severitynumber: 2, + severitytext: 'INFO', dataschema: 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-print-letter-transitioned-data.schema.json', type: 'uk.nhs.notify.digital.letters.print.letter.transitioned.v1', @@ -78,10 +84,16 @@ describe('SQS Handler', () => { expect(eventPublisher.sendEvents).toHaveBeenCalledWith( [ { - ...failedLetterEvent, id: '550e8400-e29b-41d4-a716-446655440001', time: '2023-06-20T12:00:00.250Z', recordedtime: '2023-06-20T12:00:00.250Z', + plane: 'data', + specversion: '1.0', + datacontenttype: 'application/json', + traceparent: + '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01', + severitynumber: 2, + severitytext: 'INFO', dataschema: 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-print-letter-transitioned-data.schema.json', type: 'uk.nhs.notify.digital.letters.print.letter.transitioned.v1', @@ -181,9 +193,14 @@ describe('SQS Handler', () => { it('should return failed items to the queue if an invalid letter.ACCEPTED event is received', async () => { const invalidAcceptedLetterEvent = { ...acceptedLetterEvent, - source: 'invalid letter.ACCEPTED source', + data: { + ...acceptedLetterEvent.data, + status: 'INVALID_STATUS', + }, }; - const event = recordEvent([invalidAcceptedLetterEvent]); + const event = recordEvent([ + invalidAcceptedLetterEvent as LetterStatusChangeEvent, + ]); const result = await handler(event); @@ -191,7 +208,7 @@ describe('SQS Handler', () => { err: expect.objectContaining({ issues: expect.arrayContaining([ expect.objectContaining({ - path: ['source'], + path: ['data', 'status'], }), ]), }), @@ -231,7 +248,7 @@ describe('SQS Handler', () => { }), ]), }), - description: 'Invalid origin.subject format', + description: 'Error parsing queue item', }); expect(logger.info).toHaveBeenCalledWith( diff --git a/lambdas/print-status-handler/src/__tests__/test-data.ts b/lambdas/print-status-handler/src/__tests__/test-data.ts index d950876c3..fa00fab6b 100644 --- a/lambdas/print-status-handler/src/__tests__/test-data.ts +++ b/lambdas/print-status-handler/src/__tests__/test-data.ts @@ -1,5 +1,5 @@ import type { SQSEvent, SQSRecord } from 'aws-lambda'; -import { LetterEvent } from '@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-events'; +import { LetterStatusChangeEvent } from '@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-events'; export const acceptedLetterEvent = { id: '550e8400-e29b-41d4-a716-446655440001', @@ -34,7 +34,7 @@ export const acceptedLetterEvent = { 'client/f47ac10b-58cc-4372-a567-0e02b2c3d479/letter-request/2503cbd5-6722-4e90-9fbd-5f1e96d65c22', }, }, -} as LetterEvent; +} as LetterStatusChangeEvent; export const failedLetterEvent = { id: '550e8400-e29b-41d4-a716-446655440002', @@ -71,7 +71,7 @@ export const failedLetterEvent = { 'client/f47ac10b-58cc-4372-a567-0e02b2c3d480/letter-request/2503cbd5-6722-4e90-9fbd-5f1e96d65c22', }, }, -} as LetterEvent; +} as LetterStatusChangeEvent; const busEvent = { version: '0', @@ -94,7 +94,7 @@ const sqsRecord = { awsRegion: '', } as SQSRecord; -export const recordEvent = (events: LetterEvent[]): SQSEvent => ({ +export const recordEvent = (events: LetterStatusChangeEvent[]): SQSEvent => ({ Records: events.map((event, i) => ({ ...sqsRecord, messageId: String(i + 1), diff --git a/lambdas/print-status-handler/src/apis/sqs-handler.ts b/lambdas/print-status-handler/src/apis/sqs-handler.ts index 9a3feeb94..3ffb523ed 100644 --- a/lambdas/print-status-handler/src/apis/sqs-handler.ts +++ b/lambdas/print-status-handler/src/apis/sqs-handler.ts @@ -4,16 +4,16 @@ import type { SQSEvent, } from 'aws-lambda'; import { randomUUID } from 'node:crypto'; -import { z } from 'zod'; -import { - $LetterEvent, - LetterEvent, -} from '@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-events'; import { PrintLetterTransitioned, validatePrintLetterTransitioned, } from 'digital-letters-events'; -import { EventPublisher, Logger } from 'utils'; +import { + $SupplierApiLetterEvent, + EventPublisher, + Logger, + SupplierApiLetterEvent, +} from 'utils'; export interface HandlerDependencies { eventPublisher: EventPublisher; @@ -22,16 +22,9 @@ export interface HandlerDependencies { type ValidatedRecord = { messageId: string; - event: LetterEvent; + event: SupplierApiLetterEvent; }; -const originSubjectSchema = z - .string() - .regex( - /^client\/[^/]+\/letter-request\/[^/]+$/, - 'Subject must be in format: client/{senderId}/letter-request/{messageReference}', - ); - function validateRecord( { body, messageId }: { body: string; messageId: string }, logger: Logger, @@ -44,7 +37,7 @@ function validateRecord( data: item, error: parseError, success: parseSuccess, - } = $LetterEvent.safeParse(sqsEventDetail); + } = $SupplierApiLetterEvent.safeParse(sqsEventDetail); if (!parseSuccess) { logger.warn({ @@ -55,19 +48,6 @@ function validateRecord( return null; } - const subjectValidation = originSubjectSchema.safeParse( - item.data.origin.subject, - ); - - if (!subjectValidation.success) { - logger.warn({ - err: subjectValidation.error, - description: 'Invalid origin.subject format', - }); - - return null; - } - logger.info({ description: 'Successfully validated SQS record', messageId, @@ -85,7 +65,9 @@ function validateRecord( } } -function generateUpdatedEvent(event: LetterEvent): PrintLetterTransitioned { +function generateUpdatedEvent( + event: SupplierApiLetterEvent, +): PrintLetterTransitioned { const eventTime = new Date().toISOString(); const { @@ -104,7 +86,6 @@ function generateUpdatedEvent(event: LetterEvent): PrintLetterTransitioned { const messageReference = subject.split('/')[3]; return { - ...event, id: randomUUID(), time: eventTime, recordedtime: eventTime, @@ -116,6 +97,11 @@ function generateUpdatedEvent(event: LetterEvent): PrintLetterTransitioned { source: '/nhs/england/notify/production/primary/digitalletters/print', plane: 'data', dataschemaversion: '1.0.0', + specversion: '1.0', + datacontenttype: 'application/json', + traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01', + severitynumber: 2, + severitytext: 'INFO', data: { senderId, messageReference, diff --git a/package-lock.json b/package-lock.json index 64dd9bd22..e5950eecd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3075,12 +3075,12 @@ "name": "nhs-notify-digital-letters-print-status-handler", "version": "0.0.1", "dependencies": { - "@nhsdigital/nhs-notify-event-schemas-supplier-api": "1.0.17", "digital-letters-events": "^0.0.1", "utils": "^0.0.1", "zod": "^4.1.12" }, "devDependencies": { + "@nhsdigital/nhs-notify-event-schemas-supplier-api": "^1.0.17", "@tsconfig/node22": "^22.0.2", "@types/aws-lambda": "^8.10.155", "@types/jest": "^29.5.14", @@ -9848,9 +9848,9 @@ } }, "node_modules/@nhsdigital/nhs-notify-event-schemas-supplier-api": { - "version": "1.0.17", - "resolved": "https://npm.pkg.github.com/download/@nhsdigital/nhs-notify-event-schemas-supplier-api/1.0.17/05b451c993a18b89f4a91922a7affc1d23c1e2a8", - "integrity": "sha512-QDWOasKri3iky7StzWdrkV0YIQMnPJNlG4tQZoCOG8BTtAuMte/Xh5vZpQFm1DHQJxUVWMYrAL6n9ZSrKNxVkQ==", + "version": "1.0.18", + "resolved": "https://npm.pkg.github.com/download/@nhsdigital/nhs-notify-event-schemas-supplier-api/1.0.18/e81996aca0271b686b521dec9ec33a8735b351b5", + "integrity": "sha512-+PJ4XSWYZ+hZ2jhbvt2Wpi0QzPAYug2bnl6NrdEQmcURVhAh5JxJkuQ7OayHXFS2A7hiEF+4nJnS6B0ZLiWSgw==", "license": "MIT", "dependencies": { "@asyncapi/bundler": "^0.6.4", @@ -26008,6 +26008,7 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -27213,6 +27214,7 @@ "version": "0.0.1", "devDependencies": { "@nhsdigital/nhs-notify-event-schemas-status-published": "^1.0.1", + "@nhsdigital/nhs-notify-event-schemas-supplier-api": "^1.0.17", "@pact-foundation/pact": "^16.3.0", "@types/jest": "^29.5.14", "jest": "^29.7.0", @@ -27591,7 +27593,7 @@ "@aws-sdk/lib-dynamodb": "^3.900.0", "@aws-sdk/util-dynamodb": "^3.933.0", "@faker-js/faker": "^9.6.0", - "@nhsdigital/nhs-notify-event-schemas-supplier-api": "1.0.17", + "@nhsdigital/nhs-notify-event-schemas-supplier-api": "^1.0.17", "@playwright/test": "^1.51.1", "csv-parse": "^6.1.0", "digital-letters-events": "^0.0.1", diff --git a/pact-contracts/package.json b/pact-contracts/package.json index 1b3b4fd56..4a33a2404 100644 --- a/pact-contracts/package.json +++ b/pact-contracts/package.json @@ -17,5 +17,5 @@ "test:unit:coverage": "echo Unit tests not required", "typecheck": "echo Typecheck not required" }, - "version": "1.0.0" + "version": "1.0.1" } diff --git a/scripts/tests/contract.sh b/scripts/tests/contract.sh index 2141eced0..a435c8252 100755 --- a/scripts/tests/contract.sh +++ b/scripts/tests/contract.sh @@ -2,26 +2,31 @@ set -euo pipefail -schema_package="@nhsdigital/nhs-notify-event-schemas-status-published" +schema_packages=( + "@nhsdigital/nhs-notify-event-schemas-status-published" + "@nhsdigital/nhs-notify-event-schemas-supplier-api" +) cd "$(git rev-parse --show-toplevel)" -if npm outdated "$schema_package" >/dev/null 2>&1; then - outdated_status=0 -else - outdated_status=$? -fi +for schema_package in "${schema_packages[@]}"; do + if npm outdated "$schema_package" >/dev/null 2>&1; then + outdated_status=0 + else + outdated_status=$? + fi -if [[ $outdated_status -eq 1 ]]; then - echo "The provider schema package ($schema_package) is outdated." - echo "Please run npm update $schema_package to update to the latest version and re-run." - echo - exit 1 -fi + if [[ $outdated_status -eq 1 ]]; then + echo "The provider schema package ($schema_package) is outdated." + echo "Please run npm update $schema_package to update to the latest version and re-run." + echo + exit 1 + fi -if [[ $outdated_status -gt 1 ]]; then - echo "Failed to check provider schema package version (npm outdated exited with status $outdated_status)." - exit $outdated_status -fi + if [[ $outdated_status -gt 1 ]]; then + echo "Failed to check provider schema package version (npm outdated exited with status $outdated_status)." + exit $outdated_status + fi +done npm run test:contract diff --git a/tests/pact-tests/consumer/channel-status-published.consumer.pact.test.ts b/tests/pact-tests/consumer/channel-status-published.consumer.pact.test.ts index b71c1e52e..e85f905d7 100644 --- a/tests/pact-tests/consumer/channel-status-published.consumer.pact.test.ts +++ b/tests/pact-tests/consumer/channel-status-published.consumer.pact.test.ts @@ -7,20 +7,22 @@ import { import { $ChannelStatusPublishedEvent } from 'utils'; import { PACT_CONSUMER, - PACT_DIRECTORY, PACT_MESSAGE_DESCRIPTION, - PACT_PROVIDER, + PACT_STATUS_PUBLISHED_PROVIDER, } from '../utils/pact-config'; +import { getPathFromProvider } from '../utils/path-utils'; async function handle(event: unknown) { // The schema used by the nhsapp-status-handler to validate the event. $ChannelStatusPublishedEvent.parse(event); } +const PACT_DIRECTORY = getPathFromProvider(PACT_STATUS_PUBLISHED_PROVIDER); + describe('Pact message consumer - ChannelStatusPublished event', () => { const messagePact = new MessageConsumerPact({ consumer: PACT_CONSUMER, - provider: PACT_PROVIDER, + provider: PACT_STATUS_PUBLISHED_PROVIDER, dir: PACT_DIRECTORY, logLevel: 'error', pactfileWriteMode: 'update', diff --git a/tests/pact-tests/consumer/supplier-api.consumer.pact.test.ts b/tests/pact-tests/consumer/supplier-api.consumer.pact.test.ts new file mode 100644 index 000000000..2e650beaa --- /dev/null +++ b/tests/pact-tests/consumer/supplier-api.consumer.pact.test.ts @@ -0,0 +1,163 @@ +import LetterAcceptedEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.ACCEPTED.json'; +import LetterCancelledEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.CANCELLED.json'; +import LetterDeliveredEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.DELIVERED.json'; +import LetterDispatchedEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.DISPATCHED.json'; +import LetterEnclosedEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.ENCLOSED.json'; +import LetterFailedEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.FAILED.json'; +import LetterForwardedEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.FORWARDED.json'; +import LetterPendingEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.PENDING.json'; +import LetterPrintedEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.PRINTED.json'; +import LetterRejectedEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.REJECTED.json'; +import LetterReturnedEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.RETURNED.json'; +import { + MatchersV3, + MessageConsumerPact, + asynchronousBodyHandler, +} from '@pact-foundation/pact'; +import { $SupplierApiLetterEvent } from 'utils'; +import { + PACT_CONSUMER, + PACT_SUPPLIER_API_PROVIDER, +} from '../utils/pact-config'; +import { getPathFromProvider } from '../utils/path-utils'; + +const PACT_DIRECTORY = getPathFromProvider(PACT_SUPPLIER_API_PROVIDER); + +async function handle(event: unknown) { + $SupplierApiLetterEvent.parse(event); +} + +function buildValidator(status: string, includeReason = false) { + return { + data: { + origin: { + subject: MatchersV3.regex( + /^client\/[^/]+\/letter-request\/[^/]+$/, + LetterAcceptedEvent.data.origin.subject, + ), + }, + specificationId: MatchersV3.string( + LetterAcceptedEvent.data.specificationId, + ), + status, + supplierId: MatchersV3.string(LetterAcceptedEvent.data.supplierId), + ...(includeReason && { + reasonCode: MatchersV3.string(LetterFailedEvent.data.reasonCode), + reasonText: MatchersV3.string(LetterFailedEvent.data.reasonText), + }), + }, + time: MatchersV3.timestamp( + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + LetterAcceptedEvent.time, + ), + }; +} + +describe('Pact message consumer - Supplier API events', () => { + const messagePact = new MessageConsumerPact({ + consumer: PACT_CONSUMER, + provider: PACT_SUPPLIER_API_PROVIDER, + dir: PACT_DIRECTORY, + logLevel: 'error', + pactfileWriteMode: 'update', + }); + + it('validates a letter accepted event', async () => { + await expect( + messagePact + .expectsToReceive('SupplierApiEvent-letter_accepted') + .withContent(buildValidator(LetterAcceptedEvent.data.status)) + .verify(asynchronousBodyHandler(handle)), + ).resolves.not.toThrow(); + }); + + it('validates a letter returned event', async () => { + await expect( + messagePact + .expectsToReceive('SupplierApiEvent-letter_returned') + .withContent(buildValidator(LetterReturnedEvent.data.status, true)) + .verify(asynchronousBodyHandler(handle)), + ).resolves.not.toThrow(); + }); + + it('validates a letter failed event', async () => { + await expect( + messagePact + .expectsToReceive('SupplierApiEvent-letter_failed') + .withContent(buildValidator(LetterFailedEvent.data.status, true)) + .verify(asynchronousBodyHandler(handle)), + ).resolves.not.toThrow(); + }); + + it('validates a letter dispatched event', async () => { + await expect( + messagePact + .expectsToReceive('SupplierApiEvent-letter_dispatched') + .withContent(buildValidator(LetterDispatchedEvent.data.status)) + .verify(asynchronousBodyHandler(handle)), + ).resolves.not.toThrow(); + }); + + it('validates a letter printed event', async () => { + await expect( + messagePact + .expectsToReceive('SupplierApiEvent-letter_printed') + .withContent(buildValidator(LetterPrintedEvent.data.status)) + .verify(asynchronousBodyHandler(handle)), + ).resolves.not.toThrow(); + }); + + it('validates a letter rejected event', async () => { + await expect( + messagePact + .expectsToReceive('SupplierApiEvent-letter_rejected') + .withContent(buildValidator(LetterRejectedEvent.data.status, true)) + .verify(asynchronousBodyHandler(handle)), + ).resolves.not.toThrow(); + }); + + it('validates a letter cancelled event', async () => { + await expect( + messagePact + .expectsToReceive('SupplierApiEvent-letter_cancelled') + .withContent(buildValidator(LetterCancelledEvent.data.status, true)) + .verify(asynchronousBodyHandler(handle)), + ).resolves.not.toThrow(); + }); + + it('validates a letter delivered event', async () => { + await expect( + messagePact + .expectsToReceive('SupplierApiEvent-letter_delivered') + .withContent(buildValidator(LetterDeliveredEvent.data.status)) + .verify(asynchronousBodyHandler(handle)), + ).resolves.not.toThrow(); + }); + + it('validates a letter enclosed event', async () => { + await expect( + messagePact + .expectsToReceive('SupplierApiEvent-letter_enclosed') + .withContent(buildValidator(LetterEnclosedEvent.data.status)) + .verify(asynchronousBodyHandler(handle)), + ).resolves.not.toThrow(); + }); + + it('validates a letter forwarded event', async () => { + await expect( + messagePact + .expectsToReceive('SupplierApiEvent-letter_forwarded') + .withContent(buildValidator(LetterForwardedEvent.data.status, true)) + .verify(asynchronousBodyHandler(handle)), + ).resolves.not.toThrow(); + }); + + it('validates a letter pending event', async () => { + await expect( + messagePact + .expectsToReceive('SupplierApiEvent-letter_pending') + .withContent(buildValidator(LetterPendingEvent.data.status)) + .verify(asynchronousBodyHandler(handle)), + ).resolves.not.toThrow(); + }); +}); diff --git a/tests/pact-tests/package.json b/tests/pact-tests/package.json index bbc94ce3e..edd705f94 100644 --- a/tests/pact-tests/package.json +++ b/tests/pact-tests/package.json @@ -1,6 +1,7 @@ { "devDependencies": { "@nhsdigital/nhs-notify-event-schemas-status-published": "^1.0.1", + "@nhsdigital/nhs-notify-event-schemas-supplier-api": "^1.0.17", "@pact-foundation/pact": "^16.3.0", "@types/jest": "^29.5.14", "jest": "^29.7.0", diff --git a/tests/pact-tests/pact-verification/channel-status-published.provider.pact.test.ts b/tests/pact-tests/pact-verification/channel-status-published.provider.pact.test.ts index 53e6db2e4..5a70ae49a 100644 --- a/tests/pact-tests/pact-verification/channel-status-published.provider.pact.test.ts +++ b/tests/pact-tests/pact-verification/channel-status-published.provider.pact.test.ts @@ -1,17 +1,22 @@ import { MessageProviderPact } from '@pact-foundation/pact'; import ChannelStatusPublishedEventPaperLetterOptedOut from '@nhsdigital/nhs-notify-event-schemas-status-published/examples/ChannelStatusPublishedEvent/v1/paper_letter_opted_out.json'; - +import { getPactFilePath } from '../utils/path-utils'; import { - PACT_FILE, + PACT_CONSUMER, PACT_MESSAGE_DESCRIPTION, - PACT_PROVIDER, + PACT_STATUS_PUBLISHED_PROVIDER, } from '../utils/pact-config'; +const PACT_FILE = getPactFilePath( + PACT_CONSUMER, + PACT_STATUS_PUBLISHED_PROVIDER, +); + describe('Channel status published provider tests', () => { test('verify pacts', async () => { const p = new MessageProviderPact({ - provider: PACT_PROVIDER, + provider: PACT_STATUS_PUBLISHED_PROVIDER, pactUrls: [PACT_FILE], messageProviders: { [PACT_MESSAGE_DESCRIPTION]: () => diff --git a/tests/pact-tests/pact-verification/supplier-api.provider.pact.test.ts b/tests/pact-tests/pact-verification/supplier-api.provider.pact.test.ts new file mode 100644 index 000000000..50d0d7252 --- /dev/null +++ b/tests/pact-tests/pact-verification/supplier-api.provider.pact.test.ts @@ -0,0 +1,46 @@ +import { MessageProviderPact } from '@pact-foundation/pact'; + +import LetterAcceptedEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.ACCEPTED.json'; +import LetterCancelledEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.CANCELLED.json'; +import LetterDeliveredEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.DELIVERED.json'; +import LetterDispatchedEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.DISPATCHED.json'; +import LetterEnclosedEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.ENCLOSED.json'; +import LetterFailedEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.FAILED.json'; +import LetterForwardedEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.FORWARDED.json'; +import LetterPendingEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.PENDING.json'; +import LetterPrintedEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.PRINTED.json'; +import LetterRejectedEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.REJECTED.json'; +import LetterReturnedEvent from '@nhsdigital/nhs-notify-event-schemas-supplier-api/schemas/examples/letter.RETURNED.json'; + +import { + PACT_CONSUMER, + PACT_SUPPLIER_API_PROVIDER, +} from '../utils/pact-config'; +import { getPactFilePath } from '../utils/path-utils'; + +const PACT_FILE = getPactFilePath(PACT_CONSUMER, PACT_SUPPLIER_API_PROVIDER); + +describe('Supplier API provider tests', () => { + test('verify pacts', async () => { + const p = new MessageProviderPact({ + provider: PACT_SUPPLIER_API_PROVIDER, + pactUrls: [PACT_FILE], + messageProviders: { + 'SupplierApiEvent-letter_accepted': () => LetterAcceptedEvent, + 'SupplierApiEvent-letter_cancelled': () => LetterCancelledEvent, + 'SupplierApiEvent-letter_delivered': () => LetterDeliveredEvent, + 'SupplierApiEvent-letter_dispatched': () => LetterDispatchedEvent, + 'SupplierApiEvent-letter_enclosed': () => LetterEnclosedEvent, + 'SupplierApiEvent-letter_failed': () => LetterFailedEvent, + 'SupplierApiEvent-letter_forwarded': () => LetterForwardedEvent, + 'SupplierApiEvent-letter_pending': () => LetterPendingEvent, + 'SupplierApiEvent-letter_printed': () => LetterPrintedEvent, + 'SupplierApiEvent-letter_rejected': () => LetterRejectedEvent, + 'SupplierApiEvent-letter_returned': () => LetterReturnedEvent, + }, + logLevel: 'error', + }); + + await expect(p.verify()).resolves.not.toThrow(); + }); +}); diff --git a/tests/pact-tests/utils/pact-config.ts b/tests/pact-tests/utils/pact-config.ts index 14d31c473..1188dc28c 100644 --- a/tests/pact-tests/utils/pact-config.ts +++ b/tests/pact-tests/utils/pact-config.ts @@ -1,14 +1,6 @@ -import path from 'node:path'; - export const PACT_CONSUMER = 'digital-letters'; -export const PACT_PROVIDER = 'status-published'; +export const PACT_STATUS_PUBLISHED_PROVIDER = 'status-published'; +export const PACT_SUPPLIER_API_PROVIDER = 'supplier-api'; + export const PACT_MESSAGE_DESCRIPTION = 'ChannelStatusPublishedEvent-paper_letter_opted_out'; -export const PACT_DIRECTORY = path.resolve( - __dirname, - '../.pacts/status-published', -); -export const PACT_FILE = path.join( - PACT_DIRECTORY, - `${PACT_CONSUMER}-${PACT_PROVIDER}.json`, -); diff --git a/tests/pact-tests/utils/path-utils.ts b/tests/pact-tests/utils/path-utils.ts new file mode 100644 index 000000000..27fa6d9a3 --- /dev/null +++ b/tests/pact-tests/utils/path-utils.ts @@ -0,0 +1,12 @@ +import path from 'node:path'; + +export function getPathFromProvider(provider: string): string { + return path.resolve(__dirname, `../.pacts/${provider}`); +} + +export function getPactFilePath(consumer: string, provider: string): any { + return path.join( + getPathFromProvider(provider), + `${consumer}-${provider}.json`, + ); +} diff --git a/tests/playwright/digital-letters-component-tests/print-status-handler.component.spec.ts b/tests/playwright/digital-letters-component-tests/print-status-handler.component.spec.ts index d6b013389..82977f082 100644 --- a/tests/playwright/digital-letters-component-tests/print-status-handler.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/print-status-handler.component.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { LetterEvent } from '@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-events'; +import { LetterStatusChangeEvent } from '@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-events'; import { ENV, PRINT_STATUS_HANDLER_DLQ_NAME, @@ -36,7 +36,7 @@ const baseLetterEvent = { source: '/data-plane/letter-rendering/prod/render-pdf', }, }, -} as LetterEvent; +} as LetterStatusChangeEvent; const letterStatuses = [ 'ACCEPTED', @@ -76,7 +76,10 @@ test.describe('Print status handler', () => { }, }; - await eventPublisher.sendEvents([letterEvent], () => true); + await eventPublisher.sendEvents( + [letterEvent], + () => true, + ); await expectToPassEventually(async () => { const eventLogEntry = await getLogsFromCloudwatch( @@ -100,7 +103,7 @@ test.describe('Print status handler', () => { const messageReference = uuidv4(); // Send letter.ACCEPTED event with no data.status - await eventPublisher.sendEvents( + await eventPublisher.sendEvents( [ { ...baseLetterEvent, diff --git a/tests/playwright/package.json b/tests/playwright/package.json index 7ebede52e..8be7af124 100644 --- a/tests/playwright/package.json +++ b/tests/playwright/package.json @@ -10,7 +10,7 @@ "@aws-sdk/lib-dynamodb": "^3.900.0", "@aws-sdk/util-dynamodb": "^3.933.0", "@faker-js/faker": "^9.6.0", - "@nhsdigital/nhs-notify-event-schemas-supplier-api": "1.0.17", + "@nhsdigital/nhs-notify-event-schemas-supplier-api": "^1.0.17", "@playwright/test": "^1.51.1", "csv-parse": "^6.1.0", "digital-letters-events": "^0.0.1", diff --git a/utils/utils/src/types/index.ts b/utils/utils/src/types/index.ts index 27bdf3d92..c5272f269 100644 --- a/utils/utils/src/types/index.ts +++ b/utils/utils/src/types/index.ts @@ -1,4 +1,5 @@ export * from './channel-status-published-event'; export * from './pdm-types'; export * from './sender'; +export * from './supplier-api-letter-event'; export * from './ttl-dynamodb-record'; diff --git a/utils/utils/src/types/supplier-api-letter-event.ts b/utils/utils/src/types/supplier-api-letter-event.ts new file mode 100644 index 000000000..6456e7a25 --- /dev/null +++ b/utils/utils/src/types/supplier-api-letter-event.ts @@ -0,0 +1,34 @@ +import { z } from 'zod'; + +export const $SupplierApiLetterEvent = z.object({ + data: z.object({ + origin: z.object({ + subject: z + .string() + .regex( + /^client\/[^/]+\/letter-request\/[^/]+$/, + 'Subject must be in format: client/{senderId}/letter-request/{messageReference}', + ), + }), + specificationId: z.string(), + status: z.enum([ + 'PENDING', + 'RETURNED', + 'DISPATCHED', + 'PRINTED', + 'REJECTED', + 'ACCEPTED', + 'FAILED', + 'CANCELLED', + 'FORWARDED', + 'DELIVERED', + 'ENCLOSED', + ]), + supplierId: z.string(), + reasonCode: z.string().optional(), + reasonText: z.string().optional(), + }), + time: z.string(), +}); + +export type SupplierApiLetterEvent = z.infer;