Source: test/ChainedTemplateScenario.js

/**
 * @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;