@defra/forms-engine-plugin 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.server/server/plugins/engine/helpers.d.ts +1 -1
  2. package/.server/server/plugins/engine/outputFormatters/human/v1.d.ts +2 -1
  3. package/.server/server/plugins/engine/outputFormatters/human/v1.js +1 -1
  4. package/.server/server/plugins/engine/outputFormatters/human/v1.js.map +1 -1
  5. package/.server/server/plugins/engine/outputFormatters/index.d.ts +2 -1
  6. package/.server/server/plugins/engine/outputFormatters/index.js.map +1 -1
  7. package/.server/server/plugins/engine/outputFormatters/machine/v1.d.ts +2 -1
  8. package/.server/server/plugins/engine/outputFormatters/machine/v1.js +2 -1
  9. package/.server/server/plugins/engine/outputFormatters/machine/v1.js.map +1 -1
  10. package/.server/server/plugins/engine/outputFormatters/machine/v2.d.ts +2 -1
  11. package/.server/server/plugins/engine/outputFormatters/machine/v2.js +3 -2
  12. package/.server/server/plugins/engine/outputFormatters/machine/v2.js.map +1 -1
  13. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +9 -2
  14. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  15. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +4 -7
  16. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  17. package/.server/server/plugins/engine/plugin.js +1 -2
  18. package/.server/server/plugins/engine/plugin.js.map +1 -1
  19. package/.server/server/plugins/engine/routes/questions.js +1 -1
  20. package/.server/server/plugins/engine/routes/questions.js.map +1 -1
  21. package/.server/server/plugins/engine/services/notifyService.d.ts +2 -1
  22. package/.server/server/plugins/engine/services/notifyService.js +2 -2
  23. package/.server/server/plugins/engine/services/notifyService.js.map +1 -1
  24. package/.server/server/plugins/engine/types.d.ts +1 -1
  25. package/.server/server/plugins/engine/types.js.map +1 -1
  26. package/.server/server/services/cacheService.d.ts +0 -2
  27. package/.server/server/services/cacheService.js +1 -4
  28. package/.server/server/services/cacheService.js.map +1 -1
  29. package/.server/server/types.d.ts +2 -2
  30. package/.server/server/types.js.map +1 -1
  31. package/package.json +2 -2
  32. package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +3 -3
  33. package/src/server/plugins/engine/outputFormatters/human/v1.ts +2 -0
  34. package/src/server/plugins/engine/outputFormatters/index.ts +2 -0
  35. package/src/server/plugins/engine/outputFormatters/machine/v1.test.ts +40 -1
  36. package/src/server/plugins/engine/outputFormatters/machine/v1.ts +3 -0
  37. package/src/server/plugins/engine/outputFormatters/machine/v2.test.ts +40 -1
  38. package/src/server/plugins/engine/outputFormatters/machine/v2.ts +4 -1
  39. package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +81 -16
  40. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +13 -1
  41. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +4 -4
  42. package/src/server/plugins/engine/plugin.ts +1 -2
  43. package/src/server/plugins/engine/routes/questions.ts +1 -1
  44. package/src/server/plugins/engine/services/notifyService.test.ts +26 -3
  45. package/src/server/plugins/engine/services/notifyService.ts +3 -1
  46. package/src/server/plugins/engine/types.ts +0 -1
  47. package/src/server/services/cacheService.test.ts +8 -2
  48. package/src/server/services/cacheService.ts +1 -13
  49. package/src/server/types.ts +2 -0
@@ -1,6 +1,7 @@
1
1
  import { type PageQuestion } from '@defra/forms-model'
2
2
  import { type ResponseToolkit } from '@hapi/hapi'
3
3
 
4
+ import { getCacheService } from '~/src/server/plugins/engine/helpers.js'
4
5
  import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
5
6
  import { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'
6
7
  import {
@@ -1333,63 +1334,127 @@ describe('Save and Return functionality', () => {
1333
1334
 
1334
1335
  describe('handleSaveAndReturn', () => {
1335
1336
  it('should save state and redirect to exit page', async () => {
1337
+ const sessionPersisterMock = jest.fn()
1336
1338
  const state: FormSubmissionState = {
1337
1339
  $$__referenceNumber: 'foobar',
1338
1340
  yesNoField: true
1339
1341
  }
1340
1342
  const request = {
1341
1343
  ...requestPage1,
1344
+ server: {
1345
+ plugins: {
1346
+ 'forms-engine-plugin': {
1347
+ saveAndReturn: {
1348
+ sessionPersister: sessionPersisterMock
1349
+ },
1350
+ cacheService: {
1351
+ clearState: jest.fn()
1352
+ } as unknown as CacheService
1353
+ }
1354
+ }
1355
+ },
1342
1356
  method: 'post',
1343
1357
  payload: { yesNoField: true, action: 'save-and-return' }
1344
1358
  } as unknown as FormRequestPayload
1345
1359
 
1346
- const context = model.getFormContext(request, state)
1360
+ const cacheService = getCacheService(request.server)
1347
1361
 
1348
- jest.spyOn(controller1, 'setState').mockResolvedValue(state)
1362
+ const context = model.getFormContext(request, state)
1349
1363
 
1350
1364
  await controller1.handleSaveAndReturn(request, context, h)
1351
1365
 
1352
- expect(controller1.setState).toHaveBeenCalledWith(request, state)
1366
+ expect(sessionPersisterMock).toHaveBeenCalledWith(context.state, request)
1367
+ expect(cacheService.clearState).toHaveBeenCalledWith(request)
1353
1368
  expect(h.redirect).toHaveBeenCalledWith('/test/exit')
1354
1369
  })
1355
1370
 
1356
- it('should handle save-and-return with incomplete data', async () => {
1371
+ it('should throw if sessionPersister inside saveAndReturn options provided', async () => {
1372
+ const sessionPersisterMock = jest.fn()
1357
1373
  const state: FormSubmissionState = {
1358
1374
  $$__referenceNumber: 'foobar',
1359
- yesNoField: null
1375
+ yesNoField: true
1360
1376
  }
1361
1377
  const request = {
1362
1378
  ...requestPage1,
1379
+ server: {
1380
+ plugins: {
1381
+ 'forms-engine-plugin': {
1382
+ // No sessionPersister object
1383
+ saveAndReturn: {}
1384
+ }
1385
+ }
1386
+ },
1363
1387
  method: 'post',
1364
- payload: { yesNoField: '', action: 'save-and-return' }
1388
+ payload: { yesNoField: true, action: 'save-and-return' }
1365
1389
  } as unknown as FormRequestPayload
1366
1390
 
1367
1391
  const context = model.getFormContext(request, state)
1368
1392
 
1369
- jest.spyOn(controller1, 'setState').mockResolvedValue(state)
1393
+ await expect(
1394
+ controller1.handleSaveAndReturn(request, context, h)
1395
+ ).rejects.toThrow('Server misconfigured for save and return')
1370
1396
 
1371
- await controller1.handleSaveAndReturn(request, context, h)
1397
+ expect(sessionPersisterMock).not.toHaveBeenCalled()
1398
+ expect(h.redirect).not.toHaveBeenCalled()
1399
+ })
1372
1400
 
1373
- expect(controller1.setState).toHaveBeenCalledWith(request, state)
1374
- expect(h.redirect).toHaveBeenCalledWith('/test/exit')
1401
+ it('should throw if no saveAndReturn options provided', async () => {
1402
+ const sessionPersisterMock = jest.fn()
1403
+ const state: FormSubmissionState = {
1404
+ $$__referenceNumber: 'foobar',
1405
+ yesNoField: true
1406
+ }
1407
+ const request = {
1408
+ ...requestPage1,
1409
+ server: {
1410
+ plugins: {
1411
+ 'forms-engine-plugin': {
1412
+ // No saveAndReturn object
1413
+ }
1414
+ }
1415
+ },
1416
+ method: 'post',
1417
+ payload: { yesNoField: true, action: 'save-and-return' }
1418
+ } as unknown as FormRequestPayload
1419
+
1420
+ const context = model.getFormContext(request, state)
1421
+
1422
+ await expect(
1423
+ controller1.handleSaveAndReturn(request, context, h)
1424
+ ).rejects.toThrow('Server misconfigured for save and return')
1425
+
1426
+ expect(sessionPersisterMock).not.toHaveBeenCalled()
1427
+ expect(h.redirect).not.toHaveBeenCalled()
1375
1428
  })
1376
1429
 
1377
- it('should handle save-and-return with validation errors', async () => {
1430
+ it('should throw if sessionPersister throws as well with validation errors', async () => {
1431
+ const sessionPersisterMock = jest.fn().mockImplementation(() => {
1432
+ throw new Error('Session persister error')
1433
+ })
1378
1434
  const state: FormSubmissionState = { $$__referenceNumber: 'foobar' }
1379
1435
  const request = {
1380
1436
  ...requestPage1,
1381
1437
  method: 'post',
1438
+ server: {
1439
+ plugins: {
1440
+ 'forms-engine-plugin': {
1441
+ saveAndReturn: {
1442
+ sessionPersister: sessionPersisterMock
1443
+ }
1444
+ }
1445
+ }
1446
+ },
1382
1447
  payload: { action: 'save-and-return' }
1383
1448
  } as unknown as FormRequestPayload
1384
1449
 
1385
1450
  const context = model.getFormContext(request, state)
1386
1451
 
1387
- jest.spyOn(controller1, 'setState').mockResolvedValue(state)
1388
-
1389
- await controller1.handleSaveAndReturn(request, context, h)
1452
+ await expect(
1453
+ controller1.handleSaveAndReturn(request, context, h)
1454
+ ).rejects.toThrow('Session persister error')
1390
1455
 
1391
- expect(controller1.setState).toHaveBeenCalledWith(request, state)
1392
- expect(h.redirect).toHaveBeenCalledWith('/test/exit')
1456
+ expect(sessionPersisterMock).toHaveBeenCalledWith(context.state, request)
1457
+ expect(h.redirect).not.toHaveBeenCalledWith('/test/exit')
1393
1458
  })
1394
1459
  })
1395
1460
 
@@ -8,6 +8,7 @@ import {
8
8
  type Link,
9
9
  type Page
10
10
  } from '@defra/forms-model'
11
+ import Boom from '@hapi/boom'
11
12
  import { type ResponseToolkit, type RouteOptions } from '@hapi/hapi'
12
13
  import { type ValidationErrorItem } from 'joi'
13
14
 
@@ -17,6 +18,7 @@ import { type BackLink } from '~/src/server/plugins/engine/components/types.js'
17
18
  import {
18
19
  getCacheService,
19
20
  getErrors,
21
+ getSaveAndReturnHelpers,
20
22
  normalisePath,
21
23
  proceed
22
24
  } from '~/src/server/plugins/engine/helpers.js'
@@ -548,7 +550,17 @@ export class QuestionPageController extends PageController {
548
550
  const { state } = context
549
551
 
550
552
  // Save the current state and redirect to exit page
551
- await this.setState(request, state)
553
+ const saveAndReturn = getSaveAndReturnHelpers(request.server)
554
+
555
+ if (!saveAndReturn?.sessionPersister) {
556
+ throw Boom.internal('Server misconfigured for save and return')
557
+ }
558
+
559
+ await saveAndReturn.sessionPersister(state, request)
560
+
561
+ const cacheService = getCacheService(request.server)
562
+ await cacheService.clearState(request)
563
+
552
564
  return h.redirect(this.getHref('/exit'))
553
565
  }
554
566
 
@@ -105,7 +105,6 @@ export class SummaryPageController extends QuestionPageController {
105
105
  ) => {
106
106
  const { model } = this
107
107
  const { params } = request
108
- const { state } = context
109
108
  const cacheService = getCacheService(request.server)
110
109
 
111
110
  const { formsService } = this.model.services
@@ -121,7 +120,7 @@ export class SummaryPageController extends QuestionPageController {
121
120
  // Send submission email
122
121
  if (emailAddress) {
123
122
  const viewModel = this.getSummaryViewModel(request, context)
124
- await submitForm(request, viewModel, model, state, emailAddress)
123
+ await submitForm(context, request, viewModel, model, emailAddress)
125
124
  }
126
125
 
127
126
  await cacheService.setConfirmationState(request, { confirmed: true })
@@ -147,13 +146,13 @@ export class SummaryPageController extends QuestionPageController {
147
146
  }
148
147
 
149
148
  async function submitForm(
149
+ context: FormContext,
150
150
  request: FormRequestPayload,
151
151
  summaryViewModel: SummaryViewModel,
152
152
  model: FormModel,
153
- state: FormSubmissionState,
154
153
  emailAddress: string
155
154
  ) {
156
- await extendFileRetention(model, state, emailAddress)
155
+ await extendFileRetention(model, context.state, emailAddress)
157
156
 
158
157
  const formStatus = checkFormStatus(request.params)
159
158
  const logTags = ['submit', 'submissionApi']
@@ -180,6 +179,7 @@ async function submitForm(
180
179
  }
181
180
 
182
181
  return model.services.outputService.submit(
182
+ context,
183
183
  request,
184
184
  model,
185
185
  emailAddress,
@@ -43,8 +43,7 @@ export const plugin = {
43
43
  cacheName,
44
44
  options: {
45
45
  keyGenerator: saveAndReturn?.keyGenerator,
46
- sessionHydrator: saveAndReturn?.sessionHydrator,
47
- sessionPersister: saveAndReturn?.sessionPersister
46
+ sessionHydrator: saveAndReturn?.sessionHydrator
48
47
  }
49
48
  })
50
49
 
@@ -71,7 +71,7 @@ function makeGetHandler(
71
71
  )
72
72
 
73
73
  // @ts-expect-error - function signature will be refactored in the next iteration of the formatter
74
- const payload = format(items, model, undefined, undefined)
74
+ const payload = format(context, items, model, undefined, undefined)
75
75
  const opts = { payload }
76
76
 
77
77
  if (preparePageEventRequestOptions) {
@@ -3,6 +3,7 @@ import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
3
3
  import { type DetailItem } from '~/src/server/plugins/engine/models/types.js'
4
4
  import { getFormatter } from '~/src/server/plugins/engine/outputFormatters/index.js'
5
5
  import { submit } from '~/src/server/plugins/engine/services/notifyService.js'
6
+ import { type FormContext } from '~/src/server/plugins/engine/types.js'
6
7
  import {
7
8
  FormStatus,
8
9
  type FormRequestPayload
@@ -36,6 +37,7 @@ describe('notifyService', () => {
36
37
  } as unknown as FormRequestPayload)
37
38
  let model: FormModel
38
39
  const sendNotificationMock = jest.mocked(sendNotification)
40
+ const formContext = {} as FormContext
39
41
 
40
42
  beforeEach(() => {
41
43
  jest.resetAllMocks()
@@ -58,7 +60,14 @@ describe('notifyService', () => {
58
60
  })
59
61
  jest.mocked(getFormatter).mockReturnValue(() => 'dummy-live')
60
62
 
61
- await submit(mockRequest, model, 'test@defra.gov.uk', items, submitResponse)
63
+ await submit(
64
+ formContext,
65
+ mockRequest,
66
+ model,
67
+ 'test@defra.gov.uk',
68
+ items,
69
+ submitResponse
70
+ )
62
71
 
63
72
  expect(sendNotificationMock).toHaveBeenCalledWith(
64
73
  expect.objectContaining({
@@ -87,7 +96,14 @@ describe('notifyService', () => {
87
96
  })
88
97
  jest.mocked(getFormatter).mockReturnValue(() => 'dummy-preview')
89
98
 
90
- await submit(mockRequest, model, 'test@defra.gov.uk', items, submitResponse)
99
+ await submit(
100
+ formContext,
101
+ mockRequest,
102
+ model,
103
+ 'test@defra.gov.uk',
104
+ items,
105
+ submitResponse
106
+ )
91
107
 
92
108
  expect(sendNotificationMock).toHaveBeenCalledWith(
93
109
  expect.objectContaining({
@@ -118,7 +134,14 @@ describe('notifyService', () => {
118
134
  .mocked(getFormatter)
119
135
  .mockReturnValue(() => 'dummy-preview " Hello world \' !@/')
120
136
 
121
- await submit(mockRequest, model, 'test@defra.gov.uk', items, submitResponse)
137
+ await submit(
138
+ formContext,
139
+ mockRequest,
140
+ model,
141
+ 'test@defra.gov.uk',
142
+ items,
143
+ submitResponse
144
+ )
122
145
 
123
146
  expect(sendNotificationMock).toHaveBeenCalledWith(
124
147
  expect.objectContaining({
@@ -6,12 +6,14 @@ import { checkFormStatus } from '~/src/server/plugins/engine/helpers.js'
6
6
  import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
7
7
  import { type DetailItem } from '~/src/server/plugins/engine/models/types.js'
8
8
  import { getFormatter } from '~/src/server/plugins/engine/outputFormatters/index.js'
9
+ import { type FormContext } from '~/src/server/plugins/engine/types.js'
9
10
  import { type FormRequestPayload } from '~/src/server/routes/types.js'
10
11
  import { sendNotification } from '~/src/server/utils/notify.js'
11
12
 
12
13
  const templateId = config.get('notifyTemplateId')
13
14
 
14
15
  export async function submit(
16
+ context: FormContext,
15
17
  request: FormRequestPayload,
16
18
  model: FormModel,
17
19
  emailAddress: string,
@@ -33,7 +35,7 @@ export async function submit(
33
35
  const outputVersion = model.def.output?.version ?? '1'
34
36
 
35
37
  const outputFormatter = getFormatter(outputAudience, outputVersion)
36
- let body = outputFormatter(items, model, submitResponse, formStatus)
38
+ let body = outputFormatter(context, items, model, submitResponse, formStatus)
37
39
 
38
40
  // GOV.UK Notify transforms quotes into curly quotes, so we can't just send the raw payload
39
41
  // This is logic specific to Notify, so we include the logic here rather than in the formatter
@@ -370,7 +370,6 @@ export interface PluginOptions {
370
370
  keyGenerator: (request: RequestType) => string
371
371
  sessionHydrator: (request: RequestType) => Promise<FormSubmissionState>
372
372
  sessionPersister: (
373
- key: string,
374
373
  state: FormSubmissionState,
375
374
  request: RequestType
376
375
  ) => Promise<void>
@@ -136,7 +136,7 @@ describe('CacheService', () => {
136
136
  })
137
137
 
138
138
  describe('setState', () => {
139
- it('should set state with correct TTL', async () => {
139
+ it('should set state with correct TTL and return updated state', async () => {
140
140
  const mockRequest = {
141
141
  yar: { id: 'some-session' },
142
142
  params: { state: 'form1', slug: 'page1' }
@@ -146,7 +146,12 @@ describe('CacheService', () => {
146
146
 
147
147
  jest.spyOn(config, 'get').mockReturnValue(mockTTL)
148
148
 
149
- await cacheService.setState(mockRequest, mockState)
149
+ // Mock getState to return the updated state after set
150
+ jest.spyOn(cacheService, 'getState').mockResolvedValue(mockState)
151
+
152
+ await expect(
153
+ cacheService.setState(mockRequest, mockState)
154
+ ).resolves.toEqual(mockState)
150
155
 
151
156
  expect(mockCache.set).toHaveBeenCalledWith(
152
157
  {
@@ -156,6 +161,7 @@ describe('CacheService', () => {
156
161
  mockState,
157
162
  mockTTL
158
163
  )
164
+ expect(cacheService.getState).toHaveBeenCalledWith(mockRequest)
159
165
  })
160
166
  })
161
167
 
@@ -30,12 +30,6 @@ export class CacheService {
30
30
  request: Request | FormRequest | FormRequestPayload
31
31
  ) => Promise<FormSubmissionState | null>
32
32
 
33
- customPersister?: (
34
- key: string,
35
- state: FormSubmissionState,
36
- request: Request | FormRequest | FormRequestPayload
37
- ) => Promise<void>
38
-
39
33
  logger: Server['logger']
40
34
 
41
35
  constructor({
@@ -52,14 +46,9 @@ export class CacheService {
52
46
  sessionHydrator?: (
53
47
  request: Request | FormRequest | FormRequestPayload
54
48
  ) => Promise<FormSubmissionState | null>
55
- sessionPersister?: (
56
- key: string,
57
- state: FormSubmissionState,
58
- request: Request | FormRequest | FormRequestPayload
59
- ) => Promise<void>
60
49
  }
61
50
  }) {
62
- const { keyGenerator, sessionHydrator, sessionPersister } = options ?? {}
51
+ const { keyGenerator, sessionHydrator } = options ?? {}
63
52
  if (!cacheName) {
64
53
  server.log(
65
54
  'warn',
@@ -68,7 +57,6 @@ export class CacheService {
68
57
  }
69
58
  this.generateKey = keyGenerator ?? this.defaultKeyGenerator.bind(this)
70
59
  this.customFetcher = sessionHydrator ?? undefined
71
- this.customPersister = sessionPersister ?? undefined
72
60
  this.cache = server.cache({ cache: cacheName, segment: 'formSubmission' })
73
61
  this.logger = server.logger
74
62
  }
@@ -9,6 +9,7 @@ import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
9
9
  import { type DetailItem } from '~/src/server/plugins/engine/models/types.js'
10
10
  import { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'
11
11
  import {
12
+ type FormContext,
12
13
  type OnRequestCallback,
13
14
  type PluginOptions,
14
15
  type PreparePageEventRequestOptions
@@ -53,6 +54,7 @@ export interface RouteConfig {
53
54
 
54
55
  export interface OutputService {
55
56
  submit: (
57
+ context: FormContext,
56
58
  request: FormRequestPayload,
57
59
  model: FormModel,
58
60
  emailAddress: string,