diff --git a/README.md b/README.md index 1e056347fa9..7e1e5336b5f 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ This client supports the following Google Cloud Platform services: * [Google Cloud Pub/Sub](#google-cloud-pubsub) * [Google Cloud Storage](#google-cloud-storage) * [Google Compute Engine](#google-compute-engine) +* [Google Cloud Resource Manager](#google-cloud-resource-manager-beta) (Beta) * [Google Cloud Search](#google-cloud-search-alpha) (Alpha) If you need support for other Google APIs, check out the [Google Node.js API Client library][googleapis]. @@ -181,8 +182,8 @@ var gcloud = require('gcloud'); // global basis (see Authorization section above). var dns = gcloud.dns({ - keyFilename: '/path/to/keyfile.json', - projectId: 'my-project' + projectId: 'my-project', + keyFilename: '/path/to/keyfile.json' }); // Create a managed zone. @@ -264,8 +265,8 @@ var gcloud = require('gcloud'); // global basis (see Authorization section above). var gcs = gcloud.storage({ - keyFilename: '/path/to/keyfile.json', - projectId: 'my-project' + projectId: 'my-project', + keyFilename: '/path/to/keyfile.json' }); // Create a new bucket. @@ -335,6 +336,42 @@ zone.createVM(name, { os: 'ubuntu' }, function(err, vm, operation) { ``` +## Google Cloud Resource Manager (Beta) + +> This is a *Beta* release of Google Cloud Resource Manager. This feature is not covered by any SLA or deprecation policy and may be subject to backward-incompatible changes. + +- [API Documentation][gcloud-resource-docs] +- [Official Documentation][cloud-resource-docs] + +#### Preview + +```js +var gcloud = require('gcloud'); + +// Authorizing on a per-API-basis. You don't need to do this if you auth on a +// global basis (see Authorization section above). + +var resource = gcloud.resource({ + projectId: 'my-project', + keyFilename: '/path/to/keyfile.json' +}); + +// Get all of the projects you maintain. +resource.getProjects(function(err, projects) { + if (!err) { + // `projects` contains all of your projects. + } +}); + +// Get the metadata from your project. (defaults to `my-project`) +var project = resource.project(); + +project.getMetadata(function(err, metadata) { + // `metadata` describes your project. +}); +``` + + ## Google Cloud Search (Alpha) > This is an *Alpha* release of Google Cloud Search. This feature is not covered by any SLA or deprecation policy and may be subject to backward-incompatible changes. @@ -351,8 +388,8 @@ var gcloud = require('gcloud'); // global basis (see Authorization section above). var search = gcloud.search({ - keyFilename: '/path/to/keyfile.json', - projectId: 'my-project' + projectId: 'my-project', + keyFilename: '/path/to/keyfile.json' }); // Create a document in a new index. @@ -396,6 +433,7 @@ Apache 2.0 - See [COPYING](COPYING) for more information. [gcloud-datastore-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/datastore [gcloud-dns-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/dns [gcloud-pubsub-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/pubsub +[gcloud-resource-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/resource [gcloud-search-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/search [gcloud-storage-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/storage @@ -421,6 +459,8 @@ Apache 2.0 - See [COPYING](COPYING) for more information. [cloud-pubsub-docs]: https://cloud.google.com/pubsub/docs +[cloud-resource-docs]: https://cloud.google.com/resource-manager + [cloud-search-docs]: https://cloud.google.com/search/ [cloud-storage-docs]: https://cloud.google.com/storage/docs/overview diff --git a/docs/json/master/resource/.gitkeep b/docs/json/master/resource/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/site/components/docs/docs-values.js b/docs/site/components/docs/docs-values.js index 59c2ef1638f..ace3d321cb5 100644 --- a/docs/site/components/docs/docs-values.js +++ b/docs/site/components/docs/docs-values.js @@ -150,6 +150,17 @@ angular.module('gcloud.docs') ] }, + resource: { + title: 'Resource', + _url: '{baseUrl}/resource', + pages: [ + { + title: 'Project', + url: '/project' + } + ] + }, + search: { title: 'Search', _url: '{baseUrl}/search', @@ -226,6 +237,9 @@ angular.module('gcloud.docs') '>=0.18.0': ['dns'], // introduce compute api. - '>=0.20.0': ['compute'] + '>=0.20.0': ['compute'], + + // introduce resource api. + '>=0.22.0': ['resource'] } }); diff --git a/docs/site/components/docs/docs.html b/docs/site/components/docs/docs.html index 1d94fa00dbd..214ea44b7de 100644 --- a/docs/site/components/docs/docs.html +++ b/docs/site/components/docs/docs.html @@ -44,7 +44,7 @@


-
diff --git a/docs/site/components/docs/resource-overview.html b/docs/site/components/docs/resource-overview.html new file mode 100644 index 00000000000..2992b01f435 --- /dev/null +++ b/docs/site/components/docs/resource-overview.html @@ -0,0 +1,10 @@ +

Resource Overview

+

+ This is a Beta release of Cloud Resource Manager. This feature is not covered by any SLA or deprecation policy and may be subject to backward-incompatible changes. +

+

+ The object returned from gcloud.resource gives you complete access to your projects. +

+

+ To learn more about Resource Manager, see What is the Google Cloud Resource Manager? +

diff --git a/lib/common/util.js b/lib/common/util.js index a1df22a6232..585a5c1d363 100644 --- a/lib/common/util.js +++ b/lib/common/util.js @@ -473,11 +473,13 @@ util.decorateRequest = decorateRequest; * @param {?object} localConfig - api level configurations * @return {object} config - merged and validated configurations */ -function normalizeArguments(globalContext, localConfig) { +function normalizeArguments(globalContext, localConfig, options) { var globalConfig = globalContext && globalContext.config_ || {}; var config = util.extendGlobalConfig(globalConfig, localConfig); - if (!config.projectId) { + options = options || {}; + + if (!config.projectId && options.projectIdRequired !== false) { throw util.missingProjectIdError; } diff --git a/lib/index.js b/lib/index.js index 0481d0a9d1c..2b6df596340 100644 --- a/lib/index.js +++ b/lib/index.js @@ -126,6 +126,29 @@ var apis = { */ pubsub: require('./pubsub'), + /** + * [The Cloud Resource Manager](https://cloud.google.com/resource-manager/) + * provides methods that you can use to programmatically manage your projects + * in the Google Cloud Platform. With this API, you can do the following: + * + * - Get a list of all projects associated with an account. + * - Create new projects. + * - Update existing projects. + * - Delete projects. + * - Recover projects. + * + *

+ * **This is a *Beta* release of Cloud Resource Manager.** This feature is + * not covered by any SLA or deprecation policy and may be subject to + * backward-incompatible changes. + *

+ * + * @type {module:resource} + * + * @return {module:resource} + */ + resource: require('./resource'), + /** * [Google Cloud Search](https://cloud.google.com/search/) allows you to * quickly perform full-text and geospatial searches against your data without diff --git a/lib/resource/index.js b/lib/resource/index.js new file mode 100644 index 00000000000..0f6901a019c --- /dev/null +++ b/lib/resource/index.js @@ -0,0 +1,301 @@ +/*! + * 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 resource + */ + +'use strict'; + +var extend = require('extend'); +var is = require('is'); + +/** + * @type {module:resource/project} + * @private + */ +var Project = require('./project.js'); + +/** + * @type {module:common/streamrouter} + * @private + */ +var streamRouter = require('../common/stream-router.js'); + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/** + * @const {string} + * @private + */ +var BASE_URL = 'https://cloudresourcemanager.googleapis.com/v1beta1/projects'; + +/** + * Required scopes for Google Cloud Resource Manager API. + * @const {array} + * @private + */ +var SCOPES = [ + 'https://www.googleapis.com/auth/cloud-platform' +]; + +/** + * [The Cloud Resource Manager](https://cloud.google.com/resource-manager/) + * provides methods that you can use to programmatically manage your projects + * in the Google Cloud Platform. With this API, you can do the following: + * + * - Get a list of all projects associated with an account. + * - Create new projects. + * - Update existing projects. + * - Delete projects. + * - Recover projects. + * + * @alias module:resource + * @constructor + * + * @param {object} options - [Configuration object](#/docs/?method=gcloud). + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json', + * projectId: 'grape-spaceship-123' + * }); + * + * var resource = gcloud.resource(); + */ +function Resource(options) { + if (!(this instanceof Resource)) { + options = util.normalizeArguments(this, options, { + projectIdRequired: false + }); + return new Resource(options); + } + + this.defaultProjectId_ = options.projectId; + + this.makeAuthorizedRequest_ = util.makeAuthorizedRequestFactory({ + credentials: options.credentials, + keyFile: options.keyFilename, + scopes: SCOPES, + email: options.email + }); +} + +/** + * Create a project. + * + * @resource [Projects Overview]{@link https://cloud.google.com/compute/docs/networking#networks} + * @resource [projects: create API Documentation]{@link https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects/create} + * + * @private + * + * @param {string} name - Name of the project. + * @param {object=} options - See a + * [Project resource](https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects#Project). + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:resource/project} callback.project - The created Project + * object. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * resource.createProject('new project name', function(err, project) { + * if (!err) { + * // `project` is a new Project instance. + * } + * }); + */ +Resource.prototype.createProject = function(id, options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + var body = extend({}, options, { + projectId: id + }); + + this.makeReq_('POST', '/', null, body, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + var project = self.project(resp.projectId); + project.metadata = resp; + + callback(null, project, resp); + }); +}; + +/** + * Get a list of projects. + * + * @resource [Projects Overview]{@link https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects} + * @resource [projects: list API Documentation]{@link https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects/list} + * + * @param {object=} options - Operation search options. + * @param {boolean} options.autoPaginate - Have pagination handled + * automatically. Default: true. + * @param {string} options.filter - An expression for filtering the results. + * @param {number} options.pageSize - Maximum number of projects to return. + * @param {string} options.pageToken - A previously-returned page token + * representing part of the larger set of results to view. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:resource/project} callback.operations - Project objects from + * your account. + * @param {?object} callback.nextQuery - If present, query with this object to + * check for more results. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * resource.getProjects(function(err, projects) { + * // `projects` is an array of `Project` objects. + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * function callback(err, projects, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * resource.getProjects(nextQuery, callback); + * } + * } + * + * resource.getProjects({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the projects from your account as a readable object stream. + * //- + * resource.getProjects() + * .on('error', console.error) + * .on('data', function(project) { + * // `project` is a `Project` object. + * }) + * .on('end', function() { + * // All projects retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * resource.getProjects() + * .on('data', function(project) { + * this.end(); + * }); + */ +Resource.prototype.getProjects = function(options, callback) { + var self = this; + + if (is.fn(options)) { + callback = options; + options = {}; + } + + options = options || {}; + + this.makeReq_('GET', '/', options, null, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var projects = (resp.projects || []).map(function(project) { + var projectInstance = self.project(project.name); + projectInstance.metadata = project; + return projectInstance; + }); + + callback(null, projects, nextQuery, resp); + }); +}; + +/** + * Create a Project object to reference an existing project. See + * {module:resoucemanager/createProject} to create a project. + * + * @throws {Error} If an ID is not provided. + * + * @param {string} id - The ID of the project (eg: `grape-spaceship-123`). + * @return {module:resource/project} + * + * @example + * var project = resource.project('grape-spaceship-123'); + */ +Resource.prototype.project = function(id) { + id = id || this.defaultProjectId_; + + if (!id) { + throw new Error('A project ID is required.'); + } + + return new Project(this, id); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Resource.prototype.makeReq_ = function(method, path, query, body, callback) { + var reqOpts = { + method: method, + qs: query, + uri: BASE_URL + path + }; + + if (body) { + reqOpts.json = body; + } + + this.makeAuthorizedRequest_(reqOpts, callback); +}; + +/*! Developer Documentation + * + * These methods can be used with either a callback or as a readable object + * stream. `streamRouter` is used to add this dual behavior. + */ +streamRouter.extend(Resource, ['getProjects']); + +module.exports = Resource; diff --git a/lib/resource/project.js b/lib/resource/project.js new file mode 100644 index 00000000000..d52dfdc6547 --- /dev/null +++ b/lib/resource/project.js @@ -0,0 +1,208 @@ +/*! + * 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 resource/project + */ + +'use strict'; + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/*! Developer Documentation + * + * @param {module:resource} resource - Resource object this project belongs to. + * @param {string} id - The project's ID. + */ +/** + * A Project object allows you to interact with a Google Cloud Platform project. + * + * @resource [Projects Overview]{@link https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects} + * @resource [Project Resource]{@link https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects#Project} + * + * @constructor + * @alias module:resource/project + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json', + * projectId: 'grape-spaceship-123' + * }); + * + * var resource = gcloud.resource(); + * var project = resource.project('grape-spaceship-123'); + * + * //- + * // If no ID is passed to `resource.project()`, the returned object will refer + * // to the project originally specified during instantiation of `gcloud`. + * // + * // Thus, in this case, these are equivalent: + * //- + * var project = resource.project('grape-spaceship-123'); + * var project = resource.project(); + */ +function Project(resource, id) { + this.resource = resource; + this.id = id; + this.metadata = {}; +} + +/** + * Delete the project. + * + * @resource [projects: delete API Documentation]{@link https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects/delete} + * + * @private + * + * @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. + * + * @example + * project.delete(function(err, apiResponse) { + * if (!err) { + * // The project was deleted! + * } + * }); + */ +Project.prototype.delete = function(callback) { + callback = callback || util.noop; + + this.makeReq_('DELETE', '', null, null, function(err, resp) { + callback(err, resp); + }); +}; + +/** + * Get the metadata for the project. + * + * @resource [projects: get API Documentation]{@link https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects/get} + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {?object} callback.metadata - Metadata of the project from the API. + * @param {object} callback.apiResponse - Raw API response. + * + * @example + * project.getMetadata(function(err, metadata, apiResponse) {}); + */ +Project.prototype.getMetadata = function(callback) { + var self = this; + + callback = callback || util.noop; + + this.makeReq_('GET', '', null, null, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + self.metadata = resp; + + callback(null, self.metadata, resp); + }); +}; + +/** + * Restore a project. + * + * @resource [projects: undelete API Documentation]{@link https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects/undelete} + * + * @private + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {object} callback.apiResponse - Raw API response. + * + * @example + * project.restore(function(err, apiResponse) { + * if (!err) { + * // Project restored. + * } + * }); + */ +Project.prototype.restore = function(callback) { + callback = callback || util.noop; + + this.makeReq_('POST', ':undelete', null, null, function(err, resp) { + callback(err, resp); + }); +}; + +/** + * Set the project's metadata. + * + * @resource [projects: update API Documentation]{@link https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects/update} + * @resource [Project Resource]{@link https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects#Project} + * + * @private + * + * @param {object} metadata - See a + * [Project resource](https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects#Project). + * @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. + * + * @example + * var metadata = { + * name: 'New name' + * }; + * + * project.setMetadata(metadata, function(err, apiResponse) { + * if (!err) { + * // The project has been successfully updated. + * } + * }); + */ +Project.prototype.setMetadata = function(metadata, callback) { + var self = this; + + callback = callback || util.noop; + + this.makeReq_('PUT', '', null, metadata, function(err, resp) { + if (err) { + callback(err, resp); + return; + } + + self.metadata = resp; + + callback(null, resp); + }); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Project.prototype.makeReq_ = function(method, path, query, body, callback) { + path = '/' + this.id + path; + this.resource.makeReq_(method, path, query, body, callback); +}; + +module.exports = Project; diff --git a/scripts/docs.sh b/scripts/docs.sh index a4f6c6f2f70..b88506605d3 100755 --- a/scripts/docs.sh +++ b/scripts/docs.sh @@ -48,6 +48,9 @@ ./node_modules/.bin/dox < lib/pubsub/topic.js > docs/json/master/pubsub/topic.json & ./node_modules/.bin/dox < lib/pubsub/iam.js > docs/json/master/pubsub/iam.json & +./node_modules/.bin/dox < lib/resource/index.js > docs/json/master/resource/index.json & +./node_modules/.bin/dox < lib/resource/project.js > docs/json/master/resource/project.json & + ./node_modules/.bin/dox < lib/search/index.js > docs/json/master/search/index.json & ./node_modules/.bin/dox < lib/search/index-class.js > docs/json/master/search/index-class.json & ./node_modules/.bin/dox < lib/search/document.js > docs/json/master/search/document.json & diff --git a/system-test/resource.js b/system-test/resource.js new file mode 100644 index 00000000000..78a4ea1ec55 --- /dev/null +++ b/system-test/resource.js @@ -0,0 +1,129 @@ +/*! + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); + +var env = require('./env.js'); +var Resource = require('../lib/resource/index.js'); + +describe('Resource', function() { + // ------------- + // >> Attention! + // ------------- + // As of 9/14/15, creating projects is not supported. Once we have support, + // the following description outlines how we should run our tests. + // ------------- + // + // Before the tests run, we create a project. That acts as the test for being + // able to create a project. Similarly, the after hook deletes it, testing if + // a project can be deleted. + // + // ---------- + // >> Notice! + // ---------- + // All tests should only manipulate a short-lived project. NOT the project the + // user has been running our test suite with. That would just be rude. + // ---------- + + // var PROJECT_ID = 'gcloud-tests-' + Date.now(); + var PROJECT_NAME = 'gcloud-tests-project-name'; + + var resource = new Resource(env); + var project; + + before(function(done) { + // Uncomment after we support creating a project. + // resource.createProject(PROJECT_ID, function(err, project_) { + // if (err) { + // done(err); + // return; + // } + // + // project = project_; + // }); + + // ** SEE "Notice!" SECTION ABOVE ** + // Remove once we support creating a project. + project = resource.project(); + done(); + }); + + // Uncomment after we support creating a project. + // after(function(done) { + // project.delete(done); + // }); + + describe('resource', function() { + it('should get a list of projects', function(done) { + resource.getProjects(function(err, projects) { + assert.ifError(err); + assert(projects.length > 0); + done(); + }); + }); + + it('should get a list of projects in stream mode', function(done) { + var resultsMatched = 0; + + resource.getProjects() + .on('error', done) + .on('data', function() { + resultsMatched++; + }) + .on('end', function() { + assert(resultsMatched > 0); + done(); + }); + }); + }); + + describe('project', function() { + it('should get metadata', function(done) { + project.getMetadata(function(err, metadata) { + assert.ifError(err); + assert.strictEqual(metadata.projectId, project.id); + done(); + }); + }); + + it.skip('should set metadata', function(done) { + project.getMetadata(function(err, metadata) { + assert.ifError(err); + + var originalProjectName = metadata.name; + + project.setMetadata({ + name: PROJECT_NAME + }, function(err) { + assert.ifError(err); + + project.setMetadata({ + name: originalProjectName + }, done); + }); + }); + }); + + it.skip('should delete and restore a project', function(done) { + project.delete(function(err) { + assert.ifError(err); + project.restore(done); + }); + }); + }); +}); diff --git a/test/resource/index.js b/test/resource/index.js new file mode 100644 index 00000000000..6c4bc39c167 --- /dev/null +++ b/test/resource/index.js @@ -0,0 +1,369 @@ +/*! + * 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 arrify = require('arrify'); +var assert = require('assert'); +var extend = require('extend'); +var mockery = require('mockery'); +var util = require('../../lib/common/util.js'); + +function FakeProject() { + this.calledWith_ = [].slice.call(arguments); +} + +var extended = false; +var fakeStreamRouter = { + extend: function(Class, methods) { + if (Class.name !== 'Resource') { + return; + } + + methods = arrify(methods); + assert.equal(Class.name, 'Resource'); + assert.deepEqual(methods, ['getProjects']); + extended = true; + } +}; + +var makeAuthorizedRequestFactoryOverride; +var fakeUtil = extend({}, util, { + makeAuthorizedRequestFactory: function() { + if (makeAuthorizedRequestFactoryOverride) { + return makeAuthorizedRequestFactoryOverride.apply(null, arguments); + } else { + return util.makeAuthorizedRequestFactory.apply(null, arguments); + } + } +}); + +describe('Resource', function() { + var PROJECT_ID = 'test-project-id'; + + var Resource; + var resource; + + before(function() { + mockery.registerMock('../common/stream-router.js', fakeStreamRouter); + mockery.registerMock('../common/util.js', fakeUtil); + mockery.registerMock('./project.js', FakeProject); + mockery.enable({ + useCleanCache: true, + warnOnUnregistered: false + }); + + Resource = require('../../lib/resource/index.js'); + }); + + after(function() { + mockery.deregisterAll(); + mockery.disable(); + }); + + beforeEach(function() { + makeAuthorizedRequestFactoryOverride = null; + + resource = new Resource({ + projectId: PROJECT_ID + }); + }); + + describe('instantiation', function() { + it('should extend the correct methods', function() { + assert(extended); // See `fakeStreamRouter.extend` + }); + + it('should normalize the arguments', function() { + var normalizeArguments = fakeUtil.normalizeArguments; + var normalizeArgumentsCalled = false; + var fakeOptions = { projectId: PROJECT_ID }; + var fakeContext = {}; + + fakeUtil.normalizeArguments = function(context, options) { + normalizeArgumentsCalled = true; + assert.strictEqual(context, fakeContext); + assert.strictEqual(options, fakeOptions); + return options; + }; + + Resource.call(fakeContext, fakeOptions); + assert(normalizeArgumentsCalled); + + fakeUtil.normalizeArguments = normalizeArguments; + }); + + it('should create an authorized request function', function(done) { + var options = { + projectId: 'projectId', + credentials: 'credentials', + email: 'email', + keyFilename: 'keyFile' + }; + + makeAuthorizedRequestFactoryOverride = function(options_) { + assert.deepEqual(options_, { + credentials: options.credentials, + email: options.email, + keyFile: options.keyFilename, + scopes: [ + 'https://www.googleapis.com/auth/cloud-platform' + ] + }); + return done; + }; + + var resource = new Resource(options); + resource.makeAuthorizedRequest_(); + }); + + it('should localize the projectId', function() { + assert.equal(resource.defaultProjectId_, PROJECT_ID); + }); + }); + + describe('createProject', function() { + var NEW_PROJECT_ID = 'new-project-id'; + var OPTIONS = { a: 'b', c: 'd' }; + var EXPECTED_BODY = extend({}, OPTIONS, { projectId: NEW_PROJECT_ID }); + + it('should not require any options', function(done) { + var expectedBody = { projectId: NEW_PROJECT_ID }; + + resource.makeReq_ = function(method, path, query, body) { + assert.deepEqual(body, expectedBody); + done(); + }; + + resource.createProject(NEW_PROJECT_ID, assert.ifError); + }); + + it('should make the correct API request', function(done) { + resource.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'POST'); + assert.strictEqual(path, '/'); + assert.strictEqual(query, null); + assert.deepEqual(body, EXPECTED_BODY); + + done(); + }; + + resource.createProject(NEW_PROJECT_ID, OPTIONS, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + resource.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + resource.createProject(NEW_PROJECT_ID, OPTIONS, function(err, p, res) { + assert.strictEqual(err, error); + assert.strictEqual(p, null); + assert.strictEqual(res, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { projectId: NEW_PROJECT_ID }; + + beforeEach(function() { + resource.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should exec callback with Project & API response', function(done) { + var project = {}; + + resource.project = function(id) { + assert.strictEqual(id, NEW_PROJECT_ID); + return project; + }; + + resource.createProject(NEW_PROJECT_ID, OPTIONS, function(err, p, res) { + assert.ifError(err); + + assert.strictEqual(p, project); + + assert.strictEqual(res, apiResponse); + done(); + }); + }); + }); + }); + + describe('getProjects', function() { + it('should accept only a callback', function(done) { + resource.makeReq_ = function(method, path, query) { + assert.deepEqual(query, {}); + done(); + }; + + resource.getProjects(assert.ifError); + }); + + it('should make the correct API request', function(done) { + var query = { a: 'b', c: 'd' }; + + resource.makeReq_ = function(method, path, query_, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, '/'); + assert.strictEqual(query_, query); + assert.strictEqual(body, null); + + done(); + }; + + resource.getProjects(query, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + resource.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error & API response', function(done) { + resource.getProjects({}, function(err, projects, nextQuery, apiResp) { + assert.strictEqual(err, error); + assert.strictEqual(projects, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(apiResp, apiResponse); + done(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + projects: [ + { projectId: PROJECT_ID } + ] + }; + + beforeEach(function() { + resource.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should build a nextQuery if necessary', function(done) { + var nextPageToken = 'next-page-token'; + var apiResponseWithNextPageToken = extend({}, apiResponse, { + nextPageToken: nextPageToken + }); + var expectedNextQuery = { + pageToken: nextPageToken + }; + + resource.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponseWithNextPageToken); + }; + + resource.getProjects({}, function(err, projects, nextQuery) { + assert.ifError(err); + + assert.deepEqual(nextQuery, expectedNextQuery); + + done(); + }); + }); + + it('should execute callback with Projects & API resp', function(done) { + var project = {}; + + resource.project = function(name) { + assert.strictEqual(name, apiResponse.projects[0].name); + return project; + }; + + resource.getProjects({}, function(err, projects, nextQuery, apiResp) { + assert.ifError(err); + + assert.strictEqual(projects[0], project); + assert.strictEqual(projects[0].metadata, apiResponse.projects[0]); + + assert.strictEqual(apiResp, apiResponse); + + done(); + }); + }); + }); + }); + + describe('project', function() { + it('should return a Project object', function() { + var project = resource.project(PROJECT_ID); + assert(project instanceof FakeProject); + assert.strictEqual(project.calledWith_[0], resource); + assert.strictEqual(project.calledWith_[1], PROJECT_ID); + }); + + it('should use the project ID from the resource', function() { + var project = resource.project(); + assert(project instanceof FakeProject); + assert.strictEqual(project.calledWith_[1], PROJECT_ID); + }); + + it('should throw if no project ID was given or found', function() { + var resourceWithoutProjectId = new Resource({}); + + assert.throws(function() { + resourceWithoutProjectId.project(); + }, /A project ID is required/); + }); + }); + + describe('makeReq_', function() { + it('should make the correct request', function(done) { + var base = 'https://cloudresourcemanager.googleapis.com/v1beta1/projects'; + + var method = 'POST'; + var path = '/test'; + var query = { + a: 'b', + c: 'd' + }; + var body = { + a: 'b', + c: 'd' + }; + + resource.makeAuthorizedRequest_ = function(reqOpts, callback) { + assert.strictEqual(reqOpts.method, method); + + assert.strictEqual(reqOpts.uri, base + path); + assert.strictEqual(reqOpts.qs, query); + assert.strictEqual(reqOpts.json, body); + callback(); + }; + + resource.makeReq_(method, path, query, body, done); + }); + }); +}); diff --git a/test/resource/project.js b/test/resource/project.js new file mode 100644 index 00000000000..10a65c28548 --- /dev/null +++ b/test/resource/project.js @@ -0,0 +1,318 @@ +/*! + * 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 Project = require('../../lib/resource/project.js'); + +describe('Project', function() { + var RESOURCE = {}; + var ID = 'project-id'; + + var project; + + beforeEach(function() { + project = new Project(RESOURCE, ID); + }); + + describe('instantiation', function() { + it('should localize the resource', function() { + assert.strictEqual(project.resource, RESOURCE); + }); + + it('should localize the ID', function() { + assert.strictEqual(project.id, ID); + }); + + it('should default metadata to an empty object', function() { + assert.deepEqual(project.metadata, {}); + }); + }); + + describe('delete', function() { + it('should make the correct API request', function(done) { + project.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'DELETE'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + done(); + }; + + project.delete(assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + project.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should return an error if the request fails', function(done) { + project.delete(function(err, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + project.delete(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + projectId: ID + }; + + beforeEach(function() { + project.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should execute callback with error and API response', function(done) { + project.delete(function(err, apiResponse_) { + assert.ifError(err); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + project.delete(); + }); + }); + }); + }); + + describe('getMetadata', function() { + it('should make the correct API request', function(done) { + project.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'GET'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + + done(); + }; + + project.getMetadata(assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + project.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should execute callback with error and API response', function(done) { + project.getMetadata(function(err, metadata, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(metadata, null); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + project.getMetadata(); + }); + }); + }); + + describe('success', function() { + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + project.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should update the metadata to the API response', function(done) { + project.getMetadata(function(err) { + assert.ifError(err); + assert.strictEqual(project.metadata, apiResponse); + done(); + }); + }); + + it('should exec callback with metadata and API response', function(done) { + project.getMetadata(function(err, metadata, apiResponse_) { + assert.ifError(err); + assert.strictEqual(metadata, apiResponse); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + project.getMetadata(); + }); + }); + }); + }); + + describe('restore', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + project.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should make the correct API request', function(done) { + project.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'POST'); + assert.strictEqual(path, ':undelete'); + assert.strictEqual(query, null); + assert.strictEqual(body, null); + + done(); + }; + + project.restore(assert.ifError); + }); + + it('should execute the callback with error & API response', function(done) { + project.restore(function(err, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + project.restore(); + }); + }); + }); + + describe('setMetadata', function() { + var METADATA = { a: 'b', c: 'd' }; + + it('should make the correct API request', function(done) { + project.makeReq_ = function(method, path, query, body) { + assert.strictEqual(method, 'PUT'); + assert.strictEqual(path, ''); + assert.strictEqual(query, null); + assert.strictEqual(body, METADATA); + + done(); + }; + + project.setMetadata(METADATA, assert.ifError); + }); + + describe('error', function() { + var error = new Error('Error.'); + var apiResponse = { a: 'b', c: 'd' }; + + beforeEach(function() { + project.makeReq_ = function(method, path, query, body, callback) { + callback(error, apiResponse); + }; + }); + + it('should return an error if the request fails', function(done) { + project.setMetadata(METADATA, function(err, apiResponse_) { + assert.strictEqual(err, error); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + project.setMetadata(METADATA); + }); + }); + }); + + describe('success', function() { + var apiResponse = { + projectId: ID + }; + + beforeEach(function() { + project.makeReq_ = function(method, path, query, body, callback) { + callback(null, apiResponse); + }; + }); + + it('should execute callback with API response', function(done) { + project.setMetadata(METADATA, function(err, apiResponse_) { + assert.ifError(err); + assert.strictEqual(apiResponse_, apiResponse); + done(); + }); + }); + + it('should not require a callback', function() { + assert.doesNotThrow(function() { + project.setMetadata(METADATA); + }); + }); + }); + }); + + describe('makeReq_', function() { + it('should make the correct request to Resource', function(done) { + var expectedPathPrefix = '/' + project.id; + + var method = 'POST'; + var path = '/test'; + var query = { + a: 'b', + c: 'd' + }; + var body = { + a: 'b', + c: 'd' + }; + + project.resource.makeReq_ = function(method_, path_, query_, body_, cb) { + assert.strictEqual(method_, method); + assert.strictEqual(path_, expectedPathPrefix + path); + assert.strictEqual(query_, query); + assert.strictEqual(body_, body); + cb(); + }; + + project.makeReq_(method, path, query, body, done); + }); + }); +});