diff --git a/jest.config.js b/jest.config.js index 31a4ed114d..9159d3354b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,6 +2,7 @@ module.exports = { roots: ['/src/main'], testRegex: '(/src/test/.*|\\.test)\\.(ts|js)$', testEnvironment: 'node', + preset: "ts-jest/presets/js-with-ts", transform: { '^.+\\.ts$': 'ts-jest', }, @@ -13,4 +14,5 @@ module.exports = { coverageThreshold: { }, verbose: true, + transformIgnorePatterns: ['/node_modules/(?!node-emoji)'], }; diff --git a/package.json b/package.json index 5c5370573b..8236676da2 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "lodash": "^4.17.21", "multer": "^1.4.5-lts.1", "negotiator": "^0.6.2", + "node-emoji": "^2.2.0", "nunjucks": "^3.2.4", "otplib": "^12.0.1", "pcf-start": "^1.31.2", @@ -106,6 +107,7 @@ "query-string": "^7.1.1", "redis": "^3.1.2", "require-directory": "^2.1.1", + "sanitize-html": "^2.13.1", "semver": "^7.6.2", "serve-favicon": "^2.5.0", "session-file-store": "^1.5.0", diff --git a/src/main/app/case/definition.ts b/src/main/app/case/definition.ts index 24e564e2c8..8afc26c48b 100644 --- a/src/main/app/case/definition.ts +++ b/src/main/app/case/definition.ts @@ -2615,6 +2615,11 @@ export const C100OrderTypeKeyMapper = { }; export const AllowedFileExtentionList = ['jpg', 'jpeg', 'bmp', 'png', 'tif', 'tiff', 'pdf', 'doc', 'docx']; export const C100MaxFileSize = '20000000'; +export const MAX_DOCUMENT_LIMITS = { + SUPPORT_DOCUMENTS: 100, + DEFAULT: 20, + OTHER_DOCUMENTS: 100 +}; export interface C100OrderTypeInterface { childArrangementOrders?: C100OrderInterface[]; emergencyProtectionOrders?: C100OrderInterface[]; diff --git a/src/main/app/form/validation.test.ts b/src/main/app/form/validation.test.ts index 74c3f14d94..f650f51f6b 100644 --- a/src/main/app/form/validation.test.ts +++ b/src/main/app/form/validation.test.ts @@ -1,4 +1,5 @@ import { CaseDate } from '../case/case'; +import { MAX_DOCUMENT_LIMITS } from '../case/definition'; import { areDateFieldsFilledIn, @@ -11,6 +12,7 @@ import { isCaseCodeValid, isDateInputInvalid, isEmailValid, + isExceedingMaxDocuments, isFieldFilledIn, isFieldLetters, isFileSizeGreaterThanMaxAllowed, @@ -447,3 +449,40 @@ describe('isValidOption', () => { expect(invalidOption).toStrictEqual(undefined); }); }); + +describe('isExceedingMaxDocuments', () => { + test('should return true if totalDocumentsLength exceeds the max limit for SUPPORT_DOCUMENTS', () => { + const categoryKey = 'SUPPORT_DOCUMENTS'; + const totalDocumentsLength = MAX_DOCUMENT_LIMITS.SUPPORT_DOCUMENTS + 1; + const result = isExceedingMaxDocuments(totalDocumentsLength, categoryKey); + expect(result).toStrictEqual(true); + }); + + test('should return false if totalDocumentsLength does not exceed the max limit for SUPPORT_DOCUMENTS', () => { + const categoryKey = 'SUPPORT_DOCUMENTS'; + const totalDocumentsLength = MAX_DOCUMENT_LIMITS.SUPPORT_DOCUMENTS - 1; + const result = isExceedingMaxDocuments(totalDocumentsLength, categoryKey); + expect(result).toStrictEqual(false); + }); + + test('should return true if totalDocumentsLength exceeds the default max limit when categoryKey is unknown', () => { + const categoryKey = 'UNKNOWN_CATEGORY'; + const totalDocumentsLength = MAX_DOCUMENT_LIMITS.DEFAULT + 1; + const result = isExceedingMaxDocuments(totalDocumentsLength, categoryKey); + expect(result).toStrictEqual(true); + }); + + test('should return false if totalDocumentsLength does not exceed the default max limit when categoryKey is unknown', () => { + const categoryKey = 'UNKNOWN_CATEGORY'; + const totalDocumentsLength = MAX_DOCUMENT_LIMITS.DEFAULT - 1; + const result = isExceedingMaxDocuments(totalDocumentsLength, categoryKey); + expect(result).toStrictEqual(false); + }); + + test('should return false if totalDocumentsLength does not exceed the default max limit when categoryKey is default', () => { + const categoryKey = 'DEFAULT'; + const totalDocumentsLength = MAX_DOCUMENT_LIMITS.DEFAULT - 1; + const result = isExceedingMaxDocuments(totalDocumentsLength, categoryKey); + expect(result).toStrictEqual(false); + }); +}); diff --git a/src/main/app/form/validation.ts b/src/main/app/form/validation.ts index 7454dc9759..5daa6a7941 100644 --- a/src/main/app/form/validation.ts +++ b/src/main/app/form/validation.ts @@ -3,7 +3,7 @@ import customParseFormat from 'dayjs/plugin/customParseFormat'; import { validate as isValidEmail } from 'email-validator'; import { Case, CaseDate } from '../case/case'; -import { AllowedFileExtentionList, C100MaxFileSize, OtherName } from '../case/definition'; +import { AllowedFileExtentionList, C100MaxFileSize, MAX_DOCUMENT_LIMITS, OtherName } from '../case/definition'; dayjs.extend(customParseFormat); @@ -260,6 +260,11 @@ export const isValidFileFormat = (files: any): boolean => { return AllowedFileExtentionList.indexOf(extension) > -1; }; +export const isExceedingMaxDocuments = (totalDocumentsLength: number, categoryKey = 'DEFAULT'): boolean => { + const maxLimit = MAX_DOCUMENT_LIMITS[categoryKey] || MAX_DOCUMENT_LIMITS.DEFAULT; + return totalDocumentsLength >= maxLimit; +}; + export const isValidOption: Validator = value => { if ((value as string)?.trim() === '') { return ValidationError.NOT_SELECTED; diff --git a/src/main/modules/sanitize-request/index.test.ts b/src/main/modules/sanitize-request/index.test.ts new file mode 100644 index 0000000000..5d73cd7a64 --- /dev/null +++ b/src/main/modules/sanitize-request/index.test.ts @@ -0,0 +1,21 @@ +import { mockRequest } from '../../../test/unit/utils/mockRequest'; + +import { RequestSanitizer } from '.'; + +describe('sanitize request > index', () => { + describe('sanitizeRequestBody', () => { + test('should sanitize request body', () => { + const req = mockRequest({ body: { too_shortStatement: 'test ☕️' } }); + RequestSanitizer.sanitizeRequestBody(req); + expect(req.body).toEqual({ too_shortStatement: 'test' }); + }); + + test('should sanitize request body for arrays', () => { + const req = mockRequest({ + body: { courtProceedingsOrders: ['childArrangementOrder', 'supervisionOrder ☕️'] }, + }); + RequestSanitizer.sanitizeRequestBody(req); + expect(req.body).toEqual({ courtProceedingsOrders: ['childArrangementOrder', 'supervisionOrder'] }); + }); + }); +}); diff --git a/src/main/modules/sanitize-request/index.ts b/src/main/modules/sanitize-request/index.ts new file mode 100644 index 0000000000..199d23af51 --- /dev/null +++ b/src/main/modules/sanitize-request/index.ts @@ -0,0 +1,31 @@ +import _ from 'lodash'; +import { strip } from 'node-emoji'; +import sanitizeHtml from 'sanitize-html'; + +import { AppRequest } from '../../app/controller/AppRequest'; + +class SanitizeRequest { + private readonly formInputsToOmit = [ + '_csrf', + 'onlyContinue', + 'saveAndComeLater', + 'onlycontinue', + 'accessCodeCheck', + 'submit', + 'startNow', + 'goBack', + 'link', + ]; + + public sanitizeRequestBody(req: AppRequest): void { + const sanitizeText = _.flow([strip, sanitizeHtml, _.unescape, _.trim]); + + Object.entries(req.body) + .filter(([key]) => !this.formInputsToOmit.includes(key)) + .forEach(([key, value]) => { + req.body[key] = _.isArray(value) ? value.map(item => sanitizeText(item)) : sanitizeText(value); + }); + } +} + +export const RequestSanitizer = new SanitizeRequest(); diff --git a/src/main/routes.test.ts b/src/main/routes.test.ts index 9914936caf..f2a236906d 100644 --- a/src/main/routes.test.ts +++ b/src/main/routes.test.ts @@ -2,6 +2,8 @@ import { Application } from 'express'; import { RAProvider } from '../main/modules/reasonable-adjustments/index'; +import { mockRequest } from '../test/unit/utils/mockRequest'; +import { mockResponse } from '../test/unit/utils/mockResponse'; import { Routes } from './routes'; @@ -37,4 +39,16 @@ describe('Routes', () => { test('should setup routes', () => { expect(appMock.get).toHaveBeenCalledWith('/csrf-token-error', mockCSRFTokenError); }); + + test('should sanitize request body', () => { + const req = mockRequest({ body: { too_shortStatement: 'test ☕️' } }); + const res = mockResponse(); + const mockNext = jest.fn(); + const routes = new Routes(); + + routes.enableFor(appMock); + routes['sanitizeRequestBody'](req, res, mockNext); + + expect(req.body).toEqual({ too_shortStatement: 'test' }); + }); }); diff --git a/src/main/routes.ts b/src/main/routes.ts index d68d3ab5b0..99fd86bf9c 100644 --- a/src/main/routes.ts +++ b/src/main/routes.ts @@ -9,6 +9,7 @@ import { RespondentSubmitResponseController } from './app/controller/RespondentS import TSDraftController from './app/testingsupport/TSDraftController'; import { PaymentHandler, PaymentValidationHandler } from './modules/payments/paymentController'; import { RAProvider } from './modules/reasonable-adjustments'; +import { RequestSanitizer } from './modules/sanitize-request'; import { StepWithContent, getStepsWithContent, stepsWithContent } from './steps/'; import UploadDocumentController from './steps/application-within-proceedings/document-upload/postController'; import { routeGuard } from './steps/application-within-proceedings/routeGuard'; @@ -133,6 +134,7 @@ export class Routes { : step.postController ?? PostController; app.post( step.url, + this.sanitizeRequestBody.bind(this), // eslint-disable-next-line prettier/prettier this.routeGuard.bind(this, step, 'post'), errorHandler(new postController(step.form.fields).post) @@ -158,4 +160,9 @@ export class Routes { next(); } } + + private sanitizeRequestBody(req, res, next) { + RequestSanitizer.sanitizeRequestBody(req); + next(); + } } diff --git a/src/main/steps/application-within-proceedings/document-upload/content.test.ts b/src/main/steps/application-within-proceedings/document-upload/content.test.ts index 0dfceba7fa..bdd2f7385d 100644 --- a/src/main/steps/application-within-proceedings/document-upload/content.test.ts +++ b/src/main/steps/application-within-proceedings/document-upload/content.test.ts @@ -32,6 +32,7 @@ const en = { errors: { awpUploadApplicationForm: { required: `Upload your ${applicationType} application form`, + maxDocumentsReached: 'you have reached maximum number of documents that you can upload.', fileFormat: 'The file you uploaded is in the wrong format. Upload your file again in the correct format', fileSize: 'The file you uploaded is too large. Maximum file size allowed is 20MB', }, @@ -62,6 +63,7 @@ const cy: typeof en = { errors: { awpUploadApplicationForm: { required: `Mae’n rhaid i chi uwchlwytho eich ffurflen gais ${applicationType}`, + maxDocumentsReached: 'you have reached maximum number of documents that you can upload.', fileFormat: "Mae'r ffeil a lwythwyd gennych yn y fformat anghywir. Llwythwch eich ffeil eto yn y fformat cywir.", fileSize: "Mae'r ffeil yr ydych wedi ei llwytho yn rhy fawr", }, diff --git a/src/main/steps/application-within-proceedings/document-upload/content.ts b/src/main/steps/application-within-proceedings/document-upload/content.ts index 149ae03d3d..bcf567fc27 100644 --- a/src/main/steps/application-within-proceedings/document-upload/content.ts +++ b/src/main/steps/application-within-proceedings/document-upload/content.ts @@ -32,6 +32,7 @@ export const en = { errors: { awpUploadApplicationForm: { required: 'Upload your {applicationType} application form', + maxDocumentsReached: 'you have reached maximum number of documents that you can upload.', fileFormat: 'The file you uploaded is in the wrong format. Upload your file again in the correct format', fileSize: 'The file you uploaded is too large. Maximum file size allowed is 20MB', }, @@ -62,6 +63,7 @@ export const cy: typeof en = { errors: { awpUploadApplicationForm: { required: 'Mae’n rhaid i chi uwchlwytho eich ffurflen gais {applicationType}', + maxDocumentsReached: 'you have reached maximum number of documents that you can upload.', fileFormat: "Mae'r ffeil a lwythwyd gennych yn y fformat anghywir. Llwythwch eich ffeil eto yn y fformat cywir.", fileSize: "Mae'r ffeil yr ydych wedi ei llwytho yn rhy fawr", }, diff --git a/src/main/steps/application-within-proceedings/document-upload/postController.test.ts b/src/main/steps/application-within-proceedings/document-upload/postController.test.ts index 92c7a7cac2..ff2d4afad4 100644 --- a/src/main/steps/application-within-proceedings/document-upload/postController.test.ts +++ b/src/main/steps/application-within-proceedings/document-upload/postController.test.ts @@ -649,6 +649,105 @@ describe('Document upload controller', () => { expect(req.session.errors).toEqual(errors); }); + test('Should throw error if max documents limit is reached', async () => { + const mockForm = { + fields: { + field: { + type: 'file', + }, + }, + submit: { + text: l => l.continue, + }, + }; + const controller = new UploadDocumentController(mockForm.fields); + + const req = mockRequest({ + params: { + partyType: 'applicant', + applicationType: 'C2', + applicationReason: 'delay-or-cancel-hearing-date', + }, + session: { + userCase: { + awp_supportingDocuments: new Array(101).fill({}), + }, + }, + }); + req.files = { + awp_application_form: { name: 'file_example_TIFF.tiff', size: '100', data: '', mimetype: 'text' }, + }; + req.route.path = + '/:partyType/application-within-proceedings/:applicationType/:applicationReason/supporting-document-upload/:removeId?'; + const res = mockResponse(); + + await controller.post(req, res); + + expect(res.redirect).toHaveBeenCalledWith('/request'); + expect(req.session.errors).toEqual([ + { errorType: 'maxDocumentsReached', propertyName: 'awpUploadSupportingDocuments' }, + ]); + }); + + test('Should not throw error if max documents limit not reached', async () => { + const mockForm = { + fields: { + field: { + type: 'file', + }, + }, + submit: { + text: l => l.continue, + }, + }; + const controller = new UploadDocumentController(mockForm.fields); + + const req = mockRequest({ + params: { + partyType: 'applicant', + applicationType: 'C2', + applicationReason: 'delay-or-cancel-hearing-date', + }, + session: { + userCase: { + awp_supportingDocuments: new Array(15).fill({}), + }, + }, + }); + req.files = { + awp_application_form: { name: 'file_example_TIFF.tiff', size: '100', data: '', mimetype: 'text' }, + }; + req.route.path = + '/:partyType/application-within-proceedings/:applicationType/:applicationReason/supporting-document-upload/:removeId?'; + const res = mockResponse(); + mockedAxios.post.mockImplementation(url => { + switch (url) { + case '/upload-citizen-document': + return Promise.resolve({ + data: { + status: 'Success', + document: { + document_url: + 'http://dm-store-aat.service.core-compute-aat.internal/documents/c9f56483-6e2d-43ce-9de8-72661755b87c', + document_filename: 'file_example_TIFF_1MB.tiff', + document_binary_url: + 'http://dm-store-aat.service.core-compute-aat.internal/documents/c9f56483-6e2d-43ce-9de8-72661755b87c/binary', + }, + }, + }); + default: + return Promise.reject(new Error('not found')); + } + }); + + await controller.post(req, res); + + expect(res.redirect).toHaveBeenCalledWith( + '/applicant/application-within-proceedings/C2/delay-or-cancel-hearing-date/supporting-document-upload' + ); + expect(req.session.errors).toEqual([]); + }); + test('should redirect to correct page when continue pressed and file already uploaded', async () => { const mockForm = { fields: { diff --git a/src/main/steps/application-within-proceedings/document-upload/postController.ts b/src/main/steps/application-within-proceedings/document-upload/postController.ts index 07455857d8..4b4d79f673 100644 --- a/src/main/steps/application-within-proceedings/document-upload/postController.ts +++ b/src/main/steps/application-within-proceedings/document-upload/postController.ts @@ -9,7 +9,11 @@ import { DocumentUploadResponse } from '../../../app/case/definition'; import { AppRequest } from '../../../app/controller/AppRequest'; import { AnyObject, PostController } from '../../../app/controller/PostController'; import { FormFields, FormFieldsFn } from '../../../app/form/Form'; -import { isFileSizeGreaterThanMaxAllowed, isValidFileFormat } from '../../../app/form/validation'; +import { + isExceedingMaxDocuments, + isFileSizeGreaterThanMaxAllowed, + isValidFileFormat, +} from '../../../app/form/validation'; import { applyParms } from '../../../steps/common/url-parser'; import { APPLICATION_WITHIN_PROCEEDINGS_SUPPORTING_DOCUMENT_UPLOAD } from '../../../steps/urls'; @@ -102,6 +106,18 @@ export default class UploadDocumentController extends PostController propertyName: isSupportingDocuments ? 'awpUploadSupportingDocuments' : 'awpUploadApplicationForm', errorType: 'required', }); + } else if ( + isExceedingMaxDocuments( + isSupportingDocuments + ? req.session.userCase.awp_supportingDocuments?.length + : req.session.userCase.awp_uploadedApplicationForms?.length, + isSupportingDocuments ? 'SUPPORT_DOCUMENTS' : 'DEFAULT' + ) + ) { + this.handleError(req, res, { + propertyName: isSupportingDocuments ? 'awpUploadSupportingDocuments' : 'awpUploadApplicationForm', + errorType: 'maxDocumentsReached', + }); } else if (!isValidFileFormat({ documents: uploadedDocuments })) { this.handleError(req, res, { propertyName: isSupportingDocuments ? 'awpUploadSupportingDocuments' : 'awpUploadApplicationForm', diff --git a/src/main/steps/application-within-proceedings/supporting-document-upload/content.test.ts b/src/main/steps/application-within-proceedings/supporting-document-upload/content.test.ts index 9535d6bdb6..230468def2 100644 --- a/src/main/steps/application-within-proceedings/supporting-document-upload/content.test.ts +++ b/src/main/steps/application-within-proceedings/supporting-document-upload/content.test.ts @@ -30,6 +30,7 @@ const en = { errors: { awpUploadSupportingDocuments: { required: 'Upload a file', + maxDocumentsReached: 'you have reached maximum number of documents that you can upload.', fileFormat: 'The file you uploaded is in the wrong format. Upload your file again in the correct format', fileSize: 'The file you uploaded is too large. Maximum file size allowed is 20MB', }, @@ -60,6 +61,7 @@ const cy: typeof en = { errors: { awpUploadSupportingDocuments: { required: 'Llwytho ffeil', + maxDocumentsReached: 'you have reached maximum number of documents that you can upload.', fileFormat: "Mae'r ffeil a lwythwyd gennych yn y fformat anghywir. Llwythwch eich ffeil eto yn y fformat cywir.", fileSize: "Mae'r ffeil yr ydych wedi ei llwytho yn rhy fawr", }, diff --git a/src/main/steps/application-within-proceedings/supporting-document-upload/content.ts b/src/main/steps/application-within-proceedings/supporting-document-upload/content.ts index e108f52288..5c431ccd57 100644 --- a/src/main/steps/application-within-proceedings/supporting-document-upload/content.ts +++ b/src/main/steps/application-within-proceedings/supporting-document-upload/content.ts @@ -32,6 +32,7 @@ export const en = { errors: { awpUploadSupportingDocuments: { required: 'Upload a file', + maxDocumentsReached: 'you have reached maximum number of documents that you can upload.', fileFormat: 'The file you uploaded is in the wrong format. Upload your file again in the correct format', fileSize: 'The file you uploaded is too large. Maximum file size allowed is 20MB', }, @@ -62,6 +63,7 @@ export const cy: typeof en = { errors: { awpUploadSupportingDocuments: { required: 'Llwytho ffeil', + maxDocumentsReached: 'you have reached maximum number of documents that you can upload.', fileFormat: "Mae'r ffeil a lwythwyd gennych yn y fformat anghywir. Llwythwch eich ffeil eto yn y fformat cywir.", fileSize: "Mae'r ffeil yr ydych wedi ei llwytho yn rhy fawr", }, diff --git a/src/main/steps/c100-rebuild/miam/domestic-abuse/upload-evidence/content.test.ts b/src/main/steps/c100-rebuild/miam/domestic-abuse/upload-evidence/content.test.ts index f7e653f6b1..8bd59ef122 100644 --- a/src/main/steps/c100-rebuild/miam/domestic-abuse/upload-evidence/content.test.ts +++ b/src/main/steps/c100-rebuild/miam/domestic-abuse/upload-evidence/content.test.ts @@ -26,6 +26,7 @@ const en = { errors: { miam_domesticAbuseEvidenceDocs: { maxFileSize: 'The file you uploaded is too large. Maximum file size allowed is 20MB', + maxDocumentsReached: 'you have reached maximum number of documents that you can upload.', invalidFileFormat: 'The file you uploaded is in the wrong format. Upload your file again in the correct format', uploadError: 'Document could not be uploaded', deleteFile: 'Document could not be deleted', @@ -53,6 +54,7 @@ const cy = { errors: { miam_domesticAbuseEvidenceDocs: { maxFileSize: "Mae'r ffeil yr ydych wedi ei llwytho yn rhy fawr. Uchafswm maint y ffeil yw 20MB", + maxDocumentsReached: 'you have reached maximum number of documents that you can upload.', invalidFileFormat: "Mae'r ffeil a lwythwyd gennych yn y fformat anghywir. Llwythwch eich ffeil eto yn y fformat cywir.", uploadError: 'Document could not be uploaded - welsh', diff --git a/src/main/steps/c100-rebuild/miam/domestic-abuse/upload-evidence/content.ts b/src/main/steps/c100-rebuild/miam/domestic-abuse/upload-evidence/content.ts index 59770c783e..cb89117d61 100644 --- a/src/main/steps/c100-rebuild/miam/domestic-abuse/upload-evidence/content.ts +++ b/src/main/steps/c100-rebuild/miam/domestic-abuse/upload-evidence/content.ts @@ -26,6 +26,7 @@ const en = { errors: { miam_domesticAbuseEvidenceDocs: { maxFileSize: 'The file you uploaded is too large. Maximum file size allowed is 20MB', + maxDocumentsReached: 'you have reached maximum number of documents that you can upload.', invalidFileFormat: 'The file you uploaded is in the wrong format. Upload your file again in the correct format', uploadError: 'Document could not be uploaded', deleteFile: 'Document could not be deleted', @@ -53,6 +54,7 @@ const cy: typeof en = { errors: { miam_domesticAbuseEvidenceDocs: { maxFileSize: "Mae'r ffeil yr ydych wedi ei llwytho yn rhy fawr. Uchafswm maint y ffeil yw 20MB", + maxDocumentsReached: 'you have reached maximum number of documents that you can upload.', invalidFileFormat: "Mae'r ffeil a lwythwyd gennych yn y fformat anghywir. Llwythwch eich ffeil eto yn y fformat cywir.", uploadError: 'Document could not be uploaded - welsh', diff --git a/src/main/steps/c100-rebuild/miam/domestic-abuse/upload-evidence/postController.test.ts b/src/main/steps/c100-rebuild/miam/domestic-abuse/upload-evidence/postController.test.ts index 7c7e70404a..0c65d7c6ae 100644 --- a/src/main/steps/c100-rebuild/miam/domestic-abuse/upload-evidence/postController.test.ts +++ b/src/main/steps/c100-rebuild/miam/domestic-abuse/upload-evidence/postController.test.ts @@ -1,6 +1,7 @@ import { mockRequest } from '../../../../../../test/unit/utils/mockRequest'; import { mockResponse } from '../../../../../../test/unit/utils/mockResponse'; import { CaseApi } from '../../../../../app/case/CaseApi'; +import { isExceedingMaxDocuments } from '../../../../../app/form/validation'; import MIAMDomesticAbuseEvidenceUploadController from './postController'; @@ -166,4 +167,63 @@ describe('C100-rebuild > MIAM > domestic-abuse > upload-evidence > postControlle ]); expect(req.session.userCase.miam_domesticAbuseEvidenceDocs).toStrictEqual(undefined); }); + + test('should not set errorType if not exceeding max documents', async () => { + req.body = { onlyContinue: false }; + req.files = { miam_domesticAbuseEvidenceDocs: { name: 'test.pdf', size: 8123, data: '', mimetype: 'text' } }; + req.session.userCase.miam_domesticAbuseEvidenceDocs = new Array(18).fill({ + document_url: 'test2/1234', + document_binary_url: 'binary/test2/1234', + document_filename: 'test_document_2', + document_hash: '1234', + document_creation_date: '1/1/2024', + }); + + uploadDocumentMock.mockResolvedValue({ + status: 'Success', + document: { + document_url: 'test/1234', + document_binary_url: 'binary/test/1234', + document_filename: 'test_document', + document_hash: '1234', + document_creation_date: '1/1/2024', + }, + }); + await controller.post(req, res); + + expect(isExceedingMaxDocuments(req.session.userCase.miam_domesticAbuseEvidenceDocs.length)).toBe(false); + expect(res.redirect).toHaveBeenCalledWith('/dashboard'); + expect(req.session.errors).toStrictEqual([]); + }); + + test('should set errorType if exceeding max documents', async () => { + req.body = { onlyContinue: false }; + req.files = { miam_domesticAbuseEvidenceDocs: { name: 'test.pdf', size: 8123, data: '', mimetype: 'text' } }; + req.session.userCase.miam_domesticAbuseEvidenceDocs = new Array(21).fill({ + document_url: 'test2/1234', + document_binary_url: 'binary/test2/1234', + document_filename: 'test_document_2', + document_hash: '1234', + document_creation_date: '1/1/2024', + }); + + uploadDocumentMock.mockResolvedValue({ + status: 'Success', + document: { + document_url: 'test/1234', + document_binary_url: 'binary/test/1234', + document_filename: 'test_document', + document_hash: '1234', + document_creation_date: '1/1/2024', + }, + }); + await controller.post(req, res); + + expect(isExceedingMaxDocuments(req.session.userCase.miam_domesticAbuseEvidenceDocs.length)).toBe(true); + expect(res.redirect).toHaveBeenCalledWith('/request'); + expect(req.session.errors).toContainEqual({ + propertyName: 'miam_domesticAbuseEvidenceDocs', + errorType: 'maxDocumentsReached', + }); + }); }); diff --git a/src/main/steps/c100-rebuild/miam/domestic-abuse/upload-evidence/postController.ts b/src/main/steps/c100-rebuild/miam/domestic-abuse/upload-evidence/postController.ts index 1878b0bbe6..a66a2a315a 100644 --- a/src/main/steps/c100-rebuild/miam/domestic-abuse/upload-evidence/postController.ts +++ b/src/main/steps/c100-rebuild/miam/domestic-abuse/upload-evidence/postController.ts @@ -8,7 +8,11 @@ import { caseApi } from '../../../../../app/case/CaseApi'; import { AppRequest } from '../../../../../app/controller/AppRequest'; import { AnyObject, PostController } from '../../../../../app/controller/PostController'; import { FormFields, FormFieldsFn } from '../../../../../app/form/Form'; -import { isFileSizeGreaterThanMaxAllowed, isValidFileFormat } from '../../../../../app/form/validation'; +import { + isExceedingMaxDocuments, + isFileSizeGreaterThanMaxAllowed, + isValidFileFormat, +} from '../../../../../app/form/validation'; import { applyParms } from '../../../../common/url-parser'; import { C100_MIAM_UPLOAD_DA_EVIDENCE } from '../../../../urls'; import { handleEvidenceDocError, removeEvidenceDocErrors } from '../../util'; @@ -19,10 +23,12 @@ export default class MIAMDomesticAbuseEvidenceUploadController extends PostContr super(fields); } - private hasError(uploadedDocument: Record): string | undefined { + private hasError(uploadedDocument: Record, req: AppRequest): string | undefined { let errorType; - if (!isValidFileFormat({ documents: uploadedDocument })) { + if (isExceedingMaxDocuments(req.session.userCase.miam_domesticAbuseEvidenceDocs?.length ?? 0)) { + errorType = 'maxDocumentsReached'; + } else if (!isValidFileFormat({ documents: uploadedDocument })) { errorType = 'invalidFileFormat'; } else if (isFileSizeGreaterThanMaxAllowed({ documents: uploadedDocument })) { errorType = 'maxFileSize'; @@ -45,7 +51,7 @@ export default class MIAMDomesticAbuseEvidenceUploadController extends PostContr return super.redirect(req, res, C100_MIAM_UPLOAD_DA_EVIDENCE); } - const error = this.hasError(fileUploaded); + const error = this.hasError(fileUploaded, req); if (error) { handleEvidenceDocError(error, req, 'miam_domesticAbuseEvidenceDocs'); diff --git a/src/main/steps/common/documents/upload/upload-your-documents/content.test.ts b/src/main/steps/common/documents/upload/upload-your-documents/content.test.ts index 100bf56626..35fa0e4121 100644 --- a/src/main/steps/common/documents/upload/upload-your-documents/content.test.ts +++ b/src/main/steps/common/documents/upload/upload-your-documents/content.test.ts @@ -38,6 +38,7 @@ const en = { }, uploadDocumentFileUpload: { multipleFiles: 'You can upload only one document.', + maxDocumentsReached: 'you have reached maximum number of documents that you can upload.', noFile: 'Upload a file.', noStatementOrFile: 'Enter your statement or upload a file.', uploadError: 'Document could not be uploaded.', @@ -80,6 +81,7 @@ const cy: typeof en = { }, uploadDocumentFileUpload: { multipleFiles: 'Gallwch uwchlwytho un dogfen yn unig', + maxDocumentsReached: 'you have reached maximum number of documents that you can upload.', noFile: 'Uwchlwytho ffeil', noStatementOrFile: 'Rhowch eich datganiad neu llwythwch ffeil.', uploadError: 'Ni ellir uwchlwytho’r ddogfen.', diff --git a/src/main/steps/common/documents/upload/upload-your-documents/content.ts b/src/main/steps/common/documents/upload/upload-your-documents/content.ts index b1128d1f39..d63cc50776 100644 --- a/src/main/steps/common/documents/upload/upload-your-documents/content.ts +++ b/src/main/steps/common/documents/upload/upload-your-documents/content.ts @@ -46,6 +46,7 @@ const en = { noStatementOrFile: 'Enter your statement or upload a file.', noFile: 'Upload a file.', multipleFiles: 'You can upload only one document.', + maxDocumentsReached: 'you have reached maximum number of documents that you can upload.', uploadError: 'Document could not be uploaded.', deleteError: 'Document could not be deleted.', }, @@ -87,6 +88,7 @@ const cy: typeof en = { }, uploadDocumentFileUpload: { multipleFiles: 'Gallwch uwchlwytho un dogfen yn unig', + maxDocumentsReached: 'you have reached maximum number of documents that you can upload.', noFile: 'Uwchlwytho ffeil', noStatementOrFile: 'Rhowch eich datganiad neu llwythwch ffeil.', uploadError: 'Ni ellir uwchlwytho’r ddogfen.', diff --git a/src/main/steps/common/documents/upload/upload-your-documents/postController.test.ts b/src/main/steps/common/documents/upload/upload-your-documents/postController.test.ts index 0aebb96413..fd2e53088e 100644 --- a/src/main/steps/common/documents/upload/upload-your-documents/postController.test.ts +++ b/src/main/steps/common/documents/upload/upload-your-documents/postController.test.ts @@ -360,6 +360,89 @@ describe('documents > upload > upload-your-documents > postController', () => { }, ]); }); + + test('should not set error when not exceeding max documents', async () => { + const req = mockRequest({ + body: { + uploadFile: true, + documentDataRef: 'documentDataRef', + redirectUrl: '/some-redirect-url', + }, + params: { + docCategory: 'your-position-statements', + }, + session: { + user: { id: '1234' }, + userCase: { + id: '1234', + caseType: 'FL401', + applicantsFL401: { + firstName: 'test', + lastName: 'user', + }, + documentDataRef: new Array(18).fill({}), + }, + }, + }); + req.files = { + statementDocument: { name: 'file_example_TIFF_1MB.tiff', data: '', mimetype: 'text' }, + }; + uploadDocumentListFromCitizenMock.mockResolvedValue({ + status: 'Success', + document: { + document_url: 'test/1234', + document_binary_url: 'binary/test/1234', + document_filename: 'test_document', + document_hash: '1234', + document_creation_date: '1/1/2024', + }, + }); + const res = mockResponse(); + + const controller = new UploadDocumentPostController({}); + await controller.post(req, res); + await new Promise(process.nextTick); + expect(req.session.errors).toStrictEqual([]); + + expect(res.redirect).not.toHaveBeenCalledWith('/some-redirect-url'); + }); + + test('should set error when exceeding max documents', async () => { + const req = mockRequest({ + body: { + uploadFile: true, + }, + params: { + docCategory: 'your-position-statements', + }, + session: { + user: { id: '1234' }, + userCase: { + id: '1234', + caseType: 'FL401', + applicantsFL401: { + firstName: 'test', + lastName: 'user', + }, + applicantUploadFiles: new Array(21).fill({}), + }, + }, + }); + req.files = { + statementDocument: { name: 'file_example_TIFF_1MB.tiff', data: '', mimetype: 'text' }, + }; + const res = mockResponse(); + const controller = new UploadDocumentPostController({}); + + await controller.post(req, res); + await new Promise(process.nextTick); + expect(req.session.errors).toStrictEqual([ + { + errorType: 'maxDocumentsReached', + propertyName: 'uploadDocumentFileUpload', + }, + ]); + }); }); describe('submitDocuments', () => { diff --git a/src/main/steps/common/documents/upload/upload-your-documents/postController.ts b/src/main/steps/common/documents/upload/upload-your-documents/postController.ts index 4d34cfcfca..754553f47b 100644 --- a/src/main/steps/common/documents/upload/upload-your-documents/postController.ts +++ b/src/main/steps/common/documents/upload/upload-your-documents/postController.ts @@ -9,6 +9,7 @@ import { PartyType, YesOrNo } from '../../../../../app/case/definition'; import { AppRequest } from '../../../../../app/controller/AppRequest'; import { AnyObject, PostController } from '../../../../../app/controller/PostController'; import { Form, FormFields, FormFieldsFn } from '../../../../../app/form/Form'; +import { isExceedingMaxDocuments } from '../../../../../app/form/validation'; import { applyParms } from '../../../../../steps/common/url-parser'; import { getCasePartyType } from '../../../../../steps/prl-cases/dashboard/utils'; import { UPLOAD_DOCUMENT_UPLOAD_YOUR_DOCUMENTS } from '../../../../../steps/urls'; @@ -169,7 +170,6 @@ export default class UploadDocumentPostController extends PostController