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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -57,7 +144,7 @@ pages:
- name: startInfoOne
title: Html
type: Html
content: <p class="govuk-body">Config Broker Version 1.0.1 - This service uses DefraID and the Save and Return
content: <p class="govuk-body">This service uses DefraID and the Save and Return
feature, and demonstrates the use of the following components and page
types:</p>
id: 434ab3dc-fad9-44c5-8601-b19477a1d77e
Expand Down Expand Up @@ -129,7 +216,6 @@ pages:
application
type: YesNoField
options:
classes: govuk-radios--inline
customValidationMessages:
any.required: Select 'Yes' to continue
shortDescription: Yes or No
Expand Down Expand Up @@ -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: |
<div class="govuk-body">
<p>
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.
</p>

<p>
For larger holdings, this is all woodland within the same geographical
location.
</p>

<p>
Land parcels must be entirely within England and defined as an area of land
that:
</p>

<ul class="govuk-list govuk-list--bullet">
<li>is at least 0.5 hectares</li>
<li>has an average width of at least 20 metres</li>
<li>
is a group or line of trees that are, or will reach, at least 5 metres in height
</li>
<li>has a crown cover of more than 20% of the ground area</li>
</ul>

<p>
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.
</p>

<p>
To find out more about what land is eligible for a WMP read our
<a href="#" class="govuk-link">
Applicant's guide: PA3 Woodland management plan 2026 guidance
</a>.
</p>
</div>
supportDetailsSummaryText: Get help with your application
supportDetailsHtml: |
<p>Phone: <a href="tel:02080262395" class="telephone-link" aria-label="Call us on 020 8026 2395">020 8026 2395</a></p>
<p>Monday to Friday, 8:30am to 5pm, except bank holidays</p>
<p>
<a href='https://www.gov.uk/call-charges' target='_blank'>
Find out about call charges (opens in new tab)
</a>
</p>
<p>Email: <a href='mailto:farmpayments@rpa.gov.uk'>farmpayments@rpa.gov.uk</a></p>
<p>We aim to respond to emails within 10 working days</p>
path: /select-land-parcel
id: f9f1a33b-2fb5-4418-951b-680067efc179
- title: Multi Field Form Example
path: /multi-field-form
components:
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/server/common/forms/services/api-form-service.js
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand Down Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions src/server/common/forms/services/api-form-service.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
27 changes: 27 additions & 0 deletions src/server/common/helpers/form-verify-and-request-load.js
Original file line number Diff line number Diff line change
@@ -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) {
Comment thread
swdpcomputing marked this conversation as resolved.
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 }
}
40 changes: 40 additions & 0 deletions src/server/common/helpers/form-verify-and-request-load.test.js
Original file line number Diff line number Diff line change
@@ -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' })
})
})
Loading
Loading