diff --git a/package.json b/package.json index c8286380..07fc4e4e 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "test:unit:silent": "npm run test:unit > tmp/test-unit-log.txt 2>&1", "test:browser": "karma start --single-run", "test:watch": "npm run test:unit -- --watch", - "coverage": "nyc --reporter=text --include='src/**/*.js' --temp-dir=./tmp/ --check-coverage --lines 50 npm run test:unit:silent", + "coverage": "nyc --reporter=text --include='src/**/*.js' --temp-dir=./tmp/ --check-coverage --lines 91 npm run test:unit:silent", "lint": "eslint . --ext .js --format unix --ignore-path .gitignore --ignore-pattern \"dist/*\"", "lint:fix": "npm run lint -- --fix", "docs": "documentation build src/Particle.js --shallow -g -f md -o docs/api.md", diff --git a/src/Defaults.js b/src/Defaults.js index 7a2e8a55..990ffae9 100644 --- a/src/Defaults.js +++ b/src/Defaults.js @@ -2,5 +2,6 @@ export default { baseUrl: 'https://api.particle.io', clientSecret: 'particle-api', clientId: 'particle-api', - tokenDuration: 7776000 // 90 days + tokenDuration: 7776000, // 90 days + auth: undefined }; diff --git a/src/Particle.js b/src/Particle.js index ce17ca83..8d8093e4 100644 --- a/src/Particle.js +++ b/src/Particle.js @@ -23,6 +23,10 @@ class Particle { * @param {Object} options Options for this API call Options to be used for all requests (see [Defaults](../src/Defaults.js)) */ constructor(options = {}){ + if (options.auth) { + this.setDefaultAuth(options.auth); + } + // todo - this seems a bit dangerous - would be better to put all options/context in a contained object Object.assign(this, Defaults, options); this.context = {}; @@ -38,7 +42,7 @@ class Particle { if (this._isValidContext(name, context)){ this.context[name] = context; } else { - throw Error('uknown context name or undefined context: '+name); + throw Error('unknown context name or undefined context: '+name); } } } @@ -887,6 +891,7 @@ class Particle { uri += `/${encodeURIComponent(name)}`; } + auth = this._getActiveAuthToken(auth); return new EventStream(`${this.baseUrl}${uri}`, auth).connect(); } @@ -2099,6 +2104,27 @@ class Particle { }); } + /** + * Set default auth token that will be used in each method if `auth` is not provided + * @param {String} auth A Particle access token + * @returns {undefined} + */ + setDefaultAuth(auth){ + if (typeof auth === 'string' && auth.length !== 0) { + this._defaultAuth = auth; + } else { + throw new Error('Must pass a non-empty string'); + } + } + /** + * Return provided token if truthy else use default auth if truthy else undefined + * @param {*} auth Optional auth token or undefined + * @private + * @returns {String|undefined} a Particle auth token or undefined + */ + _getActiveAuthToken(auth) { + return auth || this._defaultAuth; + } /** * API URI to access a device * @param {Object} options Options for this API call @@ -2113,31 +2139,37 @@ class Particle { get({ uri, auth, headers, query, context }){ context = this._buildContext(context); + auth = this._getActiveAuthToken(auth); return this.agent.get({ uri, auth, headers, query, context }); } head({ uri, auth, headers, query, context }){ context = this._buildContext(context); + auth = this._getActiveAuthToken(auth); return this.agent.head({ uri, auth, headers, query, context }); } post({ uri, auth, headers, data, context }){ context = this._buildContext(context); + auth = this._getActiveAuthToken(auth); return this.agent.post({ uri, auth, headers, data, context }); } put({ uri, auth, headers, data, context }){ context = this._buildContext(context); + auth = this._getActiveAuthToken(auth); return this.agent.put({ uri, auth, headers, data, context }); } delete({ uri, auth, headers, data, context }){ context = this._buildContext(context); + auth = this._getActiveAuthToken(auth); return this.agent.delete({ uri, auth, headers, data, context }); } request(args){ args.context = this._buildContext(args.context); + args.auth = this._getActiveAuthToken(args.auth); return this.agent.request(args); } @@ -2147,6 +2179,7 @@ class Particle { // Internal method used to target Particle's APIs other than the default setBaseUrl(baseUrl){ + this.baseUrl = baseUrl; this.agent.setBaseUrl(baseUrl); } } diff --git a/test/Defaults.spec.js b/test/Defaults.spec.js new file mode 100644 index 00000000..d33a062d --- /dev/null +++ b/test/Defaults.spec.js @@ -0,0 +1,29 @@ +import { expect } from './test-setup'; +import Defaults from '../src/Defaults'; + +describe('Default Particle constructor options', () => { + it('includes baseUrl', () => { + expect(Defaults).to.have.property('baseUrl'); + expect(Defaults.baseUrl).to.eql('https://api.particle.io'); + }); + + it('includes clientSecret', () => { + expect(Defaults).to.have.property('clientSecret'); + expect(Defaults.clientSecret).to.eql('particle-api'); + }); + + it('includes clientId', () => { + expect(Defaults).to.have.property('clientId'); + expect(Defaults.clientId).to.eql('particle-api'); + }); + + it('includes tokenDuration', () => { + expect(Defaults).to.have.property('tokenDuration'); + expect(Defaults.tokenDuration).to.eql(7776000); + }); + + it('includes defaultAuth', () => { + expect(Defaults).to.have.property('auth'); + expect(Defaults.auth).to.eql(undefined); + }); +}); diff --git a/test/Particle.spec.js b/test/Particle.spec.js index c3cb6bff..5a6ee098 100644 --- a/test/Particle.spec.js +++ b/test/Particle.spec.js @@ -124,9 +124,27 @@ describe('ParticleAPI', () => { }); describe('constructor', () => { - it('sets the defaults', () => { + it('sets maps defaults to instance properties', () => { Object.keys(Defaults).forEach((setting) => { - api[setting].should.equal(Defaults[setting]); + expect(api[setting]).to.eql(Defaults[setting]); + }); + }); + + describe('without defaultAuth', () => { + it('does NOT call .setDefaultAuth(defaultAuth) unless provided value is truthy', () => { + sinon.stub(api, 'setDefaultAuth'); + expect(api.setDefaultAuth).to.have.property('callCount', 0); + }); + }); + + describe('with defaultAuth', () => { + it('calls .setDefaultAuth(defaultAuth) when provided defaultAuth value is truthy', () => { + const fakeAuthToken = 'foo'; + sinon.stub(Particle.prototype, 'setDefaultAuth'); + api = new Particle({ auth: fakeAuthToken }); + expect(api.setDefaultAuth).to.have.property('callCount', 1); + expect(api.setDefaultAuth.firstCall.args).to.have.lengthOf(1); + expect(api.setDefaultAuth.firstCall.args[0]).to.eql(fakeAuthToken); }); }); }); @@ -1013,6 +1031,13 @@ describe('ParticleAPI', () => { uri.should.endWith(`v1/products/test-product/devices/${props.deviceId}/events/foo`); }); }); + + it('calls _getActiveAuthToken(auth)', () => { + const fakeToken = 'abc123'; + sinon.stub(api, '_getActiveAuthToken').returns(fakeToken); + api.getEventStream({}); + expect(api._getActiveAuthToken).to.have.property('callCount', 1); + }); }); describe('.publishEvent', () => { @@ -2618,13 +2643,15 @@ describe('ParticleAPI', () => { contextResult = { def: 456 }; result = 'fake-result'; api._buildContext = sinon.stub().returns(contextResult); + api._getActiveAuthToken = sinon.stub().returns(auth); }); afterEach(() => { expect(api._buildContext).to.have.been.calledWith(context); + expect(api._getActiveAuthToken).to.have.been.calledWith(auth); }); - it('calls _buildContext from get', () => { + it('calls _buildContext and _getActiveAuthToken from get', () => { api.agent.get = sinon.stub().returns(result); const options = { uri, auth, headers, query, context }; const res = api.get(options); @@ -2638,7 +2665,7 @@ describe('ParticleAPI', () => { }); }); - it('calls _buildContext from head', () => { + it('calls _buildContext and _getActiveAuthToken from head', () => { api.agent.head = sinon.stub().returns(result); const options = { uri, auth, headers, query, context }; const res = api.head(options); @@ -2652,7 +2679,7 @@ describe('ParticleAPI', () => { }); }); - it('calls _buildContext from post', () => { + it('calls _buildContext and _getActiveAuthToken from post', () => { api.agent.post = sinon.stub().returns(result); const options = { uri, auth, headers, data, context }; const res = api.post(options); @@ -2666,7 +2693,7 @@ describe('ParticleAPI', () => { }); }); - it('calls _buildContext from put', () => { + it('calls _buildContext and _getActiveAuthToken from put', () => { api.agent.put = sinon.stub().returns(result); const options = { uri, auth, headers, data, context }; const res = api.put(options); @@ -2680,7 +2707,7 @@ describe('ParticleAPI', () => { }); }); - it('calls _buildContext from delete', () => { + it('calls _buildContext and _getActiveAuthToken from delete', () => { api.agent.delete = sinon.stub().returns(result); const options = { uri, auth, headers, data, context }; const res = api.delete(options); @@ -2694,10 +2721,10 @@ describe('ParticleAPI', () => { }); }); - it('calls _buildContext from request', () => { + it('calls _buildContext and _getActiveAuthToken from request', () => { api.agent.request = sinon.stub().returns(result); - api.request({ context }).should.eql(result); - expect(api.agent.request).to.have.been.calledWith({ context:contextResult }); + api.request({ context, auth }).should.eql(result); + expect(api.agent.request).to.have.been.calledWith({ context:contextResult, auth }); }); }); }); @@ -2707,6 +2734,12 @@ describe('ParticleAPI', () => { sinon.restore(); }); + it('sets baseUrl instance property', () => { + const baseUrl = 'foo'; + api.setBaseUrl(baseUrl); + expect(api.baseUrl).to.eql(baseUrl); + }); + it('calls agent.setBaseUrl', () => { const baseUrl = 'foo'; sinon.stub(api.agent, 'setBaseUrl'); @@ -2716,4 +2749,52 @@ describe('ParticleAPI', () => { expect(api.agent.setBaseUrl.firstCall.args[0]).to.eql(baseUrl); }); }); + + describe('setDefaultAuth(auth)', () => { + afterEach(() => { + sinon.restore(); + }); + + it('sets ._defaultAuth', () => { + const auth = 'foo'; + api.setDefaultAuth(auth); + expect(api._defaultAuth).to.eql(auth); + }); + + it('throws error unless given a non-empty string', () => { + let error; + try { + api.setDefaultAuth(undefined); + } catch (e) { + error = e; + } + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.eql('Must pass a non-empty string'); + }); + }); + + describe('_getActiveAuthToken(auth)', () => { + afterEach(() => { + sinon.restore(); + }); + + it('returns provided value when provided value is truthy', () => { + const expectedReturnValue = 'pass through'; + expect(api._getActiveAuthToken(expectedReturnValue)).to.eql(expectedReturnValue); + }); + + it('returns value of _defaultAuth when provided value is NOT truthy', () => { + const providedValue = undefined; + const expectedReturnValue = 'default auth value'; + api.setDefaultAuth(expectedReturnValue); + expect(api._getActiveAuthToken(providedValue)).to.eql(expectedReturnValue); + }); + + it('returns undefined when both provided value and _defaultAuth are NOT truthy', () => { + const providedValue = undefined; + const expectedReturnValue = undefined; + api._defaultAuth = undefined; + expect(api._getActiveAuthToken(providedValue)).to.eql(expectedReturnValue); + }); + }); });