Skip to content

Commit 072b12c

Browse files
CCM-16553: Refactor to include potentially other event types and to send to the event bus
1 parent 505cb38 commit 072b12c

12 files changed

Lines changed: 295 additions & 32 deletions

package-lock.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/nft-event-generator/jest.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const config: Config = {
1717
target: 'ES2020',
1818
moduleResolution: 'node',
1919
noEmit: true,
20+
typeRoots: ['../../node_modules/@types'],
2021
},
2122
diagnostics: {
2223
ignoreCodes: [1343], // Ignore TS1343: import.meta errors
@@ -47,6 +48,7 @@ const config: Config = {
4748
moduleNameMapper: {
4849
'^(.*)\\.ts$': '$1',
4950
},
51+
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
5052
testTimeout: 10_000,
5153
};
5254

scripts/nft-event-generator/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
{
22
"author": "",
33
"dependencies": {
4+
"@aws-sdk/client-eventbridge": "^3.914.0",
45
"@aws-sdk/client-sqs": "^3.914.0",
56
"@aws-sdk/client-sts": "^3.914.0",
67
"csv-parse": "^6.1.0",
8+
"digital-letters-events": "^0.0.1",
79
"tsx": "^4.20.6",
810
"yargs": "^17.7.2"
911
},
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import {
2+
PutEventsCommand,
3+
PutEventsResultEntry,
4+
} from '@aws-sdk/client-eventbridge';
5+
import { fakeEvent } from '__tests__/helpers/fake-event';
6+
import { PublishableEvent } from 'destination-client';
7+
import { mock } from 'jest-mock-extended';
8+
import { sendEventsToEventBus } from 'send-events-to-event-bus';
9+
10+
const environment = 'dev';
11+
12+
const mockEventBridgeClient = { send: jest.fn() };
13+
jest.mock('@aws-sdk/client-eventbridge', () => {
14+
const originalModule = jest.requireActual('@aws-sdk/client-eventbridge');
15+
16+
return {
17+
__esModule: true,
18+
...originalModule,
19+
EventBridgeClient: jest.fn(() => mockEventBridgeClient),
20+
};
21+
});
22+
23+
const successEntry = mock<PutEventsResultEntry>({ ErrorCode: undefined });
24+
const successfulSendResponse = { Entries: [successEntry] };
25+
26+
describe('sendEventsToEventBus', () => {
27+
beforeEach(() => {
28+
mockEventBridgeClient.send.mockReset();
29+
});
30+
31+
it('should send the expected request to EventBridge', async () => {
32+
mockEventBridgeClient.send.mockResolvedValue(successfulSendResponse);
33+
34+
await sendEventsToEventBus(environment, [fakeEvent], 5);
35+
36+
expect(mockEventBridgeClient.send).toHaveBeenCalled();
37+
const putEventsCommand: PutEventsCommand =
38+
mockEventBridgeClient.send.mock.calls[0][0];
39+
40+
expect(putEventsCommand.input.Entries).toHaveLength(1);
41+
const entry = putEventsCommand.input.Entries![0];
42+
expect(entry.EventBusName).toBe(`nhs-${environment}-dl`);
43+
expect(entry.Source).toBe(fakeEvent.source);
44+
expect(entry.DetailType).toBe(fakeEvent['detail-type']);
45+
expect(entry.Detail).toBe(JSON.stringify(fakeEvent.detail));
46+
});
47+
48+
it('should send a request for each batch of messages', async () => {
49+
const events: PublishableEvent[] = Array.from(
50+
{ length: 52 },
51+
() => fakeEvent,
52+
);
53+
mockEventBridgeClient.send.mockResolvedValue(successfulSendResponse);
54+
55+
await sendEventsToEventBus(environment, events, 5);
56+
57+
// Batch size is 10, so 52 events = 6 batches.
58+
expect(mockEventBridgeClient.send).toHaveBeenCalledTimes(6);
59+
});
60+
61+
it('should continue sending batches if an error is raised', async () => {
62+
mockEventBridgeClient.send.mockRejectedValueOnce(
63+
new Error('Something went wrong!'),
64+
);
65+
mockEventBridgeClient.send.mockResolvedValue(successfulSendResponse);
66+
67+
const events: PublishableEvent[] = Array.from(
68+
{ length: 30 },
69+
() => fakeEvent,
70+
);
71+
72+
await sendEventsToEventBus(environment, events, 5);
73+
74+
// Batch size is 10, so 30 events = 3 batches.
75+
expect(mockEventBridgeClient.send).toHaveBeenCalledTimes(3);
76+
});
77+
78+
it('should warn when some events fail to publish', async () => {
79+
const failedEntry = mock<PutEventsResultEntry>({
80+
ErrorCode: 'InternalFailure',
81+
});
82+
mockEventBridgeClient.send.mockResolvedValue({
83+
Entries: [failedEntry],
84+
});
85+
86+
await sendEventsToEventBus(environment, [fakeEvent], 5);
87+
88+
expect(console.warn).toHaveBeenCalled();
89+
});
90+
});

scripts/nft-event-generator/src/__tests__/send-events-to-sqs.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import {
33
SendMessageBatchCommand,
44
} from '@aws-sdk/client-sqs';
55
import { fakeEvent } from '__tests__/helpers/fake-event';
6+
import { PublishableEvent } from 'destination-client';
67
import { mock } from 'jest-mock-extended';
78
import { sendEventsToSqs } from 'send-events-to-sqs';
8-
import { SupplierStatusEvent } from 'types';
99

1010
const accountId = '257995483745';
1111
const environment = 'dev';
@@ -68,7 +68,7 @@ describe('sendEventsToSqs', () => {
6868
});
6969

7070
it('should send a request for each batch of messages', async () => {
71-
const events: SupplierStatusEvent[] = Array.from(
71+
const events: PublishableEvent[] = Array.from(
7272
{
7373
length: 52,
7474
},
@@ -88,7 +88,7 @@ describe('sendEventsToSqs', () => {
8888
);
8989
mockSqsClient.send.mockResolvedValue(successfulSendResponse);
9090

91-
const events: SupplierStatusEvent[] = Array.from(
91+
const events: PublishableEvent[] = Array.from(
9292
{
9393
length: 30,
9494
},
@@ -107,7 +107,7 @@ describe('sendEventsToSqs', () => {
107107
});
108108
mockSqsClient.send.mockResolvedValue(successfulSendResponse);
109109

110-
const events: SupplierStatusEvent[] = Array.from(
110+
const events: PublishableEvent[] = Array.from(
111111
{
112112
length: 30,
113113
},
@@ -122,7 +122,7 @@ describe('sendEventsToSqs', () => {
122122

123123
it('should continue sending batches if an empty response is received', async () => {
124124
mockSqsClient.send.mockResolvedValue({});
125-
const events: SupplierStatusEvent[] = Array.from(
125+
const events: PublishableEvent[] = Array.from(
126126
{
127127
length: 30,
128128
},

scripts/nft-event-generator/src/cli.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { DestinationClient } from 'destination-client';
12
import { generateSupplierStatusEvents } from 'generate-events';
2-
import { sendEventsToSqs } from 'send-events-to-sqs';
3+
import { EventBusDestinationClient } from 'send-events-to-event-bus';
4+
import { SqsDestinationClient } from 'send-events-to-sqs';
35
import yargs from 'yargs';
46
import { hideBin } from 'yargs/helpers';
57

@@ -24,15 +26,32 @@ const argv = yargs(hideBin(process.argv))
2426
default: 0.5,
2527
describe: 'Ratio of events with delayedFallback = true (0 to 1)',
2628
})
29+
.option('destination', {
30+
type: 'string',
31+
choices: ['sqs', 'event-bus'] as const,
32+
default: 'sqs',
33+
describe: 'Destination to send events to',
34+
})
2735
.help()
2836
.alias('help', 'h').argv as any;
2937

30-
const { delayedFallbackRatio, environment, interval, numberOfEvents } = argv;
38+
const {
39+
delayedFallbackRatio,
40+
destination,
41+
environment,
42+
interval,
43+
numberOfEvents,
44+
} = argv;
3145

3246
const events = generateSupplierStatusEvents({
3347
numberOfEvents,
3448
environment,
3549
delayedFallbackRatio,
3650
});
3751

38-
sendEventsToSqs(environment, events, interval);
52+
const client: DestinationClient =
53+
destination === 'event-bus'
54+
? new EventBusDestinationClient(environment)
55+
: new SqsDestinationClient(environment);
56+
57+
client.sendEvents(events, interval);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Minimum shape required by all destination clients.
3+
* Each publishable event must carry an `id` so the sending implementation
4+
* can use it as a unique identifier within a batch.
5+
*/
6+
export type PublishableEvent = { id: string };
7+
8+
/**
9+
* Common interface for all event destinations (SQS, EventBridge, etc.).
10+
* Implementations are responsible for batching, retries and back-off.
11+
*/
12+
export interface DestinationClient {
13+
sendEvents(events: PublishableEvent[], interval: number): Promise<void>;
14+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"detail": {
3+
"data": {
4+
"clientId": "apim_integration_test_client_id",
5+
"nhsNumber": "9001648967",
6+
"previousSupplierStatus": "received",
7+
"supplierStatus": "unnotified"
8+
},
9+
"datacontenttype": "application/json",
10+
"dataschema": "https://notify.nhs.uk/events/schemas/supplier-status/v1.json",
11+
"dataschemaversion": "1.0.0",
12+
"plane": "data",
13+
"specversion": "1.0",
14+
"type": "uk.nhs.notify.channels.nhsapp.SupplierStatusChange.v1"
15+
},
16+
"detail-type": "uk.nhs.notify.channels.nhsapp.SupplierStatusChange.v1",
17+
"region": "eu-west-2",
18+
"resources": [],
19+
"source": "custom.event",
20+
"version": "0"
21+
}

scripts/nft-event-generator/src/generate-events.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { randomUUID } from 'node:crypto';
22
import { SupplierStatusEvent } from 'types';
3+
import supplierStatusTemplate from 'event-templates/supplier-status.json';
34

45
type GenerateEventsParams = {
56
numberOfEvents: number;
@@ -21,38 +22,25 @@ function generateSupplierStatusEvent(
2122
const sharedPlanId = randomUUID();
2223

2324
return {
24-
version: '0',
25+
...supplierStatusTemplate,
2526
id: eventId,
26-
'detail-type': 'uk.nhs.notify.channels.nhsapp.SupplierStatusChange.v1',
27-
source: 'custom.event',
2827
account: '257995483745',
2928
time: now.toISOString(),
30-
region: 'eu-west-2',
31-
resources: [],
3229
detail: {
30+
...supplierStatusTemplate.detail,
3331
id: detailId,
3432
source: `//nhs.notify.uk/supplier-status/${environment}`,
35-
specversion: '1.0',
36-
type: 'uk.nhs.notify.channels.nhsapp.SupplierStatusChange.v1',
37-
plane: 'data',
3833
subject: sharedPlanId,
3934
time: now.toISOString(),
40-
datacontenttype: 'application/json',
41-
dataschema:
42-
'https://notify.nhs.uk/events/schemas/supplier-status/v1.json',
43-
dataschemaversion: '1.0.0',
4435
data: {
45-
nhsNumber: '9001648967',
36+
...supplierStatusTemplate.detail.data,
4637
delayedFallback,
4738
sendingGroupId,
48-
clientId: 'apim_integration_test_client_id',
49-
supplierStatus: 'unnotified',
50-
previousSupplierStatus: 'received',
5139
requestItemId,
5240
requestItemPlanId: sharedPlanId,
5341
},
5442
},
55-
};
43+
} as SupplierStatusEvent;
5644
}
5745

5846
function shuffle<T>(array: T[]): T[] {

0 commit comments

Comments
 (0)