diff --git a/.eslintrc.json b/.eslintrc.json index 9a84f13..769defe 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -15,7 +15,10 @@ "rules": { "indent": [ "error", - "tab" + "tab", + { + "SwitchCase": 1 + } ], "linebreak-style": [ "error", diff --git a/lib/__tests__/validator.test.js b/lib/__tests__/validator.test.js new file mode 100644 index 0000000..ca75b39 --- /dev/null +++ b/lib/__tests__/validator.test.js @@ -0,0 +1,238 @@ +'use strict'; + +const validator = require('../validator'); + +describe('validEmail', () => { + test('Valid email address', () => { + const email = 'bob@bob.com'; + + expect(validator.validEmail(email)).toBeTrue(); + }); + + test('Invalid email address', () => { + const email = 'bobbobcom'; + + expect(validator.validEmail(email)).toBeFalse(); + }); + + test('Invalid email address with @', () => { + const email = 'bob@bobcom'; + + expect(validator.validEmail(email)).toBeFalse(); + }); + + test('Obscure valid email address', () => { + const email = 'jimmy.bob+joe@google.edu.gov.uk'; + + expect(validator.validEmail(email)).toBeTrue(); + }); + + test('Even more obscure valid email address', () => { + // TODO more obscure email address + const email = 'bob@bob.com'; + + expect(validator.validEmail(email)).toBeTrue(); + }); +}); + +describe('passwordsMatch', () => { + test('Passwords match', () => { + const p1 = 'password123'; + + expect(validator.passwordsMatch(p1, p1)).toBeTrue(); + }); + + test('Passwords do not match', () => { + const p1 = 'bobby'; + const p2 = 'jimmy'; + + expect(validator.passwordsMatch(p1, p2)).toBeFalse(); + }); + + test('Passwords match and contain special characters', () => { + const p1 = 'p2 ssw_0 rd&--1 t3'; + + expect(validator.passwordsMatch(p1, p1)).toBeTrue(); + }); + + test('Passwords do not match and contain special characters', () => { + const p1 = 'p2ssw_0rd&--1t3'; + const p2 = 'this-is.././really 99(not) +the+[same'; + + expect(validator.passwordsMatch(p1, p2)).toBeFalse(); + }); + + test('Passwords match but are not strings', () => { + const p1 = { password: 'password123' }; + + expect(validator.passwordsMatch(p1, p1)).toBeFalse(); + }); + + test('Passwords do not match and are not strings', () => { + const p1 = { password: 'password123' }; + const p2 = { password: 'this is not the same' }; + + expect(validator.passwordsMatch(p1, p2)).toBeFalse(); + }); + + test('Passwords do not match but differ by whitespace', () => { + const p1 = 'password123'; + const p2 = 'password 123'; + + expect(validator.passwordsMatch(p1, p2)).toBeFalse(); + }); +}); + +describe('validate', () => { + test('All required fields filled in', () => { + const body = { + name: 'Bob', + message: 'Hi Jim!' + }; + + const fields = [ + 'name', + 'message' + ]; + + const result = validator.validate(body, fields); + + expect(result).toBeObject(); + expect(result).toContainKey('fields'); + expect(result.fields.get('name')).toBe('Bob'); + expect(result.fields.get('message')).toBe('Hi Jim!'); + }); + + test('Required fields missing', () => { + const body = { + dob: '01/01/01' + }; + + const fields = [ + 'name', + 'message' + ]; + + expect(() => { + validator.validate(body, fields); + }).toThrow('missing'); + }); + + test('Valid email validation', () => { + const body = { + name: 'Bob', + message: 'Hi Jim!', + email: 'bob@bob.com' + }; + + const fields = [ + 'name', + 'message', + 'email' + ]; + + const validation = { + email: 'email' + }; + + const result = validator.validate(body, fields, validation); + + expect(result).toBeObject(); + expect(result).toContainKey('fields'); + expect(result.fields.get('email')).toBe('bob@bob.com'); + }); + + test('Invalid email validation', () => { + const body = { + name: 'Bob', + message: 'Hi Jim!', + email: 'bobbobcom' + }; + + const fields = [ + 'name', + 'message', + 'email' + ]; + + const validation = { + email: 'email' + }; + + expect(() => { + validator.validate(body, fields, validation); + }).toThrow('Invalid'); + }); + + test('Valid password validation', () => { + const body = { + name: 'Bob', + message: 'Hi Jim!', + p1: 'bob', + p2: 'bob' + }; + + const fields = [ + 'name', + 'message', + 'p1', + 'p2' + ]; + + const validation = { + password: [ 'p1', 'p2' ] + }; + + const result = validator.validate(body, fields, validation); + + expect(result).toBeObject(); + expect(result).toContainKey('fields'); + expect(result.fields.get('p1')).toBe('bob'); + expect(result.fields.get('p2')).toBe('bob'); + }); + + test('Invalid password validation', () => { + const body = { + name: 'Bob', + message: 'Hi Jim!', + p1: 'bob', + p2: 'jim' + }; + + const fields = [ + 'name', + 'message', + 'p1', + 'p2' + ]; + + const validation = { + password: [ 'p1', 'p2' ] + }; + + expect(() => { + validator.validate(body, fields, validation); + }).toThrow('Invalid'); + }); + + test('Invalid validation type', () => { + const body = { + name: 'Bob', + message: 'Hi Jim!' + }; + + const fields = [ + 'name', + 'message' + ]; + + const validation = { + joeseph: [] + }; + + expect(() => { + validator.validate(body, fields, validation); + }).toThrow('Invalid validation type'); + }); +}); + diff --git a/lib/validator.js b/lib/validator.js new file mode 100644 index 0000000..261afa9 --- /dev/null +++ b/lib/validator.js @@ -0,0 +1,88 @@ +'use strict'; + +/** + * validEmail() Check that an email email matches a simple valid email regex + * + * @param {string} email - The email address to be validated + * @param {RegExp} [emailRegex] - RegExp to use for validation + * + * @return {boolean} - If the email address is valid + */ +function validEmail(email, emailRegex) { + if (typeof emailRegex === 'undefined') + emailRegex = /\S+@\S+\.\S+/; + + return emailRegex.test(email); +} + +/** + * passwordsMatch() Check that two password fields match + * + * @param {string} password1 - The first password + * @param {string} password2 - The second password + * + * @return {boolean} - If the passwords match + */ +function passwordsMatch(password1, password2) { + if (typeof password1 !== 'string' || typeof password2 !== 'string') + return false; + + return password1 === password2; +} + +/** + * validate() Main validation wrapper function to validate full POST form body + * + * @param {Object} body - The body of an express request + * @param {Array} fields - Fields to check + * @param {Object} [validation] - Fields to run special validation against + * + * @return {Object} results + * @return {Map} results.fields - Sanitised and validated fields + */ +function validate(body, fields, validation = {}) { + const fieldsMap = new Map(); + + // Check all required fields are not empty, and sanitise them + for (const field of fields) { + const cleanField = body[field]?.trim() ?? false; + + if (cleanField === false || cleanField.length < 1) + throw new Error(`${field} is missing`); + + fieldsMap.set(field, cleanField); + } + + // Handle validation as required in options + for (const [ check, checkOpts ] of Object.entries(validation)) { + let valid; + + // Handlers for validation types go here + switch (check) { + case 'email': + valid = validEmail(fieldsMap.get(checkOpts)); + break; + case 'password': + valid = passwordsMatch( + fieldsMap.get(checkOpts[0]), + fieldsMap.get(checkOpts[1]) + ); + break; + default: + throw new Error('Invalid validation type'); + } + + if (!valid) + throw new Error(`Invalid ${check}`); + } + + return { + fields: fieldsMap + }; +} + +module.exports = { + validEmail: validEmail, + passwordsMatch: passwordsMatch, + validate: validate +};