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
40 changes: 36 additions & 4 deletions src/server/plugins/engine/form-availability.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FormStatus } from '@defra/forms-model'
import Boom from '@hapi/boom'

import {
Expand All @@ -8,17 +9,48 @@ import { metadata } from '~/test/fixtures/form.js'

describe('form-availability', () => {
describe('assertFormAvailable', () => {
it('should do nothing if form is online', () => {
it('should do nothing if live form is online', () => {
expect(() =>
assertFormAvailable({ ...metadata, offline: false })
assertFormAvailable(
{ ...metadata, offline: false },
FormStatus.Live,
false
)
).not.toThrow()
expect(() =>
assertFormAvailable({ ...metadata, offline: undefined })
assertFormAvailable(
{ ...metadata, offline: undefined },
FormStatus.Live,
false
)
).not.toThrow()
})

it('should do nothing if draft form or live preview is online', () => {
expect(() =>
assertFormAvailable(
{ ...metadata, offline: false },
FormStatus.Draft,
true
)
).not.toThrow()
expect(() =>
assertFormAvailable(
{ ...metadata, offline: undefined },
FormStatus.Live,
true
)
).not.toThrow()
})

it('should throw a 503 Boom error if form is offline', () => {
expect(() => assertFormAvailable({ ...metadata, offline: true })).toThrow(
expect(() =>
assertFormAvailable(
{ ...metadata, offline: true },
FormStatus.Live,
false
)
).toThrow(
expect.objectContaining({
message: `Form ${metadata.slug} is offline`,
data: {
Expand Down
14 changes: 11 additions & 3 deletions src/server/plugins/engine/form-availability.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type FormMetadata } from '@defra/forms-model'
import { FormStatus, type FormMetadata } from '@defra/forms-model'
import Boom from '@hapi/boom'

export interface OfflineBoomData {
Expand All @@ -11,8 +11,16 @@ export interface OfflineBoomData {
* unavailable-response extension catches the marker and renders the
* "Sorry, this form is unavailable" view at HTTP 200.
*/
export function assertFormAvailable(metadata: FormMetadata): void {
if (metadata.offline === true) {
export function assertFormAvailable(
metadata: FormMetadata,
formState: FormStatus,
isPreview: boolean
): void {
if (
metadata.offline === true &&
formState === FormStatus.Live &&
!isPreview
) {
const data: OfflineBoomData = { offline: true, metadata }
throw Boom.boomify(new Error(`Form ${metadata.slug} is offline`), {
statusCode: 503,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
getFormModel,
resolveFormModel,
type FormModelOptions
} from '~/src/server/plugins/engine/beta/form-context.js'
} from '~/src/server/plugins/engine/form-context.js'
import { PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'
import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'
import { type FormContext } from '~/src/server/plugins/engine/types.js'
Expand All @@ -17,7 +17,7 @@ const mockGetCacheService = jest.fn()
const mockCacheService = { getState: jest.fn() }
const mockCheckEmailAddressForLiveFormSubmission = jest.fn()

jest.mock('../models/index.ts', () => ({
jest.mock('~/src/server/plugins/engine/models/index.ts', () => ({
__esModule: true,
FormModel: jest.fn()
}))
Expand All @@ -32,7 +32,7 @@ jest.mock('~/src/server/plugins/engine/services/index.js', () => ({
outputService: {}
}))

jest.mock('../pageControllers/index.ts', () => {
jest.mock('~/src/server/plugins/engine/pageControllers/index.ts', () => {
class MockTerminalPageController {
path = ''
}
Expand All @@ -43,8 +43,8 @@ jest.mock('../pageControllers/index.ts', () => {
}
})

jest.mock('../helpers.ts', () => ({
...jest.requireActual<object>('../helpers.ts'),
jest.mock('~/src/server/plugins/engine/helpers.ts', () => ({
...jest.requireActual<object>('~/src/server/plugins/engine/helpers.ts'),
getCacheService: (...args: unknown[]): unknown =>
mockGetCacheService(...args),
checkEmailAddressForLiveFormSubmission: (...args: unknown[]): unknown =>
Expand All @@ -56,13 +56,14 @@ const mockServices: {
} = jest.requireMock('~/src/server/plugins/engine/services/index.js')
const mockFormsService = mockServices.formsService

const { FormModel }: { FormModel: jest.Mock } =
jest.requireMock('../models/index.ts')
const { FormModel }: { FormModel: jest.Mock } = jest.requireMock(
'~/src/server/plugins/engine/models/index.ts'
)

const {
TerminalPageController: MockTerminalPageController
}: { TerminalPageController: new () => { path: string } } = jest.requireMock(
'../pageControllers/index.ts'
'~/src/server/plugins/engine/pageControllers/index.ts'
)

describe('getFormContext helper', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export async function getFormModel(
const formState = resolveState(state)

const metadata = await formsService.getFormMetadata(slug)
assertFormAvailable(metadata)
assertFormAvailable(metadata, formState, isPreview)

const definition = await formsService.getFormDefinition(
metadata.id,
Expand Down Expand Up @@ -136,9 +136,9 @@ export async function resolveFormModel(
const { formsService } = services

const metadata = await formsService.getFormMetadata(slug)
assertFormAvailable(metadata)
const formState = resolveState(state)
const isPreview = options.isPreview ?? isPreviewState(state, options)
assertFormAvailable(metadata, formState, isPreview)
const stateMetadata = metadata[formState]

if (!stateMetadata) {
Expand Down
2 changes: 1 addition & 1 deletion src/server/plugins/engine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export {
getFormContext,
getFormModel,
resolveFormModel
} from '~/src/server/plugins/engine/beta/form-context.js'
} from '~/src/server/plugins/engine/form-context.js'
export * from '~/src/server/plugins/engine/form-availability.js'

const globals = {
Expand Down
42 changes: 2 additions & 40 deletions src/server/plugins/engine/models/unavailable-view-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,46 +17,8 @@ describe('unavailableViewModel', () => {
it('should strip the organisation suffix if present', () => {
const result = unavailableViewModel({
...metadata,
organisation: 'Rural Payments Agency RPA'
organisation: 'Rural Payments Agency - RPA'
} as FormMetadata)
expect(result.organisationName).toBe('Rural Payments Agency')
})

it('should handle multiple phone lines correctly', () => {
const result = unavailableViewModel({
...metadata,
contact: {
phone: '01234 567 890\n09876 543 210'
}
} as FormMetadata)
expect(result.phoneLines).toEqual(['01234 567 890', '09876 543 210'])
})

it('should filter out empty phone lines and trim whitespace', () => {
const result = unavailableViewModel({
...metadata,
contact: {
phone: ' 01234 567 890 \n \n 09876 543 210 '
}
} as FormMetadata)
expect(result.phoneLines).toEqual(['01234 567 890', '09876 543 210'])
})

it('should return undefined if phone is empty or only whitespace', () => {
const result = unavailableViewModel({
...metadata,
contact: {
phone: ' \n '
}
} as FormMetadata)
expect(result.phoneLines).toBeUndefined()
})

it('should handle missing contact property', () => {
const result = unavailableViewModel({
...metadata,
contact: undefined
} as FormMetadata)
expect(result.phoneLines).toBeUndefined()
expect(result.organisationName).toBe('Rural Payments Agency - RPA')
})
})
25 changes: 4 additions & 21 deletions src/server/plugins/engine/models/unavailable-view-model.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,10 @@
import { type FormMetadata } from '@defra/forms-model'
import { type FormMetadata, type FormMetadataContact } from '@defra/forms-model'

export interface UnavailableViewModel {
pageTitle: string
formTitle: string
organisationName: string
phoneLines?: string[]
}

/**
* Defra organisations carry an abbreviation suffix on the enum value, e.g.
* "Rural Payments Agency – RPA". The unavailable page reads cleanly without it.
*/
function stripOrgSuffix(organisation: string) {
return organisation.split(' – ')[0]
}

function splitPhoneLines(phone: string | undefined) {
if (!phone) return undefined
const lines = phone
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0)
return lines.length > 0 ? lines : undefined
contact?: FormMetadataContact
}

export function unavailableViewModel(
Expand All @@ -30,7 +13,7 @@ export function unavailableViewModel(
return {
pageTitle: 'Sorry, this form is unavailable',
formTitle: metadata.title,
organisationName: stripOrgSuffix(metadata.organisation),
phoneLines: splitPhoneLines(metadata.contact?.phone)
organisationName: metadata.organisation,
contact: metadata.contact
}
}
2 changes: 1 addition & 1 deletion src/server/plugins/engine/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import {
EXTERNAL_STATE_APPENDAGE,
EXTERNAL_STATE_PAYLOAD
} from '~/src/server/constants.js'
import { resolveFormModel } from '~/src/server/plugins/engine/beta/form-context.js'
import {
FormComponent,
isFormState
} from '~/src/server/plugins/engine/components/FormComponent.js'
import { resolveFormModel } from '~/src/server/plugins/engine/form-context.js'
import {
checkFormStatus,
findPage,
Expand Down
30 changes: 22 additions & 8 deletions src/server/plugins/engine/views/unavailable.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,31 @@
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<h1 class="govuk-heading-l">Sorry, this form is unavailable</h1>
<p class="govuk-body">'{{ formTitle }}' has been archived and is no longer available.</p>
<p class="govuk-body">Contact the {{ organisationName }}.</p>
<p class="govuk-body">'{{ formTitle }}' is no longer available.</p>
<h2 class="govuk-heading-m">Contact details for {{ organisationName }}</h2>

{% if phoneLines %}
<ul class="govuk-list govuk-list--bullet">
{% for line in phoneLines %}
<li>{{ line }}</li>
{% endfor %}
</ul>
{% if contact.phone %}
<h3 class="govuk-heading-s">Telephone</h3>
<div class="app-prose-scope">
{{ contact.phone | markdown(3) | safe }}
</div>
<p class="govuk-body"><a href="https://www.gov.uk/call-charges" class="govuk-link govuk-link--no-visited-state">Find out about call charges</a></p>
{% endif %}

{% if contact.email %}
<h3 class="govuk-heading-s">Email</h3>
<ul class="govuk-list">
<li><a class="govuk-link govuk-link--no-visited-state" href="mailto:{{ contact.email.address }}">{{ contact.email.address }}</a></li>
<li>{{ contact.email.responseTime }}</li>
</ul>
{% endif %}

{% if contact.online %}
<h3 class="govuk-heading-s">Online contact form</h3>
<ul class="govuk-list">
<li><a class="govuk-link govuk-link--no-visited-state" href="{{ contact.online.url }}">{{ contact.online.text }}</a></li>
</ul>
{% endif %}
</div>
</div>
{% endblock %}
Loading