Skip to content

Commit

Permalink
feat: GEO-1166 - added additional input validation for report rich te…
Browse files Browse the repository at this point in the history
…xt input (#848)
  • Loading branch information
banders authored Nov 20, 2024
1 parent be4c28e commit c1edbd6
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 0 deletions.
122 changes: 122 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"morgan": "^1.10.0",
"nconf": "^0.12.1",
"nocache": "^4.0.0",
"node-html-parser": "^6.1.13",
"papaparse": "^5.4.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
Expand Down
6 changes: 6 additions & 0 deletions backend/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ config.defaults({
retries: {
minTimeout: 1000,
},
reportRichText: {
maxParagraphs:
parseInt(process.env.REPORT_RICH_TEXT_MAX_PARAGRAPHS) || 100,
maxItemsPerList:
parseInt(process.env.REPORT_RICH_TEXT_MAX_ITEMS_PER_LIST) || 30,
},
},
oidc: {
adminKeycloakUrl: process.env.ADMIN_KEYCLOAK_URL,
Expand Down
57 changes: 57 additions & 0 deletions backend/src/v1/services/validate-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,63 @@ describe('validate-service', () => {
});

describe('validate-service-private', () => {
describe('validateRichText', () => {
describe('if rich text is valid', () => {
it('returns an empty list', () => {
const richText = `
<p>Some text</p>:
<ol>
<li>Item 1</li>
<li>Item 2</li>
</ol>
<p>More <strong>text</strong></p>.
`;
const fieldName = 'Employer Statement';
const errors = validateServicePrivate.validateRichText(
richText,
fieldName,
);
expect(errors).toStrictEqual([]);
});
});
describe('if rich text has too many paragraphs', () => {
it('returns an error', () => {
const maxParagraphs = config.get('server:reportRichText:maxParagraphs');
const richText = '<p></p>'.repeat(maxParagraphs + 1);
const fieldName = 'Employer Statement';
const errors = validateServicePrivate.validateRichText(
richText,
fieldName,
);
expect(errors.length).toBeGreaterThan(0);
const hasParagraphBreakError = doesAnyStringContainAll(errors, [
fieldName,
'paragraph breaks',
]);
expect(hasParagraphBreakError).toBeTruthy();
});
});
describe('if rich text has a list with too many bullet points', () => {
it('returns an error', () => {
const maxItemsPerList = config.get(
'server:reportRichText:maxItemsPerList',
);
const tooManyListItems = '<li></li>'.repeat(maxItemsPerList + 1);
const richText = `<ol>${tooManyListItems}</ol>`;
const fieldName = 'Employer Statement';
const errors = validateServicePrivate.validateRichText(
richText,
fieldName,
);
expect(errors.length).toBeGreaterThan(0);
const hasParagraphBreakError = doesAnyStringContainAll(errors, [
fieldName,
'list with more than the allowable number of items',
]);
expect(hasParagraphBreakError).toBeTruthy();
});
});
});
describe('validateOvertimePayAndHours', () => {
describe("if Overtime Pay is specified, but Overtime Hours isn't", () => {
it('returns an error', () => {
Expand Down
65 changes: 65 additions & 0 deletions backend/src/v1/services/validate-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LocalDate, TemporalAdjusters } from '@js-joda/core';
import { parse as htmlParse } from 'node-html-parser';
import { config } from '../../config';
import { JSON_REPORT_DATE_FORMAT } from '../../constants';
import { ISubmission } from './file-upload-service';
Expand Down Expand Up @@ -110,6 +111,20 @@ const validateService = {
`Start date and end date must always be 12 months apart.`,
);
}
const employerStatementErrors = validateServicePrivate.validateRichText(
submission.comments,
'Employer Statement',
);
if (employerStatementErrors?.length) {
bodyErrors.push(...employerStatementErrors);
}
const dataConstraintsErrors = validateServicePrivate.validateRichText(
submission.dataConstraints,
'Data Constraints',
);
if (dataConstraintsErrors?.length) {
bodyErrors.push(...dataConstraintsErrors);
}

const validReportingYears = this.getValidReportingYears();
if (!validReportingYears.includes(submission.reportingYear)) {
Expand Down Expand Up @@ -406,6 +421,56 @@ const validateService = {
};

export const validateServicePrivate = {
/**
* Validates that the given rich text meets meets the following requirements:
* - The content is not subdivided into more than the allowable number of paragraphs
* - If any lists exist, they shouldn't have more bullet points than the allowable amount.
* Returns a list of strings containing any validation error messages. If no validation
* errors were found, returns an empty list.
*/
validateRichText(richText: string, fieldName: string): string[] {
const errorMsgs = [];

const listTypes = ['ol', 'ul'];

if (richText) {
try {
const result = htmlParse(richText);
const numParagraphs = result.childNodes.length;

// Check that there are not too many paragraph breaks
// (because it is too resource-intensive for the doc-gen-service to split
// such content into multiple pages)
if (numParagraphs > config.get('server:reportRichText:maxParagraphs')) {
errorMsgs.push(
`'${fieldName}' contains ${numParagraphs} paragraph breaks which exceeds the limit of ${config.get('server:reportRichText:maxParagraphs')}.`,
);
}

// Check that lists don't have to many bullet points.
// (because the it would add complexity to the doc-gen-service to split long
// lists at page boundaries.)
result.childNodes.forEach((node) => {
if (listTypes.indexOf(node.rawTagName.toLowerCase()) >= 0) {
if (
node.childNodes.length >
config.get('server:reportRichText:maxItemsPerList')
) {
errorMsgs.push(
`'${fieldName}' contains a list with more than the allowable number of items (${config.get('server:reportRichText:maxItemsPerList')}).`,
);
}
}
});
} catch (e) {
//if parsing the HTML failed, return a not-very-specific error message
errorMsgs.push(`'${fieldName}' is not valid`);
}
}

return errorMsgs;
},

/*
Performs partial validation of the given record.
Only considers values of the Overtime Pay and Overtime Hours fields.
Expand Down

0 comments on commit c1edbd6

Please sign in to comment.