Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .github/workflows/temp-unit-tests-only.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: "Temp unit tests only"

on:
workflow_dispatch:
inputs:
nodejs_version:
description: "Node.js version, set by the CI/CD pipeline workflow"
required: true
type: string
python_version:
description: "Python version, set by the CI/CD pipeline workflow"
required: true
type: string
push:
branches:
- feature/CCM-14615_unit-test-quickening

env:
AWS_REGION: eu-west-2
TERM: xterm-256color

jobs:
test-unit:
name: "Unit tests"
runs-on: ubuntu-latest
timeout-minutes: 7
permissions:
contents: read
packages: read
steps:
- name: "Checkout code"
uses: actions/checkout@v5
- uses: ./.github/actions/node-install
with:
node-version: ${{ inputs.nodejs_version }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: "Setup Python"
uses: actions/setup-python@v6
with:
python-version: ${{ inputs.python_version }}
cache: 'pip'
cache-dependency-path: '**/requirements*.txt'
- name: "Run unit test suite"
run: |
make test-unit
- name: "Save the result of fast test suite"
uses: actions/upload-artifact@v4
with:
name: unit-tests
path: "**/.reports/unit"
include-hidden-files: true
if: always()
- name: "Save the result of code coverage"
uses: actions/upload-artifact@v4
with:
name: code-coverage-report
path: ".reports/lcov.info"
- name: "Save Python coverage reports"
uses: actions/upload-artifact@v4
with:
name: python-coverage-reports
path: |
src/**/coverage.xml
utils/**/coverage.xml
lambdas/**/coverage.xml
151 changes: 151 additions & 0 deletions jest.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* Root Jest config — runs all TypeScript/JavaScript workspace test suites in
* parallel via Jest's native `projects` support.
*
* Written as CJS (.cjs) so Jest can load it without needing a root tsconfig.json.
* The base config is inlined from jest.config.base.ts to keep this file
* self-contained and avoid any TypeScript compilation at load time, which would
* require a root tsconfig.json and risk interfering with workspace ts-jest runs.
*
* When jest.config.base.ts changes, this file must be kept in sync manually.
*
* Each project's rootDir is set to its workspace directory so that relative
* paths (coverageDirectory, HTML reporter outputPath, etc.) resolve relative
* to the workspace, not the repo root.
*
* Note: src/cloudevents has a hand-rolled jest.config.cjs; it is included via
* its directory path so Jest discovers that file directly.
*
* Note: src/digital-letters-events and tests/playwright have no Jest tests
* and are intentionally excluded.
*/

const base = {
preset: 'ts-jest',
clearMocks: true,
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/__tests__/**',
'!src/**/*.test.{ts,tsx}',
'!src/**/*.spec.{ts,tsx}',
],
coverageDirectory: './.reports/unit/coverage',
coverageProvider: 'babel',
coverageThreshold: {
global: { branches: 100, functions: 100, lines: 100, statements: -10 },
},
coveragePathIgnorePatterns: ['/__tests__/'],
transform: { '^.+\\.ts$': 'ts-jest' },
testPathIgnorePatterns: ['.build'],
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
reporters: [
'default',
[
'jest-html-reporter',
{
pageTitle: 'Test Report',
outputPath: './.reports/unit/test-report.html',
includeFailureMsg: true,
},
],
],
testEnvironment: 'node',
moduleDirectories: ['node_modules', 'src'],
};

// Workspaces that use the base config with no overrides
const standardWorkspaces = [
'lambdas/file-scanner-lambda',
'lambdas/key-generation',
'lambdas/refresh-apim-access-token',
'lambdas/pdm-mock-lambda',
'lambdas/pdm-poll-lambda',
'lambdas/ttl-create-lambda',
'lambdas/ttl-handle-expiry-lambda',
'lambdas/ttl-poll-lambda',
'lambdas/pdm-uploader-lambda',
'lambdas/print-sender-lambda',
'lambdas/print-analyser',
'lambdas/report-scheduler',
'lambdas/report-event-transformer',
'lambdas/move-scanned-files-lambda',
'lambdas/report-generator',
'utils/sender-management',
];

/** @type {import('jest').Config} */
module.exports = {
projects: [
// Standard workspaces — no overrides
...standardWorkspaces.map((ws) => ({
...base,
rootDir: `<rootDir>/${ws}`,
displayName: ws,
})),

// utils/utils — relaxed coverage thresholds + exclude index.ts
{
...base,
rootDir: '<rootDir>/utils/utils',
displayName: 'utils/utils',
coverageThreshold: {
global: { branches: 85, functions: 85, lines: 85, statements: -10 },
},
coveragePathIgnorePatterns: [...base.coveragePathIgnorePatterns, 'index.ts'],
},

// lambdas/core-notifier-lambda — moduleNameMapper unifies `crypto` and
// `node:crypto` in Jest's registry so that jest.mock('node:crypto') in the
// test files also intercepts the bare require('crypto') call made by
// node-jose at module-load time, preventing an undefined helpers.nodeCrypto
// crash in ecdsa.js.
{
...base,
rootDir: '<rootDir>/lambdas/core-notifier-lambda',
displayName: 'lambdas/core-notifier-lambda',
},

// lambdas/print-status-handler — @nhsdigital/nhs-notify-event-schemas-supplier-api
// ships ESM source; it must be transformed by ts-jest rather than skipped.
{
...base,
rootDir: '<rootDir>/lambdas/print-status-handler',
displayName: 'lambdas/print-status-handler',
transformIgnorePatterns: [
'node_modules/(?!@nhsdigital/nhs-notify-event-schemas-supplier-api)',
],
},

// src/python-schema-generator — excludes merge-allof CLI entry point
{
...base,
rootDir: '<rootDir>/src/python-schema-generator',
displayName: 'src/python-schema-generator',
coveragePathIgnorePatterns: [
...base.coveragePathIgnorePatterns,
'src/merge-allof-cli.ts',
],
},

// src/typescript-schema-generator — excludes CLI entry points.
// Requires --experimental-vm-modules (set via NODE_OPTIONS in the
// test:unit:parallel script) because json-schema-to-typescript uses a
// dynamic import() of prettier at runtime, which Node.js rejects inside a
// Jest VM context without the flag.
{
...base,
rootDir: '<rootDir>/src/typescript-schema-generator',
displayName: 'src/typescript-schema-generator',
coveragePathIgnorePatterns: [
...base.coveragePathIgnorePatterns,
'src/generate-types-cli.ts',
'src/generate-validators-cli.ts',
],
},

// src/cloudevents — uses its own jest.config.cjs (hand-rolled ts-jest options)
'<rootDir>/src/cloudevents',
],
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ import { IAccessTokenRepository, NotifyClient } from 'app/notify-api-client';
import { RequestAlreadyReceivedError } from 'domain/request-already-received-error';

jest.mock('utils');
jest.mock('node:crypto');
// Use a partial manual mock so that node-jose's require('crypto') still gets
// the real crypto implementation (needed for getHashes() etc.) while
// randomUUID is replaced with a jest.fn() for test control.
jest.mock('node:crypto', () => ({
...jest.requireActual<typeof import('node:crypto')>('node:crypto'),
randomUUID: jest.fn(),
}));
jest.mock('axios', () => {
const original: AxiosStatic = jest.requireActual('axios');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ import { PDMResourceAvailable } from 'digital-letters-events';
import { randomUUID } from 'node:crypto';

jest.mock('utils');
jest.mock('node:crypto');
// Use a partial manual mock so that node-jose's require('crypto') still gets
// the real crypto implementation (needed for getHashes() etc.) while
// randomUUID is replaced with a jest.fn() for test control.
jest.mock('node:crypto', () => ({
...jest.requireActual<typeof import('node:crypto')>('node:crypto'),
randomUUID: jest.fn(),
}));

const mockLogger = jest.mocked(logger);
const mockRandomUUID = jest.mocked(randomUUID);
Expand Down
1 change: 1 addition & 0 deletions lambdas/mesh-acknowledge/pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ addopts = -v --tb=short

[coverage:run]
relative_files = True
data_file = lambdas/mesh-acknowledge/.coverage
omit =
*/mesh_acknowledge/__tests__/*
*/test_*.py
Expand Down
1 change: 1 addition & 0 deletions lambdas/mesh-download/pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ addopts = -v --tb=short

[coverage:run]
relative_files = True
data_file = lambdas/mesh-download/.coverage
omit =
*/tests/*
*/test_*.py
Expand Down
1 change: 1 addition & 0 deletions lambdas/mesh-poll/pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ addopts = -v --tb=short

[coverage:run]
relative_files = True
data_file = lambdas/mesh-poll/.coverage
omit =
*/mesh_poll/__tests__/*
*/test_*.py
Expand Down
1 change: 1 addition & 0 deletions lambdas/report-sender/pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ addopts = -v --tb=short

[coverage:run]
relative_files = True
data_file = lambdas/report-sender/.coverage
omit =
*/report_sender/__tests__/*
*/test_*.py
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"lint:fix": "npm run lint:fix --workspaces",
"start": "npm run start --workspace frontend",
"test:unit": "npm run test:unit --workspaces",
"test:unit:parallel": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --config jest.config.cjs",
"typecheck": "npm run typecheck --workspaces"
},
"version": "0.0.1",
Expand Down
Loading
Loading