Skip to content

Commit

Permalink
Merge pull request #106 from companieshouse/feature/add-psc-type-vali…
Browse files Browse the repository at this point in the history
…dation

Feature: Add validation for PSC Type screen
  • Loading branch information
hepsimo authored Nov 5, 2024
2 parents 9d70aee + 69ad541 commit 853e3f5
Show file tree
Hide file tree
Showing 11 changed files with 397 additions and 46 deletions.
5 changes: 5 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export const ExternalUrls = {
// POLICIES: env.POLICIES_LINK,
} as const;

// used as Session keys (Redis)
export const SessionKeys = {
COMPANY_NUMBER: "companyNumber"
} as const;

// For use by Matomo
export const Ids = {
BUTTON_ID_INDIVIDUAL_STATEMENT: "continue_button_ind_statement"
Expand Down
92 changes: 77 additions & 15 deletions src/lib/utils/error-manifests/errorManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,97 @@ const errorManifest = {
summary: "Your request contains validation errors",
inline: "Your request contains validation errors"
},
email: {
firstName: {
blank: {
summary: "Enter an email address",
inline: "Enter an email address"
summary: "Enter the first name",
inline: "Enter the first name"
},
incorrect: {
summary: "Email is not valid",
inline: "Enter an email address in the correct format, like name@example.com"
summary: "Enter a valid firstname",
inline: "Enter a valid firstname"
}
},
companyName: {
middleName: {
blank: {},
incorrect: {
summary: "Enter a valid middle name",
inline: "Enter a valid middle name"
}
},
lastName: {
blank: {
summary: "Enter the last name",
inline: "Enter the last name"
},
incorrect: {
summary: "Enter a valid last name",
inline: "Enter a valid last name"
}
},
dateOfBirth: {
blank: {
summary: "Enter the date of birth",
inline: "Enter the date of birth"
},
incorrect: {
summary: "Enter a valid date of birth",
inline: "Enter a valid date of birth"
}
},
pscType: {
blank: {
summary: "Select if you're providing verification details for a PSC or RLE",
inline: "Select if you're providing verification details for a PSC or RLE"
},
incorrect: {}
},
pscVerificationStatus: {
blank: {
summary: "Select the PSC you're providing verification details for",
inline: "Select the PSC you're providing verification details for"
},
incorrect: {}
},
relevantOfficerConfirmation: {
blank: {
summary: "Confirm if the relevant officer is a director of the relevant legal entity, or someone whose roles and responsibilities correspond to that of a company director.",
inline: "Confirm if the relevant officer is a director of the relevant legal entity, or someone whose roles and responsibilities correspond to that of a company director."
},
incorrect: {}
},
relevantOfficerPersonalCode: {
blank: {
summary: "Enter a company name",
inline: "Enter a company name"
summary: "Enter the personal code for the relevant officer",
inline: "Enter the personal code for the relevant officer"
},
incorrect: {
summary: "Company name is not valid",
inline: "Enter a valid company name"
summary: "Enter a valid personal code for the relevant officer",
inline: "Enter a valid personal code for the relevant officer"
}
},
description: {
pscPersonalCode: {
blank: {
summary: "Enter a company description",
inline: "Enter a company description"
summary: "Enter the personal code for the PSC",
inline: "Enter the personal code for the PSC"
},
incorrect: {
summary: "Company description is not valid",
inline: "Enter a valid company description; 120 max characters"
summary: "Enter a valid personal code for the PSC",
inline: "Enter a valid personal code for the PSC"
}
},
pscIdentityVerificationStatement: {
blank: {
summary: "Confirm if the identity verification statement is correct",
inline: "Confirm if the identity verification statement is correct"
},
incorrect: {}
},
relevantOfficerStatementConfirmation: {
blank: {
summary: "Summary error text for relevantOfficerStatementConfirmation to be provided",
inline: "Inline error text for relevantOfficerStatementConfirmation to be provided"
},
incorrect: {}
}
}
};
Expand Down
41 changes: 41 additions & 0 deletions src/lib/validation/form-validators/pscVerification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { logger } from "./../../logger";
import { GenericValidator } from "./../../validation/generic";

export class PscVerificationFormsValidator extends GenericValidator {

constructor (classParam?: string) {
super();
}

validatePscType (payload: any): Promise<Object> {
logger.info(`Request to validate PSC-Type form`);
try {
if (typeof payload.pscType === "undefined" || !this.isValidPscType(payload.pscType)) {
this.errors.stack.pscType = this.errorManifest.validation.pscType.blank;
}

// validate additional form fields here

if (!Object.keys(this.errors.stack).length) {
return Promise.resolve({});
} else {
return Promise.reject(this.errors);
}
} catch (err) {
this.errors.serverError = this.errorManifest.generic.serverError;
return Promise.reject(this.errors);
}
}

validateRleVerificationStatus (payload: any): Promise<any> {
return Promise.resolve({});
}

validateRelevantOfficerDetails (payload: any): Promise<any> {
return Promise.resolve({});
}

validateRelevantOfficerConfirmationStatements (payload: any): Promise<any> {
return Promise.resolve({});
}
};
72 changes: 72 additions & 0 deletions src/lib/validation/generic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Single field validators that are called by the form validators or "field-group" validators

import { logger } from "./../logger";
import errorManifest from "../utils/error-manifests/errorManifest";

export class GenericValidator {

errors: any;
payload: any;
errorManifest: any;

constructor () {
this.errors = this.getErrorSignature();
this.errorManifest = errorManifest;
}

protected getErrorSignature () {
return {
status: 400,
name: "VALIDATION_ERRORS",
message: errorManifest.validation.default.summary,
stack: {}
};
}

isValidPscType (pscType: string): boolean {
logger.info(`Request to validate PSC Type`);
// List of PSC types can be separated out into a util file
if (["individual", "rle"].includes(pscType)) {
return true;
}
return false;
}

isValidFirstName (firstName: string): boolean {
logger.info(`Request to validate email: ${firstName}`);
const regex = /^[a-z\d_-][a-z\d_\-.\s&]{1,71}$/ig;
if (regex.test(firstName)) {
return true;
}
return false;
}

isValidMiddleName (middleName: string): boolean {
logger.info(`Request to validate company name: ${middleName}`);
const regex = /^[a-z\d_-][a-z\d_\-.\s&]{1,71}$/ig;
if (regex.test(middleName)) {
return true;
}
return false;
}

isValidLastName (lastName: string): boolean {
logger.info(`Request to validate company name: ${lastName}`);
const regex = /^[a-z\d_-][a-z\d_\-.\s&]{1,71}$/ig;
if (regex.test(lastName)) {
return true;
}
return false;
}

isValidDateOfBirth (dob: string): boolean {
logger.info(`Request to validate PostCode: ${dob}`);

// use Luxon to validate DOB

if (dob) {
return true;
}
return false;
}
};
42 changes: 33 additions & 9 deletions src/routers/handlers/psc-type/pscTypeHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { getLocaleInfo, getLocalesService, selectLang } from "../../../utils/loc
import { BaseViewData, GenericHandler, ViewModel } from "../generic";
import { getUrlWithTransactionIdAndSubmissionId } from "../../../utils/url";
import { addSearchParams } from "../../../utils/queryParams";
interface PscTypeViewData extends BaseViewData { pscType: string }
import { PscVerificationFormsValidator } from "../../../lib/validation/form-validators/pscVerification";
interface PscTypeViewData extends BaseViewData { pscType: string, nextPageUrl: string }

export class PscTypeHandler extends GenericHandler<PscTypeViewData> {

Expand Down Expand Up @@ -48,17 +49,40 @@ export class PscTypeHandler extends GenericHandler<PscTypeViewData> {
};
}

public executePost (req: Request, res: Response): string {
const lang = selectLang(req.query.lang);
const selectedType = req.body.pscType;
public async executePost (req: Request, res: Response): Promise<ViewModel<PscTypeViewData>> {
const viewData = await this.getViewData(req, res);
try {
const lang = selectLang(req.query.lang);
const selectedType = req.body.pscType;

const queryParams = new URLSearchParams(req.url.split("?")[1]);
queryParams.set("lang", lang);
queryParams.set("pscType", selectedType);
const nextPageUrl = getUrlWithTransactionIdAndSubmissionId(selectPscType(selectedType), req.params.transactionId, req.params.submissionId);

viewData.pscType = req.query.pscType as string;
viewData.nextPageUrl = `${nextPageUrl}?${queryParams}`;

const queryParams = new URLSearchParams(req.url.split("?")[1]);
queryParams.set("lang", lang);
queryParams.set("pscType", selectedType);
const validator = new PscVerificationFormsValidator();

const nextPageUrl = getUrlWithTransactionIdAndSubmissionId(selectPscType(selectedType), req.params.transactionId, req.params.submissionId);
return `${nextPageUrl}?${queryParams}`;
viewData.errors = await validator.validatePscType(req.body);
await this.save(req.body);
} catch (err: any) {
logger.error(`${req.method} error: problem handling PSC type request: ${err.message}`);
viewData.errors = this.processHandlerException(err);
}

return {
templatePath: PscTypeHandler.templatePath,
viewData
};
}

// call API service to save data here
private save (payload: any): Object {
return Promise.resolve(true);
}

}

// TODO update default when error page available.
Expand Down
7 changes: 6 additions & 1 deletion src/routers/pscTypeRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ pscTypeRouter.get("/", handleExceptions(async (req: Request, res: Response, _nex

pscTypeRouter.post("/", handleExceptions(async (req: Request, res: Response, _next: NextFunction) => {
const handler = new PscTypeHandler();
res.redirect(handler.executePost(req, res));
const params = await handler.executePost(req, res);
if (!Object.keys(params.viewData.errors).length) {
res.redirect(params.viewData.nextPageUrl);
} else {
res.render(params.templatePath, params.viewData);
}
}));

export default pscTypeRouter;
6 changes: 5 additions & 1 deletion src/views/router_views/psc_type/psc_type.njk
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
{% set title = i18n.psc_type_title %}

{% block main_content %}

<form action="" method="post">
{% include "includes/csrf_token.njk" %}
{{ govukRadios({
<div class="govuk-form-group{{ ' govuk-form-group--error' if errors.pscType is not undefined }}">
{{ govukRadios({
idPrefix: "pscType",
name: "pscType",
value: pscType,
Expand All @@ -20,6 +22,7 @@
classes: "govuk-fieldset__legend--l"
}
},
errorMessage: errors.pscType.inline if errors.pscType,
items: [
{
value: "individual",
Expand All @@ -42,6 +45,7 @@
]
})
}}
</div>

{{ govukDetails({
summaryText: i18n.psc_type_details_title,
Expand Down
48 changes: 48 additions & 0 deletions test/routers/handlers/psc-type/pscType.int.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { HttpStatusCode } from "axios";
import * as cheerio from "cheerio";
import request from "supertest";
import middlewareMocks from "../../../mocks/allMiddleware.mock";
import app from "../../../../src/app";
import { PrefixedUrls } from "../../../../src/constants";
import { getUrlWithTransactionIdAndSubmissionId } from "../../../../src/utils/url";
import { CREATED_RESOURCE, PSC_VERIFICATION_ID, TRANSACTION_ID } from "../../../mocks/pscVerification.mock";
import { getPscVerification } from "../../../../src/services/pscVerificationService";
import { URLSearchParams } from "url";

jest.mock("../../../../src/services/pscVerificationService", () => ({
getPscVerification: () => ({
httpStatusCode: HttpStatusCode.Ok,
resource: CREATED_RESOURCE
})
}));

const mockGetPscVerification = getPscVerification as jest.Mock;

describe.skip("psc type view", () => {

beforeEach(() => {
middlewareMocks.mockSessionMiddleware.mockClear();
});

afterEach(() => {
expect(middlewareMocks.mockSessionMiddleware).toHaveBeenCalledTimes(1);
jest.clearAllMocks();
});

/**
* This test does not appear to be needed. Will skip it for now until there has been another discussion around it.
*/
it.each([[undefined, undefined], ["individual", "pscType=individual"], ["rle", "pscType=rle"]])("Should render the Psc Type page with a success status code and %s radio button checked", async (expectedSelection, expectedQuery) => {
const queryParams = new URLSearchParams(expectedQuery);
const uriWithQuery = `${PrefixedUrls.PSC_TYPE}?${queryParams}`;
const uri = getUrlWithTransactionIdAndSubmissionId(uriWithQuery, TRANSACTION_ID, PSC_VERIFICATION_ID);

const resp = await request(app).get(uri);

const $ = cheerio.load(resp.text);

expect(resp.status).toBe(HttpStatusCode.Ok);
expect($("input[name=pscType]:checked").val()).toBe(expectedSelection);
});

});
Loading

0 comments on commit 853e3f5

Please sign in to comment.