diff --git a/lib/common/util.js b/lib/common/util.js index f88e94d06ec..8a55b69b732 100644 --- a/lib/common/util.js +++ b/lib/common/util.js @@ -424,7 +424,17 @@ function makeAuthorizedRequest(config) { '\n' ].join(''); - var authorize = gsa(config); + 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); + }; + } else { + authorize = gsa(config); + } function makeRequest(reqOpts, callback) { var tokenRefreshAttempts = 0; diff --git a/lib/datastore/dataset.js b/lib/datastore/dataset.js index 5ddc2a20b87..e8ed4fdcaeb 100644 --- a/lib/datastore/dataset.js +++ b/lib/datastore/dataset.js @@ -78,6 +78,9 @@ var SCOPES = [ * `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"). * @param {string} options.namespace - Namespace to isolate transactions to. * * @example @@ -85,6 +88,14 @@ var SCOPES = [ * projectId: 'my-project', * keyFilename: '/path/to/keyfile.json' * }); + * + * //- + * // Connect to your local Datastore server. + * //- + * var dataset = datastore.dataset({ + * projectId: 'my-project', + * apiEndpoint: 'http://localhost:8080' + * }); */ function Dataset(options) { if (!(this instanceof Dataset)) { @@ -94,11 +105,19 @@ function Dataset(options) { options = options || {}; this.makeAuthorizedRequest_ = util.makeAuthorizedRequest({ + customEndpoint: typeof options.apiEndpoint !== 'undefined', credentials: options.credentials, keyFile: options.keyFilename, scopes: SCOPES }); + if (options.apiEndpoint && options.apiEndpoint.indexOf('http') !== 0) { + options.apiEndpoint = 'http://' + options.apiEndpoint; + } + + this.apiEndpoint = options.apiEndpoint || 'https://www.googleapis.com'; + this.apiEndpoint = this.apiEndpoint.replace(/\/*$/, ''); + this.namespace = options.namespace; this.projectId = options.projectId; } diff --git a/lib/datastore/request.js b/lib/datastore/request.js index c1af903b667..d4556472e1d 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,12 @@ 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('{apiEndpoint}/{path}/{projectId}/{method}', { + apiEndpoint: this.apiEndpoint, + path: 'datastore/v1beta2/datasets', + projectId: this.projectId, + method: method + }), headers: { 'Content-Type': 'application/x-protobuf' } @@ -609,7 +607,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 +629,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 121b5511ed7..3f882994e95 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.apiEndpoint = dataset.apiEndpoint; this.makeAuthorizedRequest_ = dataset.makeAuthorizedRequest_; this.projectId = projectId; diff --git a/test/common/util.js b/test/common/util.js index 55d00259266..41f2531731d 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({ customEndpoint: 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 e3102c073a3..274a129e068 100644 --- a/test/datastore/dataset.js +++ b/test/datastore/dataset.js @@ -23,6 +23,33 @@ 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.apiEndpoint, 'https://www.googleapis.com'); + }); + + it('should set API connection details', function() { + var ds = new Dataset({ apiEndpoint: 'http://localhost:8080' }); + assert.equal(ds.apiEndpoint, 'http://localhost:8080'); + }); + + it('should remove slashes from the apiEndpoint', function() { + var ds1 = new Dataset({ apiEndpoint: 'http://localhost:8080' }); + var ds2 = new Dataset({ apiEndpoint: 'http://localhost:8080/' }); + var ds3 = new Dataset({ apiEndpoint: 'http://localhost:8080//' }); + + assert.equal(ds1.apiEndpoint, 'http://localhost:8080'); + assert.equal(ds2.apiEndpoint, 'http://localhost:8080'); + assert.equal(ds3.apiEndpoint, 'http://localhost:8080'); + }); + + it('should default to http if protocol is unspecified', function() { + var ds = new Dataset({ apiEndpoint: 'localhost:8080' }); + assert.equal(ds.apiEndpoint, 'http://localhost: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 deef8e8bf03..a80e6a92c64 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,11 @@ describe('Request', function() { var Request; var key; var request; + var CUSTOM_ENDPOINT = 'http://localhost:8080'; before(function() { mockery.registerMock('./pb.js', pb); - mockery.registerMock('https', https); + mockery.registerMock('request', fakeRequest); mockery.enable({ useCleanCache: true, warnOnUnregistered: false @@ -72,16 +68,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.apiEndpoint = CUSTOM_ENDPOINT; request.makeAuthorizedRequest_ = function(req, callback) { (callback.onAuthorized || callback)(null, req); }; @@ -558,20 +554,27 @@ describe('Request', function() { it('should assemble correct request', function(done) { var method = 'commit'; var projectId = 'project-id'; + var expectedUri = + util.format('{apiEndpoint}/datastore/v1beta2/datasets/{pId}/{method}', { + apiEndpoint: CUSTOM_ENDPOINT, + 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 +588,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 +603,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.apiEndpoint = CUSTOM_ENDPOINT; + + request_Override = function(req) { + assert.equal(req.uri.indexOf(CUSTOM_ENDPOINT), 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 +641,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 +656,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 +677,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 +690,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 182d4275934..f4ee6b735e0 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 = { + apiEndpoint: 'http://localhost:8080', + makeAuthorizedRequest_: function fakeMakeAuthorizedRequest_() {} + }; + + var transaction = new Transaction(fakeDataset, projectId); + + assert.strictEqual(transaction.id, null); + assert.deepEqual(transaction.apiEndpoint, fakeDataset.apiEndpoint); + 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) {