From b3d1ad2d575552e11b36f7a307050d79f8bc90d3 Mon Sep 17 00:00:00 2001 From: Stephen Sawchuk Date: Mon, 23 Mar 2015 12:09:39 -0400 Subject: [PATCH] integrate google-auth-library --- .jshintrc | 10 +- lib/bigquery/index.js | 3 +- lib/common/util.js | 263 ++++++++----- lib/datastore/dataset.js | 3 +- lib/pubsub/index.js | 3 +- lib/storage/index.js | 3 +- package.json | 2 +- regression/index.js | 61 +++ test/common/util.js | 815 ++++++++++++++++++++++++++------------- test/pubsub/index.js | 16 +- 10 files changed, 799 insertions(+), 380 deletions(-) create mode 100644 regression/index.js diff --git a/.jshintrc b/.jshintrc index 42d4fa95a8f..4c934ed6055 100644 --- a/.jshintrc +++ b/.jshintrc @@ -15,5 +15,13 @@ "strict": true, "trailing": true, "undef": true, - "unused": true + "unused": true, + "globals": { + "describe": true, + "it": true, + "before": true, + "after": true, + "beforeEach": true, + "afterEach": true + } } diff --git a/lib/bigquery/index.js b/lib/bigquery/index.js index 24f308cad8e..6f5d35aeddb 100644 --- a/lib/bigquery/index.js +++ b/lib/bigquery/index.js @@ -112,7 +112,8 @@ function BigQuery(options) { this.makeAuthorizedRequest_ = util.makeAuthorizedRequest({ credentials: options.credentials, keyFile: options.keyFilename, - scopes: SCOPES + scopes: SCOPES, + email: options.email }); this.projectId = options.projectId; diff --git a/lib/common/util.js b/lib/common/util.js index 762f39db469..1997d93cd57 100644 --- a/lib/common/util.js +++ b/lib/common/util.js @@ -22,20 +22,19 @@ */ var extend = require('extend'); -var gsa = require('google-service-account'); +var GoogleAuth = require('google-auth-library'); var request = require('request'); -var util = require('util'); +var nodeutil = require('util'); var uuid = require('node-uuid'); -/** @const {number} Maximum amount of times to attempt refreshing a token. */ -var MAX_TOKEN_REFRESH_ATTEMPTS = 1; - /** @const {object} gcloud-node's package.json file. */ var PKG = require('../../package.json'); /** @const {string} User agent. */ var USER_AGENT = 'gcloud-node/' + PKG.version; +var util = module.exports; + /** * Extend a global configuration object with user options provided at the time * of sub-module instantiation. @@ -76,7 +75,7 @@ function extendGlobalConfig(globalConfig, overrides) { return extend(true, {}, options, overrides); } -module.exports.extendGlobalConfig = extendGlobalConfig; +util.extendGlobalConfig = extendGlobalConfig; /** * Wrap an array around a non-Array object. If given an Array, it is returned. @@ -103,7 +102,7 @@ function arrayize(input) { return input; } -module.exports.arrayize = arrayize; +util.arrayize = arrayize; /** * Format a string with values from the provided object. @@ -125,7 +124,7 @@ function format(template, args) { }); } -module.exports.format = format; +util.format = format; /** * No op. @@ -137,7 +136,7 @@ module.exports.format = format; */ function noop() {} -module.exports.noop = noop; +util.noop = noop; /** * Extend the native Error object. @@ -155,7 +154,7 @@ function ApiError(errorBody) { this.response = errorBody.response; } -util.inherits(ApiError, Error); +nodeutil.inherits(ApiError, Error); /** * Uniformly process an API response. @@ -194,7 +193,7 @@ function handleResp(err, resp, body, callback) { callback(null, body, resp); } -module.exports.handleResp = handleResp; +util.handleResp = handleResp; /** * Get the type of a value. @@ -252,7 +251,7 @@ function prop(name) { }; } -module.exports.prop = prop; +util.prop = prop; /** * Assign a value to a property in an Array iterator. @@ -268,7 +267,7 @@ function propAssign(prop, value) { }; } -module.exports.propAssign = propAssign; +util.propAssign = propAssign; /** * Check if an object is of the given type. @@ -285,7 +284,7 @@ function is(value, type) { return getType(value).toLowerCase() === type.toLowerCase(); } -module.exports.is = is; +util.is = is; /** * Convert an object into an array. @@ -305,7 +304,7 @@ function toArray(object) { return [].slice.call(object); } -module.exports.toArray = toArray; +util.toArray = toArray; /** * Take a Duplexify stream, fetch an authorized connection header, and create an @@ -407,13 +406,13 @@ function makeWritableStream(dup, options, onComplete) { }); } -module.exports.makeWritableStream = makeWritableStream; +util.makeWritableStream = makeWritableStream; /** * Returns an exponential distributed time to wait given the number of retries * that have been previously been attempted on the request. * - * @param {number} retryNumber - The number of retries previously attempted. + * @param {number} retryNumber - The number of retries previously attempted. * @return {number} An exponentially distributed time to wait E.g. for use with * exponential backoff. */ @@ -421,114 +420,196 @@ function getNextRetryWait(retryNumber) { return (Math.pow(2, retryNumber) * 1000) + Math.floor(Math.random() * 1000); } -module.exports.getNextRetryWait = getNextRetryWait; +util.getNextRetryWait = getNextRetryWait; /** * Returns true if the API request should be retried, given the error that was * given the first time the request was attempted. This is used for rate limit * related errors as well as intermittent server errors. * - * @param {error} err - The API error to check if it is appropriate to retry. + * @param {error} err - The API error to check if it is appropriate to retry. * @return {boolean} True if the API request should be retried, false otherwise. */ -function shouldRetry(err) { +function shouldRetryRequest(err) { return !!err && [429, 500, 503].indexOf(err.code) !== -1; } -module.exports.shouldRetryErr = shouldRetry; - -function makeAuthorizedRequest(config) { - var GAE_OR_GCE = !config || (!config.credentials && !config.keyFile); - var MAX_RETRIES = config && config.maxRetries || 3; - var autoRetry = !config || config.autoRetry !== false ? true : false; - var attemptedRetries = 0; +util.shouldRetryRequest = shouldRetryRequest; - var missingCredentialsError = new Error(); - missingCredentialsError.message = [ - 'A connection to gcloud must be established via either a `keyFilename` ', - 'property or a `credentials` object.', - '\n\n', - 'See the "Getting Started with gcloud" section for more information:', - '\n\n', - '\thttps://googlecloudplatform.github.io/gcloud-node/#/docs/', - '\n' - ].join(''); - - var authorize; - - if (config.customEndpoint) { - // Using a custom API override. Do not use `google-service-account` for - // authentication. (ex: connecting to a local Datastore server) - authorize = function(reqOpts, callback) { - callback(null, reqOpts); - }; +/** + * Create an Auth Client from Google Auth Library, used to get an access token + * for authenticating API requests. + * + * @param {object} config - Configuration object. + * @param {object=} config.credentials - Credentials object. + * @param {string=} config.email - Account email address, required for PEM/P12 + * usage. + * @param {string=} config.keyFile - Path to a .json, .pem, or .p12 keyfile. + * @param {array} config.scopes - Array of scopes required for the API. + * @param {function} callback - The callback function. + */ +function getAuthClient(config, callback) { + var googleAuth = new GoogleAuth(); + + if (config.keyFile) { + var authClient = new googleAuth.JWT(); + authClient.keyFile = config.keyFile; + authClient.email = config.email; + authClient.scopes = config.scopes; + addScope(null, authClient); + } else if (config.credentials) { + googleAuth.fromJSON(config.credentials, addScope); } else { - authorize = gsa(config); + googleAuth.getApplicationDefault(addScope); } - function makeRequest(reqOpts, callback) { - var tokenRefreshAttempts = 0; - reqOpts.headers = reqOpts.headers || {}; + function addScope(err, authClient) { + if (err) { + callback(err); + return; + } - if (reqOpts.headers['User-Agent']) { - reqOpts.headers['User-Agent'] += '; ' + USER_AGENT; - } else { - reqOpts.headers['User-Agent'] = USER_AGENT; + if (authClient.createScopedRequired && authClient.createScopedRequired()) { + authClient = authClient.createScoped(config.scopes); + } + + callback(null, authClient); + } +} + +util.getAuthClient = getAuthClient; + +/** + * Authenticate a request by extending its headers object with an access token. + * + * @param {object} config - Configuration object. + * @param {object=} config.credentials - Credentials object. + * @param {string=} config.email - Account email address, required for PEM/P12 + * usage. + * @param {string=} config.keyFile - Path to a .json, .pem, or .p12 keyfile. + * @param {array} config.scopes - Array of scopes required for the API. + * @param {object} reqOpts - HTTP request options. Its `headers` object is + * created or extended with a valid access token. + * @param {function} callback - The callback function. + */ +function authorizeRequest(config, reqOpts, callback) { + util.getAuthClient(config, function(err, authClient) { + if (err) { + // google-auth-library returns a "Could not load..." error if it can't get + // an access token. However, it's possible an API request doesn't need to + // be authenticated, e.g. when downloading a file from a public bucket. We + // consider this error a warning, and allow the request to go through + // without authorization, relying on the upstream API to return an error + // the user would find more helpful, should one occur. + if (err.message.indexOf('Could not load') === 0) { + callback(null, reqOpts); + } else { + callback(err); + } + return; } - function onAuthorizedRequest(err, authorizedReqOpts) { + authClient.getAccessToken(function(err, token) { if (err) { - if (GAE_OR_GCE && err.code === 'ENOTFOUND') { - // The metadata server wasn't found. The user must not actually be in - // a GAE or GCE environment. - throw missingCredentialsError; + callback(err); + return; + } + + var authorizedReqOpts = extend(true, {}, reqOpts, { + headers: { + Authorization: 'Bearer ' + token } + }); + + callback(null, authorizedReqOpts); + }); + }); +} + +util.authorizeRequest = authorizeRequest; + +/** + * Get a function for making authorized requests. + * + * @param {object} config - Configuration object. + * @param {object=} config.credentials - Credentials object. + * @param {boolean=} config.customEndpoint - If true, just return the provided + * request options. Default: false. + * @param {string=} config.email - Account email address, required for PEM/P12 + * usage. + * @param {string=} config.keyFile - Path to a .json, .pem, or .p12 keyfile. + * @param {array} config.scopes - Array of scopes required for the API. + */ +function makeAuthorizedRequest(config) { + config = config || {}; - if (err.code === 401 && - ++tokenRefreshAttempts <= MAX_TOKEN_REFRESH_ATTEMPTS) { - authorize(reqOpts, onAuthorizedRequest); + var MAX_RETRIES = config.maxRetries || 3; + var autoRetry = config.autoRetry !== false ? true : false; + var attemptedRetries = 0; + + function makeRequest(reqOpts, callback) { + if (config.customEndpoint) { + // Using a custom API override. Do not use `google-auth-library` for + // authentication. (ex: connecting to a local Datastore server) + (callback.onAuthorized || callback)(null, reqOpts); + } else { + util.authorizeRequest(config, reqOpts, function(err, authorizedReqOpts) { + if (err) { + (callback.onAuthorized || callback)(err); return; } - // For detecting Sign errors on io.js (1.x) (or node 0.11.x) - // E.g. errors in form: error:code:PEM routines:PEM_read_bio:error_name - var pemError = err.message && - err.message.indexOf('error:') !== -1; + authorizedReqOpts.headers = authorizedReqOpts.headers || {}; + authorizedReqOpts.headers['User-Agent'] = USER_AGENT; - if (err.message === 'SignFinal error' || pemError) { - err.message = [ - 'Your private key is in an unexpected format and cannot be used.', - 'Please try again with another private key.' - ].join(' '); + if (callback.onAuthorized) { + callback.onAuthorized(null, authorizedReqOpts); + } else { + makeRateLimitedRequest(); } - (callback.onAuthorized || callback)(err); - return; - } + function makeRateLimitedRequest() { + request(authorizedReqOpts, function(err, res, body) { + if (err && shouldRetry(err)) { + var delay = util.getNextRetryWait(attemptedRetries++); + setTimeout(makeRateLimitedRequest, delay); + } else { + util.handleResp(err, res, body, callback); + } + }); + } - function handleRateLimitResp(err, res, body) { - if (shouldRetry(err) && autoRetry && MAX_RETRIES > attemptedRetries) { - setTimeout(function() { - request(authorizedReqOpts, handleRateLimitResp); - }, getNextRetryWait(attemptedRetries++)); - } else { - handleResp(err, res, body, callback); + function shouldRetry(err) { + return autoRetry && + MAX_RETRIES > attemptedRetries && + util.shouldRetryRequest(err); } - } + }); + } + } - if (callback.onAuthorized) { - callback.onAuthorized(null, authorizedReqOpts); - } else { - request(authorizedReqOpts, handleRateLimitResp); + makeRequest.getCredentials = function(callback) { + util.getAuthClient(config, function(err, authClient) { + if (err) { + callback(err); + return; } - } - authorize(reqOpts, onAuthorizedRequest); - } + authClient.authorize(function(err) { + if (err) { + callback(err); + return; + } - makeRequest.getCredentials = authorize.getCredentials; + callback(null, { + client_email: authClient.email, + private_key: authClient.key + }); + }); + }); + }; return makeRequest; } -module.exports.makeAuthorizedRequest = makeAuthorizedRequest; +util.makeAuthorizedRequest = makeAuthorizedRequest; diff --git a/lib/datastore/dataset.js b/lib/datastore/dataset.js index e8ed4fdcaeb..2623a8ac1d9 100644 --- a/lib/datastore/dataset.js +++ b/lib/datastore/dataset.js @@ -108,7 +108,8 @@ function Dataset(options) { customEndpoint: typeof options.apiEndpoint !== 'undefined', credentials: options.credentials, keyFile: options.keyFilename, - scopes: SCOPES + scopes: SCOPES, + email: options.email }); if (options.apiEndpoint && options.apiEndpoint.indexOf('http') !== 0) { diff --git a/lib/pubsub/index.js b/lib/pubsub/index.js index 14fa0ae7b8c..4d4874b7f9b 100644 --- a/lib/pubsub/index.js +++ b/lib/pubsub/index.js @@ -112,7 +112,8 @@ function PubSub(options) { this.makeAuthorizedRequest_ = util.makeAuthorizedRequest({ credentials: options.credentials, keyFile: options.keyFilename, - scopes: SCOPES + scopes: SCOPES, + email: options.email }); this.projectId = options.projectId; diff --git a/lib/storage/index.js b/lib/storage/index.js index 2fb51e82a12..2d2a77e57fb 100644 --- a/lib/storage/index.js +++ b/lib/storage/index.js @@ -107,7 +107,8 @@ function Storage(config) { this.makeAuthorizedRequest_ = util.makeAuthorizedRequest({ credentials: config.credentials, keyFile: config.keyFilename, - scopes: SCOPES + scopes: SCOPES, + email: config.email }); this.projectId = config.projectId; diff --git a/package.json b/package.json index 27e78ae33c4..0d325d7ed07 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "duplexify": "^3.2.0", "extend": "^2.0.0", "fast-crc32c": "^0.1.3", - "google-service-account": "^1.0.3", + "google-auth-library": "google/google-auth-library-nodejs#master", "mime-types": "^2.0.8", "node-uuid": "^1.4.2", "once": "^1.3.1", diff --git a/regression/index.js b/regression/index.js new file mode 100644 index 00000000000..354e00c2083 --- /dev/null +++ b/regression/index.js @@ -0,0 +1,61 @@ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var env = require('./env'); +var gcloud = require('../lib'); +var uuid = require('node-uuid'); + +// Test used to confirm we can perform a successful API operation. +function canConnect(config, callback) { + var storage = gcloud.storage(config); + + var BUCKET_NAME = 'gcloud-test-bucket-temp-' + uuid.v1(); + storage.createBucket(BUCKET_NAME, function(err, bucket) { + if (err) { + callback(err); + return; + } + + bucket.delete(callback); + }); +} + +describe('environment', function() { + it('should connect with credentials object', canConnect.bind(null, { + projectId: env.projectId, + credentials: require(env.keyFilename) + })); + + it('should connect from a JSON keyFilename', canConnect.bind(null, { + projectId: env.projectId, + keyFilename: env.keyFilename + })); + + it('should connect from environment variable', function(done) { + var ENV_VAR = 'GOOGLE_APPLICATIONS_CREDENTIALS'; + + process.env[ENV_VAR] = env.keyFilename; + + canConnect({ projectId: env.projectId }, function(err) { + assert.ifError(err); + delete process.env[ENV_VAR]; + done(); + }); + }); +}); diff --git a/test/common/util.js b/test/common/util.js index e7c287f307a..2a91f810cc2 100644 --- a/test/common/util.js +++ b/test/common/util.js @@ -20,35 +20,45 @@ var assert = require('assert'); var duplexify = require('duplexify'); -var gsa = require('google-service-account'); +var extend = require('extend'); +var googleAuthLibrary = require('google-auth-library'); var mockery = require('mockery'); var request = require('request'); var stream = require('stream'); -var gsa_Override; -function fakeGsa() { - var args = [].slice.apply(arguments); - var results = (gsa_Override || gsa).apply(null, args); - return results || { getCredentials: function() {} }; +var googleAuthLibrary_Override; +function fakeGoogleAuthLibrary() { + return (googleAuthLibrary_Override || googleAuthLibrary) + .apply(null, arguments); } var request_Override; function fakeRequest() { - var args = [].slice.apply(arguments); - return (request_Override || request).apply(null, args); + return (request_Override || request).apply(null, arguments); } describe('common/util', function() { var util; + var utilOverrides = {}; before(function() { - mockery.registerMock('google-service-account', fakeGsa); + mockery.registerMock('google-auth-library', fakeGoogleAuthLibrary); mockery.registerMock('request', fakeRequest); mockery.enable({ useCleanCache: true, warnOnUnregistered: false }); util = require('../../lib/common/util'); + var util_Cached = extend(true, {}, util); + + // Override all util methods, allowing them to be mocked. Overrides are + // removed before each test. + Object.keys(util).forEach(function(utilMethod) { + util[utilMethod] = function() { + return (utilOverrides[utilMethod] || util_Cached[utilMethod]) + .apply(this, arguments); + }; + }); }); after(function() { @@ -57,8 +67,9 @@ describe('common/util', function() { }); beforeEach(function() { - gsa_Override = null; + googleAuthLibrary_Override = null; request_Override = null; + utilOverrides = {}; }); describe('arrayize', function() { @@ -287,363 +298,567 @@ describe('common/util', function() { }); }); - describe('makeAuthorizedRequest', function() { - it('should pass configuration to gsa', function(done) { - var config = { keyFile: 'key', scopes: [1, 2] }; + describe('getAuthClient', function() { + it('should use google-auth-library', function() { + var googleAuthLibraryCalled = false; - gsa_Override = function(cfg) { - assert.deepEqual(cfg, config); - done(); + googleAuthLibrary_Override = function() { + googleAuthLibraryCalled = true; + return { + getApplicationDefault: util.noop + }; }; - util.makeAuthorizedRequest(config); + util.getAuthClient({}); + + assert.strictEqual(googleAuthLibraryCalled, true); }); - it('should not authenticate requests with a custom API', function(done) { - var makeRequest = util.makeAuthorizedRequest({ customEndpoint: true }); + it('should create a JWT auth client from a keyFile', function(done) { + var jwt = {}; - var gsaCalled = false; - gsa_Override = function() { - gsaCalled = true; + googleAuthLibrary_Override = function() { + return { + JWT: function() { return jwt; } + }; }; - makeRequest({}, { - onAuthorized: function(err) { - assert.ifError(err); - assert.strictEqual(gsaCalled, false); - done(); - } + var config = { + keyFile: 'key.json', + email: 'example@example.com', + scopes: ['dev.scope'] + }; + + util.getAuthClient(config, function(err, authClient) { + assert.ifError(err); + + assert.equal(jwt.keyFile, config.keyFile); + assert.equal(jwt.email, config.email); + assert.deepEqual(jwt.scopes, config.scopes); + + assert.deepEqual(authClient, jwt); + + done(); }); }); - it('should return gsa.getCredentials function', function() { - var getCredentials = util.makeAuthorizedRequest({}).getCredentials; - assert.equal(typeof getCredentials, 'function'); - }); + it('should create an auth client from credentials', function(done) { + var credentialsSet; - describe('makeRequest', function() { - it('should add a user agent onto headers', function(done) { - gsa_Override = function() { - return function authorize(reqOpts) { - assert(reqOpts.headers['User-Agent'].indexOf('gcloud') > -1); - done(); - }; + googleAuthLibrary_Override = function() { + return { + fromJSON: function(credentials, callback) { + credentialsSet = credentials; + callback(null, {}); + } }; + }; + + var config = { + credentials: { a: 'b', c: 'd' } + }; - var makeRequest = util.makeAuthorizedRequest({}); - makeRequest({}); + util.getAuthClient(config, function() { + assert.deepEqual(credentialsSet, config.credentials); + done(); }); + }); - it('should extend an existing user agent', function(done) { - gsa_Override = function() { - return function authorize(reqOpts) { - var index = reqOpts.headers['User-Agent'].indexOf('test; gcloud'); - assert.equal(index, 0); - done(); - }; + it('should create an auth client from magic', function(done) { + googleAuthLibrary_Override = function() { + return { + getApplicationDefault: function(callback) { + callback(null, {}); + } }; + }; - var makeRequest = util.makeAuthorizedRequest({}); - makeRequest({ headers: { 'User-Agent': 'test' } }); - }); + util.getAuthClient({}, done); + }); - it('should execute callback with error', function(done) { - var error = new Error('Error.'); + it('should scope an auth client if necessary', function(done) { + var config = { + scopes: ['a.scope', 'b.scope'] + }; - gsa_Override = function() { - return function authorize(reqOpts, callback) { - callback(error); - }; + var fakeAuthClient = { + createScopedRequired: function() { + return true; + }, + createScoped: function(scopes) { + assert.deepEqual(scopes, config.scopes); + done(); + return fakeAuthClient; + }, + getAccessToken: function() {} + }; + + googleAuthLibrary_Override = function() { + return { + getApplicationDefault: function(callback) { + callback(null, fakeAuthClient); + } }; + }; + + util.getAuthClient(config, assert.ifError); + }); + }); + + describe('authorizeRequest', function() { + it('should gets an auth client', function(done) { + var config = { a: 'b', c: 'd' }; - var makeRequest = util.makeAuthorizedRequest({}); - makeRequest({}, function(err) { - assert.equal(err, error); + utilOverrides.getAuthClient = function(cfg) { + assert.deepEqual(cfg, config); + done(); + }; + + util.authorizeRequest(config); + }); + + it('should ignore "Could not load" error from google-auth', function(done) { + var reqOpts = { a: 'b', c: 'd' }; + var couldNotLoadError = new Error('Could not load'); + + utilOverrides.getAuthClient = function(config, callback) { + callback(couldNotLoadError); + }; + + util.authorizeRequest({}, reqOpts, function(err, authorizedReqOpts) { + assert.ifError(err); + assert.deepEqual(reqOpts, authorizedReqOpts); + done(); + }); + }); + + it('should return an error to the callback', function(done) { + var error = new Error('Error.'); + + utilOverrides.getAuthClient = function(config, callback) { + callback(error); + }; + + util.authorizeRequest({}, {}, function(err) { + assert.deepEqual(err, error); + done(); + }); + }); + + it('should get an access token', function(done) { + var fakeAuthClient = { + getAccessToken: function() { done(); - }); + } + }; + + utilOverrides.getAuthClient = function(config, callback) { + callback(null, fakeAuthClient); + }; + + util.authorizeRequest(); + }); + + it('should return an access token error to callback', function(done) { + var error = new Error('Error.'); + + var fakeAuthClient = { + getAccessToken: function(callback) { + callback(error); + } + }; + + utilOverrides.getAuthClient = function(config, callback) { + callback(null, fakeAuthClient); + }; + + util.authorizeRequest({}, {}, function(err) { + assert.deepEqual(err, error); + done(); }); + }); - it('should throw if not GCE/GAE & missing credentials', function() { - gsa_Override = function() { - return function authorize(reqOpts, callback) { - // Simulate the metadata server not existing. - callback({ code: 'ENOTFOUND' }); - }; - }; + it('should extend the request options with token', function(done) { + var token = 'abctoken'; + + var reqOpts = { + uri: 'a', + headers: { + a: 'b', + c: 'd' + } + }; - assert.throws(function() { - // Don't provide a keyFile or credentials object. - var connectionConfig = {}; - var makeRequest = util.makeAuthorizedRequest(connectionConfig); - makeRequest({}, util.noop); - }, /A connection to gcloud must be established/); + var expectedAuthorizedReqOpts = extend(true, {}, reqOpts, { + headers: { + Authorization: 'Bearer ' + token + } }); - it('should handle malformed key response', function(done) { - var makeRequest = util.makeAuthorizedRequest({ - credentials: { - client_email: 'invalid@email', - private_key: 'invalid-key' - } - }); + var fakeAuthClient = { + getAccessToken: function(callback) { + callback(null, token); + } + }; - makeRequest({}, function (err) { - var errorMessage = [ - 'Your private key is in an unexpected format and cannot be used.', - 'Please try again with another private key.' - ].join(' '); - assert.equal(err.message, errorMessage); - done(); - }); + utilOverrides.getAuthClient = function(config, callback) { + callback(null, fakeAuthClient); + }; + + util.authorizeRequest({}, reqOpts, function(err, authorizedReqOpts) { + assert.ifError(err); + + assert.deepEqual(authorizedReqOpts, expectedAuthorizedReqOpts); + + done(); }); + }); + }); - it('should try to reconnect if token invalid', function(done) { - var attempts = 0; - var expectedAttempts = 2; - var error = { code: 401 }; + describe('makeAuthorizedRequest', function() { + describe('customEndpoint (no authorization attempted)', function() { + var makeRequest; - gsa_Override = function() { - return function authorize(reqOpts, callback) { - attempts++; - callback(error); - }; + beforeEach(function() { + makeRequest = util.makeAuthorizedRequest({ customEndpoint: true }); + }); + + it('should not authenticate requests with a custom API', function(done) { + var authorizeRequestCalled = false; + var reqOpts = { a: 'b', c: 'd' }; + + utilOverrides.authorizeRequest = function() { + authorizeRequestCalled = true; }; - var makeRequest = util.makeAuthorizedRequest({}); - makeRequest({}, function (err) { - assert.equal(attempts, expectedAttempts); - assert.equal(err, error); + makeRequest(reqOpts, function(err, authorizedReqOpts) { + assert.ifError(err); + + assert.deepEqual(reqOpts, authorizedReqOpts); + assert.strictEqual(authorizeRequestCalled, false); + done(); }); }); - it('should execute the onauthorized callback', function(done) { - gsa_Override = function() { - return function authorize(reqOpts, callback) { - callback(); - }; + it('should execute onAuthorized callback if provided', function(done) { + makeRequest({}, { onAuthorized: done }); + }); + }); + + describe('needs authorization', function() { + it('should pass correct arguments to authorizeRequest', function(done) { + var config = { a: 'b', c: 'd' }; + var reqOpts = { e: 'f', g: 'h' }; + + utilOverrides.authorizeRequest = function(cfg, rOpts) { + assert.deepEqual(cfg, config); + assert.deepEqual(rOpts, reqOpts); + done(); }; - var makeRequest = util.makeAuthorizedRequest({}); - makeRequest({}, { onAuthorized: done }); + var makeRequest = util.makeAuthorizedRequest(config); + makeRequest(reqOpts); }); - it('should execute the onauthorized callback with error', function(done) { + describe('authorization errors', function() { var error = new Error('Error.'); - gsa_Override = function() { - return function authorize(reqOpts, callback) { + beforeEach(function() { + utilOverrides.authorizeRequest = function(cfg, rOpts, callback) { callback(error); }; - }; + }); - var makeRequest = util.makeAuthorizedRequest({}); - makeRequest({}, { - onAuthorized: function(err) { - assert.equal(err, error); + it('should invoke the callback with error', function(done) { + var makeRequest = util.makeAuthorizedRequest(); + makeRequest({}, function(err) { + assert.deepEqual(err, error); done(); - } + }); }); - }); - it('should make the authorized request', function(done) { - var authorizedReqOpts = { a: 'b', c: 'd' }; + it('should invoke the onAuthorized handler with error', function(done) { + var makeRequest = util.makeAuthorizedRequest(); + makeRequest({}, { + onAuthorized: function(err) { + assert.deepEqual(err, error); + done(); + } + }); + }); + }); - gsa_Override = function() { - return function authorize(reqOpts, callback) { - callback(null, authorizedReqOpts); - }; - }; + describe('authorization success', function() { + var PKG = require('../../package.json'); + var USER_AGENT = 'gcloud-node/' + PKG.version; + var reqOpts = { a: 'b', c: 'd' }; + var expectedAuthorizedReqOpts = extend(true, {}, reqOpts, { + headers: { + 'User-Agent': USER_AGENT + } + }); - request_Override = function(reqOpts) { - assert.deepEqual(reqOpts, authorizedReqOpts); - done(); - }; + describe('returns authorized request to callback', function() { + it('should add the user agent', function(done) { + utilOverrides.authorizeRequest = function(cfg, rOpts, callback) { + callback(null, rOpts); + }; + + var makeRequest = util.makeAuthorizedRequest(); + makeRequest(reqOpts, { + onAuthorized: function(err, authorizedReqOpts) { + assert.ifError(err); + assert.deepEqual(authorizedReqOpts, expectedAuthorizedReqOpts); + done(); + } + }); + }); + }); - var makeRequest = util.makeAuthorizedRequest({}); - makeRequest({}, assert.ifError); + describe('makes request', function() { + beforeEach(function() { + utilOverrides.authorizeRequest = function(cfg, rOpts, callback) { + callback(null, rOpts); + }; + + request_Override = function(reqOpts, callback) { + callback(null, reqOpts); + }; + }); + + it('should makes request with correct options', function(done) { + request_Override = function(authorizedReqOpts) { + assert.deepEqual(authorizedReqOpts, expectedAuthorizedReqOpts); + done(); + }; + + var makeRequest = util.makeAuthorizedRequest(); + makeRequest(reqOpts, assert.ifError); + }); + + describe('request errors', function() { + var nonRateLimitError = new Error('Error.'); + + it('should let handleResp handle non rate errors', function(done) { + request_Override = function(reqOpts, callback) { + callback(nonRateLimitError); + }; + + utilOverrides.handleResp = function(err) { + assert.deepEqual(err, nonRateLimitError); + done(); + }; + + var makeRequest = util.makeAuthorizedRequest(); + makeRequest({}, assert.ifError); + }); + + describe('rate limit errors', function() { + var rateLimitError = new Error('Rate limit error.'); + rateLimitError.code = 500; + + beforeEach(function() { + // Always return a rate limit error. + request_Override = function (reqOpts, callback) { + callback(rateLimitError); + }; + + // Always suggest retrying. + utilOverrides.shouldRetryRequest = function() { + return true; + }; + + // Always return a 0 retry wait. + utilOverrides.getNextRetryWait = function() { + return 0; + }; + }); + + it('should check with shouldRetryRequest', function(done) { + utilOverrides.shouldRetryRequest = function() { + done(); + }; + + var makeRequest = util.makeAuthorizedRequest(); + makeRequest({}, function() {}); + }); + + it('should default to 3 retries', function(done) { + var attemptedRetries = 0; + var expectedAttempts = 4; // default 3 + the original req + + request_Override = function(reqOpts, callback) { + attemptedRetries++; + callback(rateLimitError); + }; + + // handleResp is called after all retry attempts have failed. + utilOverrides.handleResp = function(err) { + assert.equal(attemptedRetries, expectedAttempts); + assert.equal(err, rateLimitError); + done(); + }; + + var makeRequest = util.makeAuthorizedRequest(); + makeRequest({}, assert.ifError); + }); + + it('should allow max retries to be specified', function(done) { + var attemptedRetries = 0; + var maxRetries = 5; + var expectedAttempts = maxRetries + 1; // the original req + + request_Override = function(reqOpts, callback) { + attemptedRetries++; + callback(rateLimitError); + }; + + // handleResp is called after all retry attempts have failed. + utilOverrides.handleResp = function(err) { + assert.equal(attemptedRetries, expectedAttempts); + assert.equal(err, rateLimitError); + done(); + }; + + var makeRequest = util.makeAuthorizedRequest({ + maxRetries: maxRetries + }); + makeRequest({}, assert.ifError); + }); + + it('should not retry reqs if autoRetry is false', function(done) { + var attemptedRetries = 0; + var expectedAttempts = 1; // the original req + + request_Override = function(reqOpts, callback) { + attemptedRetries++; + callback(rateLimitError); + }; + + // handleResp is called after all retry attempts have failed. + utilOverrides.handleResp = function(err) { + assert.equal(attemptedRetries, expectedAttempts); + assert.equal(err, rateLimitError); + done(); + }; + + var makeRequest = util.makeAuthorizedRequest({ + autoRetry: false + }); + makeRequest({}, assert.ifError); + }); + }); + }); + + describe('request success', function() { + it('should let handleResp handle response', function(done) { + utilOverrides.handleResp = function() { + done(); + }; + + request_Override = function(reqOpts, callback) { + callback(); + }; + + var makeRequest = util.makeAuthorizedRequest(); + makeRequest({}, assert.ifError); + }); + }); + }); }); + }); - it('should retry rate limit requests by default', function(done) { - var attemptedRetries = 0; - var error = new Error('Rate Limit Error.'); - error.code = 429; // Rate limit error - - var authorizedReqOpts = { a: 'b', c: 'd' }; + describe('getCredentials', function() { + var fakeAuthClient = { + email: 'fake-email@example.com', + key: 'fake-key', - var old_setTimeout = setTimeout; - setTimeout = function(callback, time) { - var MIN_TIME = (Math.pow(2, attemptedRetries) * 1000); - var MAX_TIME = (Math.pow(2, attemptedRetries) * 1000) + 1000; - assert(time >= MIN_TIME && time <= MAX_TIME); - attemptedRetries++; - callback(); // make the request again - }; + authorize: function(callback) { callback(); } + }; + var config = { a: 'b', c: 'd' }; - gsa_Override = function() { - return function authorize(reqOpts, callback) { - callback(null, authorizedReqOpts); - }; + it('should return getCredentials method', function() { + utilOverrides.getAuthClient = function(config, callback) { + callback(null, fakeAuthClient); }; - request_Override = function(reqOpts, callback) { - if (attemptedRetries === 3) { - setTimeout = old_setTimeout; - done(); - } else { - callback(error); // this callback should check for rate limits - } - }; + var makeRequest = util.makeAuthorizedRequest(config, assert.ifError); - var makeRequest = util.makeAuthorizedRequest({}); - makeRequest({}, assert.ifError); + assert.equal(typeof makeRequest.getCredentials, 'function'); }); - it('should retry rate limits 3x on 429, 500, 503', function(done) { - var attemptedRetries = 0; - var codes = [429, 503, 500, 'done']; - var error = new Error('Rate Limit Error.'); - error.code = codes[0]; // Rate limit error - - var authorizedReqOpts = { a: 'b', c: 'd' }; - - var old_setTimeout = setTimeout; - setTimeout = function(callback, time) { - var MIN_TIME = (Math.pow(2, attemptedRetries) * 1000); - var MAX_TIME = (Math.pow(2, attemptedRetries) * 1000) + 1000; - assert(time >= MIN_TIME && time <= MAX_TIME); - attemptedRetries++; - error.code = codes[attemptedRetries]; // test a new code - callback(); // make the request again + it('should pass config to getAuthClient', function(done) { + utilOverrides.getAuthClient = function(cfg) { + assert.deepEqual(cfg, config); + done(); }; - gsa_Override = function() { - return function authorize(reqOpts, callback) { - callback(null, authorizedReqOpts); - }; - }; + var makeRequest = util.makeAuthorizedRequest(config, assert.ifError); + makeRequest.getCredentials(); + }); - request_Override = function(reqOpts, callback) { - callback(error); // this callback should check for rate limits + it('should execute callback with error', function(done) { + var error = new Error('Error.'); + + utilOverrides.getAuthClient = function(config, callback) { + callback(error); }; - var makeRequest = util.makeAuthorizedRequest({}); - makeRequest({}, function(err) { - setTimeout = old_setTimeout; - assert.equal(err, error); - assert.equal(err.code, 'done'); + var makeRequest = util.makeAuthorizedRequest(config, assert.ifError); + makeRequest.getCredentials(function(err) { + assert.deepEqual(err, error); done(); }); }); - it('should retry rate limits 3x by default', function(done) { - var attemptedRetries = 0; - var error = new Error('Rate Limit Error.'); - error.code = 429; // Rate limit error - - var authorizedReqOpts = { a: 'b', c: 'd' }; - - var old_setTimeout = setTimeout; - setTimeout = function(callback, time) { - var MIN_TIME = (Math.pow(2, attemptedRetries) * 1000); - var MAX_TIME = (Math.pow(2, attemptedRetries) * 1000) + 1000; - assert(time >= MIN_TIME && time <= MAX_TIME); - attemptedRetries++; - callback(); // make the request again - }; - - gsa_Override = function() { - return function authorize(reqOpts, callback) { - callback(null, authorizedReqOpts); - }; + it('should authorize the connection', function(done) { + fakeAuthClient.authorize = function(callback) { + callback(); }; - request_Override = function(reqOpts, callback) { - callback(error); // this callback should check for rate limits + utilOverrides.getAuthClient = function(config, callback) { + callback(null, fakeAuthClient); }; - var makeRequest = util.makeAuthorizedRequest({}); - makeRequest({}, function(err) { - setTimeout = old_setTimeout; - assert.equal(attemptedRetries, 3); - assert.equal(err, error); - done(); - }); + var makeRequest = util.makeAuthorizedRequest(config, assert.ifError); + makeRequest.getCredentials(done); }); - it('should retry rate limits by maxRetries if provided', function(done) { - var MAX_RETRIES = 5; - var attemptedRetries = 0; - var error = new Error('Rate Limit Error.'); - error.code = 429; // Rate limit error - - var authorizedReqOpts = { a: 'b', c: 'd' }; - - var old_setTimeout = setTimeout; - setTimeout = function(callback, time) { - var MIN_TIME = (Math.pow(2, attemptedRetries) * 1000); - var MAX_TIME = (Math.pow(2, attemptedRetries) * 1000) + 1000; - assert(time >= MIN_TIME && time <= MAX_TIME); - attemptedRetries++; - callback(); // make the request again - }; - gsa_Override = function() { - return function authorize(reqOpts, callback) { - callback(null, authorizedReqOpts); - }; - }; + it('should execute callback with authorization error', function(done) { + var error = new Error('Error.'); - request_Override = function(reqOpts, callback) { - callback(error); // this callback should check for rate limits + fakeAuthClient.authorize = function(cb) { + cb(error); }; - var makeRequest = util.makeAuthorizedRequest({ - maxRetries: MAX_RETRIES - }); + utilOverrides.getAuthClient = function(config, callback) { + callback(null, fakeAuthClient); + }; - makeRequest({}, function(err) { - setTimeout = old_setTimeout; - assert.equal(attemptedRetries, MAX_RETRIES); - assert.equal(err, error); + var makeRequest = util.makeAuthorizedRequest(config, assert.ifError); + makeRequest.getCredentials(function(err) { + assert.deepEqual(err, error); done(); }); }); - it('should not retry rate limits if autoRetry is false', function(done) { - var attemptedRetries = 0; - var error = new Error('Rate Limit Error.'); - error.code = 429; // Rate limit error - - var authorizedReqOpts = { a: 'b', c: 'd' }; - - var old_setTimeout = setTimeout; - setTimeout = function(callback, time) { - var MIN_TIME = (Math.pow(2, attemptedRetries) * 1000); - var MAX_TIME = (Math.pow(2, attemptedRetries) * 1000) + 1000; - assert(time >= MIN_TIME && time <= MAX_TIME); - attemptedRetries++; - callback(); // make the request again + it('should exec callback with client_email & client_key', function(done) { + fakeAuthClient.authorize = function(callback) { + callback(); }; - gsa_Override = function() { - return function authorize(reqOpts, callback) { - callback(null, authorizedReqOpts); - }; - }; - - request_Override = function(reqOpts, callback) { - callback(error); // this callback should check for rate limits + utilOverrides.getAuthClient = function(config, callback) { + callback(null, fakeAuthClient); }; - var makeRequest = util.makeAuthorizedRequest({ - autoRetry: false - }); - - makeRequest({}, function(err) { - setTimeout = old_setTimeout; - assert.equal(attemptedRetries, 0); - assert.equal(err, error); + var makeRequest = util.makeAuthorizedRequest(config, assert.ifError); + makeRequest.getCredentials(function(err, credentials) { + assert.deepEqual(credentials, { + client_email: fakeAuthClient.email, + private_key: fakeAuthClient.key + }); done(); }); }); @@ -669,4 +884,50 @@ describe('common/util', function() { assert.equal(obj.prop, 'value'); }); }); + + describe('shouldRetryRequest', function() { + it('should return false if there is no error', function() { + assert.strictEqual(util.shouldRetryRequest(), false); + }); + + it('should return true with error code 429', function() { + var error = new Error('429'); + error.code = 429; + + assert.strictEqual(util.shouldRetryRequest(error), true); + }); + + it('should return true with error code 500', function() { + var error = new Error('500'); + error.code = 500; + + assert.strictEqual(util.shouldRetryRequest(error), true); + }); + + it('should return true with error code 503', function() { + var error = new Error('503'); + error.code = 503; + + assert.strictEqual(util.shouldRetryRequest(error), true); + }); + }); + + describe('getNextRetryWait', function() { + function secs(seconds) { + return seconds * 1000; + } + + it('should return exponential retry delay', function() { + [1, 2, 3, 4, 5].forEach(assertTime); + + function assertTime(retryNumber) { + var min = (Math.pow(2, retryNumber) * secs(1)); + var max = (Math.pow(2, retryNumber) * secs(1)) + secs(1); + + var time = util.getNextRetryWait(retryNumber); + + assert(time >= min && time <= max); + } + }); + }); }); diff --git a/test/pubsub/index.js b/test/pubsub/index.js index f613c6f22a0..d52c69b1f81 100644 --- a/test/pubsub/index.js +++ b/test/pubsub/index.js @@ -202,11 +202,15 @@ describe('PubSub', function() { }); }); - it('should pass network requests to the connection object', function(done) { - var pubsub = new PubSub(); - request_Override = function() { - done(); - }; - pubsub.makeReq_(); + describe('makeReq_', function() { + it('should pass network requests to the connection object', function(done) { + var pubsub = new PubSub(); + + pubsub.makeAuthorizedRequest_ = function() { + done(); + }; + + pubsub.makeReq_(null, null, null, null, assert.ifError); + }); }); });