diff --git a/README.md b/README.md
index 7ae035f3..f99d8fcf 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,7 @@ Core delivery platform Node.js Frontend Template.
- [Requirements](#requirements)
- [Node.js](#nodejs)
- [DXT Forms Engine Plugin](#dxt-forms-engine-plugin)
+- [Static File Routes & Accessibility Scanner Support](#static-file-routes--accessibility-scanner-support)
- [Features](#features)
- [Development Tools & Configuration](#development-tools--configuration)
- [Testing Framework](#testing-framework)
@@ -144,6 +145,24 @@ sequenceDiagram
Ctrl-->>User: Redirect to confirmation
```
+## Static File Routes & Accessibility Scanner Support
+
+The CDP dev environment runs automated accessibility testing using HeadlessChrome. This scanner attempts to load icon resources (e.g., `aria.svg`, `header.svg`, `h1.svg`, etc.) to visualise accessibility features on the page. These icons are not part of the application itself but are used by the scanner's visualisation overlay.
+
+To prevent 400 errors in logs from these scanner requests, a catch-all route was added in `src/server/common/helpers/serve-static-files.js` that:
+
+- Intercepts all `/img/{param*}` requests
+- Returns a valid empty SVG with 200 OK status
+- Satisfies the accessibility scanner without cluttering logs with errors
+
+This ensures clean logs while maintaining compatibility with CDP platform accessibility monitoring tools.
+
+Test locally with:
+
+```bash
+curl -v http://localhost:3000/img/icons/aria.svg`
+```
+
## Development Tools & Configuration
### Testing Framework
diff --git a/package-lock.json b/package-lock.json
index f0fd0823..bc939854 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -204,7 +204,6 @@
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -1906,7 +1905,6 @@
"integrity": "sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@keyv/serialize": "^1.1.1"
}
@@ -2020,7 +2018,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
},
@@ -2044,7 +2041,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -5627,7 +5623,6 @@
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/estree": "*",
"@types/json-schema": "*"
@@ -5719,7 +5714,6 @@
"integrity": "sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -5779,7 +5773,6 @@
"integrity": "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.42.0",
"@typescript-eslint/types": "8.42.0",
@@ -6445,7 +6438,6 @@
"integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@vitest/utils": "3.2.4",
"fflate": "^0.8.2",
@@ -6757,7 +6749,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -6802,7 +6793,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -7639,7 +7629,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754",
@@ -7957,7 +7946,6 @@
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@@ -9406,7 +9394,6 @@
"integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -9484,7 +9471,6 @@
"integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -9664,7 +9650,6 @@
"integrity": "sha512-vPZZsiOKaBAIATpFE2uMI4w5IRwdv/FpQ+qZZMR4E+PeOcM4OeoEbqxRMnywdxP19TyB/3h6QBB0EWon7letSQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/types": "^8.35.0",
"comment-parser": "^1.4.1",
@@ -11168,7 +11153,6 @@
"integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
@@ -12567,7 +12551,6 @@
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
@@ -12577,7 +12560,6 @@
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
"integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
"license": "BSD-3-Clause",
- "peer": true,
"dependencies": {
"@hapi/hoek": "^9.3.0",
"@hapi/topo": "^5.1.0",
@@ -12660,7 +12642,6 @@
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -13580,9 +13561,9 @@
}
},
"node_modules/node-forge": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
- "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz",
+ "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==",
"license": "(BSD-3-Clause OR GPL-2.0)",
"engines": {
"node": ">= 6.13.0"
@@ -14895,7 +14876,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -15433,7 +15413,6 @@
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -15513,7 +15492,6 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -16348,7 +16326,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
@@ -16370,7 +16347,6 @@
"integrity": "sha512-daqnoAA+AmXvcL1fvJRMd4RDPZM2s27qYxb51c5TYc1B1Zugu0gVGyA5leoXQJEzo6sDTQ95J8X0yFcdBNGNtw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@bufbuild/protobuf": "^2.5.0",
"buffer-builder": "^0.2.0",
@@ -17808,7 +17784,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4",
@@ -18251,7 +18226,6 @@
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
"dev": true,
"license": "BSD-2-Clause",
- "peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
@@ -18450,7 +18424,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -18683,7 +18656,6 @@
"integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
@@ -18858,7 +18830,6 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -18927,7 +18898,6 @@
"integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.47.0",
"@typescript-eslint/types": "8.47.0",
@@ -19567,7 +19537,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -19581,7 +19550,6 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
@@ -19731,7 +19699,6 @@
"integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
diff --git a/package.json b/package.json
index 8ab9bd69..d190b438 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,8 @@
"author": "Defra DDTS",
"license": "OGL-UK-3.0",
"overrides": {
- "tmp": "0.2.4"
+ "tmp": "0.2.4",
+ "node-forge": "1.3.2"
},
"dependencies": {
"@babel/runtime": "7.28.4",
diff --git a/src/server/common/helpers/serve-static-files.js b/src/server/common/helpers/serve-static-files.js
index 5495538a..999d1149 100644
--- a/src/server/common/helpers/serve-static-files.js
+++ b/src/server/common/helpers/serve-static-files.js
@@ -44,6 +44,15 @@ export const serveStaticFiles = {
},
options
},
+ {
+ method: 'GET',
+ path: '/img/{param*}',
+ handler(_request, h) {
+ const emptySvg = ''
+ return h.response(emptySvg).code(statusCodes.ok).type('image/svg+xml')
+ },
+ options
+ },
{
method: 'GET',
path: `${config.get('assetPath')}/{param*}`,
diff --git a/src/server/common/helpers/serve-static-files.test.js b/src/server/common/helpers/serve-static-files.test.js
index 2729d978..38e2a169 100644
--- a/src/server/common/helpers/serve-static-files.test.js
+++ b/src/server/common/helpers/serve-static-files.test.js
@@ -1,54 +1,62 @@
import { vi } from 'vitest'
+import { mockHapiServer, mockHapiResponseToolkit } from '~/src/__mocks__/hapi-mocks.js'
import { statusCodes } from '~/src/server/common/constants/status-codes.js'
-import { startServer } from '~/src/server/common/helpers/start-server.js'
-import Wreck from '@hapi/wreck'
-vi.mock('@hapi/wreck')
-vi.mock('~/src/server/common/helpers/logging/log.js', () => ({}))
+vi.mock('~/src/config/config.js', () => ({
+ config: {
+ get: vi.fn((key) => {
+ switch (key) {
+ case 'staticCacheTimeout':
+ return 86400000
+ case 'assetPath':
+ return '/public'
+ default:
+ return undefined
+ }
+ })
+ }
+}))
+
+const { serveStaticFiles } = await import('./serve-static-files.js')
-describe('#serveStaticFiles', () => {
+describe('serveStaticFiles Plugin', () => {
let server
+ let mockH
- describe('When secure context is disabled', () => {
- beforeEach(async () => {
- // Mock the well-known OIDC config before server starts
- vi.mocked(Wreck.get).mockResolvedValue({
- payload: {
- authorization_endpoint: 'https://mock-auth/authorize',
- token_endpoint: 'https://mock-auth/token'
- }
- })
-
- server = await startServer()
- })
+ beforeEach(() => {
+ vi.clearAllMocks()
+ server = mockHapiServer()
+ mockH = mockHapiResponseToolkit()
+ })
- afterEach(async () => {
- if (server && typeof server.stop === 'function') {
- await server.stop({ timeout: 0 })
- }
- })
+ describe('/favicon.ico route handler', () => {
+ test('should return 204 No Content with image/x-icon type', () => {
+ serveStaticFiles.plugin.register(server)
- test('Should serve favicon as expected', async () => {
- expect(server).toBeDefined()
- expect(typeof server.inject).toBe('function')
+ const routes = server.route.mock.calls[0][0]
+ const faviconRoute = routes.find((r) => r.path === '/favicon.ico')
- const { statusCode } = await server.inject({
- method: 'GET',
- url: '/favicon.ico'
- })
+ faviconRoute.handler({}, mockH)
- expect(statusCode).toBe(statusCodes.ok)
+ expect(mockH.response).toHaveBeenCalledWith()
+ expect(mockH.code).toHaveBeenCalledWith(statusCodes.noContent)
+ expect(mockH.type).toHaveBeenCalledWith('image/x-icon')
})
+ })
+
+ describe('/img/{param*} route handler', () => {
+ test('should return empty SVG with 200 OK', () => {
+ serveStaticFiles.plugin.register(server)
+
+ const routes = server.route.mock.calls[0][0]
+ const imgRoute = routes.find((r) => r.path === '/img/{param*}')
- test('Should serve assets as expected', async () => {
- // Note npm run build is ran in the postinstall hook in package.json to make sure there is always a file
- // available for this test. Remove as you see fit
- const { statusCode } = await server.inject({
- method: 'GET',
- url: '/public/assets/images/govuk-crest.svg'
- })
+ imgRoute.handler({}, mockH)
- expect(statusCode).toBe(statusCodes.ok)
+ const expectedSvg = ''
+ expect(mockH.response).toHaveBeenCalledWith(expectedSvg)
+ expect(mockH.code).toHaveBeenCalledWith(statusCodes.ok)
+ expect(mockH.type).toHaveBeenCalledWith('image/svg+xml')
})
})
})
diff --git a/src/server/declaration/declaration.controller.test.js b/src/server/declaration/declaration.controller.test.js
index 905e1603..184b3793 100644
--- a/src/server/declaration/declaration.controller.test.js
+++ b/src/server/declaration/declaration.controller.test.js
@@ -16,6 +16,10 @@ vi.mock('../common/helpers/logging/log.js', () => ({
LogCodes: {
SUBMISSION: {
SUBMISSION_FAILURE: { level: 'error', messageFunc: vi.fn() }
+ },
+ SYSTEM: {
+ EXTERNAL_API_CALL_DEBUG: { level: 'debug', messageFunc: vi.fn() },
+ EXTERNAL_API_ERROR: { level: 'error', messageFunc: vi.fn() }
}
}
}))