diff --git a/api-docs.yml b/api-docs.yml
index 85097178..e1f5db50 100644
--- a/api-docs.yml
+++ b/api-docs.yml
@@ -279,6 +279,70 @@ paths:
items:
$ref: '#/components/schemas/RRPair'
description: successful response
+ patch:
+ tags:
+ - services
+ requestBody:
+ description: An RR pair to add to this service
+ content:
+ application/json:
+ schema:
+ description: the service to update
+ $ref: '#/components/schemas/RRPair'
+ responses:
+ 200:
+ description: successful response
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/RRPair'
+ description: successful response
+ /services/{id}/recorded:
+ get:
+ description: Gets all recorded pairs from Live Invocation recording for this service
+ tags:
+ - services
+ responses:
+ 200:
+ description: successful response
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/RRPair'
+ description: successful response
+ /services/{id}/recorded/{rrPaidId}:
+ patch:
+ description: Merges a recorded RR pair into the service
+ tags:
+ - services
+ responses:
+ 200:
+ description: successful response
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/RRPair'
+ description: successful response
+ delete:
+ description: Deletes a recorded RR pair
+ tags:
+ - services
+ responses:
+ 200:
+ description: successful response
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/RRPair'
+ description: successful response
/services/fromSpec/upload:
post:
@@ -701,6 +765,55 @@ paths:
items:
$ref: '#/components/schemas/Recording'
description: successful response
+ /recording/{id}/stop:
+ patch:
+ description: Stops a recorder. A stopped recorder will not be deleted, but will not accept any more incoming requests.
+ parameters:
+ - name: id
+ in: path
+ description: id of Recording
+ required: true
+ style: simple
+ explode: false
+ schema:
+ type: string
+ tags:
+ - recordings
+ responses:
+ 200:
+ description: successful response
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Recording'
+ description: successful response
+ /recording/{id}/start:
+
+ patch:
+ parameters:
+ - name: id
+ in: path
+ description: id of Recording
+ required: true
+ style: simple
+ explode: false
+ schema:
+ type: string
+ description: Start a recorder, and make it accept incoming requests again.
+ tags:
+ - recordings
+ responses:
+ 200:
+ description: successful response
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Recording'
+ description: successful response
/systems:
get:
diff --git a/assets/register.html b/assets/register.html
index c9d89c1e..485fef06 100644
--- a/assets/register.html
+++ b/assets/register.html
@@ -74,7 +74,8 @@
Register
} if (errorHint == 'SE') {
document.getElementById("errMsg").innerHTML = "*Oops! Something went wrong.";
}
- var ck_email = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i
+ var ck_uname = /^[a-zA-Z0-9]{4,10}$/;
+ var ck_email = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i;
var ck_pword = /^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{6,}$/;
function validate(form) {
var uname = form.username.value;
@@ -87,6 +88,9 @@
Register
if (uname != '' && uname.length < 4) {
errors[errors.length] = "Username length must be a minimum of 4 characteres.";
}
+ if (uname != '' && !ck_uname.test(uname)) {
+ errors[errors.length] = "Username length must be between 4 & 10. Username shouldn't contain special Character and should start with alphanumeric.";
+ }
if (!ck_email.test(email)) {
errors[errors.length] = "You must enter a valid email address.";
}
@@ -100,7 +104,18 @@
Register
reportErrors(errors);
return false;
}
- return true;
+ var obj = { username: uname, mail: email, password: password };
+ var formData = JSON.stringify(obj);
+ var xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = function() {
+ if (this.status == 302) {
+ window.location.href=window.location.origin+xhr.getResponseHeader('redirectUrl');
+ }
+ };
+ xhr.open('post', '/register', true);
+ xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
+ xhr.send(formData);
+ return false;
}
var errList = [];
function reportErrors(errors) {
diff --git a/controllers/matchTemplateController.js b/controllers/matchTemplateController.js
new file mode 100644
index 00000000..eb500a40
--- /dev/null
+++ b/controllers/matchTemplateController.js
@@ -0,0 +1,199 @@
+const xml2js = require('xml2js');
+const debug = require('debug')('matching');
+
+/**
+ * Applies all template options to a response string. E.g. puts out map values
+ * @param {*} response
+ * @param {*} templateOptions
+ */
+function applyTemplateOptionsToResponse(response,templateOptions){
+
+ if(templateOptions.map){
+ for(let key in templateOptions.map){
+ response = response.replace("{{" + key + "}}",templateOptions.map[key]);
+ }
+ }
+ return response;
+}
+
+
+
+/**
+ * Merges a newly returned options object from processCondition() into the existing options
+ * @param {*} oldOptions Old options- this gets mut'd!
+ * @param {*} newOptions new options
+ */
+function mergeInOptions(oldOptions,newOptions){
+ if(typeof newOptions == 'object'){
+ for(let key1 in newOptions){
+ if(oldOptions[key1]){
+ if(Array.isArray(oldOptions[key1])){
+ oldOptions[key1] = oldOptions[key1].concat(newOptions[key1]);
+ }else if(typeof oldOptions == 'object'){
+ for(let key2 in newOptions[key1]){
+ oldOptions[key1][key2] = newOptions[key1][key2];
+ }
+ }else{
+ oldOptions[key1] = newOptions[key1];
+ }
+ }else{
+ oldOptions[key1] = newOptions[key1];
+ }
+ }
+ }
+}
+
+
+/**
+ * Given a condition string, process this condition.
+ * @param {string} field Flattened field name (e.g. "params.person.firstName")
+ * @param {string} conditionString The condition string (e.g. map:firstName)
+ * @param {object} flatPayload Flattened payload
+ * @return Returns either an object that contains new options for the template, OR false if the condition is considered failed. ONLY false is considered a failure- {} or null is a pass!
+ */
+function processCondition(field,conditionString,flatPayload){
+ let split = conditionString.split(":",2);
+ let condNum, payloadNum;
+ try{
+ switch(split[0]){
+ case "map":
+ var map = {};
+ if(flatPayload[field] === undefined)
+ return false;
+ map[split[1]] = flatPayload[field] || '';
+ return {map};
+ case "lt":
+ if(flatPayload[field] === undefined)
+ return false;
+ condNum = parseFloat(split[1]);
+ payloadNum = parseFloat(flatPayload[field]);
+ return payloadNum < condNum;
+ case "gt":
+ if(flatPayload[field] === undefined)
+ return false;
+ condNum = parseFloat(split[1]);
+ payloadNum = parseFloat(flatPayload[field]);
+ return payloadNum > condNum;
+
+ case "any":
+ return flatPayload[field] !== undefined;
+ case "regex":
+ var reg = new RegExp(split[1]);
+ return flatPayload[field].match(reg) !== null;
+ default:
+ return {};
+ }
+ }catch(e){
+ console.log(e);
+ debug(e);
+ return false;
+ }
+}
+
+
+/**
+ * Iterates through multiple ; separated conditions
+ * @param {*} field
+ * @param {*} conditionString
+ * @param {*} flatPayload
+ */
+function preProcessCondition(field,conditionString,flatPayload){
+ if(typeof conditionString != "string"){
+ return false;
+ }
+ var split = conditionString.split(";");
+ var opts = {};
+ for(let splString of split){
+ let newOpts = processCondition(field,splString,flatPayload);
+ if(newOpts === false)
+ return false;
+ mergeInOptions(opts,newOpts);
+ }
+ return opts;
+}
+
+
+/**
+ * Tests a payload against a request using a template. Returns any template options that were parsed (e.g. mapping vars)
+ * @param {*} flatTemplate The flattened template
+ * @param {*} rrpair RR Pair in question
+ * @param {*} flatPayload Flattened payload
+ * @param {*} flatReqData Flattened reqData from RR pair
+ * @param {*} path Path of this req (for debug logging)
+ */
+function matchOnTemplate(flatTemplate,rrpair,flatPayload,flatReqData,path){
+ var returnOptions = {};
+ const trimmedPayload = {}; const trimmedReqData = {};
+ var hasBlank = false;
+ for (let field in flatTemplate) {
+
+ //If we have a condition here, handle its special properties
+ if(flatTemplate[field]){
+ var ret = preProcessCondition(field,flatTemplate[field],flatPayload);
+ if(ret !== false){
+ mergeInOptions(returnOptions,ret);
+ }else{
+ return false;
+ }
+ //Otherwise add this to the list to get literal equals'd
+ }else{
+ hasBlank = true;
+ trimmedPayload[field] = flatPayload[field];
+ trimmedReqData[field] = flatReqData[field];
+ }
+ }
+
+ logEvent(path, rrpair.label, 'received payload (from template): ' + JSON.stringify(trimmedPayload, null, 2));
+ logEvent(path, rrpair.label, 'expected payload (from template): ' + JSON.stringify(trimmedReqData, null, 2));
+
+ if(hasBlank && !deepEquals(trimmedPayload, trimmedReqData)){
+ return false;
+ }
+
+ // make sure we're not comparing {} == {}
+ if (hasBlank && JSON.stringify(trimmedPayload) === '{}') {
+ return false;
+ }
+ return returnOptions;
+}
+
+
+/**
+ * Given a template and the payload type, parse this template and flatten it.
+ * @param {*} template
+ * @param {*} payloadType
+ * @return Flattened template, or false if parsing fails.
+ */
+function parseAndFlattenTemplate(template,payloadType){
+ if (payloadType === 'XML') {
+ let ret = false;
+ xml2js.parseString(template, function(err, xmlTemplate) {
+ if (err) {
+ logEvent(err);
+ ret = false;
+ }
+ ret = flattenObject(xmlTemplate);
+ });
+ return ret;
+ }
+ else if (payloadType === 'JSON') {
+ try {
+ return flattenObject(JSON.parse(template));
+ }
+ catch(e) {
+ debug(e);
+ return false;
+ }
+ }else{
+ return false;
+ }
+
+}
+
+
+module.exports = {
+ matchOnTemplate : matchOnTemplate,
+ applyTemplateOptionsToResponse:applyTemplateOptionsToResponse,
+ preProcessCondition:preProcessCondition,
+ parseAndFlattenTemplate: parseAndFlattenTemplate
+}
\ No newline at end of file
diff --git a/controllers/recorderController.js b/controllers/recorderController.js
index a32d4e0e..65995ed2 100644
--- a/controllers/recorderController.js
+++ b/controllers/recorderController.js
@@ -2,59 +2,138 @@ const requestNode = require('request');
const Recording = require('../models/http/Recording');
const routing = require('../routes/recording');
const manager = require('../lib/pm2/manager');
+const constants = require('../lib/util/constants');
var activeRecorders = {};
-function escapeRegExp(string) {
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
- }
+
/**
* Recorder object
*
* This object is created whenever a new recording session is started. the recording router will route all appropriate transactions to this object
+ * Can instead pass an ID to a Recording document to 'name', and create an instance based on that recorder.
*/
-var Recorder = function(name,path,sut,remoteHost,remotePort,protocol,headerMask,ssl){
- this.path = path;
-
- //Ensure path starts with /
- if(this.path.substring(0,1) != "/")
- this.path = "/" + this.path;
-
- this.model = Recording.create({
- sut : {name:sut},
- path : path,
- remoteHost : remoteHost,
- protocol : protocol || 'REST',
- remotePort : remotePort || 80,
- headerMask : headerMask || ['Content-Type'],
- service : {
- basePath : path.substring(1),
- sut:{name:sut},
- name:name,
- type:protocol
- },
- name: name,
- ssl:ssl
- },(function(err,newModel){
- this.model = newModel;
-
- if(!(this.model.headerMask))
- this.model.headerMask = [];
-
- if(!(this.model.headerMask['Content-Type']))
- this.model.headerMask.push('Content-Type');
+var Recorder = function(name,path,sut,remoteHost,remotePort,protocol,headerMask,ssl,filters,creator, rsp){
- this.model.save(function(err){
- if(err) console.log(err);
+ var rec = this;
+
+ //If passed ID for Recording document...
+ if(arguments.length == 1){
+ rec.model = new Promise(function(resolve,reject){
+ Recording.findById(name,function(err,doc){
+ if(err){
+ reject(err);
+ }else{
+ doc.running = true;
+ doc.save(function(err,doc){
+ if(err){
+ reject(err);
+ }else if(doc){
+ rec.model = doc;
+ syncWorkersToNewRecorder(rec);
+ resolve(doc);
+ }else{
+ reject(new Error("doc save failed"));
+ }
+ });
+ }
+
+ });
});
- syncWorkersToNewRecorder(this);
- }).bind(this));
+ }else{
+
+ this.path = path;
+
+ //Ensure path starts with /
+ if(this.path.substring(0,1) != "/")
+ this.path = "/" + this.path;
+
+ //Make sure it starts with ONLY one /
+ this.path.replace(/^\/+/,"/");
+ this.model = Recording.create({
+ sut : {name:sut},
+ path : path,
+ remoteHost : remoteHost,
+ protocol : protocol || 'REST',
+ remotePort : remotePort || 80,
+ headerMask : headerMask || ['Content-Type'],
+ service : {
+ basePath : path,
+ sut:{name:sut},
+ name:name,
+ type:protocol
+ },
+ name: name,
+ ssl:ssl,
+ filters:filters
+ },(function(err,newModel){
+ if(err) {
+ handleBackEndValidationsAndErrors(err, rsp);
+ return;
+ }
+ this.model = newModel;
+
+ if(!(this.model.headerMask))
+ this.model.headerMask = [];
+
+ if(!(this.model.headerMask['Content-Type']))
+ this.model.headerMask.push('Content-Type');
+
+ this.model.save(function(err, recording){
+ if(err) {
+ handleBackEndValidationsAndErrors(err, rsp);
+ return;
+ }
+ });
+ syncWorkersToNewRecorder(this);
+ }).bind(this));
+ }
};
+/**
+ * This function handle mongoose validation Errors or any other error at backend.
+ * @param {*} err err Object contains error from backEnd.
+ * @param {*} res response Object required to send response error code.
+ * @returns blank to stop further processing and sends 400(bad request from mongoose validations) or 500(internal error) to clients.
+ */
+/* To Do:- This below function is used in both serviceController and recorderController. We
+ should keep this functiona at common place and should be call from ther at both places. */
+ function handleBackEndValidationsAndErrors(err, res) {
+ {
+ switch (err.name) {
+ case 'ValidationError':
+ LOOP1:
+ for (let field in err.errors) {
+ switch (err.errors[field].kind) {
+ case 'required':
+ handleError(err.errors[field].message, res, 400);
+ break LOOP1;
+ case 'user defined':
+ handleError(err.errors[field].message, res, 400);
+ break LOOP1;
+ case 'enum':
+ handleError(err.errors[field].message, res, 400);
+ break LOOP1;
+ case 'Number':
+ handleError(err.errors[field].message, res, 400);
+ break LOOP1;
+ default:
+ handleError(err.errors[field].message, res, 400);
+ break LOOP1;
+ }
+ }
+ break;
+ default:
+ handleError(err, res, 500);
+ }
+ return;
+ }
+ }
+
function registerRecorder(recorder){
activeRecorders[recorder.model._id] = recorder;
routing.bindRecorderToPath("/" + recorder.model.sut.name + recorder.model.path + "*",recorder);
@@ -113,6 +192,7 @@ function stripRRPairForReq(rrpair) {
myRRPair.reqData = JSON.parse(req.body);
}catch(err){
myRRPair.reqData = req.body;
+ myRRPair.payloadType = "PLAIN";
}
}
else
@@ -180,6 +260,7 @@ function stripRRPairForReq(rrpair) {
myRRPair.resData = JSON.parse(body);
}catch(err){
myRRPair.resData = body;
+ myRRPair.payloadType = "PLAIN";
}
}else{
myRRPair.resData = body;
@@ -197,8 +278,46 @@ function stripRRPairForReq(rrpair) {
break;
}
}
+
+
+ //Check our filters
+ var filters = this.model.filters;
+ var addThisRRPair = true;
+ var filteredReason = null;
+ if(filters && filters.enabled && body){
+ if(filters.bodyStrings.length){
+ for(let i = 0; i < filters.bodyStrings.length; i++){
+ if(body.includes(filters.bodyStrings[i])){
+ addThisRRPair = false;
+ filteredReason = 'Found string "' + filters.bodyStrings[i] + '" in response body.'
+ break;
+ }
+ }
+ }
+ if(addThisRRPair && filters.statuses.length){
+ for(let i = 0; i < filters.statuses.length; i++){
+ if(remoteRsp.statusCode == filters.statuses[i]){
+ addThisRRPair = false;
+ filteredReason = 'Found status code "' + filters.statuses[i] + '" in response.'
+ break;
+ }
+ }
+ }
+ if(addThisRRPair && filters.headers.length){
+ for(let i = 0; i < filters.headers.length; i++){
+ let header = filters.headers[i];
+ if(remoteRsp.headers[header.key] && remoteRsp.headers[header.key] == header.value){
+ addThisRRPair = false;
+ filteredReason = 'Found header "' + header.key + '" with value "' + header.value + '" in response headers.'
+ break;
+ }
+ }
+ }
+ }
+
+
//Push RRPair to model, then update our local model
- if(!duplicate){
+ if(!duplicate && addThisRRPair){
Recording.update({_id : this.model._id} ,
{$push:
{"service.rrpairs":myRRPair}
@@ -211,6 +330,11 @@ function stripRRPairForReq(rrpair) {
//Send back response to user
rsp.status(remoteRsp.statusCode);
var headers = remoteRsp.headers;
+
+ //Add reason for filtered, if filtered
+ if(!addThisRRPair && filteredReason){
+ headers['_mockiato-filtered-reason'] = filteredReason;
+ }
if(headers['content-type']){
rsp.type(headers['content-type'])
delete headers['content-type'];
@@ -268,6 +392,28 @@ function getRecordingById(req,rsp){
});
}
+/**
+ * API call to get a specific recording based on SUT
+ * @param {*} req express req
+ * @param {*} rsp express rsp
+ */
+function getRecordingBySystem(req,rsp){
+ let allRecorders = [];
+
+ const query = { 'sut.name': req.params.name };
+
+ Recording.find(query,function(err,docs){
+ if(err){
+ handleError(err,rsp,500);
+ return;
+ }
+ console.log("Found Recorder with SUT",docs);
+ allRecorders = docs;
+ return rsp.json(allRecorders);
+
+ });
+}
+
/**
* API call to get RR pairs for a recorder after a certain index, for use in active update/polling
* @param {*} req express req
@@ -320,8 +466,8 @@ function findDuplicateRecorder(sut,path){
* @param {string} dataType XML/JSON/etc
* @param {array{string}} headerMask array of headers to save
*/
-function beginRecordingSession(label,path,sut,remoteHost,remotePort,protocol,headerMask,ssl){
- var newRecorder = new Recorder(label,path,sut,remoteHost,remotePort,protocol,headerMask,ssl);
+function beginRecordingSession(label,path,sut,remoteHost,remotePort,protocol,headerMask,ssl,filters,creator, rsp){
+ var newRecorder = new Recorder(label,path,sut,remoteHost,remotePort,protocol,headerMask,ssl,filters,creator, rsp);
return newRecorder;
}
@@ -356,6 +502,28 @@ function removeRecorder(req,rsp){
}
+/**
+ * API call to stop a recorder based on ID.
+ * @param {*} req express req
+ * @param {*} rsp express rsp
+ */
+function stopRecorder(req,rsp){
+ var recorder = activeRecorders[req.params.id];
+ if(recorder){
+ recorder.model.running = false;
+ recorder.model.save(function(err,doc){
+ rsp.status(200);
+ rsp.json(doc);
+ deregisterRecorder(recorder);
+
+ });
+
+ }else{
+ handleError(new Error("No recorder found for that ID or recorder is already stopped."),rsp,404);
+ }
+
+}
+
function deregisterRecorder(recorder){
routing.unbindRecorder(recorder);
if(activeRecorders[recorder.model._id])
@@ -372,13 +540,23 @@ function deregisterRecorder(recorder){
*/
function addRecorder(req,rsp){
var body = req.body;
+ if (body.sut === undefined){
+ handleError(constants.REQUIRED_SUT_PARAMS_ERR, rsp, 400);
+ return;
+ }
+ if(body.basePath === undefined){
+ handleError(constants.REQUIRED_BASEPATH_ERR, rsp, 400);
+ return;
+ }
if(body.type == "SOAP")
body.payloadType = "XML";
if(findDuplicateRecorder(body.sut,body.basePath)){
- handleError("OverlappingRecorderPathError",rsp,500);
+ handleError(constants.DUP_RECORDER_PATH_BODY,rsp,400);
+ return;
}
else{
- var newRecorder = beginRecordingSession(body.name,body.basePath,body.sut,body.remoteHost,body.remotePort,body.type,body.headerMask,body.ssl);
+ //need to refactor this..just pasa body and not so much parameters to beginRecordingSession function.
+ var newRecorder = beginRecordingSession(body.name,body.basePath,body.sut,body.remoteHost,body.remotePort,body.type,body.headerMask,body.ssl,body.filters,body.creator, rsp);
newRecorder.model.then(function(doc){
rsp.json(doc);
}).catch(function(err){
@@ -389,6 +567,29 @@ function addRecorder(req,rsp){
}
+ /**
+ * Starts an existing Recording into a Recorder object.
+ * @param {string} recorderId
+ * @return {Recorder} recorder object instantiated from Recording doc
+ */
+ function startRecorderFromId(recorderId){
+ return new Recorder(recorderId);
+}
+
+/**
+ * API call to start a recorder. :id is the id to a Recording document. Starts up a Recorder based on the Recording doc.
+ * @param {*} req express req
+ * @param {*} rsp express rsp
+ */
+function startRecorder(req,rsp){
+ startRecorderFromId(req.params.id).model.then(function(doc){
+ rsp.json(doc);
+ }).catch(function(err){
+ handleError(err,rsp,500);
+ });
+}
+
+
module.exports = {
Recorder: Recorder,
@@ -398,6 +599,20 @@ function addRecorder(req,rsp){
getRecorderRRPairsAfter : getRecorderRRPairsAfter,
removeRecorder: removeRecorder,
registerRecorder: registerRecorder,
- deregisterRecorder: deregisterRecorder
+ deregisterRecorder: deregisterRecorder,
+ startRecorder : startRecorder,
+ stopRecorder : stopRecorder,
+ getRecordingBySystem : getRecordingBySystem
};
+
+//On startup, start all recorders that should be active.
+Recording.find({running:true},function(err,docs){
+ if(docs){
+ docs.forEach(function(doc){
+ new Recorder(doc);
+ });
+ }else if(err){
+ debug('error initializing recorders');
+ }
+});
\ No newline at end of file
diff --git a/controllers/rrpairController.js b/controllers/rrpairController.js
index a461deb2..04bc750e 100644
--- a/controllers/rrpairController.js
+++ b/controllers/rrpairController.js
@@ -1,8 +1,7 @@
const Service = require('../models/http/Service');
const MQService = require('../models/mq/MQService');
-const xml2js = require('xml2js');
-const xmlBuilder = new xml2js.Builder();
+
function getPairsByServiceId(req, res) {
Service.findById(req.params.serviceId, function(err, service) {
@@ -27,41 +26,6 @@ function getPairsByServiceId(req, res) {
});
}
-function trimRequestData(template, rrpair) {
- if (!template || !rrpair) {
- return;
- }
-
- xml2js.parseString(template, function(err, xmlTemplate) {
- if (err) {
- debug(err);
- return;
- }
- template = xmlTemplate;
- });
-
- let reqData;
- xml2js.parseString(rrpair.reqData, function(err, data) {
- reqData = data;
- });
-
- // flatten request data
- const flatTemplate = flattenObject(template);
- const flatReqData = flattenObject(reqData);
- const trimmedReqData = {};
-
- // pull out the fields specified in the template
- for (let field in flatTemplate) {
- trimmedReqData[field] = flatReqData[field];
- }
-
- // unflatten the trimmed request data
- const unflatReqData = unflattenObject(trimmedReqData);
-
- return xmlBuilder.buildObject(unflatReqData);
-}
-
module.exports = {
- getPairsByServiceId: getPairsByServiceId,
- trimRequestData: trimRequestData
+ getPairsByServiceId: getPairsByServiceId
};
diff --git a/controllers/serviceController.js b/controllers/serviceController.js
index 944d4eae..8903178a 100644
--- a/controllers/serviceController.js
+++ b/controllers/serviceController.js
@@ -15,6 +15,84 @@ const unzip = require('unzip2');
const YAML = require('yamljs');
const invoke = require('../routes/invoke');
const System = require('../models/common/System');
+const systemController = require('./systemController');
+const constants = require('../lib/util/constants');
+
+/**
+ * Given a User object and a service's id, return a promise that resolves if this user can edit the service, and rejects otherwise
+ * @param {User} user
+ * @param {string} serviceId
+ */
+function canUserEditServiceById(user,serviceId){
+ return new Promise(function(resolve,reject){
+ Service.findById(serviceId,function(err,doc){
+ if(err)
+ reject(err)
+ else
+ if(doc){
+ systemController.isUserMemberOfGroup(user,doc.sut).then((bool)=>{resolve(bool)},(err)=>{reject(err)});
+ }else{
+ reject("No Service Found");
+ }
+ });
+ });
+}
+
+
+
+
+
+/**
+ * Wrapper function for (MQ)Service.create. If req is provided, will also check against current logged in user's permissions first.
+ * @param {object} serv An object containing the info to create a service
+ * @param {*} req Express request.
+ * @return A promise from creating the service. Resolves to the new service. Rejects with error from mongoose OR error from lack of group permission.
+ */
+function createService(serv,req){
+ return new Promise(function(resolve,reject){
+ if(req){
+ var user = req.decoded;
+ serv.lastUpdateUser = user;
+ var authed = false;
+
+ //Get our system
+ systemController.getSystemIfMember(user.uid,serv.sut.name).exec(function(err,system){
+ if(err){
+ reject(err);
+ }else if(system){
+ serv.sut = system; //Make sure service has full system info, including proper ID!
+ performCreate();
+ }else{
+ reject(new Error(constants.USER_NOT_AUTHORIZED_ERR));
+ }
+ });
+
+ }else{
+ performCreate();
+ }
+ function performCreate(){
+ if(serv.type == "MQ"){
+ MQService.create(serv,function(err,service){
+ if(err)
+ reject(err);
+ else
+ resolve(service);
+ });
+ }else{
+ Service.create(serv,function(err,service){
+ if(err)
+ reject(err);
+ else
+ resolve(service);
+ });
+ }
+ }
+
+
+
+ });
+
+}
/**
* Helper function for search. Trims down an HTTP service for return, and filters + trims rrpairs.
@@ -390,7 +468,7 @@ function getServicesArchiveBySystem(req, res) {
let allServices = [];
const query = { $or: [ { 'service.sut.name': req.params.name }, { 'mqservice.sut.name': req.params.name } ] };
-
+console.log(query);
Archive.find(query, function(err, services) {
if (err) {
handleError(err, res, 500);
@@ -453,27 +531,31 @@ function getServicesByQuery(req, res) {
}
function getArchiveServices(req, res) {
- const sut = req.query.sut;
- const user = req.query.user;
+ //const query = {};
- const query = { $or: [ { 'service.sut.name': sut }, { 'mqservice.sut.name': sut },{ 'service.user.uid': user },
- { 'mqservice.user.uid': user } ] };
+ const sut = req.query.sut;
+ const user = req.query.user;
- Archive.find(query, function(err, services) {
- if (err) {
- handleError(err, res, 500);
- return;
- }
- return res.json(services);
- });
+ const query = { $and :[ {$or: [ { 'service.sut.name': sut }, { 'mqservice.sut.name': sut }] },{$or : [{ 'service.user.uid': user },
+ { 'mqservice.user.uid': user } ]}] };
+
+ Archive.find(query, function(err, services) {
+ if (err) {
+ handleError(err, res, 500);
+ return;
+ }
+ return res.json(services);
+ });
}
function getDraftServices(req, res) {
+ // const query = {};
+
const sut = req.query.sut;
const user = req.query.user;
- const query = { $or: [ { 'service.sut.name': sut }, { 'mqservice.sut.name': sut },{ 'service.user.uid': user },
- { 'mqservice.user.uid': user } ] };
+ const query = { $and :[ {$or: [ { 'service.sut.name': sut }, { 'mqservice.sut.name': sut }] },{$or : [{ 'service.user.uid': user },
+ { 'mqservice.user.uid': user } ]}] };
DraftService.find(query, function(err, services) {
if (err) {
@@ -491,20 +573,20 @@ function searchDuplicate(service, next) {
basePath: service.basePath
};
- const query = {
+ const dupCheckQuery = {
name: service.name,
basePath: service.basePath
};
- Service.findOne(query2ServDiffNmSmBP, function (err, sameNmDupBP) {
+ Service.findOne(query2ServDiffNmSmBP, function (err, diffNmSameBP) {
if (err) {
handleError(err, res, 500);
return;
}
- else if (sameNmDupBP)
+ else if (diffNmSameBP)
next({ twoServDiffNmSmBP: true });
else {
- Service.findOne(query, function (err, duplicate) {
+ Service.findOne(dupCheckQuery, function (err, duplicate) {
if (err) {
handleError(err, res, 500);
return;
@@ -532,7 +614,7 @@ function stripRRPair(rrpair) {
// function to merge req / res pairs of duplicate services
function mergeRRPairs(original, second) {
- for (let rrpair2 of second.rrpairs) {
+ for (let rrpair2 of second.rrpairs) {
let hasAlready = false;
let rr2 = stripRRPair(new RRPair(rrpair2));
@@ -583,8 +665,21 @@ function syncWorkers(service, action) {
}
function addService(req, res) {
- const type = req.body.type;
+ /* validating sut here because code doesn't reach to mongoose validation check. we are using sut.name to calculate base path before mongoose
+ validations. even if sut present but name or members not present in req data. unautorize error message is thrown which is not correct.*/
+ if (!req.body.sut || !req.body.sut.name){
+ handleError(constants.REQUIRED_SUT_PARAMS_ERR, res, 400);
+ return;
+ }
+
+ /* This check is mandatory for basePath validation because basePath is created using groupName. so mongoose validation will
+ not throw an error. This is the case when user may forget to put mandatory field 'basePath' in request for REST/SOAP type service.*/
+ if (!req.body.basePath && (req.body.type === 'REST' || req.body.type === 'SOAP')){
+ handleError(constants.REQUIRED_BASEPATH_ERR, res, 400);
+ return;
+ }
+ const type = req.body.type;
let serv = {
sut: req.body.sut,
user: req.decoded,
@@ -594,8 +689,7 @@ function addService(req, res) {
delayMax: req.body.delayMax,
basePath: '/' + req.body.sut.name + req.body.basePath,
matchTemplates: req.body.matchTemplates,
- rrpairs: req.body.rrpairs,
- lastUpdateUser: req.decoded
+ rrpairs: req.body.rrpairs
};
//Save req and res data string cache
@@ -615,15 +709,18 @@ function addService(req, res) {
if (type === 'MQ') {
serv.connInfo = req.body.connInfo;
- MQService.create(serv,
+ createService(serv,req).then(
+ function(service){
+ res.json(service);
+ },
// handler for db call
- function(err, service) {
+ function(err) {
if (err) {
- handleError(err, res, 500);
- return;
- }
+ handleBackEndValidationsAndErrors(err, res);
+ return;
+ }
// respond with the newly created resource
- res.json(service);
+
});
}
else {
@@ -632,39 +729,87 @@ function addService(req, res) {
searchDuplicate(serv, function(duplicate) {
if (duplicate && duplicate.twoServDiffNmSmBP){
- res.json({"error":"twoSeviceDiffNameSameBasePath"});
+ handleError(constants.SERVICES_DIFFNAME_SAMEBASEPATH_ERR, res, 400);
return;
}
- else if (duplicate) {
- // merge services
- mergeRRPairs(duplicate, serv);
- // save merged service
- duplicate.save(function(err, newService) {
- if (err) {
- handleError(err, res, 500);
+ else if (duplicate) {
+ //only merge if both service type is same othwise throw error 409.
+ if(duplicate.type == serv.type){
+ if(!serv.rrpairs){
+ handleError(constants.REQUST_NO_RRPAIR, res, 400);
return;
}
- res.json(newService);
-
- syncWorkers(newService, 'register');
- });
+ // merge services
+ mergeRRPairs(duplicate, serv);
+ // save merged service
+ duplicate.save(function(err, newService) {
+ if (err) {
+ handleBackEndValidationsAndErrors(err, res);
+ return;
+ }
+ res.json(newService);
+ syncWorkers(newService, 'register');
+ });
+ }else{
+ handleError(constants.DIFF_TYPE_SERV_ERR, res, 409);
+ return;
+ }
}
else {
- Service.create(serv,
- function(err, service) {
- if (err) {
- handleError(err, res, 500);
+ createService(serv, req).then(
+ function (service) {
+ res.json(service);
+
+ syncWorkers(service, 'register');
+ }, function (err) {
+ handleBackEndValidationsAndErrors(err, res);
return;
}
- res.json(service);
-
- syncWorkers(service, 'register');
- });
+ );
}
});
}
}
+/**
+ * This function handle mongoose validation Errors or any other error at backend.
+ * @param {*} err err Object contains error from backEnd.
+ * @param {*} res response Object required to send response error code.
+ * @returns blank to stop further processing and sends 400(bad request from mongoose validations) or 500(internal error) to clients.
+ */
+/* To Do:- This below function is used in both serviceController and recorderController. We
+ should keep this functiona at common place and should be call from ther at both places. */
+function handleBackEndValidationsAndErrors(err, res) {
+ {
+ switch (err.name) {
+ case 'ValidationError':
+ LOOP1:
+ for (let field in err.errors) {
+ switch (err.errors[field].kind) {
+ case 'required':
+ handleError(err.errors[field].message, res, 400);
+ break LOOP1;
+ case 'user defined':
+ handleError(err.errors[field].message, res, 400);
+ break LOOP1;
+ case 'enum':
+ handleError(err.errors[field].message, res, 400);
+ break LOOP1;
+ case 'Number':
+ handleError(err.errors[field].message, res, 400);
+ break LOOP1;
+ default:
+ handleError(err.errors[field].message, res, 400);
+ break LOOP1;
+ }
+ }
+ break;
+ default:
+ handleError(err, res, 500);
+ }
+ return;
+ }
+}
function addServiceAsDraft(req, res) {
const type = req.body.type;
@@ -721,8 +866,6 @@ function addServiceAsDraft(req, res) {
return;
}
res.json(service);
-
- syncWorkers(service, 'register');
});
}
}
@@ -755,6 +898,9 @@ function updateService(req, res) {
});
}
if(req.body.liveInvocation){
+ if(service.liveInvocation && service.liveInvocation.recordedRRPairs){
+ req.body.liveInvocation.recordedRRPairs = service.liveInvocation.recordedRRPairs;
+ }
service.liveInvocation = req.body.liveInvocation;
}
if (req.body.matchTemplates) {
@@ -776,7 +922,7 @@ function updateService(req, res) {
// save updated service in DB
service.save(function (err, newService) {
if (err) {
- handleError(err, res, 500);
+ handleBackEndValidationsAndErrors(err, res);
return;
}
@@ -793,8 +939,12 @@ function updateService(req, res) {
return;
}
if(draftservice.service){
- addService(req, res);
+ var reg = new RegExp("/" + draftservice.service.sut.name,"i");
+ var basePath = draftservice.service.basePath.replace(reg,"");
+ req.body.basePath = basePath;
}
+ addService(req, res);
+
});
}
});
@@ -808,8 +958,6 @@ function updateService(req, res) {
handleError(err, res, 500);
return;
}
- // if (draftservice.service) {
- console.log('Inside updateServiceAsDraft: ' + draftservice);
if(draftservice.service){
// don't let consumer alter name, base path, etc.
@@ -969,6 +1117,7 @@ function restoreService(req, res) {
}
if (archive.service) {
let newService = {
+ _id:archive.service._id,
sut: archive.service.sut,
user: archive.service.user,
name: archive.service.name,
@@ -982,7 +1131,8 @@ function restoreService(req, res) {
rrpairs: archive.service.rrpairs,
lastUpdateUser: archive.service.lastUpdateUser
};
- Service.create(newService, function (err, callback) {
+ createService(newService,req).then(function(service){},
+ function (err) {
if (err) {
handleError(err, res, 500);
}
@@ -1000,7 +1150,7 @@ function restoreService(req, res) {
rrpairs: archive.mqservice.rrpairs,
connInfo: archive.mqservice.connInfo
};
- MQService.create(newMQService, function (err, callback) {
+ createService(newMQService,req).then( function(serv) {},function (err) {
if (err) {
handleError(err, res, 500);
}
@@ -1090,44 +1240,60 @@ function publishExtractedRRPairs(req, res) {
serv.user = req.decoded;
if (type === 'MQ') {
- MQService.create(serv,
+ createService(serv,req).then(
+ function(service){
+ res.json(service);
+ },
// handler for db call
- function(err, service) {
+ function(err) {
if (err) {
- handleError(err, res, 500);
+ handleBackEndValidationsAndErrors(err, res);
return;
}
// respond with the newly created resource
- res.json(service);
+
});
}
else {
searchDuplicate(serv, function(duplicate) {
if (duplicate && duplicate.twoServDiffNmSmBP){
- res.json({"error":"twoSeviceDiffNameSameBasePath"});
+ handleError(constants.SERVICES_DIFFNAME_SAMEBASEPATH_ERR, res, 400);
return;
}
- else if (duplicate) {
+ else if (duplicate) {
+ //only merge if both service type is same othwise do nothing.
+ if(duplicate.type == serv.type){
+ if(!serv.rrpairs){
+ handleError(constants.REQUST_NO_RRPAIR, res, 400);
+ return;
+ }
// merge services
mergeRRPairs(duplicate, serv);
// save merged service
duplicate.save(function(err, newService) {
if (err) {
- handleError(err, res, 500);
+ handleBackEndValidationsAndErrors(err, res);
return;
}
res.json(newService);
-
syncWorkers(newService, 'register');
});
+ }else{
+ handleError(constants.DIFF_TYPE_SERV_ERR, res, 400);
+ return;
}
+ }
else {
- Service.create(serv, function (err, service) {
+ createService(serv,req).then(
+ function(service){
+ res.json(service);
+ syncWorkers(service , 'register');
+ },
+ function (err, service) {
if (err) {
- handleError(err, res, 500);
+ handleBackEndValidationsAndErrors(err, res);
+ return;
}
- res.json(service);
- syncWorkers(service , 'register');
});
}
});
@@ -1191,29 +1357,41 @@ function publishUploadedSpec(req, res) {
searchDuplicate(serv, function(duplicate) {
if (duplicate && duplicate.twoServDiffNmSmBP){
- res.json({"error":"twoSeviceDiffNameSameBasePath"});
+ handleError(constants.SERVICES_DIFFNAME_SAMEBASEPATH_ERR, res, 400);
return;
}
- else if (duplicate) {
+ else if (duplicate) {
+ //only merge if both service type is same othwise do nothing.
+ if(duplicate.type == serv.type){
+ if(!serv.rrpairs){
+ handleError(constants.REQUST_NO_RRPAIR, res, 400);
+ return;
+ }
// merge services
mergeRRPairs(duplicate, serv);
// save merged service
duplicate.save(function(err, newService) {
if (err) {
- handleError(err, res, 500);
+ handleBackEndValidationsAndErrors(err, res);
return;
}
res.json(newService);
-
- syncWorkers(newService, 'register');
+ syncWorkers(newService, 'register');
});
+ }else{
+ handleError(constants.DIFF_TYPE_SERV_ERR, res, 400);
+ return;
}
+ }
else {
- Service.create(serv, function (err, service) {
- if (err) handleError(err, res, 500);
-
+ createService(serv,req).then( function(service){
res.json(service);
syncWorkers(service, 'register');
+ },function (err) {
+ if (err) {
+ handleBackEndValidationsAndErrors(err, res);
+ return;
+ }
});
}
});
@@ -1264,6 +1442,117 @@ function deleteDraftService(req, res) {
});
}
+
+
+/**
+ * API Call to delete a specific recorded RR pair from liveInvocation
+ * @param {*} req Express req
+ * @param {*} res Express rsp
+ */
+function deleteRecordedRRPair(req,res){
+ var serviceId = req.params.id;
+ var rrPairId = req.params.rrpairId;
+ canUserEditServiceById(req.decoded,serviceId).then((bool)=>{
+ Service.findOneAndUpdate({_id:serviceId},{$pull:{"liveInvocation.recordedRRPairs":{_id:rrPairId}}},function(err,doc){
+ if(err)
+ handleError(err,res,500);
+ else
+ res.json(doc);
+
+ });
+ },(err)=>{
+ handleError(err,res,500);
+ });
+
+}
+
+/**
+ * API call to get just the recorded RR pairs from a service
+ * @param {*} req express req
+ * @param {*} res express rsp
+ */
+function getServiceRecordedRRPairs(req,res){
+ var serviceId = req.params.id;
+ Service.findById(serviceId).select("liveInvocation.recordedRRPairs").exec(function(err,doc){
+ if(err)
+ handleError(err,res,500);
+ else
+ res.json(doc);
+ });
+}
+
+/**
+ * API call to take one recorded pair and merge it into RRPairs with no edit
+ * @param {*} req express req
+ * @param {*} res express rsp
+ */
+function mergeRecordedRRPair(req,res){
+ var serviceId = req.params.id;
+ var rrPairId = req.params.rrpairId;
+ canUserEditServiceById(req.decoded,serviceId).then((bool)=>{
+ Service.findOne({_id:serviceId,'liveInvocation.recordedRRPairs':{$elemMatch:{_id:rrPairId}}}).exec(function(err,doc){
+ if(err){
+ handleError(err,res,500);
+ }else if(doc){
+ for(let i = 0; i < doc.liveInvocation.recordedRRPairs.length; i++){
+ var rrpair = doc.liveInvocation.recordedRRPairs[i];
+ if(rrpair._id == rrPairId){
+ var rrPairWrapper = {rrpairs:[rrpair]};
+ mergeRRPairs(doc,rrPairWrapper);
+ doc.liveInvocation.recordedRRPairs.splice(i,1);
+ doc.save(function(err,newDoc){
+ res.json(newDoc);
+ syncWorkers(newDoc, 'register');
+ });
+ break;
+ }
+ }
+ }else{
+ res.status(404);
+ res.json({});
+ }
+ });
+ },(err)=>{
+ handleError(err,res,500);
+ });
+}
+
+/**
+ * API call to add an RRPair to a service
+ * @param {*} req express req
+ * @param {*} res express rsp
+ */
+function addRRPair(req,res){
+ var serviceId = req.params.id;
+ canUserEditServiceById(req.decoded,serviceId).then((bool)=>{
+ var rrPair = req.body;
+ Service.findById(serviceId,function(err,doc){
+ if(err)
+ handleError(err,res,500);
+ else if(doc){
+ mergeRRPairs(doc,{rrpairs:[rrPair]});
+ doc.save(function(err,newDoc){
+ if(err){
+ handleError(err,res,500);
+ }else{
+ res.json(newDoc);
+ }
+ })
+ }else{
+ res.status(404);
+ res.end();
+ }
+ });
+
+ },(err)=>{
+ if(err == "No Service Found")
+ handleError(err,res,404);
+ else
+ handleError(err,res,401);
+ });
+}
+
+
module.exports = {
getServiceById: getServiceById,
getArchiveServiceInfo: getArchiveServiceInfo,
@@ -1290,27 +1579,12 @@ module.exports = {
deleteDraftService: deleteDraftService,
getDraftServicesByUser: getDraftServicesByUser,
addServiceAsDraft: addServiceAsDraft,
- updateServiceAsDraft: updateServiceAsDraft
+ updateServiceAsDraft: updateServiceAsDraft,
+ deleteRecordedRRPair: deleteRecordedRRPair,
+ canUserEditServiceById: canUserEditServiceById,
+ getServiceRecordedRRPairs: getServiceRecordedRRPairs,
+ mergeRecordedRRPair: mergeRecordedRRPair,
+ addRRPair: addRRPair
};
-//Add resDataString and rspDataString to every existing service on boot, if they do not already have it
-Service.find({'rrpairs.resDataString':{$exists:false},'rrpairs.reqDataString':{$exists:false}},function(err,docs){
- if(err){
- console.log(err);
- }else{
- if(docs){
- docs.forEach(function(doc){
- if(doc.rrpairs){
- doc.rrpairs.forEach(function(rrpair){
- if(rrpair.reqData)
- rrpair.reqDataString = typeof rrpair.reqData == 'string' ? rrpair.reqData : JSON.stringify(rrpair.reqData);
- if(rrpair.resData)
- rrpair.resDataString = typeof rrpair.resData == 'string' ? rrpair.resData : JSON.stringify(rrpair.resData);
- });
- }
- doc.save();
- });
- }
- }
-});
\ No newline at end of file
diff --git a/controllers/systemController.js b/controllers/systemController.js
index 2d31250c..f5ca7ef9 100644
--- a/controllers/systemController.js
+++ b/controllers/systemController.js
@@ -1,6 +1,7 @@
const System = require('../models/common/System');
const debug = require('debug')('default');
const _ = require('lodash/array');
+const constants = require('../lib/util/constants');
function getSystems(req, res) {
System.find({}, function(err, systems) {
@@ -53,6 +54,17 @@ function updateGroup(req, res){
}
function addSystem(req, res) {
+
+ /**
+ * In case user import template with no sut key(by mistake) in .json file. In this case req.body may be empty.
+ * It is handled in service creation using validation of mandatory fields and showing proper error message on UI.
+ * So it needs to handle here. Otherwise application will not show what is problem with template.
+ */
+ if(!req.body) {
+ res.json(constants.SUT_NOT_PRESENT_ERR_MSG);
+ return;
+ }
+
if (!req.body.members) req.body.members = [];
//adds super user to all groups created
@@ -112,10 +124,52 @@ function delSystem(req, res) {
});
}
+/**
+ * Helper function to get all SUTs this user is a member of
+ * @param {string} user uid for user of interest
+ * @return mongoose query
+ */
+function findSystemsForUser(user){
+ return System.find({members:user});
+}
+
+
+/**
+ * Get a system if and ONLY if the given user is a member of that system. Otherwise return null.
+ * @param {string} user User uid
+ * @param {string} system System name
+ * @return mongoose query
+ */
+function getSystemIfMember(user,system){
+ return System.findOne({members:user,name:system});
+}
+
+/**
+ * Tests if user is a member of this group. returns a promise. Resolves to true if they are, rejects with error otherwise.
+ * @param {User} user
+ * @param {System} system
+ */
+function isUserMemberOfGroup(user,system){
+ return new Promise(function(resolve,reject){
+ System.findOne({name:system.name,members:user.uid},function(err,doc){
+ if(err){
+ reject(err);
+ }else if(doc){
+ resolve(true);
+ }else{
+ reject(new Error("User " + user.uid + " not member of group " + system.name));
+ }
+ })
+ });
+}
+
module.exports = {
getSystems: getSystems,
addSystem: addSystem,
delSystem: delSystem,
getOneSystem: getOneSystem,
- updateGroup: updateGroup
+ updateGroup: updateGroup,
+ findSystemsForUser: findSystemsForUser,
+ getSystemIfMember : getSystemIfMember,
+ isUserMemberOfGroup: isUserMemberOfGroup
};
\ No newline at end of file
diff --git a/controllers/userController.js b/controllers/userController.js
index f9f5972b..11712c2f 100644
--- a/controllers/userController.js
+++ b/controllers/userController.js
@@ -22,7 +22,6 @@ function delUser(req, res) {
}
function getAdminUser(req, res) {
- console.log('here');
res.json(process.env.MOCKIATO_ADMIN);
}
diff --git a/lib/auth/local.js b/lib/auth/local.js
index 007f6ae3..95101796 100644
--- a/lib/auth/local.js
+++ b/lib/auth/local.js
@@ -1,5 +1,4 @@
const mongoose = require('mongoose');
-const debug = require('debug')('default');
const fs = require('fs');
const express = require('express');
const passport = require('passport');
@@ -15,16 +14,20 @@ router.post('/', function (req, res) {
if (err) {
var errName = err.name;
if (errName == 'UserExistsError') {
- console.log(err);
- res.redirect('/register?ErrHint=' + encodeURIComponent('DU'));
+ res.append('redirectUrl', '/register?ErrHint=' + encodeURIComponent('DU'));
+ res.status(302);
+ res.send('Error : This username already exist.');
}else{
- console.log(err);
- res.redirect('/register?ErrHint=' + encodeURIComponent('SE'));
+ res.append('redirectUrl', '/register?ErrHint=' + encodeURIComponent('SE'));
+ res.status(302);
+ res.send('Something went wrong.');
}
}
- passport.authenticate('local')(req, res, function () {
- res.redirect('/#regS');
- });
+ else {
+ res.append('redirectUrl', '/#regS');
+ res.status(302);
+ res.send('User Registration Successful!');
+ }
});
});
diff --git a/lib/rrpair/parser.js b/lib/rrpair/parser.js
index 863bc07f..78762389 100644
--- a/lib/rrpair/parser.js
+++ b/lib/rrpair/parser.js
@@ -40,7 +40,7 @@ function parse(path, type) {
var bulkUpload = function (newPath, newFiles, type, message) {
var serv; var rr; var req; var res; var fileCounter = 0; var filesNameJson = {}; var isMacZip = false;
if (type === 'MQ') {serv = new MQService();rr = new MQPair();} else {serv = new Service();rr = new RRPair();}
- var restMethods = constants.ALL_REST_METHODS; var numberOfFiles = newFiles.length;
+ var restMethods = constants.ALL_REST_METHODS; var numberOfFiles = newFiles.length; var labelCounter = 0;
//removing Mac related files/directory.
if (newFiles.includes("__MACOSX")) {
@@ -93,6 +93,7 @@ var bulkUpload = function (newPath, newFiles, type, message) {
var match = onlyFileNameRegx.exec(filename);
if (match[1].endsWith(constants.REQ_FILE_END)) {
req = true;
+ rr.label = 'RRPair'+ ++labelCounter;
if (type === constants.REST) {
var firstWord = content.substr(0, content.indexOf(constants.SPACE));
if (!restMethods.includes(firstWord)) {
diff --git a/lib/util/constants.js b/lib/util/constants.js
index 939e2146..8ac0ef7e 100644
--- a/lib/util/constants.js
+++ b/lib/util/constants.js
@@ -1,5 +1,4 @@
module.exports = Object.freeze({
- //below constants for bulk upload parser.js
SPACE: ' ',
HTTP: 'HTTP',
SOAP: 'SOAP',
@@ -25,6 +24,44 @@ module.exports = Object.freeze({
TEXT_XML: 'text/xml',
APPLICATION_JSON: 'application/json',
TEXT_PLAIN: 'text/plain',
- //below constants for app.js of express framework.
ORG_USR_REGISTER_VIEW: 'Mockiato ☕
You are using Mockiato in your organziation. Please login with your organization credentials.
',
+ ALL_SERVICE_TYPE: '["REST", "SOAP", "MQ"]',
+ REST_SOAP_REQUIREDFIELD_ERRMSG: 'Required fields (Group, Name, Base Path, Request/Response Pair) is not present.',
+ MQ_REQUIREDFIELD_ERRMSG: 'Required fields (Group, Name, Request/Response Pair) is not present.',
+ SOAP_MQ_RRPAIR_REQFIELD_ERRMSG: 'Required fields (Request Payload, Response Payload) not present in Request/Response Pairs.',
+ REST_RRPAIR_REQFIELD_ERRMSG: 'Required fields (HTTP Method, Payload Type) not present in Request/Response Pairs.',
+ REST_RRPAIR_REQRESDATA_FORMAT: 'Request or Response is not in correct Json format.',
+ SOAP_MQ_RRPAIR_REQRESDATA_FORMAT: 'Request or Response is not in correct xml format.',
+ ALL_PAYLOAD_TYPE: ['JSON', 'XML', 'PLAIN'],
+ SUT_NOT_PRESENT_ERR_MSG: 'Error: sut not present in imported Template',
+ MQ_VALID_XML_REQ_ERR: 'Request is not a valid xml.',
+ MQ_VALID_XML_RES_ERR: 'Response is not a valid xml.',
+ REQUIRED_REQUEST_PAYLOAD_ERR: 'Required field Request Payload (rrpairs.reqData) is not present in request.',
+ REQUIRED_RESPONSE_PAYLOAD_ERR: 'Required field Response Payload (rrpairs.resData) is not present in request.',
+ REQUIRED_SUT_ERR: 'Required field Group (sut) is not present in request.',
+ REQUIRED_SUT_PARAMS_ERR: 'Required field Group (sut or sut/name) is not present in request.',
+ REQURIED_SUT_NAME_ERR: 'Required field Gruop name (sut.name) is not present in request.',
+ REQUIRED_SUT_MEMBERS_ERR: 'Required field Gruop members (sut.members) is not present in request.',
+ REQUIRED_BASEPATH_ERR: 'Required field Base Path (basePath) is not present in request.',
+ REQUIRED_SERVICE_NAME_ERR: 'Required field Service Name (name) is not present in request.',
+ REQUIRED_SERVICE_TYPE_ERR: 'Required field Service Type (type) is not present in request.',
+ REQUIRED_RRPAIRS_ERR: 'Required field Request/Response Pair (rrpairs) is not present in request.',
+ REQUIRED_HTTP_METHOD_ERR: 'Required field HTTP Method (rrpairs.verb) is not present in request.',
+ REQUIRED_RRPAIRS_PAYLOADTYPE_ERR: 'Required field Payload Type (rrpairs.payloadType) is not present in request.',
+ SERVICETYPE_PAYLAODTYPE_COMBINATION_ERR: 'Service Type and Payload Type (payloadType in rrpairs) combination is incorrect.',
+ PAYLOADTYPE_REQDATA_NOMATCH_ERR: 'Syntax of Request Payload (reqData) is invalid. It don\'t match with given Payload Type.',
+ PAYLOADTYPE_RESDATA_NOMATCH_ERR: 'Syntax of Response Payload (resData) is invalid. It don\'t match with given Payload Type.',
+ NOT_VALID_VERB:' is not a valid Http Method' ,
+ NOT_VALID_PAYLOADTYPE:' is not a valid PayLoad Type ',
+ USER_NOT_AUTHORIZED_ERR: 'User not authorized to create on this group.',
+ REQUST_NO_RRPAIR: 'Request don\'t contain rrpairs',
+ DIFF_TYPE_SERV_ERR: 'There is already a different type(Rest/Soap) of service available with same name and basepath.',
+ SERVICES_DIFFNAME_SAMEBASEPATH_ERR: 'There is another service already exist in our system with same basepath.',
+ LIVE_OR_VIRTUAL_NOT_ERR: 'You must choose between "Live first" or "Virtual first" for a Live Invocation.',
+ REMOTE_HOST_NOT_ERR: 'Please provide remote host',
+ REMOTE_PORT_NOT_ERR: 'please provide remote port',
+ REQUIRED_SERVICE_ERR : 'There is no Service in this recorder creation request.',
+ REQUIRED_RECORDER_SERVICE_NAME_ERR: 'Record Service name is mandatory. Please provide.',
+ DUP_RECORDER_PATH_BODY: 'This recorder\'s group and path overlap with an active recorder.',
+ NOT_VALID_INTEGER: ' is not a valid Positive Integer Number for '
});
\ No newline at end of file
diff --git a/lib/util/index.js b/lib/util/index.js
index 232cfd55..f0c58897 100644
--- a/lib/util/index.js
+++ b/lib/util/index.js
@@ -2,6 +2,7 @@ const fs = require('fs');
const jwt = require('jsonwebtoken');
const assert = require('assert');
const debug = require('debug')('default');
+const logger = require('../../winston');
// function for responding with errors
global.handleError = function(e, res, stat) {
@@ -159,4 +160,19 @@ if (!Object.entries)
return resArray;
};
+
+global.logEvent = function(path, label, msg) {
+ debug(path, label, msg);
+
+ let event = {};
+ event.path = path;
+ event.label = label;
+ event.msg = msg;
+
+ logger.info(event);
+}
+
+global.escapeRegExp = function(string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
+}
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
\ No newline at end of file
diff --git a/models/common/System.js b/models/common/System.js
index 43f23f39..a80971f6 100644
--- a/models/common/System.js
+++ b/models/common/System.js
@@ -1,8 +1,15 @@
const mongoose = require('mongoose');
+const constants = require('../../lib/util/constants');
const sutSchema = new mongoose.Schema({
- name: String,
- members: {type : Array, "default" : [] }
+ name: {
+ type : String,
+ required: [true, constants.REQURIED_SUT_NAME_ERR]
+ },
+ members: {
+ type : Array,
+ default : []
+ }
});
module.exports = mongoose.model('SUT', sutSchema);
diff --git a/models/http/RRPair.js b/models/http/RRPair.js
index 856401b7..4e328d6a 100644
--- a/models/http/RRPair.js
+++ b/models/http/RRPair.js
@@ -1,13 +1,82 @@
const mongoose = require('mongoose');
+const xml2js = require("xml2js");
+const constants = require('../../lib/util/constants');
const rrSchema = new mongoose.Schema({
- verb: String,
+ verb: {
+ type: String,
+ required: [ function () {
+ return this.parent().type === 'REST';
+ },
+ constants.REQUIRED_HTTP_METHOD_ERR
+ ],
+ validate: {
+ validator: function (v) {
+ if (this.parent().type === 'REST' && !constants.ALL_REST_METHODS.includes(v))
+ return false;
+ else return true;
+ },
+ message: '{VALUE}'+constants.NOT_VALID_VERB+'({PATH}).'
+ }
+ },
+
path: String,
- payloadType: String,
+ payloadType: {
+ type: String,
+ required: [true, constants.REQUIRED_RRPAIRS_PAYLOADTYPE_ERR],
+ enum: {
+ values: constants.ALL_PAYLOAD_TYPE,
+ message: '{VALUE}'+constants.NOT_VALID_PAYLOADTYPE+'({PATH}).'
+ },
+ validate: {
+ validator: function (v) {
+ if (this.parent().type === 'SOAP' && v === 'XML')
+ return true;
+ else if (this.parent().type === 'REST' && (v === 'XML' || v === 'JSON' || v === 'PLAIN'))
+ return true;
+ else return false;
+ },
+ message: constants.SERVICETYPE_PAYLAODTYPE_COMBINATION_ERR
+ }
+ },
// use schema-less data-types
queries: mongoose.Schema.Types.Mixed,
reqHeaders: mongoose.Schema.Types.Mixed,
- reqData: mongoose.Schema.Types.Mixed,
+ reqData: {
+ type: mongoose.Schema.Types.Mixed,
+ validate: {
+ validator: function (v) {
+ /* Making validation true in case of DraftService.
+ In other cases, apply normal validations. */
+ try {
+ if (this.parent().parent().constructor.modelName === 'DraftService') return true; //else continue validations.
+ } catch (e) {/* Not a draft service so continue below validations*/ }
+ if (this.payloadType === 'JSON') {
+ try {
+ JSON.parse(JSON.stringify(v));
+ return true;
+ } catch (e) {
+ return false;
+ }
+ } else if (this.payloadType === 'XML') {
+ try{
+ xml2js.parseString(v, function (err, result) {
+ if(err) throw err;
+ });
+ return true;
+ }catch(e){return false;}
+ } else {
+ return true;
+ }
+ },
+ message: constants.PAYLOADTYPE_REQDATA_NOMATCH_ERR
+ },
+ required: [function () {
+ return this.parent().type === 'SOAP';
+ },
+ constants.REQUIRED_REQUEST_PAYLOAD_ERR
+ ]
+ },
reqDataString: String,
resStatus: {
// force integer only
@@ -17,7 +86,41 @@ const rrSchema = new mongoose.Schema({
set: function(v) { return Math.round(v); }
},
resHeaders: mongoose.Schema.Types.Mixed,
- resData: mongoose.Schema.Types.Mixed,
+ resData: {
+ type: mongoose.Schema.Types.Mixed,
+ validate: {
+ validator: function (v) {
+ /* Making validation true in case of DraftService.
+ In other cases, apply normal validations. */
+ try {
+ if (this.parent().parent().constructor.modelName === 'DraftService') return true; //else continue validations.
+ } catch (e) {/* Not a draft service so continue below validations*/ }
+ if (this.payloadType === 'JSON') {
+ try {
+ JSON.parse(JSON.stringify(v));
+ return true;
+ } catch (e) {
+ return false;
+ }
+ } else if (this.payloadType === 'XML') {
+ try{
+ xml2js.parseString(v, function (err, result) {
+ if(err) throw err;
+ });
+ return true;
+ }catch(e){return false;}
+ } else {
+ return true;
+ }
+ },
+ message: constants.PAYLOADTYPE_RESDATA_NOMATCH_ERR
+ },
+ required: [function () {
+ return this.parent().type === 'SOAP';
+ },
+ constants.REQUIRED_RESPONSE_PAYLOAD_ERR
+ ]
+ },
resDataString: String,
label: String
});
diff --git a/models/http/Recording.js b/models/http/Recording.js
index 909f0c09..4ac01d40 100644
--- a/models/http/Recording.js
+++ b/models/http/Recording.js
@@ -1,19 +1,51 @@
const mongoose = require('mongoose');
const Service = require('./Service');
const System = require('../common/System');
+const constants = require('../../lib/util/constants');
const recordingSchema = new mongoose.Schema({
- service: Service.schema,
- sut : System.schema,
- path : String,
- remoteHost : String,
- remotePort : Number,
+ service: {
+ type: Service.schema,
+ required: [true, constants.REQUIRED_SERVICE_ERR]
+ },
+ sut: {
+ type: System.schema,
+ required: [true, constants.REQUIRED_SUT_ERR]
+ },
+ path : {
+ type: String,
+ required: [true, constants.REQUIRED_BASEPATH_ERR]
+ },
+ remoteHost : {
+ type: String,
+ required: [true, constants.REMOTE_HOST_NOT_ERR]
+ },
+ remotePort : {
+ type: Number,
+ required: [true, constants.REMOTE_PORT_NOT_ERR]
+ },
payloadType : String,
protocol : String,
headerMask : Array,
- name : String,
+ name: {
+ type: String,
+ required: [true, constants.REQUIRED_RECORDER_SERVICE_NAME_ERR]
+ },
active : Boolean,
- ssl : Boolean
+ ssl : Boolean,
+ running: {
+ type: Boolean,
+ default: true
+ },
+ filters:{
+ enabled : {
+ type: Boolean,
+ default : false
+ },
+ bodyStrings : [String],
+ headers : [{key:String,value:String}],
+ statuses : [Number]
+ }
});
recordingSchema.set('usePushEach', true);
diff --git a/models/http/Service.js b/models/http/Service.js
index 4e99ba7f..71ca5e19 100644
--- a/models/http/Service.js
+++ b/models/http/Service.js
@@ -3,34 +3,58 @@ const mongoose = require('mongoose');
const User = require('../common/User');
const System = require('../common/System');
const RRPair = require('./RRPair');
+const constants = require('../../lib/util/constants');
const serviceSchema = new mongoose.Schema({
- sut: System.schema,
+ sut: {
+ type: System.schema,
+ required: [true, constants.REQUIRED_SUT_ERR]
+ },
user: User.schema,
name: {
- type: String,
+ type: String,
+ required: [true, constants.REQUIRED_SERVICE_NAME_ERR],
index: true
},
- type: String,
+ type: {
+ type: String,
+ required: [true, constants.REQUIRED_SERVICE_TYPE_ERR]
+ },
basePath: {
- type: String,
+ type: String,
+ required: [true, constants.REQUIRED_BASEPATH_ERR],
index: true
},
matchTemplates: [mongoose.Schema.Types.Mixed],
- rrpairs: [RRPair.schema],
+ rrpairs: {
+ type: [RRPair.schema]
+ //required: [true, constants.REQUIRED_RRPAIRS_ERR]
+ },
delay: {
// force integer only
type: Number,
default: 0,
- get: function(v) { return Math.round(v); },
- set: function(v) { return Math.round(v); }
+ validate: {
+ validator: function (v) {
+ if (Number.isInteger(v) && v >= 0)
+ return true;
+ else return false;
+ },
+ message: '{VALUE}'+constants.NOT_VALID_INTEGER+'({PATH}).'
+ }
},
delayMax: {
// force integer only
type: Number,
default: 0,
- get: function(v) { return Math.round(v); },
- set: function(v) { return Math.round(v); }
+ validate: {
+ validator: function (v) {
+ if (Number.isInteger(v) && v >= 0)
+ return true;
+ else return false;
+ },
+ message: '{VALUE}'+constants.NOT_VALID_INTEGER+'({PATH}).'
+ }
},
txnCount: {
type: Number,
@@ -44,17 +68,136 @@ const serviceSchema = new mongoose.Schema({
},
lastUpdateUser:{
type: User.schema
- },liveInvocation:{
+ }, liveInvocation: {
enabled: Boolean,
- liveFirst: Boolean,
- remoteHost : String,
- remotePort : Number,
- remoteBasePath : String,
- failStatusCodes : [Number],
- failStrings : [String],
- ssl: Boolean
+ liveFirst: {
+ type: Boolean,
+ required: [function () {
+ return this.liveInvocation.enabled;
+ },
+ constants.LIVE_OR_VIRTUAL_NOT_ERR
+ ]
+ },
+ remoteHost: {
+ type: String,
+ required: [function () {
+ return this.liveInvocation.enabled;
+ },
+ constants.REMOTE_HOST_NOT_ERR
+ ]
+ },
+ remotePort: {
+ type: Number,
+ required: [function () {
+ return this.liveInvocation.enabled;
+ },
+ constants.REMOTE_PORT_NOT_ERR
+ ]
+ },
+ remoteBasePath: String,
+ failStatusCodes: [Number],
+ failStrings: [String],
+ ssl: Boolean,
+ record : {
+ type: Boolean,
+ default: false
+ },
+ recordedRRPairs:[RRPair.schema]
}
},{timestamps:{createdAt:'createdAt',updatedAt:'updatedAt'}});
+/**
+ * Strips down an RRPair for quick logical comparison
+ * @param {RRPair} rrpair
+ */
+function stripRRPair(rrpair) {
+ return {
+ verb: rrpair.verb || '',
+ path: rrpair.path || '',
+ payloadType: rrpair.payloadType || '',
+ queries: rrpair.queries || {},
+ reqHeaders: rrpair.reqHeaders || {},
+ reqData: rrpair.reqData || {},
+ resStatus: rrpair.resStatus || 200,
+ resData: rrpair.resData || {}
+ };
+}
+
+/**
+ * Goes through liveinvocation.recordedRRPairs and strips out all duplicates before saving the service
+ * @param {Service} service
+ */
+function filterDuplicateRecordedPairs(service){
+
+ //This check probably looks excessive- it is necessary because service can be null, the live invo group can be null, the recorded section can be null, and recorded can be 0 length!
+ //Not checking makes the entire app grind to a half if any of those are null.
+ if(service && service.liveInvocation && service.liveInvocation.recordedRRPairs && service.liveInvocation.recordedRRPairs.length){
+ var pairs = service.liveInvocation.recordedRRPairs;
+ var keepThisRRPair = [];
+ var strippedPairs = [];
+ for(let i = 0; i < pairs.length; i++){
+ keepThisRRPair.push(true);
+ strippedPairs[i] = stripRRPair(pairs[i]);
+ }
+
+ //iterate over all RR pairs, calling them i.
+ for(let i = 0; i < strippedPairs.length; i++){
+
+ //If i has not already been eliminated...
+ if(keepThisRRPair[i]){
+
+ //Then compare it against all RRPairs further down the list than i, calling each one j
+ for(let j = i+1; j < strippedPairs.length;j++){
+
+ //If i and j are duplicates, mark off j (but not i) to NOT include in the trimmed list.
+ if(deepEquals(strippedPairs[i],strippedPairs[j])){
+ keepThisRRPair[j] = false;
+ }
+ }
+ }
+ }
+ //Extract only the rrpairs we want to
+ var rrPairs = [];
+ for(let i = 0; i < pairs.length; i++){
+ if(keepThisRRPair[i]){
+ rrPairs.push(pairs[i]);
+ }
+ }
+
+ //if changes were made, save them
+ if(rrPairs.length != pairs.length){
+ service.liveInvocation.recordedRRPairs = rrPairs;
+ logEvent("","Mongoose","Saving modified pairs. Old count:" + pairs.length + " New count: " + rrPairs.length);
+ service.save(function(err){
+ logEvent("","Mongoose",err);
+ });
+ }
+
+ }
+
+ return service;
+}
+
+
+
+/**
+ * The below hooks hook on every find or findOne call made to the Service schema. On each call, every service will be checked for duplicate recorded RRPairs under liveInvocation.recordedRRPairs.
+ * Any duplicates will be stripped (except the original pair recorded) from the list of recordedRRPairs. This will happen before Mockiato receives the result of the query, and this change will be saved immediately.
+ */
+
+serviceSchema.post('findOne',function(result){
+ filterDuplicateRecordedPairs(result);
+});
+
+serviceSchema.post('find',function(results){
+ results.forEach(function(result){
+ filterDuplicateRecordedPairs(result);
+ })
+});
+
+
+
+
+
serviceSchema.set('usePushEach', true);
module.exports = mongoose.model('Service', serviceSchema);
\ No newline at end of file
diff --git a/models/mq/MQPair.js b/models/mq/MQPair.js
index 4ffb7c11..8b84d769 100644
--- a/models/mq/MQPair.js
+++ b/models/mq/MQPair.js
@@ -1,9 +1,49 @@
const mongoose = require('mongoose');
+xml2js = require("xml2js");
+const constants = require('../../lib/util/constants');
const pairSchema = new mongoose.Schema({
label: String,
- reqData: mongoose.Schema.Types.Mixed,
- resData: mongoose.Schema.Types.Mixed
+ reqData: {
+ type: mongoose.Schema.Types.Mixed,
+ validate: {
+ validator: function (v) {
+ /* Making validation true in case of DraftService.
+ In other cases, apply normal validations. */
+ try{
+ if (this.parent().parent().constructor.modelName === 'DraftService') return true; //else continue validations.
+ } catch (e) {/* Not a draft service so continue below validations*/ }
+ try{
+ xml2js.parseString(v, function (err, result) {
+ if(err) throw err;
+ });
+ return true;
+ }catch(e){return false;}
+ },
+ message: constants.MQ_VALID_XML_REQ_ERR
+ },
+ required: [true, constants.REQUIRED_REQUEST_PAYLOAD_ERR]
+ },
+ resData: {
+ type: mongoose.Schema.Types.Mixed,
+ validate: {
+ validator: function (v) {
+ /* Making validation true in case of DraftService.
+ In other cases, apply normal validations. */
+ try{
+ if (this.parent().parent().constructor.modelName === 'DraftService') return true; //else continue validations.
+ } catch (e) {/* Not a draft service so continue below validations*/ }
+ try{
+ xml2js.parseString(v, function (err, result) {
+ if(err) throw err;
+ });
+ return true;
+ }catch(e){return false;}
+ },
+ message: constants.MQ_VALID_XML_RES_ERR
+ },
+ required: [true, constants.REQUIRED_RESPONSE_PAYLOAD_ERR]
+ },
});
module.exports = mongoose.model('MQPair', pairSchema);
\ No newline at end of file
diff --git a/models/mq/MQService.js b/models/mq/MQService.js
index b0ef9fbe..ead2750b 100644
--- a/models/mq/MQService.js
+++ b/models/mq/MQService.js
@@ -5,18 +5,29 @@ const System = require('../common/System');
const MQPair = require('./MQPair');
const MQInfo = require('./MQInfo');
+const constants = require('../../lib/util/constants');
const mqSchema = new mongoose.Schema({
- sut: System.schema,
+ sut: {
+ type: System.schema,
+ required: [true, constants.REQUIRED_SUT_ERR]
+ },
user: User.schema,
name: {
- type: String,
+ type: String,
+ required: [true, constants.REQUIRED_SERVICE_NAME_ERR],
index: true
},
- type: String,
+ type: {
+ type: String,
+ required: [true, constants.REQUIRED_SERVICE_TYPE_ERR]
+ },
matchTemplates: [mongoose.Schema.Types.Mixed],
connInfo: MQInfo.schema,
- rrpairs: [MQPair.schema],
+ rrpairs: {
+ type: [MQPair.schema],
+ required: [true, constants.REQUIRED_RRPAIRS_ERR]
+ },
running: {
type: Boolean,
default: true
diff --git a/package.json b/package.json
index b8a5fb2c..427d18f1 100644
--- a/package.json
+++ b/package.json
@@ -5,13 +5,14 @@
"scripts": {
"start": "node ./bin/www",
"serve": "pm2 start ./bin/www -i ${MOCKIATO_NODES:=1} --name app --no-daemon",
- "test": "nyc --reporter=text mocha tests --exit"
+ "test": "nyc --reporter=text --reporter=html mocha tests --exit"
},
"nyc": {
"exclude": [
"tests",
"lib/pm2",
- "lib/remove-route"
+ "lib/remove-route",
+ "winston.js"
]
},
"dependencies": {
diff --git a/public/index.html b/public/index.html
index f0807484..61a7f51c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -55,7 +55,7 @@