/**
* @version 1.0
* @author Boris GBAHOUE
* @file Generic Scenario to chain Templates with a minimum of logic.
* @module amiwo/test
*/
// =============================================================
// BASE SETUP
// =============================================================
// call the packages we need
var debug = require('debug')('comingup:test');
var util = require('util');
var u = require('../util');
var when = require('when');
var assert = require('assert');
// load our Objects
var Scenario = require('./Scenario');
var CreateTemplate = require('./db/DBObjectCreateTemplate');
var EditTemplate = require('./db/DBObjectEditTemplate');
var DeleteTemplate = require('./db/DBObjectDeleteTemplate');
// =============================================================
// CONSTRUCTOR
// =============================================================
/**
* @class
*
* @constructor
* @augments module:amiwo/test~Scenario
* @param {Object} sequence: {init: {Object}, body: {Object}, close: {Object}} object containing the dataset of the tests to run. Each nested object must contain
* - _type => used for uri and options
* - (create|edit|delete) Template dataset to run
* - can use "#ID"+<_name>+"::"+<_type> to reference the ID of a previously created Object
* @param {Objet} uri: an object containing the URI information of the routes to tests, structured as {
* <_type>: {
* (create|edit|delete): {method, route}
* }
* }
* @param {Objet} options: an object containing the options of the routes to tests, structured as {
* <_type>: {
* (create|edit|delete): {method, route}
* }
* }
*/
function ChainedTemplateScenario(sequence, uri, options) {
Scenario.call(this);
this.sequence = sequence;
this.uri = uri;
this.options = options;
this.deleteLaterObjectList = [];
this.idHash = {};
}
/**
* Inherit from `Scenario`.
*/
util.inherits(ChainedTemplateScenario, Scenario);
// =============================================================
// PUBLIC METHODS
// =============================================================
/**
* Run a test an generate a result
*
* @return {Promise} promise wrapping an Object which format must be the same as the input of the check() method
*/
ChainedTemplateScenario.prototype.run = function() {
var self = this;
if (u.isEmpty(self.sequence)) {
debug("::AMIWO::TESTS:%s::RUN::LOG No sequence data to process", self.constructor.name.toUpperCase());
return when.resolve("Nothing to do");
}
var chainedPromise = when.resolve(true);
// Init tasks
if (u.isNotEmpty(self.sequence.init)) {
var tasks = self.sequence.init;
if (!Array.isArray(tasks)) tasks = [tasks];
for (var i = 0; i < tasks.length; i++) {
chainedPromise = chainedPromise.then($runTemplate.call(self, tasks[i], "Init"));
}
}
// Body tasks
if (u.isNotEmpty(self.sequence.body)) {
var tasks = self.sequence.body;
if (!Array.isArray(tasks)) tasks = [tasks];
for (var i = 0; i < tasks.length; i++) {
chainedPromise = chainedPromise.then($runTemplate.call(self, u.clone(tasks[i]), "Body"));
}
}
// Close tasks
if (u.isNotEmpty(self.sequence.close)) {
var tasks = self.sequence.close;
if (!Array.isArray(tasks)) tasks = [tasks];
for (var i = 0; i < tasks.length; i++) {
chainedPromise = chainedPromise.then($runTemplate.call(self, u.clone(tasks[i]), "Close"));
}
}
// Delete later tasks
chainedPromise = chainedPromise.then(function $executeDeleteLate() {
var innerChainedPromise = when.resolve(true);
// deleteLaterObjectList should now be propertly initialiazed
for (var i = 0; i < self.deleteLaterObjectList.length; i++) {
innerChainedPromise = innerChainedPromise.then(self.deleteLaterObjectList[i].execute.bind(self.deleteLaterObjectList[i]));
}
return innerChainedPromise;
});
return chainedPromise;
}
// =============================================================
// PRIVATE METHODS
// =============================================================
/**
* @memberOf ChainedTemplateScenario
* @private
*/
function $runTemplate(task, debugMsg) {
var self = this;
return function(ok) {
// Sanity check on the task to run
var type = task._type;
var name = task._dbObject;
assert(u.isNotEmpty(task), util.format("::%s::$RUNINITTASK::ERROR Invalid task to run: task is empty", self.constructor.name.toUpperCase()));
assert(u.isNotEmpty(type), util.format("::%s::$RUNINITTASK::ERROR Invalid task to run: _type is not set", self.constructor.name.toUpperCase()));
assert(u.isNotEmpty(name), util.format("::%s::$RUNINITTASK::ERROR Invalid task to run: _dbObject is not set", self.constructor.name.toUpperCase()));
// Run all the Templates embedeed in this task in order (create -> edit -> delete)
debug("::AMIWO::TESTS::%s::LOG\t\t* Running task %s > %s > %s", self.constructor.name.toUpperCase(), debugMsg, task._name, type);
var dataSet = $createFrom.call(self, task);
var chainedPromise = when.resolve(true);
if (task.create) {
var $create = new CreateTemplate(name, dataSet.create, self.uri[type].create, self.options[type].create);
chainedPromise = chainedPromise.then($create.execute.bind($create)).
then(function(object) {
self.idHash["#ID" + task._name + "::" + type] = object._id;
return when.resolve(object);
});
}
if (task.edit) {
chainedPromise = chainedPromise.then(function $edit(object) {
var editData = dataSet.edit;
if (task.edit) {
// object will be the created object
if (object == null) return when.reject(new Error("::CHAINEDTEMPLATESCENARIO::$RUNTEMPLATE::ERROR Null object received after create"));
if (u.isEmpty(object._id)) return when.reject(new Error("::CHAINEDTEMPLATESCENARIO::$RUNTEMPLATE::ERROR Object with no ID set received after create"));
var idProp = editData.ID;
editData[idProp] = object._id;
delete editData.ID;
} else {
// object is empty => dataset elements must suffice
delete editData.ID;
}
var $edit = new EditTemplate(name, editData, self.uri[type].edit, self.options[type].edit);
return $edit.execute();
});
}
if (task.delete) {
if (task.delete["$later"] == true) {
// Run the delete template later
chainedPromise = chainedPromise.then(function $deleteLater(object) {
var deleteData = dataSet.delete;
if (task.create || task.edit) {
// object will be the created or edited object
if (object == null) return when.reject(new Error("::CHAINEDTEMPLATESCENARIO::$RUNTEMPLATE::ERROR Null object received after create or edit"));
if (u.isEmpty(object._id)) return when.reject(new Error("::CHAINEDTEMPLATESCENARIO::$RUNTEMPLATE::ERROR Object with no ID set received after create or edit"));
var idProp = deleteData.ID;
deleteData[idProp] = object._id;
} else {
// object is empty => dataset elements must suffice
}
self.deleteLaterObjectList.push(new DeleteTemplate(name, deleteData, self.uri[type].delete, self.options[type].delete));
return when.resolve(true); // as expected for a successfull DeleteTemplate execution
});
} else {
// Run the delete template now
chainedPromise = chainedPromise.then(function $deleteNow(object) {
var deleteData = dataSet.delete;
if (task.create || task.edit) {
// object will be the created or edited object
if (object == null) return when.reject(new Error("::CHAINEDTEMPLATESCENARIO::$RUNTEMPLATE::ERROR Null object received after create or edit"));
if (u.isEmpty(object._id)) return when.reject(new Error("::CHAINEDTEMPLATESCENARIO::$RUNTEMPLATE::ERROR Object with no ID set received after create or edit"));
var idProp = deleteData.ID;
deleteData[idProp] = object._id;
} else {
// object is empty => dataset elements must suffice
}
var $delete = new DeleteTemplate(name, deleteData, self.uri[type].delete, self.options[type].delete);
return $delete.execute();
});
}
}
return chainedPromise;
}
}
/**
* Replace special codes by their corresponding values (i.e. #ID)
*
* @memberOf ChainedTemplateScenario
* @private
*/
function $createFrom(task) {
if (u.isEmpty(task)) return {};
var obj = u.clone(task);
for (var key in obj) {
if (obj[key] instanceof Object) {
obj[key] = $createFrom.call(this, obj[key]);
} else if ((u.typeOf(obj[key]) == 'string') && (obj[key].indexOf("#ID") > -1)) {
obj[key] = this.idHash[obj[key]];
}
}
return obj;
}
module.exports = ChainedTemplateScenario;