Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit b662b4d1b38282c0fa574dfc6a38aa3acbda9c4f 1 parent 9f46454
@jherdman authored
View
3  .gitignore
@@ -0,0 +1,3 @@
+node_modules/
+*.log
+coverage.html
View
4 .npmignore
@@ -0,0 +1,4 @@
+spec/
+.git*
+.DS_Store
+integration/
View
3  .travis.yml
@@ -0,0 +1,3 @@
+language: node_js
+node_js:
+ - 0.6
View
3  History.md
@@ -0,0 +1,3 @@
+# 0.1.0
+
+* Initial release
View
15 Makefile
@@ -0,0 +1,15 @@
+EXAMPLES = spec/*_spec.js
+REPORTER = spec
+
+spec:
+ @NODE_ENV=test ./node_modules/.bin/mocha \
+ --require should \
+ --require nock \
+ --reporter $(REPORTER) \
+ --growl \
+ $(EXAMPLES)
+
+coverage:
+ @COV=1 $(MAKE) spec REPORTER=html-cov > coverage.html
+
+.PHONY: spec coverage
View
19 README.md
@@ -0,0 +1,19 @@
+# airship
+
+A wrapper for the Urban Airship API. It's largely an experiment of mine to learn more about event-based programming in Node.js.
+
+# Usage
+
+Please see examples in the `integration` directory for usage. Given that this is such an early release, I'm very open to suggestion on how to evolve the API of this library.
+
+# License
+
+## MIT License
+
+Copyright (c) 2012 James F. Herdman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
View
1  index.js
@@ -0,0 +1 @@
+module.exports = require('./lib');
View
49 integration/scenario1.js
@@ -0,0 +1,49 @@
+var key = process.argv[2]
+ , secret = process.argv[3]
+ , airship = require('../').createAirship(key, secret)
+ , deviceToken = 'FE66489F304DC75B8D6E8200DFF8A456E8DAEACEC428B427E9518741C92C6660'
+ , assert = require('assert');
+
+/**
+ * Simple integration scenario:
+ *
+ * 1. Register a device with tokens
+ * 2. Remove a token
+ * 3. Add another token
+ */
+
+var regReq = airship.register(deviceToken, { tags: ['bacon'] });
+
+// Step 1
+regReq.on('success', function (data) {
+ console.log('Successfully registered device');
+
+ // Step 2
+ var remTokReq = airship.removeTag('bacon');
+
+ remTokReq.on('success', function (data) {
+ console.log('Successfully removed tag from device');
+
+ // Step 3
+ var addTokReq = airship.addTag('waffles');
+
+ addTokReq.on('success', function (data) {
+ console.log('Successfully added another token');
+ });
+
+ addTokReq.on('fail', function (err) {
+ console.error('Failed to add another token');
+ assert.ifError(err);
+ });
+ });
+
+ remTokReq.on('fail', function (err) {
+ console.error('Failed to remove tag');
+ assert.ifError(err);
+ });
+});
+
+regReq.on('fail', function (err) {
+ console.error('Failed to register device');
+ assert.ifError(err);
+});
View
171 lib/airship.js
@@ -0,0 +1,171 @@
+var u = require('./utils')
+ , Request = require('./request').Request
+ , Airship;
+
+Airship = function (appKey, masterSecret) {
+ if (!appKey) { throw new Error('You must provide an app key'); }
+ if (!masterSecret) { throw new Error('You must provide your master secret'); }
+
+ this.appKey = appKey;
+ this.masterSecret = masterSecret;
+};
+
+Airship.prototype = {
+ /**
+ * @private
+ */
+ request: function (options) {
+ var req = new Request(options);
+ req.setAuth(this.appKey, this.masterSecret);
+ return req;
+ }
+ /**
+ * @param {Object} options Use to set the page number of results you want
+ *
+ * @see http://urbanairship.com/docs/push.html#device-token-list-api
+ */
+, deviceTokens: function (options) {
+ var path = u.deviceTokenPath();
+
+ if (options && options.page) {
+ path += '?page=' + options.page;
+ }
+
+ return this.request({ path: path }).get();
+ }
+ /**
+ * @see http://urbanairship.com/docs/tags.html#seeing-your-tags
+ */
+, tags: function () {
+ return this.request({ path: u.tagPath() }).get();
+ }
+ /**
+ * @param {String} tagName the name of the tag to add to the application
+ *
+ * @see http://urbanairship.com/docs/tags.html#adding-tags
+ */
+, addTag: function (tagName) {
+ return this.request({ path: u.tagPath(tagName) }).put();
+ }
+ /**
+ * @param {String} tagName the name of the tag to remove from the application
+ *
+ * @see http://urbanairship.com/docs/tags.html#removing-tags
+ */
+, removeTag: function (tagName) {
+ return this.request({ path: u.tagPath(tagName) }).delete();
+ }
+ /**
+ * @param {String} tagName the name of the tag to modify
+ *
+ * @param {Object} payload a JavaScript object describing how to modify the tag
+ *
+ * @see http://urbanairship.com/docs/tags.html#modifying-device-tokens-on-a-tag
+ */
+, modifyTokensOnTag: function (tagName, payload) {
+ return this.request({ path: u.tagPath(tagName) }).post(payload);
+ }
+ /**
+ * @param {String} deviceToken the token for the Device of interest
+ */
+, deviceTags: function (deviceToken) {
+ return this.request({ path: u.deviceTokenTagPath(deviceToken) }).get();
+ }
+ /**
+ * @param {String} deviceToken the token for the Device of interest
+ *
+ * @param {String} tag the tag to add to the Device
+ */
+, addTagToDevice: function (deviceToken, tag) {
+ return this.request({ path: u.deviceTokenTagPath(deviceToken, tag) }).put();
+ }
+ /**
+ * @param {String} deviceToken the token for the Device of interest
+ *
+ * @param {String} tag the tag to remove from the Device
+ */
+, removeTagFromDevice: function (deviceToken, tag) {
+ return this.request({ path: u.deviceTokenTagPath(deviceToken, tag) }).delete();
+ }
+ /**
+ * @param {String} deviceToken the token of the Device to register
+ *
+ * @param {Object} payload The optional payload of options to initialize the
+ * Device with
+ *
+ * @see http://urbanairship.com/docs/push.html#registration
+ */
+, register: function (deviceToken, payload) {
+ return this.request({ path: u.deviceTokenPath(deviceToken) }).put(payload);
+ }
+ /**
+ * Inactive a device
+ *
+ * @param {String} deviceToken the token of the Device to inactivate
+ *
+ * @see http://urbanairship.com/docs/push.html#registration
+ */
+, inactivate: function (deviceToken) {
+ return this.request({ path: u.deviceTokenPath(deviceToken) }).delete();
+ }
+ /**
+ * Send a push message
+ *
+ * @param {Object} payload describes the push message to send
+ *
+ * @see http://urbanairship.com/docs/push.html#push
+ */
+, push: function (payload) {
+ return this.request({ path: '/api/push/' }).post(payload);
+ }
+ /**
+ * Cancel a single push message, or many
+ *
+ * @param {Object, String} idOrArgs Either the payload describing the bulk
+ * or the ID of the specific message to send
+ *
+ * @see http://urbanairship.com/docs/push.html#scheduled-notifications
+ */
+, cancelPush: function (idOrArgs) {
+ var req = this.request({ path: u.scheduledPushPath(idOrArgs) });
+
+ if (typeof idOrArgs == 'object') {
+ return req.post(idOrArgs);
+ } else {
+ return req.delete();
+ }
+ }
+ /**
+ * Send a batch push message request
+ *
+ * @param {Object} payload
+ *
+ * @see http://urbanairship.com/docs/push.html#batch-push
+ */
+, batchPush: function (payload) {
+ return this.request({ path: '/api/push/batch/' }).post(payload);
+ }
+ /**
+ * Broadcast a message to all registered devices
+ *
+ * @param {Object} payload
+ *
+ * @see http://urbanairship.com/docs/push.html#broadcast
+ */
+, broadcast: function (payload) {
+ return this.request({ path: '/api/push/broadcast/' }).post(payload);
+ }
+ /**
+ * Get feedback regarding usage
+ *
+ * @param {Date} since The point in time from which you want feedback
+ *
+ * @see http://urbanairship.com/docs/push.html#feedback-service
+ */
+, feedback: function (since) {
+ var path = u.deviceTokenPath() + 'feedback/?since=' + since.toISOString();
+ return this.request({ path: path }).get();
+ }
+};
+
+exports.Airship = Airship;
View
14 lib/index.js
@@ -0,0 +1,14 @@
+var Airship = require('./airship').Airship;
+
+/**
+ * @param {String} appKey Your unique application key
+ *
+ * @param {String} secret Your application's master secret
+ *
+ * @return {Airship} a new Airship instance
+ */
+exports.createAirship = function (appKey, masterSecret) {
+ return new Airship(appKey, masterSecret);
+};
+
+exports.version = '0.1.0';
View
172 lib/request.js
@@ -0,0 +1,172 @@
+var u = require('./utils')
+ , util = require('util')
+ , https = require('https')
+ , events = require('events')
+ , DEFAULTS = { hostname: 'go.urbanairship.com' }
+ , Request;
+
+/**
+ * Abstraction for doing requests. Mostly used to handle some assumptions about
+ * dealing with UrbanAirship.
+ *
+ * Event: 'success'
+ * `function (data) { }`
+ *
+ * Emitted each time there is a successful response. I.E. the response code is
+ * 200, 201, or 204.
+ *
+ * Event: 'fail'
+ * `function (err) { }`
+ *
+ * Emitted once for each request. No further events are emitted after this one.
+ *
+ * Event: 'close'
+ * `function (err) { }`
+ *
+ * Underlying connection was terminated before the 'end' event.
+ *
+ * Event: 'error'
+ * `function (err) { }`
+ *
+ * Emitted when there is a heinous error, such as trouble establishing a connection.
+ *
+ * @extends EventEmitter
+ *
+ * @private
+ */
+Request = function (options, data) {
+ this.options = u.merge(DEFAULTS, options);
+ this.data = data;
+};
+
+util.inherits(Request, events.EventEmitter);
+
+/**
+ * Sets the authorization values for this request.
+ *
+ * @param {String} key The key for your application
+ *
+ * @param {String} secret They master secret for your application
+ */
+Request.prototype.setAuth = function (key, secret) {
+ this.options.auth = [key, secret].join(':');
+};
+
+/**
+ * @param {Object} data Data to send on the request. Automatically sets the
+ * content-type header to "application/json" when data is provided
+ *
+ * @return {http.ClientRequest}
+ */
+Request.prototype.apiCall = function (data) {
+ var self = this
+ , encodedData
+ , req;
+
+ req = https.request(this.options, function (res) {
+ var chunks = []
+ , statusCode = res.statusCode
+ , contentType
+ , responseBody
+ , encodedData;
+
+ res.setEncoding('utf8');
+
+ res.on('data', function (chunk) {
+ chunks.push(chunk);
+ });
+
+ res.on('end', function () {
+ if (res.headers['content-type']) {
+ responseBody = chunks.join('');
+ }
+
+ if (res.headers['content-type'].match(/application\/(.*)json/)) {
+ responseBody = JSON.parse(responseBody);
+ }
+
+ if (res.statusCode >= 200 && res.statusCode < 300) {
+ self.emit('success', responseBody);
+ } else {
+ self.emit('fail', responseBody);
+ }
+ });
+
+ res.on('close', function (err) {
+ self.emit('close', err);
+ });
+ });
+
+ req.on('error', function (err) {
+ self.emit('error', err);
+ });
+
+ if (typeof data !== 'undefined') {
+ encodedData = JSON.stringify(data);
+
+ req.setHeader('content-type', 'application/json');
+ req.setHeader('content-length', encodedData.length);
+
+ req.end(encodedData, 'utf8');
+ } else {
+ req.setHeader('content-length', 0);
+ req.end();
+ }
+
+ return this;
+};
+
+/**
+ * Sends an HTTP GET request. Automatically sets the 'accept' header for JSON
+ *
+ * @param {Object} options
+ *
+ * @see {request.send}
+ */
+Request.prototype.get = function () {
+ this.options.method = 'GET';
+ this.options.headers = { 'accept': 'application/json' };
+ return this.apiCall();
+};
+
+/**
+ * Sends an HTTP PUT request. Any data sent is assumed to be JSON, which will
+ * set the 'content-type' header accordingly.
+ *
+ * @param {Object} data JSON data to send in the request
+ *
+ * @see {request.send}
+ */
+Request.prototype.put = function (data) {
+ this.options.method = 'PUT';
+ return this.apiCall(data);
+};
+
+/**
+ * Sends an HTTP DELETE request
+ *
+ * @param {Object} options
+ *
+ * @see {request.send}
+ */
+Request.prototype.delete = function () {
+ this.options.method = 'DELETE';
+ return this.apiCall();
+};
+
+/**
+ * Sends an HTTP POST request. Sending data automatically sets the content-type
+ * header for JSON.
+ *
+ * @param {Object} options
+ *
+ * @param {Object} data JSON data to send in the request
+ *
+ * @see {request.send}
+ */
+Request.prototype.post = function (data) {
+ this.options.method = 'POST';
+ return this.apiCall(data);
+};
+
+exports.Request = Request;
View
34 lib/utils.js
@@ -0,0 +1,34 @@
+var utils = exports;
+
+utils.merge = function () {
+ var objects = Array.prototype.slice.call(arguments)
+ , merged = {};
+
+ objects.forEach(function (obj) {
+ Object.getOwnPropertyNames(obj).forEach(function (prop) {
+ merged[prop] = obj[prop];
+ });
+ });
+
+ return merged;
+};
+
+utils.deviceTokenPath = function (deviceToken) {
+ var root = '/api/device_tokens/';
+ return (deviceToken ? [root, deviceToken].join('') : root);
+};
+
+utils.tagPath = function (tagName) {
+ var root = '/api/tags/';
+ return (tagName ? [root, tagName].join('') : root);
+};
+
+utils.deviceTokenTagPath = function (deviceToken, tagName) {
+ var root = ['/api/device_tokens/', deviceToken, '/tags/'].join('');
+ return (tagName ? [root, tagName].join('') : root);
+};
+
+utils.scheduledPushPath = function (id) {
+ var root = '/api/push/scheduled/';
+ return (id && (typeof id !== 'object') ? [root, id].join('') : root);
+};
View
29 package.json
@@ -0,0 +1,29 @@
+{
+ "name": "airship",
+ "description": "A wrapper for the UrbanAirship API",
+ "author": "James F. Herdman <james.herdman@me.com>",
+ "version": "0.1.0",
+ "keywords": ["urban airship", "api", "push messages", "airship"],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/jherdman/airship.git"
+ },
+ "bugs": {
+ "url": "https://github.com/jherdman/airship/issues"
+ },
+ "engines": {
+ "node": "~0.6.10"
+ },
+ "dependencies": {
+ },
+ "devDependencies": {
+ "mocha": "~0.12.0",
+ "should": "~0.5.1",
+ "nock": "git://github.com/jherdman/nock.git#issue-41"
+ },
+ "main": "index",
+ "scripts": {
+ "prepublish": "npm prune",
+ "test": "make spec"
+ }
+}
View
232 spec/airship_spec.js
@@ -0,0 +1,232 @@
+var Airship = require('../lib/airship').Airship
+ , airship = new Airship('foo', 'bar')
+ , nock = require('nock')
+ , uas = nock('https://go.urbanairship.com')
+ , fs = require('fs')
+ , fixture = function (name) {
+ return __dirname + '/fixtures/' + name;
+ };
+
+describe('Airship', function () {
+ describe('constructor', function () {
+ it('sets the app key', function () {
+ var a = new Airship('foo', 'bar');
+ a.appKey.should.eql('foo');
+ });
+
+ it('sets the master secret', function () {
+ var a = new Airship('foo', 'bar');
+ a.masterSecret.should.eql('bar');
+ });
+
+ it('throws an error if an app key is not provided', function () {
+ (function () {
+ var a = new Airship();
+ }).should.throw('You must provide an app key');
+ });
+
+ it('throws an error if a master secret is not provided', function () {
+ (function () {
+ var a = new Airship('fooo');
+ }).should.throw('You must provide your master secret');
+ });
+ });
+
+ describe('#deviceTokens', function () {
+ var fixturePath = fixture('device_tokens.json');
+
+ before(function () {
+ uas
+ .get('/api/device_tokens/')
+ .replyWithFile(200, fixturePath, { 'content-type': 'application/json' })
+ .get('/api/device_tokens/?page=1')
+ .replyWithFile(200, fixturePath, { 'content-type': 'application/json' });
+ });
+
+ it('requests a page of device tokens', function () {
+ airship.deviceTokens();
+ });
+
+ it('does not include a page number by default', function () {
+ airship.deviceTokens({ page: 1 });
+ });
+ });
+
+ describe('#tags', function () {
+ before(function () {
+ uas
+ .get('/api/tags/')
+ .replyWithFile(200, fixture('tags.json'), { 'content-type': 'application/json' });
+ });
+
+ it('requests the collection of tags', function () {
+ airship.tags();
+ });
+ });
+
+ describe('#addTag', function () {
+ before(function () {
+ uas
+ .put('/api/tags/foobar')
+ .reply(201, 'CREATED', { 'content-type': 'text/plain' });
+ });
+
+ it('requests to create a new tag', function () {
+ airship.addTag('foobar');
+ });
+ });
+
+ describe('#removeTag', function () {
+ before(function () {
+ uas
+ .delete('/api/tags/foobar')
+ .reply(204, 'NO CONTENT', { 'content-type': 'text/plain' });
+ });
+
+ it('requests to remove a tag', function () {
+ airship.removeTag('foobar');
+ });
+ });
+
+ describe('#modifyTokensOnTag', function () {
+ before(function () {
+ uas
+ .post('/api/tags/pico', { device_tokens: { add: ['CAFEBABE'] } }, { 'content-type': 'application/json' })
+ .reply(204, 'NO CONTENT', { 'content-type': 'text/plain' });
+ });
+
+ it('requests to modify device tokens on a tag', function () {
+ airship.modifyTokensOnTag('pico', { device_tokens: { add: ['CAFEBABE'] } });
+ });
+ });
+
+ describe('#deviceTags', function () {
+ before(function () {
+ uas
+ .get('/api/device_tokens/CAFEBABE/tags/')
+ .replyWithFile(200, fixture('device_tags.json'), { 'content-type': 'application/json' });
+ });
+
+ it('requests the collection of tags on a device', function () {
+ airship.deviceTags('CAFEBABE');
+ });
+ });
+
+ describe('#addTagToDevice', function () {
+ before(function () {
+ uas
+ .put('/api/device_tokens/CAFEBABE/tags/fink')
+ .reply(201, 'CREATED', { 'content-type': 'text/plain' });
+ });
+
+ it('requests to add a tag to a device', function () {
+ airship.addTagToDevice('CAFEBABE', 'fink');
+ });
+ });
+
+ describe('#removeTagFromDevice', function () {
+ before(function () {
+ uas
+ .delete('/api/device_tokens/CAFEBABE/tags/fink')
+ .reply(204, 'NO CONTENT', { 'content-type': 'text/plain' });
+ });
+
+ it('requests to remove a tag from a device', function () {
+ airship.removeTagFromDevice('CAFEBABE', 'fink');
+ });
+ });
+
+ describe('#register', function () {
+ var data = { tags: ['foo', 'bar'] };
+
+ before(function () {
+ uas
+ .put('/api/device_tokens/CAFEBABE')
+ .reply(201, 'CREATED', { 'content-type': 'text/plain' })
+ .put('/api/device_tokens/DEADBEEF', JSON.stringify(data), { 'content-type': 'application/json' })
+ .reply(201, 'CREATED', { 'content-type': 'text/plain' });
+ });
+
+ it('requests to register a device', function () {
+ airship.register('CAFEBABE');
+ });
+
+ it('requests to register a device with a payload', function () {
+ airship.register('DEADBEEF', data);
+ });
+ });
+
+ describe('#inactivate', function () {
+ before(function () {
+ uas
+ .delete('/api/device_tokens/CAFEBABE')
+ .reply(204, 'NO CONTENT', { 'content-type': 'text/plain' });
+ });
+
+ it('requests to inactive the device', function () {
+ airship.inactivate('CAFEBABE');
+ });
+ });
+
+ describe('#cancelPush', function () {
+ var data = { cancel: ['https://go.urbanairship.com/api/push/scheduled/XX'] };
+
+ before(function () {
+ uas
+ .delete('/api/push/scheduled/1234')
+ .reply(204, 'NO CONTENT', { 'content-type': 'text/plain' })
+ .post('/api/push/scheduled/', data, { 'content-type': 'application/json' })
+ .reply(204, 'NO CONTENT', { 'content-type': 'text/plain' });
+ });
+
+ it('requests to cancel a scheduled push message', function () {
+ airship.cancelPush('1234');
+ });
+
+ it('requests to cancel bulk scheduled push messages', function () {
+ airship.cancelPush(data);
+ });
+ });
+
+ describe('#batchPush', function () {
+ var data = {};
+
+ before(function () {
+ uas
+ .post('/api/push/batch/', JSON.stringify(data), { 'content-type': 'application/json' })
+ .reply(200, 'OK', { 'content-type': 'text/plain' });
+ });
+
+ it('requests to send a batch push', function () {
+ airship.batchPush(data);
+ });
+ });
+
+ describe('#broadcast', function () {
+ var data = {};
+
+ before(function () {
+ uas
+ .post('/api/push/broadcast/', JSON.stringify(data), { 'content-type': 'application/json' })
+ .reply(200, 'OK', { 'content-type': 'text/plain' });
+ });
+
+ it('requests to broadcast a message to all devices', function () {
+ airship.broadcast(data);
+ });
+ });
+
+ describe('#feedback', function () {
+ var since = new Date('2011-5-19');
+
+ before(function () {
+ uas
+ .get('/api/device_tokens/feedback/?since=' + since.toISOString())
+ .replyWithFile(200, fixture('feedback.json'), { 'content-type': 'application/json' });
+ });
+
+ it('requests feedback from a point in time', function () {
+ airship.feedback(since);
+ });
+ });
+});
View
6 spec/fixtures/device_tags.json
@@ -0,0 +1,6 @@
+{
+ "tags": [
+ "tag1",
+ "some_tag"
+ ]
+}
View
5 spec/fixtures/device_tokens.json
@@ -0,0 +1,5 @@
+{
+ "device_tokens": [],
+ "device_tokens_count": 0,
+ "active_device_tokens_count": 0
+}
View
12 spec/fixtures/feedback.json
@@ -0,0 +1,12 @@
+[
+ {
+ "device_token": "1234123412341234123412341234123412341234123412341234123412341234",
+ "marked_inactive_on": "2009-06-22 10:05:00",
+ "alias": "bob"
+ },
+ {
+ "device_token": "ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD",
+ "marked_inactive_on": "2009-06-22 10:07:00",
+ "alias": null
+ }
+]
View
6 spec/fixtures/stats.csv
@@ -0,0 +1,6 @@
+2009-06-22 00:00:00,0,0,0,0
+2009-06-22 01:00:00,0,0,4,0
+2009-06-22 02:00:00,0,0,2,0
+2009-06-22 03:00:00,0,0,1,0
+2009-06-22 04:00:00,8,0,0,0
+2009-06-22 05:00:00,1,0,0,0
View
10 spec/fixtures/stats.json
@@ -0,0 +1,10 @@
+{
+ "2009-06-22": {
+ "00:00:00": [0,0,0,0],
+ "01:00:00": [0,0,4,0],
+ "02:00:00": [0,0,2,0],
+ "03:00:00": [0,0,1,0],
+ "04:00:00": [8,0,0,0],
+ "05:00:00": [1,0,0,0]
+ }
+}
View
7 spec/fixtures/tags.json
@@ -0,0 +1,7 @@
+{
+ "tags": [
+ "tag1",
+ "some_tag",
+ "portland_or"
+ ]
+}
View
9 spec/index_spec.js
@@ -0,0 +1,9 @@
+var uas = require('../lib/index')
+ , Airship = require('../lib/airship').Airship;
+
+describe('Main public interface', function () {
+ it('creates an Airship instance', function () {
+ var airship = uas.createAirship('foo', 'bar');
+ airship.should.be.instanceof(Airship);
+ });
+});
View
165 spec/request_spec.js
@@ -0,0 +1,165 @@
+var Request = require('../lib/request').Request
+ , nock = require('nock')
+ , dummyService = nock('https://go.urbanairship.com');
+
+describe('Request', function () {
+ describe('#setAuth', function () {
+ it('sets the auth params to the options', function () {
+ var req = new Request({});
+ req.setAuth('foo', 'bar');
+ req.options.auth.should.eql('foo:bar');
+ });
+ });
+
+ describe('#apiCall', function () {
+ before(function () {
+ dummyService
+ .get('/okay')
+ .reply(200, 'OKAY', { 'content-type': 'text/plain' })
+ .get('/json')
+ .reply(200, { 'my': 'data' }, { 'content-type': 'application/json' })
+ .get('/fail')
+ .reply(500, 'CLIENT ERROR', { 'content-type': 'text/plain' })
+ .get('/data')
+ .reply(200, 'DATA', { 'content-type': 'text/plain' })
+ .get('/yupyup')
+ .reply(200, 'OKAY', { 'content-type': 'text/plain' });
+ });
+
+ it('emits a "success" event when request successful', function (done) {
+ var req = new Request({ path: '/okay' });
+ req.options.method = 'GET';
+
+ req.on('success', function (d) {
+ d.should.eql('OKAY');
+ done();
+ });
+
+ req.apiCall();
+ });
+
+ it('emits a "success" event with parsed JSON data when successful', function (done) {
+ var req = new Request({ path: '/json' });
+ req.options.method = 'GET';
+
+ req.on('success', function (d) {
+ d.should.eql({ my: 'data' });
+ done();
+ });
+
+ req.apiCall();
+ });
+
+ it('emits a "fail" event when request fails', function (done) {
+ var req = new Request({ path: '/fail' });
+ req.options.method = 'GET';
+
+ req.on('fail', function (d) {
+ d.should.eql('CLIENT ERROR');
+ done();
+ });
+
+ req.apiCall();
+ });
+
+ it('returns the instance', function () {
+ var req = new Request({ path: '/yupyup' });
+ req.options.method = 'GET';
+ req.apiCall().should.be.instanceof(Request);
+ });
+ });
+
+ describe('#put', function () {
+ before(function () {
+ dummyService
+ .put('/foo')
+ .reply(201, 'CREATED', { 'content-type': 'text/plain' })
+ .put('/bar', { my: 'data' }, { 'content-type': 'application/json' })
+ .reply(201, 'CREATED', { 'content-type': 'text/plain' })
+ .put('/pickle')
+ .reply(201, 'CREATED', { 'content-type': 'text/plain' });
+ });
+
+ it('sends a PUT request to an end point', function () {
+ var req = new Request({ path: '/foo' });
+ req.put();
+ });
+
+ it('sends a PUT request with a JSON body', function () {
+ var req = new Request({ path: '/bar' });
+ req.put({ my: 'data' });
+ });
+
+ it('returns the instance', function () {
+ var req = new Request({ path: '/pickle' });
+ req.put().should.be.instanceof(Request);
+ });
+ });
+
+ describe('#post', function () {
+ before(function () {
+ dummyService
+ .post('/foo')
+ .reply(200, 'OK', { 'content-type': 'text/plain' })
+ .post('/bar', { 'my': 'data' }, { 'content-type': 'application/json' })
+ .reply(200, 'OK', { 'content-type': 'text/plain' })
+ .post('/spoon')
+ .reply(200, 'OK', { 'content-type': 'text/plain' });
+ });
+
+ it('sends a POST request to an end point', function () {
+ var req = new Request({ path: '/foo' });
+ req.post();
+ });
+
+ it('sends a POST request with a JSON body', function () {
+ var req = new Request({ path: '/bar' });
+ req.post({ my: 'data' });
+ });
+
+ it('returns the instance', function () {
+ var req = new Request({ path: '/spoon' });
+ req.post().should.be.instanceof(Request);
+ });
+ });
+
+ describe('#get', function () {
+ before(function () {
+ dummyService
+ .get('/baz')
+ .reply(200, 'OK', { 'content-type': 'text/plain' })
+ .get('/bark')
+ .reply(200, 'OK', { 'content-type': 'text/plain' });
+ });
+
+ it('sends a GET request', function () {
+ var req = new Request({ path: '/baz' });
+ req.get();
+ });
+
+ it('returns the instance', function () {
+ var req = new Request({ path: '/bark' });
+ req.get().should.be.instanceof(Request);
+ });
+ });
+
+ describe('#delete', function () {
+ before(function () {
+ dummyService
+ .delete('/quz')
+ .reply(204, 'NO CONTENT', { 'content-type': 'text/plain' })
+ .delete('/bat')
+ .reply(204, 'NO CONTENT', { 'content-type': 'text/plain' });
+ });
+
+ it('sends a DELETE request', function () {
+ var req = new Request({ path: '/quz' });
+ req.delete();
+ });
+
+ it('returns the instance', function () {
+ var req = new Request({ path: '/bat' });
+ req.delete().should.be.instanceof(Request);
+ });
+ });
+});
View
66 spec/utils_spec.js
@@ -0,0 +1,66 @@
+var utils = require('../lib/utils');
+
+describe('utils', function () {
+ describe('#merge', function () {
+ it('merges the properties of two objects into a new one', function () {
+ var o1 = { foo: 'bar' }
+ , o2 = { baz: 'qux' };
+
+ utils.merge(o1, o2).should.have.keys('foo', 'baz');
+ });
+
+ it('merges three objects properties into a new one', function () {
+ var o1 = { foo: 'bar' }
+ , o2 = { baz: 'qux' }
+ , o3 = { qui: 'abc' };
+
+ utils.merge(o1, o2, o3).should.have.keys('foo', 'baz', 'qui');
+ });
+
+ it('does not attempt to merge a non-object', function () {
+ (function () {
+ utils.merge({ foo: 'bar' }, null);
+ }).should.not.throw(/^TypeError/);
+ });
+ });
+
+ describe('#tagPath', function () {
+ it('builds a generic tags path', function () {
+ utils.tagPath().should.eql('/api/tags/');
+ });
+
+ it('builds a tag path with a named tag', function () {
+ utils.tagPath('bacon').should.eql('/api/tags/bacon');
+ });
+ });
+
+ describe('#deviceTokenPath', function () {
+ it('builds a generic device path', function () {
+ utils.deviceTokenPath().should.eql('/api/device_tokens/');
+ });
+
+ it('builds a device path for a specific device', function () {
+ utils.deviceTokenPath('CAFEBABE').should.eql('/api/device_tokens/CAFEBABE');
+ });
+ });
+
+ describe('#deviceTokenTagPath', function () {
+ it('builds a path for tags on a device', function () {
+ utils.deviceTokenTagPath('CAFEBABE').should.eql('/api/device_tokens/CAFEBABE/tags/');
+ });
+
+ it('builds a path for a tag on a device', function () {
+ utils.deviceTokenTagPath('CAFEBABE', 'bacon').should.eql('/api/device_tokens/CAFEBABE/tags/bacon');
+ });
+ });
+
+ describe('#scheduledPushPath', function () {
+ it('builds a path to a specific scheduled push message', function () {
+ utils.scheduledPushPath('burp').should.eql('/api/push/scheduled/burp');
+ });
+
+ it('builds a path to the scheduled push end point', function () {
+ utils.scheduledPushPath().should.eql('/api/push/scheduled/');
+ });
+ });
+});
Please sign in to comment.
Something went wrong with that request. Please try again.