Skip to content

Commit

Permalink
Transform testing part 1 (#82)
Browse files Browse the repository at this point in the history
* transform testing part 1 - error Handlers

* transform testing part 1 - eval args

* transform testing part 1 - misc

* transform testing part 1 - code style

* transform testing part 1 - review changes

* transform testing part 1 - change url to preparedUrl
  • Loading branch information
jgraff2 committed Oct 18, 2017
1 parent e662ea6 commit ce41a49
Show file tree
Hide file tree
Showing 15 changed files with 871 additions and 76 deletions.
2 changes: 1 addition & 1 deletion src/heartbeat/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ function assignContext(ctx, def, refocusInstance, res) {
* @param {Object} generator - Generator object from the heartbeat
*/
function setupRepeater(generator) {
if (generator.generatorTemplate.connection.bulk === true) {
if (commonUtils.isBulk(generator)) {
repeater.createGeneratorRepeater(generator);
} else {
// bulk is false
Expand Down
5 changes: 3 additions & 2 deletions src/remoteCollection/collect.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ function prepareHeaders(headers, ctx) {
*/
function sendRemoteRequest(generator, connection, simpleOauth=null) {
return new Promise((resolve) => {
const remoteUrl = prepareUrl(generator);
// Add the url to the generator so the handler has access to it later.
generator.preparedUrl = prepareUrl(generator);

// If token is present then add token to request header.
if (generator.token) {
Expand All @@ -103,7 +104,7 @@ function sendRemoteRequest(generator, connection, simpleOauth=null) {

// Remote request for fetching data.
request
.get(remoteUrl)
.get(generator.preparedUrl)
.set(headers)
.end((err, res) => {
if (err) {
Expand Down
47 changes: 47 additions & 0 deletions src/remoteCollection/errorSamples.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Copyright (c) 2017, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or
* https://opensource.org/licenses/BSD-3-Clause
*/

/**
* src/remoteCollection/errorSamples.js
*/
const debug = require('debug')
('refocus-collector:remoteCollection:errorSamples');

module.exports = (collectResponse, messageBody) => {
debug('errorSamples', collectResponse.name, messageBody);
const samples = [];
collectResponse.aspects.forEach((a) => {
collectResponse.subjects.forEach((s) => {
const relatedLinks = [];

if (collectResponse.apiLinks) {
relatedLinks.push({
name: 'Sample Generator',
url: collectResponse.apiLinks.GET,
});
}

const repository = collectResponse.generatorTemplate.repository;
if (repository && repository.url) {
relatedLinks.push({
name: 'Sample Generator Template',
url: repository.url,
});
}

samples.push({
name: `${s.absolutePath}|${a.name}`,
messageCode: 'ERROR',
messageBody,
value: 'ERROR',
relatedLinks,
});
});
});
return samples;
};
107 changes: 90 additions & 17 deletions src/remoteCollection/handleCollectResponse.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
const debug = require('debug')('refocus-collector:handleCollectResponse');
const evalUtils = require('../utils/evalUtils');
const errors = require('../errors');
const errorSamples = require('./errorSamples');
const logger = require('winston');
const enqueue = require('../sampleQueue/sampleQueueOps').enqueue;
const httpStatus = require('../constants').httpStatus;

/**
* Validates the response from the collect function. Confirms that it is an
Expand All @@ -32,15 +34,33 @@ function validateCollectResponse(cr) {
'null, and must not be an Array.');
}

if (!cr.res) {
if (!cr.name) {
throw new errors.ValidationError('The argument passed to the ' +
'"handleCollectResponse" function must have a "res" attribute.');
'"handleCollectResponse" function must have a "name" attribute.');
}

if (!cr.name) {
if (!cr.preparedUrl) {
throw new errors.ValidationError('The argument passed to the ' +
'"handleCollectResponse" function must have a "name" attribute.');
'"handleCollectResponse" function must have a "preparedUrl" attribute.');
}

// No response.
if (!cr.res) {
throw new errors.ValidationError(`No response from ${cr.preparedUrl}`);
}

// Invalid response: missing status code.
if (!cr.res.hasOwnProperty('statusCode')) {
throw new errors.ValidationError(`Invalid response from ${cr.preparedUrl}: `
+ 'missing HTTP status code');
}

// Expecting response status code to be 3 digits.
if (!/\d\d\d/.test(cr.res.statusCode)) {
throw new errors.ValidationError(`Invalid response from ${cr.preparedUrl}: `
+ `invalid HTTP status code "${cr.res.statusCode}"`);
}

} // validateCollectResponse

/**
Expand All @@ -60,23 +80,76 @@ function validateCollectResponse(cr) {
function handleCollectResponse(collectResponse) {
debug('Entered handleCollectResponse');
return collectResponse.then((collectRes) => {
try {
validateCollectResponse(collectRes);
const tr = collectRes.generatorTemplate.transform;
const t = Array.isArray(tr) ? tr.join('\n') : tr;
const transformedSamples = evalUtils.safeTransform(t, collectRes);
validateCollectResponse(collectRes);

/*
* If the transform is a string, then we use that function for all
* status codes.
*/
const tr = collectRes.generatorTemplate.transform;
const args = evalUtils.prepareTransformArgs(collectRes);
if (typeof tr === 'string') { // match all status codes
const samplesToEnqueue = evalUtils.safeTransform(tr, args);
logger.info(`{
generator: ${collectRes.name},
numSamples: ${transformedSamples.length},
url: ${collectRes.preparedUrl},
numSamples: ${samplesToEnqueue.length},
}`);
enqueue(transformedSamples);
} catch (err) {
debug(err);
logger.error('handleCollectResponse threw an error: ', err.name,
err.message);
return Promise.reject(err);
return enqueue(samplesToEnqueue);
} else {
/*
* The transform is *not* a string, so handle the response based on the
* status code.
*/
const status = collectRes.res.statusCode;
let func;

// the response was OK, so use the default transform
if (status === httpStatus.OK) {
func = collectRes.generatorTemplate.transform.transform;
}

/*
* Check for a status code regex match which maps to a transform for
* error samples. Use the first one to match. If 200 is matched, it
* will override the default transform.
*/
if (tr.errorHandlers) {
Object.keys(tr.errorHandlers).forEach((statusMatcher) => {
const re = new RegExp(statusMatcher);
if (re.test(status)) {
func = tr.errorHandlers[statusMatcher];
}
});
}

if (func) {
const samplesToEnqueue = evalUtils.safeTransform(func, args);
logger.info(`{
generator: ${collectRes.name},
url: ${collectRes.preparedUrl},
numSamples: ${samplesToEnqueue.length},
}`);
return enqueue(samplesToEnqueue);
} else {
/*
* If there is no transform designated for this HTTP status code, just
* generate default error samples.
*/
const errorMessage = `${collectRes.preparedUrl} returned HTTP status ` +
`${collectRes.res.statusCode}: ${collectRes.res.statusMessage}`;
const samplesToEnqueue = errorSamples(collectRes, errorMessage);
logger.info(`{
generator: ${collectRes.name},
url: ${collectRes.preparedUrl},
error: ${errorMessage},
numSamples: ${samplesToEnqueue.length},
}`);
return enqueue(samplesToEnqueue);
}
}
}).catch((err) => {
})
.catch((err) => {
debug(err);
logger.error('handleCollectResponse threw an error: ', err.name,
err.message);
Expand Down
3 changes: 2 additions & 1 deletion src/repeater/repeater.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const handleCollectResponse =
require('../remoteCollection/handleCollectResponse').handleCollectResponse;
const collect = require('../remoteCollection/collect').collect;
const repeaterSchema = require('../utils/schema').repeater;
const u = require('../utils/commonUtils');

/**
* Tracks all the repeaters defined in the collectors.
Expand Down Expand Up @@ -186,7 +187,7 @@ function createGeneratorRepeater(generator) {
interval: generator.interval,
func: () => collect(generator),
onProgress: handleCollectResponse,
bulk: generator.generatorTemplate.connection.bulk,
bulk: u.isBulk(generator),
subjects: generator.subjects,
});
} // createGeneratorRepeater
Expand Down
12 changes: 12 additions & 0 deletions src/utils/commonUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,16 @@ module.exports = {
return crypted;
}, // encrypt

/**
* Determine if the given Generator is set up for bulk collection
* @param {Object} generator - A Generator object
* @returns {Boolean}
*/
isBulk(generator) {
const gt = generator.generatorTemplate;
const connection = gt && gt.connection;
const bulk = connection && connection.bulk;
return Boolean(bulk);
}, // encrypt

};
44 changes: 32 additions & 12 deletions src/utils/evalUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const logger = require('winston');
const errors = require('../errors');
const evalValidation = require('./evalValidation');
const sampleSchema = require('./schema').sample;
const u = require('./commonUtils');
const ERROR_MESSAGE = {
TRANSFORM: {
NOT_ARRAY: 'The transform function must return an array.',
Expand Down Expand Up @@ -78,7 +79,7 @@ function validateTransformArgs(args) {
return evalValidation.isObject('ctx', args.ctx) &&
evalValidation.isObject('res', args.res) &&
evalValidation.aspects(args.aspects) &&
evalValidation.subjects(args.subjects);
evalValidation.validateSubjectArgs(args);
} // validateTransformArgs

/**
Expand Down Expand Up @@ -121,26 +122,21 @@ function validateSamples(sampleArr, generator) {
throw new errors.TransformError(ERROR_MESSAGE.TRANSFORM.NOT_ARRAY);
}

const subjectArr = []; // array of subject absolute paths
const aspectArr = []; // array of aspect names
let subjectArr; // array of subject absolute paths
let aspectArr; // array of aspect names

// create subject array
if (generator.subjects) {
generator.subjects.forEach((s) => {
subjectArr.push(s.absolutePath.toLowerCase());
});
subjectArr = generator.subjects.map(s => s.absolutePath.toLowerCase());
} else if (generator.subject) {
subjectArr.push(generator.subject.absolutePath.toLowerCase());
subjectArr = [generator.subject.absolutePath.toLowerCase()];
} else {
throw new errors.ValidationError(
'Generator should have subject/subjects attribute.'
'Generator passed to validateSamples should have "subjects" or "subject"'
);
}

// create aspect array, generator will have aspects attribute always
generator.aspects.forEach((a) => {
aspectArr.push(a.name.toLowerCase());
});
aspectArr = generator.aspects.map(a => a.name.toLowerCase());

// no of samples should not exceed the (no. of subjects * the no. of aspects)
if (sampleArr.length > subjectArr.length * aspectArr.length) {
Expand Down Expand Up @@ -220,6 +216,29 @@ function safeTransform(functionBody, args) {
return retval;
}

/**
* Prepare arguments to be passed to the transform function
* @param {Object} generator - Generator object
* @throws {TransformError} - if transform function does not return an array
* of zero or more samples
* @throws {ValidationError} - if any of the above mentioned check fails
*/
function prepareTransformArgs(generator) {
const args = {};

args.ctx = generator.ctx;
args.res = generator.res;
args.aspects = generator.aspects;

if (u.isBulk(generator)) {
args.subjects = generator.subjects;
} else {
args.subject = generator.subjects[0];
}

return args;
}

/**
* Safely executes the toUrl function with the arguments provided.
*
Expand Down Expand Up @@ -257,6 +276,7 @@ module.exports = {
safeEval, // exporting for testability
safeToUrl,
safeTransform,
prepareTransformArgs,
validateTransformArgs, // exporting for testability
validateToUrlArgs, // exporting for testability
validateSamples, // exporting for testability
Expand Down
27 changes: 27 additions & 0 deletions src/utils/evalValidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/
const debug = require('debug')('refocus-collector:evalUtils');
const errors = require('../errors');
const u = require('./commonUtils');

function isObject(name, val) {
debug('Entered evalValidation.isObject:', name, val);
Expand All @@ -28,6 +29,18 @@ function isObject(name, val) {
module.exports = {
isObject,

validateSubjectArgs(args) {
if (!args.subjects && !args.subject) {
throw new errors.ArgsError('Must include either "subjects" or "subject".');
} else if (args.subjects && !args.subject) {
return this.subjects(args.subjects);
} else if (!args.subjects && args.subject) {
return this.subject(args.subject);
} else if (args.subjects && args.subject) {
throw new errors.ArgsError('Must not include both "subjects" and "subject".');
}
},

aspects: (aspects) => {
debug('Entered evalValidation.aspects:', aspects);
if (!aspects) {
Expand Down Expand Up @@ -75,4 +88,18 @@ module.exports = {

return true;
}, // subjects

subject: (subject) => {
debug('Entered evalValidation.subject:', subject);
if (!subject) {
throw new errors.ArgsError('Must include a "subject" attribute.');
}

isObject(`subject`, subject);
if (typeof subject.absolutePath !== 'string') {
throw new errors.ArgsError('"subject" attribute must be a valid subject.');
}

return true;
}, // subject
};
2 changes: 1 addition & 1 deletion src/utils/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const repeater = Joi.object().keys({
});

const sample = Joi.object().keys({
name: Joi.string().regex(/^[0-9A-Za-z_\\-]{1,4096}\|[0-9A-Za-z_\\-]{1,60}$/)
name: Joi.string().regex(/^[0-9A-Za-z_.\\-]{1,4096}\|[0-9A-Za-z_\\-]{1,60}$/)
.required(),
messageBody: Joi.string().max(4096),
messageCode: Joi.string().max(5),
Expand Down
Loading

0 comments on commit ce41a49

Please sign in to comment.