diff --git a/src/server/plugins/engine/components/CheckboxesField.ts b/src/server/plugins/engine/components/CheckboxesField.ts index c3c7579c6..a4068be42 100644 --- a/src/server/plugins/engine/components/CheckboxesField.ts +++ b/src/server/plugins/engine/components/CheckboxesField.ts @@ -25,25 +25,33 @@ export class CheckboxesField extends SelectionControlField { const { listType: type } = this const { options } = def - let formSchema = + const baseSchema = type === 'string' ? joi.array() : joi.array() const itemsSchema = joi[type]() .valid(...this.values) .label(this.label) - formSchema = formSchema + let formSchema = baseSchema .items(itemsSchema) .single() .label(this.label) .required() + .default([]) + + const stateSchema = baseSchema + .items(itemsSchema) + .label(this.label) + .required() + .default(null) + .allow(null) if (options.required === false) { formSchema = formSchema.optional() } - this.formSchema = formSchema.default([]) - this.stateSchema = formSchema.default(null).allow(null) + this.formSchema = formSchema + this.stateSchema = stateSchema this.options = options } diff --git a/test/condition/checkboxes.test.js b/test/condition/checkboxes.test.js index 4e3f943f3..e61371bc1 100644 --- a/test/condition/checkboxes.test.js +++ b/test/condition/checkboxes.test.js @@ -5,8 +5,10 @@ import { StatusCodes } from 'http-status-codes' import { FORM_PREFIX } from '~/src/server/constants.js' import { createServer } from '~/src/server/index.js' import { getFormMetadata } from '~/src/server/plugins/engine/services/formsService.js' +import { CacheService } from '~/src/server/services/cacheService.js' import * as fixtures from '~/test/fixtures/index.js' import { renderResponse } from '~/test/helpers/component-helpers.js' +import { getCookie, getCookieHeader } from '~/test/utils/get-cookie.js' const basePath = `${FORM_PREFIX}/checkboxes` const key = 'wqJmSf' @@ -16,6 +18,10 @@ jest.mock('~/src/server/plugins/engine/services/formsService.js') describe('Checkboxes based conditions', () => { /** @type {Server} */ let server + /** @type {string} */ + let csrfToken + /** @type {ReturnType} */ + let headers // Create server before each test beforeAll(async () => { @@ -25,6 +31,13 @@ describe('Checkboxes based conditions', () => { }) await server.initialize() + // Navigate to first page to establish a session and get CSRF + session cookies + const response = await server.inject({ + url: `${basePath}/first-page` + }) + + csrfToken = getCookie(response, 'crumb') + headers = getCookieHeader(response, ['session', 'crumb']) }) beforeEach(() => { @@ -79,12 +92,13 @@ describe('Checkboxes based conditions', () => { } }) - test('Testing POST /first-page with nothing checked redirects correctly', async () => { - const form = {} + test('Testing POST /first-page with nothing checked redirects correctly with single value', async () => { + const form = { crumb: csrfToken } const res = await server.inject({ url: `${basePath}/first-page`, method: 'POST', + headers, payload: form }) @@ -94,17 +108,76 @@ describe('Checkboxes based conditions', () => { test('Testing POST /first-page with "other" checked redirects correctly', async () => { const form = { + crumb: csrfToken, [key]: 'other' } + const setStateSpy = jest.spyOn(CacheService.prototype, 'setState') + const res = await server.inject({ url: `${basePath}/first-page`, method: 'POST', + headers, payload: form }) expect(res.statusCode).toBe(StatusCodes.SEE_OTHER) expect(res.headers.location).toBe(`${basePath}/third-page`) + + // Ensure the stored state contains an array for the checkbox key + expect(setStateSpy).toHaveBeenCalled() + const savedState = setStateSpy.mock.calls[0][1] + expect(Array.isArray(savedState[key])).toBe(true) + + setStateSpy.mockRestore() + }) + + test('Testing POST /first-page with "other" checked redirects correctly with multiple options', async () => { + const form = { + crumb: csrfToken, + [key]: ['other', 'shire'] + } + + const setStateSpy = jest.spyOn(CacheService.prototype, 'setState') + + const res = await server.inject({ + url: `${basePath}/first-page`, + method: 'POST', + headers, + payload: form + }) + + // Ensure the stored state contains an array for the checkbox key + expect(setStateSpy).toHaveBeenCalled() + const savedState = setStateSpy.mock.calls[0][1] + expect(Array.isArray(savedState[key])).toBe(true) + + expect(res.statusCode).toBe(StatusCodes.SEE_OTHER) + expect(res.headers.location).toBe(`${basePath}/third-page`) + }) + + test('DF-686 Test that when a radio question is changed to a checkbox question, the state validation forces the user to re-answer the question', async () => { + // Create a fresh session and extract cookies + const response = await server.inject({ url: `${basePath}/first-page` }) + const hdrs = getCookieHeader(response, ['session', 'crumb']) + + // Mock CacheService.getState to return a malformed value (string instead of array) + const malformedState = { [key]: 'other' } + const getStateSpy = jest + .spyOn(CacheService.prototype, 'getState') + .mockResolvedValueOnce(malformedState) + + // GET the summary - server should redirect back to first-page because value isn't an array + const res = await server.inject({ + url: `${basePath}/third-page`, + method: 'GET', + headers: hdrs + }) + + expect(res.statusCode).toBe(StatusCodes.MOVED_TEMPORARILY) + expect(res.headers.location).toBe(`${basePath}/first-page`) + + getStateSpy.mockRestore() }) })