Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
422ff12
Add security questions schema
davidjamesstone Aug 29, 2025
02f0a20
Bump @defra/forms-model to 3.0.546
davidjamesstone Sep 3, 2025
690f789
import FormStatus from '@defra/forms-engine-plugin/types'
davidjamesstone Sep 3, 2025
9ce2474
Update save and exit routes
davidjamesstone Sep 3, 2025
c2ede95
Update save and exit message publication
davidjamesstone Sep 3, 2025
d5d65db
Add saveAndExitExpiryDays config
davidjamesstone Sep 3, 2025
2bdc75e
Use exported AnyFormRequest from forms-engine-plugin
davidjamesstone Sep 3, 2025
824ee49
Remove govuk-main-wrapper--l class from save and exit pages
davidjamesstone Sep 3, 2025
142cb53
Add title to the save and exit pages
davidjamesstone Sep 3, 2025
8784477
FormStatus as a route param not query
davidjamesstone Sep 4, 2025
4c1bfd2
Refactor outputService to use FormStatus from @defra/forms-engine-plugin
davidjamesstone Sep 5, 2025
4678ddb
Update privacy notice for save and return (#906)
davidjamesstone Sep 8, 2025
25e9c05
Feat/df 372 resume save and exit (#907)
jbarnsley10 Sep 8, 2025
493ddfa
Update JSDoc function description
davidjamesstone Sep 8, 2025
f220e24
Sonar fixes (Nested template literals)
davidjamesstone Sep 8, 2025
6656d12
Sonar fixes (Variable declared in the upper scope)
davidjamesstone Sep 8, 2025
ed35d88
Rename /save-and-exit-resume to /resume-form
davidjamesstone Sep 8, 2025
7bd2ae0
Combine save and exit routes into 1 file
davidjamesstone Sep 8, 2025
2b16c41
Remove import alias
davidjamesstone Sep 8, 2025
1cc6579
TS in JSDoc imports at the bottom
davidjamesstone Sep 8, 2025
c4d7e3e
Extra coverage
jbarnsley10 Sep 8, 2025
697fb7b
Merge branch 'feature/DF-370-save-form-progress-4' of https://github.…
jbarnsley10 Sep 8, 2025
bc8777a
Sonar fixes (parameters order)
davidjamesstone Sep 8, 2025
1c1ce99
Renamed models
jbarnsley10 Sep 8, 2025
97ace17
Extra coverage
jbarnsley10 Sep 8, 2025
4ae8b74
Extra coverage and fixed test
jbarnsley10 Sep 9, 2025
370ca07
Max attempts increased to 5
jbarnsley10 Sep 9, 2025
76dbd42
Remove empty import
davidjamesstone Sep 9, 2025
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
1,000 changes: 80 additions & 920 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,8 @@
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@aws-sdk/client-sns": "^3.864.0",
"@defra/forms-engine-plugin": "^2.1.9",
"@defra/forms-model": "^3.0.545",
"@aws-sdk/client-s3": "^3.679.0",
"@defra/forms-engine-plugin": "^3.0.0",
"@defra/forms-model": "^3.0.550",
"@defra/hapi-tracing": "^1.26.0",
"@elastic/ecs-pino-format": "^1.5.0",
"@hapi/boom": "^10.0.1",
Expand Down
5 changes: 5 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,11 @@ export const config = convict({
format: String,
default: '',
env: 'GOOGLE_ANALYTICS_TRACKING_ID'
},
saveAndExitExpiryDays: {
format: Number,
default: 30,
env: 'SAVE_AND_EXIT_EXPIRY_IN_DAYS'
}
})

Expand Down
21 changes: 21 additions & 0 deletions src/server/helpers/error-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Joi from 'joi'

/**
* @param {string} fieldName
* @param {string} message
* @returns {Joi.ValidationError}
*/
export function createJoiError(fieldName, message) {
return new Joi.ValidationError(
message,
[
{
message,
path: [fieldName],
type: 'custom',
context: { key: fieldName, label: fieldName }
}
],
{}
)
}
2 changes: 1 addition & 1 deletion src/server/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import {
UploadStatus,
type UploadStatusResponse
} from '@defra/forms-engine-plugin/engine/types.js'
import { FormStatus } from '@defra/forms-engine-plugin/types'
import { type Server } from '@hapi/hapi'
import { StatusCodes } from 'http-status-codes'

import { FORM_PREFIX } from '~/src/server/constants.js'
import { createServer } from '~/src/server/index.js'
import { FormStatus } from '~/src/server/routes/types.js'
import {
getFormDefinition,
getFormMetadata
Expand Down
27 changes: 24 additions & 3 deletions src/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { join, parse } from 'path'

import plugin from '@defra/forms-engine-plugin'
import { checkFormStatus } from '@defra/forms-engine-plugin/engine/helpers.js'
import { FormModel } from '@defra/forms-engine-plugin/engine/models/FormModel.js'
import { type PluginOptions } from '@defra/forms-engine-plugin/engine/types.js'
import { formSubmissionService } from '@defra/forms-engine-plugin/services/index.js'
import {
type FormContext,
type FormRequestPayload,
type FormResponseToolkit,
type PluginOptions
} from '@defra/forms-engine-plugin/types'
import { type FormDefinition } from '@defra/forms-model'
import { Engine as CatboxMemory } from '@hapi/catbox-memory'
import { Engine as CatboxRedis } from '@hapi/catbox-redis'
Expand Down Expand Up @@ -129,15 +135,30 @@ export const configureEnginePlugin = async ({
const pluginObject = {
plugin,
options: {
cacheName: 'session',
cache: 'session',
nunjucks: {
baseLayoutPath: 'layout.html',
paths
},
model,
services,
viewContext: context,
baseUrl: config.get('baseUrl')
baseUrl: config.get('baseUrl'),
saveAndExit: (
request: FormRequestPayload,
h: FormResponseToolkit,
_context: FormContext
) => {
const { params } = request
const { slug } = params
const { isPreview, state } = checkFormStatus(params)

return h.redirect(
!isPreview
? `/save-and-exit/${slug}`
: `/save-and-exit/${slug}/${state}`
)
}
}
}
const routeOptions = {
Expand Down
12 changes: 7 additions & 5 deletions src/server/messaging/__stubs__/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@ export function buildSaveAndExitMessageData(
partialSaveAndExitMessageData = {}
) {
return {
formId: saveAndExitFormId,
form: {
id: 'formId',
title: 'My First Form',
isPreview: false,
status: FormStatus.Draft,
baseUrl: 'http://localhost:3009'
},
security: {
question: SecurityQuestionsEnum.MemorablePlace,
answer: 'a1'
},
email: 'forms@example.com',
formStatus: {
status: FormStatus.Draft,
isPreview: false
},
state: {},
...partialSaveAndExitMessageData
}
Expand Down
28 changes: 23 additions & 5 deletions src/server/messaging/mappers/events.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,43 @@
import {
FormStatus,
SubmissionEventMessageCategory,
SubmissionEventMessageSchemaVersion,
SubmissionEventMessageSource,
SubmissionEventMessageType
} from '@defra/forms-model'

import { config } from '~/src/config/index.js'

const baseUrl = config.get('baseUrl')

/**
* @param { string } formId
* @param { string } formTitle
* @param { string } email
* @param {{ question: SecurityQuestionsEnum, answer: string }} security
* @param {{ status: FormStatus, isPreview: boolean }} formStatus
* @param { FormState } state
* @param { FormStatus } [status]
* @returns {SaveAndExitMessage}
*/
export function saveAndExitMapper(formId, email, security, formStatus, state) {
export function saveAndExitMapper(
formId,
formTitle,
email,
security,
state,
status
) {
/** @type {SaveAndExitMessageData} */
const data = {
formId,
form: {
id: formId,
title: formTitle,
status: status ?? FormStatus.Live,
isPreview: !!status,
baseUrl
},
email,
security,
formStatus,
state
}
const now = new Date()
Expand All @@ -35,6 +53,6 @@ export function saveAndExitMapper(formId, email, security, formStatus, state) {
}

/**
* @import { FormStatus, SaveAndExitMessage, SaveAndExitMessageData, SecurityQuestionsEnum } from '@defra/forms-model'
* @import { SaveAndExitMessage, SaveAndExitMessageData, SecurityQuestionsEnum } from '@defra/forms-model'
* @import { FormState } from '@defra/forms-engine-plugin/engine/types.js'
*/
35 changes: 22 additions & 13 deletions src/server/messaging/mappers/events.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,36 @@ import { saveAndExitMapper } from '~/src/server/messaging/mappers/events.js'
describe('runner-events', () => {
describe('saveAndExitMapper', () => {
it('should map a payload into a SAVE_AND_EXIT event', () => {
/**
* @type {import('@defra/forms-model').SaveAndExitMessageData}
*/
const payload = {
formId: 'formId',
form: {
id: 'formId',
title: 'My First Form',
isPreview: true,
status: FormStatus.Draft,
baseUrl: 'http://localhost:3009'
},
email: 'my-email@here.com',
security: {
question: SecurityQuestionsEnum.CharacterName,
answer: 'brown'
},
formStatus: {
status: FormStatus.Draft,
isPreview: false
},
state: {
formVal1: '123',
formVal2: '456'
}
}

expect(
saveAndExitMapper(
payload.formId,
payload.form.id,
payload.form.title,
payload.email,
payload.security,
payload.formStatus,
payload.state
payload.state,
payload.form.status
)
).toEqual({
schemaVersion: SubmissionEventMessageSchemaVersion.V1,
Expand All @@ -44,16 +51,18 @@ describe('runner-events', () => {
createdAt: expect.any(Date),
messageCreatedAt: expect.any(Date),
data: {
formId: payload.formId,
form: {
id: payload.form.id,
title: payload.form.title,
isPreview: payload.form.isPreview,
status: payload.form.status,
baseUrl: 'http://localhost:3009'
},
email: payload.email,
security: {
question: payload.security.question,
answer: payload.security.answer
},
formStatus: {
status: payload.formStatus.status,
isPreview: payload.formStatus.isPreview
},
state: payload.state
}
})
Expand Down
2 changes: 1 addition & 1 deletion src/server/messaging/publish-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function publishEvent(message) {
const result = await client.send(command)

logger.info(
`Published ${message.type} event for formId ${message.data.formId}. MessageId: ${result.MessageId}`
`Published ${message.type} event for formId ${message.data.form.id}. MessageId: ${result.MessageId}`
)

return result
Expand Down
17 changes: 13 additions & 4 deletions src/server/messaging/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,28 @@ async function validateAndPublishEvent(saveAndExitMessage) {
* Publish 'save and exit' event
* The returned entityId will be a newly-generated guid
* @param {string} formId
* @param {string} formTitle
* @param {string} email
* @param {{ question: SecurityQuestionsEnum, answer: string }} security
* @param {{ status: FormStatus, isPreview: boolean }} formStatus
* @param {FormState} state
* @param {FormStatus} [status]
*/
export async function publishSaveAndExitEvent(
formId,
formTitle,
email,
security,
formStatus,
state
state,
status
) {
const message = saveAndExitMapper(formId, email, security, formStatus, state)
const message = saveAndExitMapper(
formId,
formTitle,
email,
security,
state,
status
)

return validateAndPublishEvent(message)
}
Expand Down
28 changes: 19 additions & 9 deletions src/server/messaging/publish.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,22 @@ import { publishSaveAndExitEvent } from '~/src/server/messaging/publish.js'

jest.mock('~/src/server/messaging/publish-base.js')

/**
* @type {SaveAndExitMessageData}
*/
const saveAndExitPayload = {
formId: 'formId',
form: {
id: 'formId',
title: 'My First Form',
isPreview: true,
status: FormStatus.Draft,
baseUrl: 'http://localhost:3009'
},
email: 'my-email@here.com',
security: {
question: SecurityQuestionsEnum.CharacterName,
answer: 'brown'
},
formStatus: {
status: FormStatus.Draft,
isPreview: true
},
state: {
formVal1: '123',
formVal2: '456'
Expand All @@ -45,11 +50,12 @@ describe('publish', () => {
describe('publishSaveAndExitEvent', () => {
it('should publish SAVE_AND_EXIT event', async () => {
await publishSaveAndExitEvent(
saveAndExitPayload.formId,
saveAndExitPayload.form.id,
saveAndExitPayload.form.title,
saveAndExitPayload.email,
saveAndExitPayload.security,
saveAndExitPayload.formStatus,
saveAndExitPayload.state
saveAndExitPayload.state,
saveAndExitPayload.form.status
)

expect(publishEvent).toHaveBeenCalledWith({
Expand All @@ -72,11 +78,15 @@ describe('publish', () => {
publishSaveAndExitEvent(invalidPayload)
).rejects.toThrow(
new ValidationError(
'"data.formId" must be a string. "data.email" is required. "data.state" is required',
'"data.form.id" must be a string. "data.form.title" is required. "data.email" is required. "data.state" is required',
[],
{}
)
)
})
})
})

/**
* @import { SaveAndExitMessageData } from '@defra/forms-model'
*/
Loading
Loading