Skip to content

Commit

Permalink
Update validation page and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mel-am committed Jul 4, 2024
1 parent cd4eb9b commit c115ccd
Show file tree
Hide file tree
Showing 23 changed files with 581 additions and 59 deletions.
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ services:
- NODE_SSL_ENABLED=${NODE_SSL_ENABLED}
- BASE_URL=${BASE_URL}
- HUMAN=${HUMAN}
- LOG_LEVEL=${LOG_LEVEL}
- LOG_LEVEL=${LOG_LEVEL}
- FEATURE_FLAG_ENABLE_AUTH=${FEATURE_FLAG_ENABLE_AUTH}
16 changes: 14 additions & 2 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,27 @@ 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';
export const NOT_AVAILABLE = 'not-available';

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

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}`;

export const FEATURE_FLAG_ENABLE_AUTH = getEnvironmentValue(
'FEATURE_FLAG_ENABLE_AUTH',
'false'
);
// MISC
export const ID = 'id';
export const PARAM_ID = `/:${ID}`;

// export function VALIDATION_TEST(_VALIDATION_TEST: any) {
// throw new Error('Function not implemented.');
// }
// export function VALIDATION_TEST_URL(VALIDATION_TEST_URL: any, authentication: any, get: any) {
// throw new Error('Function not implemented.');
// }
2 changes: 1 addition & 1 deletion src/controller/error.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const errorNotFound = (_req: Request, res: Response) => {

export const errorHandler = (err: Error, _req: Request, res: Response, _next: NextFunction) => {
const statusCode = !res.statusCode || res.statusCode === 200 ? 500 : res.statusCode;
const errorMessage = err.message || 'An error has occured. Re-routing to the error screen';
const errorMessage = err.message || 'An error has occurred. Re-routing to the error screen';

log.error(`Error ${statusCode}: ${errorMessage}`);
res.status(statusCode).render(config.ERROR_PAGE);
Expand Down
17 changes: 17 additions & 0 deletions src/controller/validation-test.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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;

// validation middleware and data assignment to be implemented

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

return res.redirect(config.LANDING_PAGE);
};
26 changes: 26 additions & 0 deletions src/middleware/authentication.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NextFunction, Request, Response } from 'express';
import { log } from '../utils/logger';
import { isFeatureEnabled } from '../utils/isFeatureEnabled';
import * as config from '../config';

export const authentication = (
req: Request,
res: Response,
next: NextFunction
) => {
try {
// if auth is not enabled, render the not available page
if (!isFeatureEnabled(config.FEATURE_FLAG_ENABLE_AUTH)) {
log.infoRequest(req, 'sorry, auth service not available right now');
return res.render(config.NOT_AVAILABLE);
}

// If auth enabled
log.infoRequest(req, 'some auth here soon!');

next();
} catch (err: any) {
log.errorRequest(req, err);
next(err);
}
};
15 changes: 3 additions & 12 deletions src/middleware/validation.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { NextFunction, Request, Response } from 'express';
import { validationResult, FieldValidationError } from 'express-validator';

import * as config from '../config';
import { log } from '../utils/logger';
import { validateFilepath } from '../utils/validateFilepath';
import { FormattedValidationErrors } from '../model/validation.model';

export const checkValidations = (
Expand All @@ -15,26 +13,19 @@ export const checkValidations = (
const errorList = validationResult(req);

if (!errorList.isEmpty()) {
const validatedFilepath = validateFilepath(req.path);
const id = req.params[config.ID];
// Removing trailing slash and 36 characters from UUID length
const template_path = id
? validatedFilepath
.substring(0, validatedFilepath.length - 37)
.substring(1)
: validatedFilepath.substring(1);
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, id, errors });
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.errorRequest(req, err.message);
log.error(err.message);
next(err);
}
};
Expand Down
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;
21 changes: 21 additions & 0 deletions src/routes/validation-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Router } from 'express';

import { authentication } from '../middleware/authentication.middleware';
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, authentication, get);
validationTestRouter.post(
config.VALIDATION_TEST_URL,
authentication,
...validationTest,
checkValidations,
post
);

export default validationTestRouter;
3 changes: 3 additions & 0 deletions src/utils/isFeatureEnabled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const isFeatureEnabled = (flag: string) => {

Check warning on line 1 in src/utils/isFeatureEnabled.ts

View workflow job for this annotation

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

🕹️ Function is not covered

Warning! Not covered function
return flag === 'true';

Check warning on line 2 in src/utils/isFeatureEnabled.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement
};
3 changes: 3 additions & 0 deletions src/validation/error.messages.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export enum ErrorMessages {
TEST_INFO_ERROR = 'INFO PAGE ERROR MESSAGE TEST',
TEST_FIRST_NAME = 'Enter your first name',
TEST_DESCRIPTION_LENGTH = 'Description must be 1000 characters or less',

}
13 changes: 13 additions & 0 deletions src/validation/validation-test.validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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),
body('description')
.isLength({ max: 1000 })
.withMessage(ErrorMessages.TEST_DESCRIPTION_LENGTH),
];
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,10 @@

{% 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 %}
{% 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 +25,11 @@
}) }}
{% endblock %}


{% block pageContent %}
{% endblock %}


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

{% block pageContent %}
<h1 class="govuk-heading-l">
Sorry, the service is unavailable
</h1>

{% endblock %}
47 changes: 47 additions & 0 deletions src/views/validation-test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{% 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">
This is a page to test validation.
</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
}) }}

{{ govukTextarea({
errorMessage: errors.description if errors,
value: description,
name: "description",
id: "description",
label: {
text: "Description (optional)",
classes: "govuk-label--m",
isPageHeading: true
},
hint: {
text: "Include a description of what needs to be done."
}
}) }}

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

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';
import { ErrorMessages } from '../../../src/validation/error.messages';

const mockedLogger = logger as jest.Mock<typeof logger>;
mockedLogger.mockImplementation(
(req: Request, res: Response, next: NextFunction) => next()
);
const mockedAuth = authentication as jest.Mock<typeof authentication>;
mockedAuth.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);
expect(mockedAuth).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);
expect(mockedAuth).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: '',
description: '1000chars.'.repeat(100) + ':)',
});

expect(res.status).toEqual(200);
expect(res.text).toContain(ErrorMessages.TEST_FIRST_NAME);
expect(res.text).toContain(ErrorMessages.TEST_DESCRIPTION_LENGTH);
expect(res.text).toContain(MOCK_GET_VALIDATION_TEST_RESPONSE);
expect(mockedLogger).toHaveBeenCalledTimes(1);
expect(mockedAuth).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);
expect(mockedAuth).toHaveBeenCalledTimes(1);
});
});
});
1 change: 1 addition & 0 deletions test/mock/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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', description: 'description' };

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

Expand Down
Loading

0 comments on commit c115ccd

Please sign in to comment.