diff --git a/.github/actions/acceptance-tests/action.yml b/.github/actions/acceptance-tests/action.yml index bf6148cda..3a0998696 100644 --- a/.github/actions/acceptance-tests/action.yml +++ b/.github/actions/acceptance-tests/action.yml @@ -37,7 +37,6 @@ runs: - name: "Repo setup" uses: ./.github/actions/node-install with: - node-version: ${{ steps.nodejs_version.outputs.nodejs_version }} GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }} - name: "Set PR NUMBER" diff --git a/.github/actions/node-install/action.yaml b/.github/actions/node-install/action.yaml index 534ebc097..48527b570 100644 --- a/.github/actions/node-install/action.yaml +++ b/.github/actions/node-install/action.yaml @@ -2,9 +2,6 @@ name: 'npm install and setup' description: 'Setup node, authenticate github package repository and perform clean npm install' inputs: - node-version: - description: 'Node.js version' - required: true GITHUB_TOKEN: description: "Token for access to github package registry" required: true @@ -15,7 +12,7 @@ runs: - name: 'Use Node.js' uses: actions/setup-node@v6 with: - node-version: '${{ inputs.node-version }}' + node-version-file: '.tool-versions' registry-url: 'https://npm.pkg.github.com' scope: '@nhsdigital' diff --git a/.github/workflows/pr_closed.yaml b/.github/workflows/pr_closed.yaml index 003cf976e..31f81c713 100644 --- a/.github/workflows/pr_closed.yaml +++ b/.github/workflows/pr_closed.yaml @@ -80,7 +80,7 @@ jobs: - name: Setup NodeJS uses: actions/setup-node@v4 with: - node-version: ${{ inputs.nodejs_version }} + node-version-file: '.tool-versions' registry-url: 'https://npm.pkg.github.com' - name: check if local version differs from latest published version @@ -113,19 +113,14 @@ jobs: steps: - name: "Checkout code" uses: actions/checkout@v5.0.0 - - name: Setup NodeJS - uses: actions/setup-node@v4 + - name: "Repo setup" + uses: ./.github/actions/node-install with: - node-version: ${{ inputs.nodejs_version }} - registry-url: 'https://npm.pkg.github.com' - - name: "Install dependencies" - env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npm ci + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: "Run provider contract tests" run: make test-contract env: - GITHUB_PACKAGES_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} publish-event-schemas: name: Publish event schemas package to GitHub package registry @@ -145,7 +140,7 @@ jobs: - name: Setup NodeJS uses: actions/setup-node@v4 with: - node-version: ${{ inputs.nodejs_version }} + node-version-file: '.tool-versions' registry-url: 'https://npm.pkg.github.com' - name: Install dependencies diff --git a/Makefile b/Makefile index 9e4b8db20..14f31336f 100644 --- a/Makefile +++ b/Makefile @@ -106,6 +106,9 @@ test-component: test-performance: (cd tests && npm install && npm run test:performance) +test-contract: # Run provider contract tests @Testing + npm run test:contracts --workspace tests/contracts/provider + version: rm -f .version make version-create-effective-file dir=. diff --git a/package-lock.json b/package-lock.json index 64a58b073..9d7aec58d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "lambdas/*", "pact-contracts", "scripts/utilities/*", - "tests" + "tests", + "tests/contracts/*" ], "dependencies": { "@aws-sdk/client-api-gateway": "^3.906.0", @@ -125,7 +126,7 @@ }, "internal/events": { "name": "@nhsdigital/nhs-notify-event-schemas-supplier-api", - "version": "1.0.9", + "version": "1.0.10", "license": "MIT", "dependencies": { "@asyncapi/bundler": "^0.6.4", @@ -6079,6 +6080,10 @@ "dev": true, "license": "MIT" }, + "node_modules/@sap/contracts-provider": { + "resolved": "tests/contracts/provider", + "link": true + }, "node_modules/@sinclair/typebox": { "version": "0.34.47", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.47.tgz", @@ -21309,13 +21314,6 @@ "node": ">=18.17" } }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "extraneous": true, - "license": "MIT" - }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -22333,6 +22331,41 @@ "typescript": "^5.9.3" } }, + "tests/contracts/provider": { + "name": "@sap/contracts-provider", + "version": "1.0.0", + "dependencies": { + "@nhsdigital/nhs-notify-event-schemas-supplier-api": "*", + "@pact-foundation/pact": "^16.0.4" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.2", + "@types/jest": "^30.0.0", + "@types/node": "^22.0.0", + "eslint": "^9.27.0", + "glob": "^11.0.0", + "jest": "^30.0.0", + "ts-jest": "^29.4.0", + "typescript": "^5.9.3" + } + }, + "tests/contracts/provider/node_modules/@types/node": { + "version": "22.19.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", + "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "tests/contracts/provider/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "tests/node_modules/@types/node": { "version": "24.10.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", @@ -22343,6 +22376,62 @@ "undici-types": "~7.16.0" } }, + "tests/node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "tests/node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "tests/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "tests/node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", diff --git a/package.json b/package.json index 077c7a797..ea770ef3e 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "lambdas/*", "pact-contracts", "scripts/utilities/*", - "tests" + "tests", + "tests/contracts/*" ] } diff --git a/tests/contracts/provider/__tests__/letter-status.provider.pact.test.ts b/tests/contracts/provider/__tests__/letter-status.provider.pact.test.ts new file mode 100644 index 000000000..7d6b3b2b2 --- /dev/null +++ b/tests/contracts/provider/__tests__/letter-status.provider.pact.test.ts @@ -0,0 +1,23 @@ +import { MessageProviderPact } from "@pact-foundation/pact"; +import { + LETTER_STATUSES, + getMessageProviderForStatus, + getPactUrlForStatus, +} from "./utils/utils"; + +const CONSUMER_PACKAGE = "@nhsdigital/notify-core-consumer-contracts"; + +describe("Supplier API letter status provider tests", () => { + describe.each(LETTER_STATUSES)("letter.%s event", (status) => { + test(`verifies letter-${status.toLowerCase()} pact`, async () => { + const p = new MessageProviderPact({ + provider: `letter-${status.toLowerCase()}`, + messageProviders: getMessageProviderForStatus(status), + pactUrls: [getPactUrlForStatus(CONSUMER_PACKAGE, status)], + logLevel: "error", + }); + + await expect(p.verify()).resolves.not.toThrow(); + }, 60_000); + }); +}); diff --git a/tests/contracts/provider/__tests__/utils/utils.ts b/tests/contracts/provider/__tests__/utils/utils.ts new file mode 100644 index 000000000..15fe204b9 --- /dev/null +++ b/tests/contracts/provider/__tests__/utils/utils.ts @@ -0,0 +1,56 @@ +import path from "node:path"; +import fs from "node:fs"; + +export const LETTER_STATUSES = [ + "ACCEPTED", + "CANCELLED", + "DELIVERED", + "DISPATCHED", + "ENCLOSED", + "FAILED", + "FORWARDED", + "PENDING", + "PRINTED", + "REJECTED", + "RETURNED", +] as const; + +type LetterStatus = (typeof LETTER_STATUSES)[number]; + +export function getExampleEvent(status: LetterStatus): unknown { + const examplePath = path.join( + __dirname, + "../../../../../internal/events/schemas/examples", + `letter.${status}.json`, + ); + + const content = fs.readFileSync(examplePath, "utf8"); + return JSON.parse(content); +} + +export function getMessageProviderForStatus( + status: LetterStatus, +): Record Promise> { + return { + [`letter-${status.toLowerCase()}`]: async () => getExampleEvent(status), + }; +} + +export function getPactUrlForStatus( + consumerPackage: string, + status: LetterStatus, +): string { + return path.join( + __dirname, + "../../", + ".contracts", + consumerPackage, + "pacts", + "supplier-api", + `core-letter-${status.toLowerCase()}.json`, + ); +} + +export function getAllLetterStatuses(): readonly LetterStatus[] { + return LETTER_STATUSES; +} diff --git a/tests/contracts/provider/jest.config.ts b/tests/contracts/provider/jest.config.ts new file mode 100644 index 000000000..dd91529ca --- /dev/null +++ b/tests/contracts/provider/jest.config.ts @@ -0,0 +1,15 @@ +import type { Config } from "jest"; + +const config: Config = { + testEnvironment: "node", + transform: { + "^.+\\.tsx?$": "ts-jest", + }, + testMatch: ["**/*.test.ts"], + moduleNameMapper: { + "@nhsdigital/nhs-notify-event-schemas-supplier-api$": + "/../../../internal/events/src", + }, +}; + +export default config; diff --git a/tests/contracts/provider/package.json b/tests/contracts/provider/package.json new file mode 100644 index 000000000..8f603ba5a --- /dev/null +++ b/tests/contracts/provider/package.json @@ -0,0 +1,26 @@ +{ + "name": "@sap/contracts-provider", + "version": "1.0.0", + "private": true, + "scripts": { + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "test:contracts": "./scripts/test.sh", + "test:unit": "echo Unit tests not required", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@nhsdigital/nhs-notify-event-schemas-supplier-api": "*", + "@pact-foundation/pact": "^16.0.4" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.2", + "@types/jest": "^30.0.0", + "@types/node": "^22.0.0", + "eslint": "^9.27.0", + "glob": "^11.0.0", + "jest": "^30.0.0", + "ts-jest": "^29.4.0", + "typescript": "^5.9.3" + } +} diff --git a/tests/contracts/provider/scripts/test.sh b/tests/contracts/provider/scripts/test.sh new file mode 100755 index 000000000..80a13e778 --- /dev/null +++ b/tests/contracts/provider/scripts/test.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(git rev-parse --show-toplevel)" +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +TESTS_ROOT="$(dirname "$SCRIPT_DIR")" + +cd "$TESTS_ROOT" + +rm -rf .packages .contracts +mkdir -p .packages + +CONSUMER_PACKAGES=( + "@nhsdigital/notify-core-consumer-contracts" +) + +for PKG in "${CONSUMER_PACKAGES[@]}"; do + mkdir -p ".contracts/$PKG" + TGZ_NAME=$(npm pack "$PKG" --pack-destination .packages) + tar -xvzf ".packages/$TGZ_NAME" -C ".contracts/$PKG" --strip-components=1 +done + +npx jest --runInBand diff --git a/tests/contracts/provider/tsconfig.json b/tests/contracts/provider/tsconfig.json new file mode 100644 index 000000000..3d815db46 --- /dev/null +++ b/tests/contracts/provider/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./", + "resolveJsonModule": true, + "esModuleInterop": true, + "isolatedModules": true + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/tests/pact-tests/run-pact-tests.sh b/tests/pact-tests/run-pact-tests.sh index 163268e44..a61a9f643 100755 --- a/tests/pact-tests/run-pact-tests.sh +++ b/tests/pact-tests/run-pact-tests.sh @@ -3,7 +3,7 @@ set -euo pipefail # Ensure we have the latest package matching the major version -npm install --no-lockfile @nhsdigital/nhs-notify-event-schemas-letter-rendering@^2.0.0 +npm install --no-lockfile @nhsdigital/nhs-notify-event-schemas-letter-rendering@^2 # Remove old PACTs rm -rf ./.pacts