UNPKG

mqlight-dev

Version:
836 lines (759 loc) 25.7 kB
/* %Z% %W% %I% %E% %U% */ /* * <copyright * notice="lm-source-program" * pids="5725-P60" * years="2013,2016" * crc="3568777996" > * Licensed Materials - Property of IBM * * 5725-P60 * * (C) Copyright IBM Corp. 2016 * * US Government Users Restricted Rights - Use, duplication or * disclosure restricted by GSA ADP Schedule Contract with * IBM Corp. * </copyright> */ /* jslint node: true */ /* jshint -W083,-W097 */ 'use strict'; /** @const {string} enable unittest mode in mqlight.js */ process.env.NODE_ENV = 'unittest'; var stubproton = require('./stubs/stubproton'); var mqlight = require('../mqlight'); var testCase = require('nodeunit').testCase; var util = require('util'); var Promise = require('bluebird'); /** * Golden path for restart checking state changes. * @constructor * @param {object} test the unittest interface */ /* module.exports.test_successful_restart = function(test) { var client = mqlight.createClient({id: 'test_successful_restart', service: 'amqp://host'}); var timeout = setTimeout(function() { test.ok(false, 'Test timed out waiting for events to be emitted'); if (client) client.stop(); test.done(); }, 5000); client.on('started', function(err) { test.deepEqual(client.state, 'started', 'client status started after start'); stubproton.setConnectStatus(2); setImmediate(function() { mqlight.reconnect(client); }); }); client.on('error', function(err) { test.deepEqual(client.state, 'retrying', 'client in retrying state after error'); stubproton.setConnectStatus(0); }); client.on('restarted', function() { test.deepEqual(client.state, 'started', 'client has restarted'); clearTimeout(timeout); client.stop(function() { test.done(); }); }); }; */ /** * Check the client returns undefined if the reconnect method is invoked * while the client is in stopped state. Note that the reconnect method * is an internal part of the client, and only exposed for unit testing. * @constructor * @param {object} test the unittest interface */ module.exports.test_reconnect_when_disconnected = function(test) { test.expect(1); var opts = { id: 'test_reconnect_when_disconnected', service: 'amqp://host' }; var client = mqlight.createClient(opts, function() { client.stop(function() { test.equals(mqlight.reconnect(client), undefined, 'reconnect when in stopped state returns undefined'); test.done(); }); }); }; /** * Test multiple reconnect calls only cause a single restarted * event. Note that the reconnect method is an internal part of the client, * and only exposed for unit testing. * @constructor * @param {object} test the unittest interface */ module.exports.test_multi_restart_call = function(test) { var client = mqlight.createClient({id: 'test_multi_restart_call', service: 'amqp://host'}); var reconnectedEvents = 0; var timeout = setTimeout(function() { test.ok(false, 'Test timed out waiting for events to be emitted'); if (client) client.stop(); test.done(); }, 5000); client.on('started', function() { stubproton.setConnectStatus(1); mqlight.reconnect(client); mqlight.reconnect(client); mqlight.reconnect(client); }); client.on('error', function() { // second reconnect call should return immediately test.deepEqual(mqlight.reconnect(client).state, 'retrying'); stubproton.setConnectStatus(0); }); client.on('restarted', function() { reconnectedEvents++; test.equals(client.state, 'started', 'client state started after restart'); setTimeout(function() { test.equals(reconnectedEvents, 1, 'reconnected event happened once'); clearTimeout(timeout); client.stop(function() { test.done(); }); },1000); }); }; /** * Test the subscription list is emptied and re-populated when the client * restarts * @constructor * @param {object} test the unittest interface */ module.exports.test_resubscribe_on_restart = function(test) { var client = mqlight.createClient({id: 'test_resubscribe_on_restart', service: 'amqp://host'}); var timeout = setTimeout(function() { test.ok(false, 'Test timed out waiting for events to be emitted'); if (client) client.stop(function() { test.done(); }); }, 5000); var connectErrors = 0; client.on('error', function(err) { if (/connect error: 1/.test(err.message)) { connectErrors++; test.ok(client._subscriptions.length === 0, 'subs list has not ' + 'been cleared'); test.equal(client._queuedSubscriptions.length, 3, 'subs have ' + 'not been queued'); stubproton.setConnectStatus(0); } }); var origSubsList = []; client.once('started', function() { client.subscribe('/topic', 'myshare', function(err) { test.ifError(err); if (connectErrors > 0) return; client.subscribe('/another/topic', function(err) { test.ok(!err); if (connectErrors > 0) return; client.subscribe('/final/topic/', 'diffshare', function(err) { test.ok(!err); if (connectErrors > 0) { if (err) return; test.equal(client._subscriptions.length, origSubsList.length, 'after reconnect subs lists does not match original'); while (client._subscriptions.length > 0) { var expected = origSubsList.pop(); expected.callback = undefined; var actual = client._subscriptions.pop(); actual.callback = undefined; test.deepEqual(actual, expected, 'sub list objects do not match'); } clearTimeout(timeout); client.stop(function() { test.done(); }); } if (connectErrors === 0) { setImmediate(function() { origSubsList = origSubsList.concat(client._subscriptions); stubproton.setConnectStatus(1); mqlight.reconnect(client); }); } }); }); }); }); client.once('restarted', function() { // this allows the restarted callback to get in and re-subscribe setImmediate(function() { test.equal(3, origSubsList.length, 'origSubsList length is wrong'); }); }); }; /** * Stop while retrying behaves as expected * @constructor * @param {object} test the unittest interface */ module.exports.test_stop_while_restarting = function(test) { var client = mqlight.createClient({id: 'test_stop_while_restarting', service: 'amqp://host'}); var timeout = setTimeout(function() { test.ok(false, 'Test timed out waiting for events to be emitted'); if (client) client.stop(); test.done(); }, 5000); client.once('started', function(x, y) { stubproton.setConnectStatus(1); mqlight.reconnect(client); }); client.once('error', function(x, y) { client.stop(); }); client.once('restarted', function(x, y) { test.ok(false, 'should not have restarted'); }); client.once('stopped', function(x, y) { test.deepEqual(client.state, 'stopped', 'state disconected'); // Set connect state to 0 and wait a second in case of restart stubproton.setConnectStatus(0); setTimeout(function() { clearTimeout(timeout); client.removeAllListeners(); client.stop(function() { test.done(); }); },1000); }); }; /** * Test that an error during send result in the queueing of an * AT_LEAST_ONCE message. Then when reconnected this gets sent * and the queue of messages to send is 0. * @param {object} test the unittest interface */ //module.exports.test_single_queued_send = function(test) { // //test.expect(4); // var client = mqlight.createClient({id: 'test_single_queued_send', service: // 'amqp://host'}); // var savedSendFunction = stubproton.sender.send; // var reconnected = 0; // stubproton.sender.send = function() { // return new Promise(function(resolve, reject) { // reject(new Error('error during send')); // }); // }; // // var timeout = setTimeout(function() { // test.ok(false, 'Test timed out waiting for events to be emitted'); // stubproton.sender.send = savedSendFunction; // if (client) client.stop(); // test.done(); // }, 5000); // // var opts = {qos: mqlight.QOS_AT_LEAST_ONCE}; // client.once('started', function(err) { // stubproton.setConnectStatus(1); // client.send('test', 'message', opts, function(err) { // // This callback should only happen after the restart event is emitted // test.equals(reconnected, 1, 'has reconnected'); // test.deepEqual(client.state, 'started', 'state is started'); // test.equals(client._queuedSends.length, 0, 'queued sends now 0'); // clearTimeout(timeout); // client.stop(function() { // test.done(); // }); // }); // }); // // client.once('error', function(err) { // stubproton.setConnectStatus(0); // test.equals(client._queuedSends.length, 1, 'check for queued send'); // }); // // client.on('restarted', function(x, y) { // reconnected++; // stubproton.sender.send = savedSendFunction; // }); //}; /** * Test that when in a retrying state that any attempted * sends are queued and then go through following a restart event * in the expected order. * * @param {object} test the unittest interface. */ module.exports.test_queued_sends_retrying = function(test) { test.expect(); var client = mqlight.createClient({id: 'test_queued_sends_retrying', service: 'amqp://host'}); var callbacksCalled = 0; var callbacksCalledInError = 0; var sentMessages = []; client.on('stopped', function() { test.equal(client._queuedSends.length, 0, 'expected empty queued sends'); test.equal(callbacksCalled, 4, 'expected 4 callbacks called with success'); test.equal(callbacksCalledInError, 0, 'expected 0 callbacks in error'); test.equal(sentMessages.length, 4, 'expected 4 successfully sent messages'); for (var i = 0; i < sentMessages.length; i++) { test.equal(sentMessages[i], 'message ' + i, 'message sent out of order'); } test.done(); }); client.on('error', function(err) { test.deepEqual(client.state, 'retrying', 'client in retrying state after error'); }); client.start(function(err) { stubproton.setConnectStatus(1); mqlight.reconnect(client); // these 4 sends should get queued for ( var i = 0; i < 4; i++ ) { client.send('topic ' + i, 'message ' + i , function(err, topic, body) { if (err) { callbacksCalledInError++; process.nextTick(function() { client.stop(); }); } else { callbacksCalled++; sentMessages.push(body); process.nextTick(function() { if (callbacksCalled >= 4) { client.stop(); } }); } }); } test.equal(client._queuedSends.length, 4, 'Expected 4 queued ' + 'sends. Found: ' + util.inspect(client._queuedSends)); stubproton.setConnectStatus(0); mqlight.reconnect(client); }); }; /** * Test that when in a retrying state that any attempted * subscribes are queued and then go through following a reconnect. * * @param {object} test the unittest interface. */ module.exports.test_queued_subs_retrying = function(test) { var client = mqlight.createClient( {id: 'test_queued_subs_retrying', service: 'amqp://host'}); var timeout = setTimeout(function() { test.ok(false, 'Test timed out waiting for events to be emitted'); if (client) client.stop(); test.done(); }, 5000); client.on('error', function(err) { if (!err instanceof mqlight.NetworkError) console.error(err); }); var successCallbacks = 0; client.once('started', function() { stubproton.setConnectStatus(1); // queue up 4 subscribes for (var i = 1; i < 5; i++) { client.subscribe('queue' + i, function(err) { if (!err) { successCallbacks++; if (successCallbacks === 4) { client.stop(); } } }); } setTimeout(function() { test.strictEqual(client._queuedSubscriptions.length, 4, 'expected to see 4 queued subscriptions, but saw ' + client._queuedSubscriptions.length); stubproton.setConnectStatus(0); }, 1000); }); client.on('stopped', function() { test.equal(successCallbacks, 4, 'expecting 4 success callbacks, saw ' + successCallbacks); clearTimeout(timeout); test.done(); }); }; /** * Test that when the client is still in the 'connecting' state, any attempted * unsubscribes are queued and then go through following a connection. * * @param {object} test the unittest interface. */ module.exports.test_queued_unsubscribe_before_connect = function(test) { var successCallbacks = 0; var onUnsubscribe = function() { successCallbacks++; if (successCallbacks === 4) { client.stop(); } }; var client = mqlight.createClient({ id: 'test_queued_unsubscribe_before_connect', service: function(callback) { test.strictEqual(client.state, 'starting'); // queue up 4 unsubscribes before allowing the connection through for (var i = 1; i < 5; i++) { client.subscribe('queue' + i); client.unsubscribe('queue' + i, onUnsubscribe); } test.strictEqual(client._queuedUnsubscribes.length, 4, 'expected to see 4 queued unsubscriptions, but saw ' + client._queuedUnsubscribes.length); callback(null, 'amqp://host'); } }); client.on('stopped', function() { test.equal(successCallbacks, 4, 'expecting 4 success callbacks, saw ' + successCallbacks); test.done(); }); }; /** * Test that when messenger.unsubscribe throws an error, any attempted * unsubscribes are queued and then go through following a reconnect. * * @param {object} test the unittest interface. */ //module.exports.test_queued_unsubscribe_via_error = function(test) { // var client = mqlight.createClient({ // id: 'test_queued_unsubscribe_via_error', // service: 'amqp://host' // }); // // var savedSubscribedFn = mqlight.proton.messenger.subscribed; // mqlight.proton.messenger.subscribed = function() { // return true; // }; // // var unsubscribeErrors = 0; // client.on('error', function(err) { // if (/error on unsubscribe/.test(err.message)) { // unsubscribeErrors++; // return; // } // // if (unsubscribeErrors === 4) { // test.strictEqual(client._queuedUnsubscribes.length, 4, // 'expected to see 4 queued unsubscriptions, but saw ' + // client._queuedUnsubscribes.length); // mqlight.proton.messenger.subscribed = savedSubscribedFn; // stubproton.setConnectStatus(0); // setTimeout(function() {client.stop();},1500); // } // }); // // var successCallbacks = 0; // client.once('started', function() { // stubproton.setConnectStatus(1); // // queue up 4 unsubscribes // for (var i = 1; i < 5; i++) { // client.subscribe('queue' + i, function(err, topicPattern, share) { // if (unsubscribeErrors >= 4) return; // client.unsubscribe(topicPattern, function(err) { // if (!err) { // successCallbacks++; // } // }); // }); // } // }); // // client.on('stopped', function() { // test.equal(successCallbacks, 4, 'expecting 4 success callbacks, saw ' + // successCallbacks); // test.done(); // }); //}; /** * Test that a queued subscribe and unsubscribe for the same address cancel * each other out. We'll do this by submitting 6 subscribes and 4 unsubscribes * which should leave just two subscribes. * * @param {object} test the unittest interface. */ module.exports.test_queued_before_connect_unsubscribe_nop = function(test) { var callbacks = 0, subscribes = 0, unsubscribes = 0; var client = mqlight.createClient({ id: 'test_queued_before_connect_unsubscribe_nop', service: function(callback) { test.strictEqual(client.state, 'starting'); // queue up 4 subscribes to queue{1,2,3,4,5,6} before allowing connection for (var i = 1; i < 7; i++) { client.subscribe('queue' + i, function() { callbacks++; }); } // queue up 4 unsubscribes to queue{2,3,4,5} before allowing connection for (var j = 2; j < 6; j++) { client.unsubscribe('queue' + j, function() { callbacks++; }); } test.strictEqual(client._queuedSubscriptions.length, 6, 'expected to see 6 queued subscriptions, but saw ' + client._queuedSubscriptions.length); test.strictEqual(client._queuedUnsubscribes.length, 4, 'expected to see 4 queued unsubscriptions, but saw ' + client._queuedUnsubscribes.length); callback(null, 'amqp://host'); } }); var savedSubscribeFunction = client._messenger.createReceiver; client._messenger.createReceiver = function(address) { ++subscribes; return savedSubscribeFunction(address); }; var savedUnsubscribeFunction = stubproton.receiver.detach; stubproton.receiver.detach = function() { ++unsubscribes; return savedUnsubscribeFunction(); }; client.once('started', function() { setTimeout(function() {client.stop();},500); }); client.on('stopped', function() { // we expect all 8 of the subscribe and unsubscribe requests to have their // callbacks called test.equal(callbacks, 10, 'expecting 10 success callbacks, but saw ' + callbacks); // but we only expect 2 subscribes and 2 unsubscribes to have required // processing test.equal(subscribes, 2, 'expecting 2 subscribes, but saw ' + subscribes); test.equal(unsubscribes, 0, 'expecting 0 unsubscribes, but saw ' + unsubscribes); client._messenger.createReceiver = savedSubscribeFunction; stubproton.receiver.detach = savedUnsubscribeFunction; test.done(); }); }; /** * Test that a queued subscribe and unsubscribe for the same address cancel * each other out. We'll do this by submitting 4 subscribes and 2 unsubscribes * where there is an intersection between two of the topics used in these * cases. * * @param {object} test the unittest interface. */ module.exports.test_queued_via_error_unsubscribe_nop = function(test) { stubproton.setConnectStatus(1); var client = mqlight.createClient({ id: 'test_queued_via_error_unsubscribe_nop', service: 'amqp://host' }); var savedSubscribeFunction = client._messenger.createReceiver; client._messenger.createReceiver = function() { return new Promise(function(resolve, reject) { reject(new Error('error on subscribe')); }); }; var savedUnsubscribeFunction = stubproton.receiver.detach; stubproton.receiver.detach = function() { return new Promise(function(resolve, reject) { reject(new Error('error on unsubscribe')); }); }; var subscribeErrors = 0, unsubscribeErrors = 0, subscribeUnsubscribeErrors = 0, subscribes = 0, unsubscribes = 0; client.on('error', function(err) { if (/error on subscribe/.test(err.message)) { subscribeErrors++; return; } if (/error on unsubscribe/.test(err.message)) { unsubscribeErrors++; return; } if (/connect error/.test(err.message)) { subscribeUnsubscribeErrors++; } if ((subscribeErrors === 4 && unsubscribeErrors === 2) || subscribeUnsubscribeErrors === 6) { test.strictEqual(client._queuedSubscriptions.length, 4, 'expected to see 4 queued subscriptions, but saw ' + client._queuedSubscriptions.length); test.strictEqual(client._queuedUnsubscribes.length, 2, 'expected to see 2 queued unsubscriptions, but saw ' + client._queuedUnsubscribes.length); client._messenger.createReceiver = function() { ++subscribes; return savedSubscribeFunction(); }; stubproton.receiver.detach = function() { ++unsubscribes; return savedUnsubscribeFunction(); }; stubproton.setConnectStatus(0); setTimeout(function() {client.stop();},500); } }); var successCallbacks = 0; // queue up 4 subscribes to queue{1,2,3,4} for (var i = 1; i < 5; i++) { client.subscribe('queue' + i, function(err) { if (!err) { successCallbacks++; } }); } // queue up 2 unsubscribes to queue{2,4} for (var j = 2; j < 5; j += 2) { client.unsubscribe('queue' + j, function(err) { if (!err) { successCallbacks++; } }); } client.on('stopped', function() { // we expect all 6 of the subscribe and unsubscribe requests to have their // callbacks called test.equal(successCallbacks, 6, 'expecting 6 success callbacks, saw ' + successCallbacks); // but we only expect 2 subscribes and 0 unsubscribes to have required // processing test.equal(subscribes, 2, 'expecting 2 subscribes, but saw ' + subscribes); test.equal(unsubscribes, 0, 'expecting 0 unsubscribes, but saw ' + unsubscribes); client._messenger.createReceiver = savedSubscribeFunction; stubproton.receiver.detach = savedUnsubscribeFunction; test.done(); }); }; /** * Test that a queued duplicate subscribe for the same address throws the * expected 'already subscribed' / 'not subscribed' error * * @param {object} test the unittest interface. */ module.exports.test_queued_double_subscribe = function(test) { var callbacks = 0, subscribes = 0; var client = mqlight.createClient({ id: 'test_queued_double_subscribe', service: function(callback) { // override client 'service' property client.service = 'amqp://host:5672'; test.strictEqual(client.state, 'starting'); // queue up 2 duplicate subscribes to queue before allowing connection client.subscribe('queue', function() { callbacks++; }); test.throws(function() { client.subscribe('queue', function() { callbacks++; }); }, function(err) { if ((err instanceof mqlight.SubscribedError) && /client already has a queued subscription/.test(err)) { return true; } }, 'Service parameter as non string/array test'); test.strictEqual(client._queuedSubscriptions.length, 1, 'expected to see 1 queued subscription, but saw ' + client._queuedSubscriptions.length); callback(null, 'amqp://host'); } }); var savedSubscribeFunction = client._messenger.createReceiver; client._messenger.createReceiver = function(address) { ++subscribes; return savedSubscribeFunction(address); }; client.once('started', function() { setTimeout(function() {client.stop();},500); }); client.on('stopped', function() { // we expect only one of the subscribes to have had a successful callback test.equal(callbacks, 1, 'expecting 1 callback, but saw ' + callbacks); test.equal(subscribes, 1, 'expecting 1 subscribe, but saw ' + subscribes); client._messenger.createReceiver = savedSubscribeFunction; test.done(); }); }; /** * Test that when the initial connection fails a queued subscribe * will get processed, when it connects. * @param {object} test the unittest interface. */ module.exports.test_initial_failure_retry_sub = function(test){ var client = mqlight.createClient({id: 'test_initial_failure_retry_sub', service: 'amqp://host'}); var callbackCalled = 0; var first = true; client.on('started', function() { setTimeout(function() { client.stop() }, 20); }); client.on('error', function() { if ( first ) { client.subscribe('queuedSub', function(err){ if (err){ test.ok(false, 'should not be called in err'); } else { callbackCalled++; } }); first = false; } else { test.equal(client._queuedSubscriptions.length, 1, 'should be a queued sub'); stubproton.setConnectStatus(0); } }); client.on('stopped', function(){ test.equal(callbackCalled, 1, 'should have called in success'); test.done(); }); stubproton.setConnectStatus(1); }; /** * Test that when the initial attempt to connect to the server fails a queued * send operation will be processed if the client retrys and connects. * @param {object} test the unittest interface. */ module.exports.test_initial_failure_retry_send = function(test){ stubproton.setConnectStatus(1); var client = mqlight.createClient( {id: 'test_initial_failure_retry_send', service: 'amqp://host'} ); var callbackCalled = 0; var first = true; client.on('started', function() { setTimeout(function() { client.stop(); }, 10); }); client.on('error', function() { if (first) { client.send('topic', 'message', function(err) { if (err) { test.ok(false, 'should not be called in err'); } else { callbackCalled++; } }); first = false; } else { test.equal(client._queuedSends.length, 1, 'should be a queued send'); stubproton.setConnectStatus(0); } }); client.on('stopped', function() { test.equal(callbackCalled, 1, 'should be one callback called'); test.done(); }); };