├── .npmignore ├── .travis.yml ├── test ├── nodeunit.json ├── testexample.js ├── teststop.js ├── stubs │ └── stubproton.js ├── testunsubscribe.js ├── testsend.js ├── testsubscribe.js └── testrestart.js ├── .eslintrc.js ├── samples ├── tonic-example.js ├── recv.js ├── send.js └── uiworkout.js ├── example.js ├── .gitignore ├── sm.dot ├── package.json ├── publish.js ├── bin └── mqlight-debug.js ├── LICENSE.md └── mqlight-log.js /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | .travis.yml 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | - '8' 5 | - '10' 6 | os: 7 | - linux 8 | - osx 9 | install: 10 | - NODE_ENV=unittest npm install 11 | script: 12 | - NODE_ENV=unittest npm test 13 | -------------------------------------------------------------------------------- /test/nodeunit.json: -------------------------------------------------------------------------------- 1 | { 2 | "error_prefix": "", 3 | "error_suffix": "", 4 | "ok_prefix": "", 5 | "ok_suffix": "", 6 | "bold_prefix": "", 7 | "bold_suffix": "", 8 | "assertion_prefix": "", 9 | "assertion_suffix": "" 10 | } 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'xo', 5 | rules: { 6 | 'complexity': 0, 7 | 'curly': 0, 8 | 'indent': [2, 2, {SwitchCase: 1}], 9 | 'max-depth': 0, 10 | 'max-len': [1, 80, 4, {ignoreComments: true, ignoreUrls: true}], 11 | 'max-params': 0, 12 | 'new-cap': ['error', { 'capIsNewExceptions': ['RefreshSettled'] }], 13 | 'no-warning-comments': 0, 14 | 'require-jsdoc': 1, 15 | 'space-before-function-paren': [2, 'never'], 16 | 'valid-jsdoc': [2, {requireReturn: false, prefer: {returns: 'return'}}], 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /samples/tonic-example.js: -------------------------------------------------------------------------------- 1 | var mqlight = require('mqlight'); 2 | 3 | var client = mqlight.createClient({service: 'amqp://localhost:5672'}); 4 | 5 | client.on('started', function() { 6 | client.subscribe('news/technology', function(err, pattern) { 7 | if (err) { 8 | console.error('Problem with subscribe request: ', err.toString()); 9 | } else { 10 | console.log('Subscribed to pattern: ', pattern); 11 | console.log('Sending message : Hello World!'); 12 | client.send('news/technology', 'Hello World!'); 13 | } 14 | }); 15 | 16 | client.on('message', function(data) { 17 | console.log('Got message: ', data); 18 | console.log('Exiting.'); 19 | process.exit(0); 20 | }); 21 | }); 22 | 23 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var mqlight = require("mqlight") 2 | 3 | var client = mqlight.createClient({service: 'amqp://localhost:5672'}); 4 | 5 | client.on('started', function() { 6 | client.subscribe('news/technology', function(err, pattern) { 7 | if (err) { 8 | console.error('Problem with subscribe request: ', err.message); 9 | } else { 10 | console.log('Subscribed to pattern: ', pattern); 11 | console.log('Sending message : Hello World!'); 12 | client.send('news/technology', 'Hello World!'); 13 | } 14 | }); 15 | 16 | client.on('message', function(data, delivery) { 17 | console.log('Got message: ', data); 18 | console.log('Exiting.'); 19 | process.exit(0); 20 | }); 21 | }); 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directories 27 | node_modules/* 28 | jspm_packages 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | build/* 37 | lib/* 38 | 39 | # Include bundled node_modules in git 40 | !node_modules/amqp10 41 | -------------------------------------------------------------------------------- /sm.dot: -------------------------------------------------------------------------------- 1 | // Render using: 2 | // dot -o state-machine.gif -Tgif state-machine.dot 3 | 4 | digraph statemachine { 5 | fontsize=12; 6 | stopping -> stopped [label=" "]; 7 | stopped -> starting [label=" start()"]; 8 | starting -> retrying1 [label=" [failed]\l "]; 9 | starting -> started [label=" [connected]\l "]; 10 | starting -> stopping [label=" stop()", weight=1]; 11 | retrying1 -> retrying1 [label=" [failed]\l "]; 12 | retrying1 -> stopping [label=" stop()"]; 13 | retrying1 -> started [label=" [connected]\l "] 14 | retrying2 -> started [label=" [connected]\l "]; 15 | retrying2 -> retrying2 [label=" [failed]\l "]; 16 | started -> stopping [label=" stop()"]; 17 | started -> retrying2 [label=" [broken]\l "]; 18 | retrying2 -> stopping [label=" stop()"]; 19 | 20 | { rank = same; starting; retrying1 } 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mqlight", 3 | "version": "2.0.2019041500", 4 | "description": "IBM MQ Light Client Module", 5 | "author": "IBM MQ Light ", 6 | "homepage": "https://developer.ibm.com/messaging/mq-light/", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/mqlight/nodejs-mqlight.git" 10 | }, 11 | "tonicExampleFilename": "samples/tonic-example.js", 12 | "bugs": { 13 | "url": "https://ibm.biz/mqlight-forum", 14 | "email": "mqlight@uk.ibm.com" 15 | }, 16 | "keywords": [ 17 | "simple", 18 | "robust", 19 | "messaging", 20 | "AMQP", 21 | "offload", 22 | "responsive" 23 | ], 24 | "main": "mqlight.js", 25 | "bin": { 26 | "mqlight-debug": "./bin/mqlight-debug.js" 27 | }, 28 | "dependencies": { 29 | "amqp10-link-cache": "1.2.2", 30 | "debug": "^2.2.0", 31 | "moment": "^2.24.0", 32 | "mqlight-forked-amqp10": "=3.5.3-1.fork", 33 | "nopt": "~3.0.6", 34 | "npmlog": "~2.0.4", 35 | "uuid": "^2.0.2" 36 | }, 37 | "devDependencies": { 38 | "bluebird": "^3.4.6", 39 | "eslint": "^3.4.0", 40 | "eslint-config-xo": "^0.15.4", 41 | "istanbul": ">0.0.0", 42 | "nodeunit": ">0.0.0" 43 | }, 44 | "license": "Apache-2.0", 45 | "scripts": { 46 | "lint": "node ./node_modules/.bin/eslint mqlight.js", 47 | "test": "npm run lint && node ./node_modules/istanbul/lib/cli.js cover ./node_modules/nodeunit/bin/nodeunit -- --config test/nodeunit.json test" 48 | }, 49 | "engines": { 50 | "node": ">=4.0.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/testexample.js: -------------------------------------------------------------------------------- 1 | /* %Z% %W% %I% %E% %U% */ 2 | /* 3 | * 8 | * Licensed Materials - Property of IBM 9 | * 10 | * 5725-P60 11 | * 12 | * (C) Copyright IBM Corp. 2014 13 | * 14 | * US Government Users Restricted Rights - Use, duplication or 15 | * disclosure restricted by GSA ADP Schedule Contract with 16 | * IBM Corp. 17 | * 18 | */ 19 | /* jslint node: true */ 20 | /* jshint -W083,-W097 */ 21 | 'use strict'; 22 | 23 | // *********************************************************************** 24 | // Example unit test, that can be used as the starting point for new tests 25 | // *********************************************************************** 26 | 27 | 28 | /** @const {string} enable unittest mode in mqlight.js */ 29 | process.env.NODE_ENV = 'unittest'; 30 | 31 | var mqlight = require('../mqlight'); 32 | var testCase = require('nodeunit').testCase; 33 | 34 | // Individual test cases can be defined like this... 35 | 36 | 37 | 38 | /** 39 | * @constructor 40 | * @param {object} test the unittest interface 41 | */ 42 | module.exports.example_test1 = function(test) { 43 | test.expect(1); 44 | // How many assertion tests do we expect? 45 | test.ok(true, 'this should always work'); 46 | test.done(); // Test is done. Did we run all the assertions? 47 | }; 48 | 49 | // Groups of tests (bracketed by a setUp and tearDown function) work like 50 | // this... 51 | 52 | 53 | 54 | /** @constructor */ 55 | module.exports.example_test_group = { 56 | setUp: function(callback) { 57 | // Do some setup here.. 58 | callback(); 59 | }, 60 | tearDown: function(callback) { 61 | // Do the tear down here... 62 | callback(); 63 | }, 64 | example_test2: function(test) { 65 | test.done(); 66 | }, 67 | example_test3: function(test) { 68 | test.done(); 69 | } 70 | }; 71 | 72 | // Crib-sheet: see https://github.com/caolan/nodeunit 73 | // 74 | // Nodeunit provides the following functions for testing with: 75 | // 76 | // ok(value, [message]) 77 | // - Tests if value is a true value. 78 | // equal(actual, expected, [message]) 79 | // - Tests shallow, coercive equality with the equal comparison 80 | // operator ( == ). 81 | // notEqual(actual, expected, [message]) 82 | // - Tests shallow, coercive non-equality with the not equal comparison 83 | // operator ( != ). 84 | // deepEqual(actual, expected, [message]) 85 | // - Tests for deep equality. 86 | // notDeepEqual(actual, expected, [message]) 87 | // - Tests for any deep inequality. 88 | // strictEqual(actual, expected, [message]) 89 | // - Tests strict equality, as determined by the strict equality 90 | // operator ( === ) 91 | // notStrictEqual(actual, expected, [message]) 92 | // - Tests strict non-equality, as determined by the strict not equal 93 | // operator ( !== ) 94 | // throws(block, [error], [message]) 95 | // - Expects block to throw an error. 96 | // doesNotThrow(block, [error], [message]) 97 | // - Expects block not to throw an error. 98 | // ifError(value) 99 | // - Tests if value is not a false value, throws if it is a true value. 100 | // Useful when testing the first argument, error in callbacks. 101 | // 102 | // Nodeunit also provides the following functions within tests: 103 | // 104 | // expect(amount) 105 | // - Specify how many assertions are expected to run within a test. Very 106 | // useful for ensuring that all your callbacks and assertions are run. 107 | // done() 108 | // - Finish the current test function, and move on to the next. ALL tests 109 | // should call this! 110 | 111 | -------------------------------------------------------------------------------- /publish.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | require('core-js'); 4 | var child_process = require('child_process'); 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | var request = require('request'); 8 | var package_json = require('./package.json'); 9 | 10 | var GITHUB_API = "https://github.ibm.com/api/v3"; 11 | var TOKEN = process.env.GITHUB_TOKEN; 12 | 13 | // var remote_path_parts = package_json.binary.remote_path 14 | // .replace(/^\/|\/$/, '').split('/'); 15 | var OWNER = 'mqlight'; 16 | var REPO = 'nodejs-mqlight'; 17 | var TAG = package_json.version; 18 | 19 | var NODE_PRE_GYP = path.join(__dirname, 'node_modules', '.bin', 'node-pre-gyp'); 20 | 21 | var merge = function(baseo, newo) { 22 | if (newo) { 23 | Object.keys(newo).forEach(function(k) { 24 | var v = newo[k]; 25 | if (typeof(v)==='object' && !Buffer.isBuffer(v)) { 26 | baseo[k] = merge(baseo[k] || Object.create(null), v); 27 | } else { 28 | baseo[k] = v; 29 | } 30 | }); 31 | } 32 | return baseo; 33 | }; 34 | 35 | var api = function(options, not_json) { 36 | return new Promise(function(resolve, reject) { 37 | options = merge({ 38 | url: GITHUB_API + '/repos/' + OWNER + '/' + REPO + '/releases', 39 | headers: { 40 | Authorization: 'token ' + TOKEN 41 | } 42 | }, options); 43 | request(options, function(error, response, body) { 44 | if (error) { return reject(error); } 45 | if (!(response.statusCode >= 200 && response.statusCode <= 299)) { 46 | var msg = 'ERROR ' + response.statusCode+': ' + response.statusMessage; 47 | try { 48 | body = JSON.parse(body); 49 | if (body.message) { 50 | msg += ': ' + body.message; 51 | } 52 | } catch (e) { /* ignore */ } 53 | msg += ' (' + options.url + ')'; 54 | return reject(msg); 55 | } 56 | if (!not_json) { body = JSON.parse(body); } 57 | resolve({ resp: response, body: body}); 58 | }); 59 | }); 60 | }; 61 | 62 | var npgReveal = function() { 63 | return new Promise(function(resolve, reject) { 64 | child_process.execFile(NODE_PRE_GYP, [ 'reveal' ], 65 | function(error, stdout, stderr) { 66 | if (error) { return reject(error); } 67 | resolve(stdout); 68 | }); 69 | }).then(function(stdout) { return JSON.parse(stdout); }); 70 | }; 71 | 72 | var getReleases = function() { 73 | return api(); 74 | }; 75 | 76 | var makeRelease = function() { 77 | return api({ 78 | method: 'POST', 79 | body: JSON.stringify({ 80 | tag_name: TAG, 81 | name: TAG, 82 | draft: true 83 | }), 84 | headers: { 85 | 'Content-Type': 'application/json' 86 | } 87 | }).then(function(resp) { return resp.body; }); 88 | }; 89 | 90 | var deleteAsset = function(asset) { 91 | return api({ 92 | method: 'DELETE', 93 | url: asset.url 94 | }, true /* no json */); 95 | }; 96 | 97 | var uploadAsset = function(reveal, release) { 98 | return api({ 99 | url: release.upload_url.replace(/\{\?[^\}]*\}$/, ''), 100 | qs: { name: reveal.package_name }, 101 | method: 'POST', 102 | headers: { 103 | 'Content-Type': 'application/gzip' 104 | }, 105 | body: fs.readFileSync(path.join(__dirname, reveal.staged_tarball)) 106 | }).then(function(resp) { return resp.body; }); 107 | }; 108 | 109 | var publish = function() { 110 | var release, asset, reveal; 111 | return npgReveal().then(function(_reveal) { 112 | reveal = _reveal; 113 | return getReleases(); 114 | }).then(function(resp) { 115 | // look through the releases for something matching $TAG 116 | var r = resp.body.filter(function(release) { 117 | return release.tag_name === TAG; 118 | }); 119 | if (r.length === 0) { 120 | // create a new release! 121 | return makeRelease(); 122 | } else { 123 | return r[0]; 124 | } 125 | }).then(function(_release) { 126 | release = _release; 127 | // okay, see if this file was previously uploaded 128 | var a = release.assets.filter(function(asset) { 129 | return asset.name === reveal.package_name; 130 | }); 131 | if (a.length > 0) { 132 | return deleteAsset(a[0]); 133 | } 134 | }).then(function() { 135 | return uploadAsset(reveal, release); 136 | }); 137 | }; 138 | 139 | publish().then(function(asset) { 140 | console.log(asset.name + ' uploaded successfully to'); 141 | console.log(asset.browser_download_url); 142 | }, function(e) { 143 | setTimeout(function() { throw e; }, 0); 144 | }); 145 | -------------------------------------------------------------------------------- /test/teststop.js: -------------------------------------------------------------------------------- 1 | /* %Z% %W% %I% %E% %U% */ 2 | /* 3 | * 8 | * Licensed Materials - Property of IBM 9 | * 10 | * 5725-P60 11 | * 12 | * (C) Copyright IBM Corp. 2014 13 | * 14 | * US Government Users Restricted Rights - Use, duplication or 15 | * disclosure restricted by GSA ADP Schedule Contract with 16 | * IBM Corp. 17 | * 18 | */ 19 | /* jslint node: true */ 20 | /* jshint -W083,-W097 */ 21 | 'use strict'; 22 | 23 | 24 | /** @const {string} enable unittest mode in mqlight.js */ 25 | process.env.NODE_ENV = 'unittest'; 26 | 27 | var mqlight = require('../mqlight'); 28 | var testCase = require('nodeunit').testCase; 29 | 30 | 31 | /** 32 | * Test a successful stop, ensuring that both the 'stopped' 33 | * event and the callback passed into client.stop(...) are driven. In 34 | * both cases 'this' should point at client that the event listener / callback 35 | * is associated with. 36 | * @param {object} test the unittest interface 37 | */ 38 | module.exports.test_stop_callback_and_event = function(test) { 39 | var client = mqlight.createClient({ 40 | service: 'amqp://host', 41 | id: 'test_stop_callback_and_event' 42 | }); 43 | client.start(function() { 44 | var count = 0; 45 | client.on('stopped', function() { 46 | test.ok(this === client); 47 | test.equals(arguments.length, 0); 48 | test.equals(client.state, 'stopped'); 49 | test.equals(client.service, undefined); 50 | if (++count == 2) { 51 | test.done(); 52 | } 53 | }); 54 | client.stop(function() { 55 | test.ok(this === client); 56 | test.equals(arguments.length, 0); 57 | if (++count == 2) { 58 | test.done(); 59 | } 60 | }); 61 | }); 62 | }; 63 | 64 | 65 | /** 66 | * Test that the stopped event is fired on a subsequent tick from that in 67 | * which the client.stop(...) call is run - meaning that it is possible 68 | * to call client.stop(...) and client.on('stopped',...) on the same 69 | * tick and still have the event listener fire. 70 | * @param {object} test the unittest interface 71 | */ 72 | module.exports.test_listener_fired_on_subsequent_tick = function(test) { 73 | var client = mqlight.createClient({ 74 | service: 'amqp://host', 75 | id: 'test_listener_fired_on_subsequent_tick' 76 | }); 77 | client.start(); 78 | client.on('started', function() { 79 | client.stop(); 80 | client.on('stopped', function() { 81 | test.done(); 82 | }); 83 | }); 84 | }; 85 | 86 | 87 | /** 88 | * Test that when an argument is specified to the client.stop(...) 89 | * function it must be a callback (e.g. of type function). 90 | * @param {object} test the unittest interface 91 | */ 92 | module.exports.test_stop_argument_is_function = function(test) { 93 | var client = mqlight.createClient({ 94 | service: 'amqp://host', 95 | id: 'test_stop_argument_is_function' 96 | }); 97 | test.throws( 98 | function() { 99 | client.stop(1234); 100 | }, 101 | TypeError, 102 | 'stop should throw TypeError if argument is not a function' 103 | ); 104 | client.stop(); 105 | test.done(); 106 | }; 107 | 108 | 109 | /** 110 | * Test that the stop(...) method returns the instance of the client that 111 | * it is invoked on. This is to allow chaining of methods. 112 | * @param {object} test the unittest interface 113 | */ 114 | module.exports.test_stop_method_returns_client = function(test) { 115 | var client = mqlight.createClient({ 116 | service: 'amqp://host', 117 | id: 'test_stop_method_returns_client' 118 | }); 119 | var result = client.stop(); 120 | test.ok(result === client); 121 | test.done(); 122 | }; 123 | 124 | 125 | /** 126 | * Tests that calling stop on an already stopped client has no 127 | * effect other than to callback any supplied callback function to indicate 128 | * success. 129 | * @param {object} test the unittest interface 130 | */ 131 | module.exports.test_stop_when_already_stopped = function(test) { 132 | var client = mqlight.createClient({ 133 | service: 'amqp://host', 134 | id: 'test_stop_when_already_stopped' 135 | }).stop(); 136 | client.once('stopped', function() { 137 | setImmediate(function() { 138 | client.on('stopped', function() { 139 | test.ok(false, "shouldn't receive stopped event if already stopped"); 140 | }); 141 | client.stop(function(err) { 142 | test.ok(!err); 143 | test.done(); 144 | }); 145 | }); 146 | }); 147 | }; 148 | 149 | 150 | /** 151 | * Tests that when calling stop multiple times, all callbacks 152 | * get invoked. 153 | * @param {object} test the unittest interface 154 | */ 155 | module.exports.test_stop_all_callbacks_called = function(test) { 156 | var count = 0; 157 | var client = mqlight.createClient({ 158 | service: 'amqp://host', 159 | id: 'test_start_all_callbacks_called' 160 | }); 161 | var stopped = function(err) { 162 | test.ok(!err); 163 | count++; 164 | if (count == 3) { 165 | test.done(); 166 | } 167 | }; 168 | client.stop(stopped); 169 | client.stop(stopped); 170 | client.stop(stopped); 171 | }; 172 | 173 | 174 | /** 175 | * Test that if too many arguments are supplied to stop - then they are 176 | * ignored. 177 | * @param {object} test the unittest interface 178 | */ 179 | module.exports.test_stop_too_many_arguments = function(test) { 180 | var client = mqlight.createClient({ 181 | service: 'amqp://host', 182 | id: 'test_stop_too_many_arguments' 183 | }); 184 | client.stop(function(err) { 185 | test.ok(!err); 186 | test.done(); 187 | }, 'spurious'); 188 | }; 189 | 190 | 191 | /** 192 | * Test that the client.subscriptions list is cleared upon a user-requested 193 | * client.stop(...) call. 194 | * 195 | * @param {object} test the unittest interface 196 | */ 197 | module.exports.test_stop_cleared_subscriptions = function(test) { 198 | var client = mqlight.createClient({ 199 | service: 'amqp://host', 200 | id: 'test_stop_cleared_subscriptions' 201 | }); 202 | client.on('started', function() { 203 | client.on('stopped', function() { 204 | test.deepEqual(client._subscriptions, [], 205 | 'client.subscriptions was not ' + 206 | 'cleared during client.stop() call'); 207 | test.done(); 208 | }); 209 | client.subscribe('/foo', function(err) { 210 | test.ifError(err); 211 | test.deepEqual(client._subscriptions.length, 1, 'client.subscriptions ' + 212 | 'was not appended to'); 213 | client.stop(); 214 | }); 215 | }); 216 | }; 217 | -------------------------------------------------------------------------------- /test/stubs/stubproton.js: -------------------------------------------------------------------------------- 1 | /* %Z% %W% %I% %E% %U% */ 2 | /* 3 | * 8 | * Licensed Materials - Property of IBM 9 | * 10 | * 5725-P60 11 | * 12 | * (C) Copyright IBM Corp. 2013, 2016 13 | * 14 | * US Government Users Restricted Rights - Use, duplication or 15 | * disclosure restricted by GSA ADP Schedule Contract with 16 | * IBM Corp. 17 | * 18 | */ 19 | /* jslint node: true */ 20 | /* jshint -W083,-W097 */ 21 | 'use strict'; 22 | 23 | var util = require('util'); 24 | var Promise = require('bluebird'); 25 | 26 | var DEBUG = process.env.MQLIGHT_NODE_STUB_DEBUG || false; 27 | var log = process.env.MQLIGHT_NODE_STUB_LOG_ERROR ? console.error : console.log; 28 | 29 | var connectStatus = 0; 30 | 31 | 32 | /** 33 | * Override the proton connection status. 34 | * 35 | * @param {string} status Specifies the status to override with. 36 | */ 37 | exports.setConnectStatus = function(status) { 38 | if (DEBUG) log('setting connect status to:', status); 39 | connectStatus = status; 40 | }; 41 | 42 | var sendStatus = 7; // PN_STATUS_SETTLED = 7 43 | 44 | 45 | /** 46 | * Temporarily blocks message sends from completing by forcing the status to 47 | * return as PN_STATUS_PENDING. 48 | */ 49 | exports.blockSendCompletion = function() { 50 | if (DEBUG) log('blocking send completion'); 51 | sendStatus = 1; // PN_STATUS_PENDING = 1 52 | }; 53 | 54 | 55 | /** 56 | * Removes a block on message sends by forcing the status to PN_STATUS_SETTLED. 57 | */ 58 | exports.unblockSendCompletion = function() { 59 | if (DEBUG) log('unblocking send completion'); 60 | sendStatus = 7; 61 | }; 62 | 63 | var remoteIdleTimeout = -1; 64 | var workCallback; 65 | 66 | 67 | /** 68 | * Sets a remoteIdleTimeout value to return. 69 | * 70 | * @param {Number} 71 | * interval the value to override the remote idle timeout 72 | * property with. 73 | * @param {function} callback 74 | */ 75 | exports.setRemoteIdleTimeout = function(interval, callback) { 76 | if (DEBUG) log('setRemoteIdleTimeout to', interval); 77 | remoteIdleTimeout = interval; 78 | workCallback = callback; 79 | }; 80 | 81 | exports.receiver = { 82 | remote: { 83 | handle: 0 84 | }, 85 | on: function() {}, 86 | detach: function() { 87 | return new Promise(function(resolve, reject) { 88 | if (connectStatus === 0) { 89 | resolve(true); 90 | } else { 91 | var err = new Error('error on unsubscribe: ' + connectStatus); 92 | err.name = 'NetworkError'; 93 | reject(err); 94 | } 95 | }); 96 | }, 97 | _sendDetach2: function() {} 98 | }; 99 | exports.sender = { 100 | send: function() { 101 | return new Promise(function(resolve) { 102 | resolve(true); 103 | }); 104 | } 105 | }; 106 | 107 | /** 108 | * A no-function stub for the native Proton code. 109 | * 110 | * @return {object} a stub for the proton module. 111 | */ 112 | module.exports.createProtonStub = function() { 113 | return { 114 | messenger: { 115 | on: function(event) { 116 | if (DEBUG) log('stub event handler added for event', event); 117 | }, 118 | send: function() { 119 | if (DEBUG) log('stub send function called'); 120 | }, 121 | sending: function(address) { 122 | if (DEBUG) log('stub sending function called with address', address); 123 | return true; 124 | }, 125 | status: function(msg) { 126 | var result = sendStatus; 127 | if (result === 7 && msg.unitTestQos === 0) { 128 | result = 0; 129 | } 130 | if (DEBUG) log('stub status function called, returning:', result); 131 | return result; 132 | }, 133 | statusError: function() { 134 | if (DEBUG) log('stub statusError function called'); 135 | return ''; 136 | }, 137 | accept: function() { 138 | if (DEBUG) log('stub accept function called'); 139 | }, 140 | settle: function() { 141 | if (DEBUG) log('stub settle function called'); 142 | }, 143 | settledCount: 0, 144 | settled: function() { 145 | if (DEBUG) log('stub settled function called'); 146 | if (connectStatus !== 0) { 147 | var err = new Error('error on settle: ' + connectStatus); 148 | err.name = 'NetworkError'; 149 | throw err; 150 | } else { 151 | if (++this.settledCount >= 2) { 152 | return true; 153 | } else { 154 | return false; 155 | } 156 | } 157 | }, 158 | connect: function(service, options) { 159 | var self = this; 160 | return new Promise(function(resolve, reject) { 161 | if (DEBUG) log('stub connect function called for service:', service); 162 | if (!self.stopped) throw new Error('already connected'); 163 | var href = service.href; 164 | var err = null; 165 | if (href.indexOf('fail') !== -1) { 166 | if (DEBUG) log('connect received bad service'); 167 | err = new TypeError('bad service ' + href); 168 | } else if (options.host.indexOf('bad') !== -1) { 169 | if (DEBUG) log('connect received bad connection'); 170 | err = new Error('ECONNREFUSED bad service ' + options.host); 171 | err.code = 'ECONNREFUSED'; 172 | } else if (options.sslTrustCertificate === 'BadCertificate') { 173 | if (DEBUG) log('connect received bad certificate'); 174 | err = new Error('Bad Certificate wrong tag'); 175 | } else if (options.sslTrustCertificate === 'BadVerify') { 176 | if (DEBUG) log('connect received bad verify'); 177 | err = 178 | new Error('Hostname/IP doesn\'t match certificate\'s altnames'); 179 | } else if (options.sslTrustCertificate === 'SelfSignedCertificate') { 180 | if (DEBUG) log('connect received self-signed certificate'); 181 | err = 182 | new Error('DEPTH_ZERO_SELF_SIGNED_CERT'); 183 | err.code = err.message; 184 | } else if (options.sslTrustCertificate === 'ExpiredCertificate') { 185 | if (DEBUG) log('connect received expired certificate'); 186 | err = new Error('CERT_HAS_EXPIRED'); 187 | err.code = err.message; 188 | } else if (options.sslClientCertificate === 'BadCertificate') { 189 | if (DEBUG) log('connect received bad client certificate'); 190 | err = new Error('Bad Certificate'); 191 | } else if (options.sslClientKey === 'BadKey') { 192 | if (DEBUG) log('connect received bad client key'); 193 | err = new Error('Bad Key'); 194 | } else if (options.sslKeystore === 'BadKeystore') { 195 | if (DEBUG) log('connect received bad keystore'); 196 | err = new Error('Bad Keystore'); 197 | } else if (connectStatus !== 0) { 198 | if (DEBUG) log('connect received connect error'); 199 | err = new Error('connect error: ' + connectStatus); 200 | err.name = 'NetworkError'; 201 | } 202 | if (err) { 203 | reject(err); 204 | } else { 205 | self.stopped = false; 206 | if (DEBUG) log('successfully connected'); 207 | resolve(); 208 | } 209 | }); 210 | }, 211 | connected: function() { 212 | if (DEBUG) log('stub connected function called'); 213 | return !this.stopped; 214 | }, 215 | receive: function() { 216 | // Commented - as generates a lot of output... 217 | // if (DEBUG) log('stub receive function called'); 218 | return []; 219 | }, 220 | disconnect: function() { 221 | var self = this; 222 | return new Promise(function(resolve, reject) { 223 | if (DEBUG) log('stub disconnect function called'); 224 | self.stopped = true; 225 | resolve(); 226 | }); 227 | }, 228 | put: function(msg, qos) { 229 | if (DEBUG) log('stub put function called'); 230 | msg.unitTestQos = qos; 231 | }, 232 | hasSent: function() { 233 | if (DEBUG) log('stub hasSent function called'); 234 | return true; 235 | }, 236 | started: function() { 237 | return true; 238 | }, 239 | stopped: true, 240 | subscribe: function() { 241 | }, 242 | createSender: function() { 243 | return new Promise(function(resolve) { 244 | resolve(exports.sender); 245 | }); 246 | }, 247 | createReceiver: function() { 248 | return new Promise(function(resolve, reject) { 249 | if (DEBUG) log('stub subscribe function called'); 250 | if (connectStatus === 0) { 251 | resolve(exports.receiver); 252 | } else { 253 | var err = new Error('error on subscribe: ' + connectStatus); 254 | err.name = 'NetworkError'; 255 | reject(err); 256 | } 257 | }); 258 | }, 259 | }, 260 | }; 261 | }; 262 | -------------------------------------------------------------------------------- /samples/recv.js: -------------------------------------------------------------------------------- 1 | /* %Z% %W% %I% %E% %U% */ 2 | /* 3 | * 8 | * Licensed Materials - Property of IBM 9 | * 10 | * 5725-P60 11 | * 12 | * (C) Copyright IBM Corp. 2013, 2016 13 | * 14 | * US Government Users Restricted Rights - Use, duplication or 15 | * disclosure restricted by GSA ADP Schedule Contract with 16 | * IBM Corp. 17 | * 18 | */ 19 | /* jslint node: true */ 20 | /* jshint -W083,-W097 */ 21 | 'use strict'; 22 | 23 | var mqlight = require('mqlight'); 24 | var nopt = require('nopt'); 25 | var uuid = require('uuid'); 26 | var fs = require('fs'); 27 | 28 | // parse the commandline arguments 29 | var types = { 30 | help: Boolean, 31 | service: String, 32 | 'keystore': String, 33 | 'keystore-passphrase': String, 34 | 'client-certificate': String, 35 | 'client-key': String, 36 | 'client-key-passphrase': String, 37 | 'trust-certificate': String, 38 | 'verify-name': Boolean, 39 | 'topic-pattern': String, 40 | id: String, 41 | 'destination-ttl': Number, 42 | 'share-name': String, 43 | file: String, 44 | delay: Number, 45 | 'verbose': Boolean 46 | }; 47 | var shorthands = { 48 | h: ['--help'], 49 | s: ['--service'], 50 | k: ['--keystore'], 51 | p: ['--keystore-passphrase'], 52 | c: ['--trust-certificate'], 53 | t: ['--topic-pattern'], 54 | i: ['--id'], 55 | n: ['--share-name'], 56 | f: ['--file'], 57 | d: ['--delay'] 58 | }; 59 | var parsed = nopt(types, shorthands, process.argv, 2); 60 | var remain = parsed.argv.remain; 61 | 62 | var showUsage = function() { 63 | var puts = console.log; 64 | 65 | puts('Usage: recv.js [options]'); 66 | puts(''); 67 | puts('Options:'); 68 | puts(' -h, --help show this help message and exit'); 69 | puts(' -s URL, --service=URL service to connect to, for example:\n' + 70 | ' amqp://user:password@host:5672 or\n' + 71 | ' amqps://host:5671 to use SSL/TLS\n' + 72 | ' (default: amqp://localhost)'); 73 | puts(' -k FILE, --keystore=FILE\n' + 74 | ' use key store contained in FILE (in PKCS#12' + 75 | ' format) to\n' + 76 | ' supply the client certificate, private key' + 77 | ' and trust\n' + 78 | ' certificates.\n' + 79 | ' The Connection must be secured with SSL/TLS' + 80 | ' (e.g. the\n' + 81 | " service URL must start with 'amqps://').\n" + 82 | ' Option is mutually exclusive with the' + 83 | ' client-key,\n' + 84 | ' client-certificate and trust-certifcate' + 85 | ' options'); 86 | puts(' -p PASSPHRASE, --keystore-passphrase=PASSPHRASE\n' + 87 | ' use PASSPHRASE to access the key store'); 88 | puts(' --client-certificate=FILE\n' + 89 | ' use the certificate contained in FILE (in' + 90 | ' PEM format) to\n' + 91 | ' supply the identity of the client. The' + 92 | ' connection must\n' + 93 | ' be secured with SSL/TLS'); 94 | puts(' --client-key=FILE use the private key contained in FILE (in' + 95 | ' PEM format)\n' + 96 | ' for encrypting the specified client' + 97 | ' certificate'); 98 | puts(' --client-key-passphrase=PASSPHRASE\n' + 99 | ' use PASSPHRASE to access the client private' + 100 | ' key'); 101 | puts(' -c FILE, --trust-certificate=FILE\n' + 102 | ' use the certificate contained in FILE (in' + 103 | ' PEM format) to\n' + 104 | ' validate the identity of the server. The' + 105 | ' connection must\n' + 106 | ' be secured with SSL/TLS'); 107 | puts(' --no-verify-name specify to not additionally check the' + 108 | " server's common\n" + 109 | ' name in the specified trust certificate' + 110 | ' matches the\n' + 111 | " actual server's DNS name"); 112 | puts(' -t TOPICPATTERN, --topic-pattern=TOPICPATTERN\n' + 113 | ' subscribe to receive messages matching' + 114 | ' TOPICPATTERN'); 115 | puts(' (default: public)'); 116 | puts(' -i ID, --id=ID the ID to use when connecting to MQ Light\n' + 117 | ' (default: recv_[0-9a-f]{7})'); 118 | puts(' --destination-ttl=NUM set destination time-to-live to NUM seconds'); 119 | puts(' -n NAME, --share-name NAME'); 120 | puts(' optionally, subscribe to a shared' + 121 | ' destination using\n' + 122 | ' NAME as the share name.'); 123 | puts(' -f FILE, --file=FILE write the payload of the next message' + 124 | ' received to\n' + 125 | ' FILE (overwriting previous file contents)' + 126 | ' then end.\n' + 127 | ' (default is to print messages to stdout)'); 128 | puts(' -d NUM, --delay=NUM delay for NUM seconds each time a message' + 129 | ' is received.'); 130 | puts(' --verbose print additional information about each' + 131 | ' message\n' + 132 | ' received.'); 133 | puts(''); 134 | }; 135 | 136 | if (parsed.help) { 137 | showUsage(); 138 | process.exit(0); 139 | } else if (remain.length > 0) { 140 | showUsage(); 141 | process.exit(1); 142 | } 143 | 144 | Object.getOwnPropertyNames(parsed).forEach(function(key) { 145 | if (key !== 'argv' && !types.hasOwnProperty(key)) { 146 | console.error('Error: Unsupported commandline option "%s"', key); 147 | console.error(); 148 | showUsage(); 149 | process.exit(1); 150 | } 151 | }); 152 | 153 | var service = parsed.service ? parsed.service : 'amqp://localhost'; 154 | var pattern = parsed['topic-pattern'] ? parsed['topic-pattern'] : 'public'; 155 | var id = parsed.id ? parsed.id : 'recv_' + uuid.v4().substring(0, 7); 156 | var share = parsed['share-name'] ? parsed['share-name'] : undefined; 157 | 158 | // connect client to server 159 | var opts = { 160 | service: service, 161 | id: id 162 | }; 163 | var checkService = false; 164 | if (parsed['keystore']) { 165 | /** the keystore to use for a TLS/SSL connection */ 166 | opts.sslKeystore = parsed['keystore']; 167 | checkService = true; 168 | } 169 | if (parsed['keystore-passphrase']) { 170 | /** the keystore-passphrase to use for a TLS/SSL connection */ 171 | opts.sslKeystorePassphrase = parsed['keystore-passphrase']; 172 | checkService = true; 173 | } 174 | if (parsed['client-certificate']) { 175 | /** the client-certificate to use for a TLS/SSL connection */ 176 | opts.sslClientCertificate = parsed['client-certificate']; 177 | checkService = true; 178 | } 179 | if (parsed['client-key']) { 180 | /** the client-key to use for a TLS/SSL connection */ 181 | opts.sslClientKey = parsed['client-key']; 182 | checkService = true; 183 | } 184 | if (parsed['client-key-passphrase']) { 185 | /** the client-key-passphrase to use for a TLS/SSL connection */ 186 | opts.sslClientKeyPassphrase = parsed['client-key-passphrase']; 187 | checkService = true; 188 | } 189 | if (parsed['trust-certificate']) { 190 | /** the trust-certificate to use for a TLS/SSL connection */ 191 | opts.sslTrustCertificate = parsed['trust-certificate']; 192 | checkService = true; 193 | } 194 | if (parsed['verify-name'] === false) { 195 | /** 196 | * Indicate not to additionally check the MQ Light server's 197 | * common name in the certificate matches the actual server's DNS name. 198 | */ 199 | opts.sslVerifyName = false; 200 | checkService = true; 201 | } 202 | 203 | if (checkService) { 204 | if (parsed.service) { 205 | if (service.indexOf('amqps', 0) !== 0) { 206 | console.error('*** error ***'); 207 | console.error("The service URL must start with 'amqps://' when using " + 208 | 'SSL/TLS options.'); 209 | console.error('Exiting.'); 210 | process.exit(1); 211 | } 212 | } else { 213 | /** if none specified, change the default service to be amqps:// */ 214 | opts.service = 'amqps://localhost'; 215 | } 216 | } 217 | var client = mqlight.createClient(opts); 218 | 219 | // create an event listener to handle any messages that arrive for us 220 | // and an event listener to handle any malformed messages 221 | var i = 0; 222 | var delayMs = 0; 223 | client.on('message', function(data, delivery) { 224 | ++i; 225 | if (parsed.verbose) console.log('# received message (%d)', i); 226 | if (parsed.file) { 227 | console.log('Writing message data to %s', parsed.file); 228 | fs.writeFileSync(parsed.file, data); 229 | delivery.message.confirmDelivery(); 230 | client.stop(function() { 231 | console.error('Exiting.'); 232 | process.exit(0); 233 | }); 234 | } else { 235 | console.log(data); 236 | if (parsed.verbose) console.log(delivery); 237 | if (delayMs > 0) { 238 | setTimeout(delivery.message.confirmDelivery, delayMs); 239 | } else { 240 | delivery.message.confirmDelivery(); 241 | } 242 | } 243 | }); 244 | client.on('malformed', function(data, delivery) { 245 | console.error('*** received malformed message (%d)', (++i)); 246 | if (data) { 247 | console.error(data); 248 | } 249 | console.error(delivery); 250 | }); 251 | 252 | // once started, receive messages for the supplied pattern 253 | client.on('started', function() { 254 | console.log('Connected to %s using client-id %s', client.service, client.id); 255 | var options = { qos: mqlight.QOS_AT_LEAST_ONCE, autoConfirm: false }; 256 | if (parsed['destination-ttl'] !== undefined) { 257 | options.ttl = Number(parsed['destination-ttl']) * 1000; 258 | } 259 | if (parsed.delay !== undefined) { 260 | delayMs = Number(parsed.delay) * 1000; 261 | if (delayMs > 0) options.credit = 1; 262 | } 263 | 264 | // now subscribe to pattern for messages 265 | client.subscribe(pattern, share, options, function(err, pattern) { 266 | if (err) { 267 | console.error('Problem with subscribe request: %s', err.toString()); 268 | setImmediate(function() { 269 | process.exit(1); 270 | }); 271 | } else { 272 | if (pattern) { 273 | if (share) { 274 | console.log('Subscribed to share: %s, pattern: %s', share, pattern); 275 | } else { 276 | console.log('Subscribed to pattern: %s', pattern); 277 | } 278 | } 279 | } 280 | }); 281 | }); 282 | 283 | client.on('error', function(error) { 284 | console.error('*** error ***'); 285 | if (error) { 286 | if (error.message) console.error('message: %s', error.toString()); 287 | else if (error.stack) console.error(error.stack); 288 | } 289 | console.error('Exiting.'); 290 | process.exit(1); 291 | }); 292 | -------------------------------------------------------------------------------- /bin/mqlight-debug.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* %Z% %W% %I% %E% %U% */ 4 | /* 5 | * 10 | * Licensed Materials - Property of IBM 11 | * 12 | * 5725-P60 13 | * 14 | * (C) Copyright IBM Corp. 2014 15 | * 16 | * US Government Users Restricted Rights - Use, duplication or 17 | * disclosure restricted by GSA ADP Schedule Contract with 18 | * IBM Corp. 19 | * 20 | */ 21 | 22 | var logger = require('../mqlight-log'); 23 | var nopt = require('nopt'); 24 | var debug = require('_debugger'); 25 | var os = require('os'); 26 | 27 | var port = process.debugPort; 28 | var host = 'localhost'; 29 | var command; 30 | var evaluated = false; 31 | 32 | /* 33 | * Show the usage statement and exit. 34 | */ 35 | var showUsage = function(rc) { 36 | console.log('Usage: mqlight-debug.js [options] command'); 37 | console.log(''); 38 | console.log('Options:'); 39 | console.log(' -h, --help show this help message and exit'); 40 | console.log(' -d PORT, --port PORT the port running the debugger'); 41 | console.log(' -n HOST, --host HOST the host running the debugger'); 42 | console.log(' -p PID, --pid PID the process identifier to debug'); 43 | console.log(''); 44 | console.log('Command:'); 45 | console.log(' -e CMD, --eval=CMD evaluate command CMD'); 46 | console.log(' -f, --ffdc cause an FFDC to be generated'); 47 | console.log(' -l LVL, --level=LVL set the logging level to LVL'); 48 | console.log(' -s STREAM --stream=STREAM set the logger stream to STREAM') 49 | console.log(''); 50 | process.exit(rc); 51 | }; 52 | 53 | /* 54 | * The list of known command line options. 55 | */ 56 | var knownOpts = { 57 | eval: String, 58 | ffdc: Boolean, 59 | help: Boolean, 60 | host: String, 61 | level: String, 62 | pid: Number, 63 | port: Number, 64 | stream: String 65 | }; 66 | 67 | /* 68 | * The list of command line option short hands. 69 | */ 70 | var shortHands = { 71 | d: ['--port'], 72 | e: ['--eval'], 73 | f: ['--ffdc'], 74 | h: ['--help'], 75 | l: ['--level'], 76 | n: ['--host'], 77 | p: ['--pid'], 78 | s: ['--stream'] 79 | }; 80 | 81 | /* 82 | * Parse the supplied command line arguments. 83 | */ 84 | var parsed = nopt(knownOpts, shortHands); 85 | logger.log('debug', logger.NO_CLIENT_ID, 'parsed:', parsed); 86 | 87 | /* 88 | * Display the usage statement if it was asked for. 89 | */ 90 | if (parsed.help) { 91 | showUsage(0); 92 | } 93 | 94 | /* 95 | * Check that a debug command was specified. 96 | */ 97 | if (parsed.eval) { 98 | command = parsed.eval; 99 | } else if (parsed.ffdc) { 100 | command = 'logger.ffdc()'; 101 | } else if (parsed.level) { 102 | command = 'logger.setLevel(\'' + parsed.level + '\')'; 103 | } else if (parsed.stream) { 104 | command = 'logger.setStream(\'' + parsed.stream + '\')'; 105 | } else { 106 | logger.log('error', logger.NO_CLIENT_ID, 'No command specified'); 107 | showUsage(1); 108 | } 109 | 110 | /* 111 | * Save the host and port, if they were specified. 112 | */ 113 | if (parsed.host) { 114 | host = parsed.host; 115 | } 116 | if (parsed.port) { 117 | port = parsed.port; 118 | } 119 | 120 | /* 121 | * Log the options. 122 | */ 123 | logger.log('debug', logger.NO_CLIENT_ID, 'host:', host); 124 | logger.log('debug', logger.NO_CLIENT_ID, 'port:', port); 125 | 126 | /* 127 | * If a process identifier was specified, then signal to that process that it 128 | * should start the debugger. We don't have any control from here what port it 129 | * will start the debugger on, so the --port option will need to match what it 130 | * starts on (or the MQLIGHT_NODE_DEBUG_PORT environment variable can be set 131 | * to pass it to us). 132 | */ 133 | if (parsed.pid) { 134 | try { 135 | if(os.platform() !== 'win32') 136 | { 137 | /* On Unix use a SIGUSR1 signal to kick start the debugger. */ 138 | logger.entry('process.kill', logger.NO_CLIENT_ID); 139 | process.kill(parsed.pid, 'SIGUSR1'); 140 | logger.exit('process.kill', logger.NO_CLIENT_ID, null); 141 | } 142 | 143 | logger.entry('process._debugProcess', logger.NO_CLIENT_ID); 144 | logger.log('parms', logger.NO_CLIENT_ID, 'parsed.pid:', parsed.pid); 145 | process._debugProcess(parsed.pid); 146 | logger.exit('process._debugProcess', logger.NO_CLIENT_ID, null); 147 | } catch (err) { 148 | logger.log('error', logger.NO_CLIENT_ID, err); 149 | console.error('Error: ' + parsed.pid + 150 | ' is not a valid process identifier (' + err.message + ')'); 151 | process.exit(1); 152 | } 153 | } 154 | 155 | /* 156 | * Create a debugger client object. 157 | */ 158 | logger.entry('debug.Client', logger.NO_CLIENT_ID); 159 | client = new debug.Client(); 160 | logger.exit('debug.Client', logger.NO_CLIENT_ID, client); 161 | 162 | /* 163 | * Connect to the debugger port on the specified host. 164 | */ 165 | logger.entry('client.connect', logger.NO_CLIENT_ID); 166 | logger.log('parms', logger.NO_CLIENT_ID, 'port:', port); 167 | logger.log('parms', logger.NO_CLIENT_ID, 'host:', host); 168 | 169 | client.connect(port, host, function() { 170 | logger.entry('client.connect.callback', logger.NO_CLIENT_ID); 171 | logger.log('data', logger.NO_CLIENT_ID, 172 | 'Connected to debugger on ' + host + ':' + port); 173 | logger.exit('client.connect.callback', logger.NO_CLIENT_ID, null); 174 | }); 175 | 176 | logger.exit('client.connect', logger.NO_CLIENT_ID, null); 177 | 178 | /* 179 | * Exit if we fail to connect to the debugger. 180 | */ 181 | logger.entry('client.on.error', logger.NO_CLIENT_ID); 182 | client.on('error', function(err) { 183 | logger.entry('client.on.error.callback', logger.NO_CLIENT_ID); 184 | logger.log('error', logger.NO_CLIENT_ID, err); 185 | console.error('Failed to connect to ' + host + ':' + port + 186 | ' (' + err.message + ')'); 187 | logger.exit('client.on.error.callback', logger.NO_CLIENT_ID, null); 188 | process.exit(2); 189 | }); 190 | logger.exit('client.on.error', logger.NO_CLIENT_ID, null); 191 | 192 | /* 193 | * Wait until the debugger is ready to start evaluating commands. 194 | */ 195 | logger.entry('client.on.ready', logger.NO_CLIENT_ID); 196 | client.on('ready', function() { 197 | logger.entry('client.on.ready.callback', logger.NO_CLIENT_ID); 198 | logger.log('data', logger.NO_CLIENT_ID, 'Debugger ready for commands'); 199 | 200 | sendCommand(); // Send the command. 201 | 202 | logger.exit('client.on.ready.callback', logger.NO_CLIENT_ID, null); 203 | }); 204 | logger.exit('client.on.ready', logger.NO_CLIENT_ID, null); 205 | 206 | /* 207 | * If the debugger breaks, send the command and continue. 208 | */ 209 | logger.entry('client.on.break', logger.NO_CLIENT_ID); 210 | client.on('break', function(res) { 211 | logger.entry('client.on.break.callback', logger.NO_CLIENT_ID); 212 | logger.log('data', logger.NO_CLIENT_ID, 'Debugger break received'); 213 | logger.log('detail', logger.NO_CLIENT_ID, 'res:', res); 214 | 215 | sendCommand(); // Send the command. 216 | 217 | logger.entry('client.reqContinue', logger.NO_CLIENT_ID); 218 | client.reqContinue(function(err, res) { 219 | logger.entry('client.reqContinue.callback', logger.NO_CLIENT_ID); 220 | logger.log('data', logger.NO_CLIENT_ID, 'err:', err); 221 | logger.log('detail', logger.NO_CLIENT_ID, 'res:', res); 222 | if (err) { 223 | logger.log('error', logger.NO_CLIENT_ID, 'Debugger failed to continue'); 224 | } 225 | logger.exit('client.reqContinue.callback', logger.NO_CLIENT_ID, null); 226 | }); 227 | logger.exit('client.reqContinue', logger.NO_CLIENT_ID, null); 228 | logger.exit('client.on.break.callback', logger.NO_CLIENT_ID, null); 229 | }); 230 | logger.exit('client.on.break', logger.NO_CLIENT_ID, null); 231 | 232 | /* 233 | * If the port is closed while we're executing, we'll need to end. 234 | */ 235 | logger.entry('client.on.close', logger.NO_CLIENT_ID); 236 | client.on('close', function(had_error) { 237 | logger.entry('client.on.close.callback', logger.NO_CLIENT_ID); 238 | logger.log('parms', logger.NO_CLIENT_ID, 'had_error:', had_error); 239 | logger.log('data', logger.NO_CLIENT_ID, 'Connection to debugger closed'); 240 | logger.exit('client.on.close.callback', logger.NO_CLIENT_ID, null); 241 | process.exit(3); 242 | }); 243 | logger.exit('client.on.close', logger.NO_CLIENT_ID, null); 244 | 245 | /* 246 | * If the port is closed while we're executing, we'll need to end. 247 | */ 248 | logger.entry('client.on.end', logger.NO_CLIENT_ID); 249 | client.on('end', function() { 250 | logger.entry('client.on.end.callback', logger.NO_CLIENT_ID); 251 | logger.log('error', logger.NO_CLIENT_ID, 'Connection to debugger ended'); 252 | logger.exit('client.on.end.callback', logger.NO_CLIENT_ID, null); 253 | process.exit(4); 254 | }); 255 | logger.exit('client.on.end', logger.NO_CLIENT_ID, null); 256 | 257 | /* 258 | * Send the command the user specified to the debugger and wait for the 259 | * response. 260 | */ 261 | var sendCommand = function() { 262 | var exitCode = 0; 263 | var req = { command: 'disconnect' }; 264 | 265 | logger.entry('sendCommand', logger.NO_CLIENT_ID); 266 | 267 | if (evaluated) { 268 | // No need to send the same command twice. 269 | logger.log('data', logger.NO_CLIENT_ID, 'Command already sent'); 270 | } 271 | else { 272 | // Send the command to the debugger. 273 | logger.entry('client.reqEval', logger.NO_CLIENT_ID); 274 | logger.log('parms', logger.NO_CLIENT_ID, 'command:', command); 275 | 276 | client.reqEval(command, function(err, res) { 277 | // Wait for the response from the debugger 278 | logger.entry('client.reqEval.callback', logger.NO_CLIENT_ID); 279 | logger.log('data', logger.NO_CLIENT_ID, 'err:', err); 280 | logger.log('detail', logger.NO_CLIENT_ID, 'res:', res); 281 | if (err) { 282 | // The debugger failed to evaluate the command. 283 | logger.log('error', logger.NO_CLIENT_ID, 284 | 'Debugger failed to evaluate command'); 285 | console.error(res.message); 286 | exitCode = 20; 287 | } else { 288 | // The debugger did what we asked! 289 | console.log('Command evaluated successfully'); 290 | } 291 | 292 | // Regardless of the result, disconnect from the debugger. 293 | logger.entry('client.req', logger.NO_CLIENT_ID); 294 | logger.log('parms', logger.NO_CLIENT_ID, 'req:', req); 295 | client.req(req, function(err, res) { 296 | logger.entry('client.req.callback', logger.NO_CLIENT_ID); 297 | logger.log('data', logger.NO_CLIENT_ID, 'err:', err); 298 | logger.log('detail', logger.NO_CLIENT_ID, 'res:', res); 299 | if (err) { 300 | logger.log('error', logger.NO_CLIENT_ID, 301 | 'Debugger failed to disconnect'); 302 | } 303 | 304 | // Destroy the client, closing the socket. 305 | logger.entry('client.destroy', logger.NO_CLIENT_ID); 306 | client.destroy(); 307 | logger.exit('client.destroy', logger.NO_CLIENT_ID, null); 308 | logger.exit('client.req.callback', logger.NO_CLIENT_ID, null); 309 | 310 | // Time to end. 311 | process.exit(exitCode); 312 | }); 313 | logger.exit('client.reqEval.callback', logger.NO_CLIENT_ID, null); 314 | }); 315 | 316 | // Mark that we've now sent the command. 317 | evaluated = true; 318 | logger.log('debug', logger.NO_CLIENT_ID, 'evaluated:', evaluated); 319 | } 320 | 321 | logger.exit('sendCommand', logger.NO_CLIENT_ID, null); 322 | }; 323 | -------------------------------------------------------------------------------- /samples/send.js: -------------------------------------------------------------------------------- 1 | /* %Z% %W% %I% %E% %U% */ 2 | /* 3 | * 8 | * Licensed Materials - Property of IBM 9 | * 10 | * 5725-P60 11 | * 12 | * (C) Copyright IBM Corp. 2013, 2016 13 | * 14 | * US Government Users Restricted Rights - Use, duplication or 15 | * disclosure restricted by GSA ADP Schedule Contract with 16 | * IBM Corp. 17 | * 18 | */ 19 | /* jslint node: true */ 20 | /* jshint -W083,-W097 */ 21 | 'use strict'; 22 | 23 | var mqlight = require('mqlight'); 24 | var nopt = require('nopt'); 25 | var uuid = require('uuid'); 26 | var fs = require('fs'); 27 | 28 | // parse the commandline arguments 29 | var types = { 30 | help: Boolean, 31 | service: String, 32 | 'keystore': String, 33 | 'keystore-passphrase': String, 34 | 'client-certificate': String, 35 | 'client-key': String, 36 | 'client-key-passphrase': String, 37 | 'trust-certificate': String, 38 | 'verify-name': Boolean, 39 | topic: String, 40 | id: String, 41 | 'message-ttl': Number, 42 | delay: Number, 43 | repeat: Number, 44 | sequence: Boolean, 45 | file: String 46 | }; 47 | var shorthands = { 48 | h: ['--help'], 49 | s: ['--service'], 50 | k: ['--keystore'], 51 | p: ['--keystore-passphrase'], 52 | c: ['--trust-certificate'], 53 | t: ['--topic'], 54 | i: ['--id'], 55 | d: ['--delay'], 56 | r: ['--repeat'], 57 | f: ['--file'] 58 | }; 59 | var parsed = nopt(types, shorthands, process.argv, 2); 60 | 61 | var showUsage = function() { 62 | var puts = console.log; 63 | puts('Usage: send.js [options] ... '); 64 | puts(''); 65 | puts('Options:'); 66 | puts(' -h, --help show this help message and exit'); 67 | puts(' -s URL, --service=URL service to connect to, for example:\n' + 68 | ' amqp://user:password@host:5672 or\n' + 69 | ' amqps://host:5671 to use SSL/TLS\n' + 70 | ' (default: amqp://localhost)'); 71 | puts(' -k FILE, --keystore=FILE\n' + 72 | ' use key store contained in FILE (in PKCS#12' + 73 | ' format) to\n' + 74 | ' supply the client certificate, private key' + 75 | ' and trust\n' + 76 | ' certificates.\n' + 77 | ' The Connection must be secured with SSL/TLS' + 78 | ' (e.g. the\n' + 79 | " service URL must start with 'amqps://').\n" + 80 | ' Option is mutually exclusive with the' + 81 | ' client-key,\n' + 82 | ' client-certificate and trust-certifcate' + 83 | ' options'); 84 | puts(' -p PASSPHRASE, --keystore-passphrase=PASSPHRASE\n' + 85 | ' use PASSPHRASE to access the key store'); 86 | puts(' --client-certificate=FILE\n' + 87 | ' use the certificate contained in FILE (in' + 88 | ' PEM format) to\n' + 89 | ' supply the identity of the client. The' + 90 | ' connection must\n' + 91 | ' be secured with SSL/TLS'); 92 | puts(' --client-key=FILE use the private key contained in FILE (in' + 93 | ' PEM format)\n' + 94 | ' for encrypting the specified client' + 95 | ' certificate'); 96 | puts(' --client-key-passphrase=PASSPHRASE\n' + 97 | ' use PASSPHRASE to access the client private' + 98 | ' key'); 99 | puts(' -c FILE, --trust-certificate=FILE\n' + 100 | ' use the certificate contained in FILE (in' + 101 | ' PEM format) to\n' + 102 | ' validate the identity of the server. The' + 103 | ' connection must\n' + 104 | ' be secured with SSL/TLS'); 105 | puts(' --no-verify-name specify to not additionally check the' + 106 | " server's common\n" + 107 | ' name in the specified trust certificate' + 108 | ' matches the\n' + 109 | " actual server's DNS name"); 110 | puts(' -t TOPIC, --topic=TOPIC'); 111 | puts(' send messages to topic TOPIC\n' + 112 | ' (default: public)'); 113 | puts(' -i ID, --id=ID the ID to use when connecting to MQ Light\n' + 114 | ' (default: send_[0-9a-f]{7})'); 115 | puts(' --message-ttl=NUM set message time-to-live to NUM seconds'); 116 | puts(' -d NUM, --delay=NUM add NUM seconds delay between each request'); 117 | puts(' -r NUM, --repeat=NUM send messages NUM times, default is 1, if\n' + 118 | ' NUM <= 0 then repeat forever'); 119 | puts(' --sequence prefix a sequence number to the message\n' + 120 | ' payload (ignored for binary messages)'); 121 | puts(' -f FILE, --file=FILE send FILE as binary data. Cannot be\n' + 122 | ' specified at the same time as '); 123 | puts(''); 124 | }; 125 | 126 | if (parsed.help) { 127 | showUsage(); 128 | process.exit(0); 129 | } 130 | 131 | Object.getOwnPropertyNames(parsed).forEach(function(key) { 132 | if (key !== 'argv' && !types.hasOwnProperty(key)) { 133 | console.error('Error: Unsupported commandline option "%s"', key); 134 | console.error(); 135 | showUsage(); 136 | process.exit(1); 137 | } 138 | }); 139 | 140 | var service = parsed.service ? parsed.service : 'amqp://localhost'; 141 | var topic = parsed.topic ? parsed.topic : 'public'; 142 | var id = parsed.id ? parsed.id : 'send_' + uuid.v4().substring(0, 7); 143 | var repeat = parsed.repeat !== undefined ? Number(parsed.repeat) : 1; 144 | 145 | // create client to connect to server with: 146 | var opts = { 147 | service: service, 148 | id: id 149 | }; 150 | var checkService = false; 151 | if (parsed['keystore']) { 152 | /** the keystore to use for a TLS/SSL connection */ 153 | opts.sslKeystore = parsed['keystore']; 154 | checkService = true; 155 | } 156 | if (parsed['keystore-passphrase']) { 157 | /** the keystore-passphrase to use for a TLS/SSL connection */ 158 | opts.sslKeystorePassphrase = parsed['keystore-passphrase']; 159 | checkService = true; 160 | } 161 | if (parsed['client-certificate']) { 162 | /** the client-certificate to use for a TLS/SSL connection */ 163 | opts.sslClientCertificate = parsed['client-certificate']; 164 | checkService = true; 165 | } 166 | if (parsed['client-key']) { 167 | /** the client-key to use for a TLS/SSL connection */ 168 | opts.sslClientKey = parsed['client-key']; 169 | checkService = true; 170 | } 171 | if (parsed['client-key-passphrase']) { 172 | /** the client-key-passphrase to use for a TLS/SSL connection */ 173 | opts.sslClientKeyPassphrase = parsed['client-key-passphrase']; 174 | checkService = true; 175 | } 176 | if (parsed['trust-certificate']) { 177 | /** the trust-certificate to use for a TLS/SSL connection */ 178 | opts.sslTrustCertificate = parsed['trust-certificate']; 179 | checkService = true; 180 | } 181 | if (parsed['verify-name'] === false) { 182 | /** 183 | * Indicate not to additionally check the MQ Light server's 184 | * common name in the certificate matches the actual server's DNS name. 185 | */ 186 | opts.sslVerifyName = false; 187 | checkService = true; 188 | } 189 | 190 | if (checkService) { 191 | if (parsed.service) { 192 | if (service.indexOf('amqps', 0) !== 0) { 193 | console.error('*** error ***'); 194 | console.error("The service URL must start with 'amqps://' when using " + 195 | 'SSL/TLS options.'); 196 | console.error('Exiting.'); 197 | process.exit(1); 198 | } 199 | } else { 200 | /** if none specified, change the default service to be amqps:// */ 201 | opts.service = 'amqps://localhost'; 202 | } 203 | } 204 | 205 | var client = mqlight.createClient(opts); 206 | 207 | // get message body data to send 208 | var remain = parsed.argv.remain; 209 | var messages = []; 210 | 211 | if (parsed.file) { 212 | if (remain.length > 0) { 213 | console.error('*** warning: ignoring additionally supplied arguments %s', 214 | remain); 215 | console.error(); 216 | } 217 | messages.push(fs.readFileSync(parsed.file)); 218 | } else if (remain.length > 0) { 219 | messages = remain; 220 | } else { 221 | messages.push('Hello World!'); 222 | } 223 | 224 | // insert a delay between sends if requested 225 | var delay = parsed.delay * 1000 || 0; 226 | 227 | // once connection is acquired, send messages 228 | client.on('started', function() { 229 | console.log('Connected to %s using client-id %s', client.service, client.id); 230 | console.log('Sending to: %s', topic); 231 | 232 | // send the next message, inserting a delay if requested 233 | var sendNextMessage = function() { 234 | if (delay > 0) { 235 | setTimeout(sendMessage, delay); 236 | } else { 237 | setImmediate(sendMessage); 238 | } 239 | }; 240 | 241 | // queue all messages for sending 242 | var i = 0; 243 | var sequenceNum = 0; 244 | var sentMessages = 0; 245 | var sendMessage = function() { 246 | var buffered = null; 247 | var msgNum = i++; 248 | 249 | // keep going until all messages have been sent 250 | if (messages.length > msgNum) { 251 | var body = messages[msgNum]; 252 | var options = { qos: mqlight.QOS_AT_LEAST_ONCE }; 253 | var callback = function(err, topic, data, options) { 254 | if (err) { 255 | console.error('**** Problem with send request: %s', err.toString()); 256 | setImmediate(function() { 257 | client.stop(function() { 258 | process.exit(1); 259 | }); 260 | }); 261 | } else { 262 | if (data) { 263 | console.log(data); 264 | } 265 | } 266 | sentMessages++; 267 | // if no more messages to be sent, disconnect 268 | if (messages.length == i) { 269 | client.stop(function(err) { 270 | if (err) { 271 | console.error('Problem with stopping client: %s', err.toString()); 272 | process.exit(1); 273 | } else { 274 | console.log('stopping client'); 275 | process.exit(0); 276 | } 277 | }); 278 | } 279 | }; 280 | 281 | if (parsed['message-ttl'] !== undefined) { 282 | options.ttl = Number(parsed['message-ttl']) * 1000; 283 | } 284 | if (parsed.sequence && !parsed.file) { 285 | body = (++sequenceNum) + ': ' + body; 286 | } 287 | 288 | buffered = !client.send(topic, body, options, callback); 289 | } 290 | 291 | // check if the messages should be repeated again 292 | if (messages.length == i) { 293 | if (repeat != 1) i = 0; 294 | if (repeat > 1) --repeat; 295 | } 296 | 297 | // check if all our messages have been sent 298 | if (messages.length != i) { 299 | if (buffered) { 300 | // there's a backlog of messages to send, so wait until the backlog is 301 | // cleared before sending any more 302 | client.once('drain', sendNextMessage); 303 | } else { 304 | // send the next message now 305 | sendNextMessage(); 306 | } 307 | } 308 | }; 309 | 310 | setImmediate(sendMessage); 311 | }); 312 | 313 | client.on('error', function(error) { 314 | console.error('*** error ***'); 315 | if (error) { 316 | if (error.message) console.error('message: %s', error.toString()); 317 | else if (error.stack) console.error(error.stack); 318 | } 319 | console.error('Exiting.'); 320 | process.exit(1); 321 | }); 322 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /samples/uiworkout.js: -------------------------------------------------------------------------------- 1 | /* %Z% %W% %I% %E% %U% */ 2 | /* 3 | * 8 | * Licensed Materials - Property of IBM 9 | * 10 | * 5725-P60 11 | * 12 | * (C) Copyright IBM Corp. 2013, 2016 13 | * 14 | * US Government Users Restricted Rights - Use, duplication or 15 | * disclosure restricted by GSA ADP Schedule Contract with 16 | * IBM Corp. 17 | * 18 | */ 19 | /* jslint node: true */ 20 | /* jshint -W083,-W097 */ 21 | 'use strict'; 22 | 23 | var mqlight = require('mqlight'); 24 | var uuid = require('uuid'); 25 | var nopt = require('nopt'); 26 | 27 | var mqlightServiceName = 'mqlight' 28 | var messageHubServiceName = 'messagehub'; 29 | 30 | // The URL to use when connecting to the MQ Light server 31 | var serviceURL = 'amqp://localhost'; 32 | 33 | // The number of clients that will connect to any given shared destination 34 | var clientsPerSharedDestination = 2; 35 | 36 | // The topics to subscribe to for shared destinations 37 | var sharedTopics = ['shared1', 'shared/shared2']; 38 | 39 | // The topics to subscribe to for private destinations 40 | var privateTopics = [ 41 | 'private1', 42 | 'private/private2', 43 | 'private/private3', 44 | 'private4' 45 | ]; 46 | 47 | // All topics. An entry is picked at random each time a message is sent 48 | var allTopics = sharedTopics.concat(privateTopics); 49 | 50 | // A count of all messages sent by the application 51 | var messageCount = 0; 52 | 53 | // Text used to compose message bodies. A random number of words are picked. 54 | var loremIpsum = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, ' + 55 | 'sed do eiusmod tempor incididunt ut labore et dolore ' + 56 | 'magna aliqua. Ut enim ad minim veniam, quis nostrud ' + 57 | 'exercitation ullamco laboris nisi ut aliquip ex ea ' + 58 | 'commodo consequat. Duis aute irure dolor in reprehenderit ' + 59 | 'in voluptate velit esse cillum dolore eu fugiat nulla ' + 60 | 'pariatur. Excepteur sint occaecat cupidatat non proident, ' + 61 | 'sunt in culpa qui officia deserunt mollit anim id est ' + 62 | 'laborum.'; 63 | 64 | // parse the command-line arguments 65 | var types = { 66 | help: Boolean, 67 | service: String, 68 | 'keystore': String, 69 | 'keystore-passphrase': String, 70 | 'client-certificate': String, 71 | 'client-key': String, 72 | 'client-key-passphrase': String, 73 | 'trust-certificate': String, 74 | 'verify-name': Boolean 75 | }; 76 | var shorthands = { 77 | h: ['--help'], 78 | s: ['--service'], 79 | k: ['--keystore'], 80 | p: ['--keystore-passphrase'], 81 | c: ['--trust-certificate'] 82 | }; 83 | var parsed = nopt(types, shorthands, process.argv, 2); 84 | 85 | var showUsage = function() { 86 | var puts = console.log; 87 | puts('Usage: uiworkout.js [options]'); 88 | puts(''); 89 | puts('Options:'); 90 | puts(' -h, --help show this help message and exit'); 91 | puts(' -s URL, --service=URL service to connect to, for example:\n' + 92 | ' amqp://user:password@host:5672 or\n' + 93 | ' amqps://host:5671 to use SSL/TLS\n' + 94 | ' (default: amqp://localhost)'); 95 | puts(' -k FILE, --keystore=FILE\n' + 96 | ' use key store contained in FILE (in PKCS#12' + 97 | ' format) to\n' + 98 | ' supply the client certificate, private key' + 99 | ' and trust\n' + 100 | ' certificates.\n' + 101 | ' The Connection must be secured with SSL/TLS' + 102 | ' (e.g. the\n' + 103 | " service URL must start with 'amqps://').\n" + 104 | ' Option is mutually exclusive with the' + 105 | ' client-key,\n' + 106 | ' client-certificate and trust-certifcate' + 107 | ' options'); 108 | puts(' -p PASSPHRASE, --keystore-passphrase=PASSPHRASE\n' + 109 | ' use PASSPHRASE to access the key store'); 110 | puts(' --client-certificate=FILE\n' + 111 | ' use the certificate contained in FILE (in' + 112 | ' PEM format) to\n' + 113 | ' supply the identity of the client. The' + 114 | ' connection must\n' + 115 | ' be secured with SSL/TLS'); 116 | puts(' --client-key=FILE use the private key contained in FILE (in' + 117 | ' PEM format)\n' + 118 | ' for encrypting the specified client' + 119 | ' certificate'); 120 | puts(' --client-key-passphrase=PASSPHRASE\n' + 121 | ' use PASSPHRASE to access the client private' + 122 | ' key'); 123 | puts(' -c FILE, --trust-certificate=FILE\n' + 124 | ' use the certificate contained in FILE (in' + 125 | ' PEM format) to\n' + 126 | ' validate the identity of the server. The' + 127 | ' connection must\n' + 128 | ' be secured with SSL/TLS'); 129 | puts(' --no-verify-name specify to not additionally check the' + 130 | " server's common\n" + 131 | ' name in the specified trust certificate' + 132 | ' matches the\n' + 133 | " actual server's DNS name"); 134 | puts(''); 135 | }; 136 | 137 | if (parsed.help) { 138 | showUsage(); 139 | process.exit(0); 140 | } else if (parsed.argv.remain.length > 0) { 141 | showUsage(); 142 | process.exit(1); 143 | } 144 | 145 | Object.getOwnPropertyNames(parsed).forEach(function(key) { 146 | if (key !== 'argv' && !types.hasOwnProperty(key)) { 147 | console.error('Error: Unsupported commandline option "%s"', key); 148 | console.error(); 149 | showUsage(); 150 | process.exit(1); 151 | } 152 | }); 153 | 154 | // Build an array of word ending offsets for loremIpsum 155 | var loremIpsumWords = []; 156 | for (var i = 0;;) { 157 | i = loremIpsum.indexOf(' ', i); 158 | if (i == -1) { 159 | loremIpsumWords.push(loremIpsum.length); 160 | break; 161 | } else { 162 | loremIpsumWords.push(i); 163 | i += 1; 164 | } 165 | } 166 | 167 | // Create clients that subscribe to a shared topic, and send messages 168 | // randomly to any of the topics. 169 | for (var i = sharedTopics.length - 1; i >= 0; i--) { 170 | for (var j = 0; j < clientsPerSharedDestination; j++) { 171 | startClient(sharedTopics[i], ('share' + (i + 1))); 172 | } 173 | } 174 | 175 | // Create clients that subscribe to private topics, and send messages 176 | // randomly to any of the topics. 177 | for (var i = privateTopics.length - 1; i >= 0; i--) { 178 | startClient(privateTopics[i]); 179 | } 180 | 181 | // Checks to see if the application is running in IBM Bluemix. If it is, tries 182 | // to retrieve connection details from the environent and populates the 183 | // options object passed as an argument. 184 | function bluemixServiceLookup(options, verbose) { 185 | var result = false; 186 | if (process.env.VCAP_SERVICES) { 187 | if (verbose) console.log('VCAP_SERVICES variable present in environment'); 188 | var services = JSON.parse(process.env.VCAP_SERVICES); 189 | for (var key in services) { 190 | if (key.lastIndexOf(mqlightServiceName, 0) === 0) { 191 | var mqlightService = services[key][0]; 192 | options.service = mqlightService.credentials.connectionLookupURI; 193 | options.user = mqlightService.credentials.username; 194 | options.password = mqlightService.credentials.password; 195 | } else if (key.lastIndexOf(messageHubServiceName, 0) === 0) { 196 | var messageHubService = services[key][0]; 197 | options.service = messageHubService.credentials.mqlight_lookup_url; 198 | options.user = messageHubService.credentials.user; 199 | options.password = messageHubService.credentials.password; 200 | } 201 | } 202 | if (!options.hasOwnProperty('service') || 203 | !options.hasOwnProperty('user') || 204 | !options.hasOwnProperty('password')) { 205 | throw 'Error - Check that app is bound to service'; 206 | } 207 | result = true; 208 | } else if (verbose) { 209 | console.log('VCAP_SERVICES variable not present in environment'); 210 | } 211 | return result; 212 | } 213 | 214 | // Creates a client. The client will subscribe to 'topic'. If the 215 | // 'share' argument is undefined the destination will be private to the 216 | // client. If the 'share' argument is not undefined, it will be used 217 | // as the share name for subscribing to a shared destination. 218 | // The client is also used to periodically publish a message to a 219 | // randomly chosen topic. 220 | function startClient(topic, share) { 221 | var opts = {id: 'CLIENT_' + uuid.v4().substring(0, 7)}; 222 | if (parsed.service) { 223 | opts.service = parsed.service; 224 | } else if (!bluemixServiceLookup(opts, false)) { 225 | opts.service = 'amqp://localhost'; 226 | } 227 | 228 | var checkService = false; 229 | if (parsed['keystore']) { 230 | /** the keystore to use for a TLS/SSL connection */ 231 | opts.sslKeystore = parsed['keystore']; 232 | checkService = true; 233 | } 234 | if (parsed['keystore-passphrase']) { 235 | /** the keystore-passphrase to use for a TLS/SSL connection */ 236 | opts.sslKeystorePassphrase = parsed['keystore-passphrase']; 237 | checkService = true; 238 | } 239 | if (parsed['client-certificate']) { 240 | /** the client-certificate to use for a TLS/SSL connection */ 241 | opts.sslClientCertificate = parsed['client-certificate']; 242 | checkService = true; 243 | } 244 | if (parsed['client-key']) { 245 | /** the client-key to use for a TLS/SSL connection */ 246 | opts.sslClientKey = parsed['client-key']; 247 | checkService = true; 248 | } 249 | if (parsed['client-key-passphrase']) { 250 | /** the client-key-passphrase to use for a TLS/SSL connection */ 251 | opts.sslClientKeyPassphrase = parsed['client-key-passphrase']; 252 | checkService = true; 253 | } 254 | if (parsed['trust-certificate']) { 255 | /** the trust-certificate to use for a TLS/SSL connection */ 256 | opts.sslTrustCertificate = parsed['trust-certificate']; 257 | checkService = true; 258 | } 259 | if (parsed['verify-name'] === false) { 260 | /** 261 | * Indicate not to additionally check the MQ Light server's 262 | * common name in the certificate matches the actual server's DNS name. 263 | */ 264 | opts.sslVerifyName = false; 265 | checkService = true; 266 | } 267 | 268 | if (checkService) { 269 | if (parsed.service) { 270 | if (opts.service.indexOf('amqps', 0) !== 0) { 271 | console.error('*** error ***'); 272 | console.error("The service URL must start with 'amqps://' when using " + 273 | 'SSL/TLS options.'); 274 | console.error('Exiting.'); 275 | process.exit(1); 276 | } 277 | } else { 278 | /** if none specified, change the default service to be amqps:// */ 279 | opts.service = 'amqps://localhost'; 280 | } 281 | } 282 | 283 | var client = mqlight.createClient(opts, function(err) { 284 | if (err) { 285 | console.error('Problem with connect: ', err.toString()); 286 | process.exit(1); 287 | } 288 | }); 289 | 290 | client.on('started', function() { 291 | console.log('Connected to ' + client.service + ' using id ' + client.id); 292 | client.subscribe(topic, share, function(err, topicPattern, share) { 293 | if (err) { 294 | console.error('Problem with subscribe request: ', err.toString()); 295 | process.exit(1); 296 | } 297 | console.log("Receiving messages from topic pattern '" + topicPattern + 298 | (share ? "' and share '" + share + "'" : "'")); 299 | }); 300 | 301 | // Loop sending messages to randomly picked topics. Delay for a small 302 | // (random) amount of time, each time around. 303 | var sendMessages = function() { 304 | var delay = Math.random() * 20000; 305 | var sendTopic = allTopics[Math.floor(Math.random() * allTopics.length)]; 306 | var sendCallback = function(err, msg) { 307 | if (err) { 308 | console.error('Problem with send request: ' + err.toString()); 309 | process.exit(0); 310 | } else { 311 | if (messageCount === 0) { 312 | console.log('Sending messages'); 313 | } 314 | ++messageCount; 315 | if (messageCount % 10 === 0) { 316 | console.log('Sent ' + messageCount + ' messages'); 317 | } 318 | } 319 | }; 320 | 321 | setTimeout(function() { 322 | var start = Math.floor(Math.random() * (loremIpsumWords.length - 15)); 323 | var end = start + 5 + Math.floor(Math.random() * 10); 324 | var message = 325 | loremIpsum.substring(loremIpsumWords[start], loremIpsumWords[end]); 326 | client.send(sendTopic, message, sendCallback); 327 | sendMessages(); 328 | },delay); 329 | }; 330 | sendMessages(); 331 | }); 332 | 333 | client.on('error', function(error) { 334 | console.error('*** error ***'); 335 | if (error) { 336 | if (error.message) console.error('message: %s', error.toString()); 337 | else if (error.stack) console.error(error.stack); 338 | } 339 | console.error('Exiting.'); 340 | process.exit(1); 341 | }); 342 | } 343 | -------------------------------------------------------------------------------- /test/testunsubscribe.js: -------------------------------------------------------------------------------- 1 | /* %Z% %W% %I% %E% %U% */ 2 | /* 3 | * 8 | * Licensed Materials - Property of IBM 9 | * 10 | * 5725-P60 11 | * 12 | * (C) Copyright IBM Corp. 2014 13 | * 14 | * US Government Users Restricted Rights - Use, duplication or 15 | * disclosure restricted by GSA ADP Schedule Contract with 16 | * IBM Corp. 17 | * 18 | */ 19 | /* jslint node: true */ 20 | /* jshint -W083,-W097 */ 21 | 'use strict'; 22 | 23 | 24 | /** @const {string} enable unittest mode in mqlight.js */ 25 | process.env.NODE_ENV = 'unittest'; 26 | 27 | var stubproton = require('./stubs/stubproton'); 28 | var mqlight = require('../mqlight'); 29 | var testCase = require('nodeunit').testCase; 30 | 31 | 32 | /** 33 | * Test a calling client.unsubscribe(...) with too few arguments (no arguments) 34 | * causes an Error to be thrown. 35 | * @param {object} test the unittest interface 36 | */ 37 | module.exports.test_unsubscribe_too_few_arguments = function(test) { 38 | var opts = { 39 | id: 'test_unsubscribe_too_few_arguments', 40 | service: 'amqp://host' 41 | }; 42 | var client = mqlight.createClient(opts, function() { 43 | test.throws(function() { 44 | client.unsubscribe(); 45 | }, TypeError); 46 | client.stop(function() { 47 | test.done(); 48 | }); 49 | }); 50 | }; 51 | 52 | 53 | /** 54 | * Test that calling client.unsubscribe(...) with too many arguments results in 55 | * the additional arguments being ignored. 56 | * @param {object} test the unittest interface 57 | */ 58 | module.exports.test_unsubscribe_too_many_arguments = function(test) { 59 | var opts = { 60 | id: 'test_unsubscribe_too_many_arguments', 61 | service: 'amqp://host' 62 | }; 63 | var client = mqlight.createClient(opts, function() { 64 | test.doesNotThrow(function() { 65 | client.subscribe('/foo', 'share1', function() { 66 | client.unsubscribe('/foo', 'share1', {}, function() {}, 'stowaway'); 67 | client.stop(function() { 68 | test.done(); 69 | }); 70 | }); 71 | }); 72 | }); 73 | }; 74 | 75 | 76 | /** 77 | * Test that the callback argument to client.unsubscribe(...) must be a 78 | * function 79 | * 80 | * @param {object} test the unittest interface 81 | */ 82 | module.exports.test_unsubscribe_callback_must_be_function = function(test) { 83 | var opts = { 84 | id: 'test_unsubscribe_callback_must_be_function', 85 | service: 'amqp://host' 86 | }; 87 | var client = mqlight.createClient(opts, function() { 88 | test.throws(function() { 89 | client.unsubscribe('/foo1', 'share', {}, 7); 90 | }, TypeError); 91 | test.doesNotThrow(function() { 92 | client.subscribe('/foo2', function() { 93 | client.unsubscribe('/foo2', function() { 94 | client.subscribe('/foo3', 'share', function() { 95 | client.unsubscribe('/foo3', 'share', function() { 96 | client.subscribe('/foo4', 'share', function() { 97 | client.unsubscribe('/foo4', 'share', {}, function() { 98 | client.stop(function() { 99 | test.done(); 100 | }); 101 | }); 102 | }); 103 | }); 104 | }); 105 | }); 106 | }); 107 | }); 108 | }); 109 | }; 110 | 111 | 112 | // /** 113 | // * Test that unsubscribe correctly interprets its parameters. This can be 114 | // * tricky for two and three parameter invocations where there is the 115 | // * potential for ambiguity between what is a share name and what is the 116 | // * options object. 117 | // * 118 | // * @param {object} test the unittest interface 119 | // */ 120 | // module.exports.test_unsubscribe_parameters = function(test) { 121 | // var client; 122 | // var service = 'amqp://host:5672'; 123 | // var pattern = '/pattern'; 124 | // var currentCallbackInvocations = 0; 125 | // var expectedCallbackInvocations = 0; 126 | // var cb = function() { 127 | // ++currentCallbackInvocations; 128 | // }; 129 | // var share = 'share'; 130 | // var object = {}; 131 | // var testNum = 0; 132 | // 133 | // // Data to drive the test with. 'args' is the argument list to pass into 134 | // // the unsubscribe function. The 'share', 'object' and 'callback' properties 135 | // // indicate the expected interpretation of 'args'. 136 | // var data = [{args: [pattern + '0']}, 137 | // {args: [pattern + '1', cb], callback: cb}, 138 | // {args: [pattern + '2', share], share: share}, 139 | // {args: [pattern + '3', object], object: object}, 140 | // {args: [pattern + '4', share, cb], share: share, callback: cb}, 141 | // {args: [pattern + '5', object, cb], object: object, callback: cb}, 142 | // {args: [pattern + '6', share, object], 143 | // share: share, object: object}, 144 | // {args: [pattern + '7', 7], share: 7}, 145 | // {args: [pattern + '8', 'boo'], share: 'boo'}, 146 | // {args: [pattern + '9', {}], object: {}}, 147 | // {args: [pattern + '10', 7, cb], share: 7, callback: cb}, 148 | // {args: [pattern + '11', {}, cb], object: {}, callback: cb}, 149 | // {args: [pattern + '12', [], []], share: [], object: []}, 150 | // {args: [pattern + '13', share, object, cb], 151 | // share: share, object: object, callback: cb}]; 152 | // 153 | // // Count up the expected number of callback invocations, so the test can 154 | // // wait for these to complete. 155 | // for (var i = 0; i < data.length; ++i) { 156 | // if (data[i].callback) ++expectedCallbackInvocations; 157 | // } 158 | // 159 | // // Replace the messenger unsubscribe method with our own implementation 160 | // // that simply records the address that mqlight.js tries to unsubscribe from. 161 | // var lastUnsubscribedAddress; 162 | // var savedUnsubscribeFunction = stubproton.receiver.detach; 163 | // stubproton.receiver.detach = function(address) { 164 | // lastUnsubscribedAddress = address; 165 | // 166 | // var expectedAddress = 167 | // // service + '/' + 168 | // ((data[testNum].share) ? 169 | // ('share:' + data[testNum].share + ':') : 'private:') + 170 | // pattern + testNum; 171 | // 172 | // test.deepEqual(lastUnsubscribedAddress, expectedAddress); 173 | // 174 | // if (testNum < data.length - 1) { 175 | // process.nextTick(function() { 176 | // runTests(++testNum); 177 | // }); 178 | // } else { 179 | // // Restore the saved messenger unsubscribe implementation 180 | // stubproton.receiver.detach = savedUnsubscribeFunction; 181 | // client.stop(); 182 | // } 183 | // return savedUnsubscribeFunction(); 184 | // }; 185 | // 186 | // var runTests = function(i) { 187 | // var clientUnsubscribeMethod = client.unsubscribe; 188 | // var clientSubscribeMethod = client.subscribe; 189 | // lastUnsubscribedAddress = undefined; 190 | // clientSubscribeMethod.apply(client, [pattern + i, 191 | // ('share' in data[i]) ? data[i].args[1] : undefined, {}, function() { 192 | // clientUnsubscribeMethod.apply(client, data[i].args); 193 | // }] 194 | // ); 195 | // }; 196 | // 197 | // client = mqlight.createClient({ 198 | // id: 'test_unsubscribe_parameters', 199 | // service: service 200 | // }, function() { 201 | // runTests(testNum); 202 | // }); 203 | // 204 | // // Callbacks passed into unsubscribe(...) are scheduled to be run once 205 | // // outside of the main loop - so use setImmediate(...) to schedule checking 206 | // // for test completion. 207 | // var testIsDone = function() { 208 | // if (currentCallbackInvocations === expectedCallbackInvocations) { 209 | // test.done(); 210 | // } else { 211 | // setImmediate(testIsDone); 212 | // } 213 | // }; 214 | // testIsDone(); 215 | // }; 216 | 217 | 218 | /** 219 | * Test that the callback (invoked when the unsubscribe operation completes 220 | * successfully) specifies the right number of arguments, and is invoked with 221 | * 'this' set to reference the client that the corresponding invocation of 222 | * unsubscribe(...) was made against. 223 | * @param {object} test the unittest interface 224 | */ 225 | module.exports.test_unsubscribe_ok_callback = function(test) { 226 | var client = mqlight.createClient({ 227 | id: 'test_unsubscribe_ok_callback', 228 | service: 'amqp://host' 229 | }); 230 | client.on('started', function() { 231 | client.subscribe('/foo', function() { 232 | client.unsubscribe('/foo', function() { 233 | test.equals(arguments.length, 3); 234 | test.deepEqual(arguments[0], null); // error argument 235 | test.deepEqual(arguments[1], '/foo'); // topic pattern argument 236 | test.deepEqual(arguments[2], undefined); // share argument 237 | test.ok(this === client); 238 | client.subscribe('/foo2', 'share', function() { 239 | client.unsubscribe('/foo2', 'share', function() { 240 | test.equals(arguments.length, 3); 241 | test.deepEqual(arguments[0], null); // error argument 242 | test.deepEqual(arguments[1], '/foo2'); // topic pattern argument 243 | test.deepEqual(arguments[2], 'share'); // share argument 244 | test.ok(this === client); 245 | client.stop(); 246 | }); 247 | }); 248 | }); 249 | }); 250 | }); 251 | client.on('stopped', test.done); 252 | }; 253 | 254 | 255 | /** 256 | * Test that trying to remove a subscription, while the client is in 257 | * stopped state, throws an Error. 258 | * 259 | * @param {object} test the unittest interface 260 | */ 261 | module.exports.test_unsubscribe_when_stopped = function(test) { 262 | var client = mqlight.createClient({id: 'test_unsubscribe_when_stopped', 263 | service: 'amqp://host'}); 264 | client.stop(); 265 | client.on('stopped', function() { 266 | test.throws(function() { 267 | client.unsubscribe('/foo'); 268 | }, mqlight.StoppedError); 269 | test.done(); 270 | }); 271 | }; 272 | 273 | 274 | /** 275 | * Test that trying to remove a subscription that does not exist throws an 276 | * Error. 277 | * 278 | * @param {object} test the unittest interface 279 | */ 280 | module.exports.test_unsubscribe_when_not_subscribed = function(test) { 281 | var client = mqlight.createClient({ 282 | id : 'test_unsubscribe_when_not_subscribed', 283 | service : 'amqp://host'}); 284 | client.on('started', function() { 285 | client.subscribe('/bar'); 286 | test.throws(function() { client.unsubscribe('/foo'); }, 287 | mqlight.UnsubscribedError); 288 | client.stop(test.done); 289 | }); 290 | }; 291 | 292 | 293 | /** 294 | * Test that calling the unsubscribe(...) method returns, as a value, the 295 | * client object that the method was invoked on (for method chaining purposes). 296 | * 297 | * @param {object} test the unittest interface 298 | */ 299 | module.exports.test_unsubscribe_returns_client = function(test) { 300 | var client = mqlight.createClient({id: 'test_unsubscribe_returns_client', 301 | service: 'amqp://host'}, 302 | function() { 303 | client.subscribe('/foo', function() { 304 | test.deepEqual(client.unsubscribe('/foo'), client); 305 | client.stop(function(){ 306 | test.done(); 307 | }); 308 | }); 309 | }); 310 | }; 311 | 312 | 313 | /** 314 | * Test a variety of valid and invalid patterns. Invalid patterns 315 | * should result in the client.unsubscribe(...) method throwing a TypeError. 316 | * @param {object} test the unittest interface 317 | */ 318 | module.exports.test_unsubscribe_topics = function(test) { 319 | var data = [{valid: false, pattern: ''}, 320 | {valid: false, pattern: undefined}, 321 | {valid: false, pattern: null}, 322 | {valid: true, pattern: 1234}, 323 | {valid: true, pattern: function() {}}, 324 | {valid: true, pattern: 'kittens'}, 325 | {valid: true, pattern: '/kittens'}, 326 | {valid: true, pattern: '+'}, 327 | {valid: true, pattern: '#'}, 328 | {valid: true, pattern: '/#'}, 329 | {valid: true, pattern: '/+'}]; 330 | 331 | var client = mqlight.createClient({id: 'test_unsubscribe_topics', service: 332 | 'amqp://host'}); 333 | 334 | var runTests = function (i) { 335 | if (i === data.length) { 336 | client.stop(function(){ 337 | test.done(); 338 | }); 339 | return; 340 | } 341 | 342 | if (data[i].valid) { 343 | test.doesNotThrow(function() { 344 | client.subscribe(data[i].pattern, function() { 345 | client.unsubscribe(data[i].pattern, function() { 346 | runTests(i + 1); 347 | }); 348 | }); 349 | }); 350 | } else { 351 | test.throws(function() { 352 | client.unsubscribe(data[i].pattern); 353 | }, TypeError, 'pattern should have been rejected: ' + data[i].pattern); 354 | runTests(i + 1); 355 | } 356 | }; 357 | 358 | client.once('started', function() { 359 | runTests(0); 360 | }); 361 | }; 362 | 363 | 364 | /** 365 | * Tests a variety of valid and invalid share names to check that they are 366 | * accepted or rejected (by throwing an Error) as appropriate. 367 | * @param {object} test the unittest interface 368 | */ 369 | module.exports.test_unsubscribe_share_names = function(test) { 370 | var data = [{valid: true, share: 'abc'}, 371 | {valid: true, share: 7}, 372 | {valid: false, share: ':'}, 373 | {valid: false, share: 'a:'}, 374 | {valid: false, share: ':a'}]; 375 | 376 | var client = mqlight.createClient({ 377 | id: 'test_unsubscribe_share_names', 378 | service: 'amqp://host' 379 | }); 380 | 381 | var runTests = function(i) { 382 | if (i === data.length) { 383 | client.stop(function() { 384 | test.done(); 385 | }); 386 | return; 387 | } 388 | 389 | if (data[i].valid) { 390 | test.doesNotThrow(function() { 391 | client.subscribe('/foo', data[i].share, function() { 392 | client.unsubscribe('/foo', data[i].share, function() { 393 | runTests(i + 1); 394 | }); 395 | }); 396 | }); 397 | } else { 398 | test.throws(function() { 399 | client.unsubscribe('/foo', data[i].share); 400 | }, mqlight.InvalidArgumentError, i); 401 | runTests(i + 1); 402 | } 403 | }; 404 | 405 | client.on('started', function() { 406 | runTests(0); 407 | }); 408 | }; 409 | 410 | 411 | /** 412 | * Test a variety of valid and invalid options values. Invalid options 413 | * should result in the client.unsubscribe(...) method throwing a TypeError. 414 | *

415 | * Note that this test just checks that the options parameter is only 416 | * accepted when it is of the correct type. The actual validation of 417 | * individual options will be in separate tests. 418 | * @param {object} test the unittest interface 419 | */ 420 | module.exports.test_unsubscribe_options = function(test) { 421 | var data = [{valid: false, options: ''}, 422 | {valid: true, options: undefined}, 423 | {valid: true, options: null}, 424 | {valid: false, options: function() {}}, 425 | {valid: false, options: '1'}, 426 | {valid: false, options: 2}, 427 | {valid: false, options: true}, 428 | {valid: true, options: {}}, 429 | {valid: true, options: data}, 430 | {valid: true, options: { a: 1 } }]; 431 | 432 | var client = mqlight.createClient({id: 'test_unsubscribe_options', service: 433 | 'amqp://host'}); 434 | 435 | var runTests = function(i) { 436 | if (i === data.length) { 437 | client.stop(function() { 438 | test.done(); 439 | }); 440 | return; 441 | } 442 | 443 | if (data[i].valid) { 444 | test.doesNotThrow( 445 | function() { 446 | client.subscribe('testpattern', 'share', function() { 447 | client.unsubscribe('testpattern', 'share', data[i].options, 448 | function() { 449 | runTests(i + 1); 450 | }); 451 | }); 452 | } 453 | ); 454 | } else { 455 | test.throws( 456 | function() { 457 | client.unsubscribe('testpattern', 'share', data[i].options, 458 | function() {}); 459 | }, 460 | TypeError, 461 | 'options should have been rejected: ' + data[i].options 462 | ); 463 | runTests(i + 1); 464 | } 465 | }; 466 | 467 | client.on('started', function() { 468 | runTests(0); 469 | }); 470 | }; 471 | 472 | 473 | /** 474 | * Test a variety of valid and invalid ttl options. Invalid ttl values should 475 | * result in the client.unsubscribe(...) method throwing a TypeError. 476 | * 477 | * @param {object} test the unittest interface 478 | */ 479 | module.exports.test_unsubscribe_ttl_validity = function(test) { 480 | var data = [ 481 | {valid: false, ttl: undefined}, 482 | {valid: false, ttl: function() {}}, 483 | {valid: false, ttl: -9007199254740992}, 484 | {valid: false, ttl: -NaN}, 485 | {valid: false, ttl: NaN}, 486 | {valid: false, ttl: -Infinity}, 487 | {valid: false, ttl: Infinity}, 488 | {valid: false, ttl: -1}, 489 | {valid: false, ttl: 1}, 490 | {valid: false, ttl: 9007199254740992}, 491 | {valid: true, ttl: 0}, 492 | {valid: true, ttl: null}, // treated as 0 493 | {valid: true, ttl: ''} // treated as 0 494 | ]; 495 | 496 | var client = mqlight.createClient({id: 'test_unsubscribe_ttl_validity', 497 | service: 'amqp://host'}); 498 | 499 | var runTests = function(i) { 500 | if (i === data.length) { 501 | client.stop(function() { 502 | test.done(); 503 | }); 504 | return; 505 | } 506 | 507 | var opts = { ttl: data[i].ttl }; 508 | if (data[i].valid) { 509 | test.doesNotThrow(function() { 510 | client.subscribe('testpattern', function() { 511 | client.unsubscribe('testpattern', opts, function() { 512 | runTests(i + 1); 513 | }); 514 | }); 515 | }); 516 | } else { 517 | test.throws(function() { 518 | client.unsubscribe('testpattern', opts); 519 | }, RangeError, 'ttl should have been rejected: ' + data[i].ttl); 520 | runTests(i + 1); 521 | } 522 | }; 523 | 524 | client.on('started', function() { 525 | runTests(0); 526 | }); 527 | }; 528 | -------------------------------------------------------------------------------- /test/testsend.js: -------------------------------------------------------------------------------- 1 | /* %Z% %W% %I% %E% %U% */ 2 | /* 3 | * 8 | * Licensed Materials - Property of IBM 9 | * 10 | * 5725-P60 11 | * 12 | * (C) Copyright IBM Corp. 2014 13 | * 14 | * US Government Users Restricted Rights - Use, duplication or 15 | * disclosure restricted by GSA ADP Schedule Contract with 16 | * IBM Corp. 17 | * 18 | */ 19 | /* jslint node: true */ 20 | /* jshint -W083,-W097 */ 21 | 'use strict'; 22 | 23 | 24 | /** @const {string} enable unittest mode in mqlight.js */ 25 | process.env.NODE_ENV = 'unittest'; 26 | 27 | var stubproton = require('./stubs/stubproton'); 28 | var testCase = require('nodeunit').testCase; 29 | var mqlight = require('../mqlight'); 30 | var AMQP = require('mqlight-forked-amqp10'); 31 | var Promise = require('bluebird'); 32 | 33 | 34 | /** 35 | * Test that supplying too few arguments to client.send(...) results in an 36 | * error being thrown. 37 | * @param {object} test the unittest interface 38 | */ 39 | module.exports.test_send_too_few_arguments = function(test) { 40 | var client = mqlight.createClient({ 41 | id: 'test_send_too_few_arguments', 42 | service: 'amqp://host' 43 | }); 44 | client.on('started', function() { 45 | test.throws(function() { 46 | client.send(); 47 | }, TypeError); 48 | test.throws(function() { 49 | client.send('topic'); 50 | }, TypeError); 51 | client.stop(function() { 52 | test.done(); 53 | }); 54 | }); 55 | }; 56 | 57 | 58 | /** 59 | * Test that if too many arguments are supplied to client.send(...) then the 60 | * additional arguments are ignored. 61 | * @param {object} test the unittest interface 62 | */ 63 | module.exports.test_send_too_many_arguments = function(test) { 64 | var client = mqlight.createClient({id: 'test_send_too_many_arguments', 65 | service: 'amqp://host'}); 66 | client.on('started', function() { 67 | test.doesNotThrow( 68 | function() { 69 | client.send('topic', 'message', {}, function() {}, 'interloper'); 70 | } 71 | ); 72 | client.stop(function() { 73 | test.done(); 74 | }); 75 | }); 76 | }; 77 | 78 | 79 | /** 80 | * Test that if a bad callback is specified then an exception is 81 | * thrown. 82 | * 83 | * @param {object} test the unittest interface 84 | */ 85 | module.exports.test_send_callback_must_be_a_function = function(test) { 86 | var client = mqlight.createClient({ 87 | id: 'test_send_callback_must_be_a_function', 88 | service: 'amqp://host'}); 89 | client.on('started', function() { 90 | test.throws( 91 | function() { 92 | client.send('topic', 'message', {}, 123); 93 | } 94 | ); 95 | client.stop(function() { 96 | test.done(); 97 | }); 98 | }); 99 | }; 100 | 101 | 102 | /** 103 | * Test a variety of valid and invalid topic names. Invalid topic names 104 | * should result in the client.send(...) method throwing a TypeError. 105 | * @param {object} test the unittest interface 106 | */ 107 | module.exports.test_send_topics = function(test) { 108 | var data = [{valid: false, topic: ''}, 109 | {valid: false, topic: undefined}, 110 | {valid: false, topic: null}, 111 | {valid: true, topic: 1234}, 112 | {valid: true, topic: function() {}}, 113 | {valid: true, topic: 'kittens'}, 114 | {valid: true, topic: '/kittens'}]; 115 | 116 | var client = mqlight.createClient({id: 'test_send_topics', service: 117 | 'amqp://host'}); 118 | client.on('started', function() { 119 | for (var i = 0; i < data.length; ++i) { 120 | if (data[i].valid) { 121 | test.doesNotThrow( 122 | function() { 123 | client.send(data[i].topic, 'message'); 124 | } 125 | ); 126 | } else { 127 | test.throws( 128 | function() { 129 | client.send(data[i].topic, 'message'); 130 | }, 131 | TypeError, 132 | 'topic should have been rejected: ' + data[i].topic 133 | ); 134 | } 135 | } 136 | client.stop(); 137 | test.done(); 138 | }); 139 | }; 140 | 141 | 142 | /** 143 | * Tests sending a variety of different message body types. Each type should 144 | * result in one of the following outcomes: 145 | *

    146 | *
  • error - the client.send(...) call throws an error.
  • 147 | *
  • string - the data is passed to proton as a string.
  • 148 | *
  • buffer - the data is passed to proton as a buffer.
  • 149 | *
  • json - the data is passed to proton as a string containing JSON.
  • 150 | * 151 | * @param {object} test the unittest interface 152 | */ 153 | module.exports.test_send_payloads = function(test) { 154 | var data = [{result: 'error', message: undefined}, 155 | {result: 'error', message: function() {}}, 156 | {result: 'string', message: 'a string'}, 157 | {result: 'string', message: ''}, 158 | {result: 'buffer', message: new Buffer('abc')}, 159 | {result: 'buffer', message: new Buffer(0)}, 160 | {result: 'json', message: null}, 161 | {result: 'json', message: {}}, 162 | {result: 'json', message: {color: 'red'}}, 163 | {result: 'json', message: {func: function() {}}}, 164 | {result: 'json', message: []}, 165 | {result: 'json', message: [1, 'red']}, 166 | {result: 'json', message: [true, function() {}]}, 167 | {result: 'json', message: 123}, 168 | {result: 'json', message: 3.14159}, 169 | {result: 'json', message: true}]; 170 | 171 | var client = mqlight.createClient({id: 'test_send_payloads', service: 172 | 'amqp://host'}); 173 | client.on('started', function() { 174 | for (var i = 0; i < data.length; ++i) { 175 | if (data[i].result === 'error') { 176 | test.throws( 177 | function() {client.send('topic', data[i].message);}, 178 | TypeError, 179 | 'expected send(...) to reject a payload of ' + data[i].message); 180 | } else { 181 | test.doesNotThrow( 182 | function() { 183 | client.send('topic', data[i].message); 184 | } 185 | ); 186 | var lastMsg = client._outstandingSends.shift().msg; 187 | switch (data[i].result) { 188 | case ('string'): 189 | test.ok(typeof lastMsg.body === 'string'); 190 | test.deepEqual(lastMsg.body, data[i].message); 191 | test.equals(lastMsg.properties.contentType, 'text/plain'); 192 | break; 193 | case ('buffer'): 194 | test.ok(lastMsg.body instanceof AMQP.DescribedType); 195 | test.deepEqual(lastMsg.body.value, data[i].message); 196 | break; 197 | case ('json'): 198 | test.ok(typeof lastMsg.body === 'string'); 199 | test.deepEqual(lastMsg.body, JSON.stringify(data[i].message)); 200 | test.equals(lastMsg.properties.contentType, 'application/json'); 201 | break; 202 | default: 203 | test.ok(false, "unexpected result type: '" + data[i].result + "'"); 204 | } 205 | } 206 | } 207 | 208 | client.stop(function() { 209 | test.done(); 210 | }); 211 | }); 212 | }; 213 | 214 | 215 | /** 216 | * Tests that, if a callback function is supplied to client.test(...) then the 217 | * function is invoked when the send operation completes, and this references 218 | * the client. 219 | * @param {object} test the unittest interface 220 | */ 221 | module.exports.test_send_callback = function(test) { 222 | var timeout = setTimeout(function() { 223 | test.ok(false, 'test timed out before all callbacks were triggered.'); 224 | test.done(); 225 | }, 5000); 226 | var testData = [{topic: 'topic1', data: 'data1', options: {}}, 227 | {topic: 'topic2', data: 'data2', options: undefined}]; 228 | test.expect(testData.length * 7); 229 | var client = mqlight.createClient({id: 'test_send_callback', service: 230 | 'amqp://host'}); 231 | var count = 0; 232 | var callback = function() { 233 | test.equals(arguments.length, 4); 234 | test.equals(arguments[0], undefined); 235 | test.equals(arguments[1], testData[count].topic); 236 | test.equals(arguments[2], testData[count].data); 237 | test.equals(arguments[3], testData[count].options); 238 | test.ok(this === client); 239 | ++count; 240 | if (count === testData.length) { 241 | client.stop(); 242 | clearTimeout(timeout); 243 | test.done(); 244 | } 245 | }; 246 | client.on('started', function() { 247 | for (var i = 0; i < testData.length; ++i) { 248 | test.doesNotThrow(function() { 249 | client.send(testData[i].topic, testData[i].data, testData[i].options, 250 | callback); 251 | }); 252 | } 253 | }); 254 | }; 255 | 256 | 257 | /** 258 | * Tests that client.send(...) throws and error if it is called while the 259 | * client is in stopped state. 260 | * @param {object} test the unittest interface 261 | */ 262 | module.exports.test_send_fails_if_stopped = function(test) { 263 | var opts = { 264 | id: 'test_send_fails_if_stopped', 265 | service: 'amqp://host' 266 | }; 267 | var client = mqlight.createClient(opts, function() { 268 | client.stop(function() { 269 | test.throws( 270 | function() { 271 | client.send('topic', 'message'); 272 | }, 273 | mqlight.StoppedError 274 | ); 275 | test.done(); 276 | }); 277 | }); 278 | }; 279 | 280 | 281 | /** 282 | * Test a variety of valid and invalid options values. Invalid options 283 | * should result in the client.send(...) method throwing a TypeError. 284 | *

    285 | * Note that this test just checks that the options parameter is only 286 | * accepted when it is of the correct type. The actual validation of 287 | * individual options will be in separate tests. 288 | * @param {object} test the unittest interface 289 | */ 290 | module.exports.test_send_options = function(test) { 291 | var data = [{valid: false, options: ''}, 292 | {valid: true, options: undefined}, 293 | {valid: true, options: null}, 294 | {valid: false, options: function() {}}, 295 | {valid: false, options: '1'}, 296 | {valid: false, options: 2}, 297 | {valid: false, options: true}, 298 | {valid: true, options: {}}, 299 | {valid: true, options: data}, 300 | {valid: true, options: { a: 1 } }]; 301 | 302 | var client = mqlight.createClient({id: 'test_send_options', service: 303 | 'amqp://host'}); 304 | client.on('started', function() { 305 | for (var i = 0; i < data.length; ++i) { 306 | if (data[i].valid) { 307 | test.doesNotThrow( 308 | function() { 309 | client.send('test', 'message', data[i].options, function() {}); 310 | } 311 | ); 312 | } else { 313 | test.throws( 314 | function() { 315 | client.send('test', 'message', data[i].options, function() {}); 316 | }, 317 | TypeError, 318 | 'options should have been rejected: ' + data[i].options 319 | ); 320 | } 321 | } 322 | client.stop(function() { 323 | test.done(); 324 | }); 325 | }); 326 | }; 327 | 328 | 329 | /** 330 | * Test a variety of valid and invalid QoS values. Invalid QoS values 331 | * should result in the client.send(...) method throwing a TypeError. 332 | * @param {object} test the unittest interface 333 | */ 334 | module.exports.test_send_qos = function(test) { 335 | var number = Number(1); 336 | var data = [{valid: false, qos: ''}, 337 | {valid: false, qos: undefined}, 338 | {valid: false, qos: null}, 339 | {valid: false, qos: function() {}}, 340 | {valid: false, qos: '1'}, 341 | {valid: false, qos: 2}, 342 | {valid: true, qos: 0}, 343 | {valid: true, qos: 1}, 344 | {valid: true, qos: number}, 345 | {valid: true, qos: 9 - 8}, 346 | {valid: true, qos: mqlight.QOS_AT_MOST_ONCE}, 347 | {valid: true, qos: mqlight.QOS_AT_LEAST_ONCE}]; 348 | 349 | var client = mqlight.createClient({id: 'test_send_qos', service: 350 | 'amqp://host'}); 351 | client.on('started', function() { 352 | for (var i = 0; i < data.length; ++i) { 353 | var opts = { qos: data[i].qos }; 354 | if (data[i].valid) { 355 | test.doesNotThrow( 356 | function() { 357 | client.send('test', 'message', opts, function() {}); 358 | } 359 | ); 360 | } else { 361 | test.throws( 362 | function() { 363 | client.send('test', 'message', opts); 364 | }, 365 | RangeError, 366 | 'qos value should have been rejected: ' + data[i].qos 367 | ); 368 | } 369 | } 370 | client.stop(); 371 | test.done(); 372 | }); 373 | }; 374 | 375 | 376 | /** 377 | * Test that a function is required when QoS is 1. 378 | * @param {object} test the unittest interface 379 | */ 380 | module.exports.test_send_qos_function = function(test) { 381 | var data = [{valid: false, qos: 1, callback: undefined}, 382 | {valid: true, qos: 1, callback: function() {}}, 383 | {valid: true, qos: 0, callback: undefined}, 384 | {valid: true, qos: 0, callback: function() {}}]; 385 | 386 | var client = mqlight.createClient({id: 'test_send_qos_function', service: 387 | 'amqp://host'}); 388 | client.on('started', function() { 389 | for (var i = 0; i < data.length; ++i) { 390 | var opts = { qos: data[i].qos }; 391 | if (data[i].valid) { 392 | test.doesNotThrow( 393 | function() { 394 | client.send('test', 'message', opts, data[i].callback); 395 | } 396 | ); 397 | } else { 398 | test.throws( 399 | function() { 400 | client.send('test', 'message', opts, data[i].callback); 401 | }, 402 | mqlight.InvalidArgumentError, 403 | 'Should have thrown, as qos and callback combination is invalid' 404 | ); 405 | } 406 | } 407 | client.stop(function() { 408 | test.done(); 409 | }); 410 | }); 411 | }; 412 | 413 | 414 | /** 415 | * Test that any queued sends are cleared when stop is called 416 | * and that the sends callback is called with an error to indicate 417 | * failure. 418 | * @param {object} test the unittest interface 419 | */ 420 | module.exports.test_clear_queuedsends_disconnect = function(test) { 421 | var client = mqlight.createClient({id: 'test_clear_queuedsends_disconnect', 422 | service: 'amqp://host'}); 423 | var savedSendFunction = stubproton.sender.send; 424 | stubproton.sender.send = function() { 425 | return new Promise(function(resolve, reject) { 426 | reject(new Error('stub error during send')); 427 | }); 428 | }; 429 | 430 | var timeout = setTimeout(function() { 431 | test.ok(false, 'test timed out before callback'); 432 | stubproton.sender.send = savedSendFunction; 433 | client.stop(); 434 | test.done(); 435 | }, 436 | 5000); 437 | var opts = {qos: mqlight.QOS_AT_LEAST_ONCE}; 438 | 439 | client.on('started', function(err) { 440 | stubproton.setConnectStatus(1); 441 | client.send('test', 'message', opts, function(err) { 442 | //test.deepEqual(client.state, 'stopped', 443 | // 'callback called when stopped'); 444 | test.notEqual(err, undefined, 'not undefined so err set'); 445 | test.equal(client._queuedSends.length, 0, 'no queued sends left'); 446 | stubproton.sender.send = savedSendFunction; 447 | clearTimeout(timeout); 448 | stubproton.setConnectStatus(0); 449 | test.done(); 450 | }); 451 | }); 452 | 453 | client.on('error', function(err) { 454 | client.stop(); 455 | }); 456 | }; 457 | 458 | 459 | /** 460 | * Test that supplying a valid time-to-live value for a send operation is 461 | * correctly propagated to the proton message object. Also test that 462 | * supplying invalid values result in the client.send(...) method throwing 463 | * a TypeError. 464 | * @param {object} test the unittest interface 465 | */ 466 | module.exports.test_send_ttl = function(test) { 467 | var data = [ 468 | {valid: false, ttl: undefined}, 469 | {valid: false, ttl: function() {}}, 470 | {valid: false, ttl: -9007199254740992}, 471 | {valid: false, ttl: -NaN}, 472 | {valid: false, ttl: NaN}, 473 | {valid: false, ttl: -Infinity}, 474 | {valid: false, ttl: Infinity}, 475 | {valid: false, ttl: -1}, 476 | {valid: false, ttl: 0}, 477 | {valid: false, ttl: null}, // treated as 0 478 | {valid: false, ttl: ''}, // treated as 0 479 | {expected: 1, valid: true, ttl: 1}, 480 | {expected: 1, valid: true, ttl: 9 - 8}, 481 | {expected: 4294967295, valid: true, ttl: 9007199254740992} 482 | ]; 483 | 484 | var client = 485 | mqlight.createClient({id: 'test_send_ttl', service: 'amqp://host'}); 486 | var savedSendFunction = stubproton.sender.send; 487 | var lastMsg; 488 | stubproton.sender.send = function(msg) { 489 | lastMsg = msg; 490 | return savedSendFunction(msg); 491 | }; 492 | var sendNext = function() { 493 | var next = data.shift(); 494 | if (next) { 495 | var opts = {ttl: next.ttl}; 496 | if (next.valid) { 497 | test.doesNotThrow(function() { 498 | client.send('topic', 'data', opts, function() { 499 | test.deepEqual(lastMsg.header.ttl, next.expected, 500 | 'ttl value in proton message should match that ' + 501 | 'passed into the send(...) method'); 502 | sendNext(); 503 | }); 504 | }); 505 | } else { 506 | test.throws(function() { 507 | client.send('topic', 'data', opts); 508 | }, RangeError, 'ttl should have been rejected: ' + next.ttl); 509 | sendNext(); 510 | } 511 | } else { 512 | client.stop(function() { 513 | stubproton.sender.send = savedSendFunction; 514 | test.done(); 515 | }); 516 | } 517 | }; 518 | client.on('started', function() { 519 | sendNext(); 520 | }); 521 | }; 522 | 523 | 524 | /** 525 | * Test that if send returns false then, in time, a drain event is emitted. 526 | * @param {object} test the unittest interface 527 | */ 528 | module.exports.test_send_drain_event = function(test) { 529 | var client = mqlight.createClient({id: 'test_send_drain_event', service: 530 | 'amqp://host'}); 531 | client.on('started', function() { 532 | var drainExpected = false; 533 | 534 | var timeout = setTimeout(function() { 535 | test.ok(false, 'Test timed out waiting for drain event to be emitted'); 536 | test.done(); 537 | if (client) client.stop(); 538 | }, 5000); 539 | 540 | client.on('drain', function() { 541 | test.ok(drainExpected, 'Drain event not expected to be emitted'); 542 | clearTimeout(timeout); 543 | if (client) { 544 | client.stop(function() {test.done()}); 545 | } else { 546 | test.done(); 547 | } 548 | }); 549 | 550 | for(var i = 0; i < 100; i++) { 551 | if(client.send('topic', 'data') == false) 552 | { 553 | drainExpected = true; 554 | break; 555 | } 556 | } 557 | 558 | test.ok(drainExpected, 'send always returned true'); 559 | }); 560 | }; 561 | 562 | 563 | /** 564 | * Tests that the client correctly handles the server rejecting messages, 565 | * by invoking the callback function supplied to send (if any). 566 | * @param {object} test the unittest interface 567 | */ 568 | module.exports.test_message_rejected = function(test) { 569 | var rejectErrorMessage = 'get away from me!'; 570 | var savedSendFunction = stubproton.sender.send; 571 | stubproton.sender.send = function() { 572 | return new Promise(function(resolve, reject) { 573 | reject(new Error(rejectErrorMessage)); 574 | }); 575 | }; 576 | var client = mqlight.createClient({ 577 | id: 'test_message_rejected', 578 | service: 'amqp://host'}); 579 | 580 | client.on('started', function() { 581 | test.doesNotThrow(function() { 582 | // Test that a message being rejected result in the send(...) method's 583 | // callback being run. 584 | client.send('topic', 'data', {qos: 1}, function(err) { 585 | test.ok(err); 586 | test.equals(err.message, rejectErrorMessage); 587 | 588 | stubproton.sender.send = savedSendFunction; 589 | client.stop(function() { 590 | test.done(); 591 | }); 592 | }); 593 | }); 594 | }); 595 | }; 596 | 597 | 598 | ///** 599 | // * This is a simple test to confirm that a client that attempts to send, but 600 | // * which has been replaced by another client with the same id, gets the 601 | // * ReplacedError 602 | // * 603 | // * @param {object} test the unittest interface 604 | // */ 605 | //module.exports.test_send_client_replaced = function(test) { 606 | // var client = mqlight.createClient({ 607 | // service: 'amqp://host', 608 | // id: 'test_send_client_replaced' 609 | // }); 610 | // var savedSendFunction = stubproton.sender.send; 611 | // stubproton.sender.send = function() { 612 | // return new Promise(function(resolve, reject) { 613 | // var err = new Error('CONNECTION ERROR (ServerContext_Takeover)'); 614 | // err.name = 'ReplacedError'; 615 | // reject(err); 616 | // }); 617 | // }; 618 | // 619 | // client.on('error', function() {}); 620 | // 621 | // client.once('started', function() { 622 | // test.ok(this === client); 623 | // test.equals(arguments.length, 0); 624 | // test.equals(client.state, 'started'); 625 | // test.equals(client._messenger.stopped, false); 626 | // 627 | // client.send('topic', 'data', function(err) { 628 | // test.ok(err instanceof Error); 629 | // test.ok(err instanceof mqlight.ReplacedError); 630 | // test.ok(/ReplacedError: /.test(err.toString())); 631 | // stubproton.sender.send = savedSendFunction; 632 | // client.stop(); 633 | // test.done(); 634 | // }); 635 | // }); 636 | //}; 637 | 638 | 639 | /** 640 | * Unit test for the fix to defect 74527. Calls send twice, each time 641 | * specifying a different callback. The expected behaviour is that both send 642 | * calls complete in order and invoke their respective callbacks. 643 | * 644 | * @param {object} test the unittest interface 645 | */ 646 | module.exports.test_sends_call_correct_callbacks = function(test) { 647 | var client = mqlight.createClient({ 648 | service: 'amqp://host', 649 | id: 'test_sends_call_correct_callbacks' 650 | }); 651 | var firstCallbackRun = false; 652 | var secondCallbackRun = false; 653 | 654 | client.once('started', function() { 655 | client.send('topic', 'data', function(err) { 656 | test.ok(!firstCallbackRun, 'first callback run twice!'); 657 | test.ok(!secondCallbackRun, 'second callback called before first(1)!'); 658 | firstCallbackRun = true; 659 | }); 660 | 661 | client.send('topic', 'data', function(err) { 662 | test.ok(!secondCallbackRun, 'second callback called twice!'); 663 | test.ok(firstCallbackRun, 'second callback called before first(2)!'); 664 | secondCallbackRun = true; 665 | client.stop(); 666 | test.done(); 667 | }); 668 | }); 669 | }; 670 | -------------------------------------------------------------------------------- /mqlight-log.js: -------------------------------------------------------------------------------- 1 | /* %Z% %W% %I% %E% %U% */ 2 | /* 3 | * 8 | * Licensed Materials - Property of IBM 9 | * 10 | * 5725-P60 11 | * 12 | * (C) Copyright IBM Corp. 2014, 2016 13 | * 14 | * US Government Users Restricted Rights - Use, duplication or 15 | * disclosure restricted by GSA ADP Schedule Contract with 16 | * IBM Corp. 17 | * 18 | */ 19 | /* jslint node: true */ 20 | /* jshint -W083,-W097 */ 21 | 'use strict'; 22 | var logger = exports; 23 | 24 | var pkg = require('./package.json'); 25 | var os = require('os'); 26 | var moment = require('moment'); 27 | var npmlog = require('npmlog'); 28 | var fs = require('fs'); 29 | var util = require('util'); 30 | 31 | var isWin = (os.platform() === 'win32'); 32 | var stack = ['']; 33 | var startLevel; 34 | var historyLevel; 35 | var ffdcSequence = 0; 36 | var fd = 0; 37 | var dataSize; 38 | var exceptionThrown = null; 39 | var potentialUnwinds = 0; 40 | 41 | var ENTRY_IND = '>-----------------------------------------------------------'; 42 | var EXIT_IND = '<-----------------------------------------------------------'; 43 | var HEADER_BANNER = '+---------------------------------------' + 44 | '---------------------------------------+'; 45 | 46 | var styles = { 47 | blue: { fg: 'blue', bg: 'black' }, 48 | green: { fg: 'green', bg: 'black' }, 49 | inverse: { inverse: true }, 50 | red: { fg: 'red', bg: 'black' }, 51 | yellow: { fg: 'yellow', bg: 'black' } 52 | }; 53 | 54 | /* 55 | * Set the npmlog heading to include a timestamp and process identifier. 56 | */ 57 | Object.defineProperty(npmlog, 'heading', { 58 | get: function() { 59 | return moment().format('HH:mm:ss.SSS') + ' [' + process.pid + ']'; 60 | } 61 | }); 62 | 63 | 64 | /* 65 | * Write a log or ffdc header, including basic information about the program 66 | * and host. 67 | */ 68 | var header = function(lvl, clientId, options) { 69 | if (npmlog.levels[npmlog.level] <= npmlog.levels[lvl]) { 70 | npmlog.log(lvl, clientId, HEADER_BANNER); 71 | npmlog.log(lvl, clientId, '| IBM MQ Light Node.js Client Module -', 72 | options.title); 73 | npmlog.log(lvl, clientId, HEADER_BANNER); 74 | npmlog.log(lvl, clientId, '| Date/Time :-', 75 | moment().format('ddd MMMM DD YYYY HH:mm:ss.SSS Z')); 76 | npmlog.log(lvl, clientId, '| Host Name :-', os.hostname()); 77 | npmlog.log(lvl, clientId, '| Operating System :-', 78 | os.type(), os.release()); 79 | npmlog.log(lvl, clientId, '| Architecture :-', 80 | os.platform(), os.arch()); 81 | npmlog.log(lvl, clientId, '| Node Version :-', process.version); 82 | npmlog.log(lvl, clientId, '| Node Path :-', process.execPath); 83 | npmlog.log(lvl, clientId, '| Node Arguments :-', process.execArgs); 84 | if (!isWin) { 85 | npmlog.log(lvl, clientId, '| User Id :-', process.getuid()); 86 | npmlog.log(lvl, clientId, '| Group Id :-', process.getgid()); 87 | } 88 | npmlog.log(lvl, clientId, '| Name :-', pkg.name); 89 | npmlog.log(lvl, clientId, '| Version :-', pkg.version); 90 | npmlog.log(lvl, clientId, '| Description :-', pkg.description); 91 | npmlog.log(lvl, clientId, '| Installation Path :-', __dirname); 92 | npmlog.log(lvl, clientId, '| Uptime :-', process.uptime()); 93 | npmlog.log(lvl, clientId, '| Log Level :-', npmlog.level); 94 | npmlog.log(lvl, clientId, '| Data Size :-', dataSize); 95 | if ('fnc' in options) { 96 | npmlog.log(lvl, clientId, '| Function :-', options.fnc); 97 | } 98 | if ('probeId' in options) { 99 | npmlog.log(lvl, clientId, '| Probe Id :-', options.probeId); 100 | } 101 | if ('ffdcSequence' in options) { 102 | npmlog.log(lvl, clientId, '| FFDCSequenceNumber:-', 103 | options.ffdcSequence++); 104 | } 105 | if (potentialUnwinds !== 0) { 106 | npmlog.log(lvl, clientId, '| potentialUnwinds :-', potentialUnwinds); 107 | } 108 | npmlog.log(lvl, clientId, HEADER_BANNER); 109 | if ('fnc' in options && options.fnc.indexOf('SIG') == 0) { 110 | npmlog.log(lvl, clientId, '(Set MQLIGHT_NODE_NO_HANDLER to ' + 111 | 'disable user requested FFDCs)'); 112 | } 113 | npmlog.log(lvl, clientId, ''); 114 | } 115 | }; 116 | 117 | 118 | /** 119 | * Set the logging level. 120 | * 121 | * @param {String} level The logging level to write at. 122 | */ 123 | logger.setLevel = function(level) { 124 | var lvl = String(level).toLowerCase().trim(); 125 | if (npmlog.levels[lvl]) { 126 | npmlog.level = lvl; 127 | 128 | header('header', logger.NO_CLIENT_ID, {title: 'Log'}); 129 | 130 | var debug = process.env.PN_TRACE_FRM + ',' || ''; 131 | if (npmlog.levels[npmlog.level] <= npmlog.levels.debug) { 132 | logger.log('debug', logger.NO_CLIENT_ID, 'Setting basic amqp10 debug'); 133 | process.env.DEBUG = 134 | debug + 'amqp10:*,-amqp10:session,-amqp10:framing,-amqp10:sasl'; 135 | if (npmlog.levels[npmlog.level] <= npmlog.levels.detail) { 136 | logger.log('debug', logger.NO_CLIENT_ID, 137 | 'Setting detailed amqp10 debug'); 138 | process.env.DEBUG = debug + 'amqp10:*'; 139 | } 140 | } else if (process.env.DEBUG && /amqp10:/.test(debug)) { 141 | logger.log('debug', logger.NO_CLIENT_ID, 'Unsetting amqp10 debug'); 142 | debug = debug.slice(0, debug.indexOf('amqp10')); 143 | if (debug.length > 0) { 144 | process.env.DEBUG = debug; 145 | } else { 146 | delete process.env.DEBUG; 147 | } 148 | } 149 | } else { 150 | console.error('ERROR: MQ Light log level \'' + lvl + '\' is invalid'); 151 | } 152 | }; 153 | 154 | 155 | /** 156 | * Get the logging level. 157 | * 158 | * @return {String} The logging level. 159 | */ 160 | logger.getLevel = function() { 161 | return npmlog.level; 162 | }; 163 | 164 | 165 | /** 166 | * Set the logging stream. 167 | * 168 | * @param {String} stream The stream to log to. If the value 169 | * isn't 'stderr' or 'stdout' then stream will be treated 170 | * as a file which will be written to as well as 171 | * stderr/stdout. 172 | */ 173 | logger.setStream = function(stream) { 174 | if (stream === 'stderr') { 175 | // Log to stderr. 176 | npmlog.stream = process.stderr; 177 | 178 | // Stop writing to a file. 179 | if (fd) { 180 | fs.closeSync(fd); 181 | } 182 | } else if (stream === 'stdout') { 183 | // Log to stdout. 184 | npmlog.stream = process.stdout; 185 | 186 | // Stop writing to a file. 187 | if (fd) { 188 | fs.closeSync(fd); 189 | } 190 | } else { 191 | // A file has been specified. As well as writing to stderr/stdout, we 192 | // additionally write the output to a file. 193 | 194 | // Stop writing to an existing file. 195 | if (fd) { 196 | fs.closeSync(fd); 197 | } 198 | 199 | // Open the specified file. 200 | fd = fs.openSync(stream, 'a', '0644'); 201 | 202 | // Set up a listener for log events. 203 | npmlog.on('log', function(m) { 204 | if (fd) { 205 | if (npmlog.levels[npmlog.level] <= npmlog.levels[m.level]) { 206 | // We'll get called for every log event, so filter to ones we're 207 | // interested in. 208 | fs.writeSync(fd, util.format('%s %s %s %s\n', 209 | npmlog.heading, npmlog.disp[m.level], 210 | m.prefix, m.message)); 211 | } 212 | } 213 | }); 214 | } 215 | }; 216 | 217 | 218 | /** 219 | * Set the amount of message data that will get logged. 220 | * 221 | * @param {Number} size amount of message data that will get logged. 222 | */ 223 | logger.setDataSize = function(size) { 224 | logger.entry('logger.setDataSize', logger.NO_CLIENT_ID); 225 | logger.log('parms', logger.NO_CLIENT_ID, 'size:', size); 226 | 227 | if (typeof size === 'string') { 228 | dataSize = parseInt(size); 229 | if (isNaN(dataSize)) { 230 | throw new TypeError('MQLIGHT_NODE_MESSAGE_DATA_SIZE is not a number'); 231 | } 232 | } else { 233 | dataSize = size; 234 | } 235 | 236 | logger.exit('logger.setDataSize', logger.NO_CLIENT_ID); 237 | }; 238 | 239 | 240 | /** 241 | * Get the amount of message data that will get logged. 242 | * 243 | * @return {Number} The data size. 244 | */ 245 | logger.getDataSize = function() { 246 | return dataSize; 247 | }; 248 | 249 | 250 | /** 251 | * Log entry into a function, specifying the logging level to 252 | * write at. 253 | * 254 | * @param {String} lvl The logging level to write at. 255 | * @param {String} name The name of the function being entered. 256 | * @param {String} id The id of the client causing the function 257 | * to be entered. 258 | */ 259 | logger.entryLevel = function(lvl, name, id) { 260 | if (exceptionThrown) { 261 | logger.log('error', id, '* Uncaught exception'); 262 | exceptionThrown = null; 263 | while (stack.length > 1) { 264 | stack.pop(); 265 | } 266 | } 267 | npmlog.log(lvl, id, ENTRY_IND.substring(0, stack.length), name); 268 | stack.push(name); 269 | }; 270 | 271 | 272 | /** 273 | * Log entry into a function. 274 | * 275 | * @param {String} name The name of the function being entered. 276 | * @param {String} id The id of the client causing the function 277 | * to be entered. 278 | */ 279 | logger.entry = function(name, id) { 280 | logger.entryLevel('entry', name, id); 281 | }; 282 | 283 | 284 | /** 285 | * Log exit from a function, specifying the logging level to 286 | * write at. 287 | * 288 | * @param {String} lvl The logging level to write at. 289 | * @param {String} name The name of the function being exited. 290 | * @param {String} id The id of the client causing the function 291 | * to be exited. 292 | * @param {Object} rc The function return code. 293 | */ 294 | logger.exitLevel = function(lvl, name, id, rc) { 295 | // Only log object type returns if object logging is enabled. 296 | if (npmlog.levels[npmlog.level] <= npmlog.levels.object) { 297 | npmlog.log(lvl, id, EXIT_IND.substring(0, Math.max(1, stack.length - 1)), 298 | name, rc ? rc : ''); 299 | } else { 300 | npmlog.log(lvl, id, EXIT_IND.substring(0, Math.max(1, stack.length - 1)), 301 | name, rc ? (typeof rc === 'object' ? '[object]' : rc) : ''); 302 | } 303 | var last; 304 | do { 305 | // Check if we've unwound to the bottom of the stack. 306 | if (stack.length == 1) { 307 | // We have. Generate an FFDC if we believe the stack to be good. Most 308 | // likely we've exited with the wrong function name. 309 | if (potentialUnwinds === 0) { 310 | logger.ffdc('logger.exitLevel', 10, null, name); 311 | } 312 | potentialUnwinds--; 313 | logger.log('debug', id, 'Potential unwinds decreased to', 314 | potentialUnwinds); 315 | break; 316 | } 317 | 318 | // Get rid of the last function put on the stack. 319 | last = stack.pop(); 320 | } while (last != name); 321 | }; 322 | 323 | 324 | /** 325 | * Log exit from a function. 326 | * 327 | * @param {String} name The name of the function being exited. 328 | * @param {String} id The id of the client causing the function 329 | * to be exited. 330 | * @param {Object} rc The function return code. 331 | */ 332 | logger.exit = function(name, id, rc) { 333 | logger.exitLevel('exit', name, id, rc); 334 | }; 335 | 336 | 337 | /** 338 | * Log data. 339 | * 340 | * @this {log} 341 | * @param {String} lvl The level at which to log the data. 342 | * @param {String} id The id of the client logging the data. 343 | * @param {Object} args The data to be logged. 344 | */ 345 | logger.log = function(lvl, id, args) { 346 | if (npmlog.levels[npmlog.level] <= npmlog.levels[lvl]) { 347 | npmlog.log.apply(this, arguments); 348 | } 349 | }; 350 | 351 | 352 | /** 353 | * Log message body. 354 | * 355 | * @this {log} 356 | * @param {String} id The id of the client logging the data. 357 | * @param {Object} data The message body to be logged subject to 358 | * specified data size. Must be either a string or a 359 | * Buffer object. 360 | */ 361 | logger.body = function(id, data) { 362 | if (npmlog.levels[npmlog.level] <= npmlog.levels.data) { 363 | npmlog.log('data', id, '! length:', data.length); 364 | if (typeof data === 'string') { 365 | if ((dataSize >= data.length) || (dataSize < 0)) { 366 | npmlog.log('data', id, '! string:', data); 367 | } else { 368 | npmlog.log('data', id, '! string:', data.substring(0, dataSize), '...'); 369 | } 370 | } else { 371 | if ((dataSize >= data.length) || (dataSize < 0)) { 372 | npmlog.log('data', id, '! buffer:', 373 | data.toString('hex')); 374 | } else { 375 | npmlog.log('data', id, '! buffer:', 376 | data.toString('hex', 0, dataSize), '...'); 377 | } 378 | } 379 | } 380 | }; 381 | 382 | 383 | /** 384 | * Log an exception being thrown, specifying the logging level to 385 | * write at. 386 | * 387 | * @this {log} 388 | * @param {String} lvl The logging level to write at. 389 | * @param {String} name The name of the function throwing the 390 | * exception. 391 | * @param {String} id The id of the client logging the 392 | * exception. 393 | * @param {Object} err The exception being thrown. 394 | */ 395 | logger.throwLevel = function(lvl, name, id, err) { 396 | logger.log('error', id, '* Thrown exception:', err); 397 | exceptionThrown = err; 398 | logger.exitLevel(lvl, name, id, 'Exception thrown'); 399 | potentialUnwinds += stack.length - 1; 400 | logger.log('debug', id, 'Potential unwinds increased to', potentialUnwinds); 401 | }; 402 | 403 | 404 | /** 405 | * Log an exception being thrown. 406 | * 407 | * @this {log} 408 | * @param {String} name The name of the function throwing the 409 | * exception. 410 | * @param {String} id The id of the client logging the 411 | * exception. 412 | * @param {Error} err The exception being thrown. 413 | */ 414 | logger.throw = function(name, id, err) { 415 | logger.throwLevel('exit', name, id, err); 416 | }; 417 | 418 | 419 | /** 420 | * Log an exception being caught, specifying the logging level to 421 | * write at. 422 | * 423 | * @this {log} 424 | * @param {String} lvl The logging level to write at. 425 | * @param {String} name The name of the function catching the 426 | * exception. 427 | * @param {String} id The id of the client logging the data. 428 | * @param {Error} err The exception being caught. 429 | */ 430 | logger.caughtLevel = function(lvl, name, id, err) { 431 | logger.log('error', id, '* Caught exception:', err); 432 | if (exceptionThrown) { 433 | exceptionThrown = null; 434 | while (stack.length > 1) { 435 | if (stack[stack.length - 1] === name) { 436 | break; 437 | } 438 | stack.pop(); 439 | potentialUnwinds--; 440 | logger.log('debug', id, 'Potential unwinds decreased to', 441 | potentialUnwinds); 442 | } 443 | if (stack.length == 1) { 444 | logger.entryLevel(lvl, name, id); 445 | } 446 | } 447 | }; 448 | 449 | 450 | /** 451 | * Log an exception being caught. 452 | * 453 | * @this {log} 454 | * @param {String} name The name of the function catching the 455 | * exception. 456 | * @param {String} id The id of the client logging the data. 457 | * @param {Error} err The exception being caught. 458 | */ 459 | logger.caught = function(name, id, err) { 460 | logger.caughtLevel('entry', name, id, err); 461 | }; 462 | 463 | 464 | /** 465 | * Dump First Failure Data Capture information in the event of 466 | * failure to aid in diagnosis of an error. 467 | * 468 | * @param {String=} opt_fnc The name of the calling function. 469 | * @param {Number=} opt_probeId An identifier for the error 470 | * location. 471 | * @param {Client=} opt_client The client having a problem. 472 | * @param {Object=} opt_data Extra data to aid in problem 473 | * diagnosis. 474 | */ 475 | logger.ffdc = function(opt_fnc, opt_probeId, opt_client, opt_data) { 476 | var opts = { 477 | title: 'First Failure Data Capture', 478 | fnc: opt_fnc || 'User-requested FFDC by function', 479 | probeId: opt_probeId || 255, 480 | ffdcSequence: ffdcSequence++, 481 | clientId: opt_client ? opt_client.id : logger.NO_CLIENT_ID 482 | }; 483 | 484 | logger.entry('logger.ffdc', opts.clientId); 485 | logger.log('parms', opts.clientId, 'fnc:', opt_fnc); 486 | logger.log('parms', opts.clientId, 'probeId:', opt_probeId); 487 | logger.log('parms', opts.clientId, 'data:', opt_data); 488 | 489 | if (npmlog.levels[npmlog.level] <= npmlog.levels.ffdc) { 490 | header('ffdc', opts.clientId, opts); 491 | npmlog.log('ffdc', opts.clientId, new Error().stack); 492 | npmlog.log('ffdc', opts.clientId, ''); 493 | npmlog.log('ffdc', opts.clientId, 'Function Stack'); 494 | npmlog.log('ffdc', opts.clientId, stack.slice(1)); 495 | npmlog.log('ffdc', opts.clientId, ''); 496 | npmlog.log('ffdc', opts.clientId, 'Function History'); 497 | for (var idx = 0; idx < npmlog.record.length; idx++) { 498 | var rec = npmlog.record[idx]; 499 | if ((rec.level !== 'ffdc') && 500 | (npmlog.levels[rec.level] >= npmlog.levels[historyLevel])) { 501 | npmlog.log('ffdc', opts.clientId, '%d %s %s %s', 502 | rec.id, npmlog.disp[rec.level], rec.prefix, rec.message); 503 | } 504 | } 505 | if (opt_client) { 506 | npmlog.log('ffdc', opts.clientId, ''); 507 | npmlog.log('ffdc', opts.clientId, 'Client'); 508 | npmlog.log('ffdc', opts.clientId, opt_client); 509 | } 510 | if (opt_data) { 511 | npmlog.log('ffdc', opts.clientId, ''); 512 | npmlog.log('ffdc', opts.clientId, 'Data'); 513 | npmlog.log('ffdc', opts.clientId, opt_data); 514 | } 515 | npmlog.log('ffdc', opts.clientId, ''); 516 | npmlog.log('ffdc', opts.clientId, 'Memory Usage'); 517 | npmlog.log('ffdc', opts.clientId, process.memoryUsage()); 518 | if ((ffdcSequence === 1) || (opts.probeId === 255)) { 519 | npmlog.log('ffdc', opts.clientId, ''); 520 | npmlog.log('ffdc', opts.clientId, 'Environment Variables'); 521 | npmlog.log('ffdc', opts.clientId, process.env); 522 | } 523 | npmlog.log('ffdc', opts.clientId, ''); 524 | } 525 | 526 | // In a unit testing environment we expect to get no ffdcs. 527 | if (process.env.NODE_ENV === 'unittest') { 528 | var err = new Error('No ffdcs expected during unit tests'); 529 | logger.throw('logger.ffdc', opts.clientId, err); 530 | throw err; 531 | } 532 | 533 | logger.exit('logger.ffdc', opts.clientId, null); 534 | 535 | // Exit if fail on FFDC is required. 536 | if (process.env.MQLIGHT_NODE_FAIL_ON_FFDC) { 537 | console.error('Aborting due to FFDC'); 538 | process.abort(); 539 | } 540 | }; 541 | 542 | 543 | /** 544 | * The identifier used when a log entry is not associated with a 545 | * particular client. 546 | * 547 | * @const {string} 548 | */ 549 | logger.NO_CLIENT_ID = '*'; 550 | 551 | npmlog.addLevel('all', -Infinity, styles.inverse, 'all '); 552 | npmlog.addLevel('proton_data', -Infinity, styles.green, 'data '); 553 | npmlog.addLevel('proton_exit', -Infinity, styles.yellow, 'exit '); 554 | npmlog.addLevel('proton_entry', -Infinity, styles.yellow, 'entry '); 555 | npmlog.addLevel('proton', -Infinity, styles.yellow, 'func '); 556 | npmlog.addLevel('data_often', 100, styles.green, 'data '); 557 | npmlog.addLevel('exit_often', 100, styles.yellow, 'exit '); 558 | npmlog.addLevel('entry_often', 100, styles.yellow, 'entry '); 559 | npmlog.addLevel('often', 100, styles.yellow, 'func '); 560 | npmlog.addLevel('raw', 200, styles.inverse, 'raw '); 561 | npmlog.addLevel('detail', 300, styles.green, 'detail'); 562 | npmlog.addLevel('debug', 500, styles.inverse, 'debug '); 563 | npmlog.addLevel('emit', 800, styles.green, 'emit '); 564 | npmlog.addLevel('data', 1000, styles.green, 'data '); 565 | npmlog.addLevel('parms', 1200, styles.yellow, 'parms '); 566 | npmlog.addLevel('header', 1500, styles.yellow, 'header'); 567 | npmlog.addLevel('exit', 1500, styles.yellow, 'exit '); 568 | npmlog.addLevel('entry', 1500, styles.yellow, 'entry '); 569 | npmlog.addLevel('entry_exit', 1500, styles.yellow, 'func '); 570 | npmlog.addLevel('error', 1800, styles.red, 'error '); 571 | npmlog.addLevel('object', 1900, styles.red, 'object'); 572 | npmlog.addLevel('ffdc', 2000, styles.red, 'ffdc '); 573 | 574 | 575 | /** 576 | * Set the logging stream. By default stderr will be used, but 577 | * this can be changed to stdout by setting the environment 578 | * variable MQLIGHT_NODE_LOG_STREAM=stdout. 579 | */ 580 | logger.setStream(process.env.MQLIGHT_NODE_LOG_STREAM || 'stderr'); 581 | 582 | 583 | /** 584 | * Set the amount of message data that will get logged. The 585 | * default is 100 bytes, but this can be altered by setting the 586 | * environment variable MQLIGHT_NODE_MESSAGE_DATA_SIZE to a 587 | * different number. 588 | */ 589 | logger.setDataSize(process.env.MQLIGHT_NODE_MESSAGE_DATA_SIZE || 100); 590 | 591 | 592 | /** 593 | * Set the level of logging. By default only 'ffdc' entries will 594 | * be logged, but this can be altered by setting the environment 595 | * variable MQLIGHT_NODE_LOG to one of the defined levels. 596 | */ 597 | startLevel = process.env.MQLIGHT_NODE_LOG || 'ffdc'; 598 | logger.setLevel(startLevel); 599 | 600 | 601 | /** 602 | * Set the maximum size of logging history. By default a maximum 603 | * of 10,000 entries will be retained, but this can be altered 604 | * by setting the environment variable 605 | * MQLIGHT_NODE_LOG_HISTORY_SIZE to a different number. 606 | */ 607 | npmlog.maxRecordSize = process.env.MQLIGHT_NODE_LOG_HISTORY_SIZE || 10000; 608 | logger.log('debug', logger.NO_CLIENT_ID, 609 | 'npmlog.maxRecordSize:', npmlog.maxRecordSize); 610 | 611 | /* 612 | * Set the level of entries that will dumped in the ffdc function history. 613 | * By default only entries at debug level or above will be dumped, but this can 614 | * be altered by setting the environment variable MQLIGHT_NODE_LOG_HISTORY to 615 | * one of the defined levels. 616 | */ 617 | historyLevel = process.env.MQLIGHT_NODE_LOG_HISTORY || 'debug'; 618 | logger.log('debug', logger.NO_CLIENT_ID, 'historyLevel:', historyLevel); 619 | 620 | 621 | /* 622 | * Set up a signal handler that will cause an ffdc to be generated when 623 | * the signal is caught. Set the environment variable MQLIGHT_NODE_NO_HANDLER 624 | * to stop the signal handler being registered. 625 | */ 626 | if (!process.env.MQLIGHT_NODE_NO_HANDLER) { 627 | var signal = isWin ? 'SIGBREAK' : 'SIGUSR2'; 628 | logger.log('debug', logger.NO_CLIENT_ID, 'Registering signal handler for', 629 | signal); 630 | process.on(signal, function() { 631 | logger.ffdc(signal, 255, null, 'User-requested FFDC on signal'); 632 | 633 | // Start logging at the 'debug' level if we're not doing so, or turn off 634 | // logging if we already are. 635 | if (npmlog.levels[startLevel] > npmlog.levels.debug) { 636 | if (npmlog.level === startLevel) { 637 | logger.log('ffdc', logger.NO_CLIENT_ID, 'Setting npmlog.level: debug'); 638 | logger.setLevel('debug'); 639 | } else { 640 | logger.log('ffdc', logger.NO_CLIENT_ID, 'Setting npmlog.level:', 641 | startLevel); 642 | logger.setLevel(startLevel); 643 | } 644 | } 645 | }); 646 | } 647 | 648 | if (process.env.MQLIGHT_NODE_DEBUG_PORT) { 649 | /** 650 | * Set the port which the debugger will listen on to the value of the 651 | * MQLIGHT_NODE_DEBUG_PORT environment variable, if it's set. 652 | */ 653 | process.debugPort = process.env.MQLIGHT_NODE_DEBUG_PORT; 654 | } 655 | 656 | /* 657 | * If the MQLIGHT_NODE_FFDC_ON_UNCAUGHT environment variable is set, then 658 | * an ffdc will be produced on an uncaught exception. 659 | */ 660 | if (process.env.MQLIGHT_NODE_FFDC_ON_UNCAUGHT) { 661 | logger.log('debug', logger.NO_CLIENT_ID, 662 | 'Registering uncaught exception handler'); 663 | process.on('uncaughtException', function(err) { 664 | logger.ffdc('uncaughtException', 100, null, err); 665 | throw err; 666 | }); 667 | } 668 | -------------------------------------------------------------------------------- /test/testsubscribe.js: -------------------------------------------------------------------------------- 1 | /* %Z% %W% %I% %E% %U% */ 2 | /* 3 | * 8 | * Licensed Materials - Property of IBM 9 | * 10 | * 5725-P60 11 | * 12 | * (C) Copyright IBM Corp. 2014 13 | * 14 | * US Government Users Restricted Rights - Use, duplication or 15 | * disclosure restricted by GSA ADP Schedule Contract with 16 | * IBM Corp. 17 | * 18 | */ 19 | /* jslint node: true */ 20 | /* jshint -W083,-W097 */ 21 | 'use strict'; 22 | 23 | 24 | /** @const {string} enable unittest mode in mqlight.js */ 25 | process.env.NODE_ENV = 'unittest'; 26 | 27 | var mqlight = require('../mqlight'); 28 | var testCase = require('nodeunit').testCase; 29 | var stubproton = require('./stubs/stubproton'); 30 | var Promise = require('bluebird'); 31 | 32 | 33 | /** 34 | * Test a calling client.subscribe(...) with too few arguments (no arguments) 35 | * causes an Error to be thrown. 36 | * @param {object} test the unittest interface 37 | */ 38 | module.exports.test_subscribe_too_few_arguments = function(test) { 39 | var client = mqlight.createClient({id: 'test_subscribe_too_few_arguments', 40 | service: 'amqp://host'}); 41 | client.on('started', function() { 42 | test.throws(function() { 43 | client.subscribe(); 44 | }, TypeError); 45 | client.stop(); 46 | test.done(); 47 | }); 48 | }; 49 | 50 | 51 | /** 52 | * Test that calling client.subscribe(...) with too many arguments results in 53 | * the additional arguments being ignored. 54 | * @param {object} test the unittest interface 55 | */ 56 | module.exports.test_subscribe_too_many_arguments = function(test) { 57 | var client = mqlight.createClient({id: 'test_subscribe_too_many_arguments', 58 | service: 'amqp://host'}); 59 | client.on('started', function() { 60 | test.doesNotThrow(function() { 61 | client.subscribe('/foo', 'share1', {}, function() {}, 'stowaway'); 62 | }); 63 | client.stop(); 64 | test.done(); 65 | }); 66 | }; 67 | 68 | 69 | /** 70 | * Test that the callback argument to client.subscribe(...) must be a function 71 | * @param {object} test the unittest interface 72 | */ 73 | module.exports.test_subscribe_callback_must_be_function = function(test) { 74 | var client = mqlight.createClient({id: 75 | 'test_subscribe_callback_must_be_function', 76 | service: 'amqp://host'}); 77 | client.on('started', function() { 78 | test.throws(function() { 79 | client.subscribe('/foo1', 'share', {}, 7); 80 | }, TypeError); 81 | test.doesNotThrow(function() { 82 | client.subscribe('/foo2', function() {}); 83 | }); 84 | test.doesNotThrow(function() { 85 | client.subscribe('/foo3', 'share', function() {}); 86 | }); 87 | test.doesNotThrow(function() { 88 | client.subscribe('/foo4', 'share', {}, function() {}); 89 | }); 90 | client.stop(); 91 | test.done(); 92 | }); 93 | }; 94 | 95 | 96 | /** 97 | * Test that subscribe correctly interprets its parameters. This can be 98 | * tricky for two and three parameter invocations where there is the 99 | * potential for ambiguity between what is a share name and what is the 100 | * options object. 101 | * @param {object} test the unittest interface 102 | */ 103 | module.exports.test_subscribe_parameters = function(test) { 104 | var service = 'amqp://host:5672'; 105 | var pattern = '/pattern'; 106 | var currentCallbackInvocations = 0; 107 | var expectedCallbackInvocations = 0; 108 | var cb = function() { 109 | ++currentCallbackInvocations; 110 | }; 111 | var share = 'share'; 112 | var object = {}; 113 | 114 | // Data to drive the test with. 'args' is the argument list to pass into 115 | // the subscribe function. The 'share', 'object' and 'callback' properties 116 | // indicate the expected interpretation of 'args'. 117 | var data = [ 118 | {args: [pattern + '0']}, 119 | {args: [pattern + '1', cb], callback: cb}, 120 | {args: [pattern + '2', share], share: share}, 121 | {args: [pattern + '3', object], object: object}, 122 | {args: [pattern + '4', share, cb], share: share, callback: cb}, 123 | {args: [pattern + '5', object, cb], object: object, callback: cb}, 124 | {args: [pattern + '6', share, object], share: share, object: object}, 125 | {args: [pattern + '7', 7], share: 7}, 126 | {args: [pattern + '8', 'boo'], share: 'boo'}, 127 | {args: [pattern + '9', {}], object: {}}, 128 | {args: [pattern + '10', 7, cb], share: 7, callback: cb}, 129 | {args: [pattern + '11', {}, cb], object: {}, callback: cb}, 130 | {args: [pattern + '12', [], []], share: [], object: []}, 131 | {args: [pattern + '13', share, object, cb], share: share, object: object, 132 | callback: cb} 133 | ]; 134 | 135 | // Count up the expected number of callback invocations, so the test can 136 | // wait for these to complete. 137 | for (var i = 0; i < data.length; ++i) { 138 | if (data[i].callback) ++expectedCallbackInvocations; 139 | } 140 | 141 | // Replace the messenger subscribe method with our own implementation 142 | // that simply records the address that mqlight.js tries to subscribe to. 143 | var lastSubscribedAddress; 144 | 145 | var client = mqlight.createClient({id: 'test_subscribe_parameters', service: 146 | service}); 147 | var savedSubscribeFunction = client._messenger.createReceiver; 148 | client._messenger.createReceiver = function(address) { 149 | lastSubscribedAddress = address; 150 | return savedSubscribeFunction(); 151 | }; 152 | client.on('started', function() { 153 | for (var i = 0; i < data.length; ++i) { 154 | lastSubscribedAddress = undefined; 155 | client.subscribe.apply(client, data[i].args); 156 | 157 | var expectedAddress = 158 | ((data[i].share) ? ('share:' + data[i].share + ':') : 'private:') + 159 | pattern + i; 160 | 161 | test.deepEqual(lastSubscribedAddress, expectedAddress); 162 | } 163 | 164 | // Restore the saved messenger subscribe implementation 165 | client._messenger.createReceiver = savedSubscribeFunction; 166 | client.stop(); 167 | }); 168 | 169 | // Callbacks passed into subscribe(...) are scheduled to be run once 170 | // outside of the main loop - so use setImmediate(...) to schedule checking 171 | // for test completion. 172 | var testIsDone = function() { 173 | if (currentCallbackInvocations === expectedCallbackInvocations) { 174 | test.done(); 175 | } else { 176 | setImmediate(testIsDone); 177 | } 178 | }; 179 | testIsDone(); 180 | }; 181 | 182 | 183 | /** 184 | * Test that the callback (invoked when the subscribe operation completes 185 | * successfully) specifies the right number of arguments, and is invoked with 186 | * 'this' set to reference the client that the corresponding invocation of 187 | * subscribe(...) was made against. 188 | * @param {object} test the unittest interface 189 | */ 190 | module.exports.test_subscribe_ok_callback = function(test) { 191 | var client = mqlight.createClient({id: 'test_subscribe_ok_callback', service: 192 | 'amqp://host'}); 193 | client.on('started', function() { 194 | client.subscribe('/foo', function() { 195 | test.equals(arguments.length, 3); 196 | test.deepEqual(arguments[0], null); // error argument 197 | test.deepEqual(arguments[1], '/foo'); // topic pattern 198 | test.equals(arguments[2], undefined); // share name 199 | test.ok(this === client); 200 | client.stop(); 201 | test.done(); 202 | }); 203 | }); 204 | }; 205 | 206 | 207 | /** 208 | * Test that the callback (invoked when the subscribe operation completes 209 | * unsuccessfully) specifies the right number of arguments, and is invoked 210 | * with 'this' set to reference the client that the corresponding invocation 211 | * of subscribe(...) was made against. 212 | * @param {object} test the unittest interface 213 | */ 214 | module.exports.test_subscribe_fail_callback = function(test) { 215 | var client = mqlight.createClient({id: 'test_subscribe_fail_callback', 216 | service: 'amqp://host'}); 217 | 218 | // Replace the messenger subscribe method with our own implementation. 219 | var savedSubscribeFunction = client._messenger.createReceiver; 220 | client._messenger.createReceiver = function() { 221 | return new Promise(function(resolve, reject) { 222 | var err = new TypeError('topic space on fire'); 223 | reject(err); 224 | }); 225 | }; 226 | 227 | client.on('started', function() { 228 | client.subscribe('/foo', 'share', function(err) { 229 | test.ok(err instanceof TypeError); 230 | test.equals(arguments.length, 3); 231 | test.deepEqual(arguments[1], '/foo'); 232 | test.deepEqual(arguments[2], 'share'); 233 | test.ok(this === client); 234 | 235 | client._messenger.createReceiver = savedSubscribeFunction; 236 | test.done(); 237 | setImmediate(function() { 238 | client.stop(); 239 | }); 240 | }); 241 | }); 242 | }; 243 | 244 | 245 | /** 246 | * Test that trying to establish a subscription, while the client is in 247 | * stopped state, throws an Error. 248 | * @param {object} test the unittest interface 249 | */ 250 | module.exports.test_subscribe_when_stopped = function(test) { 251 | var opts = { 252 | id: 'test_subscribe_when_stopped', 253 | service: 'amqp://host' 254 | }; 255 | var client = mqlight.createClient(opts, function() { 256 | client.stop(function() { 257 | test.throws(function() { 258 | client.subscribe('/foo'); 259 | }, mqlight.StoppedError); 260 | test.done(); 261 | }); 262 | }); 263 | }; 264 | 265 | 266 | /** 267 | * Test that calling the subscribe(...) method returns, as a value, the client 268 | * object that the method was invoked on (for method chaining purposes). 269 | * @param {object} test the unittest interface 270 | */ 271 | module.exports.test_subscribe_returns_client = function(test) { 272 | var client = mqlight.createClient({id: 'test_subscribe_returns_client', 273 | service: 'amqp://host'}); 274 | client.on('started', function() { 275 | test.deepEqual(client.subscribe('/foo'), client); 276 | client.stop(); 277 | test.done(); 278 | }); 279 | }; 280 | 281 | 282 | /** 283 | * Test a variety of valid and invalid patterns. Invalid patterns 284 | * should result in the client.subscribe(...) method throwing a TypeError. 285 | * @param {object} test the unittest interface 286 | */ 287 | module.exports.test_subscribe_topics = function(test) { 288 | var data = [{valid: false, pattern: ''}, 289 | {valid: false, pattern: undefined}, 290 | {valid: false, pattern: null}, 291 | {valid: true, pattern: 1234}, 292 | {valid: true, pattern: function() {}}, 293 | {valid: true, pattern: 'kittens'}, 294 | {valid: true, pattern: '/kittens'}, 295 | {valid: true, pattern: '+'}, 296 | {valid: true, pattern: '#'}, 297 | {valid: true, pattern: '/#'}, 298 | {valid: true, pattern: '/+'}]; 299 | 300 | var client = mqlight.createClient({id: 'test_subscribe_topics', service: 301 | 'amqp://host'}); 302 | client.on('started', function() { 303 | for (var i = 0; i < data.length; ++i) { 304 | if (data[i].valid) { 305 | test.doesNotThrow(function() { 306 | client.subscribe(data[i].pattern); 307 | }); 308 | } else { 309 | test.throws(function() { 310 | client.subscribe(data[i].pattern); 311 | }, TypeError, 'pattern should have been rejected: ' + data[i].pattern); 312 | } 313 | } 314 | client.stop(function() { 315 | test.done(); 316 | }); 317 | }); 318 | }; 319 | 320 | 321 | /** 322 | * Tests a variety of valid and invalid share names to check that they are 323 | * accepted or rejected (by throwing an Error) as appropriate. 324 | * @param {object} test the unittest interface 325 | */ 326 | module.exports.test_subscribe_share_names = function(test) { 327 | var data = [{valid: true, share: 'abc'}, 328 | {valid: true, share: 7}, 329 | {valid: false, share: ':'}, 330 | {vaild: false, share: 'a:'}, 331 | {valid: false, share: ':a'}]; 332 | 333 | var client = mqlight.createClient({id: 'test_subscribe_share_names', service: 334 | 'amqp://host'}); 335 | client.on('started', function() { 336 | for (var i = 0; i < data.length; ++i) { 337 | if (data[i].valid) { 338 | test.doesNotThrow(function() { 339 | client.subscribe('/foo', data[i].share); 340 | }); 341 | } else { 342 | test.throws(function() { 343 | client.subscribe('/foo', data[i].share); 344 | }, mqlight.InvalidArgumentError, i); 345 | } 346 | } 347 | client.stop(function() { 348 | test.done(); 349 | }); 350 | }); 351 | }; 352 | 353 | 354 | /** 355 | * Test a variety of valid and invalid options values. Invalid options 356 | * should result in the client.subscribe(...) method throwing a TypeError. 357 | *

    358 | * Note that this test just checks that the options parameter is only 359 | * accepted when it is of the correct type. The actual validation of 360 | * individual options will be in separate tests. 361 | * @param {object} test the unittest interface 362 | */ 363 | module.exports.test_subscribe_options = function(test) { 364 | var data = [{valid: false, options: ''}, 365 | {valid: true, options: undefined}, 366 | {valid: true, options: null}, 367 | {valid: false, options: function() {}}, 368 | {valid: false, options: '1'}, 369 | {valid: false, options: 2}, 370 | {valid: false, options: true}, 371 | {valid: true, options: {}}, 372 | {valid: true, options: data}, 373 | {valid: true, options: { a: 1 } }]; 374 | 375 | var client = mqlight.createClient({id: 'test_subscribe_options', service: 376 | 'amqp://host'}); 377 | client.on('started', function() { 378 | for (var i = 0; i < data.length; ++i) { 379 | if (data[i].valid) { 380 | test.doesNotThrow( 381 | function() { 382 | client.subscribe('testpattern' + i, 'share', data[i].options, 383 | function() {}); 384 | } 385 | ); 386 | } else { 387 | test.throws( 388 | function() { 389 | client.subscribe('testpattern' + i, 'share', data[i].options, 390 | function() {}); 391 | }, 392 | TypeError, 393 | 'options should have been rejected: ' + data[i].options 394 | ); 395 | } 396 | } 397 | client.stop(function() { 398 | test.done(); 399 | }); 400 | }); 401 | }; 402 | 403 | 404 | /** 405 | * Test a variety of valid and invalid QoS options. Invalid QoS values 406 | * should result in the client.subscribe(...) method throwing a TypeError. 407 | * @param {object} test the unittest interface 408 | */ 409 | module.exports.test_subscribe_qos = function(test) { 410 | var number = Number(0); 411 | var data = [{valid: false, qos: ''}, 412 | {valid: false, qos: undefined}, 413 | {valid: false, qos: null}, 414 | {valid: false, qos: function() {}}, 415 | {valid: false, qos: '1'}, 416 | {valid: false, qos: 2}, 417 | {valid: true, qos: 0}, 418 | {valid: true, qos: 1}, 419 | {valid: true, qos: number}, 420 | {valid: true, qos: 9 - 8}, 421 | {valid: true, qos: mqlight.QOS_AT_MOST_ONCE}, 422 | {valid: true, qos: mqlight.QOS_AT_LEAST_ONCE}]; 423 | 424 | var client = mqlight.createClient({id: 'test_subscribe_qos', service: 425 | 'amqp://host'}); 426 | client.on('started', function() { 427 | for (var i = 0; i < data.length; ++i) { 428 | var opts = { qos: data[i].qos }; 429 | if (data[i].valid) { 430 | test.doesNotThrow(function() { 431 | client.subscribe('testpattern' + i, opts); 432 | }); 433 | } else { 434 | test.throws(function() { 435 | client.subscribe('testpattern' + i, opts); 436 | }, RangeError, 'qos should have been rejected: ' + data[i].qos); 437 | } 438 | } 439 | client.stop(function() { 440 | test.done(); 441 | }); 442 | }); 443 | }; 444 | 445 | 446 | /** 447 | * Test a variety of valid and invalid autoConfirm options. Invalid 448 | * autoConfirm values should result in the client.subscribe(...) method 449 | * throwing a TypeError. 450 | * 451 | * @param {object} test the unittest interface 452 | */ 453 | module.exports.test_subscribe_autoConfirm = function(test) { 454 | var a = Boolean(true); 455 | var data = [{valid: false, opts: { autoConfirm: '' } }, 456 | {valid: false, opts: { autoConfirm: undefined } }, 457 | {valid: false, opts: { autoConfirm: null } }, 458 | {valid: false, opts: { autoConfirm: function() {} } }, 459 | {valid: false, opts: { autoConfirm: 'true'} }, 460 | {valid: false, opts: { autoConfirm: 'false'} }, 461 | {valid: false, opts: { autoConfirm: 2 } }, 462 | {valid: true, opts: { autoConfirm: true } }, 463 | {valid: true, opts: { autoConfirm: false } }, 464 | {valid: true, opts: { qos: 0, autoConfirm: true } }, 465 | {valid: true, opts: { qos: 0, autoConfirm: false } }, 466 | {valid: true, opts: { qos: 1, autoConfirm: true } }, 467 | {valid: true, opts: { qos: 1, autoConfirm: false } }, 468 | {valid: true, opts: { autoConfirm: 1 == 1 } }, 469 | {valid: true, opts: { autoConfirm: 'abc' == 'abc' } }, 470 | {valid: true, opts: { autoConfirm: a } }]; 471 | 472 | var client = mqlight.createClient({id: 'test_subscribe_autoConfirm', service: 473 | 'amqp://host'}); 474 | client.on('started', function() { 475 | for (var i = 0; i < data.length; ++i) { 476 | if (data[i].valid) { 477 | test.doesNotThrow(function() { 478 | client.subscribe('testpattern' + i, data[i].opts); 479 | }); 480 | } else { 481 | test.throws(function() { 482 | client.subscribe('testpattern' + i, data[i].opts); 483 | }, TypeError, 'autoConfirm should have been rejected: ' + data[i].opts); 484 | } 485 | } 486 | client.stop(function() { 487 | test.done(); 488 | }); 489 | }); 490 | }; 491 | 492 | 493 | /** 494 | * Test a variety of valid and invalid ttl options. Invalid ttl values should 495 | * result in the client.subscribe(...) method throwing a TypeError. 496 | * 497 | * @param {object} test the unittest interface 498 | */ 499 | module.exports.test_subscribe_ttl_validity = function(test) { 500 | var data = [ 501 | {valid: false, ttl: undefined}, 502 | {valid: false, ttl: function() {}}, 503 | {valid: false, ttl: -9007199254740992}, 504 | {valid: false, ttl: -NaN}, 505 | {valid: false, ttl: NaN}, 506 | {valid: false, ttl: -Infinity}, 507 | {valid: false, ttl: Infinity}, 508 | {valid: false, ttl: -1}, 509 | {valid: true, ttl: 0}, 510 | {valid: true, ttl: 1}, 511 | {valid: true, ttl: 9 - 8}, 512 | {valid: true, ttl: 9007199254740992}, 513 | {valid: true, ttl: null}, // treated as 0 514 | {valid: true, ttl: ''} // treated as 0 515 | ]; 516 | 517 | var client = mqlight.createClient({id: 'test_subscribe_ttl_validity', 518 | service: 'amqp://host'}); 519 | client.on('started', function() { 520 | for (var i = 0; i < data.length; ++i) { 521 | var opts = { ttl: data[i].ttl }; 522 | if (data[i].valid) { 523 | test.doesNotThrow(function() { 524 | client.subscribe('testpattern' + i, opts); 525 | }); 526 | } else { 527 | test.throws(function() { 528 | client.subscribe('testpattern' + i, opts); 529 | }, RangeError, 'ttl should have been rejected: ' + data[i].ttl); 530 | } 531 | } 532 | client.stop(function() { 533 | test.done(); 534 | }); 535 | }); 536 | }; 537 | 538 | 539 | /** 540 | * Test a variety of ttl options are correctly rounded to the nearest second 541 | * before being passed to the messenger.subscribe request. 542 | * 543 | * @param {object} test the unittest interface 544 | */ 545 | module.exports.test_subscribe_ttl_rounding = function(test) { 546 | var data = [ 547 | {ttl: 0, rounded: 0}, 548 | {ttl: 1, rounded: 0}, 549 | {ttl: 499, rounded: 0}, 550 | {ttl: 500, rounded: 1}, 551 | {ttl: 1000, rounded: 1}, 552 | {ttl: 66149, rounded: 66}, 553 | {ttl: 9007199254740992, rounded: 9007199254741} 554 | ]; 555 | 556 | var client = mqlight.createClient({id: 'test_subscribe_ttl_rounding', 557 | service: 'amqp://host'}); 558 | 559 | var savedSubscribeFunction = client._messenger.createReceiver; 560 | var subscribedTtl = -1; 561 | client._messenger.createReceiver = function(address, link) { 562 | subscribedTtl = link.attach.source.timeout; 563 | return savedSubscribeFunction(address, link); 564 | }; 565 | 566 | client.on('started', function() { 567 | for (var i = 0; i < data.length; ++i) { 568 | var opts = { ttl: data[i].ttl }; 569 | test.doesNotThrow(function() { 570 | client.subscribe('testpattern' + i, opts); 571 | test.equal(subscribedTtl, data[i].rounded, 'ttl should have been ' + 572 | 'rounded to ' + data[i].rounded + ' not ' + subscribedTtl); 573 | }); 574 | } 575 | client._messenger.createReceiver = savedSubscribeFunction; 576 | client.stop(function() { 577 | test.done(); 578 | }); 579 | }); 580 | }; 581 | 582 | 583 | /** 584 | * Tests that passing a variety of credit values to the subscribe method 585 | * results in invalid values being rejected and valid values being passed 586 | * through to the Proton Messenger mock. 587 | * 588 | * @param {object} test the unittest interface 589 | */ 590 | module.exports.test_subscribe_credit_values = function(test) { 591 | var data = [ 592 | {credit: undefined, expected: 1024}, // default value 593 | {credit: 0, expected: 0}, 594 | {credit: 1, expected: 1}, 595 | {credit: 2, expected: 2}, 596 | {credit: NaN}, 597 | {credit: -1}, 598 | {credit: -2}, 599 | {credit: -NaN}, 600 | {credit: 4294967294, expected: 4294967294}, 601 | {credit: 4294967295, expected: 4294967295}, 602 | {credit: 4294967296}, 603 | {credit: '4294967294', expected: 4294967294}, 604 | {credit: '4294967295', expected: '4294967295'}, 605 | {credit: '4294967296'}, 606 | {credit: '0', expected: 0}, 607 | {credit: '1', expected: 1}, 608 | {credit: '-1'}, 609 | {credit: "Credit is a system whereby a person who can't pay, gets " + 610 | "another person who can't pay to guarantee that he can pay"}, 611 | {credit: function() {}}, 612 | {credit: true, expected: 1}, 613 | {credit: false, expected: 0} 614 | ]; 615 | 616 | var client = mqlight.createClient({id: 'test_subscribe_credit_values', 617 | service: 'amqp://host'}); 618 | 619 | var savedSubscribeFunction = client._messenger.createReceiver; 620 | var subscribedCredit; 621 | client._messenger.createReceiver = function(address, link) { 622 | return new Promise(function(resolve, reject) { 623 | subscribedCredit = link.creditQuantum; 624 | resolve(stubproton.receiver); 625 | }); 626 | }; 627 | 628 | var runTests = function(i) { 629 | if (i === data.length) { 630 | client._messenger.createReceiver = savedSubscribeFunction; 631 | client.stop(function() { 632 | test.done(); 633 | }); 634 | return; 635 | } 636 | 637 | subscribedCredit = 0; 638 | if (data[i].expected !== undefined) { 639 | test.doesNotThrow(function() { 640 | client.subscribe('testpattern' + i, 641 | data[i].credit !== undefined ? data[i] : {}, 642 | function() { 643 | test.deepEqual(subscribedCredit, data[i].expected, 644 | 'wrong value passed to proton messenger - test ' + 645 | 'data index: ' + i + ' expected: ' + 646 | data[i].expected + ' actual : ' + 647 | subscribedCredit); 648 | runTests(i + 1); 649 | } 650 | ); 651 | }, undefined, 'test data index: ' + i); 652 | } else { 653 | test.throws(function() { 654 | client.subscribe('testpattern', data[i]); 655 | }, RangeError, 'test data index: ' + i); 656 | runTests(i + 1); 657 | } 658 | }; 659 | 660 | client.on('started', function() { 661 | runTests(0); 662 | }); 663 | }; 664 | 665 | 666 | /** 667 | * This is a simple test to confirm that a client that attempts to subscribe, 668 | * but which has been replaced by another client with the same id, gets the 669 | * ReplacedError 670 | * 671 | * @param {object} test the unittest interface 672 | */ 673 | module.exports.test_subscribe_client_replaced = function(test) { 674 | var client = mqlight.createClient({ 675 | service: 'amqp://host', 676 | id: 'test_subscribe_client_replaced' 677 | }); 678 | 679 | var savedSubscribeFunction = client._messenger.createReceiver; 680 | client._messenger.createReceiver = function() { 681 | return new Promise(function(resolve, reject) { 682 | var err = new Error('CONNECTION ERROR (ServerContext_Takeover)'); 683 | err.name = 'ReplacedError'; 684 | reject(err); 685 | }); 686 | }; 687 | client.on('error', function() {}); 688 | 689 | client.once('started', function() { 690 | test.ok(this === client); 691 | test.equals(arguments.length, 0); 692 | test.equals(client.state, 'started'); 693 | test.equals(client._messenger.stopped, false); 694 | 695 | client.subscribe('topic', 'share', function(err) { 696 | test.ok(err instanceof Error); 697 | // FIXME: how should ReplacedError surface here? 698 | // test.ok(err instanceof mqlight.ReplacedError); 699 | test.ok(/ReplacedError: /.test(err.toString())); 700 | client._messenger.createReceiver = savedSubscribeFunction; 701 | client.stop(); 702 | test.done(); 703 | }); 704 | }); 705 | }; 706 | 707 | 708 | /** 709 | * Test that an attempt to subscribe twice results in a 710 | * SubscribedError being thrown. 711 | * 712 | * @param {object} test the unittest interface 713 | */ 714 | module.exports.test_subscribe_client_twice = function(test) { 715 | var client = mqlight.createClient({ 716 | service: 'amqp://host', 717 | id: 'test_subscribe_client_twice' 718 | }, function(err) { 719 | test.ok(!err); 720 | client.subscribe('topic', function(err) { 721 | test.ok(!err); 722 | test.throws(function () { 723 | client.subscribe('topic'); 724 | }, mqlight.SubscribedError); 725 | client.stop(); 726 | test.done(); 727 | }); 728 | }); 729 | }; 730 | -------------------------------------------------------------------------------- /test/testrestart.js: -------------------------------------------------------------------------------- 1 | /* %Z% %W% %I% %E% %U% */ 2 | /* 3 | * 8 | * Licensed Materials - Property of IBM 9 | * 10 | * 5725-P60 11 | * 12 | * (C) Copyright IBM Corp. 2016 13 | * 14 | * US Government Users Restricted Rights - Use, duplication or 15 | * disclosure restricted by GSA ADP Schedule Contract with 16 | * IBM Corp. 17 | * 18 | */ 19 | /* jslint node: true */ 20 | /* jshint -W083,-W097 */ 21 | 'use strict'; 22 | 23 | 24 | /** @const {string} enable unittest mode in mqlight.js */ 25 | process.env.NODE_ENV = 'unittest'; 26 | 27 | var stubproton = require('./stubs/stubproton'); 28 | var mqlight = require('../mqlight'); 29 | var testCase = require('nodeunit').testCase; 30 | var util = require('util'); 31 | var Promise = require('bluebird'); 32 | 33 | 34 | /** 35 | * Golden path for restart checking state changes. 36 | * @constructor 37 | * @param {object} test the unittest interface 38 | */ 39 | /* 40 | module.exports.test_successful_restart = function(test) { 41 | var client = mqlight.createClient({id: 'test_successful_restart', service: 42 | 'amqp://host'}); 43 | var timeout = setTimeout(function() { 44 | test.ok(false, 'Test timed out waiting for events to be emitted'); 45 | if (client) client.stop(); 46 | test.done(); 47 | }, 5000); 48 | 49 | client.on('started', function(err) { 50 | test.deepEqual(client.state, 'started', 51 | 'client status started after start'); 52 | stubproton.setConnectStatus(2); 53 | setImmediate(function() { 54 | mqlight.reconnect(client); 55 | }); 56 | }); 57 | 58 | client.on('error', function(err) { 59 | test.deepEqual(client.state, 'retrying', 60 | 'client in retrying state after error'); 61 | stubproton.setConnectStatus(0); 62 | }); 63 | 64 | client.on('restarted', function() { 65 | test.deepEqual(client.state, 'started', 'client has restarted'); 66 | clearTimeout(timeout); 67 | client.stop(function() { 68 | test.done(); 69 | }); 70 | }); 71 | }; 72 | */ 73 | 74 | 75 | 76 | /** 77 | * Check the client returns undefined if the reconnect method is invoked 78 | * while the client is in stopped state. Note that the reconnect method 79 | * is an internal part of the client, and only exposed for unit testing. 80 | * @constructor 81 | * @param {object} test the unittest interface 82 | */ 83 | module.exports.test_reconnect_when_disconnected = function(test) { 84 | test.expect(1); 85 | var opts = { 86 | id: 'test_reconnect_when_disconnected', 87 | service: 'amqp://host' 88 | }; 89 | var client = mqlight.createClient(opts, function() { 90 | client.stop(function() { 91 | test.equals(mqlight.reconnect(client), undefined, 92 | 'reconnect when in stopped state returns undefined'); 93 | test.done(); 94 | }); 95 | }); 96 | }; 97 | 98 | 99 | 100 | /** 101 | * Test multiple reconnect calls only cause a single restarted 102 | * event. Note that the reconnect method is an internal part of the client, 103 | * and only exposed for unit testing. 104 | * @constructor 105 | * @param {object} test the unittest interface 106 | */ 107 | module.exports.test_multi_restart_call = function(test) { 108 | var client = mqlight.createClient({id: 'test_multi_restart_call', service: 109 | 'amqp://host'}); 110 | var reconnectedEvents = 0; 111 | var timeout = setTimeout(function() { 112 | test.ok(false, 'Test timed out waiting for events to be emitted'); 113 | if (client) client.stop(); 114 | test.done(); 115 | }, 5000); 116 | client.on('started', function() { 117 | stubproton.setConnectStatus(1); 118 | mqlight.reconnect(client); 119 | mqlight.reconnect(client); 120 | mqlight.reconnect(client); 121 | }); 122 | client.on('error', function() { 123 | // second reconnect call should return immediately 124 | test.deepEqual(mqlight.reconnect(client).state, 'retrying'); 125 | stubproton.setConnectStatus(0); 126 | }); 127 | 128 | client.on('restarted', function() { 129 | reconnectedEvents++; 130 | test.equals(client.state, 'started', 131 | 'client state started after restart'); 132 | setTimeout(function() { 133 | test.equals(reconnectedEvents, 1, 'reconnected event happened once'); 134 | clearTimeout(timeout); 135 | client.stop(function() { 136 | test.done(); 137 | }); 138 | },1000); 139 | }); 140 | }; 141 | 142 | 143 | 144 | /** 145 | * Test the subscription list is emptied and re-populated when the client 146 | * restarts 147 | * @constructor 148 | * @param {object} test the unittest interface 149 | */ 150 | module.exports.test_resubscribe_on_restart = function(test) { 151 | var client = mqlight.createClient({id: 'test_resubscribe_on_restart', 152 | service: 'amqp://host'}); 153 | 154 | var timeout = setTimeout(function() { 155 | test.ok(false, 'Test timed out waiting for events to be emitted'); 156 | if (client) client.stop(function() { 157 | test.done(); 158 | }); 159 | }, 5000); 160 | 161 | var connectErrors = 0; 162 | client.on('error', function(err) { 163 | if (/connect error: 1/.test(err.message)) { 164 | connectErrors++; 165 | test.ok(client._subscriptions.length === 0, 'subs list has not ' + 166 | 'been cleared'); 167 | test.equal(client._queuedSubscriptions.length, 3, 'subs have ' + 168 | 'not been queued'); 169 | stubproton.setConnectStatus(0); 170 | } 171 | }); 172 | 173 | var origSubsList = []; 174 | client.once('started', function() { 175 | client.subscribe('/topic', 'myshare', function(err) { 176 | test.ifError(err); 177 | if (connectErrors > 0) return; 178 | client.subscribe('/another/topic', function(err) { 179 | test.ok(!err); 180 | if (connectErrors > 0) return; 181 | client.subscribe('/final/topic/', 'diffshare', function(err) { 182 | test.ok(!err); 183 | if (connectErrors > 0) { 184 | if (err) return; 185 | test.equal(client._subscriptions.length, origSubsList.length, 186 | 'after reconnect subs lists does not match original'); 187 | while (client._subscriptions.length > 0) { 188 | var expected = origSubsList.pop(); 189 | expected.callback = undefined; 190 | var actual = client._subscriptions.pop(); 191 | actual.callback = undefined; 192 | test.deepEqual(actual, expected, 'sub list objects do not match'); 193 | } 194 | clearTimeout(timeout); 195 | client.stop(function() { 196 | test.done(); 197 | }); 198 | } 199 | if (connectErrors === 0) { 200 | setImmediate(function() { 201 | origSubsList = origSubsList.concat(client._subscriptions); 202 | stubproton.setConnectStatus(1); 203 | mqlight.reconnect(client); 204 | }); 205 | } 206 | }); 207 | }); 208 | }); 209 | }); 210 | 211 | client.once('restarted', function() { 212 | // this allows the restarted callback to get in and re-subscribe 213 | setImmediate(function() { 214 | test.equal(3, origSubsList.length, 'origSubsList length is wrong'); 215 | }); 216 | }); 217 | }; 218 | 219 | 220 | 221 | /** 222 | * Stop while retrying behaves as expected 223 | * @constructor 224 | * @param {object} test the unittest interface 225 | */ 226 | module.exports.test_stop_while_restarting = function(test) { 227 | var client = mqlight.createClient({id: 'test_stop_while_restarting', 228 | service: 'amqp://host'}); 229 | 230 | var timeout = setTimeout(function() { 231 | test.ok(false, 'Test timed out waiting for events to be emitted'); 232 | if (client) client.stop(); 233 | test.done(); 234 | }, 5000); 235 | 236 | client.once('started', function(x, y) { 237 | stubproton.setConnectStatus(1); 238 | mqlight.reconnect(client); 239 | }); 240 | 241 | client.once('error', function(x, y) { 242 | client.stop(); 243 | }); 244 | 245 | client.once('restarted', function(x, y) { 246 | test.ok(false, 'should not have restarted'); 247 | }); 248 | 249 | client.once('stopped', function(x, y) { 250 | test.deepEqual(client.state, 'stopped', 'state disconected'); 251 | // Set connect state to 0 and wait a second in case of restart 252 | stubproton.setConnectStatus(0); 253 | setTimeout(function() { 254 | clearTimeout(timeout); 255 | client.removeAllListeners(); 256 | client.stop(function() { 257 | test.done(); 258 | }); 259 | },1000); 260 | }); 261 | }; 262 | 263 | 264 | /** 265 | * Test that an error during send result in the queueing of an 266 | * AT_LEAST_ONCE message. Then when reconnected this gets sent 267 | * and the queue of messages to send is 0. 268 | * @param {object} test the unittest interface 269 | */ 270 | //module.exports.test_single_queued_send = function(test) { 271 | // //test.expect(4); 272 | // var client = mqlight.createClient({id: 'test_single_queued_send', service: 273 | // 'amqp://host'}); 274 | // var savedSendFunction = stubproton.sender.send; 275 | // var reconnected = 0; 276 | // stubproton.sender.send = function() { 277 | // return new Promise(function(resolve, reject) { 278 | // reject(new Error('error during send')); 279 | // }); 280 | // }; 281 | // 282 | // var timeout = setTimeout(function() { 283 | // test.ok(false, 'Test timed out waiting for events to be emitted'); 284 | // stubproton.sender.send = savedSendFunction; 285 | // if (client) client.stop(); 286 | // test.done(); 287 | // }, 5000); 288 | // 289 | // var opts = {qos: mqlight.QOS_AT_LEAST_ONCE}; 290 | // client.once('started', function(err) { 291 | // stubproton.setConnectStatus(1); 292 | // client.send('test', 'message', opts, function(err) { 293 | // // This callback should only happen after the restart event is emitted 294 | // test.equals(reconnected, 1, 'has reconnected'); 295 | // test.deepEqual(client.state, 'started', 'state is started'); 296 | // test.equals(client._queuedSends.length, 0, 'queued sends now 0'); 297 | // clearTimeout(timeout); 298 | // client.stop(function() { 299 | // test.done(); 300 | // }); 301 | // }); 302 | // }); 303 | // 304 | // client.once('error', function(err) { 305 | // stubproton.setConnectStatus(0); 306 | // test.equals(client._queuedSends.length, 1, 'check for queued send'); 307 | // }); 308 | // 309 | // client.on('restarted', function(x, y) { 310 | // reconnected++; 311 | // stubproton.sender.send = savedSendFunction; 312 | // }); 313 | //}; 314 | 315 | 316 | /** 317 | * Test that when in a retrying state that any attempted 318 | * sends are queued and then go through following a restart event 319 | * in the expected order. 320 | * 321 | * @param {object} test the unittest interface. 322 | */ 323 | module.exports.test_queued_sends_retrying = function(test) { 324 | test.expect(); 325 | var client = mqlight.createClient({id: 'test_queued_sends_retrying', service: 326 | 'amqp://host'}); 327 | var callbacksCalled = 0; 328 | var callbacksCalledInError = 0; 329 | var sentMessages = []; 330 | 331 | client.on('stopped', function() { 332 | test.equal(client._queuedSends.length, 0, 'expected empty queued sends'); 333 | test.equal(callbacksCalled, 4, 'expected 4 callbacks called with success'); 334 | test.equal(callbacksCalledInError, 0, 'expected 0 callbacks in error'); 335 | test.equal(sentMessages.length, 4, 'expected 4 successfully sent messages'); 336 | for (var i = 0; i < sentMessages.length; i++) { 337 | test.equal(sentMessages[i], 'message ' + i, 'message sent out of order'); 338 | } 339 | test.done(); 340 | }); 341 | 342 | client.on('error', function(err) { 343 | test.deepEqual(client.state, 'retrying', 344 | 'client in retrying state after error'); 345 | }); 346 | 347 | client.start(function(err) { 348 | stubproton.setConnectStatus(1); 349 | mqlight.reconnect(client); 350 | // these 4 sends should get queued 351 | for ( var i = 0; i < 4; i++ ) { 352 | client.send('topic ' + i, 'message ' + i , function(err, topic, body) { 353 | if (err) { 354 | callbacksCalledInError++; 355 | process.nextTick(function() { 356 | client.stop(); 357 | }); 358 | } else { 359 | callbacksCalled++; 360 | sentMessages.push(body); 361 | process.nextTick(function() { 362 | if (callbacksCalled >= 4) { 363 | client.stop(); 364 | } 365 | }); 366 | } 367 | }); 368 | } 369 | test.equal(client._queuedSends.length, 4, 'Expected 4 queued ' + 370 | 'sends. Found: ' + util.inspect(client._queuedSends)); 371 | stubproton.setConnectStatus(0); 372 | mqlight.reconnect(client); 373 | }); 374 | }; 375 | 376 | 377 | /** 378 | * Test that when in a retrying state that any attempted 379 | * subscribes are queued and then go through following a reconnect. 380 | * 381 | * @param {object} test the unittest interface. 382 | */ 383 | module.exports.test_queued_subs_retrying = function(test) { 384 | var client = mqlight.createClient( 385 | {id: 'test_queued_subs_retrying', service: 'amqp://host'}); 386 | 387 | var timeout = setTimeout(function() { 388 | test.ok(false, 'Test timed out waiting for events to be emitted'); 389 | if (client) client.stop(); 390 | test.done(); 391 | }, 5000); 392 | 393 | client.on('error', function(err) { 394 | if (!err instanceof mqlight.NetworkError) console.error(err); 395 | }); 396 | 397 | var successCallbacks = 0; 398 | client.once('started', function() { 399 | stubproton.setConnectStatus(1); 400 | // queue up 4 subscribes 401 | for (var i = 1; i < 5; i++) { 402 | client.subscribe('queue' + i, function(err) { 403 | if (!err) { 404 | successCallbacks++; 405 | if (successCallbacks === 4) { 406 | client.stop(); 407 | } 408 | } 409 | }); 410 | } 411 | setTimeout(function() { 412 | test.strictEqual(client._queuedSubscriptions.length, 4, 413 | 'expected to see 4 queued subscriptions, but saw ' + 414 | client._queuedSubscriptions.length); 415 | stubproton.setConnectStatus(0); 416 | }, 1000); 417 | }); 418 | 419 | client.on('stopped', function() { 420 | test.equal(successCallbacks, 4, 'expecting 4 success callbacks, saw ' + 421 | successCallbacks); 422 | clearTimeout(timeout); 423 | test.done(); 424 | }); 425 | }; 426 | 427 | 428 | /** 429 | * Test that when the client is still in the 'connecting' state, any attempted 430 | * unsubscribes are queued and then go through following a connection. 431 | * 432 | * @param {object} test the unittest interface. 433 | */ 434 | module.exports.test_queued_unsubscribe_before_connect = function(test) { 435 | var successCallbacks = 0; 436 | var onUnsubscribe = function() { 437 | successCallbacks++; 438 | if (successCallbacks === 4) { 439 | client.stop(); 440 | } 441 | }; 442 | var client = mqlight.createClient({ 443 | id: 'test_queued_unsubscribe_before_connect', 444 | service: function(callback) { 445 | test.strictEqual(client.state, 'starting'); 446 | // queue up 4 unsubscribes before allowing the connection through 447 | for (var i = 1; i < 5; i++) { 448 | client.subscribe('queue' + i); 449 | client.unsubscribe('queue' + i, onUnsubscribe); 450 | } 451 | test.strictEqual(client._queuedUnsubscribes.length, 4, 452 | 'expected to see 4 queued unsubscriptions, but saw ' + 453 | client._queuedUnsubscribes.length); 454 | callback(null, 'amqp://host'); 455 | } 456 | }); 457 | 458 | client.on('stopped', function() { 459 | test.equal(successCallbacks, 4, 'expecting 4 success callbacks, saw ' + 460 | successCallbacks); 461 | test.done(); 462 | }); 463 | }; 464 | 465 | 466 | /** 467 | * Test that when messenger.unsubscribe throws an error, any attempted 468 | * unsubscribes are queued and then go through following a reconnect. 469 | * 470 | * @param {object} test the unittest interface. 471 | */ 472 | //module.exports.test_queued_unsubscribe_via_error = function(test) { 473 | // var client = mqlight.createClient({ 474 | // id: 'test_queued_unsubscribe_via_error', 475 | // service: 'amqp://host' 476 | // }); 477 | // 478 | // var savedSubscribedFn = mqlight.proton.messenger.subscribed; 479 | // mqlight.proton.messenger.subscribed = function() { 480 | // return true; 481 | // }; 482 | // 483 | // var unsubscribeErrors = 0; 484 | // client.on('error', function(err) { 485 | // if (/error on unsubscribe/.test(err.message)) { 486 | // unsubscribeErrors++; 487 | // return; 488 | // } 489 | // 490 | // if (unsubscribeErrors === 4) { 491 | // test.strictEqual(client._queuedUnsubscribes.length, 4, 492 | // 'expected to see 4 queued unsubscriptions, but saw ' + 493 | // client._queuedUnsubscribes.length); 494 | // mqlight.proton.messenger.subscribed = savedSubscribedFn; 495 | // stubproton.setConnectStatus(0); 496 | // setTimeout(function() {client.stop();},1500); 497 | // } 498 | // }); 499 | // 500 | // var successCallbacks = 0; 501 | // client.once('started', function() { 502 | // stubproton.setConnectStatus(1); 503 | // // queue up 4 unsubscribes 504 | // for (var i = 1; i < 5; i++) { 505 | // client.subscribe('queue' + i, function(err, topicPattern, share) { 506 | // if (unsubscribeErrors >= 4) return; 507 | // client.unsubscribe(topicPattern, function(err) { 508 | // if (!err) { 509 | // successCallbacks++; 510 | // } 511 | // }); 512 | // }); 513 | // } 514 | // }); 515 | // 516 | // client.on('stopped', function() { 517 | // test.equal(successCallbacks, 4, 'expecting 4 success callbacks, saw ' + 518 | // successCallbacks); 519 | // test.done(); 520 | // }); 521 | //}; 522 | 523 | 524 | /** 525 | * Test that a queued subscribe and unsubscribe for the same address cancel 526 | * each other out. We'll do this by submitting 6 subscribes and 4 unsubscribes 527 | * which should leave just two subscribes. 528 | * 529 | * @param {object} test the unittest interface. 530 | */ 531 | module.exports.test_queued_before_connect_unsubscribe_nop = function(test) { 532 | var callbacks = 0, 533 | subscribes = 0, 534 | unsubscribes = 0; 535 | 536 | var client = mqlight.createClient({ 537 | id: 'test_queued_before_connect_unsubscribe_nop', 538 | service: function(callback) { 539 | test.strictEqual(client.state, 'starting'); 540 | // queue up 4 subscribes to queue{1,2,3,4,5,6} before allowing connection 541 | for (var i = 1; i < 7; i++) { 542 | client.subscribe('queue' + i, function() { 543 | callbacks++; 544 | }); 545 | } 546 | // queue up 4 unsubscribes to queue{2,3,4,5} before allowing connection 547 | for (var j = 2; j < 6; j++) { 548 | client.unsubscribe('queue' + j, function() { 549 | callbacks++; 550 | }); 551 | } 552 | test.strictEqual(client._queuedSubscriptions.length, 6, 553 | 'expected to see 6 queued subscriptions, but saw ' + 554 | client._queuedSubscriptions.length); 555 | test.strictEqual(client._queuedUnsubscribes.length, 4, 556 | 'expected to see 4 queued unsubscriptions, but saw ' + 557 | client._queuedUnsubscribes.length); 558 | callback(null, 'amqp://host'); 559 | } 560 | }); 561 | 562 | var savedSubscribeFunction = client._messenger.createReceiver; 563 | client._messenger.createReceiver = function(address) { 564 | ++subscribes; 565 | return savedSubscribeFunction(address); 566 | }; 567 | var savedUnsubscribeFunction = stubproton.receiver.detach; 568 | stubproton.receiver.detach = function() { 569 | ++unsubscribes; 570 | return savedUnsubscribeFunction(); 571 | }; 572 | 573 | client.once('started', function() { 574 | setTimeout(function() {client.stop();},500); 575 | }); 576 | 577 | client.on('stopped', function() { 578 | // we expect all 8 of the subscribe and unsubscribe requests to have their 579 | // callbacks called 580 | test.equal(callbacks, 10, 'expecting 10 success callbacks, but saw ' + 581 | callbacks); 582 | // but we only expect 2 subscribes and 2 unsubscribes to have required 583 | // processing 584 | test.equal(subscribes, 2, 'expecting 2 subscribes, but saw ' + 585 | subscribes); 586 | test.equal(unsubscribes, 0, 'expecting 0 unsubscribes, but saw ' + 587 | unsubscribes); 588 | client._messenger.createReceiver = savedSubscribeFunction; 589 | stubproton.receiver.detach = savedUnsubscribeFunction; 590 | test.done(); 591 | }); 592 | }; 593 | 594 | 595 | /** 596 | * Test that a queued subscribe and unsubscribe for the same address cancel 597 | * each other out. We'll do this by submitting 4 subscribes and 2 unsubscribes 598 | * where there is an intersection between two of the topics used in these 599 | * cases. 600 | * 601 | * @param {object} test the unittest interface. 602 | */ 603 | module.exports.test_queued_via_error_unsubscribe_nop = function(test) { 604 | stubproton.setConnectStatus(1); 605 | var client = mqlight.createClient({ 606 | id: 'test_queued_via_error_unsubscribe_nop', 607 | service: 'amqp://host' 608 | }); 609 | 610 | var savedSubscribeFunction = client._messenger.createReceiver; 611 | client._messenger.createReceiver = function() { 612 | return new Promise(function(resolve, reject) { 613 | reject(new Error('error on subscribe')); 614 | }); 615 | }; 616 | var savedUnsubscribeFunction = stubproton.receiver.detach; 617 | stubproton.receiver.detach = function() { 618 | return new Promise(function(resolve, reject) { 619 | reject(new Error('error on unsubscribe')); 620 | }); 621 | }; 622 | 623 | var subscribeErrors = 0, 624 | unsubscribeErrors = 0, 625 | subscribeUnsubscribeErrors = 0, 626 | subscribes = 0, 627 | unsubscribes = 0; 628 | client.on('error', function(err) { 629 | if (/error on subscribe/.test(err.message)) { 630 | subscribeErrors++; 631 | return; 632 | } 633 | if (/error on unsubscribe/.test(err.message)) { 634 | unsubscribeErrors++; 635 | return; 636 | } 637 | if (/connect error/.test(err.message)) { 638 | subscribeUnsubscribeErrors++; 639 | } 640 | 641 | if ((subscribeErrors === 4 && unsubscribeErrors === 2) || 642 | subscribeUnsubscribeErrors === 6) { 643 | test.strictEqual(client._queuedSubscriptions.length, 4, 644 | 'expected to see 4 queued subscriptions, but saw ' + 645 | client._queuedSubscriptions.length); 646 | test.strictEqual(client._queuedUnsubscribes.length, 2, 647 | 'expected to see 2 queued unsubscriptions, but saw ' + 648 | client._queuedUnsubscribes.length); 649 | client._messenger.createReceiver = function() { 650 | ++subscribes; 651 | return savedSubscribeFunction(); 652 | }; 653 | stubproton.receiver.detach = function() { 654 | ++unsubscribes; 655 | return savedUnsubscribeFunction(); 656 | }; 657 | stubproton.setConnectStatus(0); 658 | setTimeout(function() {client.stop();},500); 659 | } 660 | }); 661 | 662 | var successCallbacks = 0; 663 | // queue up 4 subscribes to queue{1,2,3,4} 664 | for (var i = 1; i < 5; i++) { 665 | client.subscribe('queue' + i, function(err) { 666 | if (!err) { 667 | successCallbacks++; 668 | } 669 | }); 670 | } 671 | // queue up 2 unsubscribes to queue{2,4} 672 | for (var j = 2; j < 5; j += 2) { 673 | client.unsubscribe('queue' + j, function(err) { 674 | if (!err) { 675 | successCallbacks++; 676 | } 677 | }); 678 | } 679 | 680 | client.on('stopped', function() { 681 | // we expect all 6 of the subscribe and unsubscribe requests to have their 682 | // callbacks called 683 | test.equal(successCallbacks, 6, 'expecting 6 success callbacks, saw ' + 684 | successCallbacks); 685 | // but we only expect 2 subscribes and 0 unsubscribes to have required 686 | // processing 687 | test.equal(subscribes, 2, 'expecting 2 subscribes, but saw ' + 688 | subscribes); 689 | test.equal(unsubscribes, 0, 'expecting 0 unsubscribes, but saw ' + 690 | unsubscribes); 691 | client._messenger.createReceiver = savedSubscribeFunction; 692 | stubproton.receiver.detach = savedUnsubscribeFunction; 693 | test.done(); 694 | }); 695 | }; 696 | 697 | 698 | /** 699 | * Test that a queued duplicate subscribe for the same address throws the 700 | * expected 'already subscribed' / 'not subscribed' error 701 | * 702 | * @param {object} test the unittest interface. 703 | */ 704 | module.exports.test_queued_double_subscribe = function(test) { 705 | var callbacks = 0, 706 | subscribes = 0; 707 | 708 | var client = mqlight.createClient({ 709 | id: 'test_queued_double_subscribe', 710 | service: function(callback) { 711 | // override client 'service' property 712 | client.service = 'amqp://host:5672'; 713 | test.strictEqual(client.state, 'starting'); 714 | // queue up 2 duplicate subscribes to queue before allowing connection 715 | client.subscribe('queue', function() { 716 | callbacks++; 717 | }); 718 | test.throws(function() { 719 | client.subscribe('queue', function() { 720 | callbacks++; 721 | }); 722 | }, function(err) { 723 | if ((err instanceof mqlight.SubscribedError) && 724 | /client already has a queued subscription/.test(err)) { 725 | return true; 726 | } 727 | }, 'Service parameter as non string/array test'); 728 | test.strictEqual(client._queuedSubscriptions.length, 1, 729 | 'expected to see 1 queued subscription, but saw ' + 730 | client._queuedSubscriptions.length); 731 | callback(null, 'amqp://host'); 732 | } 733 | }); 734 | var savedSubscribeFunction = client._messenger.createReceiver; 735 | client._messenger.createReceiver = function(address) { 736 | ++subscribes; 737 | return savedSubscribeFunction(address); 738 | }; 739 | 740 | client.once('started', function() { 741 | setTimeout(function() {client.stop();},500); 742 | }); 743 | 744 | client.on('stopped', function() { 745 | // we expect only one of the subscribes to have had a successful callback 746 | test.equal(callbacks, 1, 'expecting 1 callback, but saw ' + callbacks); 747 | test.equal(subscribes, 1, 'expecting 1 subscribe, but saw ' + subscribes); 748 | client._messenger.createReceiver = savedSubscribeFunction; 749 | test.done(); 750 | }); 751 | }; 752 | 753 | 754 | /** 755 | * Test that when the initial connection fails a queued subscribe 756 | * will get processed, when it connects. 757 | * @param {object} test the unittest interface. 758 | */ 759 | module.exports.test_initial_failure_retry_sub = function(test){ 760 | 761 | var client = mqlight.createClient({id: 'test_initial_failure_retry_sub', 762 | service: 'amqp://host'}); 763 | var callbackCalled = 0; 764 | var first = true; 765 | client.on('started', function() { 766 | setTimeout(function() { client.stop() }, 20); 767 | }); 768 | 769 | client.on('error', function() { 770 | if ( first ) { 771 | client.subscribe('queuedSub', function(err){ 772 | if (err){ 773 | test.ok(false, 'should not be called in err'); 774 | } else { 775 | callbackCalled++; 776 | } 777 | }); 778 | first = false; 779 | } else { 780 | test.equal(client._queuedSubscriptions.length, 1, 781 | 'should be a queued sub'); 782 | stubproton.setConnectStatus(0); 783 | } 784 | }); 785 | 786 | client.on('stopped', function(){ 787 | test.equal(callbackCalled, 1, 'should have called in success'); 788 | test.done(); 789 | }); 790 | 791 | stubproton.setConnectStatus(1); 792 | }; 793 | 794 | 795 | /** 796 | * Test that when the initial attempt to connect to the server fails a queued 797 | * send operation will be processed if the client retrys and connects. 798 | * @param {object} test the unittest interface. 799 | */ 800 | module.exports.test_initial_failure_retry_send = function(test){ 801 | stubproton.setConnectStatus(1); 802 | var client = mqlight.createClient( 803 | {id: 'test_initial_failure_retry_send', 804 | service: 'amqp://host'} 805 | ); 806 | var callbackCalled = 0; 807 | var first = true; 808 | client.on('started', function() { 809 | setTimeout(function() { 810 | client.stop(); 811 | }, 10); 812 | }); 813 | 814 | client.on('error', function() { 815 | if (first) { 816 | client.send('topic', 'message', function(err) { 817 | if (err) { 818 | test.ok(false, 'should not be called in err'); 819 | } else { 820 | callbackCalled++; 821 | } 822 | }); 823 | first = false; 824 | } else { 825 | test.equal(client._queuedSends.length, 1, 826 | 'should be a queued send'); 827 | stubproton.setConnectStatus(0); 828 | } 829 | }); 830 | 831 | client.on('stopped', function() { 832 | test.equal(callbackCalled, 1, 'should be one callback called'); 833 | test.done(); 834 | }); 835 | }; 836 | --------------------------------------------------------------------------------