Skip to content

Commit

Permalink
add validation-test page with unit/integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
harley-harris committed Jul 9, 2024
1 parent 46576a2 commit c4977a3
Show file tree
Hide file tree
Showing 19 changed files with 407 additions and 2 deletions.
4 changes: 3 additions & 1 deletion src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ export const SERVICE_NAME = 'Node Prototype';
export const LANDING_PAGE = 'info';
export const NOT_FOUND = 'page-not-found';
export const ERROR_PAGE = 'error';
export const VALIDATION_TEST = 'validation-test';

// Routing paths
export const ROOT_URL = '/';
export const LANDING_URL = '/info';

export const INFO_URL = '/info';
export const VALIDATION_TEST_URL = '/validation-test';
export const HEALTHCHECK_URL = '/healthcheck';
export const SERVICE_URL = `${BASE_URL}${LANDING_URL}`;
15 changes: 15 additions & 0 deletions src/controller/validation-test.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Request, Response } from 'express';
import { log } from '../utils/logger';
import * as config from '../config';

export const get = (_req: Request, res: Response) => {
return res.render(config.VALIDATION_TEST);
};

export const post = (req: Request, res: Response) => {
const firstName = req.body.first_name;

log.info(`First Name: ${firstName}`);

return res.redirect(config.LANDING_PAGE);
};
40 changes: 40 additions & 0 deletions src/middleware/validation.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { NextFunction, Request, Response } from 'express';
import { validationResult, FieldValidationError } from 'express-validator';

import { log } from '../utils/logger';
import { FormattedValidationErrors } from '../model/validation.model';

export const checkValidations = (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const errorList = validationResult(req);

if (!errorList.isEmpty()) {
const template_path = req.path.substring(1);
const errors = formatValidationError(
errorList.array() as FieldValidationError[]
);

log.info(`Validation error on ${template_path} page`);

return res.render(template_path, { ...req.body, errors });

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
}

return next();
} catch (err: any) {
log.error(err.message);

Check warning on line 28 in src/middleware/validation.middleware.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
next(err);

Check warning on line 29 in src/middleware/validation.middleware.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}
};

const formatValidationError = (errorList: FieldValidationError[]) => {
const errors = { errorList: [] } as FormattedValidationErrors;
errorList.forEach((e: FieldValidationError) => {
errors.errorList.push({ href: `#${e.path}`, text: e.msg });
errors[e.path] = { text: e.msg };
});
return errors;
};
9 changes: 9 additions & 0 deletions src/model/validation.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ErrorMessages } from '../validation/error.messages';

export interface FormattedValidationErrors {
[key: string]: any;
errorList: {
href: string;
text: ErrorMessages;
}[];
}
2 changes: 2 additions & 0 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { Router } from 'express';
import { logger } from '../middleware/logger.middleware';
import healthcheckRouter from './healthcheck';
import infoRouter from './info';
import validationTestRouter from './validation-test';

const router = Router();

router.use(logger);
router.use(healthcheckRouter);
router.use(infoRouter);
router.use(validationTestRouter);

export default router;
19 changes: 19 additions & 0 deletions src/routes/validation-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Router } from 'express';

import { checkValidations } from '../middleware/validation.middleware';
import { validationTest } from '../validation/validation-test.validation';
import { get, post } from '../controller/validation-test.controller';

import * as config from '../config';

const validationTestRouter = Router();

validationTestRouter.get(config.VALIDATION_TEST_URL, get);
validationTestRouter.post(
config.VALIDATION_TEST_URL,
...validationTest,
checkValidations,
post
);

export default validationTestRouter;
4 changes: 4 additions & 0 deletions src/validation/error.messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum ErrorMessages {
TEST_INFO_ERROR = 'INFO PAGE ERROR MESSAGE TEST',
TEST_FIRST_NAME = 'Enter your first name',
}
10 changes: 10 additions & 0 deletions src/validation/validation-test.validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { body } from 'express-validator';

import { ErrorMessages } from './error.messages';

export const validationTest = [
body('first_name')
.not()
.isEmpty({ ignore_whitespace: true })
.withMessage(ErrorMessages.TEST_FIRST_NAME),
];
9 changes: 9 additions & 0 deletions src/views/error-list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% if errors and errors.errorList and errors.errorList.length > 0 %}
{{ govukErrorSummary({
titleText: "There is a problem",
errorList: errors.errorList if errors,
attributes: {
"tabindex": "0"
}
}) }}
{% endif %}
9 changes: 9 additions & 0 deletions src/views/include/error-list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% if errors and errors.errorList and errors.errorList.length > 0 %}
{{ govukErrorSummary({
titleText: "There is a problem",
errorList: errors.errorList if errors,
attributes: {
"tabindex": "0"
}
}) }}
{% endif %}
10 changes: 9 additions & 1 deletion src/views/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

{% from "govuk/components/footer/macro.njk" import govukFooter %}
{% from "govuk/components/header/macro.njk" import govukHeader %}

{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %}
{% from "govuk/components/back-link/macro.njk" import govukBackLink %}
{% from "govuk/components/input/macro.njk" import govukInput %}
{% from "govuk/components/textarea/macro.njk" import govukTextarea %}
{% from "govuk/components/button/macro.njk" import govukButton %}
{% block head %}

<link rel="stylesheet" type="text/css" media="all" href="//{{CDN_HOST}}/stylesheets/govuk-frontend/v4.6.0/govuk-frontend-4.6.0.min.css">
Expand All @@ -22,6 +26,10 @@
}) }}
{% endblock %}

{% block content %}
{% endblock %}


{% block footer %}
{{ govukFooter({
meta: {
Expand Down
36 changes: 36 additions & 0 deletions src/views/validation-test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{% extends "layout.html" %}

{% block content %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<h1 class="govuk-heading-l">Validation Test</h1>

<p class="govuk-body">Submit field empty to test validation functionality.</p>

{% include "include/error-list.html" %}

<form method="post">

{{ govukInput({
errorMessage: errors.first_name if errors,
label: {
text: "First Name",
classes: "govuk-label--m"
},
classes: "govuk-input--width-10",
id: "first_name",
name: "first_name",
value: first_name
}) }}

{{ govukButton({
text: "Submit",
attributes: {
"id": "submit"
}
}) }}

</form>
</div>
</div>
{% endblock %}
74 changes: 74 additions & 0 deletions test/integration/routes/validation-test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
jest.mock('../../../src/middleware/logger.middleware');
jest.mock('../../../src/utils/logger');

import { jest, beforeEach, describe, expect, test } from '@jest/globals';
import { Request, Response, NextFunction } from 'express';
import request from 'supertest';

import app from '../../../src/app';
import * as config from '../../../src/config';
import { logger } from '../../../src/middleware/logger.middleware';
import { log } from '../../../src/utils/logger';

import {
MOCK_REDIRECT_MESSAGE,
MOCK_GET_VALIDATION_TEST_RESPONSE,
MOCK_POST_VALIDATION_TEST_RESPONSE,
} from '../../mock/text.mock';
import { MOCK_POST_VALIDATION_TEST } from '../../mock/data';

const mockedLogger = logger as jest.Mock<typeof logger>;
mockedLogger.mockImplementation(
(_req: Request, _res: Response, next: NextFunction) => next()
);

describe('validation-test endpoint integration tests', () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe('GET tests', () => {
test('renders the validation-test page', async () => {
const res = await request(app).get(config.VALIDATION_TEST_URL);

expect(res.status).toEqual(200);
expect(res.text).toContain(MOCK_GET_VALIDATION_TEST_RESPONSE);
expect(mockedLogger).toHaveBeenCalledTimes(1);
});
});

describe('POST tests', () => {
test('Should redirect to landing page after POST request', async () => {
const res = await request(app)
.post(config.VALIDATION_TEST_URL)
.send(MOCK_POST_VALIDATION_TEST);

expect(res.status).toEqual(302);
expect(res.text).toContain(MOCK_REDIRECT_MESSAGE);
expect(mockedLogger).toHaveBeenCalledTimes(1);
});

test('Should render the same page with error messages after POST request', async () => {
const res = await request(app)
.post(config.VALIDATION_TEST_URL)
.send({
first_name: '',
});

expect(res.status).toEqual(200);
expect(res.text).toContain(MOCK_GET_VALIDATION_TEST_RESPONSE);
expect(mockedLogger).toHaveBeenCalledTimes(1);
});

test('Should log the First Name and More Details on POST request.', async () => {
const mockLog = log.info as jest.Mock;
const res = await request(app)
.post(config.VALIDATION_TEST_URL)
.send(MOCK_POST_VALIDATION_TEST);

expect(mockLog).toBeCalledWith(MOCK_POST_VALIDATION_TEST_RESPONSE);
expect(res.text).toContain(MOCK_REDIRECT_MESSAGE);
expect(mockedLogger).toHaveBeenCalledTimes(1);
});
});
});
3 changes: 3 additions & 0 deletions test/mock/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import * as config from '../../src/config';
import express from 'express';

export const GET_REQUEST_MOCK = { method: 'GET', path: '/test' };
export const MOCK_POST_VALIDATION_TEST = { first_name: 'example' };

export const MOCK_POST_INFO = { test: 'test' };

export const MOCK_CORS_VALUE = {
origin: [config.CDN_HOST, config.BASE_URL],
Expand Down
4 changes: 4 additions & 0 deletions test/mock/log.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { log } from '../../src/utils/logger';

export const mockLogInfo = log.info as jest.Mock;
export const mockLogErrorRequest = log.errorRequest as jest.Mock;
1 change: 1 addition & 0 deletions test/mock/session.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const mockID = 'c3931b00-a8b4-4d2d-a165-b9b4d148cd88';
3 changes: 3 additions & 0 deletions test/mock/text.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ export const MOCK_SERVER_ERROR = 'Pipe 3000 requires elevated privileges';
export const MOCK_ERROR_MESSAGE = 'Something has went wrong';
export const MOCK_WRONG_URL = '/infooo';
export const MOCK_VIEWS_PATH = '/path/to/views';
export const MOCK_REDIRECT_MESSAGE = 'Found. Redirecting to info';
export const MOCK_GET_VALIDATION_TEST_RESPONSE = 'Submit field empty to test validation functionality.';
export const MOCK_POST_VALIDATION_TEST_RESPONSE = 'First Name: example';
59 changes: 59 additions & 0 deletions test/unit/controller/validation-test.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
jest.mock('../../../src/utils/logger');

import { describe, expect, afterEach, test, jest } from '@jest/globals';
import { Request, Response } from 'express';

import { get, post } from '../../../src/controller/validation-test.controller';
import * as config from '../../../src/config';
import { log } from '../../../src/utils/logger';

import { MOCK_POST_VALIDATION_TEST } from '../../mock/data';
import { MOCK_POST_VALIDATION_TEST_RESPONSE } from '../../mock/text.mock';

const req = {
body: MOCK_POST_VALIDATION_TEST,
} as Request;

const mockResponse = () => {
const res = {} as Response;
res.render = jest.fn().mockReturnValue(res) as any;
res.redirect = jest.fn().mockReturnValue(res) as any;
return res;
};

describe('Validation test controller test suites', () => {
afterEach(() => {
jest.resetAllMocks();
});

describe('validation test GET tests', () => {
test('should render validation test page', () => {
const res = mockResponse();

get(req, res);

expect(res.render).toHaveBeenCalledWith(config.VALIDATION_TEST);
});
});

describe('validation-test POST tests', () => {
test('should redirect to landing-page on POST request', () => {
const res = mockResponse();

post(req, res);

expect(res.redirect).toBeCalledWith(config.LANDING_PAGE);
});
test('should log GitHub handle and More Details on POST request', () => {
const res = mockResponse();

const mockLogInfo = log.info as jest.Mock;

post(req, res);

expect(mockLogInfo).toHaveBeenCalledWith(
MOCK_POST_VALIDATION_TEST_RESPONSE
);
});
});
});
Loading

0 comments on commit c4977a3

Please sign in to comment.