diff --git a/.jshintrc b/.jshintrc index c0713fe..28fc75e 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,28 +1,33 @@ { + "globals": { + "describe": true, + "it": true, + "beforeEach": true + }, "asi" : true, "camelcase" : false, - "bitwise" : false, + "bitwise" : false, "unused" : true, "laxbreak" : true, "laxcomma" : true, - "curly" : false, + "curly" : false, "eqeqeq" : true, - "evil" : true, - "forin" : false, - "immed" : true, - "latedef" : false, - "newcap" : false, - "noarg" : true, + "evil" : true, + "expr" : true, + "forin" : false, + "immed" : true, + "latedef" : false, + "newcap" : false, + "noarg" : true, "noempty" : true, "nonew" : true, - "plusplus" : false, - "regexp" : true, - "undef" : false, - "strict" : false, + "plusplus" : false, + "regexp" : true, + "undef" : false, + "strict" : false, "sub" : true, - "trailing" : true, - "node" : true, - "maxerr" : 100, - "indent" : 2 + "trailing" : true, + "node" : true, + "maxerr" : 100, + "indent" : 2 } - diff --git a/Gruntfile.js b/Gruntfile.js index 3da36c9..67dc519 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -30,7 +30,8 @@ module.exports = function(grunt) { /*'./test/test_web.js',*/ './test/test_fhauth.js', './test/test_init.js', - './test/test_log.js' + './test/test_log.js', + './test/test_fhpush.js' ]; var unit_args = _.map(tests, makeTestArgs); var test_runner = '_mocha'; diff --git a/lib/fhutils.js b/lib/fhutils.js index 713b701..814edf6 100644 --- a/lib/fhutils.js +++ b/lib/fhutils.js @@ -2,9 +2,18 @@ var assert = require('assert'); module.exports = function (cfg) { assert.ok(cfg, 'cfg is undefined'); - this.addAppApiKeyHeader = function (header) { + + /** + * Injects an RHMAP API Key into a provided Object "headers" Object. + * + * By default this will use the API Key for the running node process, but + * passing a custom "value" is also supported. + * @param {Object} headers + * @param {String} [value] + */ + this.addAppApiKeyHeader = function (headers, value) { if (cfg.fhapi && cfg.fhapi.appapikey && cfg.fhapi.appapikey.length > 0) { - header[cfg.APP_API_KEY_HEADER] = cfg.fhapi.appapikey; + headers[cfg.APP_API_KEY_HEADER] = value || cfg.fhapi.appapikey; } }; diff --git a/lib/push.js b/lib/push.js index 207753f..d605f7e 100644 --- a/lib/push.js +++ b/lib/push.js @@ -1,42 +1,76 @@ var assert = require('assert'), futils = require('./fhutils'), - aeroGearLoaded = false; + xtend = require('xtend'), + AeroGear = require('unifiedpush-node-sender'); module.exports = function (cfg) { assert.ok(cfg, 'cfg is undefined'); + var fhutils = new futils(cfg); + var millicoreProps = fhutils.getMillicoreProps(); + var defaultPushSettings = getPushSettings(millicoreProps); - var props = fhutils.getMillicoreProps(); - var headers = { - 'X-Project-Id': props.widget, - 'X-App-Id': props.instance - }; - fhutils.addAppApiKeyHeader(headers); - var settings = { - url: 'https://' + props.millicore + ':' + props.port + '/box/api/unifiedpush/mbaas/', - applicationId: "fake", // we have to use fake ID, it will be added by supercore - masterSecret: "fake", // we have to use fake secret, it will be added by supercore - headers: headers - }; + /** + * Generates settings to the used when creating an AeroGear.sender. + * @param {Object} opts [description] + * @return {Object} + */ + function getPushSettings (opts) { + assert.ok(opts, 'opts is undefined'); + var headers = { + 'X-Project-Id': opts.widget, + 'X-App-Id': opts.instance + }; + + fhutils.addAppApiKeyHeader(headers, opts.appapikey); + + return { + url: 'https://' + opts.millicore + ':' + opts.port + '/box/api/unifiedpush/mbaas/', + applicationId: "fake", // we have to use fake ID, it will be added by supercore + masterSecret: "fake", // we have to use fake secret, it will be added by supercore + headers: headers + }; + } + + + /** + * Creates a push client (aka the $fh.push API) + * @param {Object} settings + * @return {Function} + */ + function getPushClient (settings) { + var sender = AeroGear.Sender(settings); + + return function push(message, options, callback) { + if (!message) return callback(new Error("Missing required 'message' parameter")); + if (!options) return callback(new Error("Missing required 'options' parameter")); + if (!options.broadcast) { + if (!options.apps) return callback(new Error("Missing required 'options.apps' parameter while 'options.broadcast' not specified" + JSON.stringify(options))); + } - // $fh.push - var sender; - return function push(message, options, callback) { - if (!aeroGearLoaded) { - var AeroGear = require("unifiedpush-node-sender"); - sender = AeroGear.Sender(settings); - aeroGearLoaded = true; - } - - if (!message) return callback(new Error("Missing required 'message' parameter")); - if (!options) return callback(new Error("Missing required 'options' parameter")); - if (!options.broadcast) { - if (!options.apps) return callback(new Error("Missing required 'options.apps' parameter while 'options.broadcast' not specified" + JSON.stringify(options))); - } - if (sender){ sender.send(message, options, callback); - } + }; } -}; + // $fh.push + var push = getPushClient(defaultPushSettings); + + /** + * Allows developers to get a custom $fh.push instance that targets a + * specific project. Useful if this app is an MBaaS Service. + * @param {Object} overrides + * @return {Function} + */ + push.getPushClient = function (overrides) { + // Create settings for a new push client, but add in overrides such as using + // a custom widget and instance (project and cloud app id) + var settings = getPushSettings( + xtend(millicoreProps, overrides) + ); + + return getPushClient(settings); + }; + + return push; +}; diff --git a/package.json b/package.json index 6784c2e..3876dbe 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,12 @@ "stack-trace": "0.0.9", "underscore": "1.7.0", "unifiedpush-node-sender": "0.12.0", - "winston": "0.8.0" + "winston": "0.8.0", + "xtend": "4.0.1" }, "devDependencies": { "chai": "^3.5.0", + "clear-require": "1.0.1", "express": "3.3.4", "grunt": "^0.4.5", "grunt-fh-build": "^0.5.0", @@ -38,6 +40,7 @@ "nock": "0.22.1", "plato": "^1.0.1", "proxyquire": "1.4.0", + "sinon": "^1.17.5", "valid-url": "1.0.9" }, "scripts": { diff --git a/test/test_fhpush.js b/test/test_fhpush.js new file mode 100644 index 0000000..35a0efc --- /dev/null +++ b/test/test_fhpush.js @@ -0,0 +1,143 @@ +'use strict'; + +var proxyquire = require('proxyquire') + , expect = require('chai').expect + , sinon = require('sinon'); + +describe('$fh.push', function () { + + var mod, stubs, validCfg, senderStub, utilStubs; + + var STUB_MAP = { + FHUTIL: './fhutils', + UPS: 'unifiedpush-node-sender' + }; + + beforeEach(function () { + // Each test should start with fresh copies of deps + require('clear-require').all(); + + senderStub = { + send: sinon.stub() + }; + + validCfg = { + fhapi: { + widget: 'a', + instance: 'b', + millicore: 'fake-domain.feedhenry.com', + port: 4567 + } + }; + + utilStubs = { + getMillicoreProps: sinon.stub().returns(validCfg.fhapi), + addAppApiKeyHeader: sinon.stub() + }; + + stubs = {}; + + stubs[STUB_MAP.UPS] = { + Sender: sinon.stub().returns(senderStub) + }; + + stubs[STUB_MAP.FHUTIL] = sinon.stub().returns(utilStubs); + + mod = proxyquire('lib/push', stubs); + }); + + describe('#push', function () { + it('should return a function', function () { + expect(mod(validCfg)).to.be.a('function'); + expect(utilStubs.getMillicoreProps.callCount).to.equal(1); + expect(utilStubs.addAppApiKeyHeader.callCount).to.equal(1); + expect(stubs[STUB_MAP.UPS].Sender.callCount).to.equal(1); + }); + + it('should return an error - missing "messsage"', function (done) { + mod(validCfg)(null, null, function (err) { + expect(err).to.exist; + expect(err.toString()).to.contain( + 'Missing required \'message\' parameter' + ); + + done(); + }); + }); + + it('should return an error - missing "options"', function (done) { + mod(validCfg)({ alert: 'fáilte!' }, null, function (err) { + expect(err).to.exist; + expect(err.toString()).to.contain( + 'Missing required \'options\' parameter' + ); + + done(); + }); + }); + + it('should return an error - missing "options.app"', function (done) { + mod(validCfg)({ alert: 'slán' }, {}, function (err) { + expect(err).to.exist; + expect(err.toString()).to.contain( + 'Missing required \'options.apps\' parameter' + ); + + done(); + }); + }); + + it('should send a push payload to the AeroGear.Sender', function (done) { + senderStub.send.yields(null); + + var message = { alert: 'go raibh maith agat' }; + var options = { broadcast: true }; + + mod(validCfg)(message, options, function (err) { + expect(err).to.not.exist; + expect(senderStub.send.getCall(0).args[0]).to.deep.equal(message); + expect(senderStub.send.getCall(0).args[1]).to.deep.equal(options); + + done(); + }); + }); + }); + + describe('#getPushClient', function () { + it('should return a push function using custom options', function () { + var fhpush = mod(validCfg); + + var customOpts = { + widget: '123', + instance: '321', + appapikey: 'abc' + }; + + var customPush = fhpush.getPushClient(customOpts); + + expect(customPush).to.be.a('function'); + expect(customPush).to.not.equal(fhpush); // should be a new instance + + // This should only ever be called on the first creation + expect(utilStubs.getMillicoreProps.callCount).to.equal(1); + + // Should have called all of these twice, once for the default $fh.push + // and a second time for our custom setup + expect(utilStubs.addAppApiKeyHeader.callCount).to.equal(2); + expect(stubs[STUB_MAP.UPS].Sender.callCount).to.equal(2); + + // Verify our custom options are being used + var senderSettings = stubs[STUB_MAP.UPS].Sender.getCall(1).args[0]; + expect(senderSettings).to.deep.equal({ + url: 'https://fake-domain.feedhenry.com:4567/box/api/unifiedpush/mbaas/', + applicationId: 'fake', + masterSecret: 'fake', + headers: { + 'X-Project-Id': customOpts.widget, + 'X-App-Id': customOpts.instance + } + }); + + }); + }); +}); diff --git a/test/test_fhutils.js b/test/test_fhutils.js index 2461fa2..b2d97f2 100644 --- a/test/test_fhutils.js +++ b/test/test_fhutils.js @@ -1,8 +1,49 @@ var assert = require('assert'); var futils = require('../lib/fhutils'); var fhutils = new futils({}); +var expect = require('chai').expect; module.exports = { + 'test addAppApiKeyHeader - without override': function () { + var fhutils = new futils({ + APP_API_KEY_HEADER: 'x-fh-api-key', + fhapi: { + appapikey: 'thedefaultapikey' + } + }); + + var headers = { + 'x-custom-header': 'rhmap rocks' + }; + + fhutils.addAppApiKeyHeader(headers); + + expect(headers).to.deep.equal({ + 'x-custom-header': 'rhmap rocks', + 'x-fh-api-key': 'thedefaultapikey' + }); + }, + 'test addAppApiKeyHeader - with override': function () { + var fhutils = new futils({ + APP_API_KEY_HEADER: 'x-fh-api-key', + fhapi: { + appapikey: 'thedefaultapikey' + } + }); + + var headers = { + 'x-custom-header': 'rhmap rocks' + }; + + var customApiKey = 'thecustomapikey'; + + fhutils.addAppApiKeyHeader(headers, customApiKey); + + expect(headers).to.deep.equal({ + 'x-custom-header': 'rhmap rocks', + 'x-fh-api-key': customApiKey + }); + }, 'test urlPathJoin': function(finish) { assert.equal(fhutils.urlPathJoin('/p1', '/p2'), "/p1/p2");