diff --git a/dba/seeders/users.js b/dba/seeders/users.js index 319d9832..438a43cf 100644 --- a/dba/seeders/users.js +++ b/dba/seeders/users.js @@ -3,11 +3,17 @@ require('dotenv').config(); module.exports = { - up: function (queryInterface, Sequelize) { - return queryInterface.bulkInsert('users', [ - {user_name: process.env.ADMINROLE_USER, pass_hash: process.env.ADMINROLE_HASH, user_role: 'admin', created: 'now()', updated: 'now()'}, - {user_name: process.env.USERROLE_USER, pass_hash: process.env.USERROLE_HASH, user_role: 'user', created: 'now()', updated: 'now()'} - ]); + up: function (queryInterface, Sequelize) { + let userAccounts = []; + if(process.env.ADMINROLE_USER && process.env.ADMINROLE_HASH){ + userAccounts.push({user_name: process.env.ADMINROLE_USER, pass_hash: process.env.ADMINROLE_HASH, user_role: 'admin', created: 'now()', updated: 'now()'}); + } + if(process.env.USERROLE_USER && process.env.USERROLE_HASH){ + userAccounts.push({user_name: process.env.USERROLE_USER, pass_hash: process.env.USERROLE_HASH, user_role: 'user', created: 'now()', updated: 'now()'}); + } + if(userAccounts.length > 0){ + return queryInterface.bulkInsert('users', userAccounts); + } }, down: function (queryInterface, Sequelize) { return queryInterface.bulkDelete('users', [ diff --git a/docs/permit-creation.md b/docs/permit-creation.md index 8b0caeb3..94140097 100644 --- a/docs/permit-creation.md +++ b/docs/permit-creation.md @@ -42,9 +42,7 @@ These steps define the process for creating a new permit type using Example Perm "/permits/applications/special-uses/commercial/example-permit/": { "post": { - "x-validation":{ - "$ref":"validation.json#examplePermit" - }, + "x-validation":"validation.json#examplePermit", "parameters": [ { "in": "formData", @@ -96,7 +94,23 @@ These steps define the process for creating a new permit type using Example Perm "basicField":"securityId", "default":"", "fromIntake":false, - "madeOf":["region","forest","district"], + "madeOf":{ + "fields":[ + { + "fromIntake":true, + "field":"region" + }, + { + "fromIntake":true, + "field":"forest" + }, + { + "fromIntake":false, + "value":"123" + } + ], + "function":"concat" + }, "store":["basic:/application", "basic:/contact/address", "basic:/contact/phone"], "type" : "string" }, @@ -119,6 +133,14 @@ These steps define the process for creating a new permit type using Example Perm - `store` describes where this field should be stored, either in the middlelayer DB or in the basic API. It can list multiple places to store this field + - `madeOf` describes how to auto-populate the field, if fromIntake is false. + - `fields` lists the fields, and values which are to be used when auto-populating the field. + - `fromIntake` indicates whether this piece of the field is from the intake module or not + - If `fromIntake` is true, `field` is expected in the same object, specifying the field where this part of the field should come from. + - If `fromIntake` is false, `value` is expected in the same object, specifying what value is to be used in this part of the field. + -`function` describes the function that should be used on an array of all indicies of `fields`, current options are `concat` and `contId`. + - To add an option for this field, create a function in basic.js which takes an array as input and outputs a string. Then update the `buildAutoPopulatedFields` function in basic.js by adding a case to the switch/case statement for the name of the newly created function and then a call to that function inside the case statement. + Files: - `maxSize` is measured in megabytes diff --git a/mocks/basic.json b/mocks/basic.json index 46328e77..20e70d10 100644 --- a/mocks/basic.json +++ b/mocks/basic.json @@ -8,7 +8,7 @@ "tags": [ ], "paths": { - "/contact/person/{lastname}/": { + "/contact/lastname/{lastname}": { "get": { "tags": [ "Contact Person" @@ -31,24 +31,37 @@ "200": { "description": "Success", "examples": { - "application/json": { - "contCn": "987654321", - "contId": "1234", - "contName": "Jane Roe", + "application/json": [ + { + "contCn": "100000001", + "contId": "LNAME, FNAME", + "contName": "fname lname", "contactType": "PERSON", - "firstName": "Jane", - "lastName": "Roe", + "firstName": "Fname", + "lastName": "Lname", "orgCode": null, "orgType": null, "personPrefix": "MS", - "securityId": "0000" - } + "securityId": "112233" + }, + { + "contCn": "100000002", + "contId": "LNAME2, FNAME2", + "contName": "fname2 lname2", + "contactType": "PERSON", + "firstName": "Fname2", + "lastName": "Lname2", + "orgCode": null, + "orgType": null, + "personPrefix": "MS", + "securityId": "112221" + }] } } } } }, - "/contact/person/": { + "/contact/person": { "post": { "tags": [ "Contact Person" @@ -76,7 +89,7 @@ } } }, - "/contact/orgcode/{orgname}/": { + "/contact/orgcode/{orgname}": { "get": { "tags": [ "Contact Organization" @@ -99,9 +112,9 @@ "200": { "description": "Success", "examples": { - "application/json": { - "contCn": "987654321", - "contId": "1234", + "application/json": [{ + "contCn": "200000001", + "contId": "TEMP ORGANIZATION", "contName": "Acme", "contactType": "ORGANIZATION", "firstName": null, @@ -109,14 +122,38 @@ "orgCode": "INC", "orgType": "CORPORATION", "personPrefix": null, - "securityId": null - } + "securityId": "110022" + }, + { + "contCn": "200000002", + "contId": "TEMP ORGANIZATION2", + "contName": "Acme", + "contactType": "ORGANIZATION", + "firstName": null, + "lastName": null, + "orgCode": "INC", + "orgType": "CORPORATION", + "personPrefix": null, + "securityId": "112230" + }, + { + "contCn": "200000003", + "contId": "TEMP ORGANIZATION", + "contName": "Acme", + "contactType": "ORGANIZATION", + "firstName": null, + "lastName": null, + "orgCode": "INC", + "orgType": "CORPORATION", + "personPrefix": null, + "securityId": "110024" + }] } } } } }, - "/contact/orgcode/": { + "/contact/orgcode": { "post": { "tags": [ "Contact Organization" @@ -147,7 +184,7 @@ } } }, - "/contact-address/": { + "/contact-address": { "post": { "tags": [ "Contact Address" @@ -178,7 +215,7 @@ } } }, - "/contact-phone/": { + "/contact-phone": { "post": { "tags": [ "Contact Phone" @@ -207,7 +244,7 @@ } } }, - "/application/": { + "/application": { "post": { "tags": [ "Application" diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 0ae40748..9693c3ec 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "fs-middlelayer-api", - "version": "0.0.13", + "version": "0.0.14", "dependencies": { "abbrev": { "version": "1.0.9", diff --git a/package.json b/package.json index 1cd59291..9430da6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fs-middlelayer-api", - "version": "0.0.13", + "version": "0.0.14", "description": "US Forest Service ePermit Middlelayer API", "engines": { "node": "6.9.x", diff --git a/src/api.json b/src/api.json index a223a561..a0ea3bda 100644 --- a/src/api.json +++ b/src/api.json @@ -1,9 +1,8 @@ { "swagger": "2.0", - "basePath":"/", "info": { "description": "This page is for developers. We built this site entirely off the APIs below and we want to share them with you.", - "version": "0.0.13", + "version": "0.0.14", "title": "API Documentation for Developers" }, "tags": [ @@ -158,9 +157,7 @@ }, "/permits/applications/special-uses/noncommercial/": { "post": { - "x-validation":{ - "$ref":"controllers/validation.json#noncommercialApplication" - }, + "x-validation":"controllers/validation.json#noncommercialApplication", "tags": [ "Noncommercial Permit" ], @@ -424,9 +421,7 @@ }, "/permits/applications/special-uses/commercial/temp-outfitters/": { "post": { - "x-validation":{ - "$ref":"controllers/validation.json#tempOutfitterApplication" - }, + "x-validation":"controllers/validation.json#tempOutfitterApplication", "tags": [ "Outfitter and Guide Permit" ], diff --git a/src/controllers/basic.js b/src/controllers/basic.js index 4cd2797b..578cbdeb 100644 --- a/src/controllers/basic.js +++ b/src/controllers/basic.js @@ -19,11 +19,58 @@ const request = require('request-promise'); // other files const db = require('./db.js'); +const DuplicateContactsError = require('./duplicateContactsError.js'); const SUDS_API_URL = process.env.SUDS_API_URL; +//******************************************************************* +// AUTO-POPULATE FUNCTIONS +/** + * Concats all indexs of input + * @param {Array} input - Array of strings to be joined together + * @return {String} - Single string made up of all indicies of input + */ +function concat(input){ + const output = input.join(''); + return output; +} + +/** + * Ensures all characters of input are upper case then joins them + * @param {Array} input - Array of strings to be joined together + * @return {String} - Single string made up of all indicies of input + */ +function contId(input){ + return concat( + input.map((i)=>{ + return i.toUpperCase(); + }) + ); +} + +/** + * Adds UNIX timestamp and then joins all elements of input + * @param {Array} input - Array of strings to be joined together + * @return {String} - Single string made up of all indicies of input + */ +function ePermitId(input){ + const timeStamp = + new Date(); + input.push(timeStamp); + return concat(input); +} + //******************************************************************* -/** Finds basic API fields are to be auto-populated +/** + * Returns whether application is for an individual. + * @param {Object} body - User input + * @return {Boolean} - Whether application is for an individual + */ +function isAppFromPerson(body){ + const output = (!body.applicantInfo.orgType || body.applicantInfo.orgType === 'Individual'); + return output; +} + +/** Finds basic API fields which are to be auto-populated * @param {Array} basicFields - Fields(Objects) which are stored in SUDS * @return {Array} - Fields(Objects) which are to be auto-populated */ @@ -37,25 +84,72 @@ function getAutoPopulatedFields(basicFields){ }); return autoPop; } + +/** + * Given a path seperated by periods, return the field specified if it exists, else false. + * @param {String} path - String made of the path to the desired field, must be seperated by periods + * @param {Object} body - Object representing the user input + * @return {Boolean|String|Number|Object} - Contents of the field specified or false + */ +function getFieldFromBody(path, body){ + const pathParts = path.split('.'); + pathParts.forEach((pathPart)=>{ + body = body[pathPart]; + }); + if (body){ + return body; + } + else { + return false; + } +} /** Given list of fields which must be auto-populate, returns values to store - * @param {Array} - Array of objects representing Fields which need to be auto-populated - * @param {Object} body - user input - * @return {Array} - created values + * @param {Array} fieldsToBuild - Array of objects representing Fields which need to be auto-populated + * @param {Object} body - user input + * @return {Array} - created values */ -function buildAutoPopulatedFields(toBuild, body){ +function buildAutoPopulatedFields(fieldsToBuild, body){ const output = {}; - toBuild.forEach((field)=>{ + fieldsToBuild.forEach((field)=>{ const key = Object.keys(field)[0]; - let fieldValue = ''; - field[key].madeOf.forEach((component)=>{ - if (body[component]){ - fieldValue = `${fieldValue}${body[component]}`; + const fieldMakeUp = []; + let autoPopulatedFieldValue = ''; + field[key].madeOf.fields.forEach((madeOfField)=>{ + if (madeOfField.fromIntake){ + const fieldValue = getFieldFromBody(madeOfField.field, body); + if (fieldValue){ + fieldMakeUp.push(fieldValue); + } + else { + console.error(`${madeOfField.field} does not exist`); + } } else { - fieldValue = `${fieldValue}${component}`; + fieldMakeUp.push(madeOfField.value); } }); - output[key] = fieldValue; + switch (field[key].madeOf.function){ + case 'concat': + autoPopulatedFieldValue = concat(fieldMakeUp); + break; + case 'contId': + if (isAppFromPerson(body)){ + if (fieldMakeUp.length > 3){ + fieldMakeUp.pop(); + } + autoPopulatedFieldValue = contId(fieldMakeUp); + } + else { + const toUse = []; + toUse.push(fieldMakeUp.pop()); + autoPopulatedFieldValue = contId(toUse); + } + break; + case 'ePermitId': + autoPopulatedFieldValue = ePermitId(fieldMakeUp); + break; + } + output[key] = autoPopulatedFieldValue; }); return output; } @@ -142,20 +236,26 @@ function prepareBasicPost(sch, body){ /** * Creates request for Basic API calls to create contact * @param {Object} res - Response of previous request - * @param {Object} postObject - Object used to save the request and response for each post to the basic api. Used for testing purposes. + * @param {Object} apiCallsObject - Object used to save the request and response for each post to the basic api. Used for testing purposes. * @param {Object} fieldsObj - Object containing post objects to be sent to basic api - * @param {String} responseKey - Key in postObject for the response object of the previous request - * @param {String} requestKey - Key in postObject for the request object of this request + * @param {String} responseKey - Key in apiCallsObject for the response object of the previous request + * @param {String} requestKey - Key in apiCallsObject for the request object of this request * @param {String} requestPath - Path from basic API route this response needs to be sent to * @return {Promise} - Promise to be fulfilled */ -function postRequest(res, postObject, fieldsObj, responseKey, requestKey, requestPath){ - postObject[responseKey].response = res; - const cn = res.contCn; +function postRequest(res, apiCallsObject, fieldsObj, responseKey, requestKey, requestPath){ + apiCallsObject.POST[responseKey].response = res; + let cn = ''; + if (requestPath === '/contact-address'){ + cn = res.contCn; + } + else { + cn = res.contact; + } const addressField = fieldsObj[requestKey]; addressField.contact = cn; - const addressURL = `${SUDS_API_URL}${requestPath}/`; - postObject[requestPath].request = addressField; + const addressURL = `${SUDS_API_URL}${requestPath}`; + apiCallsObject.POST[requestPath].request = addressField; const createAddressOptions = { method: 'POST', uri: addressURL, @@ -168,21 +268,24 @@ function postRequest(res, postObject, fieldsObj, responseKey, requestKey, reques * Calls basic API to create a contact in SUDS * @param {Object} fieldsObj - Object containing post objects to be sent to basic api * @param {boolean} person - Boolean indicating whether the contract being created is for a person or not - * @param {Object} postObject - Object used to save the request and response for each post to the basic api. Used for testing purposes. + * @param {Object} apiCallsObject - Object used to save the request and response for each post to the basic api. Used for testing purposes. * @return {Promise} - Promise to be fulfilled */ -function createContact(fieldsObj, person, postObject){ +function createContact(fieldsObj, person, apiCallsObject){ return new Promise(function(fulfill, reject){ - let contactField, createPersonOrOrgURL; + let contactField, createPersonOrOrgURL, responseKey; if (person){ contactField = fieldsObj['/contact/person']; - createPersonOrOrgURL = `${SUDS_API_URL}/contact/person/`; + createPersonOrOrgURL = `${SUDS_API_URL}/contact/person`; + responseKey = '/contact/person'; + apiCallsObject.POST[responseKey].request = contactField; } else { contactField = fieldsObj['/contact/organization']; - createPersonOrOrgURL = `${SUDS_API_URL}/contact/orgcode/`; + createPersonOrOrgURL = `${SUDS_API_URL}/contact/orgcode`; + responseKey = '/contact/orgcode'; + apiCallsObject.POST[responseKey].request = contactField; } - postObject['/contact/personOrOrgcode'].request = contactField; const createContactOptions = { method: 'POST', uri: createPersonOrOrgURL, @@ -191,13 +294,13 @@ function createContact(fieldsObj, person, postObject){ }; request(createContactOptions) .then(function(res){ - return postRequest(res, postObject, fieldsObj, '/contact/personOrOrgcode', '/contact/address', '/contact-address'); + return postRequest(res, apiCallsObject, fieldsObj, responseKey, '/contact/address', '/contact-address'); }) .then(function(res){ - return postRequest(res, postObject, fieldsObj, '/contact-address', '/contact/phone', '/contact-phone'); + return postRequest(res, apiCallsObject, fieldsObj, '/contact-address', '/contact/phone', '/contact-phone'); }) .then(function(res){ - postObject['/contact-phone'].response = res; + apiCallsObject.POST['/contact-phone'].response = res; fulfill(res.contact); }) .catch(function(err){ @@ -210,14 +313,14 @@ function createContact(fieldsObj, person, postObject){ * Calls basic API to create an application in SUDS * @param {Object} fieldsObj - Object containing post objects to be sent to basic api * @param {Number} contCN - Contact control number of contact associated with this application - * @param {Object} postObject - Object used to save the request and response for each post to the basic api. Used for testing purposes. + * @param {Object} apiCallsObject - Object used to save the request and response for each post to the basic api. Used for testing purposes. * @return {Promise} - Promise to be fulfilled */ -function createApplication(fieldsObj, contCN, postObject){ - const createApplicationURL = `${SUDS_API_URL}/application/`; +function createApplication(fieldsObj, contCN, apiCallsObject){ + const createApplicationURL = `${SUDS_API_URL}/application`; fieldsObj['/application'].contCn = contCN; const applicationPost = fieldsObj['/application']; - postObject['/application'].request = applicationPost; + apiCallsObject.POST['/application'].request = applicationPost; const createApplicationOptions = { method: 'POST', uri: createApplicationURL, @@ -227,21 +330,37 @@ function createApplication(fieldsObj, contCN, postObject){ return request(createApplicationOptions); } +function getContId(fieldsObj, person){ + if (person){ + return fieldsObj['/contact/person'].contId; + } + else { + return fieldsObj['/contact/organization'].contId; + } +} + /** Sends requests needed to create an application via the Basic API * @param {Object} req - Request Object * @param {Object} res - Response Object - * @param {Object} sch - Schema object + * @param {Object} sch - Schema object * @param {Object} body - User input */ function postToBasic(req, res, sch, body){ //Should remove control number once we get from BASIC api return new Promise(function (fulfill, reject){ - const postObject = { - '/contact/personOrOrgcode':{}, - '/contact-address':{}, - '/contact-phone':{}, - '/application':{} + const apiCallsObject = { + 'GET':{ + '/contact/lastname/{lastName}':{}, + '/contact/orgcode/{orgCode}':{} + }, + 'POST':{ + '/contact/person':{}, + '/contact/orgcode':{}, + '/contact-address':{}, + '/contact-phone':{}, + '/application':{} + } }; const fieldsToPost = prepareBasicPost(sch, body); const fieldsObj = {}; @@ -250,20 +369,18 @@ function postToBasic(req, res, sch, body){ //Should remove control number once w fieldsObj[key] = post[key]; }); - const org = (body.applicantInfo.orgType && body.applicantInfo.orgType !== 'Individual'); + const person = isAppFromPerson(body); let existingContactCheck; - if (org){ - let orgName = body.applicantInfo.organizationName; - if (!orgName){ - orgName = 'abc'; - } - existingContactCheck = `${SUDS_API_URL}/contact/orgcode/${orgName}/`; + if (person){ + const lastName = body.applicantInfo.lastName; + existingContactCheck = `${SUDS_API_URL}/contact/lastname/${lastName}`; + apiCallsObject.GET['/contact/lastname/{lastName}'].request = {'lastName':lastName}; } else { - const lastName = body.applicantInfo.lastName; - existingContactCheck = `${SUDS_API_URL}/contact/person/${lastName}/`; + const orgName = body.applicantInfo.organizationName; + existingContactCheck = `${SUDS_API_URL}/contact/orgcode/${orgName}`; + apiCallsObject.GET['/contact/orgcode/{orgCode}'].request = {'orgCode':orgName}; } - const getContactOptions = { method: 'GET', uri: existingContactCheck, @@ -272,19 +389,63 @@ function postToBasic(req, res, sch, body){ //Should remove control number once w }; request(getContactOptions) .then(function(res){ - if (res.contCN){ - Promise.resolve(res.contCN); + if (person){ + apiCallsObject.GET['/contact/lastname/{lastName}'].response = res; + } + else { + apiCallsObject.GET['/contact/orgcode/{orgCode}'].response = res; + } + const contId = getContId(fieldsObj, person); + if (res.length === 1 && res[0].contCn){ + if (contId === res[0].contId){ + return new Promise(function(resolve){ + resolve(res[0].contCn); + }); + } + else { + return createContact(fieldsObj, person, apiCallsObject); + } + } + else if (res.length > 1){ + + const matchingContacts = res; + const duplicateContacts = []; + let tmpContCn; + + matchingContacts.forEach((contact)=>{ + if (contId === contact.contId){ + duplicateContacts.push(contact); + tmpContCn = contact.contCn; + } + }); + + if (duplicateContacts.length === 0){ + return createContact(fieldsObj, true, apiCallsObject); + } + else if (duplicateContacts.length === 1){ + return new Promise(function(resolve){ + resolve(tmpContCn); + }); + } + else { + throw new DuplicateContactsError(duplicateContacts); + } } else { - return createContact(fieldsObj, true, postObject); + return createContact(fieldsObj, person, apiCallsObject); } }) - .then(function(contCN){ - return createApplication(fieldsObj, contCN, postObject); + .then(function(contCn){ + return createApplication(fieldsObj, contCn, apiCallsObject); }) .then(function(response){ - postObject['/application'].response = response; - fulfill(postObject); + const applResponse = response; + if (SUDS_API_URL.endsWith('/mocks')){ + const controlNumber = (Math.floor((Math.random() * 10000000000) + 1)).toString(); + applResponse.accinstCn = controlNumber; + } + apiCallsObject.POST['/application'].response = applResponse; + fulfill(apiCallsObject); }) .catch(function(err){ reject(err); diff --git a/src/controllers/createUser.js b/src/controllers/createUser.js new file mode 100644 index 00000000..3d3ed4ae --- /dev/null +++ b/src/controllers/createUser.js @@ -0,0 +1,49 @@ +/* + + ___ ___ ___ _ _ _ ___ ___ + | __/ __| ___| _ \___ _ _ _ __ (_) |_ /_\ | _ \_ _| + | _|\__ \ / -_) _/ -_) '_| ' \| | _| / _ \| _/| | + |_| |___/ \___|_| \___|_| |_|_|_|_|\__| /_/ \_\_| |___| + +*/ + +//******************************************************************* + +'use strict'; + +//******************************************************************* +// required modules +require('dotenv').config(); +const minimist = require('minimist'); +const bcrypt = require('bcrypt-nodejs'); +const db = require('./db.js'); + +//************************************************************* + +const args = minimist(process.argv.slice(2)); +const username = args.u; +const password = args.p; +const userrole = args.r; + +if (username && password && userrole && (userrole === 'admin' || userrole === 'user')){ + const salt = bcrypt.genSaltSync(10); + const hash = bcrypt.hashSync(password, salt); + + const user = {}; + user.userName = username; + user.passHash = hash; + user.userRole = userrole; + + db.saveUser(user, function(err, usr){ + if (err){ + console.error('\nERROR!\n' + err); + } + else { + console.log('\nSUCCESS! User account created with username=' + usr.userName); + return; + } + }); +} +else { + console.error('\nERROR! Invlid parameters supplied.'); +} diff --git a/src/controllers/db.js b/src/controllers/db.js index e2514e0f..693c0be0 100644 --- a/src/controllers/db.js +++ b/src/controllers/db.js @@ -213,6 +213,45 @@ function getDataToStoreInDB(schema, body){ return output; } +/** + * Save user data to DB + * @param {Object} user - user object containing fields to save in DB + * @param {Function} callback - Function to call after saving user to DB + */ +const saveUser = function(user, callback) { + models.users.create(user) + .then(function(usr) { + return callback(null, usr); + }) + .catch(function(err) { + console.error(err); + return callback(err, null); + }); +}; + +/** + * Delete user from DB + * @param {String} username - username to be deleted from DB + * @param {Function} callback - Function to call after deleting user from DB + */ +const deleteUser = function(username, callback) { + models.users.destroy({ + where: { + userName: username + } + }).then(function(rowDeleted){ + if (rowDeleted === 1){ + return callback(null); + } + else { + return callback('row could not be be deleted'); + } + }, function(err){ + console.error(err); + return callback(err); + }); +}; + module.exports.getDataToStoreInDB = getDataToStoreInDB; module.exports.getFieldsToStore = getFieldsToStore; module.exports.saveFile = saveFile; @@ -220,3 +259,5 @@ module.exports.getFile = getFile; module.exports.getFiles = getFiles; module.exports.getApplication = getApplication; module.exports.saveApplication = saveApplication; +module.exports.saveUser = saveUser; +module.exports.deleteUser = deleteUser; diff --git a/src/controllers/duplicateContactsError.js b/src/controllers/duplicateContactsError.js new file mode 100644 index 00000000..5e761182 --- /dev/null +++ b/src/controllers/duplicateContactsError.js @@ -0,0 +1,23 @@ +/* + + ___ ___ ___ _ _ _ ___ ___ + | __/ __| ___| _ \___ _ _ _ __ (_) |_ /_\ | _ \_ _| + | _|\__ \ / -_) _/ -_) '_| ' \| | _| / _ \| _/| | + |_| |___/ \___|_| \___|_| |_|_|_|_|\__| /_/ \_\_| |___| + +*/ + +//******************************************************************* + +'use strict'; + +//******************************************************************* + +module.exports = function DuplicateContactsError(duplicateContacts) { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; + this.message = duplicateContacts.length + ' duplicate contacts found!'; + if (duplicateContacts) this.duplicateContacts = duplicateContacts; +}; + +require('util').inherits(module.exports, Error); diff --git a/src/controllers/index.js b/src/controllers/index.js index f76d99c1..2b14516d 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -32,6 +32,7 @@ const db = require('./db.js'); const basic = require('./basic.js'); const validation = require('./validation.js'); const util = require('./utility.js'); +const DuplicateContactsError = require('./duplicateContactsError.js'); //************************************************************* @@ -105,11 +106,13 @@ function saveAndUploadFiles(req, res, possbileFiles, files, controlNumber, appli fileInfo.keyname = `${controlNumber}/${fileInfo.filename}`; store.uploadFile(fileInfo, function(err, data){ if (err){ + console.error(err); return error.sendError(req, res, 500, 'unable to process request.'); } else { db.saveFile(application.id, fileInfo, function(err, fileInfo){ if (err){ + console.error(err); return error.sendError(req, res, 500, 'unable to process request.'); } else { @@ -156,6 +159,7 @@ const getControlNumberFileName = function(req, res, reqData) { db.getFile(filePath, function (err, file){ if (err){ + console.error(err); error.sendError(req, res, 500, 'unable to process request.'); } else { @@ -164,6 +168,7 @@ const getControlNumberFileName = function(req, res, reqData) { store.getFile(controlNumber, fileName, function(err, data){ if (err){ + console.error(err); error.sendError(req, res, 404, 'file not found'); } else { @@ -209,6 +214,7 @@ const getControlNumber = function(req, res, reqData){ db.getApplication(controlNumber, function(err, appl, fileData){ if (err) { + console.error(err); return error.sendError(req, res, 500, 'unable to process request.'); } @@ -249,6 +255,7 @@ const getControlNumber = function(req, res, reqData){ db.getApplication(controlNumber, function(err, appl, fileData){ if (err){ + console.error(err); return error.sendError(req, res, 500, 'unable to process request.'); } else { @@ -323,15 +330,17 @@ const postApplication = function(req, res, reqData){ basic.postToBasic(req, res, sch, body) .then((postObject)=>{ const toStoreInDB = db.getDataToStoreInDB(sch, body); - const controlNumber = (Math.floor((Math.random() * 10000000000) + 1)).toString(); //TODO: remove - used for mocks + const controlNumber = postObject.POST['/application'].response.accinstCn; toStoreInDB.controlNumber = controlNumber; db.saveApplication(toStoreInDB, function(err, appl){ if (err){ + console.error(err); return error.sendError(req, res, 500, 'unable to process request.'); } else { saveAndUploadFiles(req, res, possbileFiles, req.files, controlNumber, appl, function(err, data){ if (err) { + console.error(err); return error.sendError(req, res, 500, 'unable to process request.'); } else { @@ -348,7 +357,19 @@ const postApplication = function(req, res, reqData){ }); }) .catch((err)=>{ - return error.sendError(req, res, 500, 'unable to process request.'); + + console.error(err); + if (err instanceof DuplicateContactsError){ + if (err.duplicateContacts){ + return error.sendError(req, res, 400, err.duplicateContacts.length + ' duplicate contacts found.', err.duplicateContacts); + } + else { + return error.sendError(req, res, 400, 'duplicate contacts found.'); + } + } + else { + return error.sendError(req, res, 500, 'unable to process request.'); + } }); } }; diff --git a/src/controllers/validation.js b/src/controllers/validation.js index a095befa..c149d6ee 100644 --- a/src/controllers/validation.js +++ b/src/controllers/validation.js @@ -31,58 +31,10 @@ const fileMimes = [ 'application/pdf' ]; -/** - * Checks to see whether input is a number with num digits. Will return true if digit is not a number as another validation piece will catch and handle that error. - * @param {Number} input - user input to be checked - * @param {Number} num - number of digits input should consist of - * @return {boolean} - true if input has only num digits, otherwise false - */ -function digitCheck(input, num){ - - let valid = true; - - if (typeof input === 'number'){ - - const inputStr = input + ''; - - if (!inputStr.match(new RegExp(`^[0-9]{${num}}$`))){ - - valid = false; - - } - - } - - return valid; - -} - -/** - * Checks that areacode is a valid area code - * @param {Number} input - user input to be validated - * @return {boolean} - whether or not input is a valid area code - */ -function areaCodeFormat(input){ - - return digitCheck(input, 3); - -} - -/** - * Checks that number is a valid phone number - * @param {Number} input - user input to be validated - * @return {boolean} - whether or not input is a valid phone number - */ -function phoneNumberFormat(input){ - - return digitCheck(input, 7); - -} - /** * Removes 'instance' from prop field of validation errors. Used to make fields human readable * - * @param {string} prop - Prop field from validation error + * @param {string} prop - Prop field from validation error * @return {string} */ function removeInstance(prop){ @@ -104,7 +56,7 @@ function removeInstance(prop){ * * @param {string} property - Upper field to combine * @param {string} argument - Field where error is. - * @return {string} - Concatination of property, '.', and argument + * @return {string} - Concatination of property, '.', and argument */ function combinePropArgument(property, argument){ @@ -126,12 +78,12 @@ function combinePropArgument(property, argument){ /** * Creates error object which can be read by error message building function * - * @param {string} field - Field where error occured at - * @param {string} errorType - Type of error returned + * @param {string} field - Field where error occured at + * @param {string} errorType - Type of error returned * @param {string} expectedFieldType - Type that the field is expected to be - * @param {string} enumMessage - Enum message returned by validation - * @param {string} dependency - Fields that are a dependeny of field - * @param {array} anyOfFields - Array of strings of all field included in anyOf + * @param {string} enumMessage - Enum message returned by validation + * @param {string} dependency - Fields that are a dependeny of field + * @param {array} anyOfFields - Array of strings of all field included in anyOf * * @return Error object */ @@ -154,6 +106,10 @@ function makeErrorObj(field, errorType, expectedFieldType, enumMessage, dependen } let requiredFields = []; +/** + * Checks for additional required fields if a missing field has sub fields, stores these fields in requiredFields + * @param {Object} schema - schema to traverse in search for all required fields + */ function checkForExtraRequired(schema){ const keys = schema.properties; for (const key in keys){ @@ -189,9 +145,9 @@ function getAllRequired(schema){ }); } /** Traverses through schema to find field specified. Once found it executes a function on that field in the schema. - * @param {Object} schema - schema to look for field in - * @param {Array} field - Array(String) containing the path to the field to find - * @param {Function} func - Function to be run on the schema of field + * @param {Object} schema - schema to look for field in + * @param {Array} field - Array(String) containing the path to the field to find + * @param {Function} func - Function to be run on the schema of field */ function findField(schema, field, func){ const fieldCopy = JSON.parse(JSON.stringify(field)); @@ -224,11 +180,11 @@ function findField(schema, field, func){ /** * Handles errors where a required field is missing. - * @param {Object} output - Object used to keep track of any errors, will be outputted if any found - * @param {Array} output.errorArray - Array containing error objects which detail errors in schema - * @param {Array} result - Array of all errors from schema validator - * @param {Number} counter - Index of the current error - * @param {Object} schema - schema which input is being validated against + * @param {Object} output - Object used to keep track of any errors, will be outputted if any found + * @param {Array} output.errorArray - Array containing error objects which detail errors in schema + * @param {Array} result - Array of all errors from schema validator + * @param {Number} counter - Index of the current error + * @param {Object} schema - schema which input is being validated against */ function handleMissingError(output, result, counter, schema){ @@ -248,10 +204,10 @@ function handleMissingError(output, result, counter, schema){ /** * Handles errors where a field is the wrong type. - * @param {Object} output - Object used to keep track of any errors, will be outputted if any found - * @param {Array} output.errorArray - Array containing error objects which detail errors in schema - * @param {Array} result - Array of all errors from schema validator - * @param {Number} counter - Index of the current error + * @param {Object} output - Object used to keep track of any errors, will be outputted if any found + * @param {Array} output.errorArray - Array containing error objects which detail errors in schema + * @param {Array} result - Array of all errors from schema validator + * @param {Number} counter - Index of the current error */ function handleTypeError(output, result, counter){ @@ -263,10 +219,10 @@ function handleTypeError(output, result, counter){ /** * Handles errors where a field is formatted wrong. - * @param {Object} output - Object used to keep track of any errors, will be outputted if any found - * @param {Array} output.errorArray - Array containing error objects which detail errors in schema - * @param {Array} result - Array of all errors from schema validator - * @param {Number} counter - Index of the current error + * @param {Object} output - Object used to keep track of any errors, will be outputted if any found + * @param {Array} output.errorArray - Array containing error objects which detail errors in schema + * @param {Array} result - Array of all errors from schema validator + * @param {Number} counter - Index of the current error */ function handleFormatError(output, result, counter){ @@ -277,10 +233,10 @@ function handleFormatError(output, result, counter){ /** * Handles errors where a field is not one of the enum values. - * @param {Object} output - Object used to keep track of any errors, will be outputted if any found - * @param {Array} output.errorArray - Array containing error objects which detail errors in schema - * @param {Array} result - Array of all errors from schema validator - * @param {Number} counter - Index of the current error + * @param {Object} output - Object used to keep track of any errors, will be outputted if any found + * @param {Array} output.errorArray - Array containing error objects which detail errors in schema + * @param {Array} result - Array of all errors from schema validator + * @param {Number} counter - Index of the current error */ function handleEnumError(output, result, counter){ @@ -291,8 +247,8 @@ function handleEnumError(output, result, counter){ /** * Pulls the dependency of a certain field from the error message generated by the schema validator - * @param {Array} result - Array of all errors from schema validator - * @param {Number} counter - Index of the current error + * @param {Array} result - Array of all errors from schema validator + * @param {Number} counter - Index of the current error */ function getDependency(result, counter){ @@ -304,10 +260,10 @@ function getDependency(result, counter){ /** * Handles errors where a field has a dependency which is not provided. - * @param {Object} output - Object used to keep track of any errors, will be outputted if any found - * @param {Array} output.errorArray - Array containing error objects which detail errors in schema - * @param {Array} result - Array of all errors from schema validator - * @param {Number} counter - Index of the current error + * @param {Object} output - Object used to keep track of any errors, will be outputted if any found + * @param {Array} output.errorArray - Array containing error objects which detail errors in schema + * @param {Array} result - Array of all errors from schema validator + * @param {Number} counter - Index of the current error */ function handleDependencyError(output, result, counter){ @@ -322,10 +278,10 @@ function handleDependencyError(output, result, counter){ /** * Creates error object for errors resulting from an anyOf section of the validation schema * - * @param {Object} errorTracking - Error object containing all error to report and the error message to deliver. - * @param {Array} errorTracking.errorArray - Array contain all errors to report to user. - * @param {Array} result - Array of errors found during validation. - * @param {Number} counter - Position in result that the current error being handled is. + * @param {Object} errorTracking - Error object containing all error to report and the error message to deliver. + * @param {Array} errorTracking.errorArray - Array contain all errors to report to user. + * @param {Array} result - Array of errors found during validation. + * @param {Number} counter - Position in result that the current error being handled is. */ function handleAnyOfError(errorTracking, result, counter){ @@ -341,11 +297,11 @@ function handleAnyOfError(errorTracking, result, counter){ /** Get the schema to be used for validating user input * @param {Object} pathData - All data from swagger for the path that has been run - * @return {Object} schemas - fullSchema is the full validation schemas for all permit types. schemaToUse is the validation schema for this route + * @return {Object} schemas - fullSchema is the full validation schemas for all permit types. schemaToUse is the validation schema for this route */ function getValidationSchema(pathData){ - const fileToGet = `src/${pathData['x-validation'].$ref.split('#')[0]}`; - const schemaToGet = pathData['x-validation'].$ref.split('#')[1]; + const fileToGet = `src/${pathData['x-validation'].split('#')[0]}`; + const schemaToGet = pathData['x-validation'].split('#')[1]; const applicationSchema = include(fileToGet); return { 'fullSchema':applicationSchema, @@ -387,12 +343,12 @@ function processErrors(errors, processedErrors, schema){ } /** Validates the fields in user input - * @param {Object} body - Input from user to be validated - * @param {Object} pathData - All data from swagger for the path that has been run - * @param {Object} derefSchema - schema to be used for validating input - * @return {Array} - Array of ValidationErrors from validation + * @param {Object} body - Input from user to be validated + * @param {Object} pathData - All data from swagger for the path that has been run + * @param {Object} validationSchema - schema to be used for validating input, same as validation.json without refs + * @return {Array} - Array of ValidationErrors from validation */ -function validateBody(body, pathData, derefSchema){ +function validateBody(body, pathData, validationSchema){ const processedFieldErrors = { errorArray:[] }; @@ -403,12 +359,10 @@ function validateBody(body, pathData, derefSchema){ for (key in applicationSchema){ v.addSchema(applicationSchema[key], key); } - v.customFormats.areaCodeFormat = areaCodeFormat; - v.customFormats.phoneNumberFormat = phoneNumberFormat; const val = v.validate(body, schemaToUse); const error = val.errors; if (error.length > 0){ - processErrors(error, processedFieldErrors, derefSchema); + processErrors(error, processedFieldErrors, validationSchema); } return processedFieldErrors; } @@ -457,7 +411,8 @@ function makePathReadable(input){ } /** - * [buildFormatErrorMessage description] + * Creates error message for format errors + * * @param {String} fullPath - path to field where error is at * @return {String} - error message to be given to user */ @@ -509,10 +464,10 @@ function concatErrors(errorMessages){ /** * Creates error messages for all file errors - * @param {Object} output - Error object containing all error to report and the error message to deliver. + * @param {Object} output - Error object containing all error to report and the error message to deliver. * @param {Array} output.errorArray - Array contain all errors to report to user. - * @param {Object} error - error object to be processed - * @param {Array} messages - Array of all error messages to be returned + * @param {Object} error - error object to be processed + * @param {Array} messages - Array of all error messages to be returned */ function generateFileErrors(output, error, messages){ const reqFile = `${makePathReadable(error.field)} is a required file.`; @@ -550,11 +505,11 @@ function generateFileErrors(output, error, messages){ /** * Creates error messages for all field errors - * @param {Object} output - Error object containing all error to report and the error message to deliver. - * @param {Array} output.errorArray - Array contain all errors to report to user. - * @param {Object} error - error object to be processed - * @param {Array} messages - Array of all error messages to be returned - * @return {String} - All field error messages concated together + * @param {Object} output - Error object containing all error to report and the error message to deliver. + * @param {Array} output.errorArray - Array contain all errors to report to user. + * @param {Object} error - error object to be processed + * @param {Array} messages - Array of all error messages to be returned + * @return {String} - All field error messages concated together */ function generateErrorMesage(output){ @@ -612,7 +567,7 @@ function generateErrorMesage(output){ /** * Checks schema for any files that could be provided. * @param {Object} schema - Schema for an application - * @param {Array} toCheck - List of files to check for, and if present, validate + * @param {Array} toCheck - List of files to check for, and if present, validate */ function checkForFilesInSchema(schema, toCheck){ const keys = Object.keys(schema); @@ -642,7 +597,7 @@ function checkForFilesInSchema(schema, toCheck){ /** * Gets basic information about a given file and returns it - * @param {Array} file - Information about file, include the contents of it in hex + * @param {Array} file - Information about file, include the contents of it in hex * @param {Object} constraints - Description of how to validate file * @return {Object} - basic information about file */ @@ -669,7 +624,7 @@ function getFileInfo(file, constraints){ /** * Driving function for validating file - * @param {Array} uploadFile - Information about file, include the contents of it in hex + * @param {Array} uploadFile - Information about file, include the contents of it in hex * @param {Object} validationConstraints - Description of how to validate file * @param {String} fileName - Name of file being validated * @return {Array} - Array of all error objects for this file @@ -708,12 +663,12 @@ function validateFile(uploadFile, validationConstraints, fileName){ /** * Checks the length of all fields with a maxLength field in schema - * @param {Object} schema - Section of the validation schema being used - * @param {Object} input - User input being validated - * @param {Object} processedFieldErrors - Current object containing errors - * @param {Array} processedFieldErrors.errorArray - Array of all errors found so far - * @param {String} path - Path to field being checked - * @return {Array} - Array of error objects representing all errors found so far + * @param {Object} schema - Section of the validation schema being used + * @param {Object} input - User input being validated + * @param {Object} processedFieldErrors - Current object containing errors + * @param {Array} processedFieldErrors.errorArray - Array of all errors found so far + * @param {String} path - Path to field being checked + * @return {Array} - Array of error objects representing all errors found so far */ function checkFieldLengths(schema, input, processedFieldErrors, path){ const keys = Object.keys(schema); @@ -728,7 +683,7 @@ function checkFieldLengths(schema, input, processedFieldErrors, path){ case 'properties': checkFieldLengths(schema.properties, input, processedFieldErrors, path); break; - default: + default:{ let field; if (path === ''){ field = `${key}`; @@ -757,28 +712,67 @@ function checkFieldLengths(schema, input, processedFieldErrors, path){ } break; } + } }); return processedFieldErrors; } +function checkForIndividualIsCitizen(input, processedFieldErrors){ + if (input.tempOutfitterFields && input.applicantInfo){ + if (!input.applicantInfo.orgType || input.applicantInfo.orgType === 'Individual'){ + if ((typeof input.tempOutfitterFields.individualIsCitizen) !== 'boolean'){ + processedFieldErrors.errorArray.push(makeErrorObj('tempOutfitterFields.individualIsCitizen', 'missing')); + } + } + } + return processedFieldErrors; +} + +function checkForSmallBusiness(input, processedFieldErrors){ + if (input.tempOutfitterFields && input.applicantInfo){ + if (input.applicantInfo.orgType && input.applicantInfo.orgType !== 'Individual'){ + if ((typeof input.tempOutfitterFields.smallBusiness) !== 'boolean'){ + processedFieldErrors.errorArray.push(makeErrorObj('tempOutfitterFields.smallBusiness', 'missing')); + } + } + } + return processedFieldErrors; +} + +function checkForOrgName(input, processedFieldErrors){ + if (input.applicantInfo){ + if (input.applicantInfo.orgType && input.applicantInfo.orgType !== 'Individual'){ + if (!input.applicantInfo.organizationName || input.applicantInfo.organizationName.length <= 0){ + processedFieldErrors.errorArray.push(makeErrorObj('applicantInfo.organizationName', 'missing')); + } + } + } + return processedFieldErrors; +} + +function additionalValidation(schema, input, processedFieldErrors){ + processedFieldErrors = checkFieldLengths(schema, input, processedFieldErrors, ''); + processedFieldErrors = checkForOrgName(input, processedFieldErrors); + processedFieldErrors = checkForIndividualIsCitizen(input, processedFieldErrors); + processedFieldErrors = checkForSmallBusiness(input, processedFieldErrors); + return processedFieldErrors; +} + /** * Drives validation of fields - * @param {Object} body - User input - * @param {Object} pathData - information about path - * @param {Object} derefSchema - schema to be used for validating input - * @return {Array} - Array of error objects for every error with fields + * @param {Object} body - User input + * @param {Object} pathData - information about path + * @param {Object} validationSchema - schema to be used for validating input, same as validation.json without refs + * @return {Array} - Array of error objects for every error with fields */ -function getFieldValidationErrors(body, pathData, derefSchema){ +function getFieldValidationErrors(body, pathData, validationSchema){ - let processedFieldErrors = validateBody(body, pathData, derefSchema); - processedFieldErrors = checkFieldLengths(derefSchema, body, processedFieldErrors, ''); + let processedFieldErrors = validateBody(body, pathData, validationSchema); + processedFieldErrors = additionalValidation(validationSchema, body, processedFieldErrors, ''); return processedFieldErrors; } -module.exports.digitCheck = digitCheck; -module.exports.areaCodeFormat = areaCodeFormat; -module.exports.phoneNumberFormat = phoneNumberFormat; module.exports.removeInstance = removeInstance; module.exports.combinePropArgument = combinePropArgument; module.exports.makeErrorObj = makeErrorObj; @@ -805,3 +799,6 @@ module.exports.checkForFilesInSchema = checkForFilesInSchema; module.exports.getFileInfo = getFileInfo; module.exports.validateFile = validateFile; module.exports.getFieldValidationErrors = getFieldValidationErrors; +module.exports.checkForSmallBusiness = checkForSmallBusiness; +module.exports.checkForIndividualIsCitizen = checkForIndividualIsCitizen; +module.exports.checkForOrgName = checkForOrgName; diff --git a/src/controllers/validation.json b/src/controllers/validation.json index f8f096ba..f1f8016d 100644 --- a/src/controllers/validation.json +++ b/src/controllers/validation.json @@ -221,7 +221,7 @@ "type": "object", "allOf":[ {"$ref":"applicantInfoBase"}, - { + { "properties": { "organizationName": { "basicField":"contName", @@ -274,14 +274,14 @@ "startDateTime": { "default":"", "fromIntake":true, - "pattern":"^(19|20)\\d\\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T(0\\d|1\\d|2[0-3]):(0\\d|1\\d|2[0-3]):(0\\d|1\\d|2[0-3])Z$", + "pattern":"^(19|20)\\d\\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T(0\\d|1\\d|2[0-3]):([0-5]\\d):([0-5]\\d)Z$", "store":["middleLayer:startDatetime"], "type": "string" }, "endDateTime": { "default":"", "fromIntake":true, - "pattern":"^(19|20)\\d\\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T(0\\d|1\\d|2[0-3]):(0\\d|1\\d|2[0-3]):(0\\d|1\\d|2[0-3])Z$", + "pattern":"^(19|20)\\d\\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T(0\\d|1\\d|2[0-3]):([0-5]\\d):([0-5]\\d)Z$", "store":["middleLayer:endDatetime"], "type": "string" }, @@ -293,9 +293,38 @@ }, "formName":{ "basicField":"formName", - "default":"FS-2700-3b", + "default":"FS-2700-3B", + "fromIntake":false, "store":["middleLayer:formName", "basic:/application"], "type":"string" + }, + "purpose":{ + "basicField":"purpose", + "default":"", + "fromIntake":false, + "madeOf":{ + "fields":[ + { + "fromIntake":true, + "field":"noncommercialFields.activityDescription" + }, + { + "fromIntake":true, + "field":"noncommercialFields.locationDescription" + }, + { + "fromIntake":true, + "field":"noncommercialFields.startDateTime" + }, + { + "fromIntake":true, + "field":"noncommercialFields.endDateTime" + } + ], + "function":"concat" + }, + "store":["basic:/application"], + "type":"string" } }, "required": ["activityDescription", "locationDescription", "startDateTime", "endDateTime", "numberParticipants"] @@ -306,19 +335,19 @@ "properties": { "areaCode": { "basicField":"areaCode", - "default":0, + "default":"0", "fromIntake":true, - "format": "areaCodeFormat", + "pattern":"^[0-9]{3}$", "store":["basic:/contact/phone"], - "type": "integer" + "type": "string" }, "number": { "basicField":"phoneNumber", - "default":0, + "default":"0", "fromIntake":true, - "format": "phoneNumberFormat", + "pattern":"^[0-9]{7}$", "store":["basic:/contact/phone"], - "type": "integer" + "type": "string" }, "extension": { "basicField":"extension", @@ -411,6 +440,12 @@ "fromIntake":true, "store":["middleLayer:website"], "type": "string" + }, + "countryName":{ + "basicField":"countryName", + "default":"United States of America", + "store":["basic:/contact/address"], + "type": "string" } }, "required": ["firstName", "lastName", "dayPhone", "emailAddress", "mailingAddress", "mailingCity", "mailingZIP", "mailingState"] @@ -444,7 +479,19 @@ "basicField":"securityId", "default":"", "fromIntake":false, - "madeOf":["region","forest","district"], + "madeOf":{ + "fields":[ + { + "fromIntake":true, + "field":"region" + }, + { + "fromIntake":true, + "field":"forest" + } + ], + "function":"concat" + }, "store":["basic:/application", "basic:/contact/address", "basic:/contact/phone"], "type" : "string" }, @@ -452,7 +499,23 @@ "basicField":"managingOrg", "default":"", "fromIntake":false, - "madeOf":["region","forest","district"], + "madeOf":{ + "fields":[ + { + "fromIntake":true, + "field":"region" + }, + { + "fromIntake":true, + "field":"forest" + }, + { + "fromIntake":true, + "field":"district" + } + ], + "function":"concat" + }, "store":["basic:/application"], "type" : "string" }, @@ -460,15 +523,43 @@ "basicField":"adminOrg", "default":"", "fromIntake":false, - "madeOf":["region","forest","district"], + "madeOf":{ + "fields":[ + { + "fromIntake":true, + "field":"region" + }, + { + "fromIntake":true, + "field":"forest" + }, + { + "fromIntake":true, + "field":"district" + } + ], + "function":"concat" + }, "store":["basic:/application"], "type" : "string" }, - "ePermitID":{ - "basicField":"ePermitID", + "epermitId":{ + "basicField":"epermitId", "default":"", "fromIntake":false, - "madeOf":["region","forest","00000"], + "madeOf":{ + "fields":[ + { + "fromIntake":true, + "field":"region" + }, + { + "fromIntake":true, + "field":"forest" + } + ], + "function":"ePermitId" + }, "store":["basic:/application"], "type" : "string" }, @@ -479,11 +570,39 @@ "store":["basic:/application"], "type" : "integer" }, - "contCN":{ + "contact":{ "basicField":"contact", "default":"", "fromIntake":false, - "store":["basic:/contact/phone", "basic:/contact/address", "basic:/contact/person"], + "store":["basic:/contact/phone", "basic:/contact/address"], + "type" : "string" + }, + "contId":{ + "basicField":"contId", + "default":"", + "fromIntake":false, + "madeOf":{ + "fields":[ + { + "fromIntake":true, + "field":"applicantInfo.lastName" + }, + { + "fromIntake":false, + "value":"," + }, + { + "fromIntake":true, + "field":"applicantInfo.firstName" + }, + { + "fromIntake":true, + "field":"applicantInfo.organizationName" + } + ], + "function":"contId" + }, + "store":["basic:/contact/person", "basic:/contact/organization"], "type" : "string" } }, diff --git a/test/authentication.js b/test/authentication.js index 6bca9473..4984fcb1 100644 --- a/test/authentication.js +++ b/test/authentication.js @@ -21,6 +21,9 @@ const util = include('test/utility.js'); const chai = require('chai'); const expect = chai.expect; +const bcrypt = require('bcrypt-nodejs'); +const db = include('src/controllers/db.js'); +const models = include('src/models'); const factory = require('unionized'); const loginFactory = factory.factory({'username': null, 'password': null}); @@ -28,10 +31,75 @@ const loginFactory = factory.factory({'username': null, 'password': null}); const noncommercialInput = include('test/data/testInputNoncommercial.json'); const noncommercialFactory = factory.factory(noncommercialInput); +const adminUsername = 'admin' + (Math.floor((Math.random() * 1000000) + 1)).toString(); +const adminPassword = 'pwd' + (Math.floor((Math.random() * 1000000) + 1)).toString(); + +const userUsername = 'user' + (Math.floor((Math.random() * 1000000) + 1)).toString(); +const userPassword = 'pwd' + (Math.floor((Math.random() * 1000000) + 1)).toString(); + //******************************************************************* describe('authentication validation', function() { + let token; + let userToken; + + before(function(done) { + + models.users.sync({ force: false }); + const salt = bcrypt.genSaltSync(10); + const hash = bcrypt.hashSync(adminPassword, salt); + const userHash = bcrypt.hashSync(userPassword, salt); + + const adminUser = { + userName: adminUsername, + passHash: hash, + userRole: 'admin' + }; + const userUser = { + userName: userUsername, + passHash: userHash, + userRole: 'user' + }; + + db.saveUser(adminUser, function(err, usr){ + if (err){ + return false; + } + else { + db.saveUser(userUser, function(err, usr){ + if (!err){ + util.getToken(adminUsername, adminPassword, function(t){ + token = t; + }); + util.getToken(userUsername, userPassword, function(t2){ + userToken = t2; + return done(); + }); + } + }); + } + }); + + }); + + after(function(done) { + + db.deleteUser(adminUsername, function(err){ + if (err){ + return false; + } + else { + db.deleteUser(userUsername, function(err){ + if (!err){ + return done(); + } + }); + } + }); + + }); + it('should return valid json with a 401 status code for invalid username or password', function(done) { request(server) .post('/auth') @@ -44,7 +112,7 @@ describe('authentication validation', function() { request(server) .post('/auth') .set('Accept', 'application/json') - .send(loginFactory.create({username: process.env.ADMINROLE_USER, password: process.env.ADMINROLE_PWD})) + .send(loginFactory.create({username: adminUsername, password: adminPassword})) .expect(function(res){ expect(res.body).to.have.property('token'); }) @@ -67,20 +135,7 @@ describe('authentication validation', function() { .expect(401, done); }); -}); - -describe('autherization with a token with admin role', function() { - - let token; - - before(function(done) { - util.getToken(function(t){ - token = t; - return done(); - }); - }); - - it('should return valid json with 200 for a noncommercial GET request', function(done) { + it('should return valid json with 200 for a noncommercial GET request when using admin role autherization', function(done) { request(server) .post('/permits/applications/special-uses/noncommercial/') .set('x-access-token', token) @@ -89,32 +144,10 @@ describe('autherization with a token with admin role', function() { .expect(200, done); }); -}); - -describe('autherization with a token with user (unauthorized) role', function() { - - let token; - - before(function(done) { - request(server) - .post('/auth') - .set('Accept', 'application/json') - .send(loginFactory.create({username: process.env.USERROLE_USER, password: process.env.USERROLE_PWD})) - .expect('Content-Type', /json/) - .expect(200) - .end(function(err, res) { - if (err) { - console.error(err); - } - token = res.body.token; - done(); - }); - }); - - it('should return valid json with 403 for a noncommercial GET request with an unauthorized token provided', function(done) { + it('should return valid json with 403 for a noncommercial GET request when using user (unauthorized) role authentication', function(done) { request(server) .get('/permits/applications/special-uses/noncommercial/123456789/') - .set('x-access-token', token) + .set('x-access-token', userToken) .expect('Content-Type', /json/) .expect(403, done); }); diff --git a/test/data/testInputNoncommercial.json b/test/data/testInputNoncommercial.json index ac302e2b..a6c740c9 100644 --- a/test/data/testInputNoncommercial.json +++ b/test/data/testInputNoncommercial.json @@ -8,8 +8,8 @@ "firstName": "John", "lastName": "Doe", "dayPhone": { - "areaCode": 541, - "number": 8156141, + "areaCode": "541", + "number": "8156141", "extension": "0", "phoneType": "BUSINESS" }, diff --git a/test/data/testInputTempOutfitters.json b/test/data/testInputTempOutfitters.json index 547ad01b..ca4c5c9d 100644 --- a/test/data/testInputTempOutfitters.json +++ b/test/data/testInputTempOutfitters.json @@ -8,8 +8,8 @@ "firstName": "John", "lastName": "Doe", "dayPhone": { - "areaCode": 202, - "number": 5551234, + "areaCode": "202", + "number": "5551234", "extension": "0", "phoneType": "BUSINESS" }, @@ -18,7 +18,8 @@ "mailingCity": "Washington", "mailingState": "DC", "mailingZIP": "12345", - "orgType": "Limited Liability Company" + "orgType": "Limited Liability Company", + "organizationName":"ABC Company" }, "type": "tempOutfitters", "tempOutfitterFields": { diff --git a/test/data/testObjects.json b/test/data/testObjects.json index 0b032ed0..c1057eb4 100644 --- a/test/data/testObjects.json +++ b/test/data/testObjects.json @@ -1,9 +1,7 @@ { "outfitters":{ "pathData":{ - "x-validation": { - "$ref":"controllers/validation.json#tempOutfitterApplication" - }, + "x-validation":"controllers/validation.json#tempOutfitterApplication", "tags": [ "Outfitter and Guide Permit" ], @@ -627,9 +625,7 @@ }, "noncommercial":{ "pathData":{ - "x-validation": { - "$ref":"controllers/validation.json#noncommercialApplication" - }, + "x-validation": "controllers/validation.json#noncommercialApplication", "tags": [ "Noncommercial Permit" ], diff --git a/test/functions-test.js b/test/functions-test.js index 8fc43dfa..44fbf7be 100644 --- a/test/functions-test.js +++ b/test/functions-test.js @@ -25,41 +25,6 @@ validationJs.functions = require('../src/controllers/validation.js'); describe('Function Tests: validation.js ', function(){ - it('digitCheck should return true with a valid input (123)', function(){ - expect( validationJs.functions.digitCheck(123, 3) ) - .to.be.equal(true); - }); - - it('digitCheck should return false with an invalid input (1234)', function(){ - expect( validationJs.functions.digitCheck(1234, 3) ) - .to.be.equal(false); - }); - - it('digitCheck should return true if input is not a number(\'1234\')', function(){ - expect( validationJs.functions.digitCheck('1234', 3) ) - .to.be.equal(true); - }); - - it('areaCodeFormat should return true with a valid input (123)', function(){ - expect( validationJs.functions.areaCodeFormat(123) ) - .to.be.equal(true); - }); - - it('areaCodeFormat should return false with an invalid input (1234)', function(){ - expect( validationJs.functions.areaCodeFormat(1234) ) - .to.be.equal(false); - }); - - it('phoneNumberFormat should return true with a valid input (1234567)', function(){ - expect( validationJs.functions.phoneNumberFormat(1234567) ) - .to.be.equal(true); - }); - - it('phoneNumberFormat should return false with an invalid input (1234)', function(){ - expect( validationJs.functions.phoneNumberFormat(1234) ) - .to.be.equal(false); - }); - it('removeInstance should return just the property with an input (abc.xyz)', function(){ expect( validationJs.functions.removeInstance('abc.xyz') ) .to.be.equal('xyz'); diff --git a/test/noncommercial.js b/test/noncommercial.js index 284a035e..637db476 100644 --- a/test/noncommercial.js +++ b/test/noncommercial.js @@ -28,6 +28,12 @@ const testURL = '/permits/applications/special-uses/noncommercial/'; const chai = require('chai'); const expect = chai.expect; +const bcrypt = require('bcrypt-nodejs'); +const db = include('src/controllers/db.js'); +const models = include('src/models'); + +const adminUsername = 'admin' + (Math.floor((Math.random() * 1000000) + 1)).toString(); +const adminPassword = 'pwd' + (Math.floor((Math.random() * 1000000) + 1)).toString(); //******************************************************************* //Mock Input @@ -43,15 +49,45 @@ describe('Integration tests - noncommercial', function(){ before(function(done) { - util.getToken(function(t){ - - token = t; - return done(); - + models.users.sync({ force: false }); + const salt = bcrypt.genSaltSync(10); + const hash = bcrypt.hashSync(adminPassword, salt); + + const adminUser = { + userName: adminUsername, + passHash: hash, + userRole: 'admin' + }; + + db.saveUser(adminUser, function(err, usr){ + if (err){ + return false; + } + else { + + util.getToken(adminUsername, adminPassword, function(t){ + token = t; + return done(); + }); + + } }); }); + after(function(done) { + + db.deleteUser(adminUsername, function(err){ + if (err){ + return false; + } + else { + return done(); + } + }); + + }); + it('should return valid json with a 400 status code for noncommercial POST request without an applicantInfo object', function(done) { request(server) @@ -67,6 +103,32 @@ describe('Integration tests - noncommercial', function(){ }); + it('should return valid json for noncommercial POST request (contact search - uses existing contact)', function(done) { + let noncommercialInput = noncommercialFactory.create(); + noncommercialInput.applicantInfo.firstName = 'Fname'; + noncommercialInput.applicantInfo.lastName = 'Lname'; + request(server) + .post(testURL) + .set('x-access-token', token) + .send(noncommercialInput) + .expect('Content-Type', /json/) + .expect(200, done); + + }); + + it('should return valid json with 400 status for noncommercial POST request (contact search - duplicate contacts error)', function(done) { + let noncommercialInput = noncommercialFactory.create(); + noncommercialInput.applicantInfo.organizationName = 'Temp Organization'; + noncommercialInput.applicantInfo.orgType = 'Corporation'; + request(server) + .post(testURL) + .set('x-access-token', token) + .send(noncommercialInput) + .expect('Content-Type', /json/) + .expect(400, done); + + }); + it('should return valid json for noncommercial POST request (controlNumber to be used in GET)', function(done) { request(server) diff --git a/test/outfitters.js b/test/outfitters.js index 92a60265..23365f37 100644 --- a/test/outfitters.js +++ b/test/outfitters.js @@ -29,6 +29,12 @@ const testURL = '/permits/applications/special-uses/commercial/temp-outfitters/' const chai = require('chai'); const expect = chai.expect; +const bcrypt = require('bcrypt-nodejs'); +const db = include('src/controllers/db.js'); +const models = include('src/models'); + +const adminUsername = 'admin' + (Math.floor((Math.random() * 1000000) + 1)).toString(); +const adminPassword = 'pwd' + (Math.floor((Math.random() * 1000000) + 1)).toString(); const specialUses = {}; @@ -56,16 +62,47 @@ describe('API Routes: permits/special-uses/commercial/outfitters', function() { let token; let postControlNumber; + let postFileName; before(function(done) { - util.getToken(function(t){ + models.users.sync({ force: false }); + const salt = bcrypt.genSaltSync(10); + const hash = bcrypt.hashSync(adminPassword, salt); + + const adminUser = { + userName: adminUsername, + passHash: hash, + userRole: 'admin' + }; + + db.saveUser(adminUser, function(err, usr){ + if (err){ + return false; + } + else { + + util.getToken(adminUsername, adminPassword, function(t){ + token = t; + return done(); + }); + + } + }); + + }); - token = t; - return done(); + after(function(done) { + db.deleteUser(adminUsername, function(err){ + if (err){ + return false; + } + else { + return done(); + } }); - + }); it('should return valid json for tempOutfitters POST (controlNumber to be used in GET)', function(done) { @@ -97,24 +134,7 @@ describe('API Routes: permits/special-uses/commercial/outfitters', function() { }); -}); - -describe('tempOutfitters POST: file validated', function(){ - - let token; - - before(function(done) { - - util.getToken(function(t){ - - token = t; - return done(); - - }); - - }); - - describe('tempOutfitters POST: required files checks', function(){ + describe('tempOutfitters POST files:', function(){ it('should return errors for file that is too large', function(){ expect ( @@ -206,31 +226,12 @@ describe('tempOutfitters POST: file validated', function(){ }); }); -}); - -describe('tempOutfitters GET: files validated', function(){ - let token; - - let postControlNumber; - let postFileName; - - before(function(done) { - - util.getToken(function(t){ - - token = t; - return done(); - - }); - - }); - - describe('tempOutfitters GET/POST: post a new application with files, get that application, get file', function(){ + describe('tempOutfitters GET/POST files: post a new application with files, get that application, get file', function(){ it('should return valid json when application submitted with three required files', function(done) { - this.timeout(5000); + this.timeout(10000); request(server) .post('/permits/applications/special-uses/commercial/temp-outfitters/') @@ -285,12 +286,11 @@ describe('tempOutfitters GET: files validated', function(){ }); }); -}); -describe('tempOutfitters GET: zip file validated', function(){ +}); +describe('tempOutfitters GET/POST zip file validation: ', function(){ let token; - let postControlNumber; before(function(done) { @@ -299,24 +299,50 @@ describe('tempOutfitters GET: zip file validated', function(){ AWS.restore('S3'); } - util.getToken(function(t){ - - token = t; - return done(); - + models.users.sync({ force: false }); + const salt = bcrypt.genSaltSync(10); + const hash = bcrypt.hashSync(adminPassword, salt); + + const adminUser = { + userName: adminUsername, + passHash: hash, + userRole: 'admin' + }; + + db.saveUser(adminUser, function(err, usr){ + if (err){ + return false; + } + else { + + util.getToken(adminUsername, adminPassword, function(t){ + token = t; + return done(); + }); + + } }); - + }); - after(function(done){ + after(function(done) { + if (process.env.npm_config_mock === 'Y'){ AWS.mock('S3', 'getObject', tempOutfitterObjects.mockS3Get); } - - return done(); + + db.deleteUser(adminUsername, function(err){ + if (err){ + return false; + } + else { + return done(); + } + }); + }); - - describe('tempOutfitters GET/POST: post a new application with files, get that application, get files zipped ', function(){ + + describe('post a new application with files, get that application, get files zipped', function(){ it('should return valid json when application submitted with three required files', function(done) { @@ -336,9 +362,11 @@ describe('tempOutfitters GET: zip file validated', function(){ .expect(200, done); }); - + it('should return valid zip when getting outfitters files using the controlNumber returned from POST', function(done) { + this.timeout(10000); + request(server) .get(`${testURL}${postControlNumber}/files/`) .set('x-access-token', token) @@ -355,5 +383,5 @@ describe('tempOutfitters GET: zip file validated', function(){ }); }); - }); + }); }); diff --git a/test/test.js b/test/test.js index 0bc9d244..51dba573 100644 --- a/test/test.js +++ b/test/test.js @@ -16,12 +16,18 @@ const include = require('include')(__dirname); const request = require('supertest'); const server = include('src/index.js'); +const factory = require('unionized'); const util = require('./utility.js'); const chai = require('chai'); const expect = chai.expect; -const factory = require('unionized'); +const bcrypt = require('bcrypt-nodejs'); +const db = include('src/controllers/db.js'); +const models = include('src/models'); + +const adminUsername = 'admin' + (Math.floor((Math.random() * 1000000) + 1)).toString(); +const adminPassword = 'pwd' + (Math.floor((Math.random() * 1000000) + 1)).toString(); const testURL = '/permits/applications/special-uses/noncommercial/'; const noncommercialInput = include('test/data/testInputNoncommercial.json'); @@ -36,14 +42,44 @@ describe('FS ePermit API', function() { before(function(done) { - util.getToken(function(t){ - - token = t; - return done(); - + models.users.sync({ force: false }); + const salt = bcrypt.genSaltSync(10); + const hash = bcrypt.hashSync(adminPassword, salt); + + const adminUser = { + userName: adminUsername, + passHash: hash, + userRole: 'admin' + }; + + db.saveUser(adminUser, function(err, usr){ + if (err){ + return false; + } + else { + + util.getToken(adminUsername, adminPassword, function(t){ + token = t; + return done(); + }); + + } }); }); + + after(function(done) { + + db.deleteUser(adminUsername, function(err){ + if (err){ + return false; + } + else { + return done(); + } + }); + + }); it('should return html format if web page', function(done) { diff --git a/test/utility.js b/test/utility.js index 78adde1f..7c762532 100644 --- a/test/utility.js +++ b/test/utility.js @@ -20,14 +20,14 @@ const server = include('src/index.js'); //******************************************************************* -function getToken(callback){ +function getToken(username, password, callback){ let token; request(server) .post('/auth') .set('Accept', 'application/json') - .send({ 'username': process.env.ADMINROLE_USER, 'password': process.env.ADMINROLE_PWD }) + .send({ 'username': username, 'password': password }) .expect('Content-Type', /json/) .expect(200) .end(function(err, res) { diff --git a/test/validation-test.js b/test/validation-test.js index be65cd81..daca94d0 100644 --- a/test/validation-test.js +++ b/test/validation-test.js @@ -82,15 +82,21 @@ describe('outfitters validation ', function(){ ) .to.be.equal(1); }); - it('should report issues when no tempOutfitterFields/client charges is provided', function(){ + it('should report issues when neither tempOutfitterFields/advertising url nor tempOutfitterFields/advertising description is provided', function(){ expect ( - specialUses.validate.validateBody(tempOutfitterFactory.create({'tempOutfitterFields.clientCharges' : undefined}), outfittersObjects.pathData, outfittersObjects.derefSchema).errorArray.length + specialUses.validate.validateBody(tempOutfitterFactory.create({'tempOutfitterFields.advertisingURL' : undefined, 'tempOutfitterFields.advertisingDescription' : undefined}), outfittersObjects.pathData, { errorArray: [] }).errorArray.length ) .to.be.equal(1); }); - it('should report issues when neither tempOutfitterFields/advertising url nor tempOutfitterFields/advertising description is provided', function(){ + it('should report issues when no tempOutfitterFields/small business is provided', function(){ + expect ( + specialUses.validate.checkForSmallBusiness(tempOutfitterFactory.create({'tempOutfitterFields.smallBusiness' : undefined}), { errorArray: [] }).errorArray.length + ) + .to.be.equal(1); + }); + it('should report issues when no tempOutfitterFields/individual is citizen is provided', function(){ expect ( - specialUses.validate.validateBody(tempOutfitterFactory.create({'tempOutfitterFields.advertisingURL' : undefined, 'tempOutfitterFields.advertisingDescription' : undefined}), outfittersObjects.pathData, outfittersObjects.derefSchema).errorArray.length + specialUses.validate.checkForIndividualIsCitizen(tempOutfitterFactory.create({'applicantInfo.orgType':'Individual', 'tempOutfitterFields.individualIsCitizen' : undefined}), { errorArray: [] }).errorArray.length ) .to.be.equal(1); }); @@ -197,6 +203,12 @@ describe('noncommercial validation', function(){ ) .to.be.equal(1); }); + it('should report issues when no organization name is provided', function(){ + expect ( + specialUses.validate.checkForOrgName(noncommercialFactory.create({'applicantInfo.orgType' : 'Corporation'}), { errorArray: [] }).errorArray.length + ) + .to.be.equal(1); + }); it('should report issues when no type is provided', function(){ expect ( specialUses.validate.validateBody(noncommercialFactory.create({'type' : undefined}), noncommercialObjects.pathData, noncommercialObjects.derefSchema).errorArray.length @@ -255,13 +267,13 @@ describe('noncommercial validation', function(){ }); it('should report issues when the wrong type of applicantInfo/day phone/area code is provided', function(){ expect ( - specialUses.validate.validateBody(noncommercialFactory.create({'applicantInfo.dayPhone.areaCode' : '123'}), noncommercialObjects.pathData, noncommercialObjects.derefSchema).errorArray.length + specialUses.validate.validateBody(noncommercialFactory.create({'applicantInfo.dayPhone.areaCode' : 123}), noncommercialObjects.pathData, noncommercialObjects.derefSchema).errorArray.length ) .to.be.equal(1); }); it('should report issues when the wrong type of applicantInfo/day phone/number is provided', function(){ expect ( - specialUses.validate.validateBody(noncommercialFactory.create({'applicantInfo.dayPhone.number' : '123'}), noncommercialObjects.pathData, noncommercialObjects.derefSchema).errorArray.length + specialUses.validate.validateBody(noncommercialFactory.create({'applicantInfo.dayPhone.number' : 123}), noncommercialObjects.pathData, noncommercialObjects.derefSchema).errorArray.length ) .to.be.equal(1); }); @@ -318,13 +330,13 @@ describe('noncommercial validation', function(){ it('should report issues when the wrong format of applicantInfo/day phone/area code is provided', function(){ expect ( - specialUses.validate.validateBody(noncommercialFactory.create({'applicantInfo.dayPhone.areaCode' : 12}), noncommercialObjects.pathData, noncommercialObjects.derefSchema).errorArray.length + specialUses.validate.validateBody(noncommercialFactory.create({'applicantInfo.dayPhone.areaCode' : '12'}), noncommercialObjects.pathData, noncommercialObjects.derefSchema).errorArray.length ) .to.be.equal(1); }); it('should report issues when the wrong format of applicantInfo/day phone/number is provided', function(){ expect ( - specialUses.validate.validateBody(noncommercialFactory.create({'applicantInfo.dayPhone.areaCode' : 12}), noncommercialObjects.pathData, noncommercialObjects.derefSchema).errorArray.length + specialUses.validate.validateBody(noncommercialFactory.create({'applicantInfo.dayPhone.areaCode' : '12'}), noncommercialObjects.pathData, noncommercialObjects.derefSchema).errorArray.length ) .to.be.equal(1); });