diff --git a/package-lock.json b/package-lock.json index 4d4c8da14..ae153cb9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "SEE LICENSE IN LICENSE", "dependencies": { "@aws-sdk/client-sns": "^3.1079.0", - "@defra/forms-engine-plugin": "^5.0.0-alpha.21", + "@defra/forms-engine-plugin": "^5.0.0-alpha.22", "@defra/forms-model": "^3.0.704", "@defra/hapi-tracing": "^1.30.0", "@elastic/ecs-pino-format": "^1.5.0", @@ -2492,9 +2492,9 @@ } }, "node_modules/@defra/forms-engine-plugin": { - "version": "5.0.0-alpha.21", - "resolved": "https://registry.npmjs.org/@defra/forms-engine-plugin/-/forms-engine-plugin-5.0.0-alpha.21.tgz", - "integrity": "sha512-rIh67XQNAD/5i/yHQIF+G0y+NxHLyYNeGn+qK2H7MxnHFZBSara/QnCrG0nwexuwOETpmxarZRaPsd2AQxtMjw==", + "version": "5.0.0-alpha.22", + "resolved": "https://registry.npmjs.org/@defra/forms-engine-plugin/-/forms-engine-plugin-5.0.0-alpha.22.tgz", + "integrity": "sha512-RvBiWL66qi8ahtainewL2dTFf4yBtpsZo0QHjvAgjupOl7cSse7J8FXLY/sbl3+yx9NeFUdyUHMMK/3uDStESA==", "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { diff --git a/package.json b/package.json index 3bd460389..d5fa7f5e2 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "license": "SEE LICENSE IN LICENSE", "dependencies": { "@aws-sdk/client-sns": "^3.1079.0", - "@defra/forms-engine-plugin": "^5.0.0-alpha.21", + "@defra/forms-engine-plugin": "^5.0.0-alpha.22", "@defra/forms-model": "^3.0.704", "@defra/hapi-tracing": "^1.30.0", "@elastic/ecs-pino-format": "^1.5.0", diff --git a/src/server/index.test.ts b/src/server/index.test.ts index c16564fef..de66221ab 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -5,6 +5,7 @@ import { StatusCodes } from 'http-status-codes' import { FORM_PREFIX } from '~/src/server/constants.js' import { configureEnginePlugin, createServer } from '~/src/server/index.js' import { + generateReferenceNumber, getFormDefinition, getFormMetadata } from '~/src/server/services/formsService.js' @@ -31,6 +32,7 @@ describe('Model cache', () => { beforeEach(() => { jest.mocked(getFormMetadata).mockResolvedValue(fixtures.form.metadata) + jest.mocked(generateReferenceNumber).mockResolvedValue('XXX-XXX-XXX') server.app.models.clear() }) diff --git a/src/server/index.ts b/src/server/index.ts index caeee20a4..cddb872a2 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -191,7 +191,8 @@ export const configureEnginePlugin = async ({ }, ordnanceSurveyApiKey: config.get('ordnanceSurveyApiKey'), ordnanceSurveyApiSecret: config.get('ordnanceSurveyApiSecret'), - getLanguage: resolveLanguage + getLanguage: resolveLanguage, + generateReferenceNumber: formsService.generateReferenceNumber } } const routeOptions = { diff --git a/src/server/services/formsService.js b/src/server/services/formsService.js index 4926eb709..a1aea64ef 100644 --- a/src/server/services/formsService.js +++ b/src/server/services/formsService.js @@ -113,6 +113,27 @@ export async function validateSaveAndExitCredentials( return results } +/** + * Generates a unique reference number + * @param {string} [prefix] - the prefix + */ +export async function generateReferenceNumber(prefix) { + const postJsonByType = + /** @type {typeof postJson} */ (postJson) + + const query = prefix ? `?prefix=${prefix}` : '' + const { payload: results } = await postJsonByType( + `${submissionUrl}/submission/generate-reference-number${query}`, + { payload: {}, timeout: 10 * 1000 } // 10 seconds + ) + + if (!results) { + throw new Error('Unexpected empty response in generateReferenceNumber') + } + + return results.referenceNumber +} + /** * Retrieves a form secret and decrypts the value * @param {string} formId - the id of the form @@ -130,5 +151,5 @@ export async function getFormSecret(formId, secretName) { /** * @import { FormDefinition, FormMetadata } from '@defra/forms-model' - * @import { SaveAndExitDetails, SaveAndExitResumeDetails } from '~/src/server/types.js' + * @import { GenerateReferenceNumber, SaveAndExitDetails, SaveAndExitResumeDetails } from '~/src/server/types.js' */ diff --git a/src/server/services/formsService.test.js b/src/server/services/formsService.test.js index 5423110ec..f106b0d30 100644 --- a/src/server/services/formsService.test.js +++ b/src/server/services/formsService.test.js @@ -2,6 +2,7 @@ import { FormStatus } from '@defra/forms-engine-plugin/types' import { StatusCodes } from 'http-status-codes' import { + generateReferenceNumber, getFormDefinition, getFormMetadata, getFormMetadataById, @@ -217,6 +218,56 @@ describe('Forms service', () => { }) }) + describe('generateReferenceNumber', () => { + it('returns the generated reference number', async () => { + jest.mocked(postJson).mockResolvedValue({ + res: /** @type {IncomingMessage} */ ({ + statusCode: StatusCodes.OK + }), + payload: { referenceNumber: 'XXX-XXX-XXX' } + }) + + const referenceNumber = await generateReferenceNumber() + + expect(referenceNumber).toBe('XXX-XXX-XXX') + expect(postJson).toHaveBeenCalledWith( + `${SUBMISSION_URL}/submission/generate-reference-number`, + { payload: {}, timeout: 10 * 1000 } // 10 seconds + ) + }) + + it('returns the generated reference number with prefix', async () => { + jest.mocked(postJson).mockResolvedValue({ + res: /** @type {IncomingMessage} */ ({ + statusCode: StatusCodes.OK + }), + payload: { referenceNumber: 'XYZ-XXX-XXX' } + }) + + const referenceNumber = await generateReferenceNumber('XYZ') + + expect(referenceNumber).toBe('XYZ-XXX-XXX') + expect(postJson).toHaveBeenCalledWith( + `${SUBMISSION_URL}/submission/generate-reference-number?prefix=XYZ`, + { payload: {}, timeout: 10 * 1000 } // 10 seconds + ) + }) + + it('throws if no results', async () => { + // @ts-expect-error - partial mock of payload + jest.mocked(postJson).mockResolvedValue({ + res: /** @type {IncomingMessage} */ ({ + statusCode: StatusCodes.OK + }), + payload: undefined + }) + + await expect(() => generateReferenceNumber()).rejects.toThrow( + 'Unexpected empty response in generateReferenceNumber' + ) + }) + }) + describe('getFormSecret', () => { beforeEach(() => { // @ts-expect-error - mock fetch diff --git a/src/server/types.ts b/src/server/types.ts index 26b6c24f1..c0529eff9 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -70,3 +70,7 @@ export interface SaveAndExitDetails { export interface SaveAndExitResumeDetails extends SaveAndExitDetails { validPassword: boolean } + +export interface GenerateReferenceNumber { + referenceNumber: string +} diff --git a/test/form/confirmation-email.test.js b/test/form/confirmation-email.test.js index 155af1a54..08348329e 100644 --- a/test/form/confirmation-email.test.js +++ b/test/form/confirmation-email.test.js @@ -7,7 +7,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/services/formsService.js' +import { + generateReferenceNumber, + getFormMetadata +} from '~/src/server/services/formsService.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' @@ -99,6 +102,7 @@ describe('Confirmation email', () => { // server.app.models.clear() jest.clearAllMocks() jest.mocked(getFormMetadata).mockResolvedValue(fixtures.form.metadata) + jest.mocked(generateReferenceNumber).mockResolvedValue('XXX-XXX-XXX') }) afterAll(async () => { diff --git a/test/form/cookies.test.js b/test/form/cookies.test.js index 077a1b6cb..898b635a0 100644 --- a/test/form/cookies.test.js +++ b/test/form/cookies.test.js @@ -5,7 +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/services/formsService.js' +import { + generateReferenceNumber, + getFormMetadata +} from '~/src/server/services/formsService.js' import * as fixtures from '~/test/fixtures/index.js' import { renderResponse } from '~/test/helpers/component-helpers.js' import { getCookieHeader } from '~/test/utils/get-cookie.js' @@ -18,6 +21,7 @@ describe(`Cookie banner and analytics`, () => { beforeEach(() => { jest.mocked(getFormMetadata).mockResolvedValue(fixtures.form.metadata) + jest.mocked(generateReferenceNumber).mockResolvedValue('XXX-XXX-XXX') }) afterEach(async () => { diff --git a/test/form/phase-banner.test.js b/test/form/phase-banner.test.js index 31750483b..bbee78db1 100644 --- a/test/form/phase-banner.test.js +++ b/test/form/phase-banner.test.js @@ -4,7 +4,10 @@ import { within } from '@testing-library/dom' import { FORM_PREFIX } from '~/src/server/constants.js' import { createServer } from '~/src/server/index.js' -import { getFormMetadata } from '~/src/server/services/formsService.js' +import { + generateReferenceNumber, + getFormMetadata +} from '~/src/server/services/formsService.js' import * as fixtures from '~/test/fixtures/index.js' import { renderResponse } from '~/test/helpers/component-helpers.js' jest.mock('~/src/server/services/formsService.js') @@ -19,6 +22,7 @@ describe(`Phase banner`, () => { beforeEach(() => { jest.mocked(getFormMetadata).mockResolvedValue(fixtures.form.metadata) + jest.mocked(generateReferenceNumber).mockResolvedValue('XXX-XXX-XXX') }) test('shows the server phase tag by default', async () => { diff --git a/test/form/user-feedback.test.js b/test/form/user-feedback.test.js index f5172cc4d..f2a581908 100644 --- a/test/form/user-feedback.test.js +++ b/test/form/user-feedback.test.js @@ -8,6 +8,7 @@ import { StatusCodes } from 'http-status-codes' import { FORM_PREFIX } from '~/src/server/constants.js' import { createServer } from '~/src/server/index.js' import { + generateReferenceNumber, getFormMetadata, getFormMetadataById } from '~/src/server/services/formsService.js' @@ -105,6 +106,8 @@ describe('User feedback journey', () => { beforeAll(async () => { jest.mocked(getFormMetadataById).mockResolvedValue(metadata) jest.mocked(getFormMetadata).mockResolvedValue(metadata) + jest.mocked(generateReferenceNumber).mockResolvedValue('XXX-XXX-XXX') + server = await createServer({ formFileName: 'user-feedback-with-custom-controller.js', formFilePath: join(import.meta.dirname, 'definitions'),