From 28d3938516f663e13d030fbe4470bd74a1d2dd99 Mon Sep 17 00:00:00 2001
From: aaroncarroll82 <20041831+aaroncarroll82@users.noreply.github.com>
Date: Fri, 24 Apr 2026 15:24:41 +0100
Subject: [PATCH 01/10] TGC-1259: Add hoisting for API based config form, to
match direct file based config form.
---
compose.yml | 8 +++----
.../common/forms/services/api-form-service.js | 2 ++
.../forms/services/api-form-service.test.js | 21 +++++++++++++++++++
3 files changed, 27 insertions(+), 4 deletions(-)
diff --git a/compose.yml b/compose.yml
index f9642623c..2117ab938 100644
--- a/compose.yml
+++ b/compose.yml
@@ -72,7 +72,7 @@ services:
CONFIG_API_URL: '${CONFIG_API_URL:-http://grants-ui-config-api:3011}'
CONFIG_API_JWT_SECRET: ${CONFIG_API_JWT_SECRET:-forms-config-jwt-secret}
CONFIG_API_JWT_EXPIRY: ${CONFIG_API_JWT_EXPIRY:-1h}
- FORMS_API_SLUGS: ${FORMS_API_SLUGS:-}
+ FORMS_API_SLUGS: ${FORMS_API_SLUGS:-example-grant-with-auth}
FORMS_API_CACHE_TTL_SECONDS: ${FORMS_API_CACHE_TTL_SECONDS:-300}
SFD_UPDATE_URL: '${SFD_UPDATE_URL:-http://localhost:3000/sfd/mock}'
SFD_UPDATE_ENABLED: ${SFD_UPDATE_ENABLED:-false}
@@ -137,9 +137,9 @@ services:
MONGO_URI: mongodb://mongodb:27017/
LOCALSTACK_ENDPOINT: http://localstack:4566
JWT_SECRET: ${CONFIG_API_JWT_SECRET:-forms-config-jwt-secret}
- CONFIG_BROKER_URL: '${CONFIG_BROKER_URL:-http://grants-config-broker:3012}'
- CONFIG_BROKER_AUTH_TOKEN: '${CONFIG_BROKER_AUTH_TOKEN:-config-broker-auth-token}'
- CONFIG_BROKER_ENCRYPTION_KEY: '${CONFIG_BROKER_ENCRYPTION_KEY:-config-broker-encryption-key}'
+ GRANTS_CONFIG_BROKER_URL: '${CONFIG_BROKER_URL:-http://grants-config-broker:3012}'
+ GRANTS_CONFIG_BROKER_AUTH_TOKEN: '${CONFIG_BROKER_AUTH_TOKEN:-config-broker-auth-token}'
+ GRANTS_CONFIG_BROKER_ENCRYPTION_KEY: '${CONFIG_BROKER_ENCRYPTION_KEY:-config-broker-encryption-key}'
FORMS_API_SLUGS: ${FORMS_API_SLUGS:-example-grant-with-auth}
DEFAULT_FORM_ORGANISATION: ${DEFAULT_FORM_ORGANISATION:-Defra}
DEFAULT_FORM_TEAM_NAME: ${DEFAULT_FORM_TEAM_NAME:-Digital Delivery}
diff --git a/src/server/common/forms/services/api-form-service.js b/src/server/common/forms/services/api-form-service.js
index 951004ded..916268d12 100644
--- a/src/server/common/forms/services/api-form-service.js
+++ b/src/server/common/forms/services/api-form-service.js
@@ -1,6 +1,7 @@
import jwt from 'jsonwebtoken'
import { logger } from '~/src/server/common/helpers/logging/log.js'
import { getFormDef, setFormDef, setFormMeta, setSlugReverse } from './forms-redis.js'
+import { hoistPageConfig } from '~/src/server/common/forms/services/form.js'
export class ApiFormService {
/**
@@ -122,6 +123,7 @@ export class ApiFormService {
// Apply URL substitutions
const definition = configureDefinition(rawDefinition)
+ hoistPageConfig(definition)
// Copy metadata from the definition into the cache entry
entry.metadata = definition.metadata
diff --git a/src/server/common/forms/services/api-form-service.test.js b/src/server/common/forms/services/api-form-service.test.js
index 35dc2fe6c..bb2dad37b 100644
--- a/src/server/common/forms/services/api-form-service.test.js
+++ b/src/server/common/forms/services/api-form-service.test.js
@@ -145,6 +145,27 @@ describe('ApiFormService', () => {
expect(setSlugReverse).toHaveBeenCalledWith({}, 'api-id', 'my-form')
})
+ test('hoists config section if present on a page', async () => {
+ const entry = { id: 'api-id', slug: 'my-form', title: 'My Form', metadata: {}, source: 'api' }
+ const definition = { name: 'my-form', metadata: { grantRedirectRules: {} }, pages: [{ path: 'mypage', config: { item: 'value' } }] }
+ vi.spyOn(service, 'fetchFormMetadata').mockResolvedValue(entry)
+ vi.spyOn(service, 'fetchFormDefinition').mockResolvedValue(definition)
+ const configure = vi.fn((d) => d)
+ const validateWhitelist = vi.fn()
+ const validateRedirect = vi.fn()
+ const validateDetailsPage = vi.fn()
+
+ await service.loadAll({}, ['my-form'], {}, configure, validateWhitelist, validateRedirect, validateDetailsPage)
+
+ expect(configure).toHaveBeenCalledWith(definition)
+ expect(validateWhitelist).toHaveBeenCalledWith({ title: 'My Form' }, definition)
+ expect(validateRedirect).toHaveBeenCalledWith({ title: 'My Form' }, definition)
+ expect(validateDetailsPage).toHaveBeenCalledWith({ title: 'My Form' }, definition)
+ expect(setFormMeta).toHaveBeenCalledWith({}, 'my-form', { ...entry, metadata: { grantRedirectRules: {}, pageConfig: { mypage: { item: 'value'}} } })
+ expect(setFormDef).toHaveBeenCalledWith({}, 'my-form', { ...definition, pages: [{ path: 'mypage' }]}, 300)
+ expect(setSlugReverse).toHaveBeenCalledWith({}, 'api-id', 'my-form')
+ })
+
test('merges shared redirect rules into definition metadata', async () => {
const entry = { id: 'api-id', slug: 'my-form', title: 'My Form', metadata: {}, source: 'api' }
const definition = {
From 04e9faf02055692ebfea498fcdc3bfc4545e7f5e Mon Sep 17 00:00:00 2001
From: aaroncarroll82 <20041831+aaroncarroll82@users.noreply.github.com>
Date: Fri, 24 Apr 2026 15:28:08 +0100
Subject: [PATCH 02/10] prettier got me...
---
.../common/forms/services/api-form-service.test.js | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/src/server/common/forms/services/api-form-service.test.js b/src/server/common/forms/services/api-form-service.test.js
index bb2dad37b..ec7d3f2ff 100644
--- a/src/server/common/forms/services/api-form-service.test.js
+++ b/src/server/common/forms/services/api-form-service.test.js
@@ -147,7 +147,11 @@ describe('ApiFormService', () => {
test('hoists config section if present on a page', async () => {
const entry = { id: 'api-id', slug: 'my-form', title: 'My Form', metadata: {}, source: 'api' }
- const definition = { name: 'my-form', metadata: { grantRedirectRules: {} }, pages: [{ path: 'mypage', config: { item: 'value' } }] }
+ const definition = {
+ name: 'my-form',
+ metadata: { grantRedirectRules: {} },
+ pages: [{ path: 'mypage', config: { item: 'value' } }]
+ }
vi.spyOn(service, 'fetchFormMetadata').mockResolvedValue(entry)
vi.spyOn(service, 'fetchFormDefinition').mockResolvedValue(definition)
const configure = vi.fn((d) => d)
@@ -161,8 +165,11 @@ describe('ApiFormService', () => {
expect(validateWhitelist).toHaveBeenCalledWith({ title: 'My Form' }, definition)
expect(validateRedirect).toHaveBeenCalledWith({ title: 'My Form' }, definition)
expect(validateDetailsPage).toHaveBeenCalledWith({ title: 'My Form' }, definition)
- expect(setFormMeta).toHaveBeenCalledWith({}, 'my-form', { ...entry, metadata: { grantRedirectRules: {}, pageConfig: { mypage: { item: 'value'}} } })
- expect(setFormDef).toHaveBeenCalledWith({}, 'my-form', { ...definition, pages: [{ path: 'mypage' }]}, 300)
+ expect(setFormMeta).toHaveBeenCalledWith({}, 'my-form', {
+ ...entry,
+ metadata: { grantRedirectRules: {}, pageConfig: { mypage: { item: 'value' } } }
+ })
+ expect(setFormDef).toHaveBeenCalledWith({}, 'my-form', { ...definition, pages: [{ path: 'mypage' }] }, 300)
expect(setSlugReverse).toHaveBeenCalledWith({}, 'api-id', 'my-form')
})
From 93677500d3b807d969855fb5a939cb9c9f7bf6cb Mon Sep 17 00:00:00 2001
From: aaroncarroll82 <20041831+aaroncarroll82@users.noreply.github.com>
Date: Mon, 27 Apr 2026 14:36:51 +0100
Subject: [PATCH 03/10] Update the test yaml used by config broker to match
latest
---
.../example-grant-with-auth.yaml | 154 +++++++++++++++++-
1 file changed, 152 insertions(+), 2 deletions(-)
diff --git a/localstack/config-broker/example-grant-with-auth@1.0.1/example-grant-with-auth.yaml b/localstack/config-broker/example-grant-with-auth@1.0.1/example-grant-with-auth.yaml
index 69992aac4..4cb0364a6 100644
--- a/localstack/config-broker/example-grant-with-auth@1.0.1/example-grant-with-auth.yaml
+++ b/localstack/config-broker/example-grant-with-auth@1.0.1/example-grant-with-auth.yaml
@@ -12,6 +12,93 @@ metadata:
submission:
submissionSchemaPath: ./schemas/example-grant-with-auth-submission.schema.json
grantRedirectRules: null
+ detailsPage:
+ query:
+ name: Business
+ entities:
+ - name: customer
+ variableName: crn
+ variableSource: credentials.crn
+ fields:
+ - path: info
+ fields:
+ - path: name
+ fields:
+ - path: title
+ - path: first
+ - path: middle
+ - path: last
+ - name: business
+ variableName: sbi
+ variableSource: credentials.sbi
+ fields:
+ - path: info
+ fields:
+ - path: reference
+ - path: email
+ fields:
+ - path: address
+ - path: phone
+ fields:
+ - path: mobile
+ - path: landline
+ - path: name
+ - path: address
+ fields:
+ - path: line1
+ - path: line2
+ - path: line3
+ - path: line4
+ - path: line5
+ - path: street
+ - path: city
+ - path: postalCode
+ - path: uprn
+ - path: county
+ - path: buildingName
+ - path: buildingNumberRange
+ - path: dependentLocality
+ - path: doubleDependentLocality
+ - path: flatName
+ - path: pafOrganisationName
+ - path: vat
+ - path: countyParishHoldings
+ fields:
+ - path: cphNumber
+ responseMapping:
+ business: data.business.info
+ countyParishHoldings: data.business.countyParishHoldings[0].cphNumber
+ customer: data.customer.info
+ displaySections:
+ - title: Applicant details
+ fields:
+ - label: Applicant name
+ sourcePath: customer.name
+ format: fullName
+ - title: Organisation details
+ fields:
+ - label: Organisation name
+ sourcePath: business.name
+ - label: Single Business Identifier (SBI) number
+ sourceType: credentials
+ sourcePath: sbi
+ - label: Business reference
+ sourcePath: business.reference
+ - label: Organisation email
+ sourcePath: business.email.address
+ - label: Mobile phone number
+ sourcePath: business.phone.mobile
+ - label: Landline phone number
+ sourcePath: business.phone.landline
+ - label: Organisation address
+ sourcePath: business.address
+ format: address
+ - label: VAT registration number
+ sourcePath: business.vat
+ - title: County parish holding (CPH) numbers
+ fields:
+ - label: CPH number
+ sourcePath: countyParishHoldings
printPage:
showApplicantDetails: true
configurablePrintContent:
@@ -57,7 +144,7 @@ pages:
- name: startInfoOne
title: Html
type: Html
- content:
Config Broker Version 1.0.1 - This service uses DefraID and the Save and Return
+ content:
This service uses DefraID and the Save and Return
feature, and demonstrates the use of the following components and page
types:
id: 434ab3dc-fad9-44c5-8601-b19477a1d77e
@@ -129,7 +216,6 @@ pages:
application
type: YesNoField
options:
- classes: govuk-radios--inline
customValidationMessages:
any.required: Select 'Yes' to continue
shortDescription: Yes or No
@@ -332,6 +418,66 @@ pages:
id: 264fbff1-35f6-49d3-98ae-b7582b1e41b5
schema: {}
id: 88d7b067-68c2-4fe1-9a9a-ea98f0752e03
+ - title: Select all the eligible land parcels for the location of your woodland
+ controller: CommonSelectLandParcelPageController
+ components:
+ - name: landParcels
+ type: CheckboxesField
+ title: Select land parcels
+ config:
+ enableMultipleParcelSelect: true
+ topSection: |
+
+
+ You must include all woodland on your holding in your application. A holding
+ is all the land in the same geographical location that is managed as a unit.
+
+
+
+ For larger holdings, this is all woodland within the same geographical
+ location.
+
+
+
+ Land parcels must be entirely within England and defined as an area of land
+ that:
+
+
+
+ is at least 0.5 hectares
+ has an average width of at least 20 metres
+
+ is a group or line of trees that are, or will reach, at least 5 metres in height
+
+ has a crown cover of more than 20% of the ground area
+
+
+
+ The woodland must be larger than 0.5 hectares in total. The total area for the
+ application can be made up of separate blocks of woodland but the minimum
+ block size must be 0.5ha.
+
+
+
+ To find out more about what land is eligible for a WMP read our
+
+ Applicant's guide: PA3 Woodland management plan 2026 guidance
+ .
+
+
+ supportDetailsSummaryText: Get help with your application
+ supportDetailsHtml: |
+ Phone: 020 8026 2395
+ Monday to Friday, 8:30am to 5pm, except bank holidays
+
+
+ Find out about call charges (opens in new tab)
+
+
+ Email: farmpayments@rpa.gov.uk
+ We aim to respond to emails within 10 working days
+ path: /select-land-parcel
+ id: f9f1a33b-2fb5-4418-951b-680067efc179
- title: Multi Field Form Example
path: /multi-field-form
components:
@@ -406,6 +552,10 @@ pages:
options: {}
schema: {}
id: 089374a5-13e7-494d-994e-9b38c4b7392c
+ - title: Check your details
+ path: /check-details
+ controller: CheckDetailsController
+ id: f7a2c8b1-3d4e-5f6a-7b8c-9d0e1f2a3b4c
- title: Check your answers
path: /summary
controller: CheckResponsesPageController
From bb94674970b3756a92a8f5db296a65b1c8eb63f8 Mon Sep 17 00:00:00 2001
From: aaroncarroll82 <20041831+aaroncarroll82@users.noreply.github.com>
Date: Tue, 28 Apr 2026 21:10:49 +0100
Subject: [PATCH 04/10] Load form def on non-plugin extending controllers.
Expose forms service via server methods
---
.../helpers/form-verify-and-request-load.js | 27 +++++++
.../form-verify-and-request-load.test.js | 40 +++++++++++
.../confirmation/config-confirmation.js | 71 ++++++++-----------
.../confirmation/config-confirmation.test.js | 23 +++---
.../clear-application-state.handler.js | 2 +-
.../clear-application-state.handler.test.js | 8 ++-
.../demo-print-application.handler.js | 2 +-
.../demo-print-application.handler.test.js | 10 ++-
src/server/index.js | 4 +-
.../print-submitted-application.controller.js | 36 +++-------
...t-submitted-application.controller.test.js | 30 ++++----
11 files changed, 148 insertions(+), 105 deletions(-)
create mode 100644 src/server/common/helpers/form-verify-and-request-load.js
create mode 100644 src/server/common/helpers/form-verify-and-request-load.test.js
diff --git a/src/server/common/helpers/form-verify-and-request-load.js b/src/server/common/helpers/form-verify-and-request-load.js
new file mode 100644
index 000000000..eeb5f70fb
--- /dev/null
+++ b/src/server/common/helpers/form-verify-and-request-load.js
@@ -0,0 +1,27 @@
+import { statusCodes } from '~/src/server/common/constants/status-codes.js'
+import { findFormBySlug } from '~/src/server/common/forms/services/find-form-by-slug.js'
+
+export async function validateRequestAndFindForm(request, h) {
+ const { slug } = request.params
+
+ if (!slug) {
+ return { error: h.response('Bad request - missing slug').code(statusCodes.badRequest) }
+ }
+
+ const form = await findFormBySlug(slug)
+ if (!form) {
+ return { error: h.response('Form not found').code(statusCodes.notFound) }
+ }
+
+ // For pages that do not extend the DXT controllers, this model will not be set, but it is required for retrieving the
+ // correct state from backend when using form definitions loaded via config API (as version is required)
+ if (!request.app?.model) {
+ request.app.model = {
+ def: {
+ metadata: form.metadata
+ }
+ }
+ }
+
+ return { form, slug }
+}
diff --git a/src/server/common/helpers/form-verify-and-request-load.test.js b/src/server/common/helpers/form-verify-and-request-load.test.js
new file mode 100644
index 000000000..eb37bbb5a
--- /dev/null
+++ b/src/server/common/helpers/form-verify-and-request-load.test.js
@@ -0,0 +1,40 @@
+import { mockHapiRequest, mockHapiResponseToolkit } from '~/src/__mocks__/index.js'
+import { validateRequestAndFindForm } from './form-verify-and-request-load.js'
+import { vi } from 'vitest'
+import { findFormBySlug } from '../forms/services/find-form-by-slug.js'
+import { MOCK_FORM_WITH_PATH } from '~/src/__test-fixtures__/mock-forms-cache.js'
+
+vi.mock('../forms/services/find-form-by-slug.js')
+
+const mockForm = MOCK_FORM_WITH_PATH
+
+describe('form-verify-and-request-load', () => {
+ it('should return 400 response error when slug is missing', async () => {
+ const mockH = mockHapiResponseToolkit()
+ const result = await validateRequestAndFindForm(mockHapiRequest({ params: {} }), mockH)
+ expect(result.error).toBe(mockH)
+ expect(mockH.response).toHaveBeenCalledWith('Bad request - missing slug')
+ expect(mockH.code).toHaveBeenCalledWith(400)
+ })
+
+ it('should return 404 response error when form not found', async () => {
+ findFormBySlug.mockResolvedValue(undefined)
+ const mockH = mockHapiResponseToolkit()
+ const result = await validateRequestAndFindForm(mockHapiRequest({ params: { slug: 'test-slug' } }), mockH)
+ expect(result.error).toBe(mockH)
+ expect(mockH.response).toHaveBeenCalledWith('Form not found')
+ expect(mockH.code).toHaveBeenCalledWith(404)
+ })
+
+ it('should return form and slug and set request app model when inputs valid', async () => {
+ const formWithMetaData = { ...mockForm, metadata: { some: 'metadata' } }
+ findFormBySlug.mockResolvedValue(formWithMetaData)
+ const mockH = mockHapiResponseToolkit()
+ const requestProps = { params: { slug: 'test-slug' }, app: {} }
+ const result = await validateRequestAndFindForm(mockHapiRequest(requestProps), mockH)
+ expect(result.error).toBeUndefined()
+ expect(result.form).toBe(formWithMetaData)
+ expect(result.slug).toBe('test-slug')
+ expect(requestProps.app.model.def.metadata).to.eql({ some: 'metadata' })
+ })
+})
diff --git a/src/server/confirmation/config-confirmation.js b/src/server/confirmation/config-confirmation.js
index d3c594e91..5fa9bc731 100644
--- a/src/server/confirmation/config-confirmation.js
+++ b/src/server/confirmation/config-confirmation.js
@@ -1,47 +1,9 @@
-import { findFormBySlug } from '~/src/server/common/forms/services/find-form-by-slug.js'
import { ConfirmationService } from './services/confirmation.service.js'
import { getFormsCacheService } from '~/src/server/common/helpers/forms-cache/forms-cache.js'
import { log, LogCodes } from '~/src/server/common/helpers/logging/log.js'
import { ApplicationStatus } from '~/src/server/common/constants/application-status.js'
import { statusCodes } from '~/src/server/common/constants/status-codes.js'
-
-/**
- * Validates request parameters and finds form by slug
- * @param {object} request - Hapi request object
- * @param {object} h - Hapi response toolkit
- * @returns {Promise} Validation result with form or error response
- */
-async function validateRequestAndFindForm(request, h) {
- const { slug } = request.params
-
- if (!slug) {
- log(
- LogCodes.CONFIRMATION.CONFIRMATION_ERROR,
- {
- userId: request.auth?.credentials?.userId || 'unknown',
- errorMessage: 'No slug provided in confirmation route'
- },
- request
- )
- return { error: h.response('Bad request - missing slug').code(statusCodes.badRequest) }
- }
-
- const form = await findFormBySlug(slug)
- if (!form) {
- log(
- LogCodes.CONFIRMATION.CONFIRMATION_ERROR,
- {
- userId: request.auth?.credentials?.userId || 'unknown',
- errorMessage: `Form not found for slug: ${slug}`
- },
- request
- )
- // @todo - should maybe use the `error/404` view here?
- return { error: h.response('Form not found').code(statusCodes.notFound) }
- }
-
- return { form, slug }
-}
+import { validateRequestAndFindForm } from '~/src/server/common/helpers/form-verify-and-request-load.js'
/**
* Loads and validates confirmation content for the form
@@ -135,14 +97,37 @@ export const configConfirmation = {
server.route({
method: 'GET',
path: '/{slug}/confirmation',
+ options: {
+ pre: [{ method: validateRequestAndFindForm, assign: 'validatedSlugAndForm' }]
+ },
handler: async (request, h) => {
try {
- const validationResult = await validateRequestAndFindForm(request, h)
- if (validationResult.error) {
- return validationResult.error
+ const { validationResult: validationError, form, slug } = request.pre.validatedSlugAndForm
+ if (!slug) {
+ log(
+ LogCodes.CONFIRMATION.CONFIRMATION_ERROR,
+ {
+ userId: request.auth?.credentials?.userId || 'unknown',
+ errorMessage: 'No slug provided in confirmation route'
+ },
+ request
+ )
}
- const { form, slug } = validationResult
+ if (!form) {
+ log(
+ LogCodes.CONFIRMATION.CONFIRMATION_ERROR,
+ {
+ userId: request.auth?.credentials?.userId || 'unknown',
+ errorMessage: `Form not found for slug: ${slug}`
+ },
+ request
+ )
+ }
+
+ if (validationError) {
+ return validationError
+ }
const sessionData = await getSessionDataAndState(request)
const confirmationContent = await loadConfirmationContent(form, slug, sessionData.state)
diff --git a/src/server/confirmation/config-confirmation.test.js b/src/server/confirmation/config-confirmation.test.js
index 608c91ee5..8fdba1ee0 100644
--- a/src/server/confirmation/config-confirmation.test.js
+++ b/src/server/confirmation/config-confirmation.test.js
@@ -6,6 +6,7 @@ import { mockHapiRequest, mockHapiResponseToolkit } from '~/src/__mocks__/hapi-m
import { mockRequestLogger } from '~/src/__mocks__/logger-mocks.js'
import { MOCK_CONFIRMATION_CONTENT, MOCK_FORMS } from './__test-fixtures__/confirmation-test-fixtures.js'
import { log } from '~/src/server/common/helpers/logging/log.js'
+import { statusCodes } from '~/src/server/common/constants/status-codes.js'
const mockFormsCacheService = {
getState: vi.fn()
@@ -35,6 +36,7 @@ describe('config-confirmation', () => {
mockLogger = mockRequestLogger()
mockRequest = mockHapiRequest({
params: { slug: 'test-slug' },
+ pre: { validatedSlugAndForm: { slug: 'test-slug', form: mockForm } },
logger: mockLogger,
yar: mockYarSession
})
@@ -46,7 +48,7 @@ describe('config-confirmation', () => {
ConfirmationService.hasConfigDrivenConfirmation = vi.fn().mockResolvedValue(true)
const server = { route: vi.fn() }
- await configConfirmation.plugin.register(server)
+ configConfirmation.plugin.register(server)
handler = server.route.mock.calls[0][0].handler
mockFormsCacheService.getState.mockResolvedValue({
@@ -79,7 +81,6 @@ describe('config-confirmation', () => {
await handler(mockRequest, mockH)
- expect(findFormBySlug).toHaveBeenCalledWith('test-slug')
expect(ConfirmationService.loadConfirmationContent).toHaveBeenCalledWith(mockForm)
expect(ConfirmationService.processConfirmationContent).toHaveBeenCalledWith(
mockConfirmationContent,
@@ -100,22 +101,20 @@ describe('config-confirmation', () => {
expect(mockH.view).toHaveBeenCalledWith('config-confirmation-page', { test: 'viewModel' })
})
- test('should return 400 when slug is missing', async () => {
- mockRequest.params = {}
+ test('should return validation error when pre-handler returns it', async () => {
+ mockRequest = mockHapiRequest({
+ params: {},
+ pre: {
+ validatedSlugAndForm: { error: mockH.response('Bad request - missing slug').code(statusCodes.badRequest) }
+ }
+ })
await handler(mockRequest, mockH)
+ expect(mockH.response).toHaveBeenCalledWith('Bad request - missing slug')
expect(mockH.code).toHaveBeenCalledWith(400)
})
- test('should return 404 when form not found', async () => {
- findFormBySlug.mockResolvedValue(null)
-
- await handler(mockRequest, mockH)
-
- expect(mockH.code).toHaveBeenCalledWith(404)
- })
-
test('should render page with default content when no config-driven content available', async () => {
setupHappyPathMocks({ confirmationContent: null })
ConfirmationService.buildViewModel.mockReturnValue({ test: 'viewModel' })
diff --git a/src/server/dev-tools/handlers/clear-application-state.handler.js b/src/server/dev-tools/handlers/clear-application-state.handler.js
index 22a5d0ab8..60086830e 100644
--- a/src/server/dev-tools/handlers/clear-application-state.handler.js
+++ b/src/server/dev-tools/handlers/clear-application-state.handler.js
@@ -16,7 +16,7 @@ export async function clearApplicationStateHandler(request, h) {
if (!request.app.model) {
const form = await findFormBySlug(slug)
if (form) {
- const definition = await loadFormDefinition(form, request.server.app.formsService)
+ const definition = await loadFormDefinition(form, request.server.methods.getFormService())
request.app.model = { def: definition }
}
}
diff --git a/src/server/dev-tools/handlers/clear-application-state.handler.test.js b/src/server/dev-tools/handlers/clear-application-state.handler.test.js
index 1f1de8046..1c777ace4 100644
--- a/src/server/dev-tools/handlers/clear-application-state.handler.test.js
+++ b/src/server/dev-tools/handlers/clear-application-state.handler.test.js
@@ -15,6 +15,8 @@ vi.mock('~/src/server/common/forms/services/find-form-by-slug.js', () => ({
describe('clearApplicationStateHandler', () => {
let mockRequest
+ let mockGetFormService
+ let mockFormService
let mockH
let mockCacheService
@@ -28,9 +30,11 @@ describe('clearApplicationStateHandler', () => {
getFormsCacheService.mockReturnValue(mockCacheService)
+ mockFormService = vi.fn()
+ mockGetFormService = vi.fn().mockReturnValue(mockFormService)
mockRequest = {
params: {},
- server: { app: { formsService: { getFormDefinitionBySlug: vi.fn() } } },
+ server: { methods: { getFormService: mockGetFormService } },
app: { model: {} }
}
@@ -195,7 +199,7 @@ describe('clearApplicationStateHandler', () => {
await clearApplicationStateHandler(mockRequest, mockH)
expect(findFormBySlug).toHaveBeenCalledWith('test-slug')
- expect(loadFormDefinition).toHaveBeenCalledWith(mockForm, mockRequest.server.app.formsService)
+ expect(loadFormDefinition).toHaveBeenCalledWith(mockForm, mockFormService)
expect(mockRequest.app.model).toEqual({ def: mockDefinition })
})
diff --git a/src/server/dev-tools/handlers/demo-print-application.handler.js b/src/server/dev-tools/handlers/demo-print-application.handler.js
index 5ace67d86..991d06a26 100644
--- a/src/server/dev-tools/handlers/demo-print-application.handler.js
+++ b/src/server/dev-tools/handlers/demo-print-application.handler.js
@@ -24,7 +24,7 @@ export async function demoPrintApplicationHandler(request, h) {
return generateFormNotFoundResponse(slug, h)
}
- const definition = await loadFormDefinition(form, request.server.app.formsService)
+ const definition = await loadFormDefinition(form, request.server.methods.getFormService())
enrichDefinitionWithListItems(definition)
diff --git a/src/server/dev-tools/handlers/demo-print-application.handler.test.js b/src/server/dev-tools/handlers/demo-print-application.handler.test.js
index e380aa58f..73e64ec80 100644
--- a/src/server/dev-tools/handlers/demo-print-application.handler.test.js
+++ b/src/server/dev-tools/handlers/demo-print-application.handler.test.js
@@ -26,14 +26,18 @@ const mockForm = MOCK_FORM_WITH_PATH
describe('demo-print-application.handler', () => {
let mockRequest
+ let mockGetFormService
+ let mockFormService
let mockH
beforeEach(() => {
vi.clearAllMocks()
+ mockFormService = vi.fn()
+ mockGetFormService = vi.fn().mockReturnValue(mockFormService)
mockRequest = mockHapiRequest({
params: { slug: 'test-form' },
- server: { app: { formsService: { getFormDefinitionBySlug: vi.fn() } } }
+ server: { methods: { getFormService: mockGetFormService } }
})
mockH = mockHapiResponseToolkit()
@@ -51,7 +55,7 @@ describe('demo-print-application.handler', () => {
await demoPrintApplicationHandler(mockRequest, mockH)
expect(findFormBySlug).toHaveBeenCalledWith('test-form')
- expect(loadFormDefinition).toHaveBeenCalledWith(mockForm, mockRequest.server.app.formsService)
+ expect(loadFormDefinition).toHaveBeenCalledWith(mockForm, mockFormService)
expect(enrichDefinitionWithListItems).toHaveBeenCalledWith(mockDefinition)
expect(buildDemoPrintAnswers).toHaveBeenCalledWith(mockDefinition)
expect(buildPrintViewModel).toHaveBeenCalledWith(
@@ -74,7 +78,7 @@ describe('demo-print-application.handler', () => {
test('should include demo payment data for farm-payments slug', async () => {
mockRequest = mockHapiRequest({
params: { slug: 'farm-payments' },
- server: { app: { formsService: { getFormDefinitionBySlug: vi.fn() } } }
+ server: { methods: { getFormService: mockGetFormService } }
})
findFormBySlug.mockResolvedValue(mockForm)
buildPrintViewModel.mockReturnValue({ test: 'viewModel' })
diff --git a/src/server/index.js b/src/server/index.js
index 89f9124bf..d4c88bfe4 100644
--- a/src/server/index.js
+++ b/src/server/index.js
@@ -102,6 +102,7 @@ const createHapiServer = () => {
* @param {string} prefix
*/
const registerFormsPlugin = async (server, prefix = '') => {
+ const formService = await formsService()
await server.register({
plugin,
options: {
@@ -110,7 +111,7 @@ const registerFormsPlugin = async (server, prefix = '') => {
baseUrl: config.get('baseUrl'),
onRequest: formsStatusCallback,
services: {
- formsService: await formsService(),
+ formsService: formService,
outputService
},
filters: {
@@ -146,6 +147,7 @@ const registerFormsPlugin = async (server, prefix = '') => {
}
}
})
+ server.method('getFormService', () => formService)
}
const registerPlugins = async (server) => {
diff --git a/src/server/print-submitted-application/print-submitted-application.controller.js b/src/server/print-submitted-application/print-submitted-application.controller.js
index e06e7c1e1..8b17f422f 100644
--- a/src/server/print-submitted-application/print-submitted-application.controller.js
+++ b/src/server/print-submitted-application/print-submitted-application.controller.js
@@ -2,33 +2,14 @@ import { getFormsCacheService } from '~/src/server/common/helpers/forms-cache/fo
import { log, LogCodes } from '~/src/server/common/helpers/logging/log.js'
import { ApplicationStatus } from '~/src/server/common/constants/application-status.js'
import { statusCodes } from '~/src/server/common/constants/status-codes.js'
-import { findFormBySlug, loadFormDefinition } from '../common/forms/services/find-form-by-slug.js'
+import { loadFormDefinition } from '../common/forms/services/find-form-by-slug.js'
import {
buildPrintViewModel,
enrichDefinitionWithListItems,
processConfigurablePrintContent
} from '../common/helpers/print-application-service/print-application-service.js'
import { createBusinessRows, createContactRows, createPersonRows } from '~/src/server/common/helpers/create-rows.js'
-
-/**
- * Validates the request has a slug param and finds the matching form definition.
- * @param {Request} request
- * @param {ResponseToolkit} h
- */
-async function validateRequestAndFindForm(request, h) {
- const { slug } = request.params
-
- if (!slug) {
- return { error: h.response('Bad request - missing slug').code(statusCodes.badRequest) }
- }
-
- const form = await findFormBySlug(slug)
- if (!form) {
- return { error: h.response('Form not found').code(statusCodes.notFound) }
- }
-
- return { form, slug }
-}
+import { validateRequestAndFindForm } from '~/src/server/common/helpers/form-verify-and-request-load.js'
/**
* Loads the application state from the session cache and returns it only if submitted.
@@ -78,7 +59,7 @@ function resolveApplicantDetailsSections(request, state, definition) {
* @param {ResponseToolkit} h
*/
async function buildPrintResponse({ form, state, slug }, request, h) {
- const definition = await loadFormDefinition(form, request.server.app.formsService)
+ const definition = await loadFormDefinition(form, request.server.methods.getFormService())
enrichDefinitionWithListItems(definition)
@@ -146,15 +127,16 @@ export const printSubmittedApplication = {
server.route({
method: 'GET',
path: '/{slug}/print-submitted-application',
+ options: {
+ pre: [{ method: validateRequestAndFindForm, assign: 'validatedSlugAndForm' }]
+ },
handler: async (request, h) => {
try {
- const validationResult = await validateRequestAndFindForm(request, h)
- if (validationResult.error) {
- return validationResult.error
+ const { error: validationError, form, slug } = request.pre.validatedSlugAndForm
+ if (validationError) {
+ return validationError
}
- const { form, slug } = validationResult
-
const state = await loadSubmittedApplication(request)
if (!state) {
return h.response('Application not submitted').code(statusCodes.forbidden)
diff --git a/src/server/print-submitted-application/print-submitted-application.controller.test.js b/src/server/print-submitted-application/print-submitted-application.controller.test.js
index a30f98521..b0609d64a 100644
--- a/src/server/print-submitted-application/print-submitted-application.controller.test.js
+++ b/src/server/print-submitted-application/print-submitted-application.controller.test.js
@@ -11,6 +11,7 @@ import { log, LogCodes } from '../common/helpers/logging/log.js'
import { mockHapiRequest, mockHapiResponseToolkit, mockHapiServer } from '~/src/__mocks__/hapi-mocks.js'
import { MOCK_FORM_WITH_PATH, MOCK_SINGLE_PAGE_DEFINITION } from '~/src/__test-fixtures__/mock-forms-cache.js'
import { createPersonRows, createBusinessRows, createContactRows } from '~/src/server/common/helpers/create-rows.js'
+import { statusCodes } from '~/src/server/common/constants/status-codes.js'
vi.mock('../common/forms/services/find-form-by-slug.js')
vi.mock('../common/helpers/print-application-service/print-application-service.js')
@@ -40,6 +41,8 @@ const mockState = {
describe('print-submitted-application.controller', () => {
let handler
let mockRequest
+ let mockGetFormService
+ let mockFormService
let mockH
let mockGetState
@@ -51,12 +54,15 @@ describe('print-submitted-application.controller', () => {
handler = server.route.mock.calls[0][0].handler
mockGetState = vi.fn().mockResolvedValue(mockState)
+ mockFormService = vi.fn()
+ mockGetFormService = vi.fn().mockReturnValue(mockFormService)
getFormsCacheService.mockReturnValue({ getState: mockGetState })
mockRequest = mockHapiRequest({
params: { slug: 'test-form' },
auth: { credentials: { sbi: '123456789' } },
- server: { app: { formsService: { getFormDefinitionBySlug: vi.fn() } } }
+ pre: { validatedSlugAndForm: { slug: 'test-form', form: mockForm } },
+ server: { methods: { getFormService: mockGetFormService } }
})
mockH = mockHapiResponseToolkit()
@@ -77,8 +83,13 @@ describe('print-submitted-application.controller', () => {
)
})
- test('should return 400 when slug is missing', async () => {
- mockRequest = mockHapiRequest({ params: {} })
+ test('should return validation error when pre-handler returns it', async () => {
+ mockRequest = mockHapiRequest({
+ params: {},
+ pre: {
+ validatedSlugAndForm: { error: mockH.response('Bad request - missing slug').code(statusCodes.badRequest) }
+ }
+ })
await handler(mockRequest, mockH)
@@ -86,16 +97,6 @@ describe('print-submitted-application.controller', () => {
expect(mockH.code).toHaveBeenCalledWith(400)
})
- test('should return 404 when form is not found', async () => {
- findFormBySlug.mockResolvedValue(null)
-
- await handler(mockRequest, mockH)
-
- expect(findFormBySlug).toHaveBeenCalledWith('test-form')
- expect(mockH.response).toHaveBeenCalledWith('Form not found')
- expect(mockH.code).toHaveBeenCalledWith(404)
- })
-
test.each([
['application is not submitted', { applicationStatus: 'DRAFT' }],
['state is null', null]
@@ -111,9 +112,8 @@ describe('print-submitted-application.controller', () => {
test('should render print view for a submitted application', async () => {
await handler(mockRequest, mockH)
- expect(findFormBySlug).toHaveBeenCalledWith('test-form')
expect(mockGetState).toHaveBeenCalledWith(mockRequest)
- expect(loadFormDefinition).toHaveBeenCalledWith(mockForm, mockRequest.server.app.formsService)
+ expect(loadFormDefinition).toHaveBeenCalledWith(mockForm, mockFormService)
expect(buildPrintViewModel).toHaveBeenCalledWith({
definition: mockDefinition,
form: mockForm,
From d25bb9bfe2e08404f5fdcc20326628c1ee2b4657 Mon Sep 17 00:00:00 2001
From: aaroncarroll82 <20041831+aaroncarroll82@users.noreply.github.com>
Date: Tue, 28 Apr 2026 21:44:24 +0100
Subject: [PATCH 05/10] lint check on types not happy
---
.../handlers/clear-application-state.handler.js | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/src/server/dev-tools/handlers/clear-application-state.handler.js b/src/server/dev-tools/handlers/clear-application-state.handler.js
index fc0a7b52c..3ed6016c5 100644
--- a/src/server/dev-tools/handlers/clear-application-state.handler.js
+++ b/src/server/dev-tools/handlers/clear-application-state.handler.js
@@ -6,7 +6,7 @@ import { clearParcelCache } from '~/src/server/land-grants/services/parcel-cache
/**
* @typedef {import('@hapi/hapi').Request & {
* app: { model?: { def?: object } },
- * server: import('@hapi/hapi').Request['server'] & { app: { formsService?: object } }
+ * server: import('@hapi/hapi').Request['server'] & { methods: { getFormService?: () => any } }
* }} ClearStateRequest
*/
@@ -24,7 +24,13 @@ export async function clearApplicationStateHandler(request, h) {
if (!request.app.model) {
const form = await findFormBySlug(slug)
if (form) {
- const definition = await loadFormDefinition(form, request.server.methods.getFormService())
+ const getFormService = request.server.methods.getFormService
+
+ if (typeof getFormService !== 'function') {
+ throw new Error('getFormService is not available')
+ }
+
+ const definition = await loadFormDefinition(form, getFormService())
request.app.model = { def: definition }
}
}
From 383d5bbc0b9c02779cb5b71e04eaf9ff1eb0520f Mon Sep 17 00:00:00 2001
From: aaroncarroll82 <20041831+aaroncarroll82@users.noreply.github.com>
Date: Tue, 28 Apr 2026 22:02:48 +0100
Subject: [PATCH 06/10] make sonar happy after changes to make linter happy
---
.../clear-application-state.handler.js | 20 +++++++++++--------
.../clear-application-state.handler.test.js | 14 +++++++++++++
2 files changed, 26 insertions(+), 8 deletions(-)
diff --git a/src/server/dev-tools/handlers/clear-application-state.handler.js b/src/server/dev-tools/handlers/clear-application-state.handler.js
index 3ed6016c5..90e0e1cfc 100644
--- a/src/server/dev-tools/handlers/clear-application-state.handler.js
+++ b/src/server/dev-tools/handlers/clear-application-state.handler.js
@@ -24,14 +24,7 @@ export async function clearApplicationStateHandler(request, h) {
if (!request.app.model) {
const form = await findFormBySlug(slug)
if (form) {
- const getFormService = request.server.methods.getFormService
-
- if (typeof getFormService !== 'function') {
- throw new Error('getFormService is not available')
- }
-
- const definition = await loadFormDefinition(form, getFormService())
- request.app.model = { def: definition }
+ await loadFormAndSetOnRequestModel(form, request)
}
}
@@ -56,3 +49,14 @@ export async function clearApplicationStateHandler(request, h) {
return h.redirect(`/${slug}`)
}
+
+const loadFormAndSetOnRequestModel = async (form, request) => {
+ const getFormService = request.server.methods.getFormService
+
+ if (typeof getFormService !== 'function') {
+ throw new Error('getFormService is not available')
+ }
+
+ const definition = await loadFormDefinition(form, getFormService())
+ request.app.model = { def: definition }
+}
diff --git a/src/server/dev-tools/handlers/clear-application-state.handler.test.js b/src/server/dev-tools/handlers/clear-application-state.handler.test.js
index 1c777ace4..bf9e2906e 100644
--- a/src/server/dev-tools/handlers/clear-application-state.handler.test.js
+++ b/src/server/dev-tools/handlers/clear-application-state.handler.test.js
@@ -203,6 +203,20 @@ describe('clearApplicationStateHandler', () => {
expect(mockRequest.app.model).toEqual({ def: mockDefinition })
})
+ it('should throw an error if getFormService is not available', async () => {
+ const mockForm = { id: 'form-1' }
+ const mockDefinition = { pages: [] }
+ findFormBySlug.mockResolvedValue(mockForm)
+ loadFormDefinition.mockResolvedValue(mockDefinition)
+ mockRequest = {
+ params: { slug: 'test-slug' },
+ server: { methods: { getFormService: {} } },
+ app: { }
+ }
+
+ await expect(clearApplicationStateHandler(mockRequest, mockH)).rejects.toThrow('getFormService is not available')
+ })
+
it('should not call loadFormDefinition when findFormBySlug returns null', async () => {
findFormBySlug.mockResolvedValue(null)
From 0aab160cec4351a2de763aa49fb64cd8fd1d73f0 Mon Sep 17 00:00:00 2001
From: aaroncarroll82 <20041831+aaroncarroll82@users.noreply.github.com>
Date: Tue, 28 Apr 2026 22:28:48 +0100
Subject: [PATCH 07/10] prettier
---
.../dev-tools/handlers/clear-application-state.handler.test.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/server/dev-tools/handlers/clear-application-state.handler.test.js b/src/server/dev-tools/handlers/clear-application-state.handler.test.js
index bf9e2906e..ddfef816b 100644
--- a/src/server/dev-tools/handlers/clear-application-state.handler.test.js
+++ b/src/server/dev-tools/handlers/clear-application-state.handler.test.js
@@ -211,7 +211,7 @@ describe('clearApplicationStateHandler', () => {
mockRequest = {
params: { slug: 'test-slug' },
server: { methods: { getFormService: {} } },
- app: { }
+ app: {}
}
await expect(clearApplicationStateHandler(mockRequest, mockH)).rejects.toThrow('getFormService is not available')
From 907e4c7d709aa554e9dd6ad26fa72e82846bfdf9 Mon Sep 17 00:00:00 2001
From: aaroncarroll82 <20041831+aaroncarroll82@users.noreply.github.com>
Date: Wed, 29 Apr 2026 07:57:48 +0100
Subject: [PATCH 08/10] sonar again
---
.../dev-tools/handlers/clear-application-state.handler.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/server/dev-tools/handlers/clear-application-state.handler.js b/src/server/dev-tools/handlers/clear-application-state.handler.js
index 90e0e1cfc..4248b08de 100644
--- a/src/server/dev-tools/handlers/clear-application-state.handler.js
+++ b/src/server/dev-tools/handlers/clear-application-state.handler.js
@@ -54,7 +54,7 @@ const loadFormAndSetOnRequestModel = async (form, request) => {
const getFormService = request.server.methods.getFormService
if (typeof getFormService !== 'function') {
- throw new Error('getFormService is not available')
+ throw new TypeError('getFormService is not available')
}
const definition = await loadFormDefinition(form, getFormService())
From 1fc039c7f3ef46fac803770fc6be17cc6952bd79 Mon Sep 17 00:00:00 2001
From: aaroncarroll82 <20041831+aaroncarroll82@users.noreply.github.com>
Date: Wed, 29 Apr 2026 09:40:54 +0100
Subject: [PATCH 09/10] Address some review comments
---
.../common/helpers/form-verify-and-request-load.js | 4 ++--
.../helpers/form-verify-and-request-load.test.js | 2 +-
src/server/confirmation/config-confirmation.js | 2 +-
src/server/confirmation/config-confirmation.test.js | 10 +++++-----
4 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/server/common/helpers/form-verify-and-request-load.js b/src/server/common/helpers/form-verify-and-request-load.js
index eeb5f70fb..4dac06ba6 100644
--- a/src/server/common/helpers/form-verify-and-request-load.js
+++ b/src/server/common/helpers/form-verify-and-request-load.js
@@ -5,12 +5,12 @@ export async function validateRequestAndFindForm(request, h) {
const { slug } = request.params
if (!slug) {
- return { error: h.response('Bad request - missing slug').code(statusCodes.badRequest) }
+ return { error: h.response('Bad request - missing slug').code(statusCodes.badRequest).takeover() }
}
const form = await findFormBySlug(slug)
if (!form) {
- return { error: h.response('Form not found').code(statusCodes.notFound) }
+ return { error: h.response('Form not found').code(statusCodes.notFound).takeover() }
}
// For pages that do not extend the DXT controllers, this model will not be set, but it is required for retrieving the
diff --git a/src/server/common/helpers/form-verify-and-request-load.test.js b/src/server/common/helpers/form-verify-and-request-load.test.js
index eb37bbb5a..9f4da15d9 100644
--- a/src/server/common/helpers/form-verify-and-request-load.test.js
+++ b/src/server/common/helpers/form-verify-and-request-load.test.js
@@ -35,6 +35,6 @@ describe('form-verify-and-request-load', () => {
expect(result.error).toBeUndefined()
expect(result.form).toBe(formWithMetaData)
expect(result.slug).toBe('test-slug')
- expect(requestProps.app.model.def.metadata).to.eql({ some: 'metadata' })
+ expect(requestProps.app.model.def.metadata).toEqual({ some: 'metadata' })
})
})
diff --git a/src/server/confirmation/config-confirmation.js b/src/server/confirmation/config-confirmation.js
index 5fa9bc731..38d41a093 100644
--- a/src/server/confirmation/config-confirmation.js
+++ b/src/server/confirmation/config-confirmation.js
@@ -102,7 +102,7 @@ export const configConfirmation = {
},
handler: async (request, h) => {
try {
- const { validationResult: validationError, form, slug } = request.pre.validatedSlugAndForm
+ const { error: validationError, form, slug } = request.pre.validatedSlugAndForm
if (!slug) {
log(
LogCodes.CONFIRMATION.CONFIRMATION_ERROR,
diff --git a/src/server/confirmation/config-confirmation.test.js b/src/server/confirmation/config-confirmation.test.js
index 8fdba1ee0..dec86d25a 100644
--- a/src/server/confirmation/config-confirmation.test.js
+++ b/src/server/confirmation/config-confirmation.test.js
@@ -101,18 +101,18 @@ describe('config-confirmation', () => {
expect(mockH.view).toHaveBeenCalledWith('config-confirmation-page', { test: 'viewModel' })
})
- test('should return validation error when pre-handler returns it', async () => {
+ test('should return validation error from handler when pre-handler returns an error', async () => {
+ const preHandlerError = mockH.response('Bad request - missing slug').code(statusCodes.badRequest).takeover()
mockRequest = mockHapiRequest({
params: {},
pre: {
- validatedSlugAndForm: { error: mockH.response('Bad request - missing slug').code(statusCodes.badRequest) }
+ validatedSlugAndForm: { error: preHandlerError }
}
})
- await handler(mockRequest, mockH)
+ const result = await handler(mockRequest, mockH)
- expect(mockH.response).toHaveBeenCalledWith('Bad request - missing slug')
- expect(mockH.code).toHaveBeenCalledWith(400)
+ expect(result).toEqual(preHandlerError)
})
test('should render page with default content when no config-driven content available', async () => {
From 34f8f79969cb5729debd5cb6d875a03dbdac47bb Mon Sep 17 00:00:00 2001
From: aaroncarroll82 <20041831+aaroncarroll82@users.noreply.github.com>
Date: Wed, 29 Apr 2026 10:29:38 +0100
Subject: [PATCH 10/10] Add type definition for getFormService
---
.../handlers/clear-application-state.handler.js | 8 +-------
.../clear-application-state.handler.test.js | 14 --------------
types/hapi-augmentations.d.ts | 7 +++++++
3 files changed, 8 insertions(+), 21 deletions(-)
create mode 100644 types/hapi-augmentations.d.ts
diff --git a/src/server/dev-tools/handlers/clear-application-state.handler.js b/src/server/dev-tools/handlers/clear-application-state.handler.js
index 4248b08de..216ac941e 100644
--- a/src/server/dev-tools/handlers/clear-application-state.handler.js
+++ b/src/server/dev-tools/handlers/clear-application-state.handler.js
@@ -51,12 +51,6 @@ export async function clearApplicationStateHandler(request, h) {
}
const loadFormAndSetOnRequestModel = async (form, request) => {
- const getFormService = request.server.methods.getFormService
-
- if (typeof getFormService !== 'function') {
- throw new TypeError('getFormService is not available')
- }
-
- const definition = await loadFormDefinition(form, getFormService())
+ const definition = await loadFormDefinition(form, request.server.methods.getFormService())
request.app.model = { def: definition }
}
diff --git a/src/server/dev-tools/handlers/clear-application-state.handler.test.js b/src/server/dev-tools/handlers/clear-application-state.handler.test.js
index ddfef816b..1c777ace4 100644
--- a/src/server/dev-tools/handlers/clear-application-state.handler.test.js
+++ b/src/server/dev-tools/handlers/clear-application-state.handler.test.js
@@ -203,20 +203,6 @@ describe('clearApplicationStateHandler', () => {
expect(mockRequest.app.model).toEqual({ def: mockDefinition })
})
- it('should throw an error if getFormService is not available', async () => {
- const mockForm = { id: 'form-1' }
- const mockDefinition = { pages: [] }
- findFormBySlug.mockResolvedValue(mockForm)
- loadFormDefinition.mockResolvedValue(mockDefinition)
- mockRequest = {
- params: { slug: 'test-slug' },
- server: { methods: { getFormService: {} } },
- app: {}
- }
-
- await expect(clearApplicationStateHandler(mockRequest, mockH)).rejects.toThrow('getFormService is not available')
- })
-
it('should not call loadFormDefinition when findFormBySlug returns null', async () => {
findFormBySlug.mockResolvedValue(null)
diff --git a/types/hapi-augmentations.d.ts b/types/hapi-augmentations.d.ts
new file mode 100644
index 000000000..0c4b238d6
--- /dev/null
+++ b/types/hapi-augmentations.d.ts
@@ -0,0 +1,7 @@
+import '@hapi/hapi'
+
+declare module '@hapi/hapi' {
+ interface ServerMethods {
+ getFormService: () => object
+ }
+}