integration
Version:
Enterprise integration patterns for JavaScript
671 lines (570 loc) • 22.2 kB
JavaScript
/*
* Copyright (c) 2012 VMware, Inc. All Rights Reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
(function (buster, define) {
'use strict';
var assert, refute, fail, undef;
assert = buster.assert;
refute = buster.refute;
fail = buster.assertions.fail;
define('integration-test', function (require) {
var integration, bus, when;
integration = require('integration');
when = require('when');
require('integration/channels/direct');
buster.testCase('integration', {
setUp: function () {
bus = integration.bus();
},
tearDown: function () {
bus.destroy();
},
'should detect a message bus': function () {
assert(integration.isBus(bus));
refute(integration.isBus({}));
},
'should create a message for a payload and headers': function () {
var message = bus._message('payload', { name: 'value' });
assert.same('payload', message.payload);
assert.same('value', message.headers.name);
},
'should create a message with headers, even when none are provided': function () {
var message = bus._message('payload');
assert.same('payload', message.payload);
assert(message.headers.id);
},
'should not modify an exsisting message payload, headers must be different': function () {
var message = bus._message('payload');
assert.same(message.payload, bus._message(message).payload);
refute.equals(message.headers, bus._message(message).headers);
},
'should contain unique message IDs': function () {
refute.equals(bus._message().headers.id, bus._message().headers.id);
},
'should create channels that pass messages': function () {
var publisher, consumer;
publisher = bus.channel();
consumer = {
handle: function (message) {
assert.same('hello world', message.payload);
}
};
bus.subscribe(publisher, consumer);
bus.send(publisher, 'hello world');
},
'should lookup channels with a name': function () {
bus.channel('identity');
bus.channel('not-identity');
assert.same(bus.resolveChannel('identity'), bus.resolveChannel('identity'));
refute.same(bus.resolveChannel('identity'), bus.resolveChannel('not-identity'));
refute(bus.resolveHandler('identity'));
refute(bus.resolveHandler('not-identity'));
},
'should lookup handlers with a name': function () {
bus.filter('identity', function () { return true; });
bus.filter('not-identity', function () { return true; });
assert.same(bus.resolveHandler('identity'), bus.resolveHandler('identity'));
refute.same(bus.resolveHandler('identity'), bus.resolveHandler('not-identity'));
refute(bus.resolveChannel('identity'));
refute(bus.resolveChannel('not-identity'));
},
'should return provided channels/handlers when passed as name': function () {
var channel, pseudoChannel, handler, pseudoHandler;
channel = bus.channel();
pseudoChannel = { send: function () {} };
handler = bus.filter();
pseudoHandler = { handle: function () {} };
assert.same(channel, bus.resolveChannel(channel));
assert.same(pseudoChannel, bus.resolveChannel(pseudoChannel));
refute(bus.resolveHandler(channel));
refute(bus.resolveHandler(pseudoChannel));
assert.same(handler, bus.resolveHandler(handler));
assert.same(pseudoHandler, bus.resolveHandler(pseudoHandler));
refute(bus.resolveChannel(handler));
refute(bus.resolveChannel(pseudoHandler));
},
'should adapt messages to normal function invocations with outbound adapters': function () {
var publisher, consumer;
publisher = bus.channel();
consumer = bus.outboundAdapter(function (message) {
assert.same('hello world', message);
});
bus.subscribe(publisher, consumer);
bus.send(publisher, 'hello world');
},
'should adapt normal function invocations to messages with inbound adapters': function () {
var publisher, adapter, consumer;
publisher = bus.channel();
adapter = bus.inboundAdapter(publisher);
consumer = {
handle: function (message) {
assert.same('hello world', message.payload);
}
};
bus.subscribe(publisher, consumer);
adapter('hello world');
},
'should adapt normal function invocations to messages with inbound adapters with a transform': function () {
var publisher, adapter, consumer;
publisher = bus.channel();
adapter = bus.inboundAdapter(publisher, String.prototype.toUpperCase);
consumer = {
handle: function (message) {
assert.same('HELLO WORLD', message.payload);
}
};
bus.subscribe(publisher, consumer);
adapter('hello world');
},
'should apply a sequence number to inbound messages': function () {
var handler, adapter;
handler = {
handle: this.spy(function () { return true; })
};
adapter = bus.inboundAdapter(bus.directChannel(handler));
adapter('hello');
adapter('world');
assert.same(2, handler.handle.callCount);
assert.same(0, handler.handle.getCall(0).args[0].headers.sequenceNumber);
assert.same(1, handler.handle.getCall(1).args[0].headers.sequenceNumber);
},
'should start local and then ask parent bus to find handlers': function () {
var parent, child;
parent = bus;
child = parent.bus();
refute(parent.resolveHandler('parent'));
refute(child.resolveHandler('parent'));
refute(parent.resolveHandler('child'));
refute(child.resolveHandler('child'));
parent.filter('parent', function () {});
child.filter('child', function () {});
// child can resolve to parent
assert(parent.resolveHandler('parent'));
assert(child.resolveHandler('parent'));
assert.same(parent.resolveHandler('parent'), child.resolveHandler('parent'));
// parent cannot resolve to child
refute(parent.resolveHandler('child'));
assert(child.resolveHandler('child'));
refute.same(child.resolveHandler('child'), parent.resolveHandler('child'));
},
'should start local and then ask parent bus to find channels': function () {
var parent, child;
parent = bus;
child = parent.bus();
refute(parent.resolveChannel('parent'));
refute(child.resolveChannel('parent'));
refute(parent.resolveChannel('child'));
refute(child.resolveChannel('child'));
parent.channel('parent');
child.channel('child');
// child can resolve to parent
assert(parent.resolveChannel('parent'));
assert(child.resolveChannel('parent'));
assert.same(parent.resolveChannel('parent'), child.resolveChannel('parent'));
// parent cannot resolve to child
refute(parent.resolveChannel('child'));
assert(child.resolveChannel('child'));
refute.same(child.resolveChannel('child'), parent.resolveChannel('child'));
},
'should receive dead letter messages at local and parent channels': function () {
var parent, child, channel, callback;
parent = bus;
child = parent.bus();
callback = this.spy(function (message) {
assert.same('you\'re dead to me', message);
});
channel = child.channel();
parent.deadLetterChannel.subscribe(parent.outboundAdapter(callback));
child.deadLetterChannel.subscribe(child.outboundAdapter(callback));
bus.send(channel, 'you\'re dead to me');
assert.same(2, callback.callCount);
},
'should receive invalid messages at local and parent channels': function () {
var parent, child, channel, callback;
parent = bus;
child = parent.bus();
callback = this.spy(function (message) {
assert.same('let\'s hope this works', message);
});
channel = child.channel();
parent.invalidMessageChannel.subscribe(parent.outboundAdapter(callback));
child.invalidMessageChannel.subscribe(child.outboundAdapter(callback));
channel.subscribe(child.outboundAdapter(function () { throw new Error(); }));
bus.send(channel, 'let\'s hope this works');
assert.same(2, callback.callCount);
},
'should dispatch messages to a single subscriber for default channels': function () {
var channel, aSpy, bSpy;
channel = bus.channel();
aSpy = this.spy(function (message) {
assert.equals('one of us gets a message!', message);
});
bSpy = this.spy(function (message) {
assert.equals('one of us gets a message!', message);
});
channel.subscribe(bus.outboundAdapter(aSpy));
channel.subscribe(bus.outboundAdapter(bSpy));
bus.send(channel, 'one of us gets a message!');
bus.send(channel, 'one of us gets a message!');
assert.same(2, aSpy.callCount + bSpy.callCount);
},
'should dispatch to a wiretap in addition to subscriptions': function () {
var channel, tap, sub;
channel = bus.channel();
tap = this.spy(function (message) {
assert.equals('it feels like we\'re being watched', message);
});
sub = this.spy(function (message) {
assert.equals('it feels like we\'re being watched', message);
});
bus.tap(channel, bus.outboundAdapter(tap));
bus.subscribe(channel, bus.outboundAdapter(sub));
bus.send(channel, 'it feels like we\'re being watched');
assert.same(1, tap.callCount);
assert.same(1, sub.callCount);
},
'should dispatch to each wiretap in addition to subscriptions': function () {
var channel, tapA, tapB, subA, subB;
channel = bus.channel();
tapA = this.spy(function (message) {
assert.equals('it feels like we\'re being watched', message);
});
tapB = this.spy(function (message) {
assert.equals('it feels like we\'re being watched', message);
});
subA = this.spy(function (message) {
assert.equals('it feels like we\'re being watched', message);
});
subB = this.spy(function (message) {
assert.equals('it feels like we\'re being watched', message);
});
bus.tap(channel, bus.outboundAdapter(tapA));
bus.tap(channel, bus.outboundAdapter(tapB));
bus.subscribe(channel, bus.outboundAdapter(subA));
bus.subscribe(channel, bus.outboundAdapter(subB));
bus.send(channel, 'it feels like we\'re being watched');
bus.send(channel, 'it feels like we\'re being watched');
assert.same(2, tapA.callCount);
assert.same(2, tapB.callCount);
assert.same(2, subA.callCount + subB.callCount);
},
'should not receive messages at untapped wiretaps': function () {
var channel, tap;
channel = bus.channel();
tap = { handle: this.spy() };
assert.same(0, tap.handle.callCount);
bus.tap(channel, tap);
bus.send(channel, 'it feels like we\'re being watched');
assert.same(1, tap.handle.callCount);
bus.send(channel, 'it feels like we\'re being watched');
assert.same(2, tap.handle.callCount);
bus.untap(channel, tap);
bus.send(channel, 'it feels like we\'re being watched');
assert.same(2, tap.handle.callCount);
},
'should squelch exceptions from wiretaps': function () {
var channel, tap, sub;
channel = bus.channel();
tap = {
handle: this.spy(function () {
throw new Error();
})
};
sub = this.spy(function (message) {
assert.equals('it feels like we\'re being watched', message);
});
channel.tap(tap);
bus.subscribe(channel, bus.outboundAdapter(sub));
bus.send(channel, 'it feels like we\'re being watched');
assert.same(1, tap.handle.callCount);
assert.same(1, sub.callCount);
},
'should alter the message payload with a transform': function () {
bus.channel('in');
bus.channel('out');
bus.transform(function (message) {
return message + '... NOT!';
}, { input: 'in', output: 'out' });
bus.subscribe('out', bus.outboundAdapter(function (message) {
assert.same('JavaScript sucks... NOT!', message);
}));
bus.inboundAdapter('in').call(undef, 'JavaScript sucks');
},
'should filter messages that do not match some criteria': function () {
var func, oddSpy, evenSpy;
oddSpy = this.spy(function (message) {
assert(message % 2 === 1);
});
evenSpy = this.spy(function (message) {
assert(message % 2 === 0);
});
bus.channel('in');
bus.channel('goodNumbers');
bus.channel('otherNumbers');
func = bus.inboundAdapter('in');
bus.filter(function (num) { return num % 2 === 1; }, { input: 'in', output: 'goodNumbers', discard: 'otherNumbers' });
bus.subscribe('goodNumbers', bus.outboundAdapter(oddSpy));
bus.subscribe('otherNumbers', bus.outboundAdapter(evenSpy));
func(0);
func(1);
func(2);
func(3);
func(4);
assert.same(3, evenSpy.callCount);
assert.same(2, oddSpy.callCount);
},
'should route messages dynamically': function () {
bus.channel('in');
bus.router(function (message) { return message.headers.dest; }, { input: 'in' });
bus.channel('resort');
bus.subscribe('resort', bus.outboundAdapter(function (message) {
assert.same('Did I end up at the resort?', message);
}));
bus.send('in', 'Did I end up at the resort?', { dest: 'resort' });
},
'should route messages with channel aliases': function () {
bus.channel('in');
bus.router(function (message) { return message.headers.dest; }, { routes: { resort: 'disneyWorld' }, input: 'in' });
bus.channel('disneyWorld');
bus.subscribe('disneyWorld', bus.outboundAdapter(function (message) {
assert.same('Did I end up at the resort?', message);
}));
bus.send('in', 'Did I end up at the resort?', { dest: 'resort' });
},
'should not suppress routing errors': function () {
try {
bus.router('route', function () { throw new Error(); });
bus.bridge('in', 'route');
bus.send('in', 'Did I end up at the resort?', { dest: 'resort' });
fail('Exception expected');
}
catch (e) {
assert(e);
}
},
'should resolve an aliased channel': function () {
bus.channel('a');
bus.alias('b', 'a');
bus.alias('c', 'b');
assert.same(bus.resolveHandler('a'), bus.resolveHandler('c'));
},
'should execute chain handlers in order': function () {
bus.transform('jr', function (name) {
return name + ' Jr.';
});
bus.transform('md', function (name) {
return name + ' M.D.';
});
bus.directChannel('post', bus.outboundAdapter(function (name) {
assert.equals(name, 'Bigglesworth Jr. M.D.');
}));
bus.channel('pre');
bus.chain(['jr', 'md'], { output: 'post', input: 'pre' });
bus.send('pre', 'Bigglesworth');
},
'should filter messages in a chain': function () {
var spy = this.spy(function (message) {
assert.same('hello', message);
});
bus.channel('start');
bus.channel('end');
bus.subscribe('end', bus.outboundAdapter(spy));
bus.chain([
bus.filter(function (message) {
return (/^[a-z]+$/).test(message);
})
], { input: 'start', output: 'end' });
bus.send('start', 'HELLO');
bus.send('start', 'hello');
assert.same(1, spy.callCount);
},
'should resolve the gateway promise when there is no more work to do': function () {
bus.channel('target').subscribe(bus.transform(function (payload) {
return 'Knock, knock? ' + payload;
}));
bus.inboundGateway('target')('Who\'s there?').then(function (response) {
assert.same('Knock, knock? Who\'s there?', response);
});
},
'should reject the gateway promise when an error is encountered': function () {
bus.channel('target').subscribe(bus.transform(function (/* payload */) {
throw new Error();
}));
bus.inboundGateway('target')('Who\'s there?').then(undef, function (/* response */) {
assert(true);
});
},
'should split a message into multiple messages': function () {
var spy = this.spy(function (message) {
assert.same('msg', message);
});
bus.directChannel('in', 'split');
bus.splitter('split', function (message) {
return message.payload;
}, { output: 'out' });
bus.directChannel('out', bus.outboundAdapter(spy));
bus.send('in', ['msg', 'msg']);
assert.same(2, spy.callCount);
},
'should aggregate two messages into one': function () {
var spy = this.spy(function (message) {
assert.equals(['msg', 'msg'], message);
});
bus.channel('in');
bus.aggregator((function () {
var buffer = [];
return function (message, callback) {
buffer.push(message.payload);
if (buffer.length > 1) {
callback(buffer);
buffer = [];
}
};
}()), { output: 'out', input: 'in' });
bus.directChannel('out', bus.outboundAdapter(spy));
bus.send('in', 'msg');
assert.same(0, spy.callCount);
bus.send('in', 'msg');
assert.same(1, spy.callCount);
},
'should log received messages': function () {
var console, handler;
console = {
log: this.spy()
};
handler = {
handle: this.spy()
};
bus.channel('logger');
bus.subscribe('logger', handler);
bus.logger({
console: console,
prefix: 'Integration message: ',
tap: 'logger'
});
bus.send('logger', 'Hello Console');
assert.same('Hello Console', handler.handle.getCall(0).args[0].payload);
assert.same('Integration message: ', console.log.getCall(0).args[0]);
assert.same('Hello Console', console.log.getCall(0).args[1].payload);
},
'should export channels to the parent message bus': function () {
var parent = bus,
child = parent.bus(),
spy = this.spy();
child.directChannel('out', child.outboundAdapter(spy));
child.exportChannel('subprocess', 'out');
parent.send('subprocess', 'Hello Child');
assert.same(1, spy.callCount);
assert.same('Hello Child', spy.getCall(0).args[0]);
},
'should accept a configuration closure when creating a message bus': function () {
var spy = this.spy();
bus.bus(function () {
this.directChannel('out', this.outboundAdapter(spy));
this.exportChannel('in', 'out');
});
bus.send('in', 'Hello encapsulated child');
assert.same(1, spy.callCount);
assert.same('Hello encapsulated child', spy.getCall(0).args[0]);
refute(bus.resolveChannel('out'));
},
'should post a reply message from an outbound gateway to the output channel': function (done) {
var spy = this.spy(function (message) {
assert.same('HELLO SERVICE', message);
done();
});
bus.channel('in');
bus.outboundGateway('service', function (message) {
var d = when.defer();
setTimeout(function () {
d.resolve(message.toUpperCase());
}, 10);
return d.promise;
}, { input: 'in', output: 'out', error: 'err' });
bus.directChannel('err', bus.outboundAdapter(function () {
fail('A message should not have been received on the error channel');
done();
}));
bus.directChannel('out', bus.outboundAdapter(spy));
bus.send('in', 'Hello Service');
refute(spy.called);
},
'should post a rejected promise from an outbound gateway to the error channel': function (done) {
var spy = this.spy(function (message) {
assert.same('HELLO SERVICE', message);
done();
});
bus.channel('in');
bus.outboundGateway('service', function (message) {
var d = when.defer();
setTimeout(function () {
d.reject(message.toUpperCase());
}, 10);
return d.promise;
}, { input: 'in', output: 'out', error: 'err' });
bus.directChannel('out', bus.outboundAdapter(function () {
fail('A message should not have been received on the output channel');
done();
}));
bus.directChannel('err', bus.outboundAdapter(spy));
bus.send('in', 'Hello Service');
refute(spy.called);
},
'should post a throw error from an outbound gateway to the error channel': function (done) {
var spy = this.spy(function (message) {
assert.same('Hello Service', message);
done();
});
bus.channel('in');
bus.outboundGateway('service', function (message) {
throw message.toUpperCase();
}, { input: 'in', output: 'out', error: 'err' });
bus.directChannel('out', bus.outboundAdapter(function () {
fail('A message should not have been received on the output channel');
done();
}));
bus.directChannel('err', bus.outboundAdapter(spy));
bus.send('in', 'Hello Service');
},
'should forward messages from one channel to another': function () {
var spy = this.spy(function (message) {
assert.same('hello', message);
});
bus.channel('a');
bus.channel('b');
bus.forward('a', 'b');
bus.subscribe('b', bus.outboundAdapter(spy));
bus.send('a', 'hello');
assert.same(1, spy.callCount);
}
});
});
}(
this.buster || require('buster'),
typeof define === 'function' && define.amd ? define : function (id, factory) {
var packageName = id.split(/[\/\-]/)[0], pathToRoot = id.replace(/[^\/]+/g, '..');
pathToRoot = pathToRoot.length > 2 ? pathToRoot.substr(3) : pathToRoot;
factory(function (moduleId) {
return require(moduleId.indexOf(packageName) === 0 ? pathToRoot + moduleId.substr(packageName.length) : moduleId);
});
}
// Boilerplate for AMD and Node
));