Skip to content

Commit 38bc1da

Browse files
authored
fix/df-870: Redirects to latest link when old link (#1147)
* Redirects to latest link when old link * Extra coverage * Change message content + fixed test * Removed custom property
1 parent e12e279 commit 38bc1da

4 files changed

Lines changed: 101 additions & 18 deletions

File tree

src/server/models/save-and-exit.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,8 +543,20 @@ export function resumeSuccessViewModel(form, status) {
543543
* @property {string} securityAnswer - the security answer
544544
*/
545545

546+
/**
547+
* @typedef {object} CustomErrorPayload
548+
* @property {{ latestId?: string }} [custom] - custom payload
549+
*/
550+
551+
/**
552+
* @typedef {object} BoomErrorCustomSaveAndExit
553+
* @property {{ statusCode?: StatusCodes }} [output] - contains status code
554+
* @property {{ payload?: { latestId?: string }}} [data] - custom payload for save-and-exit
555+
*/
556+
546557
/**
547558
* @import { FormMetadata } from '@defra/forms-model'
559+
* @import { StatusCodes } from 'http-status-codes'
548560
* @import { FormStatus } from '@defra/forms-engine-plugin/types'
549561
* @import { SaveAndExitResumeDetails } from '~/src/server/types.js'
550562
*/

src/server/routes/save-and-exit.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,22 @@ export default [
270270
throw new Error('No link found')
271271
}
272272
} catch (err) {
273+
const error = /** @type {BoomErrorCustomSaveAndExit} */ (err)
274+
if (error.output?.statusCode === StatusCodes.GONE) {
275+
const latestLinkId = error.data?.payload?.latestId
276+
if (latestLinkId) {
277+
logger.info(
278+
`Old link ${magicLinkId} used but redirected to ${latestLinkId}`
279+
)
280+
return h
281+
.redirect(`/resume-form/${formId}/${latestLinkId}`)
282+
.code(StatusCodes.SEE_OTHER)
283+
} else {
284+
return h
285+
.redirect(`${ERROR_BASE_URL}/${form.slug}`)
286+
.code(StatusCodes.SEE_OTHER)
287+
}
288+
}
273289
logger.error(
274290
err,
275291
`Invalid magic link id ${magicLinkId} with form id ${formId}`
@@ -497,5 +513,5 @@ export default [
497513
/**
498514
* @import { ServerRoute } from '@hapi/hapi'
499515
* @import { FormPayload } from '@defra/forms-engine-plugin/engine/types.js'
500-
* @import { SaveAndExitParams, SaveAndExitPayload, SaveAndExitResumePasswordPayload, SaveAndExitResumePasswordParams } from '~/src/server/models/save-and-exit.js'
516+
* @import { BoomErrorCustomSaveAndExit, SaveAndExitParams, SaveAndExitPayload, SaveAndExitResumePasswordPayload, SaveAndExitResumePasswordParams } from '~/src/server/models/save-and-exit.js'
501517
*/

src/server/routes/save-and-exit.test.js

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Boom from '@hapi/boom'
12
import { StatusCodes } from 'http-status-codes'
23

34
import { createJoiError } from '~/src/server/helpers/error-helper.js'
@@ -33,7 +34,7 @@ describe('Save-and-exit check routes', () => {
3334
const MAGIC_LINK_ID = 'fd4e6453-fb32-43e4-b4cf-12b381a713de'
3435

3536
describe('GET /resume-form/{formId}/{magicLinkId}', () => {
36-
test('/route forwards correctly on success', async () => {
37+
test('route forwards correctly on success', async () => {
3738
jest
3839
.mocked(getFormMetadataById)
3940
// @ts-expect-error - allow partial objects for tests
@@ -59,7 +60,7 @@ describe('Save-and-exit check routes', () => {
5960
)
6061
})
6162

62-
test('/route forwards correctly on invalid form error', async () => {
63+
test('route forwards correctly on invalid form error', async () => {
6364
jest.mocked(getFormMetadataById).mockImplementationOnce(() => {
6465
throw new Error('form not found')
6566
})
@@ -82,7 +83,7 @@ describe('Save-and-exit check routes', () => {
8283
expect(response.headers.location).toBe('/resume-form-error')
8384
})
8485

85-
test('/route forwards correctly on magic link error', async () => {
86+
test('route forwards correctly on magic link error', async () => {
8687
jest
8788
.mocked(getFormMetadataById)
8889
// @ts-expect-error - allow partial objects for tests
@@ -104,7 +105,61 @@ describe('Save-and-exit check routes', () => {
104105
)
105106
})
106107

107-
test('/route forwards correctly on magic link error - wrong form id', async () => {
108+
test('route forwards correctly on magic link consumed but redirects to latest link', async () => {
109+
jest
110+
.mocked(getFormMetadataById)
111+
// @ts-expect-error - allow partial objects for tests
112+
.mockResolvedValueOnce({ slug: 'my-form-to-resume' })
113+
jest.mocked(getSaveAndExitDetails).mockImplementationOnce(() => {
114+
const boomError = Boom.resourceGone('magic link consumed')
115+
boomError.data = {
116+
payload: {
117+
latestId: 'latest-link-id'
118+
}
119+
}
120+
throw boomError
121+
})
122+
123+
const options = {
124+
method: 'GET',
125+
url: `/resume-form/${FORM_ID}/${MAGIC_LINK_ID}`
126+
}
127+
128+
const { response } = await renderResponse(server, options)
129+
130+
expect(response.statusCode).toBe(StatusCodes.SEE_OTHER)
131+
expect(response.headers.location).toBe(
132+
'/resume-form/eab6ac6c-79b6-439f-bd94-d93eb121b3f1/latest-link-id'
133+
)
134+
})
135+
136+
test('throws if trying to redirect to latest in group, but none found', async () => {
137+
jest
138+
.mocked(getFormMetadataById)
139+
// @ts-expect-error - allow partial objects for tests
140+
.mockResolvedValueOnce({ slug: 'my-form-to-resume' })
141+
jest.mocked(getSaveAndExitDetails).mockImplementationOnce(() => {
142+
const boomError = Boom.resourceGone('magic link consumed')
143+
boomError.output.payload.custom = {
144+
latestId: undefined
145+
}
146+
throw boomError
147+
})
148+
149+
const options = {
150+
method: 'GET',
151+
url: `/resume-form/${FORM_ID}/${MAGIC_LINK_ID}`
152+
}
153+
154+
const { response } = await renderResponse(server, options)
155+
156+
expect(response.statusCode).toBe(StatusCodes.SEE_OTHER)
157+
expect(response.headers.location).toBe(
158+
'/resume-form-error/my-form-to-resume'
159+
)
160+
})
161+
162+
test('route forwards correctly on magic link error - wrong form id', async () => {
108163
jest
109164
.mocked(getFormMetadataById)
110165
// @ts-expect-error - allow partial objects for tests
@@ -127,7 +182,7 @@ describe('Save-and-exit check routes', () => {
127182
)
128183
})
129184

130-
test('/route forwards correctly on magic link error 2', async () => {
185+
test('route forwards correctly on magic link error 2', async () => {
131186
jest
132187
.mocked(getFormMetadataById)
133188
// @ts-expect-error - allow partial objects for tests
@@ -149,7 +204,7 @@ describe('Save-and-exit check routes', () => {
149204
})
150205

151206
describe('GET /resume-form-verify/{formId}/{magicLinkId}/{slug}/state?}', () => {
152-
test('/route renders page', async () => {
207+
test('route renders page', async () => {
153208
jest
154209
.mocked(getFormMetadataById)
155210
// @ts-expect-error - allow partial objects for tests
@@ -178,7 +233,7 @@ describe('Save-and-exit check routes', () => {
178233
expect($mastheadHeading).toBeInTheDocument()
179234
})
180235

181-
test('/route forwards correctly on invalid form error', async () => {
236+
test('route forwards correctly on invalid form error', async () => {
182237
jest.mocked(getFormMetadataById).mockImplementationOnce(() => {
183238
throw new Error('form not found')
184239
})
@@ -201,7 +256,7 @@ describe('Save-and-exit check routes', () => {
201256
expect(response.headers.location).toBe('/resume-form-error')
202257
})
203258

204-
test('/route forwards correctly on magic link error', async () => {
259+
test('route forwards correctly on magic link error', async () => {
205260
jest
206261
.mocked(getFormMetadataById)
207262
// @ts-expect-error - allow partial objects for tests
@@ -222,7 +277,7 @@ describe('Save-and-exit check routes', () => {
222277
})
223278

224279
describe('GET /resume-form-error', () => {
225-
test('/route renders page without slug', async () => {
280+
test('route renders page without slug', async () => {
226281
const options = {
227282
method: 'GET',
228283
url: '/resume-form-error'
@@ -244,7 +299,7 @@ describe('Save-and-exit check routes', () => {
244299
expect($button).not.toBeInTheDocument()
245300
})
246301

247-
test('/route renders page with slug', async () => {
302+
test('route renders page with slug', async () => {
248303
const options = {
249304
method: 'GET',
250305
url: '/resume-form-error/my-slug'
@@ -269,7 +324,7 @@ describe('Save-and-exit check routes', () => {
269324
})
270325

271326
describe('GET /resume-form-success', () => {
272-
test('/route renders page without state', async () => {
327+
test('route renders page without state', async () => {
273328
jest
274329
.mocked(getFormMetadata)
275330
// @ts-expect-error - allow partial objects for tests
@@ -298,7 +353,7 @@ describe('Save-and-exit check routes', () => {
298353
expect($button).toHaveAttribute('href', '/form/my-form-to-resume/summary')
299354
})
300355

301-
test('/route renders page with slug', async () => {
356+
test('route renders page with slug', async () => {
302357
jest
303358
.mocked(getFormMetadata)
304359
// @ts-expect-error - allow partial objects for tests
@@ -332,7 +387,7 @@ describe('Save-and-exit check routes', () => {
332387
})
333388

334389
describe('/resume-form-verify/{formId}/{magicLinkId}/{slug}/{state?}', () => {
335-
test('/route handles invalid password', async () => {
390+
test('route handles invalid password', async () => {
336391
jest
337392
.mocked(getFormMetadataById)
338393
// @ts-expect-error - allow partial objects for tests
@@ -369,7 +424,7 @@ describe('Save-and-exit check routes', () => {
369424
)
370425
})
371426

372-
test('/route handles lockout', async () => {
427+
test('route handles lockout', async () => {
373428
jest
374429
.mocked(getFormMetadataById)
375430
// @ts-expect-error - allow partial objects for tests
@@ -408,7 +463,7 @@ describe('Save-and-exit check routes', () => {
408463
expect($errorMessage).toBeInTheDocument()
409464
})
410465

411-
test('/route handles missing password', async () => {
466+
test('route handles missing password', async () => {
412467
jest
413468
.mocked(getFormMetadataById)
414469
// @ts-expect-error - allow partial objects for tests
@@ -449,7 +504,7 @@ describe('Save-and-exit check routes', () => {
449504
expect(createJoiError).not.toHaveBeenCalled()
450505
})
451506

452-
test('/route handles missing password and invalid url', async () => {
507+
test('route handles missing password and invalid url', async () => {
453508
jest
454509
.mocked(getFormMetadataById)
455510
// @ts-expect-error - allow partial objects for tests

src/server/views/save-and-exit/resume-password.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
<h1 class="govuk-heading-l">{{ pageTitle }}</h1>
1919
<p class="govuk-body">
20-
Enter the answer to your security question to retrieve your information and continue with your form.
20+
Enter the most recent security answer to your security question to retrieve your information and continue with your form.
2121
</p>
2222
<p class="govuk-body">
2323
You have {{ attemptsLeft }} attempts to enter the correct answer.

0 commit comments

Comments
 (0)