diff --git a/lib/common/service-object.js b/lib/common/service-object.js new file mode 100644 index 00000000000..2d77111228c --- /dev/null +++ b/lib/common/service-object.js @@ -0,0 +1,307 @@ +/*! + * 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. + */ + +/*! + * @module common/serviceObject + */ + +'use strict'; + +var exec = require('methmeth'); +var is = require('is'); + +/** + * @type {module:common/util} + * @private + */ +var util = require('./util.js'); + +/** + * ServiceObject is a base class, meant to be inherited from by a "service + * object," like a BigQuery dataset or Storage bucket. + * + * Most of the time, these objects share common functionality; they can be + * created or deleted, and you can get or set their metadata. + * + * By inheriting from this class, a service object will be extended with these + * shared behaviors. Note that any method can be overridden when the service + * object requires specific behavior. + * + * @private + * + * @param {object} config - Configuration object. + * @param {string} config.baseUrl - The base URL to make API requests to. + * @param {string} config.createMethod - The method which creates this object. + * @param {string} config.id - The identifier of the object. For example, the + * name of a Storage bucket or Pub/Sub topic. + * @param {object=} config.methods - A map of each method name that should be + * inherited. + * @param {object} config.parent - The parent service instance. For example, an + * instance of Storage if the object is Bucket. + */ +function ServiceObject(config) { + var self = this; + + this.metadata = {}; + + this.baseUrl = config.baseUrl; + this.parent = config.parent; // Parent class. + this.id = config.id; // Name or ID (e.g. dataset ID, bucket name, etc.) + this.createMethod = config.createMethod; + + if (config.methods) { + var allMethodNames = Object.keys(ServiceObject.prototype); + allMethodNames + .filter(function(methodName) { + return ( + // All ServiceObjects need `request`. + methodName !== 'request' && + + // The ServiceObject didn't redefine the method. + self[methodName] === ServiceObject.prototype[methodName] && + + // This method isn't wanted. + !config.methods[methodName] + ); + }) + .forEach(function(methodName) { + self[methodName] = undefined; + }); + } +} + +/** + * Create the object. + * + * @param {object=} options - Configuration object. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {object} callback.instance - The instance. + * @param {object} callback.apiResponse - The full API response. + */ +ServiceObject.prototype.create = function(options, callback) { + var self = this; + var args = [this.id]; + + if (is.fn(options)) { + callback = options; + } + + if (is.object(options)) { + args.push(options); + } + + // Wrap the callback to return *this* instance of the object, not the newly- + // created one. + function onCreate(err, instance, apiResponse) { + if (err) { + callback(err, null, apiResponse); + return; + } + + self.metadata = instance.metadata; + + callback(null, self, apiResponse); + } + + args.push(onCreate); + + this.createMethod.apply(null, args); +}; + +/** + * Delete the object. + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {object} callback.apiResponse - The full API response. + */ +ServiceObject.prototype.delete = function(callback) { + var reqOpts = { + method: 'DELETE', + uri: '' + }; + + callback = callback || util.noop; + + // The `request` method may have been overridden to hold any special behavior. + // Ensure we call the original `request` method. + ServiceObject.prototype.request.call(this, reqOpts, function(err, resp) { + callback(err, resp); + }); +}; + +/** + * Check if the object exists. + * + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {boolean} callback.exists - Whether the object exists or not. + */ +ServiceObject.prototype.exists = function(callback) { + this.get(function(err) { + if (err) { + if (err.code === 404) { + callback(null, false); + } else { + callback(err); + } + + return; + } + + callback(null, true); + }); +}; + +/** + * Get the object if it exists. Optionally have the object created if an options + * object is provided with `autoCreate: true`. + * + * @param {object=} config - The configuration object that will be used to + * create the object if necessary. + * @param {boolean} config.autoCreate - Create the object if it doesn't already + * exist. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {object} callback.instance - The instance. + * @param {object} callback.apiResponse - The full API response. + */ +ServiceObject.prototype.get = function(config, callback) { + var self = this; + + if (is.fn(config)) { + callback = config; + config = {}; + } + + config = config || {}; + + var autoCreate = config.autoCreate && is.fn(this.create); + delete config.autoCreate; + + this.getMetadata(function(err, metadata) { + if (err) { + if (err.code === 404 && autoCreate) { + var args = [callback]; + + if (!is.empty(config)) { + args.unshift(config); + } + + self.create.apply(self, args); + return; + } + + callback(err, null, metadata); + return; + } + + callback(null, self, metadata); + }); +}; + +/** + * Get the metadata of this object. + * + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {object} callback.metadata - The metadata for this object. + * @param {object} callback.apiResponse - The full API response. + */ +ServiceObject.prototype.getMetadata = function(callback) { + var self = this; + + var reqOpts = { + uri: '' + }; + + // The `request` method may have been overridden to hold any special behavior. + // Ensure we call the original `request` method. + ServiceObject.prototype.request.call(this, reqOpts, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + self.metadata = resp; + + callback(null, self.metadata, resp); + }); +}; + +/** + * Set the metadata for this object. + * + * @param {object} metadata - The metadata to set on this object. + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {object} callback.instance - The instance. + * @param {object} callback.apiResponse - The full API response. + */ +ServiceObject.prototype.setMetadata = function(metadata, callback) { + var self = this; + + callback = callback || util.noop; + + var reqOpts = { + method: 'PATCH', + uri: '', + json: metadata + }; + + // The `request` method may have been overridden to hold any special behavior. + // Ensure we call the original `request` method. + ServiceObject.prototype.request.call(this, reqOpts, function(err, resp) { + if (err) { + callback(err, resp); + return; + } + + self.metadata = resp; + + callback(null, resp); + }); +}; + +/** + * Make an authenticated API request. + * + * @private + * + * @param {object} reqOpts - Request options that are passed to `request`. + * @param {string} reqOpts.uri - A URI relative to the baseUrl. + * @param {function} callback - The callback function passed to `request`. + */ +ServiceObject.prototype.request = function(reqOpts, callback) { + var uriComponents = [ + this.baseUrl, + this.id, + reqOpts.uri + ]; + + reqOpts.uri = uriComponents + .filter(exec('trim')) // Limit to non-empty strings. + .map(function(uriComponent) { + var trimSlashesRegex = /^\/*|\/*$/g; + return uriComponent.replace(trimSlashesRegex, ''); + }) + .join('/'); + + this.parent.request(reqOpts, callback); +}; + +module.exports = ServiceObject; diff --git a/lib/common/service.js b/lib/common/service.js new file mode 100644 index 00000000000..68dccf8b9c8 --- /dev/null +++ b/lib/common/service.js @@ -0,0 +1,90 @@ +/*! + * 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. + */ + +/*! + * @module common/service + */ + +'use strict'; + +/** + * @type {module:common/util} + * @private + */ +var util = require('./util.js'); + +/** + * Service is a base class, meant to be inherited from by a "service," like + * BigQuery or Storage. + * + * This handles making authenticated requests by exposing a `makeReq_` function. + * + * @param {object} config - Configuration object. + * @param {string} config.baseUrl - The base URL to make API requests to. + * @param {string[]} config.scopes - The scopes required for the request. + * @param {object} options - [Configuration object](#/docs/?method=gcloud). + */ +function Service(config, options) { + this.makeAuthenticatedRequest = util.makeAuthenticatedRequestFactory({ + scopes: config.scopes, + credentials: options.credentials, + keyFile: options.keyFilename, + email: options.email + }); + + this.authClient = this.makeAuthenticatedRequest.authClient; + this.baseUrl = config.baseUrl; + this.getCredentials = this.makeAuthenticatedRequest.getCredentials; + this.projectId = options.projectId; + this.projectIdRequired = config.projectIdRequired !== false; +} + +/** + * Make an authenticated API request. + * + * @private + * + * @param {object} reqOpts - Request options that are passed to `request`. + * @param {string} reqOpts.uri - A URI relative to the baseUrl. + * @param {function} callback - The callback function passed to `request`. + */ +Service.prototype.request = function(reqOpts, callback) { + var uriComponents = [ + this.baseUrl + ]; + + if (this.projectIdRequired) { + uriComponents.push('projects'); + uriComponents.push(this.projectId); + } + + uriComponents.push(reqOpts.uri); + + reqOpts.uri = uriComponents + .map(function(uriComponent) { + var trimSlashesRegex = /^\/*|\/*$/g; + return uriComponent.replace(trimSlashesRegex, ''); + }) + .join('/') + // Some URIs have colon separators. + // Bad: https://.../projects/:list + // Good: https://.../projects:list + .replace(/\/:/g, ':'); + + this.makeAuthenticatedRequest(reqOpts, callback); +}; + +module.exports = Service; diff --git a/test/common/service-object.js b/test/common/service-object.js new file mode 100644 index 00000000000..36fb95872c9 --- /dev/null +++ b/test/common/service-object.js @@ -0,0 +1,544 @@ +/** + * 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 extend = require('extend'); + +var ServiceObject = require('../../lib/common/service-object.js'); +var util = require('../../lib/common/util.js'); + +describe('ServiceObject', function() { + var serviceObject; + var originalRequest = ServiceObject.prototype.request; + + var CONFIG = { + baseUrl: 'base-url', + parent: {}, + id: 'id', + createMethod: util.noop + }; + + beforeEach(function() { + ServiceObject.prototype.request = originalRequest; + serviceObject = new ServiceObject(CONFIG); + }); + + describe('instantiation', function() { + it('should create an empty metadata object', function() { + assert.deepEqual(serviceObject.metadata, {}); + }); + + it('should localize the baseUrl', function() { + assert.strictEqual(serviceObject.baseUrl, CONFIG.baseUrl); + }); + + it('should localize the parent instance', function() { + assert.strictEqual(serviceObject.parent, CONFIG.parent); + }); + + it('should localize the ID', function() { + assert.strictEqual(serviceObject.id, CONFIG.id); + }); + + it('should localize the createMethod', function() { + assert.strictEqual(serviceObject.createMethod, CONFIG.createMethod); + }); + + it('should clear out methods that are not asked for', function() { + var config = extend({}, CONFIG, { + methods: { + create: true + } + }); + + var serviceObject = new ServiceObject(config); + + assert.strictEqual(typeof serviceObject.create, 'function'); + assert.strictEqual(serviceObject.delete, undefined); + }); + }); + + describe('create', function() { + it('should call createMethod', function(done) { + var config = extend({}, CONFIG, { + createMethod: createMethod + }); + var options = {}; + + function createMethod(id, options_, callback) { + assert.strictEqual(id, config.id); + assert.strictEqual(options_, options); + callback(null, {}, {}); // calls done() + } + + var serviceObject = new ServiceObject(config); + serviceObject.create(options, done); + }); + + it('should not require options', function(done) { + var config = extend({}, CONFIG, { + createMethod: createMethod + }); + + function createMethod(id, options, callback) { + assert.strictEqual(id, config.id); + assert.strictEqual(typeof options, 'function'); + assert.strictEqual(callback, undefined); + options(null, {}, {}); // calls done() + } + + var serviceObject = new ServiceObject(config); + serviceObject.create(done); + }); + + it('should pass error to callback', function(done) { + var config = extend({}, CONFIG, { + createMethod: createMethod + }); + var options = {}; + + var error = new Error('Error.'); + var apiResponse = {}; + + function createMethod(id, options_, callback) { + callback(error, {}, apiResponse); + } + + var serviceObject = new ServiceObject(config); + serviceObject.create(options, function(err, instance, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(instance, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should return instance and apiResponse to callback', function(done) { + var config = extend({}, CONFIG, { + createMethod: createMethod + }); + var options = {}; + + var apiResponse = {}; + + function createMethod(id, options_, callback) { + callback(null, {}, apiResponse); + } + + var serviceObject = new ServiceObject(config); + serviceObject.create(options, function(err, instance_, apiResponse_) { + assert.ifError(err); + assert.strictEqual(instance_, serviceObject); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should assign metadata', function(done) { + var config = extend({}, CONFIG, { + createMethod: createMethod + }); + var options = {}; + + var instance = { + metadata: {} + }; + + function createMethod(id, options_, callback) { + callback(null, instance, {}); + } + + var serviceObject = new ServiceObject(config); + serviceObject.create(options, function(err, instance_) { + assert.ifError(err); + assert.strictEqual(instance_.metadata, instance.metadata); + done(); + }); + }); + }); + + describe('delete', function() { + it('should make the correct request', function(done) { + var serviceObject; + + ServiceObject.prototype.request = function(reqOpts) { + assert.strictEqual(this, serviceObject); + assert.strictEqual(reqOpts.method, 'DELETE'); + assert.strictEqual(reqOpts.uri, ''); + + done(); + }; + + serviceObject = new ServiceObject(CONFIG); + serviceObject.delete(assert.ifError); + }); + + it('should not require a callback', function() { + ServiceObject.prototype.request = function(reqOpts, callback) { + callback(); + }; + + assert.doesNotThrow(function() { + serviceObject.delete(); + }); + }); + + it('should execute callback with correct arguments', function(done) { + var error = new Error('Error.'); + var apiResponse = {}; + + ServiceObject.prototype.request = function(reqOpts, callback) { + callback(error, apiResponse); + }; + + var serviceObject = new ServiceObject(CONFIG); + serviceObject.delete(function(err, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + }); + + describe('exists', function() { + it('should call get', function(done) { + serviceObject.get = function() { + done(); + }; + + serviceObject.exists(); + }); + + it('should execute callback with false if 404', function(done) { + serviceObject.get = function(callback) { + callback({ code: 404 }); + }; + + serviceObject.exists(function(err, exists) { + assert.ifError(err); + assert.strictEqual(exists, false); + done(); + }); + }); + + it('should execute callback with error if not 404', function(done) { + var error = { code: 500 }; + + serviceObject.get = function(callback) { + callback(error); + }; + + serviceObject.exists(function(err, exists) { + assert.strictEqual(err, error); + assert.strictEqual(exists, undefined); + done(); + }); + }); + + it('should execute callback with true if no error', function(done) { + serviceObject.get = function(callback) { + callback(); + }; + + serviceObject.exists(function(err, exists) { + assert.ifError(err); + assert.strictEqual(exists, true); + done(); + }); + }); + }); + + describe('get', function() { + it('should get the metadata', function(done) { + serviceObject.getMetadata = function() { + done(); + }; + + serviceObject.get(assert.ifError); + }); + + it('should execute callback with error & metadata', function(done) { + var error = new Error('Error.'); + var metadata = {}; + + serviceObject.getMetadata = function(callback) { + callback(error, metadata); + }; + + serviceObject.get(function(err, instance, metadata_) { + assert.strictEqual(err, error); + assert.strictEqual(instance, null); + assert.strictEqual(metadata_, metadata); + + done(); + }); + }); + + it('should execute callback with instance & metadata', function(done) { + var metadata = {}; + + serviceObject.getMetadata = function(callback) { + callback(null, metadata); + }; + + serviceObject.get(function(err, instance, metadata_) { + assert.ifError(err); + + assert.strictEqual(instance, serviceObject); + assert.strictEqual(metadata_, metadata); + + done(); + }); + }); + + describe('autoCreate', function() { + var AUTO_CREATE_CONFIG; + + var ERROR = { code: 404 }; + var METADATA = {}; + + beforeEach(function() { + AUTO_CREATE_CONFIG = { + autoCreate: true + }; + + serviceObject.getMetadata = function(callback) { + callback(ERROR, METADATA); + }; + }); + + it('should not auto create if there is no create method', function(done) { + serviceObject.create = undefined; + + serviceObject.get(AUTO_CREATE_CONFIG, function(err) { + assert.strictEqual(err, ERROR); + done(); + }); + }); + + it('should pass config to create if it was provided', function(done) { + var config = extend({}, AUTO_CREATE_CONFIG, { + maxResults: 5 + }); + + serviceObject.create = function(config_) { + assert.strictEqual(config_, config); + done(); + }; + + serviceObject.get(config, assert.ifError); + }); + + it('should pass only a callback to create if no config', function(done) { + serviceObject.create = function(callback) { + callback(); // done() + }; + + serviceObject.get(AUTO_CREATE_CONFIG, done); + }); + }); + }); + + describe('getMetadata', function() { + it('should make the correct request', function(done) { + ServiceObject.prototype.request = function(reqOpts) { + assert.strictEqual(this, serviceObject); + assert.strictEqual(reqOpts.uri, ''); + done(); + }; + + serviceObject.getMetadata(); + }); + + it('should execute callback with error & apiResponse', function(done) { + var error = new Error('Error.'); + var apiResponse = {}; + + ServiceObject.prototype.request = function(reqOpts, callback) { + callback(error, apiResponse); + }; + + serviceObject.getMetadata(function(err, metadata, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(metadata, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should update metadata', function(done) { + var apiResponse = {}; + + ServiceObject.prototype.request = function(reqOpts, callback) { + callback(null, apiResponse); + }; + + serviceObject.getMetadata(function(err) { + assert.ifError(err); + assert.strictEqual(serviceObject.metadata, apiResponse); + done(); + }); + }); + + it('should execute callback with metadata & API response', function(done) { + var apiResponse = {}; + + ServiceObject.prototype.request = function(reqOpts, callback) { + callback(null, apiResponse); + }; + + serviceObject.getMetadata(function(err, metadata, apiResponse_) { + assert.ifError(err); + assert.strictEqual(metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + }); + + describe('setMetadata', function() { + it('should make the correct request', function(done) { + var metadata = {}; + + ServiceObject.prototype.request = function(reqOpts) { + assert.strictEqual(this, serviceObject); + assert.strictEqual(reqOpts.method, 'PATCH'); + assert.strictEqual(reqOpts.uri, ''); + assert.strictEqual(reqOpts.json, metadata); + done(); + }; + + serviceObject.setMetadata(metadata); + }); + + it('should execute callback with error & apiResponse', function(done) { + var error = new Error('Error.'); + var apiResponse = {}; + + ServiceObject.prototype.request = function(reqOpts, callback) { + callback(error, apiResponse); + }; + + serviceObject.setMetadata({}, function(err, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should update metadata', function(done) { + var apiResponse = {}; + + ServiceObject.prototype.request = function(reqOpts, callback) { + callback(null, apiResponse); + }; + + serviceObject.setMetadata({}, function(err) { + assert.ifError(err); + assert.strictEqual(serviceObject.metadata, apiResponse); + done(); + }); + }); + + it('should execute callback with metadata & API response', function(done) { + var apiResponse = {}; + + ServiceObject.prototype.request = function(reqOpts, callback) { + callback(null, apiResponse); + }; + + serviceObject.setMetadata({}, function(err, apiResponse_) { + assert.ifError(err); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + }); + + describe('request', function() { + var reqOpts; + + beforeEach(function() { + reqOpts = { + uri: 'uri' + }; + }); + + it('should send the right request to the parent', function(done) { + serviceObject.parent.request = function(reqOpts_, callback) { + assert.strictEqual(reqOpts_, reqOpts); + callback(); // done() + }; + + serviceObject.request(reqOpts, done); + }); + + it('should compose the correct uri', function(done) { + var expectedUri = [ + serviceObject.baseUrl, + serviceObject.id, + reqOpts.uri + ].join('/'); + + serviceObject.parent.request = function(reqOpts_) { + assert.strictEqual(reqOpts_.uri, expectedUri); + done(); + }; + + serviceObject.request(reqOpts, assert.ifError); + }); + + it('should remove empty components', function(done) { + var reqOpts = { + uri: '' + }; + + var expectedUri = [ + serviceObject.baseUrl, + serviceObject.id + // reqOpts.uri (reqOpts.uri is an empty string, so it should be removed) + ].join('/'); + + serviceObject.parent.request = function(reqOpts_) { + assert.strictEqual(reqOpts_.uri, expectedUri); + done(); + }; + + serviceObject.request(reqOpts, assert.ifError); + }); + + it('should trim slashes', function(done) { + var reqOpts = { + uri: '//1/2//' + }; + + var expectedUri = [ + serviceObject.baseUrl, + serviceObject.id, + '1/2' + ].join('/'); + + serviceObject.parent.request = function(reqOpts_) { + assert.strictEqual(reqOpts_.uri, expectedUri); + done(); + }; + + serviceObject.request(reqOpts, assert.ifError); + }); + }); +}); diff --git a/test/common/service.js b/test/common/service.js new file mode 100644 index 00000000000..de6e8443666 --- /dev/null +++ b/test/common/service.js @@ -0,0 +1,244 @@ +/** + * 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 extend = require('extend'); +var mockery = require('mockery'); + +var util = require('../../lib/common/util.js'); + +var makeAuthenticatedRequestFactoryCache = util.makeAuthenticatedRequestFactory; +var makeAuthenticatedRequestFactoryOverride; +util.makeAuthenticatedRequestFactory = function() { + if (makeAuthenticatedRequestFactoryOverride) { + return makeAuthenticatedRequestFactoryOverride.apply(this, arguments); + } else { + return makeAuthenticatedRequestFactoryCache.apply(this, arguments); + } +}; + +describe('ServiceObject', function() { + var Service; + var service; + + var CONFIG = { + scopes: [], + baseUrl: 'base-url', + projectIdRequired: false + }; + + var OPTIONS = { + credentials: {}, + keyFile: {}, + email: 'email', + projectId: 'project-id', + }; + + before(function() { + mockery.registerMock('./util.js', util); + + mockery.enable({ + useCleanCache: true, + warnOnUnregistered: false + }); + + Service = require('../../lib/common/service.js'); + }); + + after(function() { + mockery.deregisterAll(); + mockery.disable(); + }); + + beforeEach(function() { + makeAuthenticatedRequestFactoryOverride = null; + service = new Service(CONFIG, OPTIONS); + }); + + describe('instantiation', function() { + it('should create an authenticated request factory', function() { + var authenticatedRequest = {}; + + makeAuthenticatedRequestFactoryOverride = function(config) { + assert.strictEqual(config.scopes, CONFIG.scopes); + assert.strictEqual(config.credentials, OPTIONS.credentials); + assert.strictEqual(config.keyFile, OPTIONS.keyFilename); + assert.strictEqual(config.email, OPTIONS.email); + + return authenticatedRequest; + }; + + var svc = new Service(CONFIG, OPTIONS); + assert.strictEqual(svc.makeAuthenticatedRequest, authenticatedRequest); + }); + + it('should localize the authClient', function() { + var authClient = {}; + + makeAuthenticatedRequestFactoryOverride = function() { + return { + authClient: authClient + }; + }; + + var service = new Service(CONFIG, OPTIONS); + assert.strictEqual(service.authClient, authClient); + }); + + it('should localize the baseUrl', function() { + assert.strictEqual(service.baseUrl, CONFIG.baseUrl); + }); + + it('should localize the getCredentials method', function() { + function getCredentials() {} + + makeAuthenticatedRequestFactoryOverride = function() { + return { + authClient: {}, + getCredentials: getCredentials + }; + }; + + var service = new Service(CONFIG, OPTIONS); + assert.strictEqual(service.getCredentials, getCredentials); + }); + + it('should localize the projectId', function() { + assert.strictEqual(service.projectId, OPTIONS.projectId); + }); + + it('should localize the projectIdRequired', function() { + assert.strictEqual(service.projectIdRequired, CONFIG.projectIdRequired); + }); + + it('should default projectIdRequired to true', function() { + var service = new Service({}, OPTIONS); + assert.strictEqual(service.projectIdRequired, true); + }); + }); + + describe('request', function() { + var reqOpts; + + beforeEach(function() { + reqOpts = { + uri: 'uri' + }; + }); + + it('should send the right request to the parent', function(done) { + service.makeAuthenticatedRequest = function(reqOpts_, callback) { + assert.strictEqual(reqOpts_, reqOpts); + callback(); // done() + }; + + service.request(reqOpts, done); + }); + + it('should compose the correct uri', function(done) { + var expectedUri = [ + service.baseUrl, + reqOpts.uri + ].join('/'); + + service.makeAuthenticatedRequest = function(reqOpts_) { + assert.strictEqual(reqOpts_.uri, expectedUri); + done(); + }; + + service.request(reqOpts, assert.ifError); + }); + + it('should trim slashes', function(done) { + var reqOpts = { + uri: '//1/2//' + }; + + var expectedUri = [ + service.baseUrl, + '1/2' + ].join('/'); + + service.makeAuthenticatedRequest = function(reqOpts_) { + assert.strictEqual(reqOpts_.uri, expectedUri); + done(); + }; + + service.request(reqOpts, assert.ifError); + }); + + it('should replace path/:subpath with path:subpath', function(done) { + var reqOpts = { + uri: ':test' + }; + + var expectedUri = service.baseUrl + reqOpts.uri; + + service.makeAuthenticatedRequest = function(reqOpts_) { + assert.strictEqual(reqOpts_.uri, expectedUri); + done(); + }; + + service.request(reqOpts, assert.ifError); + }); + + describe('projectIdRequired', function() { + describe('false', function() { + it('should include the projectId', function(done) { + var config = extend({}, CONFIG, { projectIdRequired: false }); + var service = new Service(config, OPTIONS); + + var expectedUri = [ + service.baseUrl, + reqOpts.uri + ].join('/'); + + service.makeAuthenticatedRequest = function(reqOpts_) { + assert.strictEqual(reqOpts_.uri, expectedUri); + + done(); + }; + + service.request(reqOpts, assert.ifError); + }); + }); + + describe('true', function() { + it('should not include the projectId', function(done) { + var config = extend({}, CONFIG, { projectIdRequired: true }); + var service = new Service(config, OPTIONS); + + var expectedUri = [ + service.baseUrl, + 'projects', + service.projectId, + reqOpts.uri + ].join('/'); + + service.makeAuthenticatedRequest = function(reqOpts_) { + assert.strictEqual(reqOpts_.uri, expectedUri); + + done(); + }; + + service.request(reqOpts, assert.ifError); + }); + }); + }); + }); +});