diff --git a/compose.yml b/compose.yml index 5e3dea391..a2989bfdb 100644 --- a/compose.yml +++ b/compose.yml @@ -74,7 +74,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} @@ -139,9 +139,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/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: +

+ + + +

+ 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 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..ec7d3f2ff 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,34 @@ 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 = { 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..4dac06ba6 --- /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).takeover() } + } + + const form = await findFormBySlug(slug) + if (!form) { + 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 + // 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..9f4da15d9 --- /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).toEqual({ some: 'metadata' }) + }) +}) diff --git a/src/server/confirmation/config-confirmation.js b/src/server/confirmation/config-confirmation.js index d3c594e91..38d41a093 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 { error: 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..dec86d25a 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,20 +101,18 @@ describe('config-confirmation', () => { expect(mockH.view).toHaveBeenCalledWith('config-confirmation-page', { test: 'viewModel' }) }) - test('should return 400 when slug is missing', async () => { - mockRequest.params = {} - - await handler(mockRequest, mockH) - - expect(mockH.code).toHaveBeenCalledWith(400) - }) - - test('should return 404 when form not found', async () => { - findFormBySlug.mockResolvedValue(null) + 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: preHandlerError } + } + }) - await handler(mockRequest, mockH) + const result = await handler(mockRequest, mockH) - expect(mockH.code).toHaveBeenCalledWith(404) + expect(result).toEqual(preHandlerError) }) test('should render page with default content when no config-driven content available', async () => { 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 3846ce8f7..216ac941e 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,8 +24,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) - request.app.model = { def: definition } + await loadFormAndSetOnRequestModel(form, request) } } @@ -50,3 +49,8 @@ export async function clearApplicationStateHandler(request, h) { return h.redirect(`/${slug}`) } + +const loadFormAndSetOnRequestModel = async (form, request) => { + 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, 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 + } +}