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/docs/site/components/docs/docs.html b/docs/site/components/docs/docs.html
index d72ecb41af3..86264557d39 100644
--- a/docs/site/components/docs/docs.html
+++ b/docs/site/components/docs/docs.html
@@ -43,14 +43,11 @@
$ npm install --save gcloud
var gcloud = require('gcloud');
-
- There are a couple of ways to use the gcloud
module.
-
If you are running your app on Google App Engine or Google Compute Engine, you won't need to worry about supplying connection configuration options to gcloud
— we figure that out for you.
- However, if you're running your app elsewhere, you will need to provide this information.
+ However, if you're running your app elsewhere, you will need to provide project details to authenticate API requests.
// App Engine and Compute Engine
@@ -58,10 +55,11 @@
// Elsewhere
var gcloud = require('gcloud')({
+ projectId: 'project-id',
keyFilename: '/path/to/keyfile.json'
});
- In any environment, you are free to provide these and other default properties, which eventually will be passed to the gcloud
sub-modules (Datastore, Storage, etc.).
+ The full set of options which can be passed to gcloud and sub-modules are outlined here.
@@ -85,7 +83,7 @@ Datastore Overview
var dataset = gcloud.datastore.dataset({
- projectId: 'myProject',
+ projectId: 'project-id',
keyFilename: '/path/to/keyfile.json'
});
@@ -103,7 +101,7 @@
Pub/Sub Overview
var pubsub = gcloud.pubsub({
- projectId: 'myProject',
+ projectId: 'project-id',
keyFilename: '/path/to/keyfile.json'
});
diff --git a/lib/bigquery/index.js b/lib/bigquery/index.js
index 7c13b99de04..3ffa2c9cf9d 100644
--- a/lib/bigquery/index.js
+++ b/lib/bigquery/index.js
@@ -63,16 +63,21 @@ var BIGQUERY_BASE_URL = 'https://www.googleapis.com/bigquery/v2/projects/';
var SCOPES = ['https://www.googleapis.com/auth/bigquery'];
/**
- * The example below will demonstrate the different usage patterns your app may
+ * The examples below will demonstrate the different usage patterns your app may
* need to support to retrieve a BigQuery object.
*
+ * The full set of options that can be passed to BigQuery are
+ * [outlined here](#/docs/?method=gcloud).
+ *
* @alias module:bigquery
* @constructor
*
* @example
* var gcloud = require('gcloud');
*
+ * //-
* // Providing configuration details up-front.
+ * //-
* var myProject = gcloud({
* keyFilename: '/path/to/keyfile.json',
* projectId: 'my-project'
@@ -80,19 +85,13 @@ var SCOPES = ['https://www.googleapis.com/auth/bigquery'];
*
* var bigquery = myProject.bigquery();
*
- *
+ * //-
* // Overriding default configuration details.
- * var anotherBigQueryInstance = myProject.bigquery({
+ * //-
+ * var bigquery = myProject.bigquery({
* keyFilename: '/path/to/another/keyfile.json'
* });
*
- *
- * // Not using a default configuration.
- * var myOtherProject = gcloud.bigquery({
- * keyFilename: '/path/to/keyfile.json',
- * projectId: 'my-project'
- * });
- *
* //-
* // In the following examples from this page and the other modules (Dataset,
* // Table, etc.), we are going to be using a dataset from
@@ -109,10 +108,11 @@ function BigQuery(options) {
options = options || {};
- this.makeAuthorizedRequest_ = util.makeAuthorizedRequest({
+ this.makeAuthorizedRequest_ = util.makeAuthorizedRequestFactory({
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 8f7a984cac9..d75de15747d 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,17 +420,17 @@ 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) {
if (err) {
if ([429, 500, 503].indexOf(err.code) !== -1) {
return true;
@@ -452,102 +451,233 @@ function shouldRetry(err) {
return false;
}
-module.exports.shouldRetryErr = shouldRetry;
+util.shouldRetryRequest = shouldRetryRequest;
-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;
-
- 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 addScope(err, authClient) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ if (authClient.createScopedRequired && authClient.createScopedRequired()) {
+ authClient = authClient.createScoped(config.scopes);
+ }
+
+ callback(null, authClient);
}
+}
- function makeRequest(reqOpts, callback) {
- var tokenRefreshAttempts = 0;
- reqOpts.headers = reqOpts.headers || {};
+util.getAuthClient = getAuthClient;
- if (reqOpts.headers['User-Agent']) {
- reqOpts.headers['User-Agent'] += '; ' + USER_AGENT;
- } else {
- reqOpts.headers['User-Agent'] = USER_AGENT;
+/**
+ * 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;
+ }
- if (err.code === 401 &&
- ++tokenRefreshAttempts <= MAX_TOKEN_REFRESH_ATTEMPTS) {
- authorize(reqOpts, onAuthorizedRequest);
- return;
+ var authorizedReqOpts = extend(true, {}, reqOpts, {
+ headers: {
+ Authorization: 'Bearer ' + token
}
+ });
- // 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;
+ callback(null, authorizedReqOpts);
+ });
+ });
+}
- 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(' ');
- }
+util.authorizeRequest = authorizeRequest;
- (callback.onAuthorized || callback)(err);
- return;
+/**
+ * Get a function for making authorized requests.
+ *
+ * @param {object} config - Configuration object.
+ * @param {boolean=} config.autoRetry - Automatically retry requests if the
+ * response is related to rate limits or certain intermittent server errors.
+ * We will exponentially backoff subsequent requests by default. (default:
+ * true)
+ * @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 {number=} config.maxRetries - Maximum number of automatic retries
+ * attempted before returning the error. (default: 3)
+ * @param {string=} config.keyFile - Path to a .json, .pem, or .p12 keyfile.
+ * @param {array} config.scopes - Array of scopes required for the API.
+ */
+function makeAuthorizedRequestFactory(config) {
+ config = config || {};
+
+ /**
+ * The returned function that will make an authorized request.
+ *
+ * @param {type} reqOpts - Request options in the format `request` expects.
+ * @param {object|function} options - Configuration object or callback
+ * function.
+ * @param {function=} options.onAuthorized - If provided, a request will not
+ * be made. Instead, this function is passed the error & authorized
+ * request options.
+ */
+ function makeAuthorizedRequest(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)
+ if (callback.onAuthorized) {
+ callback.onAuthorized(null, reqOpts);
+ } else {
+ util.makeRequest(reqOpts, config, callback);
}
- function handleRateLimitResp(err, res, body) {
- handleResp(err, res, body, function(err, body, resp) {
- if (shouldRetry(err) && autoRetry && MAX_RETRIES > attemptedRetries) {
- setTimeout(function() {
- request(authorizedReqOpts, handleRateLimitResp);
- }, getNextRetryWait(attemptedRetries++));
- } else {
- callback(err, body, resp);
- }
- });
+ return;
+ }
+
+ util.authorizeRequest(config, reqOpts, function(err, authorizedReqOpts) {
+ if (err) {
+ (callback.onAuthorized || callback)(err);
+ return;
}
if (callback.onAuthorized) {
callback.onAuthorized(null, authorizedReqOpts);
} else {
- request(authorizedReqOpts, handleRateLimitResp);
+ util.makeRequest(authorizedReqOpts, config, callback);
}
- }
+ });
+ }
+
+ makeAuthorizedRequest.getCredentials = function(callback) {
+ util.getAuthClient(config, function(err, authClient) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ authClient.authorize(function(err) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ callback(null, {
+ client_email: authClient.email,
+ private_key: authClient.key
+ });
+ });
+ });
+ };
+
+ return makeAuthorizedRequest;
+}
+
+util.makeAuthorizedRequestFactory = makeAuthorizedRequestFactory;
+
+/**
+ * Make a request through the `request` module with built-in error handling and
+ * exponential back off.
+ *
+ * @param {object} reqOpts - Request options in the format `request` expects.
+ * @param {object=} config - Configuration object.
+ * @param {boolean=} config.autoRetry - Automatically retry requests if the
+ * response is related to rate limits or certain intermittent server errors.
+ * We will exponentially backoff subsequent requests by default. (default:
+ * true)
+ * @param {number=} config.maxRetries - Maximum number of automatic retries
+ * attempted before returning the error. (default: 3)
+ * @param {function} callback - The callback function.
+ */
+function makeRequest(reqOpts, config, callback) {
+ if (util.is(config, 'function')) {
+ callback = config;
+ config = {};
+ }
+
+ config = config || {};
+
+ var MAX_RETRIES = config.maxRetries || 3;
+ var autoRetry = config.autoRetry !== false ? true : false;
+ var attemptedRetries = 0;
- authorize(reqOpts, onAuthorizedRequest);
+ reqOpts.headers = reqOpts.headers || {};
+ reqOpts.headers['User-Agent'] = USER_AGENT;
+
+ function shouldRetry(err) {
+ return autoRetry &&
+ MAX_RETRIES > attemptedRetries &&
+ util.shouldRetryRequest(err);
}
- makeRequest.getCredentials = authorize.getCredentials;
+ function makeRateLimitedRequest() {
+ request(reqOpts, function(err, resp, body) {
+ util.handleResp(err, resp, body, function(err, body, resp) {
+ if (shouldRetry(err)) {
+ var delay = util.getNextRetryWait(attemptedRetries++);
+ setTimeout(makeRateLimitedRequest, delay);
+ } else {
+ callback(err || null, body, resp);
+ }
+ });
+ });
+ }
- return makeRequest;
+ makeRateLimitedRequest();
}
-module.exports.makeAuthorizedRequest = makeAuthorizedRequest;
+util.makeRequest = makeRequest;
diff --git a/lib/datastore/dataset.js b/lib/datastore/dataset.js
index a268914f1c4..4ed176c659f 100644
--- a/lib/datastore/dataset.js
+++ b/lib/datastore/dataset.js
@@ -66,6 +66,9 @@ var SCOPES = [
* Interact with a dataset from the
* [Google Cloud Datastore](https://developers.google.com/datastore/).
*
+ * The full set of options that can be passed to this method are
+ * [outlined here](#/docs/?method=gcloud).
+ *
* @constructor
* @alias module:datastore/dataset
* @mixes module:datastore/request
@@ -73,11 +76,6 @@ var SCOPES = [
* @param {object=} options - Configuration object.
* @param {string=} options.projectId - Dataset ID. This is your project ID from
* the Google Developers Console.
- * @param {string=} options.keyFilename - Full path to the JSON key downloaded
- * from the Google Developers Console. Alternatively, you may provide a
- * `credentials` object.
- * @param {object=} options.credentials - Credentials object, used in place of
- * a `keyFilename`.
* @param {string=} options.apiEndpoint - Override the default API endpoint used
* to reach Datastore. This is useful for connecting to your local Datastore
* server (usually "http://localhost:8080").
@@ -104,11 +102,12 @@ function Dataset(options) {
options = options || {};
- this.makeAuthorizedRequest_ = util.makeAuthorizedRequest({
+ this.makeAuthorizedRequest_ = util.makeAuthorizedRequestFactory({
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/datastore/index.js b/lib/datastore/index.js
index c3385e1cd4c..26c7d6a8c59 100644
--- a/lib/datastore/index.js
+++ b/lib/datastore/index.js
@@ -65,26 +65,22 @@ var Dataset = require('./dataset');
* @example
* var gcloud = require('gcloud');
*
+ * //-
* // Providing configuration details up-front.
+ * //-
* var myProject = gcloud({
* keyFilename: '/path/to/keyfile.json',
* projectId: 'my-project'
* });
*
- * var dataset = myProject.datastore.dataset();
- *
+ * var dataset = gcloud.dataset();
*
+ * //-
* // Overriding default configuration details.
- * var anotherDataset = myProject.datastore.dataset({
+ * //-
+ * var dataset = myProject.datastore.dataset({
* keyFilename: '/path/to/another/keyfile.json'
* });
- *
- *
- * // Not using a default configuration.
- * var myOtherProject = gcloud.datastore.dataset({
- * keyFilename: '/path/to/keyfile.json',
- * projectId: 'my-project'
- * });
*/
function Datastore(config) {
this.config = config || {};
diff --git a/lib/index.js b/lib/index.js
index 582af258c6f..fd1df902601 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -72,22 +72,50 @@ var util = require('./common/util.js');
* your provided configuration will remain isolated to the returned gcloud
* module.
*
+ * To authenticate API requests,
+ * [google-auth-library](https://github.com/google/google-auth-library-nodejs)
+ * is used to detect the environment your project is running in. Use the
+ * following guide to determine the appropriate configuration.
+ *
+ *
Google Compute Engine
+ *
+ * - No configuration necessary!
+ *
+ * Google App Engine Production
+ *
+ * - No configuration necessary!
+ *
+ * Other
+
+ * - Provide `config.projectId`
+ * - Use one of the following:
+ * - `config.credentials` object containing `client_email` and `private_key`
+ * properties.
+ * - `config.keyFilename` path to a .json, .pem, or .p12 key file.
+ * - `GOOGLE_APPLICATION_CREDENTIALS` environment variable with a full path
+ * to your key file.
+ *
+ * **Note**: When using a .pem or .p12 key file, `config.email` is also
+ * required.
+ *
* @alias module:gcloud
* @constructor
*
* @param {object} config - Connection configuration options.
- * @param {string=} config.keyFilename - Full path to the JSON key downloaded
- * from the Google Developers Console. Alternatively, you may provide a
- * `credentials` object.
+ * @param {string=} config.keyFilename - Full path to the a .json, .pem, or .p12
+ * key downloaded from the Google Developers Console. NOTE: .pem and .p12
+ * require you to specify `config.email` as well.
+ * @param {string=} config.email - Account email address. Required when using a
+ * .pem or .p12 keyFilename.
* @param {object=} config.credentials - Credentials object.
* @param {string} config.credentials.client_email
* @param {string} config.credentials.private_key
- * @param {boolean} config.autoRetry - Automatically retry requests if the
+ * @param {boolean=} config.autoRetry - Automatically retry requests if the
* response is related to rate limits or certain intermittent server errors.
- * (default: true). Recommended is true. We will exponentially backoff
- * subsequent requests by default.
- * @param {number} config.maxRetries - Max number of auto retries to attempt
- * before returning the error. (default: 3).
+ * We will exponentially backoff subsequent requests by default. (default:
+ * true)
+ * @param {number=} config.maxRetries - Maximum number of automatic retries
+ * attempted before returning the error. (default: 3)
*
* @example
* var gcloud = require('gcloud')({
diff --git a/lib/pubsub/index.js b/lib/pubsub/index.js
index 14fa0ae7b8c..06a4473aec9 100644
--- a/lib/pubsub/index.js
+++ b/lib/pubsub/index.js
@@ -64,6 +64,9 @@ var SCOPES = [
* subject to any SLA or deprecation policy. Request to be whitelisted to use
* it by filling the [Limited Preview application form](http://goo.gl/sO0wTu).
*
+ * The full set of options that can be passed to this method are
+ * [outlined here](#/docs/?method=gcloud).
+ *
* @constructor
* @alias module:pubsub
*
@@ -78,41 +81,31 @@ var SCOPES = [
* @example
* var gcloud = require('gcloud');
*
- * // From Google Compute Engine and Google App Engine:
- *
- * // Access `pubsub` through the `gcloud` module directly.
- * var pubsub = gcloud.pubsub();
- *
- * // Elsewhere:
- *
- * // Provide configuration details up-front.
+ * //-
+ * // Providing configuration details up-front.
+ * //-
* var myProject = gcloud({
* keyFilename: '/path/to/keyfile.json',
* projectId: 'my-project'
* });
*
- * var pubsub = myProject.pubsub();
- *
- *
- * // Override default configuration details.
- * var anotherPubsubConnection = myProject.pubsub({
- * keyFilename: '/path/to/another/keyfile.json',
- * });
- *
+ * var pubsub = gcloud.pubsub();
*
- * // Specify all options at instantiation.
- * var pubsub = gcloud.pubsub({
- * keyFilename: '/path/to/keyfile.json',
- * projectId: 'my-project'
+ * //-
+ * // Overriding default configuration details.
+ * //-
+ * var pubsub = myProject.pubsub({
+ * keyFilename: '/path/to/another/keyfile.json'
* });
*/
function PubSub(options) {
options = options || {};
- this.makeAuthorizedRequest_ = util.makeAuthorizedRequest({
+ this.makeAuthorizedRequest_ = util.makeAuthorizedRequestFactory({
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..39faa82c3e2 100644
--- a/lib/storage/index.js
+++ b/lib/storage/index.js
@@ -54,15 +54,18 @@ var STORAGE_BASE_URL = 'https://www.googleapis.com/storage/v1/b';
* there is an equivalent static and instance method. While they are classes,
* they can be instantiated without use of the `new` keyword.
*
- * @param {object} config - Configuration object.
+ * @param {object} options - Configuration object.
*/
/**
* To access your Cloud Storage buckets, you will use the `bucket` function
* returned from this `storage` object.
*
- * The example below will demonstrate the different usage patterns your app may
+ * The examples below will demonstrate the different usage patterns your app may
* need to connect to `gcloud` and access your bucket.
*
+ * The full set of options that can be passed to this method are
+ * [outlined here](#/docs/?method=gcloud).
+ *
* @alias module:storage
* @constructor
*
@@ -70,47 +73,37 @@ var STORAGE_BASE_URL = 'https://www.googleapis.com/storage/v1/b';
* var gcloud = require('gcloud');
*
* //-
- * // From Google Compute Engine and Google App Engine.
+ * // Providing configuration details up-front.
* //-
- *
- * // Access `storage` through the `gcloud` module directly.
- * var storage = gcloud.storage();
- * var musicBucket = storage.bucket('music');
- *
- * //-
- * // Elsewhere.
- * //-
- *
- * // Provide configuration details up-front.
* var myProject = gcloud({
* keyFilename: '/path/to/keyfile.json',
* projectId: 'my-project'
* });
*
- * // Use default configuration details.
* var storage = myProject.storage();
- * var albums = storage.bucket('albums');
- * var photos = storage.bucket('photos');
- *
*
- * // Override default configuration details.
+ * //-
+ * // Overriding default configuration details.
+ * //-
* var storage = myProject.storage({
* keyFilename: '/path/to/another/keyfile.json'
* });
- * var records = storage.bucket('records');
*/
-function Storage(config) {
+function Storage(options) {
if (!(this instanceof Storage)) {
- return new Storage(config);
+ return new Storage(options);
}
- this.makeAuthorizedRequest_ = util.makeAuthorizedRequest({
- credentials: config.credentials,
- keyFile: config.keyFilename,
- scopes: SCOPES
+ options = options || {};
+
+ this.makeAuthorizedRequest_ = util.makeAuthorizedRequestFactory({
+ credentials: options.credentials,
+ keyFile: options.keyFilename,
+ scopes: SCOPES,
+ email: options.email
});
- this.projectId = config.projectId;
+ this.projectId = options.projectId;
}
/**
diff --git a/package.json b/package.json
index 0722fa637da..a50b0a9a72b 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,7 @@
"duplexify": "^3.2.0",
"extend": "^2.0.0",
"fast-crc32c": "^0.1.3",
- "google-service-account": "^1.0.3",
+ "google-auth-library": "^0.9.4",
"mime-types": "^2.0.8",
"node-uuid": "^1.4.2",
"once": "^1.3.1",
@@ -66,7 +66,7 @@
"devDependencies": {
"bytebuffer": "^3.5.4",
"coveralls": "^2.11.2",
- "dox": "^0.6.1",
+ "dox": "^0.7.0",
"istanbul": "^0.3.5",
"jshint": "^2.6.0",
"mocha": "^2.1.0",
diff --git a/regression/index.js b/regression/index.js
new file mode 100644
index 00000000000..85ee0b232e7
--- /dev/null
+++ b/regression/index.js
@@ -0,0 +1,50 @@
+/**
+ * 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');
+
+// Test used to confirm we can perform a successful API operation.
+function canConnect(config, callback) {
+ gcloud.storage(config).getBuckets(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_APPLICATION_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 7d939559a33..9d9c6041a82 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,472 +298,441 @@ 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 makeRequest = util.makeAuthorizedRequest({});
- makeRequest({});
+ var config = {
+ credentials: { a: 'b', c: 'd' }
+ };
+
+ 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);
+ return fakeAuthClient;
+ },
+ getAccessToken: function() {}
+ };
+
+ googleAuthLibrary_Override = function() {
+ return {
+ getApplicationDefault: function(callback) {
+ callback(null, fakeAuthClient);
+ }
};
+ };
- var makeRequest = util.makeAuthorizedRequest({});
- makeRequest({}, function(err) {
- assert.equal(err, error);
- done();
- });
- });
+ util.getAuthClient(config, 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' });
- };
- };
+ describe('authorizeRequest', function() {
+ it('should get an auth client', function(done) {
+ var config = { 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/);
- });
+ utilOverrides.getAuthClient = function(cfg) {
+ assert.deepEqual(cfg, config);
+ done();
+ };
- it('should handle malformed key response', function(done) {
- var makeRequest = util.makeAuthorizedRequest({
- credentials: {
- client_email: 'invalid@email',
- private_key: 'invalid-key'
- }
- });
+ util.authorizeRequest(config);
+ });
- 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();
- });
+ 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 try to reconnect if token invalid', function(done) {
- var attempts = 0;
- var expectedAttempts = 2;
- var error = { code: 401 };
+ it('should return an error to the callback', function(done) {
+ var error = new Error('Error.');
- gsa_Override = function() {
- return function authorize(reqOpts, callback) {
- attempts++;
- callback(error);
- };
- };
+ utilOverrides.getAuthClient = function(config, callback) {
+ callback(error);
+ };
- var makeRequest = util.makeAuthorizedRequest({});
- makeRequest({}, function (err) {
- assert.equal(attempts, expectedAttempts);
- assert.equal(err, error);
- done();
- });
+ util.authorizeRequest({}, {}, function(err) {
+ assert.deepEqual(err, error);
+ done();
});
+ });
- it('should execute the onauthorized callback', function(done) {
- gsa_Override = function() {
- return function authorize(reqOpts, callback) {
- callback();
- };
- };
+ it('should get an access token', function(done) {
+ var fakeAuthClient = {
+ getAccessToken: function() {
+ done();
+ }
+ };
+
+ utilOverrides.getAuthClient = function(config, callback) {
+ callback(null, fakeAuthClient);
+ };
+
+ util.authorizeRequest();
+ });
- var makeRequest = util.makeAuthorizedRequest({});
- makeRequest({}, { onAuthorized: done });
+ 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 execute the onauthorized callback with error', function(done) {
- var error = new Error('Error.');
+ it('should extend the request options with token', function(done) {
+ var token = 'abctoken';
- gsa_Override = function() {
- return function authorize(reqOpts, callback) {
- callback(error);
- };
- };
+ var reqOpts = {
+ uri: 'a',
+ headers: {
+ a: 'b',
+ c: 'd'
+ }
+ };
- var makeRequest = util.makeAuthorizedRequest({});
- makeRequest({}, {
- onAuthorized: function(err) {
- assert.equal(err, error);
- done();
- }
- });
+ var expectedAuthorizedReqOpts = extend(true, {}, reqOpts, {
+ headers: {
+ Authorization: 'Bearer ' + token
+ }
});
- it('should make the authorized request', function(done) {
- var authorizedReqOpts = { a: 'b', c: 'd' };
+ var fakeAuthClient = {
+ getAccessToken: function(callback) {
+ callback(null, token);
+ }
+ };
- gsa_Override = function() {
- return function authorize(reqOpts, callback) {
- callback(null, authorizedReqOpts);
- };
- };
+ utilOverrides.getAuthClient = function(config, callback) {
+ callback(null, fakeAuthClient);
+ };
- request_Override = function(reqOpts) {
- assert.deepEqual(reqOpts, authorizedReqOpts);
- done();
- };
+ util.authorizeRequest({}, reqOpts, function(err, authorizedReqOpts) {
+ assert.ifError(err);
+
+ assert.deepEqual(authorizedReqOpts, expectedAuthorizedReqOpts);
- var makeRequest = util.makeAuthorizedRequest({});
- makeRequest({}, assert.ifError);
+ done();
});
+ });
+ });
- 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
+ describe('makeAuthorizedRequestFactory', function() {
+ it('should return a function', function() {
+ assert.equal(typeof util.makeAuthorizedRequestFactory(), 'function');
+ });
- var authorizedReqOpts = { a: 'b', c: 'd' };
+ describe('customEndpoint (no authorization attempted)', function() {
+ var makeAuthorizedRequest;
- 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
- };
+ beforeEach(function() {
+ makeAuthorizedRequest = util.makeAuthorizedRequestFactory({
+ customEndpoint: true
+ });
+ });
- gsa_Override = function() {
- return function authorize(reqOpts, callback) {
- callback(null, authorizedReqOpts);
- };
- };
+ it('should pass options back to onAuthorized callback', function(done) {
+ var reqOpts = { a: 'b', c: 'd' };
- request_Override = function(reqOpts, callback) {
- if (attemptedRetries === 3) {
- setTimeout = old_setTimeout;
+ makeAuthorizedRequest(reqOpts, {
+ onAuthorized: function(err, authorizedReqOpts) {
+ assert.ifError(err);
+ assert.deepEqual(reqOpts, authorizedReqOpts);
done();
- } else {
- callback(error); // this callback should check for rate limits
}
+ });
+ });
+
+ it('should not authenticate requests with a custom API', function(done) {
+ var reqOpts = { a: 'b', c: 'd' };
+
+ utilOverrides.makeRequest = function(rOpts) {
+ assert.deepEqual(rOpts, reqOpts);
+ done();
};
- var makeRequest = util.makeAuthorizedRequest({});
- makeRequest({}, assert.ifError);
+ makeAuthorizedRequest(reqOpts, assert.ifError);
});
+ });
- 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' };
+ 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' };
- 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
+ utilOverrides.authorizeRequest = function(cfg, rOpts) {
+ assert.deepEqual(cfg, config);
+ assert.deepEqual(rOpts, reqOpts);
+ done();
};
- gsa_Override = function() {
- return function authorize(reqOpts, callback) {
- callback(null, authorizedReqOpts);
+ var makeAuthorizedRequest = util.makeAuthorizedRequestFactory(config);
+ makeAuthorizedRequest(reqOpts);
+ });
+
+ describe('authorization errors', function() {
+ var error = new Error('Error.');
+
+ beforeEach(function() {
+ utilOverrides.authorizeRequest = function(cfg, rOpts, callback) {
+ callback(error);
};
- };
+ });
- request_Override = function(reqOpts, callback) {
- callback(error); // this callback should check for rate limits
- };
+ it('should invoke the callback with error', function(done) {
+ var makeAuthorizedRequest = util.makeAuthorizedRequestFactory();
+ makeAuthorizedRequest({}, function(err) {
+ assert.deepEqual(err, error);
+ done();
+ });
+ });
- var makeRequest = util.makeAuthorizedRequest({});
- makeRequest({}, function(err) {
- setTimeout = old_setTimeout;
- assert.equal(err, error);
- assert.equal(err.code, 'done');
- done();
+ it('should invoke the onAuthorized handler with error', function(done) {
+ var makeAuthorizedRequest = util.makeAuthorizedRequestFactory();
+ makeAuthorizedRequest({}, {
+ onAuthorized: function(err) {
+ assert.deepEqual(err, error);
+ done();
+ }
+ });
});
});
- it('should retry rate limits on API errors', 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
+ describe('authorization success', function() {
+ var reqOpts = { a: 'b', c: 'd' };
+
+ it('should return the authorized request to callback', function(done) {
+ utilOverrides.authorizeRequest = function(cfg, rOpts, callback) {
+ callback(null, rOpts);
+ };
- var authorizedReqOpts = { a: 'b', c: 'd' };
+ var makeAuthorizedRequest = util.makeAuthorizedRequestFactory();
+ makeAuthorizedRequest(reqOpts, {
+ onAuthorized: function(err, authorizedReqOpts) {
+ assert.deepEqual(authorizedReqOpts, reqOpts);
+ done();
+ }
+ });
+ });
- 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 make request with correct options', function(done) {
+ var config = { a: 'b', c: 'd' };
- gsa_Override = function() {
- return function authorize(reqOpts, callback) {
- callback(null, authorizedReqOpts);
+ utilOverrides.authorizeRequest = function(cfg, rOpts, callback) {
+ callback(null, rOpts);
};
- };
- request_Override = function(reqOpts, callback) {
- callback(null, null, { error: error });
- };
+ utilOverrides.makeRequest = function(authorizedReqOpts, cfg, cb) {
+ assert.deepEqual(authorizedReqOpts, reqOpts);
+ assert.deepEqual(cfg, config);
+ cb();
+ };
- var makeRequest = util.makeAuthorizedRequest({});
- makeRequest({}, function(err) {
- setTimeout = old_setTimeout;
- assert.equal(err.message, 'Rate Limit Error.');
- assert.equal(err.code, 'done');
- done();
+ var makeAuthorizedRequest = util.makeAuthorizedRequestFactory(config);
+ makeAuthorizedRequest(reqOpts, done);
});
});
+ });
- it('should retry rate limits on rateLimitExceeded', function(done) {
- var attemptedRetries = 0;
- var error = new Error('Rate Limit Error.');
- error.code = 403; // not a rate limit code!
- error.errors = [{ reason: 'rateLimitExceeded' }];
+ describe('getCredentials', function() {
+ var fakeAuthClient = {
+ email: 'fake-email@example.com',
+ key: 'fake-key',
- var authorizedReqOpts = { a: 'b', c: 'd' };
+ authorize: function(callback) { callback(); }
+ };
+ var config = { 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 return getCredentials method', function() {
+ utilOverrides.getAuthClient = function(config, callback) {
+ callback(null, fakeAuthClient);
};
- gsa_Override = function() {
- return function authorize(reqOpts, callback) {
- callback(null, authorizedReqOpts);
- };
- };
+ var makeAuthorizedRequest =
+ util.makeAuthorizedRequestFactory(config, assert.ifError);
- request_Override = function(reqOpts, callback) {
- callback(null, null, { error: error });
- };
+ assert.equal(typeof makeAuthorizedRequest.getCredentials, 'function');
+ });
- var makeRequest = util.makeAuthorizedRequest({});
- makeRequest({}, function(err) {
- setTimeout = old_setTimeout;
- assert.equal(attemptedRetries, 3);
- assert.equal(err.message, 'Rate Limit Error.');
+ it('should pass config to getAuthClient', function(done) {
+ utilOverrides.getAuthClient = function(cfg) {
+ assert.deepEqual(cfg, config);
done();
- });
- });
+ };
- it('should retry rate limits on userRateLimitExceeded', function(done) {
- var attemptedRetries = 0;
- var error = new Error('Rate Limit Error.');
- error.code = 403; // not a rate limit code!
- error.errors = [{ reason: 'userRateLimitExceeded' }];
+ var makeAuthorizedRequest =
+ util.makeAuthorizedRequestFactory(config, assert.ifError);
- var authorizedReqOpts = { a: 'b', c: 'd' };
+ makeAuthorizedRequest.getCredentials();
+ });
- 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 execute callback with error', function(done) {
+ var error = new Error('Error.');
- gsa_Override = function() {
- return function authorize(reqOpts, callback) {
- callback(null, authorizedReqOpts);
- };
+ utilOverrides.getAuthClient = function(config, callback) {
+ callback(error);
};
- request_Override = function(reqOpts, callback) {
- callback(null, null, { error: error });
- };
+ var makeAuthorizedRequest =
+ util.makeAuthorizedRequestFactory(config, assert.ifError);
- var makeRequest = util.makeAuthorizedRequest({});
- makeRequest({}, function(err) {
- setTimeout = old_setTimeout;
- assert.equal(attemptedRetries, 3);
- assert.equal(err.message, 'Rate Limit Error.');
+ makeAuthorizedRequest.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
+ it('should authorize the connection', function(done) {
+ fakeAuthClient.authorize = function(callback) {
+ callback();
};
- gsa_Override = function() {
- return function authorize(reqOpts, callback) {
- callback(null, authorizedReqOpts);
- };
+ utilOverrides.getAuthClient = function(config, callback) {
+ callback(null, fakeAuthClient);
};
- request_Override = function(reqOpts, callback) {
- callback(error); // this callback should check for rate limits
- };
+ var makeAuthorizedRequest =
+ util.makeAuthorizedRequestFactory(config, assert.ifError);
- var makeRequest = util.makeAuthorizedRequest({});
- makeRequest({}, function(err) {
- setTimeout = old_setTimeout;
- assert.equal(attemptedRetries, 3);
- assert.equal(err, error);
- done();
- });
+ makeAuthorizedRequest.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' };
+ it('should execute callback with authorization error', function(done) {
+ var error = new Error('Error.');
- 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
+ fakeAuthClient.authorize = function(cb) {
+ cb(error);
};
- gsa_Override = function() {
- return function authorize(reqOpts, callback) {
- callback(null, authorizedReqOpts);
- };
+ utilOverrides.getAuthClient = function(config, callback) {
+ callback(null, fakeAuthClient);
};
- request_Override = function(reqOpts, callback) {
- callback(error); // this callback should check for rate limits
- };
+ var makeAuthorizedRequest =
+ util.makeAuthorizedRequestFactory(config, assert.ifError);
- var makeRequest = util.makeAuthorizedRequest({
- maxRetries: MAX_RETRIES
- });
-
- makeRequest({}, function(err) {
- setTimeout = old_setTimeout;
- assert.equal(attemptedRetries, MAX_RETRIES);
- assert.equal(err, error);
+ makeAuthorizedRequest.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);
- };
+ utilOverrides.getAuthClient = function(config, callback) {
+ callback(null, fakeAuthClient);
};
- request_Override = function(reqOpts, callback) {
- callback(error); // this callback should check for rate limits
- };
+ var makeAuthorizedRequest =
+ util.makeAuthorizedRequestFactory(config, assert.ifError);
- var makeRequest = util.makeAuthorizedRequest({
- autoRetry: false
- });
+ makeAuthorizedRequest.getCredentials(function(err, credentials) {
+ assert.deepEqual(credentials, {
+ client_email: fakeAuthClient.email,
+ private_key: fakeAuthClient.key
+ });
- makeRequest({}, function(err) {
- setTimeout = old_setTimeout;
- assert.equal(attemptedRetries, 0);
- assert.equal(err, error);
done();
});
});
@@ -778,4 +758,227 @@ 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 false from generic error', function() {
+ var error = new Error('Generic error with no code');
+
+ assert.strictEqual(util.shouldRetryRequest(error), 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);
+ });
+
+ it('should detect rateLimitExceeded reason', function() {
+ var rateLimitError = new Error('Rate limit error without code.');
+ rateLimitError.errors = [{ reason: 'rateLimitExceeded' }];
+
+ assert.strictEqual(util.shouldRetryRequest(rateLimitError), true);
+ });
+
+ it('should detect userRateLimitExceeded reason', function() {
+ var rateLimitError = new Error('Rate limit error without code.');
+ rateLimitError.errors = [{ reason: 'userRateLimitExceeded' }];
+
+ assert.strictEqual(util.shouldRetryRequest(rateLimitError), 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);
+ }
+ });
+ });
+
+ describe('makeRequest', function() {
+ var PKG = require('../../package.json');
+ var USER_AGENT = 'gcloud-node/' + PKG.version;
+ var reqOpts = { a: 'b', c: 'd' };
+ var expectedReqOpts = extend(true, {}, reqOpts, {
+ headers: {
+ 'User-Agent': USER_AGENT
+ }
+ });
+
+ it('should make a request', function(done) {
+ request_Override = function() {
+ done();
+ };
+
+ util.makeRequest({}, assert.ifError, {});
+ });
+
+ it('should add the user agent', function(done) {
+ request_Override = function(rOpts) {
+ assert.deepEqual(rOpts, expectedReqOpts);
+ done();
+ };
+
+ util.makeRequest(reqOpts, assert.ifError, {});
+ });
+
+ it('should let handleResp handle the response', function(done) {
+ var error = new Error('Error.');
+ var response = { a: 'b', c: 'd' };
+ var body = response.a;
+
+ request_Override = function(rOpts, callback) {
+ callback(error, response, body);
+ };
+
+ utilOverrides.handleResp = function(err, resp, bdy) {
+ assert.deepEqual(err, error);
+ assert.deepEqual(resp, response);
+ assert.deepEqual(bdy, body);
+ done();
+ };
+
+ util.makeRequest({}, {}, assert.ifError);
+ });
+
+ describe('request errors', function() {
+ describe('non-rate limit error', function() {
+ it('should return error to callback', function(done) {
+ var nonRateLimitError = new Error('Error.');
+
+ request_Override = function(reqOpts, callback) {
+ callback(nonRateLimitError);
+ };
+
+ util.makeRequest({}, {}, function(err) {
+ assert.deepEqual(err, nonRateLimitError);
+ done();
+ });
+ });
+ });
+
+ 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();
+ };
+
+ util.makeRequest({}, {}, util.noop);
+ });
+
+ it('should default to 3 retries', function(done) {
+ var attempts = 0;
+ var expectedAttempts = 4; // the original request + 3 retries
+
+ utilOverrides.handleResp = function(err, resp, body, callback) {
+ attempts++;
+ callback(err);
+ };
+
+ util.makeRequest({}, {}, function(err) {
+ assert.equal(err, rateLimitError);
+ assert.equal(attempts, expectedAttempts);
+ done();
+ });
+ });
+
+ it('should allow max retries to be specified', function(done) {
+ var attempts = 0;
+ var maxRetries = 5;
+ var expectedAttempts = maxRetries + 1; // the original req
+
+ utilOverrides.handleResp = function(err, resp, body, callback) {
+ attempts++;
+ callback(err);
+ };
+
+ util.makeRequest({}, { maxRetries: maxRetries }, function(err) {
+ assert.equal(err, rateLimitError);
+ assert.equal(attempts, expectedAttempts);
+ done();
+ });
+ });
+
+ it('should not retry reqs if autoRetry is false', function(done) {
+ var attempts = 0;
+ var expectedAttempts = 1; // the original req
+
+ utilOverrides.handleResp = function(err, resp, body, callback) {
+ attempts++;
+ callback(err);
+ };
+
+ util.makeRequest({}, { autoRetry: false }, function(err) {
+ assert.equal(err, rateLimitError);
+ assert.equal(attempts, expectedAttempts);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('request success', function() {
+ it('should let handleResp handle response', function(done) {
+ utilOverrides.handleResp = function() {
+ done();
+ };
+
+ request_Override = function(reqOpts, callback) {
+ callback();
+ };
+
+ util.makeRequest({}, {}, assert.ifError);
+ });
+ });
+ });
});
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);
+ });
});
});