Skip to content

Commit 0ca439f

Browse files
committed
CCM-10483: wip spike
1 parent 8f1c6ee commit 0ca439f

File tree

11 files changed

+1528
-17
lines changed

11 files changed

+1528
-17
lines changed

package-lock.json

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

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@
4141
"lint": "npm run lint --workspaces",
4242
"lint:fix": "npm run lint:fix --workspaces",
4343
"start": "npm run start --workspace frontend",
44+
"test:contracts:clean": "npm --workspace=tests/contracts run pact:clean",
45+
"test:contracts:consumers": "npm --workspace=tests/contracts run test:consumers",
46+
"test:contracts:producer": "npm --workspace=tests/contracts run test:producer",
47+
"test:contracts:upload": "npm --workspace=tests/contracts run upload:consumer-pacts",
4448
"test:unit": "npm run test:unit --workspaces",
4549
"typecheck": "npm run typecheck --workspaces"
4650
},
@@ -53,6 +57,7 @@
5357
"lambdas/download-authorizer",
5458
"lambdas/sftp-letters",
5559
"tests/accessibility",
60+
"tests/contracts",
5661
"tests/test-team",
5762
"utils/backend-config",
5863
"utils/entity-update-command-builder",

tests/contracts/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*/pacts/
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import path from 'node:path';
2+
import {
3+
MessageConsumerPact,
4+
Matchers,
5+
asynchronousBodyHandler,
6+
} from '@pact-foundation/pact';
7+
import { z } from 'zod';
8+
9+
// I guess this would be defined in the handler source code in the event consumer and imported here
10+
// Handlers should parse the incoming event before doing anything else
11+
const $TemplateDeletedEvent = z.object({
12+
'detail-type': z.literal('TemplateDeleted'),
13+
version: z.literal('1.0'),
14+
detail: z.object({
15+
id: z.string().uuid(),
16+
owner: z.string().uuid(),
17+
}),
18+
});
19+
20+
// Simulate consumer handler that processes the incoming event
21+
// Only check the validation - don't run actual handler logic
22+
async function handleTemplateDeleted(event: unknown): Promise<void> {
23+
$TemplateDeletedEvent.parse(event);
24+
}
25+
26+
describe('Pact Message Consumer - TemplateDeleted Event', () => {
27+
const messagePact = new MessageConsumerPact({
28+
consumer: 'consumer-2',
29+
provider: 'templates',
30+
dir: path.resolve(__dirname, 'pacts'),
31+
pactfileWriteMode: 'update',
32+
logLevel: 'error',
33+
});
34+
35+
it('should validate the template deleted event structure and handler logic', async () => {
36+
await messagePact
37+
.given('A template has been deleted')
38+
.expectsToReceive('TemplateDeleted')
39+
.withContent({
40+
'detail-type': 'TemplateDeleted',
41+
version: '1.0',
42+
detail: {
43+
owner: Matchers.uuid('c0574019-4629-4b3f-8987-aa34ca8bc5b9'),
44+
id: Matchers.uuid('b18a9a49-72a8-4157-8b85-76d5ac5c7804'),
45+
},
46+
})
47+
.verify(asynchronousBodyHandler(handleTemplateDeleted));
48+
});
49+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import path from 'node:path';
2+
import {
3+
MessageConsumerPact,
4+
Matchers,
5+
asynchronousBodyHandler,
6+
} from '@pact-foundation/pact';
7+
import { z } from 'zod';
8+
9+
// I guess this would be defined in the handler source code in the event consumer and imported here
10+
// Handlers should parse the incoming event before doing anything else
11+
const $TemplateDeletedEvent = z.object({
12+
'detail-type': z.literal('TemplateDeleted'),
13+
detail: z.object({
14+
id: z.string().uuid(),
15+
owner: z.string().uuid(),
16+
}),
17+
});
18+
19+
// Simulate consumer handler that processes the incoming event
20+
// Only check the validation - don't run actual handler logic
21+
async function handleTemplateDeleted(event: unknown): Promise<void> {
22+
$TemplateDeletedEvent.parse(event);
23+
}
24+
25+
describe('Pact Message Consumer - TemplateDeleted Event', () => {
26+
const messagePact = new MessageConsumerPact({
27+
consumer: 'core',
28+
provider: 'templates',
29+
dir: path.resolve(__dirname, 'pacts'),
30+
pactfileWriteMode: 'update',
31+
logLevel: 'error',
32+
});
33+
34+
it('should validate the template deleted event structure and handler logic', async () => {
35+
await messagePact
36+
.given('A template has been deleted')
37+
.expectsToReceive('TemplateDeleted')
38+
.withContent({
39+
'detail-type': 'TemplateDeleted',
40+
detail: {
41+
owner: Matchers.uuid('c0574019-4629-4b3f-8987-aa34ca8bc5b9'),
42+
id: Matchers.uuid('b18a9a49-72a8-4157-8b85-76d5ac5c7804'),
43+
},
44+
})
45+
.verify(asynchronousBodyHandler(handleTemplateDeleted));
46+
});
47+
});

tests/contracts/jest.config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { Config } from 'jest';
2+
3+
const config: Config = {
4+
preset: 'ts-jest',
5+
testEnvironment: 'node',
6+
testMatch: ['**/tests/**/*.pact.test.ts'],
7+
transform: { '^.+\\.ts$': '@swc/jest' },
8+
};
9+
10+
export default config;

tests/contracts/package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"devDependencies": {
3+
"@pact-foundation/pact": "^15.0.1",
4+
"@swc/core": "^1.11.13",
5+
"@swc/jest": "^0.2.37",
6+
"@tsconfig/node20": "^20.1.5",
7+
"jest": "^29.7.0",
8+
"zod": "^3.24.2"
9+
},
10+
"name": "contract-tests",
11+
"private": true,
12+
"scripts": {
13+
"pact:clean": "./scripts/clean.sh",
14+
"test:consumers": "jest consumer consumer-2",
15+
"test:producer": "cp -r consumer/pacts producer && cp -r consumer-2/pacts producer && jest producer/",
16+
"upload:consumer-pacts": "./scripts/upload-consumer-pacts.sh"
17+
},
18+
"version": "1.0.0"
19+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import { MessageProviderPact } from '@pact-foundation/pact';
4+
5+
// This would be the actual function that produces the event payload in the producer source code
6+
function produceTemplateDeletedEvent(): any {
7+
return {
8+
'detail-type': 'TemplateDeleted',
9+
source: 'uk.nhs.notify.templates',
10+
time: new Date().toISOString(),
11+
version: '1.0',
12+
detail: {
13+
owner: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
14+
id: 'de305d54-75b4-431b-adb2-eb6b9e546014',
15+
},
16+
};
17+
}
18+
19+
describe('Pact Message Provider - TemplateDeleted Event', () => {
20+
const pactDir = path.resolve(__dirname, 'pacts');
21+
22+
const messagePact = new MessageProviderPact({
23+
provider: 'templates',
24+
pactUrls: fs
25+
.readdirSync(pactDir)
26+
.filter((f) => f.endsWith('.json'))
27+
.map((f) => path.join(pactDir, f)),
28+
messageProviders: {
29+
TemplateDeleted: () => produceTemplateDeletedEvent(),
30+
},
31+
logLevel: 'error',
32+
});
33+
34+
it('should produce a message that satisfies the consumer contract for TemplateDeleted', () => {
35+
return messagePact.verify();
36+
});
37+
});

tests/contracts/scripts/clean.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
script_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )";
5+
6+
dirs=("consumer" "consumer-2" "producer")
7+
8+
for dir in "${dirs[@]}"; do
9+
pact_dir="$(realpath "${script_path}/../${dir}")/pacts"
10+
11+
echo "Removing pact files in $pact_dir..."
12+
13+
rm -rf $pact_dir
14+
done
15+
16+
echo "All Pact files deleted successfully."
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
script_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )";
5+
6+
consumer_dirs=("consumer" "consumer-2")
7+
pact_dir="pacts"
8+
9+
VERSION_TAG=${PACT_VERSION:-$(git rev-parse --abbrev-ref HEAD)}
10+
11+
for consumer in "${consumer_dirs[@]}"; do
12+
consumer_pact_dir=$(realpath "${script_path}/../${consumer}/${pact_dir}")
13+
14+
echo "Looking for pact files in $consumer_pact_dir..."
15+
16+
for file in "$consumer_pact_dir"/*.json; do
17+
if [[ -f "$file" ]]; then
18+
# Extract consumer and provider names from filename
19+
filename=$(basename "$file")
20+
provider=$(cat $file | jq -r ".provider.name")
21+
22+
# Define S3 target path
23+
targetPath="pacts/$provider/$filename"
24+
25+
echo "Uploading to s3://$PACT_BUCKET/$targetPath"
26+
# aws s3 cp "$file" "s3://$PACT_BUCKET/$targetPath" --acl bucket-owner-full-control
27+
fi
28+
done
29+
done
30+
31+
echo "All Pact files uploaded successfully."

0 commit comments

Comments
 (0)