UNPKG

podio-js

Version:

Podio Platform JavaScript SDK for NodeJS and the browser

737 lines (576 loc) 22.2 kB
var transport = require('../lib/transport'); var PodioErrors = require('../lib/PodioErrors'); var sinon = require('sinon'); var _ = require('lodash'); var Promise = require('es6-promise'); Promise = Promise.Promise; // unwrap describe('transport', function() { describe('_addRequestData', function() { it('should attach data as query for a get request and return the request object', function() { var newReq = {}; var data = { authToken: 123 }; var req = { query: sinon.spy(function(data) { return newReq; }) }; var result = transport._addRequestData(data, 'get', req); expect(req.query.calledOnce).toBe(true); expect(req.query.calledWithExactly(data)).toBe(true); expect(result).toEqual(newReq); }); it('should attach data as body for a post request', function() { var data = { authToken: 123 }; var req = { send: sinon.stub() }; transport._addRequestData(data, 'post', req); expect(req.send.calledOnce).toBe(true); expect(req.send.calledWithExactly(data)).toBe(true); }); it('should attach data as body for a put request', function() { var data = { authToken: 123 }; var req = { send: sinon.stub() }; transport._addRequestData(data, 'put', req); expect(req.send.calledOnce).toBe(true); expect(req.send.calledWithExactly(data)).toBe(true); }); }); describe('_addHeaders', function() { it('should set the auth header on the request object and return it', function() { var newReq = {}; var host = { authObject: { accessToken: 123 } }; var req = { set: sinon.stub().returns(newReq) }; var result = transport._addHeaders.call(host, req); expect(req.set.calledOnce).toBe(true); expect(req.set.calledWithExactly('Authorization', 'OAuth2 123')).toBe(true); expect(result).toEqual(newReq); }); }); describe('_addCORS', function() { it('should enable cookies for cross domain requests', function() { var host = { authType: 'client' }; var req = { withCredentials: sinon.spy(function() { return req; }) }; expect(transport._addCORS.call(host, req)).toEqual(req); expect(req.withCredentials.calledOnce).toBe(true); }); it('should not enable cookies if no client auth is used', function() { var host = { authType: 'server' }; var req = { withCredentials: sinon.stub() }; expect(transport._addCORS.call(host, req)).toEqual(req); expect(req.withCredentials.called).toBe(false); }); }); describe('_setOptions', function() { it('should set the application/x-www-form-urlencoded content type', function() { var req = { type: sinon.stub() }; var options = { type: 'form' }; transport._setOptions(options, req); expect(req.type.calledOnce).toBe(true); expect(req.type.calledWithExactly('form')).toBe(true); }); it('should not call type on the request object if no type option is passed', function() { var req = { type: sinon.stub() }; var options = {}; transport._setOptions(options, req); expect(req.type.calledOnce).toBe(false); }); it('should set the authorization header when basicAuth option is passed', function () { var req = { set: sinon.stub() }; var options = { basicAuth: true }; transport.clientId = '123'; transport.clientSecret = 'abc'; var basicAuth = 'Basic ' + new Buffer(transport.clientId + ':' + transport.clientSecret).toString('base64'); transport._setOptions(options, req); expect(req.set.calledWithExactly('Authorization', basicAuth)).toBe(true); }); }); describe('_onResponse', function() { it('should call resolve and callback if response was a success', function() { var options = { resolve: sinon.stub(), reject: sinon.stub() }; var res = { body: {}, ok: true }; transport._onResponse(options, null, res); expect(options.resolve.calledOnce).toBe(true); expect(options.resolve.calledWithExactly(res.body)).toBe(true); }); it('should call reject and handle the error if response was a failure different from HTTP 401', function() { var options = { reject: sinon.stub(), requestParams: { url: 'http://url' } }; var res = { ok: false, body: { error_description: 'Error occured' }, status: 404, err: {} }; transport._onResponse.call(transport, options, res.err, res); var expectedError = new PodioErrors.PodioNotFoundError(res.body, res.status, options.requestParams.url); expect(options.reject.calledOnce).toBe(true); expect(options.reject.calledWithExactly(expectedError)).toBe(true); }); it('should try to refresh token when the token expires with an HTTP 401', function() { var response = { status: 401, body: { error_description: 'expired_token' }, ok: false, err: {} // superagent error }; var options = { requestParams: {}, resolve: sinon.stub(), reject: sinon.stub() }; var podioAuthorizationError = new PodioErrors.PodioAuthorizationError(response.body, response.status, ''); var auth = { _refreshToken: sinon.stub().callsArg(0), _clearAuthentication: sinon.stub() }; var host = { authObject: { refreshToken: 'abc1234' }, _getAuth: sinon.stub().returns(auth), _onTokenRefreshed: sinon.stub().returns(new Promise(function(){})), _handleTransportError: sinon.spy(function () { auth._clearAuthentication(); return podioAuthorizationError; }), afterTokenRefreshed: sinon.stub() }; transport._onResponse.call(host, options, response.err, response); expect(auth._clearAuthentication.calledOnce).toBe(true); expect(auth._refreshToken.calledOnce).toBe(true); expect(_.isFunction(auth._refreshToken.getCall(0).args[0])).toBe(true); expect(host._onTokenRefreshed.calledOnce).toBe(true); expect(host._onTokenRefreshed.calledWithExactly(options.requestParams)).toBe(true); expect(host.afterTokenRefreshed.calledOnce).toBe(true); }); it('should handle the error if response was a HTTP 401 but not a token expiration', function() { var res = { ok: false, body: {}, status: 401, err: new Error() }; var options = { requestParams: {}, reject: sinon.stub() }; var host = { _handleTransportError: sinon.stub() }; transport._onResponse.call(host, options, new PodioErrors.PodioAuthorizationError(), res); expect(host._handleTransportError.calledOnce).toBe(true); expect(options.reject.calledOnce).toBe(true); }); it('should handle the error if response is a HTTP 401 with an expired token error, but the refresh token is missing', function() { var res = { ok: false, body: { error: 'invalid_token' }, status: 401 }; var options = { requestParams: {}, reject: function() {} }; var host = { _handleTransportError: sinon.stub(), authObject: { authToken: 123 } }; transport._onResponse.call(host, options, new PodioErrors.PodioAuthorizationError(), res); expect(host._handleTransportError.calledOnce).toBe(true); }); }); describe('_getRequestURI', function() { var host; beforeEach(function() { host = { apiURL: 'https://api.podio.com:443' }; }); it('should return the correct url with only path provided', function() { var path = '/test'; var result = transport._getRequestURI.call(host, path); expect(result).toEqual('https://api.podio.com:443/test'); }); it('should return the correct url with both path and query provided', function() { var path = '/test?param=true'; var result = transport._getRequestURI.call(host, path); expect(result).toEqual('https://api.podio.com:443/test?param=true'); }); }); describe('request', function() { var request, host, auth; beforeEach(function() { request = { get: sinon.spy(function() { return request; }), end: sinon.stub() }; auth = { isAuthenticated: sinon.stub().returns(true) }; host = { _getRequestURI: sinon.stub(), _getAuth: sinon.stub().returns(auth), _getRequestObject: sinon.stub().returns(request), _formatMethod: sinon.stub().returns('get'), _addRequestData: sinon.spy(function(data, method, req) { req.requestData = {}; return req; }), _addHeaders: sinon.spy(function(req) { req.headers = {}; return req; }), _addCORS: sinon.spy(function(req) { req.cors = {}; return req; }), _setOptions: sinon.spy(function(options, req) { req.options = {}; return req; }), _getPromise: sinon.stub().callsArg(1), _onResponse: function() {}, apiURL: 'https://api.podio.com:443' } }); afterEach(function() { host = request = null; }); it('should call the correct method on the request object with a correct URL', function() { var path = '/test'; var url = 'https://api.podio.com:443/test'; host._getRequestURI = sinon.stub().returns(url); transport.request.call(host, 'GET', path, null, function(responseData) {}); expect(host._getRequestURI.calledOnce).toBe(true); expect(host._getRequestURI.calledWithExactly(path)).toBe(true); }); it('should throw an exception if authentication has not been performed before', function(done) { var PodioForbiddenError = function(message, status, url) { this.message = message; this.status = status; this.url = url; this.name = 'PodioForbiddenError'; }; auth.isAuthenticated = sinon.stub().returns(false); transport.request.call(host, 'get', '/test').then(function(){}, function (err) { expect(err).toEqual(new PodioForbiddenError('Authentication has not been performed')); done(); }); }); it('should call addRequestData, addHeaders, addCORS, setOptions with the request object and let them augment it', function() { var data = { data: true }; host.authObject = {}; transport.request.call(host, 'GET', '/test', data); expect(host._addRequestData.calledOnce).toBe(true); expect(host._addRequestData.calledWith(data, 'get')).toBe(true); expect(host._addHeaders.calledOnce).toBe(true); expect(host._addCORS.calledOnce).toBe(true); expect(host._setOptions.calledOnce).toBe(true); expect(request.end.getCall(0).thisValue).toEqual(request); // request has been augmented at this point }); it('should call only addRequestData and setOptions with the request object when basicAuth is true and let them augment it', function() { var data = { data: true }; transport.request.call(host, 'GET', '/test', data, { basicAuth: true }); expect(host._addRequestData.calledOnce).toBe(true); expect(host._addRequestData.calledWith(data, 'get')).toBe(true); expect(host._addHeaders.called).toBe(false); expect(host._addCORS.called).toBe(false); expect(host._setOptions.calledOnce).toBe(true); expect(request.end.getCall(0).thisValue).toEqual(request); // request has been augmented at this point }); it('should format the method passed', function() { host.authObject = {}; transport.request.call(host, 'GET', '/test', null, function(responseData) {}); expect(host._formatMethod.calledOnce).toBe(true); }); }); describe('uploadFile', function() { function getFullMocks() { var host; var req = { end: sinon.stub() }; var field = { field: sinon.stub().returns(req) }; var attach = { attach: sinon.stub().returns(field)}; req.post = sinon.stub().returns(attach); host = { _getRequestObject: sinon.stub().returns(req), _addHeaders: sinon.stub().returns(req), _addCORS: sinon.stub().returns(req), _getPromise: sinon.stub(), apiURL: 'https://api.podio.com:443', authType: 'server' }; return { host: host, req: req, attach: attach, field: field }; } it('should raise an exception when called on the client', function(done) { var host = { authType: 'client', apiURL: 'https://api.podio.com:443' }; transport.uploadFile.call(host).then(function () {}, function (err) { expect(err).toEqual(new PodioErrors.PodioError('File uploads are only supported on the server right now.')); done(); }); }); it('should assemble the url correctly', function() { var mocks = getFullMocks(); transport.uploadFile.call(mocks.host); expect(mocks.req.post.calledOnce).toBe(true); expect(mocks.req.post.calledWithExactly('https://api.podio.com:443/file')); }); it('it should assemble the body correctly', function() { var mocks = getFullMocks(); var filePath = 'tmp/image.png'; var fileName = 'image.png'; transport.uploadFile.call(mocks.host, filePath, fileName); expect(mocks.attach.attach.calledOnce).toBe(true); expect(mocks.attach.attach.calledWithExactly('source', filePath)).toBe(true); expect(mocks.field.field.calledOnce).toBe(true); expect(mocks.field.field.calledWithExactly('filename', fileName)).toBe(true); }); it('should add auth and CORS headers to the request', function() { var mocks = getFullMocks(); transport.uploadFile.call(mocks.host); expect(mocks.host._addHeaders.calledOnce).toBe(true); expect(mocks.host._addHeaders.calledWithExactly(mocks.req)).toBe(true); expect(mocks.host._addCORS.calledOnce).toBe(true); expect(mocks.host._addCORS.calledWithExactly(mocks.req)).toBe(true); }); }); describe('_onTokenRefreshed', function() { it('should repeat a failed generic request with old parameters passed as options', function() { var host = { request: sinon.stub(), uploadFile: sinon.stub() }; var request = { requestType: 'generic', method: 'GET', path: '/tasks', data: {} }; transport._onTokenRefreshed.call(host, request); expect(host.request.calledOnce).toBe(true); expect(host.request.calledWithExactly(request.method, request.path, request.data)); expect(host.uploadFile.called).toBe(false); }); it('should repeat a file upload with old parameters passed as options', function() { var host = { request: sinon.stub(), uploadFile: sinon.stub() }; var request = { requestType: 'file', filePath: 'temp/image.png', fileName: 'image.png' }; transport._onTokenRefreshed.call(host, request); expect(host.uploadFile.calledOnce).toBe(true); expect(host.uploadFile.calledWithExactly(request.filePath, request.fileName)).toBe(true); expect(host.request.called).toBe(false); }); }); describe('_handleTransportError', function() { beforeEach(function() { sinon.spy(transport, '_handleTransportError'); }); afterEach(function() { transport._handleTransportError.restore(); }); it('should throw a PodioBadRequestError with the right parameters', function() { var options = { requestParams: { url: 'https://example.com' } }; var response = { body: {}, status: 400 }; var expectedError = new PodioErrors.PodioBadRequestError(response.body, response.status, options.requestParams.url); expect(transport._handleTransportError(options, response)).toEqual(expectedError); }); it('should throw a PodioAuthorizationError with the right parameters and clear the authentication', function() { var options = { requestParams: { url: 'https://example.com' } }; var response = { body: {}, status: 401 }; var auth = { _clearAuthentication: sinon.stub() }; var host = { _getAuth: sinon.stub().returns(auth) }; var expectedError = new PodioErrors.PodioAuthorizationError(response.body, response.status, options.requestParams.url); expect(transport._handleTransportError.call(host, options, response)).toEqual(expectedError); expect(auth._clearAuthentication.calledOnce).toBe(true); }); it('should throw a PodioForbiddenError with the right parameters', function() { var options = { requestParams: { url: 'https://example.com' } }; var response = { body: {}, status: 403 }; var expectedError = new PodioErrors.PodioForbiddenError(response.body, response.status, options.requestParams.url); expect(transport._handleTransportError(options, response)).toEqual(expectedError); }); it('should throw a PodioNotFoundError with the right parameters', function() { var options = { requestParams: { url: 'https://example.com' } }; var response = { body: {}, status: 404 }; var expectedError = new PodioErrors.PodioNotFoundError(response.body, response.status, options.requestParams.url); expect(transport._handleTransportError(options, response)).toEqual(expectedError); }); it('should throw a PodioConflictError with the right parameters', function() { var options = { requestParams: { url: 'https://example.com' } }; var response = { body: {}, status: 409 }; var expectedError = new PodioErrors.PodioConflictError(response.body, response.status, options.requestParams.url); expect(transport._handleTransportError(options, response)).toEqual(expectedError); }); it('should throw a PodioGoneError with the right parameters', function() { var options = { requestParams: { url: 'https://example.com' } }; var response = { body: {}, status: 410 }; var expectedError = new PodioErrors.PodioGoneError(response.body, response.status, options.requestParams.url); expect(transport._handleTransportError(options, response)).toEqual(expectedError); }); it('should throw a PodioRateLimitError with the right parameters', function() { var options = { requestParams: { url: 'https://example.com' } }; var response = { body: {}, status: 420 }; var expectedError = new PodioErrors.PodioRateLimitError(response.body, response.status, options.requestParams.url); expect(transport._handleTransportError(options, response)).toEqual(expectedError); }); it('should throw a PodioServerError with the right parameters', function() { var options = { requestParams: { url: 'https://example.com' } }; var response = { body: {}, status: 500 }; var expectedError = new PodioErrors.PodioServerError(response.body, response.status, options.requestParams.url); expect(transport._handleTransportError(options, response)).toEqual(expectedError); }); it('should throw a PodioUnavailableError 502 with the right parameters', function() { var options = { requestParams: { url: 'https://example.com' } }; var response = { body: {}, status: 502 }; var expectedError = new PodioErrors.PodioUnavailableError(response.body, response.status, options.requestParams.url); expect(transport._handleTransportError(options, response)).toEqual(expectedError); }); it('should throw a PodioUnavailableError 503 with the right parameters', function() { var options = { requestParams: { url: 'https://example.com' } }; var response = { body: {}, status: 503 }; var expectedError = new PodioErrors.PodioUnavailableError(response.body, response.status, options.requestParams.url); expect(transport._handleTransportError(options, response)).toEqual(expectedError); }); it('should throw a PodioUnavailableError 504 with the right parameters', function() { var options = { requestParams: { url: 'https://example.com' } }; var response = { body: {}, status: 504 }; var expectedError = new PodioErrors.PodioUnavailableError(response.body, response.status, options.requestParams.url); expect(transport._handleTransportError(options, response)).toEqual(expectedError); }); it('should throw a PodioError with the right parameters', function() { var options = { requestParams: { url: 'https://example.com' } }; var response = { body: {} }; var expectedError = new PodioErrors.PodioError(response.body, response.status, options.requestParams.url); expect(transport._handleTransportError(options, response)).toEqual(expectedError); }); }); describe('_formatMethod', function() { it('should transform a method other than delete to lowercase', function() { var result = transport._formatMethod('GET'); expect(result).toEqual('get'); }); it('should return del when delete is used as the method name', function() { var result = transport._formatMethod('DELETE'); expect(result).toEqual('del'); }); }); });