From 009d76c44123a2b2706110f395ad3275d3fe7986 Mon Sep 17 00:00:00 2001 From: Stephen Sawchuk Date: Mon, 23 Feb 2015 10:28:28 -0500 Subject: [PATCH] support custom API host and port configuration. fixes #389 --- lib/common/util.js | 8 ++++ lib/datastore/dataset.js | 14 ++++++ lib/datastore/request.js | 31 ++++++++------ lib/datastore/transaction.js | 1 + test/common/util.js | 33 ++++++++++---- test/datastore/dataset.js | 14 ++++++ test/datastore/request.js | 81 +++++++++++++++++++++-------------- test/datastore/transaction.js | 20 +++++++++ 8 files changed, 150 insertions(+), 52 deletions(-) diff --git a/lib/common/util.js b/lib/common/util.js index b3ccf208a8c8..344b9e3004e5 100644 --- a/lib/common/util.js +++ b/lib/common/util.js @@ -380,6 +380,14 @@ function makeAuthorizedRequest(config) { var authorize = gsa(config); + if (config.customApi) { + // 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); + }; + } + function makeRequest(reqOpts, callback) { var tokenRefreshAttempts = 0; reqOpts.headers = reqOpts.headers || {}; diff --git a/lib/datastore/dataset.js b/lib/datastore/dataset.js index 5ddc2a20b877..c3642426a2ea 100644 --- a/lib/datastore/dataset.js +++ b/lib/datastore/dataset.js @@ -78,6 +78,10 @@ var SCOPES = [ * `credentials` object. * @param {object=} options.credentials - Credentials object, used in place of * a `keyFilename`. + * @param {string=} options.host - Override the default API endpoint used to + * reach the Datastore. This is useful for connecting to your local + * Datastore server. + * @param {number=} options.port - Useful with `options.host` to set the port. * @param {string} options.namespace - Namespace to isolate transactions to. * * @example @@ -93,12 +97,22 @@ function Dataset(options) { options = options || {}; + if (options.host && options.host.indexOf('http') !== 0) { + throw new Error('A protocol must be included when specifying a host.'); + } + this.makeAuthorizedRequest_ = util.makeAuthorizedRequest({ + customApi: typeof options.host !== 'undefined', credentials: options.credentials, keyFile: options.keyFilename, scopes: SCOPES }); + this.api = { + host: options.host || 'https://www.googleapis.com', + port: util.is(options.port, 'number') ? options.port : 443 + }; + this.namespace = options.namespace; this.projectId = options.projectId; } diff --git a/lib/datastore/request.js b/lib/datastore/request.js index c1af903b6673..9b931e2ccf27 100644 --- a/lib/datastore/request.js +++ b/lib/datastore/request.js @@ -20,8 +20,8 @@ 'use strict'; -var https = require('https'); var streamEvents = require('stream-events'); +var request = require('request'); var through = require('through2'); /** @@ -42,12 +42,6 @@ var pb = require('./pb.js'); */ var util = require('../common/util.js'); -/** - * @const {string} Host to send with API requests. - * @private - */ -var GOOGLE_APIS_HOST = 'www.googleapis.com'; - /** * @const {string} Non-transaction mode key. * @private @@ -595,8 +589,13 @@ DatastoreRequest.prototype.makeReq_ = function(method, body, callback) { var reqOpts = { method: 'POST', - host: GOOGLE_APIS_HOST, - path: '/datastore/v1beta2/datasets/' + this.projectId + '/' + method, + uri: util.format('{host}:{port}/{path}/{projectId}/{method}', { + host: this.api.host, + port: this.api.port, + path: 'datastore/v1beta2/datasets', + projectId: this.projectId, + method: method + }), headers: { 'Content-Type': 'application/x-protobuf' } @@ -609,7 +608,14 @@ DatastoreRequest.prototype.makeReq_ = function(method, body, callback) { return; } - var remoteStream = https.request(authorizedReqOpts, function(resp) { + authorizedReqOpts.headers = authorizedReqOpts.headers || {}; + authorizedReqOpts.headers['Content-Length'] = pbRequest.length; + + var apiRequest = request(authorizedReqOpts); + + apiRequest.on('error', callback); + + apiRequest.on('response', function(resp) { var buffer = new Buffer(''); resp.on('data', function(chunk) { buffer = Buffer.concat([buffer, chunk]); @@ -624,9 +630,8 @@ DatastoreRequest.prototype.makeReq_ = function(method, body, callback) { }); }); }); - remoteStream.on('error', callback); - remoteStream.write(pbRequest); - remoteStream.end(); + + apiRequest.end(pbRequest); } }); }; diff --git a/lib/datastore/transaction.js b/lib/datastore/transaction.js index 121b5511ed7f..6706b92edb1b 100644 --- a/lib/datastore/transaction.js +++ b/lib/datastore/transaction.js @@ -61,6 +61,7 @@ var extend = require('extend'); */ function Transaction(dataset, projectId) { this.id = null; + this.api = dataset.api; this.makeAuthorizedRequest_ = dataset.makeAuthorizedRequest_; this.projectId = projectId; diff --git a/test/common/util.js b/test/common/util.js index 02d49d7c0448..0b6e1f6280bb 100644 --- a/test/common/util.js +++ b/test/common/util.js @@ -299,8 +299,25 @@ describe('common/util', function() { util.makeAuthorizedRequest(config); }); + it('should not authenticate requests with a custom API', function(done) { + var makeRequest = util.makeAuthorizedRequest({ customApi: true }); + + var gsaCalled = false; + gsa_Override = function() { + gsaCalled = true; + }; + + makeRequest({}, { + onAuthorized: function(err) { + assert.ifError(err); + assert.strictEqual(gsaCalled, false); + done(); + } + }); + }); + it('should return gsa.getCredentials function', function() { - var getCredentials = util.makeAuthorizedRequest().getCredentials; + var getCredentials = util.makeAuthorizedRequest({}).getCredentials; assert.equal(typeof getCredentials, 'function'); }); @@ -313,7 +330,7 @@ describe('common/util', function() { }; }; - var makeRequest = util.makeAuthorizedRequest(); + var makeRequest = util.makeAuthorizedRequest({}); makeRequest({}); }); @@ -326,7 +343,7 @@ describe('common/util', function() { }; }; - var makeRequest = util.makeAuthorizedRequest(); + var makeRequest = util.makeAuthorizedRequest({}); makeRequest({ headers: { 'User-Agent': 'test' } }); }); @@ -339,7 +356,7 @@ describe('common/util', function() { }; }; - var makeRequest = util.makeAuthorizedRequest(); + var makeRequest = util.makeAuthorizedRequest({}); makeRequest({}, function(err) { assert.equal(err, error); done(); @@ -392,7 +409,7 @@ describe('common/util', function() { }; }; - var makeRequest = util.makeAuthorizedRequest(); + var makeRequest = util.makeAuthorizedRequest({}); makeRequest({}, function (err) { assert.equal(attempts, expectedAttempts); assert.equal(err, error); @@ -407,7 +424,7 @@ describe('common/util', function() { }; }; - var makeRequest = util.makeAuthorizedRequest(); + var makeRequest = util.makeAuthorizedRequest({}); makeRequest({}, { onAuthorized: done }); }); @@ -420,7 +437,7 @@ describe('common/util', function() { }; }; - var makeRequest = util.makeAuthorizedRequest(); + var makeRequest = util.makeAuthorizedRequest({}); makeRequest({}, { onAuthorized: function(err) { assert.equal(err, error); @@ -443,7 +460,7 @@ describe('common/util', function() { done(); }; - var makeRequest = util.makeAuthorizedRequest(); + var makeRequest = util.makeAuthorizedRequest({}); makeRequest({}, assert.ifError); }); }); diff --git a/test/datastore/dataset.js b/test/datastore/dataset.js index e3102c073a34..97ff4796c1e3 100644 --- a/test/datastore/dataset.js +++ b/test/datastore/dataset.js @@ -23,6 +23,20 @@ var Dataset = require('../../lib/datastore/dataset'); var util = require('../../lib/common/util.js'); describe('Dataset', function() { + describe('instantiation', function() { + it('should set default API connection details', function() { + var ds = new Dataset(); + assert.equal(ds.api.host, 'https://www.googleapis.com'); + assert.equal(ds.api.port, 443); + }); + + it('should set API connection details', function() { + var ds = new Dataset({ host: 'http://localhost', port: 8080 }); + assert.equal(ds.api.host, 'http://localhost'); + assert.equal(ds.api.port, 8080); + }); + }); + describe('key', function() { it('should return key scoped by default namespace', function() { var ds = new Dataset({ projectId: 'test', namespace: 'my-ns' }); diff --git a/test/datastore/request.js b/test/datastore/request.js index deef8e8bf03d..08222ed08036 100644 --- a/test/datastore/request.js +++ b/test/datastore/request.js @@ -21,23 +21,18 @@ var assert = require('assert'); var ByteBuffer = require('bytebuffer'); var entity = require('../../lib/datastore/entity.js'); -var extend = require('extend'); -var https = require('https'); var mockery = require('mockery'); var mockRespGet = require('../testdata/response_get.json'); var pb = require('../../lib/datastore/pb.js'); var Query = require('../../lib/datastore/query.js'); +var requestModule = require('request'); var stream = require('stream'); var util = require('../../lib/common/util.js'); -var httpsRequestCached = https.request; -var httpsRequestOverride = util.noop; - -extend(true, https, { - request: function() { - return httpsRequestOverride.apply(this, util.toArray(arguments)); - } -}); +var request_Override; +function fakeRequest() { + return (request_Override || requestModule).apply(null, arguments); +} // Create a protobuf "FakeMethod" request & response. pb.FakeMethodRequest = function() { @@ -58,10 +53,12 @@ describe('Request', function() { var Request; var key; var request; + var HOST = 'http://localhost'; + var PORT = 8080; before(function() { mockery.registerMock('./pb.js', pb); - mockery.registerMock('https', https); + mockery.registerMock('request', fakeRequest); mockery.enable({ useCleanCache: true, warnOnUnregistered: false @@ -72,16 +69,16 @@ describe('Request', function() { after(function() { mockery.deregisterAll(); mockery.disable(); - httpsRequestOverride = httpsRequestCached; }); beforeEach(function() { - httpsRequestOverride = util.noop; key = new entity.Key({ namespace: 'namespace', path: ['Company', 123] }); + request_Override = null; request = new Request(); + request.api = { host: HOST, port: PORT }; request.makeAuthorizedRequest_ = function(req, callback) { (callback.onAuthorized || callback)(null, req); }; @@ -558,20 +555,28 @@ describe('Request', function() { it('should assemble correct request', function(done) { var method = 'commit'; var projectId = 'project-id'; + var expectedUri = + util.format('{host}:{port}/datastore/v1beta2/datasets/{pId}/{method}', { + host: HOST, + port: PORT, + pId: projectId, + method: method + }); + request.projectId = projectId; request.makeAuthorizedRequest_ = function(opts) { assert.equal(opts.method, 'POST'); - assert.equal( - opts.path, '/datastore/v1beta2/datasets/' + projectId + '/' + method); + assert.equal(opts.uri, expectedUri); assert.equal(opts.headers['Content-Type'], 'application/x-protobuf'); done(); }; request.makeReq_(method, {}, util.noop); }); - it('should make https request', function(done) { + it('should make API request', function(done) { var mockRequest = { mock: 'request' }; - httpsRequestOverride = function(req) { + request_Override = function(req) { + assert.equal(req.headers['Content-Length'], 2); assert.deepEqual(req, mockRequest); done(); return new stream.Writable(); @@ -585,9 +590,9 @@ describe('Request', function() { it('should send protobuf request', function(done) { var requestOptions = { mode: 'NON_TRANSACTIONAL' }; var decoded = new pb.CommitRequest(requestOptions).toBuffer(); - httpsRequestOverride = function() { - var stream = { on: util.noop, end: util.noop }; - stream.write = function(data) { + request_Override = function() { + var stream = { on: util.noop }; + stream.end = function(data) { assert.equal(String(data), String(decoded)); done(); }; @@ -600,15 +605,29 @@ describe('Request', function() { pbFakeMethodResponseDecode = function() { done(); }; - httpsRequestOverride = function(req, callback) { + request_Override = function() { var ws = new stream.Writable(); - callback(ws); - ws.emit('end'); + setImmediate(function () { + ws.emit('response', ws); + ws.emit('end'); + }); return ws; }; request.makeReq_('fakeMethod', util.noop); }); + it('should respect API host and port configuration', function(done) { + request.api = { host: 'abc', port: 123 }; + + request_Override = function(req) { + assert.equal(req.uri.indexOf('abc:123'), 0); + done(); + return new stream.Writable(); + }; + + request.makeReq_('fakeMethod', util.noop); + }); + describe('transactional and non-transactional properties', function() { beforeEach(function() { request.createAuthorizedRequest_ = function(opts, callback) { @@ -624,9 +643,9 @@ describe('Request', function() { mode: 'TRANSACTIONAL', transaction: request.id }).toBuffer(); - httpsRequestOverride = function() { + request_Override = function() { var stream = { on: util.noop, end: util.noop }; - stream.write = function(data) { + stream.end = function(data) { assert.deepEqual(data, expected); done(); }; @@ -639,9 +658,9 @@ describe('Request', function() { var expected = new pb.CommitRequest({ mode: 'NON_TRANSACTIONAL' }).toBuffer(); - httpsRequestOverride = function() { + request_Override = function() { var stream = { on: util.noop, end: util.noop }; - stream.write = function(data) { + stream.end = function(data) { assert.deepEqual(data, expected); done(); }; @@ -660,9 +679,9 @@ describe('Request', function() { transaction: request.id } }).toBuffer(); - httpsRequestOverride = function() { + request_Override = function() { var stream = { on: util.noop, end: util.noop }; - stream.write = function(data) { + stream.end = function(data) { assert.deepEqual(data, expected); done(); }; @@ -673,9 +692,9 @@ describe('Request', function() { it('should not attach transactional properties', function(done) { var expected = new pb.LookupRequest().toBuffer(); - httpsRequestOverride = function() { + request_Override = function() { var ws = new stream.Writable(); - ws.write = function(data) { + ws.end = function(data) { assert.deepEqual(data, expected); done(); }; diff --git a/test/datastore/transaction.js b/test/datastore/transaction.js index 182d42759347..3345ab80fe5f 100644 --- a/test/datastore/transaction.js +++ b/test/datastore/transaction.js @@ -78,6 +78,26 @@ describe('Transaction', function() { }, 'project-id'); }); + describe('instantiation', function() { + it('should assign default properties', function() { + var projectId = 'abc'; + var fakeDataset = { + api: { host: 'host', port: 80 }, + makeAuthorizedRequest_: function fakeMakeAuthorizedRequest_() {} + }; + + var transaction = new Transaction(fakeDataset, projectId); + + assert.strictEqual(transaction.id, null); + assert.deepEqual(transaction.api, fakeDataset.api); + assert.equal( + transaction.makeAuthorizedRequest_, + fakeDataset.makeAuthorizedRequest_ + ); + assert.equal(transaction.projectId, projectId); + }); + }); + describe('begin', function() { it('should begin', function(done) { transaction.makeReq_ = function(method, req, callback) {