dm
Version:
Dependency Injection Manager
716 lines (543 loc) • 24.3 kB
JavaScript
var _ = require('lodash'),
chance = require('chance'),
sinon = require('sinon'),
chai = require('chai'),
util = require("util"),
DM = require('../../lib/dm'),
EventualParser = require('../../lib/parser/wrapping/eventual'),
Loader = require('../../lib/loader'),
Async = require('../../lib/async'),
RSVP = require('rsvp'),
assert, expect;
chance = new chance;
assert = chai.assert;
expect = chai.expect;
describe("DM", function() {
var async, loader, dm;
beforeEach(function() {
async = Object.create(Async.prototype);
sinon.stub(async, "all", function(list) {
return RSVP.Promise.all(list);
});
sinon.stub(async, "promise", function(cb) {
return new RSVP.Promise(cb);
});
sinon.stub(async, "resolve", function(value) {
return RSVP.Promise.resolve(value);
});
sinon.stub(async, "reject", function(err) {
return RSVP.Promise.reject(err);
});
loader = Object.create(Loader.prototype);
dm = new DM(async, loader);
});
describe("constructor", function() {
it("should throw error when calling without `new`", function() {
expect(DM).to.throw(Error, "Use constructor with the `new` operator");
});
it("should throw error when async is not given", function() {
expect(function() { new DM() }).to.throw(TypeError, "Async is expected");
});
it("should throw error when loader is not given", function() {
expect(function() { new DM(async) }).to.throw(TypeError, "Loader is expected");
});
it("should throw error when config is not an Object", function() {
expect(function() { new DM(async, loader, "bad") }).to.throw(TypeError, "Config is expected to be an Object");
});
it("should throw error when config is not an Object", function() {
expect(function() { new DM(async, loader, "bad") }).to.throw(TypeError, "Config is expected to be an Object");
});
it("should parse config if given", function() {
var service, definition, services,
parameter, value, parameters,
dm;
(services = {})[(service = chance.word())] = (definition = {});
(parameters = {})[(parameter = chance.word())] = (value = {});
dm = new DM(async, loader, {
parameters: parameters,
services: services
});
expect(dm.getDefinition(service)).equal(definition);
expect(dm.getParameter(parameter)).equal(value);
});
});
describe("instance", function() {
describe("#setDefinition", function() {
it("should throw error when key is not a string", function() {
expect(dm.setDefinition).to.throw(TypeError, "Key is expected to be a string");
});
it("should throw error when definition is not object", function() {
expect(dm.setDefinition.bind(dm, chance.word())).to.throw(TypeError, "Definition is expected to be an Object");
});
it("should throw error when definition is already set", function() {
var key;
key = chance.word();
function setDefinition() {
dm.setDefinition(key, {});
}
setDefinition();
expect(setDefinition).to.throw(Error, util.format("Definition for the service '%s' has been already set", key));
});
});
describe("#getDefinition", function() {
it("should throw error when key is not a string", function() {
expect(dm.getDefinition.bind(dm)).to.throw(TypeError, "Key is expected to be a string");
});
it("should return cloned copy of object value", function() {
var key, definition;
dm.setDefinition((key = chance.word()), (definition = {}));
expect(dm.getDefinition(key)).equal(definition);
});
});
describe("#setParameter", function() {
it("should throw error when key is not a string", function() {
expect(dm.setParameter.bind(dm)).to.throw(TypeError, "Key is expected to be a string");
});
it("should throw error when parameter is already set", function() {
var key;
key = chance.word();
function setParameter() {
dm.setParameter(key, chance.word());
}
setParameter();
expect(setParameter).to.throw(Error, util.format("Parameter '%s' is already exists", key));
});
});
describe("#getParameter", function() {
it("should throw error when key is not a string", function() {
expect(dm.getParameter.bind(dm)).to.throw(TypeError, "Key is expected to be a string");
});
it("should return the value", function() {
var key, value;
dm.setParameter((key = chance.word()), (value = chance.word()));
expect(dm.getParameter(key)).equal(value);
});
});
describe("#parse", function() {
var list;
it("should return promise, resolved with given value if it is not a string, array or an object", function(done) {
RSVP
.all((list = [null, chance.natural(), chance.floating(), undefined, new Error]).map(function(value, index) {
return dm.parse(value);
}))
.then(function(results) {
results.forEach(function(result, index) {
var call;
expect(call = async.resolve.getCall(index)).to.exist();
expect(call.calledWithExactly(list[index])).to.be.true();
expect(result).equal(list[index]);
});
})
.then(done)
.catch(done);
});
it("should return result of async.promise", function() {
expect(dm.parse(chance.word())).to.be.instanceof(RSVP.Promise);
});
it("should return synthetic service", function(done) {
var key, service;
key = chance.word();
service = {};
dm.setDefinition(key, {
synthetic: true
});
dm
.parse("@" + key)
.then(function(result) {
expect(result).equal(service);
})
.then(done, done);
dm.set(key, service);
});
// object
it("should resolve with unescaped object if it was escaped by DM#escape", function(done) {
var escaped, target;
escaped = DM.escape((target = { a: "@" + chance.word() }));
dm
.parse(escaped)
.then(function(result) {
expect(result).equal(target);
})
.then(done, done);
});
it("should recursively call parse for every value of an object", function(done) {
var iterable, keys, spy,
calls;
calls = [];
function extractCalls(obj) {
calls.push(obj);
if (_.isObject(obj) || _.isArray(obj)) {
_.forEach(obj, function(value) {
extractCalls(value);
});
}
}
function makeRandomObj(recursive, obj) {
return _.reduce(new Array(chance.natural({min: 3, max: 7})), function(result, value, index) {
var val;
if (obj) {
result[chance.word()] = chance.word();
} else {
switch (index) {
case 1: {
val = recursive ? makeRandomObj(false, false) : makeRandomObj(false, true);
break;
}
default: {
val = chance.word();
break;
}
}
result.push(val);
}
return result;
}, obj ? {} : []);
}
iterable = makeRandomObj(true);
extractCalls(iterable);
sinon.spy(dm, "parse");
dm
.parse(iterable)
.then(function() {
_.forEach(calls, function(target, index) {
var call;
expect(call = dm.parse.getCall(index)).to.exist();
expect(call.calledWith(target)).to.be.true();
});
})
.then(done, done);
});
});
describe("#has", function() {
it("should throw error when key is not a string", function() {
expect(dm.has.bind(dm)).to.throw(TypeError, "Key is expected to be a string");
});
it("should call #getDefinition method with given key", function() {
var key;
key = chance.word();
sinon.spy(dm, "getDefinition");
dm.has(key);
expect(dm.getDefinition.getCall(0).calledWithExactly(key)).to.be.true();
});
it("should return boolean", function() {
var key;
key = chance.word();
dm.setDefinition(key, {
synthetic: true
});
expect(dm.has(chance.word())).to.be.false();
expect(dm.has(key)).to.be.true();
});
});
describe("#initialized", function() {
it("should throw error when key is not a string", function() {
expect(dm.initialized.bind(dm)).to.throw(TypeError, "Key is expected to be a string");
});
it("should check dm.services with given key", function() {
var key;
key = chance.word();
dm.setDefinition(key, {
synthetic: true
});
dm.set(key, {});
expect(dm.initialized(key)).to.be.true();
expect(dm.initialized(chance.word())).to.be.false();
});
});
describe("#set", function() {
it("should throw error when key is not a string", function() {
expect(dm.set.bind(dm)).to.throw(TypeError, "Key is expected to be a string");
});
it("should throw error when service is not configured", function() {
var key;
key = chance.word();
expect(dm.set.bind(dm, key)).to.throw(Error, "Definition is not found for the '" + key + "' service");
});
it("should throw error when service is not synthetic", function() {
var key;
key = chance.word();
dm.setDefinition(key, {});
expect(dm.set.bind(dm, key, {})).to.throw(Error, "Could not inject non synthetic service '" + key + "'");
});
it("should throw error when service is already initialized", function() {
var key;
key = chance.word();
dm.setDefinition(key, {
synthetic: true
});
dm.set(key, {});
expect(dm.set.bind(dm, key, {})).to.throw(Error, "Service '" + key + "' is already set");
});
it("should fulfill deferred requests", function(done) {
var key, service;
key = chance.word();
service = {};
dm.setDefinition(key, {
synthetic: true
});
RSVP
.all(_.map(new Array(chance.natural({min: 3, max: 10})), function() {
return dm.get(key);
}))
.then(function(results) {
_.forEach(results, function(result) {
expect(result).to.be.equal(service);
})
})
.then(done, done);
dm.set(key, service);
});
});
describe("#get", function() {
it("should throw error when key is not a string", function() {
expect(dm.get.bind(dm)).to.throw(TypeError, "Key is expected to be a string");
});
it("should throw error when service has no definition", function(done) {
var key;
key = chance.word();
dm.get(key)
.catch(function(err) {
expect(err).to.be.instanceOf(Error);
expect(err.message).equal("Definition is not found for the '" + key + "' service")
})
.then(done, done);
});
it("should throw error when service has no path and not alias or synthetic", function(done) {
var key;
key = chance.word();
dm.setDefinition(key, {});
dm.get(key)
.catch(function(err) {
expect(err).to.be.instanceOf(Error);
expect(err.message).equal("Path is expected in definition of service '" + key + "'")
})
.then(done, done);
});
it("should return forthcoming promise for synthetic service", function() {
var key;
key = chance.word();
dm.setDefinition(key, {
synthetic: true
});
expect(dm.get(key)).to.be.instanceOf(RSVP.Promise);
});
it("should return aliased service", function(done) {
var key, alias, service;
key = chance.word();
alias = chance.word();
service = {};
dm.setDefinition(key, {
alias: alias
});
dm.setDefinition(alias, {
synthetic: true
});
dm.set(alias, service);
dm
.get(key)
.then(function(result) {
expect(result).to.be.equal(service);
})
.then(done, done);
});
it("should throw error when aliased service is not defined", function(done) {
var key, alias;
key = chance.word();
alias = chance.word();
dm.setDefinition(key, {
alias: alias
});
dm
.get(key)
.catch(function(err) {
expect(err).to.be.instanceOf(Error);
expect(err.message).equal("Service '" + key + "' could not be alias for not existing '" + alias + "' service");
})
.then(done, done);
});
it("should return provider for lazy services", function(done) {
var Service;
Service = function(){};
sinon.stub(loader, "require", function() {
return async.resolve(Service);
});
dm.setDefinition("service", {
path: chance.word(),
lazy: true
});
dm
.get("service")
.then(function(provider) {
var firstCall;
expect(typeof provider).equal("function");
return provider();
})
.then(function(result) {
expect(result).to.be.instanceof(Service);
})
.then(done, done);
});
it("should return one level (not nested) provider for lazy deferred services", function(done) {
var Service;
Service = function(){};
sinon.stub(loader, "require", function() {
return async.resolve(Service);
});
dm.setDefinition("service", {
path: chance.word(),
lazy: true
});
dm
.get("service", { deferred: true })
.then(function(provider) {
var firstCall;
expect(typeof provider).equal("function");
return provider();
})
.then(function(result) {
expect(result).to.be.instanceof(Service);
})
.then(done, done);
});
it("should build every time new service, when sharing is off", function(done) {
var key;
key = chance.word();
// stub loader
// return Object constructor
sinon.stub(loader, "require", function() {
return RSVP.Promise.resolve(Object);
});
dm.setDefinition(key, {
path: chance.word(),
share: false
});
RSVP
.all(_.map(new Array(chance.natural({min: 2, max: 10})), function() {
return dm.get(key);
}))
.then(function(results) {
expect(_.uniq(results).length).to.be.equal(results.length);
})
.then(done, done);
});
it("should build once new service, and return one instance, when sharing is on", function(done) {
var key;
key = chance.word();
// stub loader
// return Object constructor
sinon.stub(loader, "require", function() {
return RSVP.Promise.resolve(Object);
});
dm.setDefinition(key, {
path: chance.word(),
share: true
});
RSVP
.all(_.map(new Array(chance.natural({min: 2, max: 10})), function() {
return dm.get(key);
}))
.then(function(results) {
expect(_.uniq(results).length).to.be.equal(1);
})
.then(done, done);
});
});
describe("#build", function() {
it("should throw error when definition is not an object", function() {
expect(dm.build.bind(dm)).to.throw(TypeError, "Object is expected");
});
it("should reject when definition.path is not a string", function(done) {
dm.build({})
.then(function() {
done(new Error("Fulfilled, expected to be rejected"));
})
.catch(function(err) {
expect(err).to.be.instanceof(TypeError);
expect(err.message).equal("definition.path is expected to be a string");
})
.then(done, done);
});
it("should call loader.require before parsing definition", function(done) {
var requireStub, parseStub,
path;
requireStub = sinon.stub(loader, "require", function() {
return RSVP.Promise.resolve(_.noop);
});
parseStub = sinon.stub(dm, "parse", function(obj) {
return RSVP.Promise.resolve(obj);
});
dm
.build({
path: (path = chance.word()),
arguments: [],
calls: [],
properties: {},
factory: _.noop
})
.then(function() {
var rCall;
expect(rCall = requireStub.firstCall).to.exist();
expect(rCall.calledWithExactly(path));
parseStub.getCalls().forEach(function(call) {
expect(rCall.calledBefore(call));
});
})
.then(done, done);
});
it("should call #parse with new parser for lazy services", function(done) {
var requireStub, parseStub,
path;
sinon.stub(loader, "require", function() {
return RSVP.Promise.resolve(_.noop);
});
parseStub = sinon.stub(dm, "parse", function(obj) {
return RSVP.Promise.resolve(obj);
});
dm
.build({
path: chance.word(),
lazy: true,
factory: _.noop
}, [])
.then(function() {
var parseCall, parser;
expect(parseCall = parseStub.secondCall).to.exist();
parser = parseCall.args[1];
expect(parser).to.be.instanceof(EventualParser);
expect(parser).not.equal(dm.parser);
})
.then(done, done);
});
it("should pass parsed definition to the factory", function(done) {
var factory, ctor, args, calls, properties,
parseSpy, def;
factory = sinon.spy();
ctor = function() {};
sinon.stub(loader, "require", function() {
return RSVP.Promise.resolve(ctor);
});
parseSpy = sinon.stub(dm, "parse", function(obj) {return RSVP.Promise.resolve(obj);});
def = {
path: chance.word(),
arguments: (args = []),
calls: (calls = []),
properties: (properties = {}),
factory: factory
};
dm
.build(def)
.then(function() {
expect(parseSpy.callCount).equal(2);
expect(parseSpy.firstCall.args[0]).equal(def.path);
expect(parseSpy.secondCall.args[0]).deep.equal(_.omit(def, "path"));
expect(factory.callCount).equal(1);
expect(factory.firstCall.args[0].arguments).equal(args);
expect(factory.firstCall.args[0].calls).equal(calls);
expect(factory.firstCall.args[0].properties).equal(properties);
expect(factory.firstCall.args[0].operand).equal(ctor);
})
.then(done, done);
});
});
})
});