Skip to content

Commit ebf4c83

Browse files
CCM-16553: Utility tool
1 parent 79ff291 commit ebf4c83

25 files changed

Lines changed: 1788 additions & 439 deletions

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ config:: _install-dependencies version dependencies # Configure development envi
6060
serve-docs:
6161
$(MAKE) -C docs s
6262

63+
perf-test:
64+
npm run start:nft --workspace=nft-event-generator
65+
6366
version:
6467
rm -f .version
6568
make version-create-effective-file dir=.

package-lock.json

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"src/digital-letters-events",
8484
"src/python-schema-generator",
8585
"src/typescript-schema-generator",
86+
"scripts/nft-event-generator",
8687
"tests/playwright",
8788
"tests/pact-tests"
8889
]

project.code-workspace

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
{ "name": "ttl-handle-expiry-lambda", "rootPath": "lambdas/ttl-handle-expiry-lambda" },
102102
{ "name": "ttl-poll-lambda", "rootPath": "lambdas/ttl-poll-lambda" },
103103
{ "name": "utils", "rootPath": "utils/utils" },
104+
{ "name": "event-generator", "rootPath": "scripts/nft-event-generator" },
104105
],
105106
"testing.defaultGutterClickAction": "runWithCoverage",
106107
},

scripts/config/vale/styles/config/vocabularies/words/accept.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ service_sorted
8787
setCachedSchema
8888
Someobject
8989
src
90+
[Ss]ubcommand
9091
stderr
9192
Syft
9293
temp_dir
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# NFT Event Generator
2+
3+
This script generates events and sends them in batches of 10 to the AWS EventBridge event bus: **`nhs-<environment>-dl`** (where `<environment>` is the environment specified at the command line).
4+
5+
It supports two event types, each invoked as a subcommand:
6+
7+
- **`supplier-api-letter-event`** – generates `SupplierApiLetterEvent` events (mirrors the `LetterEvent` consumed by `print-status-handler`)
8+
- **`paper-letter-opt-out-event`** – generates `PaperLetterOptedOut` channel status events, reading input from a CSV file
9+
10+
## Common features
11+
12+
- Custom environments (e.g. `pr293`, `main`, `nft`)
13+
- Controlled delay between batches of maximum 10 messages
14+
15+
---
16+
17+
## Subcommand: `supplier-api-letter-event`
18+
19+
Generates a configurable number of supplier API letter events with dynamic or fixed field values.
20+
21+
### CLI Options
22+
23+
| Option | Type | Required | Default | Description |
24+
|----------------------|--------|----------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
25+
| `--numberOfEvents` | number || | Total number of events to generate and send |
26+
| `--environment` | string || `nft` | Target environment (e.g. `main`, `nft`, `pr283`) |
27+
| `--interval` | number || `1000` | Delay between batches in milliseconds |
28+
| `--status` | string || `ACCEPTED` | Letter status for generated events. One of: `ACCEPTED`, `REJECTED`, `PRINTED`, `DISPATCHED`, `FAILED`, `RETURNED`, `PENDING`, `ENCLOSED`, `CANCELLED`, `FORWARDED`, `DELIVERED` |
29+
| `--id` | string || *(generated)* | Fixed event `id` (uuid). If omitted, a new uuid is generated per event |
30+
| `--time` | string || *(generated)* | Fixed event `time` (ISO 8601). If omitted, the current time is used per event |
31+
| `--subject` | string || *(generated)* | Fixed event `subject`. If omitted, a subject is built from `messageReference` |
32+
| `--messageReference` | string || *(generated)* | Fixed message reference (uuid) embedded in `subject` and `data.origin.subject`. If omitted, a new uuid is generated per event |
33+
34+
### Examples
35+
36+
Generate 2 events in the `nft` environment with a 2-second interval between batches:
37+
38+
```shell
39+
npm start -- supplier-api-letter-event --environment nft --numberOfEvents 2 --interval 2000
40+
```
41+
42+
Generate events with a specific status:
43+
44+
```shell
45+
npm start -- supplier-api-letter-event --environment pr293 --numberOfEvents 5 --status PRINTED
46+
```
47+
48+
Generate events with a fixed `messageReference` (useful for targeting a specific letter request):
49+
50+
```shell
51+
npm start -- supplier-api-letter-event --environment pr293 --numberOfEvents 1 --messageReference aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
52+
```
53+
54+
---
55+
56+
## Subcommand: `paper-letter-opt-out-event`
57+
58+
Reads a CSV file and generates one `PaperLetterOptedOut` channel status event per row.
59+
60+
### CSV format
61+
62+
The CSV file must have two columns per row (no header):
63+
64+
| Column | Description |
65+
|--------|---------------------------------|
66+
| 1 | `messageReference` (uuid) |
67+
| 2 | `senderId` |
68+
69+
Example `opt-outs.csv`:
70+
71+
```csv
72+
aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee,sender-001
73+
11111111-2222-3333-4444-555555555555,sender-002
74+
```
75+
76+
The `messageReference` field in each generated event is built as `<senderId>_<messageReference>`.
77+
78+
### CLI Options
79+
80+
| Option | Type | Required | Default | Description |
81+
|-----------------|--------|----------|---------------|----------------------------------------------------|
82+
| `--csvFile` | string || | Path to the CSV file (`messageReference,senderId`) |
83+
| `--environment` | string || `nft` | Target environment (e.g. `main`, `nft`, `pr283`) |
84+
| `--interval` | number || `1000` | Delay between batches in milliseconds |
85+
86+
### Examples
87+
88+
Send opt-out events from a CSV file to the `nft` environment:
89+
90+
```shell
91+
npm start -- paper-letter-opt-out-event --environment nft --csvFile ./opt-outs.csv
92+
```
93+
94+
Send to a PR environment with a custom batch interval:
95+
96+
```shell
97+
npm start -- paper-letter-opt-out-event --environment pr293 --csvFile ./opt-outs.csv --interval 500
98+
```
99+
100+
---
101+
102+
## Running via Make
103+
104+
To run this script from anywhere in the repository:
105+
106+
```shell
107+
make perf-test
108+
```
109+
110+
The make command runs the following script (configured in `package.json`):
111+
112+
```shell
113+
"start:nft": "npm start -- supplier-api-letter-event --environment nft --numberOfEvents 2 --interval 2000"
114+
```
115+
116+
## Help
117+
118+
To see all available options for a subcommand:
119+
120+
```shell
121+
npm start -- supplier-api-letter-event --help
122+
npm start -- paper-letter-opt-out-event --help
123+
```
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { Config } from 'jest';
2+
3+
const config: Config = {
4+
preset: 'ts-jest',
5+
testEnvironment: 'node',
6+
roots: ['<rootDir>'],
7+
testMatch: ['**/__tests__/**/*.test.ts', '**/?(*.)+(spec|test).ts'],
8+
transform: {
9+
'^.+\\.ts$': [
10+
'ts-jest',
11+
{
12+
tsconfig: {
13+
esModuleInterop: true,
14+
allowSyntheticDefaultImports: true,
15+
allowImportingTsExtensions: true,
16+
module: 'commonjs',
17+
target: 'ES2020',
18+
moduleResolution: 'node',
19+
noEmit: true,
20+
typeRoots: ['../../node_modules/@types'],
21+
},
22+
diagnostics: {
23+
ignoreCodes: [1343], // Ignore TS1343: import.meta errors
24+
},
25+
},
26+
],
27+
},
28+
modulePaths: ['<rootDir>/src'],
29+
collectCoverageFrom: [
30+
'src/**/*.{ts,js}',
31+
'!src/**/*.d.ts',
32+
'!src/**/__tests__/**',
33+
'!src/**/*.test.ts',
34+
'!src/**/*.spec.ts',
35+
'!src/cli.ts',
36+
],
37+
coverageDirectory: 'coverage',
38+
coveragePathIgnorePatterns: ['/node_modules/', '/__tests__/'],
39+
coverageThreshold: {
40+
global: {
41+
branches: 60,
42+
functions: 60,
43+
lines: 60,
44+
statements: 60,
45+
},
46+
},
47+
moduleFileExtensions: ['ts', 'js', 'json'],
48+
moduleNameMapper: {
49+
'^(.*)\\.ts$': '$1',
50+
},
51+
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
52+
testTimeout: 10_000,
53+
};
54+
55+
export default config;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Suppress console output
2+
console.log = jest.fn();
3+
console.warn = jest.fn();
4+
console.error = jest.fn();
5+
console.group = jest.fn();
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"author": "",
3+
"dependencies": {
4+
"@aws-sdk/client-eventbridge": "^3.914.0",
5+
"@nhsdigital/nhs-notify-event-schemas-status-published": "^1.0.1",
6+
"csv-parse": "^6.1.0",
7+
"digital-letters-events": "^0.0.1",
8+
"tsx": "^4.20.6",
9+
"yargs": "^17.7.2"
10+
},
11+
"description": "This script generates events such LetterEvent from Supplier API and sends them to the AWS event bus: **`nhs-main-nudge-inbound-event-queue`**",
12+
"devDependencies": {
13+
"@jest/diff-sequences": "^30.0.1",
14+
"@tsconfig/node22": "^22.0.5",
15+
"@types/csv-parse": "^1.1.12",
16+
"@types/jest": "^30.0.0",
17+
"@types/node": "^24.5.2",
18+
"@types/yargs": "^17.0.33",
19+
"eslint": "^9.37.0",
20+
"eslint-plugin-react-hooks": "^7.0.1",
21+
"exit-x": "^0.2.2",
22+
"jest": "^30.2.0",
23+
"jest-html-reporter": "^4.3.0",
24+
"jest-mock-extended": "^4.0.0",
25+
"ts-jest": "^29.4.5",
26+
"typescript": "^5.9.3"
27+
},
28+
"keywords": [],
29+
"license": "ISC",
30+
"main": "index.js",
31+
"name": "nft-event-generator",
32+
"scripts": {
33+
"lint": "eslint .",
34+
"lint:fix": "eslint . --fix",
35+
"start": "tsx src/cli.ts",
36+
"start:nft": "npm start -- --environment nft --numberOfEvents 2 --interval 2000",
37+
"test:unit": "jest",
38+
"typecheck": "tsc --noEmit"
39+
},
40+
"type": "commonjs",
41+
"version": "1.0.0"
42+
}
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 { sampleSupplierApiLetterEvent } from '__tests__/fixtures/sample-supplier-api-letter-event';
6+
import { PublishableEvent } from 'destinations/destination-client';
7+
import { mock } from 'jest-mock-extended';
8+
import { sendEventsToEventBus } from 'destinations/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, [sampleSupplierApiLetterEvent], 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(sampleSupplierApiLetterEvent.source);
44+
expect(entry.DetailType).toBe(sampleSupplierApiLetterEvent.type);
45+
expect(entry.Detail).toBe(JSON.stringify(sampleSupplierApiLetterEvent));
46+
});
47+
48+
it('should send a request for each batch of messages', async () => {
49+
const events: PublishableEvent[] = Array.from(
50+
{ length: 52 },
51+
() => sampleSupplierApiLetterEvent,
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+
() => sampleSupplierApiLetterEvent,
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, [sampleSupplierApiLetterEvent], 5);
87+
88+
expect(console.warn).toHaveBeenCalled();
89+
});
90+
});

0 commit comments

Comments
 (0)