From 81aba400aa9176f1bb65366698d49d636b19c971 Mon Sep 17 00:00:00 2001 From: Stephen Sawchuk Date: Thu, 6 Nov 2014 16:27:23 -0500 Subject: [PATCH] use google-service-account --- lib/bigquery/index.js | 14 +- lib/bigquery/table.js | 2 +- lib/common/connection.js | 353 ---------------------------------- lib/common/util.js | 148 +++++++++----- lib/datastore/dataset.js | 12 +- lib/datastore/request.js | 50 ++--- lib/datastore/transaction.js | 4 +- lib/pubsub/index.js | 17 +- lib/storage/bucket.js | 12 +- lib/storage/file.js | 24 ++- lib/storage/index.js | 43 +---- package.json | 1 + regression/datastore.js | 1 - test/bigquery/index.js | 25 ++- test/common/connection.js | 241 ----------------------- test/common/util.js | 272 +++++++++++++++++++------- test/datastore/dataset.js | 45 +---- test/datastore/request.js | 32 +-- test/datastore/transaction.js | 6 +- test/pubsub/index.js | 25 ++- test/storage/bucket.js | 36 ++-- test/storage/file.js | 63 +++--- test/storage/index.js | 24 --- 23 files changed, 521 insertions(+), 929 deletions(-) delete mode 100644 lib/common/connection.js delete mode 100644 test/common/connection.js diff --git a/lib/bigquery/index.js b/lib/bigquery/index.js index 85133449a1e..8e233d5b429 100644 --- a/lib/bigquery/index.js +++ b/lib/bigquery/index.js @@ -24,12 +24,6 @@ var extend = require('extend'); var streamEvents = require('stream-events'); var through = require('through2'); -/** - * @type module:common/connection - * @private - */ -var conn = require('../common/connection.js'); - /** * @type module:bigquery/dataset * @private @@ -115,9 +109,9 @@ function BigQuery(options) { options = options || {}; - this.connection_ = new conn.Connection({ + this.makeAuthorizedRequest_ = util.makeAuthorizedRequest({ credentials: options.credentials, - keyFilename: options.keyFilename, + keyFile: options.keyFilename, scopes: SCOPES }); @@ -565,9 +559,7 @@ BigQuery.prototype.makeReq_ = function(method, path, query, body, callback) { reqOpts.json = body; } - this.connection_.req(reqOpts, function(err, res, body) { - util.handleResp(err, res, body, callback); - }); + this.makeAuthorizedRequest_(reqOpts, callback); }; module.exports = BigQuery; diff --git a/lib/bigquery/table.js b/lib/bigquery/table.js index efe3ba05d07..043ce5d0707 100644 --- a/lib/bigquery/table.js +++ b/lib/bigquery/table.js @@ -305,7 +305,7 @@ Table.prototype.createWriteStream = function(metadata) { dup.once('writing', function() { util.makeWritableStream(dup, { - connection: that.bigQuery.connection_, + makeAuthorizedRequest: that.bigQuery.makeAuthorizedRequest_, metadata: { configuration: { load: metadata diff --git a/lib/common/connection.js b/lib/common/connection.js deleted file mode 100644 index f0f72889db4..00000000000 --- a/lib/common/connection.js +++ /dev/null @@ -1,353 +0,0 @@ -/** - * Copyright 2014 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. - */ - -/** - * @private - * @module common/connection - */ - -'use strict'; - -var events = require('events'); -var extend = require('extend'); -var fs = require('fs'); -var GAPIToken = require('gapitoken'); -var nodeutil = require('util'); -var request = require('request'); - -/** @type {module:common/util} */ -var util = require('./util'); - -/** @const {string} Base URL for outgoing requests. */ -var METADATA_TOKEN_URL = - 'http://metadata/computeMetadata/v1/instance/service-accounts/default/' + - 'token'; - -/** @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; - -/** - * Token represents an access token. - * - * @constructor - * @param {string} accessToken - Access token. - * @param {date} expiry - Expiration datetime. - * - * @example - * var token = new Token(ACCESS_TOKEN, EXPIRY); - */ -function Token(accessToken, expiry) { - this.accessToken = accessToken; - this.expiry = expiry; -} - -/** - * Is this token expired? - * - * @return {boolean} - * - * @example - * token.isExpired(); - */ -Token.prototype.isExpired = function() { - if (!this.accessToken || !this.expiry) { - return true; - } - return new Date().getTime() - this.expiry.getTime() > 0; -}; - -module.exports.Token = Token; - -/** - * Create a connection object. - * - * @param {object} opts - Configuration options. - * @param {array=} opts.scopes - Scopes required for access. - * @param {string=} opts.keyFilename - Full path to the JSON key downloaded - * from the Google Developers Console. Alternatively, you may provide a - * `credentials` object. - * @param {object=} opts.credentials - Credentials object. - * @param {string} opts.credentials.client_email - * @param {string} opts.credentials.private_key - * - * @example - * var SCOPES = [ - * 'https://www.googleapis.com/auth/datastore', - * 'https://www.googleapis.com/auth/userinfo.email' - * ]; - * var conn = new Connection({ scopes: SCOPES }); - */ -function Connection(opts) { - events.EventEmitter.call(this); - this.setMaxListeners(0); - - opts = opts || {}; - - this.credentials = null; - this.opts = opts; - this.scopes = opts.scopes || []; - this.token = null; // existing access token, if exists - - this.isConnecting = false; - - if (opts.credentials) { - if (opts.credentials.client_email && opts.credentials.private_key) { - this.credentials = opts.credentials; - } else { - throw new Error('A credentials object must contain the following keys: ' + - 'client_email, private_key'); - } - } -} - -nodeutil.inherits(Connection, events.EventEmitter); - -/** - * Retrieve a token to authorize requests. - * - * @todo Connect should be context aware, it should not require an email and - * key, if it's running on Google Compute Engine. - * - * @example - * conn.connect(); - */ -Connection.prototype.connect = function() { - var that = this; - this.isConnecting = true; - // retrieves an access token - this.fetchToken(function(err, token) { - that.isConnecting = false; - if (err) { - that.emit('connected', err); - return; - } - that.token = token; - that.emit('connected'); - }); -}; - -/** - * Fetch a new access token. - * - * @param {function} callback - The callback function. - * - * @example - * conn.fetchToken(function(err) {}); - */ -Connection.prototype.fetchToken = function(callback) { - var that = this; - if (!this.opts.keyFilename && !this.credentials) { - // We should be on GCE, try to retrieve token from the metadata server. - this.requester({ - method: 'get', - uri: METADATA_TOKEN_URL, - headers: { - 'X-Google-Metadata-Request': 'True' - }, - json: true - }, function(err, res, body) { - if (err || !body.access_token) { - // TODO: Provide better context about the error here. - callback(err); - return; - } - var exp = new Date(Date.now() + body.expires_in * 1000); - callback(null, new Token(body.access_token, exp)); - }); - return; - } - if (!this.credentials) { - // read key file once and cache the contents. - fs.readFile(this.opts.keyFilename, function(err, data) { - if (err) { - callback(err); - return; - } - that.credentials = JSON.parse(data); - that.fetchServiceAccountToken_(callback); - }); - return; - } - this.fetchServiceAccountToken_(callback); -}; - -/** - * Fetch a service account token. - * - * @param {function} callback - The callback function. - * - * @example - * conn.fetchServiceAccountToken_(function(err) {}); - */ -Connection.prototype.fetchServiceAccountToken_ = function(callback) { - var gapi = new GAPIToken({ - iss: this.credentials.client_email, - key: this.credentials.private_key, - scope: this.scopes.join(' ') - }, function(err) { - if (err) { - callback(err); - return; - } - gapi.getToken(function(err) { - if (err) { - callback(err); - return; - } - var exp = new Date(gapi.token_expires * 1000); - callback(null, new Token(gapi.token, exp)); - }); - }); -}; - -/** - * Make an authorized request if the current connection token is still valid. If - * it's not, try to reconnect to the limit specified by - * MAX_TOKEN_REFRESH_ATTEMPTS. If a valid connection still cannot be made, - * execute the callback with the API error. - * - * @param {object} requestOptions - Request options. - * @param {function=} callback - The callback function. - * - * @example - * conn.req({}, function(err) {}); - */ -Connection.prototype.req = function(requestOptions, callback) { - var that = this; - var tokenRefreshAttempts = 0; - callback = callback || util.noop; - function onAuthorizedReq(err, authorizedReq) { - if (err) { - callback(err); - return; - } - that.requester(authorizedReq, function(err) { - if (err && err.code === 401 && - ++tokenRefreshAttempts <= MAX_TOKEN_REFRESH_ATTEMPTS) { - // Invalid token. Try to fetch a new one. - that.token = null; - that.createAuthorizedReq(requestOptions, onAuthorizedReq); - return; - } - callback.apply(null, util.toArray(arguments)); - }); - } - this.createAuthorizedReq(requestOptions, onAuthorizedReq); -}; - -/** - * Create an authorized request. - * - * @param {object} requestOptions - Request options. - * @param {function} callback - The callback function. - * - * @example - * conn.createAuthorizedReq({}, function(err) {}); - */ -Connection.prototype.createAuthorizedReq = function(requestOptions, callback) { - var that = this; - - var reqOpts = extend(true, {}, requestOptions, { headers: {} }); - - if (reqOpts.headers['User-Agent']) { - reqOpts.headers['User-Agent'] += '; ' + USER_AGENT; - } else { - reqOpts.headers['User-Agent'] = USER_AGENT; - } - - function onConnected(err) { - if (err) { - callback(err); - return; - } - callback(null, that.authorizeReq(reqOpts)); - } - - if (this.isConnected()) { - setImmediate(onConnected); - return; - } - - this.once('connected', onConnected); - - if (!this.isConnecting) { - this.connect(); - } -}; - -/** - * The default requester. - */ -Connection.prototype.requester = request; - -/** - * Get the status of the connection. - * - * `true`: connection is inited with a valid token that is not expired. - * - * @return {boolean} - * - * @example - * conn.isConnected(); - */ -Connection.prototype.isConnected = function() { - return this.token && !this.token.isExpired(); -}; - -/** - * Add authorization header to the specified request. - * - * @todo Clone the request object. - * - * @param {object} requestOptions - Request options. - * @return {object} Authorized request options. - */ -Connection.prototype.authorizeReq = function(requestOptions) { - return extend(true, {}, requestOptions, { - headers: { - Authorization: 'Bearer ' + this.token.accessToken - } - }); -}; - -/** - * Get the connection's credentials. If a token hasn't been fetched yet, one - * will be triggered now. - * - * @return {object} - */ -Connection.prototype.getCredentials = function(callback) { - var that = this; - if (this.credentials) { - setImmediate(callback, null, that.credentials); - return; - } - this.fetchToken(function(err) { - if (err) { - callback(err); - return; - } - callback(null, that.credentials); - }); -}; - -module.exports.Connection = Connection; diff --git a/lib/common/util.js b/lib/common/util.js index dc19c7ccb01..819f1261258 100644 --- a/lib/common/util.js +++ b/lib/common/util.js @@ -22,9 +22,20 @@ */ var extend = require('extend'); +var gsa = require('google-service-account'); +var request = require('request'); var util = 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; + /** * Extend a global configuration object with user options provided at the time * of sub-module instantiation. @@ -266,7 +277,7 @@ function makeWritableStream(dup, options, onComplete) { // // `options.request` takes precedence over the defaults, but not over the // headers, as these set up the boundary. - var request = extend(true, defaults, options.request, { + var reqOpts = extend(true, defaults, options.request, { headers: { 'Content-Type': 'multipart/related; boundary="' + boundary + '"' } @@ -274,54 +285,101 @@ function makeWritableStream(dup, options, onComplete) { // With the provided connection, be sure we have a valid token before // attempting to create a request. - options.connection.createAuthorizedReq(request, function(err, req) { - if (err) { - dup.emit('error', err); - return; - } - - var streamType = options.metadata.contentType || 'application/octet-stream'; - - var stream = options.connection.requester(req); - stream.callback = noop; - - // Write the metadata to the request. - stream.write('--' + boundary + '\n'); - stream.write('Content-Type: application/json\n\n'); - stream.write(JSON.stringify(options.metadata)); - stream.write('\n\n'); - stream.write('--' + boundary + '\n'); - stream.write('Content-Type: ' + streamType + '\n\n'); - - // Overwrite the `end` function, so we can close the boundary. - var oldEndFn = stream.end; - stream.end = function(data, encoding, callback) { - data = (data || '') + '\n--' + boundary + '--\n'; - stream.write(data, encoding, callback); - oldEndFn.apply(this); - }; - - // When the request is complete, parse it. If everything went well, pass the - // parsed response data to the callback handler. - stream.on('complete', function(res) { - handleResp(null, res, res.body, function(err, data) { - if (err) { - dup.emit('error', err); - dup.end(); - return; - } - onComplete(data); + options.makeAuthorizedRequest(reqOpts, { + onAuthorized: function(err, authorizedReqOpts) { + if (err) { + dup.emit('error', err); + dup.end(); + return; + } + + var streamType = + options.metadata.contentType || 'application/octet-stream'; + + var stream = request(authorizedReqOpts); + stream.callback = noop; + + // Write the metadata to the request. + stream.write('--' + boundary + '\n'); + stream.write('Content-Type: application/json\n\n'); + stream.write(JSON.stringify(options.metadata)); + stream.write('\n\n'); + stream.write('--' + boundary + '\n'); + stream.write('Content-Type: ' + streamType + '\n\n'); + + // Overwrite the `end` function, so we can close the boundary. + var oldEndFn = stream.end; + stream.end = function(data, encoding, callback) { + data = (data || '') + '\n--' + boundary + '--\n'; + stream.write(data, encoding, callback); + oldEndFn.apply(this); + }; + + // When the request is complete, parse it. If everything went well, pass + // the parsed response data to the callback handler. + stream.on('complete', function(res) { + handleResp(null, res, res.body, function(err, data) { + if (err) { + dup.emit('error', err); + dup.end(); + return; + } + onComplete(data); + }); }); - }); - // We have a writable stream - tell Duplexify about it, so it can resume - // processing incoming data. - dup.setWritable(stream); + // We have a writable stream - tell Duplexify about it, so it can resume + // processing incoming data. + dup.setWritable(stream); - // Keep part of the stream open to keep Request from closing the connection. - // Reference: http://goo.gl/zZVSif. - dup.pipe(stream); + // Keep part of the stream open to keep Request from closing the + // connection. Reference: http://goo.gl/zZVSif. + dup.pipe(stream); + } }); } module.exports.makeWritableStream = makeWritableStream; + +function makeAuthorizedRequest(config) { + var authorize = gsa(config); + + function makeRequest(reqOpts, callback) { + var tokenRefreshAttempts = 0; + reqOpts.headers = reqOpts.headers || {}; + + if (reqOpts.headers['User-Agent']) { + reqOpts.headers['User-Agent'] += '; ' + USER_AGENT; + } else { + reqOpts.headers['User-Agent'] = USER_AGENT; + } + + function onAuthorizedRequest(err, authorizedReqOpts) { + if (err) { + if (err.code === 401 && + ++tokenRefreshAttempts <= MAX_TOKEN_REFRESH_ATTEMPTS) { + authorize(reqOpts, onAuthorizedRequest); + } else { + (callback.onAuthorized || callback)(err); + } + return; + } + + if (callback.onAuthorized) { + callback.onAuthorized(null, authorizedReqOpts); + } else { + request(authorizedReqOpts, function(err, res, body) { + handleResp(err, res, body, callback); + }); + } + } + + authorize(reqOpts, onAuthorizedRequest); + } + + makeRequest.getCredentials = authorize.getCredentials; + + return makeRequest; +} + +module.exports.makeAuthorizedRequest = makeAuthorizedRequest; diff --git a/lib/datastore/dataset.js b/lib/datastore/dataset.js index 73bcdc814c4..1ff57351869 100644 --- a/lib/datastore/dataset.js +++ b/lib/datastore/dataset.js @@ -22,12 +22,6 @@ var nodeutil = require('util'); -/** - * @type module:common/connection - * @private - */ -var conn = require('../common/connection.js'); - /** * @type module:datastore/entity * @private @@ -99,9 +93,9 @@ function Dataset(options) { options = options || {}; - this.connection = new conn.Connection({ + this.makeAuthorizedRequest_ = util.makeAuthorizedRequest({ credentials: options.credentials, - keyFilename: options.keyFilename, + keyFile: options.keyFilename, scopes: SCOPES }); @@ -213,7 +207,7 @@ Dataset.prototype.runInTransaction = function(fn, callback) { * @private */ Dataset.prototype.createTransaction_ = function() { - return new Transaction(this.connection, this.projectId); + return new Transaction(this, this.projectId); }; module.exports = Dataset; diff --git a/lib/datastore/request.js b/lib/datastore/request.js index 50ab4d39964..7580ef958f3 100644 --- a/lib/datastore/request.js +++ b/lib/datastore/request.js @@ -440,36 +440,40 @@ DatastoreRequest.prototype.makeReq_ = function(method, body, callback) { var pbRequest = new pb[pbKey + 'Request'](body).toBuffer(); var pbResponse = pb[pbKey + 'Response']; - this.connection.createAuthorizedReq({ + var reqOpts = { method: 'POST', host: GOOGLE_APIS_HOST, path: '/datastore/v1beta2/datasets/' + this.projectId + '/' + method, headers: { - 'content-type': 'application/x-protobuf' + 'Content-Type': 'application/x-protobuf' } - }, function(err, request) { - if (err) { - callback(err); - return; - } - var remoteStream = https.request(request, function(resp) { - var buffer = new Buffer(''); - resp.on('data', function(chunk) { - buffer = Buffer.concat([buffer, chunk]); - }); - resp.on('end', function() { - util.handleResp(null, resp, buffer.toString(), function(err) { - if (err) { - callback(err); - return; - } - callback(null, pbResponse.decode(buffer)); + }; + + this.makeAuthorizedRequest_(reqOpts, { + onAuthorized: function(err, authorizedReqOpts) { + if (err) { + callback(err); + return; + } + var remoteStream = https.request(authorizedReqOpts, function(resp) { + var buffer = new Buffer(''); + resp.on('data', function(chunk) { + buffer = Buffer.concat([buffer, chunk]); + }); + resp.on('end', function() { + util.handleResp(null, resp, buffer.toString(), function(err) { + if (err) { + callback(err); + return; + } + callback(null, pbResponse.decode(buffer)); + }); }); }); - }); - remoteStream.on('error', callback); - remoteStream.write(pbRequest); - remoteStream.end(); + remoteStream.on('error', callback); + remoteStream.write(pbRequest); + remoteStream.end(); + } }); }; diff --git a/lib/datastore/transaction.js b/lib/datastore/transaction.js index 2c1a220b814..b387bdd84a2 100644 --- a/lib/datastore/transaction.js +++ b/lib/datastore/transaction.js @@ -58,8 +58,8 @@ var DatastoreRequest = require('./request.js'); * // `transaction` is a Transaction object. * }); */ -function Transaction(connection, projectId) { - this.connection = connection; +function Transaction(dataset, projectId) { + this.makeAuthorizedRequest_ = dataset.makeAuthorizedRequest_; this.id = null; this.isFinalized = false; this.projectId = projectId; diff --git a/lib/pubsub/index.js b/lib/pubsub/index.js index 1410300f7e3..119493baefb 100644 --- a/lib/pubsub/index.js +++ b/lib/pubsub/index.js @@ -20,12 +20,6 @@ 'use strict'; -/** - * @type module:common/connection - * @private - */ -var conn = require('../common/connection.js'); - /** * @type module:pubsub/subscription * @private @@ -115,11 +109,12 @@ var SCOPES = [ function PubSub(options) { options = options || {}; - this.connection = new conn.Connection({ + this.makeAuthorizedRequest_ = util.makeAuthorizedRequest({ credentials: options.credentials, - keyFilename: options.keyFilename, + keyFile: options.keyFilename, scopes: SCOPES }); + this.projectId = options.projectId; this.projectName = '/projects/' + this.projectId; } @@ -286,12 +281,12 @@ PubSub.prototype.makeReq_ = function(method, path, q, body, callback) { qs: q, uri: PUBSUB_BASE_URL + '/' + path }; + if (body) { reqOpts.json = body; } - this.connection.req(reqOpts, function(err, res, body) { - util.handleResp(err, res, body, callback); - }); + + this.makeAuthorizedRequest_(reqOpts, callback); }; module.exports = PubSub; diff --git a/lib/storage/bucket.js b/lib/storage/bucket.js index 76f78a5285e..16b61daf126 100644 --- a/lib/storage/bucket.js +++ b/lib/storage/bucket.js @@ -71,10 +71,10 @@ var STORAGE_BASE_URL = 'https://www.googleapis.com/storage/v1/b'; * name: 'bucket' * }); */ -function Bucket(options) { - this.connection_ = options.connection_; +function Bucket(storage, name) { this.metadata = {}; - this.name = options.name || options.bucketName; + this.name = name; + this.storage = storage; if (!this.name) { throw Error('A bucket name is needed to use Google Cloud Storage.'); @@ -338,12 +338,12 @@ Bucket.prototype.makeReq_ = function(method, path, query, body, callback) { qs: query, uri: STORAGE_BASE_URL + '/' + this.name + path }; + if (body) { reqOpts.json = body; } - this.connection_.req(reqOpts, function(err, res, body) { - util.handleResp(err, res, body, callback); - }); + + this.storage.makeAuthorizedRequest_(reqOpts, callback); }; module.exports = Bucket; diff --git a/lib/storage/file.js b/lib/storage/file.js index 8aff3844837..211af929960 100644 --- a/lib/storage/file.js +++ b/lib/storage/file.js @@ -22,6 +22,7 @@ var crypto = require('crypto'); var duplexify = require('duplexify'); +var request = require('request'); var streamEvents = require('stream-events'); /** @@ -193,15 +194,19 @@ File.prototype.copy = function(destination, callback) { * .on('error', function(err) {}); */ File.prototype.createReadStream = function() { - var bucket = this.bucket; + var storage = this.bucket.storage; var dup = duplexify(); function createAuthorizedReq(uri) { - bucket.connection_.createAuthorizedReq({ uri: uri }, function(err, req) { - if (err) { - dup.emit('error', err); - return; + var reqOpts = { uri: uri }; + storage.makeAuthorizedRequest_(reqOpts, { + onAuthorized: function(err, authorizedReqOpts) { + if (err) { + dup.emit('error', err); + dup.end(); + return; + } + dup.setReadable(request(authorizedReqOpts)); } - dup.setReadable(bucket.connection_.requester(req)); }); } if (this.metadata.mediaLink) { @@ -210,6 +215,7 @@ File.prototype.createReadStream = function() { this.getMetadata(function(err, metadata) { if (err) { dup.emit('error', err); + dup.end(); return; } createAuthorizedReq(metadata.mediaLink); @@ -262,7 +268,7 @@ File.prototype.createWriteStream = function(metadata) { dup.once('writing', function() { util.makeWritableStream(dup, { - connection: that.bucket.connection_, + makeAuthorizedRequest: that.bucket.storage.makeAuthorizedRequest_, metadata: metadata, request: { qs: { @@ -361,7 +367,9 @@ File.prototype.getSignedUrl = function(options, callback) { options.resource = '/' + this.bucket.name + '/' + this.name; - this.bucket.connection_.getCredentials(function(err, credentials) { + var makeAuthorizedRequest_ = this.bucket.storage.makeAuthorizedRequest_; + + makeAuthorizedRequest_.getCredentials(function(err, credentials) { if (err) { callback(err); return; diff --git a/lib/storage/index.js b/lib/storage/index.js index 907971c3624..81ed2dfd3b1 100644 --- a/lib/storage/index.js +++ b/lib/storage/index.js @@ -28,12 +28,6 @@ var extend = require('extend'); */ var Bucket = require('./bucket.js'); -/** - * @type module:common/connection - * @private - */ -var conn = require('../common/connection.js'); - /** * @type module:common/util * @private @@ -107,29 +101,19 @@ function Storage(config) { return new Storage(config); } - this.config = config || {}; - - this.connection_ = new conn.Connection({ - credentials: this.config.credentials, - keyFilename: this.config.keyFilename, + this.makeAuthorizedRequest_ = util.makeAuthorizedRequest({ + credentials: config.credentials, + keyFile: config.keyFilename, scopes: SCOPES }); - this.config.connection_ = this.connection_; - this.projectId = this.config.projectId; + this.projectId = config.projectId; } -/*! Developer Documentation - * - * Instance method for creating a Bucket object. Options configured at - * instantiation of the Storage class will be passed through, allowing for - * overridden options specified here. - */ /** * Get a reference to a Google Cloud Storage bucket. * - * @param {object|string} options - Name of the bucket or configuration object. - * @param {string} options.name - The name of the existing bucket. + * @param {object|string} name - Name of the existing bucket. * @return {module:storage/bucket} * * @example @@ -140,15 +124,8 @@ function Storage(config) { * var albums = gcloud.storage().bucket('albums'); * var photos = gcloud.storage().bucket('photos'); */ -Storage.prototype.bucket = function(options) { - if (util.is(options, 'string')) { - options = { - name: options - }; - } - options = options || {}; - // Mix in instance config data to the provided options. - return new Bucket(util.extendGlobalConfig(this.config, options)); +Storage.prototype.bucket = function(name) { + return new Bucket(this, name); }; /** @@ -275,12 +252,12 @@ Storage.prototype.makeReq_ = function(method, path, query, body, callback) { qs: query, uri: STORAGE_BASE_URL + path }; + if (body) { reqOpts.json = body; } - this.connection_.req(reqOpts, function(err, res, body) { - util.handleResp(err, res, body, callback); - }); + + this.makeAuthorizedRequest_(reqOpts, callback); }; module.exports = Storage; diff --git a/package.json b/package.json index 81c2d0bd447..fd871bf8bb5 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "duplexify": "^3.1.2", "extend": "^1.3.0", "gapitoken": "^0.1.3", + "google-service-account": "^1.0.0", "mime": "^1.2.11", "node-uuid": "^1.4.1", "protobufjs": "^3.4.0", diff --git a/regression/datastore.js b/regression/datastore.js index 19bf0f5f0b8..9c7920c9798 100644 --- a/regression/datastore.js +++ b/regression/datastore.js @@ -26,7 +26,6 @@ var ds = datastore.dataset(env); var entity = require('../lib/datastore/entity.js'); describe('datastore', function() { - it('should allocate IDs', function(done) { ds.allocateIds(ds.key('Kind'), 10, function(err, keys) { assert.ifError(err); diff --git a/test/bigquery/index.js b/test/bigquery/index.js index 68253740d2f..a10427e78f5 100644 --- a/test/bigquery/index.js +++ b/test/bigquery/index.js @@ -21,6 +21,7 @@ var assert = require('assert'); var Dataset = require('../../lib/bigquery/dataset'); var Job = require('../../lib/bigquery/job'); +var request = require('request'); var Stream = require('stream').Stream; var Table = require('../../lib/bigquery/table'); var util = require('../../lib/common/util'); @@ -30,7 +31,9 @@ var BigQuery = require('sandboxed-module') requires: { './dataset': Dataset, './job': Job, - './table': FakeTable + './table': FakeTable, + request: fakeRequest, + 'google-service-account': fakeGsa } }); @@ -47,6 +50,22 @@ FakeTable.mergeSchemaWithRows_ = function() { .apply(null, args); }; +var request_Cached = request; +var request_Override; + +function fakeRequest() { + var args = [].slice.apply(arguments); + var results = (request_Override || request_Cached).apply(null, args); + request_Override = null; + return results; +} + +function fakeGsa() { + return function(req, callback) { + callback(null, req); + }; +} + describe('BigQuery', function() { var JOB_ID = JOB_ID; var PROJECT_ID = 'test-project'; @@ -659,7 +678,7 @@ describe('BigQuery', function() { var body = { hi: 'there' }; it('should make correct request', function(done) { - bq.connection_.req = function(request) { + request_Override = function(request) { var basePath = 'https://www.googleapis.com/bigquery/v2/projects/'; assert.equal(request.method, method); assert.equal(request.uri, basePath + bq.projectId + path); @@ -671,7 +690,7 @@ describe('BigQuery', function() { }); it('should execute callback', function(done) { - bq.connection_.req = function(request, callback) { + request_Override = function(request, callback) { callback(); }; bq.makeReq_(method, path, query, body, done); diff --git a/test/common/connection.js b/test/common/connection.js deleted file mode 100644 index db15ae949a4..00000000000 --- a/test/common/connection.js +++ /dev/null @@ -1,241 +0,0 @@ -/** - * Copyright 2014 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. - */ - -/*global describe, it, beforeEach */ - -'use strict'; - -var assert = require('assert'); -var async = require('async'); -var path = require('path'); - -var connection = require('../../lib/common/connection.js'); -var util = require('../../lib/common/util.js'); - -describe('Connection', function() { - var conn; - var privateKeyFileJson = require('../testdata/privateKeyFile.json'); - - beforeEach(function() { - conn = new connection.Connection({ - keyFilename: path.join(__dirname, '../testdata/privateKeyFile.json') - }); - conn.requester = function(opts, callback) { - callback(null); - }; - }); - - it('should use a private key json file', function(done) { - conn.fetchServiceAccountToken_ = function(callback) { - callback(null); - }; - conn.fetchToken(function(err) { - assert.ifError(err); - assert.deepEqual(conn.credentials, privateKeyFileJson); - done(); - }); - }); - - describe('credentials object', function() { - describe('getCredentials', function() { - it('should expose a method to retrieve credentials', function() { - var conn = new connection.Connection(); - assert.equal(typeof conn.getCredentials, 'function'); - }); - - it('should return credentials object if provided', function(done) { - var conn = new connection.Connection({ - credentials: privateKeyFileJson - }); - conn.getCredentials(function(err, credentials) { - assert.deepEqual(credentials, privateKeyFileJson); - done(); - }); - }); - - it('should fetch token if missing credentials object', function(done) { - var conn = new connection.Connection(); - conn.fetchToken = function(cb) { - conn.credentials = privateKeyFileJson; - cb(); - }; - conn.getCredentials(function(err, credentials) { - assert.deepEqual(credentials, privateKeyFileJson); - done(); - }); - }); - }); - - it('should accept and assign a complete credentials object', function() { - var credConnection = new connection.Connection({ - credentials: privateKeyFileJson - }); - assert.deepEqual(credConnection.credentials, privateKeyFileJson); - }); - - it('should reject an incomplete credentials object', function() { - assert.throws(function() { - new connection.Connection({ - credentials: {} - }); - }, /must contain/); - }); - }); - - describe('Token', function() { - var tokenNeverExpires = new connection.Token('token', new Date(3000, 0, 0)); - var tokenExpired = new connection.Token('token', new Date(2011, 0, 0)); - - describe('GCE', function() { - var gceConn; - var metadataResponse = { - body: { - access_token: 'y.8', - expires_in: 60 - } - }; - - beforeEach(function() { - gceConn = new connection.Connection(); - }); - - it('should fetch a token from the metadata server', function() { - gceConn.requester = function(opts) { - assert.equal(opts.uri.indexOf('http://metadata/'), 0); - }; - gceConn.fetchToken(); - }); - - it('should build token from the metadata server response', function() { - var startDate = new Date(); - gceConn.requester = function(opts, callback) { - callback(null, metadataResponse, metadataResponse.body); - }; - gceConn.fetchToken(function(err, token) { - assert.ifError(err); - assert(token instanceof connection.Token); - assert.equal(token.accessToken, metadataResponse.body.access_token); - var addedMs = metadataResponse.body.expires_in * 1000; - var tokenDate = new Date(Date.now() + addedMs); - assert(token.expiry.getTime() >= startDate.getTime()); - assert(token.expiry.getTime() <= tokenDate.getTime()); - }); - }); - }); - - it('should fetch a new token if token expires', function(done) { - conn.token = tokenExpired; - conn.fetchToken = function() { - done(); - }; - conn.req({ uri: 'https://someuri' }, function() {}); - }); - - it('should fetch a new token if API returns a 401', function() { - var fetchTokenCount = 0; - conn.fetchToken = function(callback) { - fetchTokenCount++; - callback(null, tokenNeverExpires); - }; - conn.requester = function(req, callback) { - if (fetchTokenCount === 1) { - callback({ code: 401 }); - } else { - callback(null); - } - }; - conn.req({ uri: 'https://someuri' }, function() {}); - assert.equal(fetchTokenCount, 2); - }); - - it('should try API request 2 times', function(done) { - // Fail 1: invalid token. - // -- try to get token -- - // Fail 2: invalid token. - // -- execute callback with error. - var error = { code: 401 }; - var requesterCount = 0; - conn.fetchToken = function(callback) { - callback(null, tokenNeverExpires); - }; - conn.requester = function(req, callback) { - requesterCount++; - callback(error); - }; - conn.req({ uri: 'https://someuri' }, function(err) { - assert.equal(requesterCount, 2); - assert.deepEqual(err, error); - done(); - }); - }); - - it('should pass all arguments from requester to callback', function(done) { - var args = [null, 1, 2, 3]; - conn.fetchToken = function(callback) { - callback(null, tokenNeverExpires); - }; - conn.requester = function(req, callback) { - callback.apply(null, args); - }; - conn.req({ uri: 'https://someuri' }, function() { - assert.deepEqual(util.toArray(arguments), args); - done(); - }); - }); - - it('should make other requests wait while connecting', function(done) { - var numTokenFetches = 0; - var requestedUris = []; - conn.fetchToken = function(cb) { - numTokenFetches++; - setImmediate(function() { - cb(null, tokenNeverExpires); - }); - }; - conn.requester = function(opts, callback) { - requestedUris.push(opts.uri); - callback(null); - }; - async.parallel([ - function(next) { - assert.strictEqual(conn.isConnecting, false); - conn.req({ uri: '1' }, next); - }, - function(next) { - assert.strictEqual(conn.isConnecting, true); - conn.req({ uri: '2' }, next); - }, - function(next) { - conn.req({ uri: '3' }, next); - } - ], function(err) { - assert.ifError(err); - assert.equal(numTokenFetches, 1); - assert.equal(conn.token, tokenNeverExpires); - assert.deepEqual(requestedUris, ['1', '2', '3']); - done(); - }); - }); - - it('should fetch a new token if token is invalid', function(done) { - conn.token = new connection.Token(); - conn.fetchToken = function() { - done(); - }; - conn.req({ uri: 'https://someuri' }, function() {}); - }); - }); -}); diff --git a/test/common/util.js b/test/common/util.js index 14874a415fe..f9fd0a234bb 100644 --- a/test/common/util.js +++ b/test/common/util.js @@ -20,7 +20,34 @@ var assert = require('assert'); var duplexify = require('duplexify'); -var util = require('../../lib/common/util.js'); +var request = require('request'); + +var util = require('sandboxed-module') + .require('../../lib/common/util', { + requires: { + 'google-service-account': fakeGsa, + request: fakeRequest + } + }); + +var gsa_Override; + +function fakeGsa() { + var args = [].slice.apply(arguments); + var results = (gsa_Override || util.noop).apply(null, args); + gsa_Override = null; + return results || { getCredentials: util.noop }; +} + +var request_Cached = request; +var request_Override; + +function fakeRequest() { + var args = [].slice.apply(arguments); + var results = (request_Override || request_Cached).apply(null, args); + request_Override = null; + return results; +} describe('common/util', function() { describe('arrayize', function() { @@ -97,15 +124,13 @@ describe('common/util', function() { it('should use defaults', function(done) { var dup = duplexify(); util.makeWritableStream(dup, { - connection: { - createAuthorizedReq: function(request) { - assert.equal(request.method, 'POST'); - assert.equal(request.qs.uploadType, 'multipart'); + makeAuthorizedRequest: function(request) { + assert.equal(request.method, 'POST'); + assert.equal(request.qs.uploadType, 'multipart'); - var contentType = request.headers['Content-Type']; - assert.equal(contentType.indexOf('multipart/related'), 0); - done(); - } + var contentType = request.headers['Content-Type']; + assert.equal(contentType.indexOf('multipart/related'), 0); + done(); } }); }); @@ -122,13 +147,11 @@ describe('common/util', function() { }; util.makeWritableStream(dup, { - connection: { - createAuthorizedReq: function(request) { - assert.equal(request.method, req.method); - assert.deepEqual(request.qs, req.qs); - assert.equal(request.something, req.something); - done(); - } + makeAuthorizedRequest: function(request) { + assert.equal(request.method, req.method); + assert.deepEqual(request.qs, req.qs); + assert.equal(request.something, req.something); + done(); }, request: req @@ -145,10 +168,8 @@ describe('common/util', function() { }); util.makeWritableStream(dup, { - connection: { - createAuthorizedReq: function(request, callback) { - callback(error); - } + makeAuthorizedRequest: function(request, opts) { + opts.onAuthorized(error); } }); }); @@ -158,48 +179,45 @@ describe('common/util', function() { var boundary; var metadata = { a: 'b', c: 'd' }; - util.makeWritableStream(dup, { - metadata: metadata, + request_Override = function() { + var written = []; - connection: { - createAuthorizedReq: function(request, callback) { - var contentType = request.headers['Content-Type']; - // Match the UUID boundary from the contentType - boundary = contentType.match(/boundary="([^"]*)/)[1]; - callback(); - }, + var req = duplexify(); - requester: function() { - var written = []; + req.write = function(data) { + written.push(data); + }; - var req = duplexify(); + req.end = function() { + var boundaryLine = '--' + boundary + '\n'; - req.write = function(data) { - written.push(data); - }; + var startFirstBoundaryIdx = written.indexOf(boundaryLine); + var endFirstBoundaryIdx = written.lastIndexOf(boundaryLine); + var endBoundaryIdx = written.indexOf('\n--' + boundary + '--\n'); - req.end = function() { - var boundaryLine = '--' + boundary + '\n'; + assert(startFirstBoundaryIdx > -1); + assert(endFirstBoundaryIdx > startFirstBoundaryIdx); + assert(endBoundaryIdx > -1); - var startFirstBoundaryIdx = written.indexOf(boundaryLine); - var endFirstBoundaryIdx = written.lastIndexOf(boundaryLine); - var endBoundaryIdx = written.indexOf('\n--' + boundary + '--\n'); + assert(written.indexOf(JSON.stringify(metadata)) > -1); - assert(startFirstBoundaryIdx > -1); - assert(endFirstBoundaryIdx > startFirstBoundaryIdx); - assert(endBoundaryIdx > -1); + done(); + }; - assert(written.indexOf(JSON.stringify(metadata)) > -1); + setImmediate(function() { + req.end(); + }); - done(); - }; + return req; + }; - setImmediate(function() { - req.end(); - }); + util.makeWritableStream(dup, { + metadata: metadata, - return req; - } + makeAuthorizedRequest: function(request, opts) { + var contentType = request.headers['Content-Type']; + boundary = contentType.match(/boundary="([^"]*)/)[1]; + opts.onAuthorized(); } }); }); @@ -208,20 +226,18 @@ describe('common/util', function() { var dup = duplexify(); var stream = duplexify(); + request_Override = function() { + return stream; + }; + dup.setWritable = function(writable) { assert.equal(writable, stream); done(); }; util.makeWritableStream(dup, { - connection: { - createAuthorizedReq: function(request, callback) { - callback(); - }, - - requester: function() { - return stream; - } + makeAuthorizedRequest: function(request, opts) { + opts.onAuthorized(); } }); }); @@ -230,21 +246,147 @@ describe('common/util', function() { var dup = duplexify(); var stream = duplexify(); + request_Override = function() { + return stream; + }; + dup.pipe = function(writable) { assert.equal(writable, stream); done(); }; util.makeWritableStream(dup, { - connection: { - createAuthorizedReq: function(request, callback) { + makeAuthorizedRequest: function(request, opts) { + opts.onAuthorized(); + } + }); + }); + }); + + describe('makeAuthorizedRequest', function() { + it('should pass configuration to gsa', function(done) { + var config = { keyFile: 'key', scopes: [1, 2] }; + + gsa_Override = function(cfg) { + assert.deepEqual(cfg, config); + done(); + }; + + util.makeAuthorizedRequest(config); + }); + + it('should return gsa.getCredentials function', function() { + var getCredentials = util.makeAuthorizedRequest().getCredentials; + assert.equal(typeof getCredentials, 'function'); + }); + + 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(); + }; + }; + + var makeRequest = util.makeAuthorizedRequest(); + makeRequest({}); + }); + + 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(); + }; + }; + + var makeRequest = util.makeAuthorizedRequest(); + makeRequest({ headers: { 'User-Agent': 'test' } }); + }); + + it('should execute callback with error', function(done) { + var error = new Error('Error.'); + + gsa_Override = function() { + return function authorize(reqOpts, callback) { + callback(error); + }; + }; + + var makeRequest = util.makeAuthorizedRequest(); + makeRequest({}, function(err) { + assert.equal(err, error); + done(); + }); + }); + + it('should try to reconnect if token invalid', function(done) { + var attempts = 0; + var expectedAttempts = 2; + var error = { code: 401 }; + + gsa_Override = function() { + return function authorize(reqOpts, callback) { + attempts++; + callback(error); + }; + }; + + var makeRequest = util.makeAuthorizedRequest(); + makeRequest({}, function (err) { + assert.equal(attempts, expectedAttempts); + assert.equal(err, error); + done(); + }); + }); + + it('should execute the onauthorized callback', function(done) { + gsa_Override = function() { + return function authorize(reqOpts, callback) { callback(); - }, + }; + }; + + var makeRequest = util.makeAuthorizedRequest(); + makeRequest({}, { onAuthorized: done }); + }); + + it('should execute the onauthorized callback with error', function(done) { + var error = new Error('Error.'); + + gsa_Override = function() { + return function authorize(reqOpts, callback) { + callback(error); + }; + }; - requester: function() { - return stream; + var makeRequest = util.makeAuthorizedRequest(); + makeRequest({}, { + onAuthorized: function(err) { + assert.equal(err, error); + done(); } - } + }); + }); + + it('should make the authorized request', function(done) { + var authorizedReqOpts = { a: 'b', c: 'd' }; + + gsa_Override = function() { + return function authorize(reqOpts, callback) { + callback(null, authorizedReqOpts); + }; + }; + + request_Override = function(reqOpts) { + assert.deepEqual(reqOpts, authorizedReqOpts); + done(); + }; + + var makeRequest = util.makeAuthorizedRequest(); + makeRequest({}, assert.ifError); }); }); }); diff --git a/test/datastore/dataset.js b/test/datastore/dataset.js index 757b05fac0c..24f53f24c44 100644 --- a/test/datastore/dataset.js +++ b/test/datastore/dataset.js @@ -19,47 +19,20 @@ 'use strict'; var assert = require('assert'); -var datastore = require('../../lib').datastore; -var gcloud = require('../../lib'); +var Dataset = require('../../lib/datastore/dataset'); var util = require('../../lib/common/util.js'); describe('Dataset', function() { - it('should not require connection details', function() { - var credentials = require('../testdata/privateKeyFile.json'); - var project = gcloud({ - projectId: 'test-project', - credentials: credentials - }); - var ds = project.datastore.dataset({ hi: 'there' }); - assert.equal(ds.projectId, 'test-project'); - assert.deepEqual(ds.connection.credentials, credentials); - }); - - it('should allow overriding connection details', function() { - var projectCredentials = require('../testdata/privateKeyFile.json'); - var uniqueCredentials = require('../testdata/privateKeyFile-2.json'); - var project = gcloud({ - projectId: 'test-project', - credentials: projectCredentials - }); - var ds = project.datastore.dataset({ - projectId: 'another-project', - credentials: uniqueCredentials - }); - assert.equal(ds.projectId, 'another-project'); - assert.deepEqual(ds.connection.credentials, uniqueCredentials); - }); - describe('key', function() { it('should return key scoped by default namespace', function() { - var ds = datastore.dataset({ projectId: 'test', namespace: 'my-ns' }); + var ds = new Dataset({ projectId: 'test', namespace: 'my-ns' }); var key = ds.key(['Company', 1]); assert.equal(key.namespace, 'my-ns'); assert.deepEqual(key.path, ['Company', 1]); }); it('should allow namespace specification', function() { - var ds = datastore.dataset({ projectId: 'test', namespace: 'my-ns' }); + var ds = new Dataset({ projectId: 'test', namespace: 'my-ns' }); var key = ds.key({ namespace: 'custom-ns', path: ['Company', 1] @@ -69,13 +42,13 @@ describe('Dataset', function() { }); it('should create incomplete key from string', function() { - var ds = datastore.dataset({ projectId: 'test' }); + var ds = new Dataset({ projectId: 'test' }); var key = ds.key('hello'); assert.deepEqual(key.path, ['hello']); }); it('should create incomplete key from array in obj', function() { - var ds = datastore.dataset({ projectId: 'test' }); + var ds = new Dataset({ projectId: 'test' }); var key = ds.key({ path: ['world'] }); @@ -83,7 +56,7 @@ describe('Dataset', function() { }); it('should create incomplete key from array', function() { - var ds = datastore.dataset({ projectId: 'test' }); + var ds = new Dataset({ projectId: 'test' }); var key = ds.key(['Company']); assert.deepEqual(key.path, ['Company']); }); @@ -93,7 +66,7 @@ describe('Dataset', function() { var ds; beforeEach(function() { - ds = datastore.dataset({ projectId: 'test' }); + ds = new Dataset({ projectId: 'test' }); }); it('should begin transaction', function(done) { @@ -145,8 +118,8 @@ describe('Dataset', function() { var dsWithNs; beforeEach(function() { - ds = datastore.dataset({ projectId: 'test' }); - dsWithNs = datastore.dataset({ + ds = new Dataset({ projectId: 'test' }); + dsWithNs = new Dataset({ projectId: 'test', namespace: 'my-ns' }); diff --git a/test/datastore/request.js b/test/datastore/request.js index 833d0699015..9841b60b76b 100644 --- a/test/datastore/request.js +++ b/test/datastore/request.js @@ -22,19 +22,22 @@ var assert = require('assert'); var ByteBuffer = require('bytebuffer'); var duplexify = require('duplexify'); var entity = require('../../lib/datastore/entity.js'); +var extend = require('extend'); +var https = require('https'); var mockRespGet = require('../testdata/response_get.json'); var pb = require('../../lib/datastore/pb.js'); var Query = require('../../lib/datastore/query.js'); var util = require('../../lib/common/util.js'); var httpsRequestOverride = util.noop; -var https = { + +extend(true, https, { request: function() { var requestFn = httpsRequestOverride; httpsRequestOverride = util.noop; return requestFn.apply(this, util.toArray(arguments)); } -}; +}); // Create a protobuf "FakeMethod" request & response. pb.FakeMethodRequest = function() { @@ -54,8 +57,8 @@ pb.FakeMethodResponse = { var Request = require('sandboxed-module') .require('../../lib/datastore/request.js', { requires: { - 'https': https, - './pb.js': pb + './pb.js': pb, + https: https } }); @@ -69,6 +72,9 @@ describe('Request', function() { path: ['Company', 123] }); request = new Request(); + request.makeAuthorizedRequest_ = function(req, callback) { + (callback.onAuthorized || callback)(null, req); + }; }); describe('get', function() { @@ -374,11 +380,11 @@ describe('Request', function() { var method = 'commit'; var projectId = 'project-id'; request.projectId = projectId; - request.connection.createAuthorizedReq = function(opts) { + request.makeAuthorizedRequest_ = function(opts) { assert.equal(opts.method, 'POST'); assert.equal( opts.path, '/datastore/v1beta2/datasets/' + projectId + '/' + method); - assert.equal(opts.headers['content-type'], 'application/x-protobuf'); + assert.equal(opts.headers['Content-Type'], 'application/x-protobuf'); done(); }; request.makeReq_(method, {}, util.noop); @@ -391,8 +397,8 @@ describe('Request', function() { done(); return duplexify(); }; - request.connection.createAuthorizedReq = function(opts, callback) { - callback(null, mockRequest); + request.makeAuthorizedRequest_ = function(opts, callback) { + (callback.onAuthorized || callback)(null, mockRequest); }; request.makeReq_('commit', {}, util.noop); }); @@ -408,9 +414,6 @@ describe('Request', function() { }; return stream; }; - request.connection.createAuthorizedReq = function(opts, callback) { - callback(); - }; request.makeReq_('commit', requestOptions, util.noop); }); @@ -424,16 +427,13 @@ describe('Request', function() { responseStream.emit('end'); return duplexify(); }; - request.connection.createAuthorizedReq = function(opts, callback) { - callback(); - }; request.makeReq_('fakeMethod', util.noop); }); describe('transactional and non-transactional properties', function() { beforeEach(function() { - request.connection.createAuthorizedReq = function(opts, callback) { - callback(); + request.createAuthorizedRequest_ = function(opts, callback) { + (callback.onAuthorized || callback)(); }; }); diff --git a/test/datastore/transaction.js b/test/datastore/transaction.js index 848627dd90e..cde3a33eaa0 100644 --- a/test/datastore/transaction.js +++ b/test/datastore/transaction.js @@ -26,7 +26,11 @@ describe('Transaction', function() { var TRANSACTION_ID = 'transaction-id'; beforeEach(function() { - transaction = new Transaction(null, 'project-id'); + transaction = new Transaction({ + authorizeReq_: function(req, callback) { + return callback(null, req); + } + }, 'project-id'); }); describe('begin', function() { diff --git a/test/pubsub/index.js b/test/pubsub/index.js index da93518d77b..87705f568f5 100644 --- a/test/pubsub/index.js +++ b/test/pubsub/index.js @@ -19,10 +19,29 @@ 'use strict'; var assert = require('assert'); -var PubSub = require('../../lib/pubsub/index.js'); +var request = require('request'); var Subscription = require('../../lib/pubsub/subscription.js'); var Topic = require('../../lib/pubsub/topic.js'); +var PubSub = require('sandboxed-module') + .require('../../lib/pubsub', { + requires: { + './subscription': Subscription, + './topic': Topic, + request: fakeRequest + } + }); + +var request_Cached = request; +var request_Override; + +function fakeRequest() { + var args = [].slice.apply(arguments); + var results = (request_Override || request_Cached).apply(null, args); + request_Override = null; + return results; +} + describe('PubSub', function() { var PROJECT_ID = 'test-project'; var pubsub; @@ -181,7 +200,9 @@ describe('PubSub', function() { it('should pass network requests to the connection object', function(done) { var pubsub = new PubSub(); - pubsub.connection.req = done.bind(null, null); + request_Override = function() { + done(); + }; pubsub.makeReq_(); }); }); diff --git a/test/storage/bucket.js b/test/storage/bucket.js index 72d1a1ec2a0..0adfa9edf1e 100644 --- a/test/storage/bucket.js +++ b/test/storage/bucket.js @@ -20,6 +20,7 @@ var assert = require('assert'); var duplexify = require('duplexify'); +var request = require('request'); var util = require('../../lib/common/util.js'); function FakeFile(bucket, name, metadata) { @@ -39,41 +40,46 @@ function FakeFile(bucket, name, metadata) { var Bucket = require('sandboxed-module') .require('../../lib/storage/bucket.js', { requires: { - './file.js': FakeFile + './file.js': FakeFile, + request: fakeRequest } }); +var request_Cached = request; +var request_Override; + +function fakeRequest() { + var args = [].slice.apply(arguments); + var results = (request_Override || request_Cached).apply(null, args); + request_Override = null; + return results; +} + describe('Bucket', function() { var BUCKET_NAME = 'test-bucket'; var bucket; var options = { - connection_: {}, - name: BUCKET_NAME + makeAuthorizedRequest_: function(req, callback) { + callback(null, req); + } }; beforeEach(function() { - bucket = new Bucket(options); + bucket = new Bucket(options, BUCKET_NAME); }); describe('initialization', function() { it('should re-use provided connection', function() { - assert.deepEqual(bucket.connection_, options.connection_); + assert.deepEqual(bucket.authorizeReq_, options.authorizeReq_); }); it('should default metadata to an empty object', function() { assert.deepEqual(bucket.metadata, {}); }); - it('should use name or bucketName', function() { - var newBucket = new Bucket({ name: BUCKET_NAME }); - assert.equal(newBucket.name, BUCKET_NAME); - var newerBucket = new Bucket({ bucketName: BUCKET_NAME }); - assert.equal(newerBucket.name, BUCKET_NAME); - }); - it('should throw if no name was provided', function() { assert.throws(function() { - new Bucket({}); + new Bucket(); }, /A bucket name is needed/); }); }); @@ -399,7 +405,7 @@ describe('Bucket', function() { var body = { hi: 'there' }; it('should make correct request', function(done) { - bucket.connection_.req = function(request) { + bucket.storage.makeAuthorizedRequest_ = function(request) { var basePath = 'https://www.googleapis.com/storage/v1/b'; assert.equal(request.method, method); assert.equal(request.uri, basePath + '/' + bucket.name + path); @@ -411,7 +417,7 @@ describe('Bucket', function() { }); it('should execute callback', function(done) { - bucket.connection_.req = function(request, callback) { + bucket.storage.makeAuthorizedRequest_ = function(request, callback) { callback(); }; bucket.makeReq_(method, path, query, body, done); diff --git a/test/storage/file.js b/test/storage/file.js index 8a7eb356eea..00aeb81c356 100644 --- a/test/storage/file.js +++ b/test/storage/file.js @@ -20,10 +20,10 @@ var assert = require('assert'); var Bucket = require('../../lib/storage/bucket.js'); -var credentials = require('../testdata/privateKeyFile.json'); var duplexify = require('duplexify'); var extend = require('extend'); var nodeutil = require('util'); +var request = require('request'); var url = require('url'); var util = require('../../lib/common/util'); @@ -52,24 +52,33 @@ var fakeUtil = extend({}, util, { } }); +var request_Cached = request; +var request_Override; + +function fakeRequest() { + var args = [].slice.apply(arguments); + var results = (request_Override || request_Cached).apply(null, args); + request_Override = null; + return results; +} + var File = require('sandboxed-module') .require('../../lib/storage/file.js', { requires: { - 'duplexify': FakeDuplexify, + duplexify: FakeDuplexify, + request: fakeRequest, '../common/util': fakeUtil } }); describe('File', function() { var FILE_NAME = 'file-name.png'; - var bucket = new Bucket({ - bucketName: 'bucket-name', - connection_: { - getCredentials: function(callback) { - callback(null, credentials); - } + var options = { + makeAuthorizedRequest_: function(req, callback) { + (callback.onAuthorized || callback)(null, req); } - }); + }; + var bucket = new Bucket(options, 'bucket-name'); var file; beforeEach(function() { @@ -123,7 +132,7 @@ describe('File', function() { }); it('should allow a Bucket', function(done) { - var newBucket = new Bucket({ name: 'new-bucket' }); + var newBucket = new Bucket({}, 'new-bucket'); var expectedPath = util.format('/o/{srcName}/copyTo/b/{destBucket}/o/{destName}', { srcName: file.name, @@ -135,7 +144,7 @@ describe('File', function() { }); it('should allow a File', function(done) { - var newBucket = new Bucket({ name: 'new-bucket' }); + var newBucket = new Bucket({}, 'new-bucket'); var newFile = new File(newBucket, 'new-file'); var expectedPath = util.format('/o/{srcName}/copyTo/b/{destBucket}/o/{destName}', { @@ -156,7 +165,7 @@ describe('File', function() { }); it('should re-use file object if one is provided', function(done) { - var newBucket = new Bucket({ name: 'new-bucket' }); + var newBucket = new Bucket({}, 'new-bucket'); var newFile = new File(newBucket, 'new-file'); file.copy(newFile, function(err, copiedFile) { assert.ifError(err); @@ -176,7 +185,7 @@ describe('File', function() { }); it('should create new file on the destination bucket', function(done) { - var newBucket = new Bucket({ name: 'new-bucket' }); + var newBucket = new Bucket({}, 'new-bucket'); file.copy(newBucket, function(err, copiedFile) { assert.ifError(err); assert.equal(copiedFile.bucket.name, newBucket.name); @@ -212,7 +221,7 @@ describe('File', function() { }); it('should create an authorized request', function(done) { - file.bucket.connection_.createAuthorizedReq = function(opts) { + request_Override = function(opts) { assert.equal(opts.uri, metadata.mediaLink); done(); }; @@ -224,8 +233,8 @@ describe('File', function() { it('should emit an error from authorizing', function(done) { var error = new Error('Error.'); - file.bucket.connection_.createAuthorizedReq = function(opts, callback) { - callback(error); + file.bucket.storage.makeAuthorizedRequest_ = function(opts, callback) { + (callback.onAuthorized || callback)(error); }; file.getMetadata = function(callback) { setImmediate(function() { @@ -244,12 +253,12 @@ describe('File', function() { file.getMetadata = function(callback) { callback(null, metadata); }; - file.bucket.connection_.requester = function(req) { + request_Override = function(req) { assert.deepEqual(req, fakeRequest); done(); }; - file.bucket.connection_.createAuthorizedReq = function(opts, callback) { - callback(null, fakeRequest); + file.bucket.storage.makeAuthorizedRequest_ = function(opts, callback) { + (callback.onAuthorized || callback)(null, fakeRequest); }; file.createReadStream(); }); @@ -259,11 +268,11 @@ describe('File', function() { file.getMetadata = function(callback) { callback(null, metadata); }; - file.bucket.connection_.requester = function() { + request_Override = function() { return dup; }; - file.bucket.connection_.createAuthorizedReq = function(opts, callback) { - callback(); + file.bucket.storage.makeAuthorizedRequest_ = function(opts, callback) { + (callback.onAuthorized || callback)(); }; file.createReadStream(); assert.deepEqual(readableStream, dup); @@ -299,7 +308,6 @@ describe('File', function() { var metadata = { a: 'b', c: 'd' }; makeWritableStream_Override = function(stream, options) { - assert.deepEqual(options.connection, file.bucket.connection_); assert.deepEqual(options.metadata, metadata); assert.deepEqual(options.request, { qs: { @@ -379,6 +387,15 @@ describe('File', function() { }); describe('getSignedUrl', function() { + var credentials = require('../testdata/privateKeyFile.json'); + + beforeEach(function() { + var storage = bucket.storage; + storage.makeAuthorizedRequest_.getCredentials = function(callback) { + callback(null, credentials); + }; + }); + it('should create a signed url', function(done) { file.getSignedUrl({ action: 'read', diff --git a/test/storage/index.js b/test/storage/index.js index 3bdb16b864f..405886e6133 100644 --- a/test/storage/index.js +++ b/test/storage/index.js @@ -21,12 +21,9 @@ var assert = require('assert'); var Bucket = require('../../lib/storage/bucket.js'); var extend = require('extend'); -var gcloud = require('../../lib'); var Storage = require('../../lib/storage'); var util = require('../../lib/common/util.js'); -var credentials = require('../testdata/privateKeyFile.json'); - describe('Storage', function() { var storage; @@ -35,27 +32,6 @@ describe('Storage', function() { }); describe('initialization', function() { - it('should not require connection details', function() { - var project = gcloud({ - credentials: credentials, - projectId: 'project-id' - }); - var aBucket = project.storage().bucket({ name: 'test' }); - assert.deepEqual(aBucket.connection_.credentials, credentials); - }); - - it('should allow overriding connection details', function() { - var uniqueCredentials = require('../testdata/privateKeyFile-2.json'); - var project = gcloud({ - credentials: credentials, - projectId: 'project-id' - }); - var aBucket = project - .storage({ credentials: uniqueCredentials }) - .bucket({ name: 'another-bucket' }); - assert.deepEqual(aBucket.connection_.credentials, uniqueCredentials); - }); - it('should throw if a bucket name is not passed', function() { assert.throws(function() { storage.bucket();