diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9edc3b6072d..7234ee3538b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -21,6 +21,7 @@ To run the system tests, first create and configure a project in the Google Deve
- **GCLOUD_TESTS_PROJECT_ID**: Developers Console project's ID (e.g. bamboo-shift-455)
- **GCLOUD_TESTS_KEY**: The path to the JSON key file.
+- ***GCLOUD_TESTS_DNS_DOMAIN*** (*optional*): A domain you own managed by Google Cloud DNS (expected format: `'gcloud-node.com.'`).
Install the [gcloud command-line tool][gcloudcli] to your machine and use it to create the indexes used in the datastore system tests with indexes found in `system-test/data/index/yaml`:
diff --git a/README.md b/README.md
index 6b593284305..18cec69f1ab 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ This client supports the following Google Cloud Platform services:
* [Google BigQuery](#google-bigquery)
* [Google Cloud Datastore](#google-cloud-datastore)
+* [Google Cloud DNS](#google-cloud-dns)
* [Google Cloud Storage](#google-cloud-storage)
* [Google Cloud Pub/Sub](#google-cloud-pubsub-beta) (Beta)
* [Google Cloud Search](#google-cloud-search-alpha) (Alpha)
@@ -165,6 +166,46 @@ dataset.save({
```
+## Google Cloud DNS
+
+- [API Documentation][gcloud-dns-docs]
+- [Official Documentation][cloud-dns-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 dns = gcloud.dns({
+ keyFilename: '/path/to/keyfile.json',
+ projectId: 'my-project'
+});
+
+// Create a managed zone.
+dns.createZone('my-new-zone', {
+ dnsName: 'my-domain.com.'
+}, function(err, zone) {});
+
+// Reference an existing zone.
+var zone = dns.zone('my-existing-zone');
+
+// Create an NS record.
+var nsRecord = zone.record('ns', {
+ ttl: 86400,
+ name: 'my-domain.com.',
+ data: 'ns-cloud1.googledomains.com.'
+});
+
+zone.addRecord(nsRecord, function(err, change) {});
+
+// Create a zonefile from the records in your zone.
+zone.export('/zonefile.zone', function(err) {});
+```
+
+
## Google Cloud Storage
- [API Documentation][gcloud-storage-docs]
@@ -319,6 +360,7 @@ Apache 2.0 - See [COPYING](COPYING) for more information.
[gcloud-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs
[gcloud-bigquery-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/bigquery
[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-search-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/search
[gcloud-storage-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/storage
@@ -339,6 +381,8 @@ Apache 2.0 - See [COPYING](COPYING) for more information.
[cloud-datastore-docs]: https://cloud.google.com/datastore/docs
[cloud-datastore-activation]: https://cloud.google.com/datastore/docs/activate
+[cloud-dns-docs]: https://cloud.google.com/dns/docs
+
[cloud-pubsub-docs]: https://cloud.google.com/pubsub/docs
[cloud-search-docs]: https://cloud.google.com/search/
diff --git a/test/search.index.js b/docs/json/master/dns/.gitkeep
similarity index 100%
rename from test/search.index.js
rename to docs/json/master/dns/.gitkeep
diff --git a/docs/site/components/docs/docs-values.js b/docs/site/components/docs/docs-values.js
index 2b30f1da72e..3bd5b3d4e39 100644
--- a/docs/site/components/docs/docs-values.js
+++ b/docs/site/components/docs/docs-values.js
@@ -73,6 +73,25 @@ angular.module('gcloud.docs')
]
},
+ dns: {
+ title: 'DNS',
+ _url: '{baseUrl}/dns',
+ pages: [
+ {
+ title: 'Zone',
+ url: '/zone'
+ },
+ {
+ title: 'Record',
+ url: '/record'
+ },
+ {
+ title: 'Change',
+ url: '/change'
+ }
+ ]
+ },
+
pubsub: {
title: 'PubSub',
_url: '{baseUrl}/pubsub',
@@ -158,6 +177,9 @@ angular.module('gcloud.docs')
'>=0.10.0': ['bigquery'],
// introduce search api.
- '>=0.16.0': ['search']
+ '>=0.16.0': ['search'],
+
+ // introduce dns api.
+ '>=0.18.0': ['dns']
}
});
diff --git a/lib/common/util.js b/lib/common/util.js
index e5ed5cf3e53..f675a0af91b 100644
--- a/lib/common/util.js
+++ b/lib/common/util.js
@@ -249,6 +249,55 @@ function getType(value) {
return Object.prototype.toString.call(value).match(/\s(\w+)\]/)[1];
}
+/**
+ * Iterate through an array, invoking a function by the provided name.
+ *
+ * @param {string} name - The name of the function that exists as a property on
+ * each member of the iterated array.
+ * @return {function}
+ *
+ * @example
+ * var people = [
+ * {
+ * getName: function() { return 'Stephen'; }
+ * },
+ * {
+ * getName: function() { return 'Dave'; }
+ * }
+ * };
+ *
+ * var names = people.map(exec('getName'));
+ * // names = [ 'Stephen', 'Dave' ]
+ *
+ * //-
+ * // Aguments can also be provided.
+ * //-
+ * var people = [
+ * {
+ * getName: function(prefix) { return prefix + ' Stephen'; }
+ * },
+ * {
+ * getName: function(prefix) { return prefix + ' Dave'; }
+ * }
+ * ];
+ *
+ * var names = people.map(exec('getName', 'Mr.'));
+ * // names = [ 'Mr. Stephen', 'Mr. Dave' ];
+ */
+function exec(name) {
+ var initialArguments = [].slice.call(arguments, 1);
+
+ return function(item) {
+ if (util.is(item[name], 'function')) {
+ var invokedArguments = [].slice.call(arguments, 1);
+ return item[name].apply(item, initialArguments.concat(invokedArguments));
+ }
+ return item[name];
+ };
+}
+
+util.exec = exec;
+
/**
* Used in an Array iterator usually, this will return the value of a property
* in an object by its name.
diff --git a/lib/dns/change.js b/lib/dns/change.js
new file mode 100644
index 00000000000..e7a56f6133e
--- /dev/null
+++ b/lib/dns/change.js
@@ -0,0 +1,87 @@
+/*!
+ * Copyright 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*!
+ * @module dns/change
+ */
+
+'use strict';
+
+/**
+ * @constructor
+ * @alias module:dns/change
+ *
+ * @param {module:dns/zone} zone - The parent zone object.
+ * @param {string} id - ID of the change.
+ *
+ * @example
+ * var gcloud = require('gcloud');
+ *
+ * var dns = gcloud.dns({
+ * keyFilename: '/path/to/keyfile.json',
+ * projectId: 'grape-spaceship-123'
+ * });
+ *
+ * var zone = dns.zone('zone-id');
+ * var change = zone.change('change-id');
+ */
+function Change(zone, id) {
+ this.zoneName = zone.name;
+ this.id = id;
+
+ this.metadata = {};
+ this.makeReq_ = zone.dns.makeReq_.bind(zone.dns);
+}
+
+/**
+ * Get the metadata for the change in the zone.
+ *
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An API error.
+ * @param {?object} callback.metadata - Metadata of the change from the API.
+ * @param {object} callback.apiResponse - Raw API response.
+ *
+ * @example
+ * change.getMetadata(function(err, metadata, apiResponse) {
+ * if (!err) {
+ * // metadata = {
+ * // kind: 'dns#change',
+ * // additions: [{...}],
+ * // deletions: [{...}],
+ * // startTime: '2015-07-21T14:40:06.056Z',
+ * // id: '1',
+ * // status: 'done'
+ * // }
+ * }
+ * });
+ */
+Change.prototype.getMetadata = function(callback) {
+ var self = this;
+ var path = '/managedZones/' + this.zoneName + '/changes/' + this.id;
+
+ this.makeReq_('GET', path, null, null, function(err, resp) {
+ if (err) {
+ callback(err, null, resp);
+ return;
+ }
+
+ self.metadata = resp;
+
+ callback(null, self.metadata, resp);
+ });
+};
+
+module.exports = Change;
diff --git a/lib/dns/index.js b/lib/dns/index.js
new file mode 100644
index 00000000000..92a2f63c5ce
--- /dev/null
+++ b/lib/dns/index.js
@@ -0,0 +1,250 @@
+/*!
+ * Copyright 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*!
+ * @module dns
+ */
+
+'use strict';
+
+var extend = require('extend');
+
+/**
+ * @type {module:common/streamrouter}
+ * @private
+ */
+var streamRouter = require('../common/stream-router.js');
+
+/**
+ * @type {module:common/util}
+ * @private
+ */
+var util = require('../common/util.js');
+
+/**
+ * @type {module:dns/zone}
+ * @private
+ */
+var Zone = require('./zone.js');
+
+/**
+ * @const {string} Base URL for DNS API.
+ * @private
+ */
+var DNS_BASE_URL = 'https://www.googleapis.com/dns/v1/projects/';
+
+/**
+ * @const {array} Required scopes for the DNS API.
+ * @private
+ */
+var SCOPES = [
+ 'https://www.googleapis.com/auth/ndev.clouddns.readwrite',
+ 'https://www.googleapis.com/auth/cloud-platform'
+];
+
+/**
+ * [Google Cloud DNS](https://cloud.google.com/dns) is a reliable, resilient,
+ * low-latency DNS serving from Google’s worldwide network of Anycast DNS
+ * servers.
+ *
+ * @constructor
+ * @alias module:dns
+ *
+ * @param {object} options - [Configuration object](#/docs/?method=gcloud).
+ *
+ * @example
+ * var gcloud = require('gcloud')({
+ * keyFilename: '/path/to/keyfile.json',
+ * projectId: 'grape-spaceship-123'
+ * });
+ *
+ * var dns = gcloud.dns();
+ */
+function DNS(options) {
+ if (!(this instanceof DNS)) {
+ return new DNS(options);
+ }
+
+ options = options || {};
+
+ if (!options.projectId) {
+ throw util.missingProjectIdError;
+ }
+
+ this.makeAuthorizedRequest_ = util.makeAuthorizedRequestFactory({
+ credentials: options.credentials,
+ keyFile: options.keyFilename,
+ scopes: SCOPES,
+ email: options.email
+ });
+
+ this.projectId_ = options.projectId;
+}
+
+/**
+ * Create a managed zone.
+ *
+ * @throws {error} If a zone name is not provided.
+ * @throws {error} If a zone dnsName is not provided.
+ *
+ * @param {string} name - Unique name for the zone. E.g. "my-zone"
+ * @param {object} config - Configuration object.
+ * @param {string} config.dnsName - DNS name for the zone. E.g. "example.com."
+ * @param {string=} config.description - Description text for the zone.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An API error.
+ * @param {?module:dns/zone} callback.zone - A new {module:dns/zone} object.
+ * @param {object} callback.apiResponse - Raw API response.
+ *
+ * @example
+ * dns.createZone('my-awesome-zone', {
+ * dnsName: 'example.com.', // note the period at the end of the domain.
+ * description: 'This zone is awesome!'
+ * }, function(err, zone, apiResponse) {
+ * if (!err) {
+ * // The zone was created successfully.
+ * }
+ * });
+ */
+DNS.prototype.createZone = function(name, config, callback) {
+ var self = this;
+
+ if (!name) {
+ throw new Error('A zone name is required.');
+ }
+
+ if (!config || !config.dnsName) {
+ throw new Error('A zone dnsName is required.');
+ }
+
+ config.name = name;
+
+ // Required by the API.
+ config.description = config.description || '';
+
+ this.makeReq_('POST', '/managedZones', null, config, function(err, resp) {
+ if (err) {
+ callback(err, null, resp);
+ return;
+ }
+
+ var zone = self.zone(resp.name);
+ zone.metadata = resp;
+
+ callback(null, zone, resp);
+ });
+};
+
+/**
+ * Gets a list of managed zones for the project.
+ *
+ * @param {object=} query - Query object.
+ * @param {number} query.maxResults - Maximum number of results to return.
+ * @param {string} query.pageToken - Page token.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An API error.
+ * @param {?module:dns/zone[]} callback.zones - An array of {module:dns/zone}
+ * objects.
+ * @param {object} callback.apiResponse - Raw API response.
+ *
+ * @example
+ * dns.getZones(function(err, zones, apiResponse) {});
+ */
+DNS.prototype.getZones = function(query, callback) {
+ var self = this;
+
+ if (util.is(query, 'function')) {
+ callback = query;
+ query = {};
+ }
+
+ this.makeReq_('GET', '/managedZones', query, null, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var zones = (resp.managedZones || []).map(function(zone) {
+ var zoneInstance = self.zone(zone.name);
+ zoneInstance.metadata = zone;
+ return zoneInstance;
+ });
+
+ var nextQuery = null;
+
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, query, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ callback(null, zones, nextQuery, resp);
+ });
+};
+
+/**
+ * Create a zone object representing an existing managed zone.
+ *
+ * @throws {error} If a zone name is not provided.
+ *
+ * @param {string} name - The unique name of the zone.
+ * @return {module:dns/zone}
+ *
+ * @example
+ * var zone = dns.zone('my-zone');
+ */
+DNS.prototype.zone = function(name) {
+ if (!name) {
+ throw new Error('A zone name is required.');
+ }
+
+ return new Zone(this, name);
+};
+
+/**
+ * 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.
+ */
+DNS.prototype.makeReq_ = function(method, path, query, body, callback) {
+ var reqOpts = {
+ method: method,
+ qs: query,
+ uri: DNS_BASE_URL + this.projectId_ + 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(DNS, 'getZones');
+
+module.exports = DNS;
diff --git a/lib/dns/record.js b/lib/dns/record.js
new file mode 100644
index 00000000000..379c6d31d15
--- /dev/null
+++ b/lib/dns/record.js
@@ -0,0 +1,167 @@
+/*!
+ * Copyright 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*!
+ * @module dns/record
+ */
+
+'use strict';
+
+var extend = require('extend');
+
+/**
+ * @type {module:common/util}
+ * @private
+ */
+var util = require('../common/util.js');
+
+/**
+ * Create a Resource Record object.
+ *
+ * @constructor
+ * @alias module:dns/record
+ *
+ * @param {object} type - The record type, e.g. `A`, `AAAA`, `MX`.
+ * @param {object} metadata - The metadata of this record.
+ * @param {string} metadata.name - The name of the record, e.g.
+ * `www.example.com.`.
+ * @param {string[]} metadata.data - Defined in
+ * [RFC 1035, section 5](https://goo.gl/9EiM0e) and
+ * [RFC 1034, section 3.6.1](https://goo.gl/Hwhsu9).
+ * @param {number} metadata.ttl - Seconds that the resource is cached by
+ * resolvers.
+ *
+ * @example
+ * var gcloud = require('gcloud');
+ *
+ * var dns = gcloud.dns({
+ * projectId: 'grape-spaceship-123'
+ * });
+ *
+ * var zone = dns.zone('my-awesome-zone');
+ *
+ * var record = zone.record('a', {
+ * name: 'example.com.',
+ * ttl: 86400,
+ * data: '1.2.3.4'
+ * });
+ */
+function Record(zone, type, metadata) {
+ this.zone_ = zone;
+
+ this.type = type;
+ this.metadata = metadata;
+
+ extend(this, this.toJSON());
+
+ if (this.rrdatas) {
+ this.data = this.rrdatas;
+ delete this.rrdatas;
+ }
+}
+
+/**
+ * Create a Record instance from a resource record set in a zone file.
+ *
+ * @private
+ *
+ * @param {module:dns/zone} zone [description]
+ * @param {string} type - The record type, e.g. `A`, `AAAA`, `MX`.
+ * @param {object} bindData - Metadata parsed from dns-zonefile. Properties vary
+ * based on the type of record.
+ * @return {module:dns/record}
+ */
+Record.fromZoneRecord_ = function(zone, type, bindData) {
+ var typeToZoneFormat = {
+ a: '{ip}',
+ aaaa: '{ip}',
+ cname: '{alias}',
+ mx: '{preference} {host}',
+ ns: '{host}',
+ soa: '{mname} {rname} {serial} {retry} {refresh} {expire} {minimum}',
+ spf: '{data}',
+ srv: '{priority} {weight} {port} {target}',
+ txt: '{txt}'
+ };
+
+ var metadata = {
+ data: util.format(typeToZoneFormat[type.toLowerCase()], bindData),
+ name: bindData.name,
+ ttl: bindData.ttl
+ };
+
+ return new Record(zone, type, metadata);
+};
+
+/**
+ * Delete this record by creating a change on your zone. This is a convenience
+ * method for:
+ *
+ * zone.createChange({
+ * delete: record
+ * }, function(err, change, apiResponse) {});
+ *
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An API error.
+ * @param {?module:dns/change} callback.change - A {module:dns/change} object.
+ * @param {object} callback.apiResponse - Raw API response.
+ *
+ * @example
+ * record.delete(function(err, change, apiResponse) {
+ * if (!err) {
+ * // Delete change modification was created.
+ * }
+ * });
+ */
+Record.prototype.delete = function(callback) {
+ this.zone_.deleteRecords(this, callback);
+};
+
+/**
+ * Serialize the record instance to the format the API expects.
+ *
+ * @private
+ */
+Record.prototype.toJSON = function() {
+ var recordObject = extend({}, this.metadata, {
+ type: this.type.toUpperCase()
+ });
+
+ if (recordObject.data) {
+ recordObject.rrdatas = util.arrayize(recordObject.data);
+ delete recordObject.data;
+ }
+
+ return recordObject;
+};
+
+/**
+ * Convert the record to a string, formatted for a zone file.
+ *
+ * @private
+ *
+ * @return {string}
+ */
+Record.prototype.toString = function() {
+ var json = this.toJSON();
+
+ return (json.rrdatas || [{}]).map(function(data) {
+ json.rrdata = data;
+ return util.format('{name} {ttl} IN {type} {rrdata}', json);
+ }).join('\n');
+};
+
+module.exports = Record;
diff --git a/lib/dns/zone.js b/lib/dns/zone.js
new file mode 100644
index 00000000000..6a853695f2c
--- /dev/null
+++ b/lib/dns/zone.js
@@ -0,0 +1,816 @@
+/*!
+ * Copyright 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*!
+ * @module dns/zone
+ */
+
+'use strict';
+
+var extend = require('extend');
+var fs = require('fs');
+var zonefile = require('dns-zonefile');
+
+/**
+ * @type {module:dns/change}
+ * @private
+ */
+var Change = require('./change.js');
+
+/**
+ * @type {module:dns/record}
+ * @private
+ */
+var Record = require('./record.js');
+
+/**
+ * @type {module:common/streamrouter}
+ * @private
+ */
+var streamRouter = require('../common/stream-router.js');
+
+/**
+ * @type {module:common/util}
+ * @private
+ */
+var util = require('../common/util.js');
+
+/**
+ * A Zone object is used to interact with your project's managed zone. It will
+ * help you add or delete records, delete your zone, and many other convenience
+ * methods.
+ *
+ * @constructor
+ * @alias module:dns/zone
+ *
+ * @example
+ * var gcloud = require('gcloud');
+ *
+ * var dns = gcloud.dns({
+ * keyFilename: '/path/to/keyfile.json',
+ * projectId: 'grape-spaceship-123'
+ * });
+ *
+ * var zone = dns.zone('zone-id');
+ */
+function Zone(dns, name) {
+ this.dns = dns;
+ this.name = name;
+ this.metadata = {};
+
+ this.makeReq_ = this.dns.makeReq_.bind(dns);
+}
+
+/**
+ * Add records to this zone. This is a convenience wrapper around
+ * {module:dns/zone#createChange}.
+ *
+ * @param {module:dns/record|module:dns/record[]} record - The record objects to
+ * add.
+ * @param {?error} callback.err - An API error.
+ * @param {?module:dns/change} callback.change - A {module:dns/change} object.
+ * @param {object} callback.apiResponse - Raw API response.
+ */
+Zone.prototype.addRecords = function(records, callback) {
+ this.createChange({
+ add: records
+ }, callback);
+};
+
+/**
+ * Create a reference to an existing change object in this zone.
+ *
+ * @throws {error} If an id is not provided.
+ *
+ * @param {string} id - The change id.
+ * @return {module:dns/change}
+ *
+ * @example
+ * var change = zone.change('change-id');
+ */
+Zone.prototype.change = function(id) {
+ if (!id) {
+ throw new Error('A change id is required.');
+ }
+
+ return new Change(this, id);
+};
+
+/**
+ * Create a change of resource record sets for the zone.
+ *
+ * @param {object} options - The configuration object.
+ * @param {module:dns/record|module:dns/record[]} options.add - Record objects
+ * to add to this zone.
+ * @param {module:dns/record|module:dns/record[]} options.delete - Record
+ * objects to delete from this zone. Be aware that the resource records here
+ * must match exactly to be deleted.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An API error.
+ * @param {?module:dns/change} callback.change - A {module:dns/change} object.
+ * @param {object} callback.apiResponse - Raw API response.
+ *
+ * @example
+ * var oldARecord = zone.record('a', {
+ * name: 'example.com.',
+ * data: '1.2.3.4',
+ * ttl: 86400
+ * });
+ *
+ * var newARecord = zone.record('a', {
+ * name: 'example.com.',
+ * data: '5.6.7.8',
+ * ttl: 86400
+ * });
+ *
+ * zone.createChange({
+ * add: newARecord,
+ * delete: oldARecord
+ * }, function(err, change, apiResponse) {
+ * if (!err) {
+ * // The change was created successfully.
+ * }
+ * });
+ */
+Zone.prototype.createChange = function(options, callback) {
+ var self = this;
+
+ if (!options || !options.add && !options.delete) {
+ throw new Error('Cannot create a change with no additions or deletions.');
+ }
+
+ var body = extend({}, options, {
+ additions: util.arrayize(options.add).map(util.exec('toJSON')),
+ deletions: util.arrayize(options.delete).map(util.exec('toJSON'))
+ });
+
+ delete body.add;
+ delete body.delete;
+
+ var path = '/managedZones/' + this.name + '/changes';
+
+ this.makeReq_('POST', path, null, body, function(err, resp) {
+ if (err) {
+ callback(err, null, resp);
+ return;
+ }
+
+ var change = self.change(resp.id);
+ change.metadata = resp;
+
+ callback(null, change, resp);
+ });
+};
+
+/**
+ * Delete the zone.
+ *
+ * Only empty zones can be deleted. Set options.force to `true` to call
+ * {module:dns/zone#empty} before deleting the zone. Two API calls will then be
+ * made (one to empty, another to delete), which means this is not an
+ * atomic request.
+ *
+ * @param {object=} options - Configuration object.
+ * @param {boolean} options.force - Empty the zone before deleting.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An API error.
+ * @param {object} callback.apiResponse - Raw API response.
+ *
+ * @example
+ * zone.delete(function(err, apiResponse) {
+ * if (!err) {
+ * // The zone is now deleted.
+ * }
+ * });
+ *
+ * //-
+ * // Use `force` to first empty the zone before deleting it.
+ * //-
+ * zone.delete({
+ * force: true
+ * }, function(err, apiResponse) {
+ * if (!err) {
+ * // The zone is now deleted.
+ * }
+ * });
+ */
+Zone.prototype.delete = function(options, callback) {
+ if (util.is(options, 'function')) {
+ callback = options;
+ options = {};
+ }
+
+ if (options.force) {
+ this.empty(this.delete.bind(this, callback));
+ return;
+ }
+
+ var path = '/managedZones/' + this.name;
+ this.makeReq_('DELETE', path, null, null, function(err, resp) {
+ callback(err, resp);
+ });
+};
+
+/**
+ * Delete records from this zone. This is a convenience wrapper around
+ * {module:dns/zone#createChange}.
+ *
+ * This method accepts {module:dns/record} objects or string record types in
+ * its place. This means that you can delete all A records or NS records, etc.
+ * If used this way, two API requests are made (one to get, then another to
+ * delete), which means the operation is not atomic and could
+ * result in unexpected changes.
+ *
+ * @param {module:dns/record|module:dns/record[]|string} record - If given a
+ * string, it is interpreted as a record type. All records that match that
+ * type will be retrieved and then deleted. You can also provide a
+ * {module:dns/record} object or array of objects.
+ * @param {?error} callback.err - An API error.
+ * @param {?module:dns/change} callback.change - A {module:dns/change} object.
+ * @param {object} callback.apiResponse - Raw API response.
+ *
+ * @example
+ * var oldARecord = zone.record('a', {
+ * name: 'example.com.',
+ * data: '1.2.3.4',
+ * ttl: 86400
+ * });
+ *
+ * var callback = function(err, change, apiResponse) {
+ * if (!err) {
+ * // Delete change modification was created.
+ * }
+ * };
+ *
+ * zone.deleteRecords(oldARecord, callback);
+ *
+ * //-
+ * // Delete multiple records at once.
+ * //-
+ * var oldNs1Record = zone.record('ns', {
+ * name: 'example.com.',
+ * data: 'ns-cloud1.googledomains.com.',
+ * ttl: 86400
+ * });
+ *
+ * var oldNs2Record = zone.record('ns', {
+ * name: 'example.com.',
+ * data: 'ns-cloud2.googledomains.com.',
+ * ttl: 86400
+ * });
+ *
+ * zone.deleteRecords([
+ * oldNs1Record,
+ * oldNs2Record
+ * ], callback);
+ *
+ * //-
+ * // Possibly a simpler way to perform the above change is deleting records by
+ * // type.
+ * //-
+ * zone.deleteRecords('ns', callback);
+ *
+ * //-
+ * // You can also delete records of multiple types.
+ * //-
+ * zone.deleteRecords(['ns', 'a'], callback);
+ */
+Zone.prototype.deleteRecords = function(records, callback) {
+ records = util.arrayize(records);
+
+ if (util.is(records[0], 'string')) {
+ this.deleteRecordsByType_(records, callback);
+ return;
+ }
+
+ this.createChange({
+ delete: records
+ }, callback);
+};
+
+/**
+ * Emptying your zone means leaving only 'NS' and 'SOA' records. This method
+ * will first fetch the non-NS and non-SOA records from your zone using
+ * {module:dns/zone#getRecords}, then use {module:dns/zone#createChange} to
+ * create a deletion change.
+ *
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An API error.
+ * @param {?module:dns/change} callback.change - A {module:dns/change} object.
+ * @param {object} callback.apiResponse - Raw API response.
+ */
+Zone.prototype.empty = function(callback) {
+ var self = this;
+
+ this.getRecords(function(err, records) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ var recordsToDelete = records.filter(function(record) {
+ return record.type !== 'NS' && record.type !== 'SOA';
+ });
+
+ if (recordsToDelete.length === 0) {
+ callback();
+ } else {
+ self.deleteRecords(recordsToDelete, callback);
+ }
+ });
+};
+
+/**
+ * Provide a path to a zone file to copy records into the zone.
+ *
+ * @param {string} localPath - The fully qualified path to the zone file.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An API or file system error.
+ *
+ * @example
+ * var zoneFilename = '/Users/stephen/zonefile.zone';
+ *
+ * zone.export(zoneFilename, function(err) {
+ * if (!err) {
+ * // The zone file was created successfully.
+ * }
+ * });
+ */
+Zone.prototype.export = function(localPath, callback) {
+ this.getRecords(function(err, records) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ var stringRecords = records.map(util.exec('toString')).join('\n');
+
+ fs.writeFile(localPath, stringRecords, 'utf-8', function(err) {
+ callback(err || null);
+ });
+ });
+};
+
+/**
+ * Get the list of changes associated with this zone. A change is an atomic
+ * update to a collection of records.
+ *
+ * @param {object=} query - The query object.
+ * @param {number} query.maxResults - Maximum number of results to return.
+ * @param {string} query.pageToken - The page token.
+ * @param {string} query.sort - Set to 'asc' for ascending, and 'desc' for
+ * descending or omit for no sorting.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An API error.
+ * @param {?module:dns/change[]} callback.changes - An array of
+ * {module:dns/change} objects.
+ * @param {?object} callback.nextQuery - A query object representing the next
+ * page of results.
+ * @param {object} callback.apiResponse - Raw API response.
+ *
+ * @example
+ * var callback = function(err, changes, nextQuery, apiResponse) {
+ * // The `metadata` property is populated for you with the metadata at the
+ * // time of fetching.
+ * changes[0].metadata;
+ *
+ * // However, in cases where you are concerned the metadata could have
+ * // changed, use the `getMetadata` method.
+ * changes[0].getMetadata(function(err, metadata) {});
+
+ * if (nextQuery) {
+ * // nextQuery will be non-null if there are more results.
+ * zone.getChanges(nextQuery, callback);
+ * }
+ * };
+ *
+ * zone.getChanges(callback);
+ *
+ * //-
+ * // Get the changes from your zone as a readable object stream.
+ * //-
+ * zone.getChanges()
+ * .on('error', console.error)
+ * .on('data', function(change) {
+ * // change is a Change object.
+ * })
+ * .on('end', function() {
+ * // All changes retrieved.
+ * });
+ *
+ * //-
+ * // If you anticipate many results, you can end a stream early to prevent
+ * // unnecessary processing and API requests.
+ * //-
+ * zone.getChanges()
+ * .on('data', function(change) {
+ * this.end();
+ * });
+ */
+Zone.prototype.getChanges = function(query, callback) {
+ var self = this;
+
+ if (util.is(query, 'function')) {
+ callback = query;
+ query = {};
+ }
+
+ if (query.sort) {
+ query.sortOrder = query.sort === 'asc' ? 'ascending' : 'descending';
+ delete query.sort;
+ }
+
+ var path = '/managedZones/' + this.name + '/changes';
+
+ this.makeReq_('GET', path, query, null, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var changes = (resp.changes || []).map(function(change) {
+ var changeInstance = self.change(change.id);
+ changeInstance.metadata = change;
+ return changeInstance;
+ });
+
+ var nextQuery = null;
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, query, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ callback(null, changes, nextQuery, resp);
+ });
+};
+
+/**
+ * Get the metadata for the zone.
+ *
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An API error.
+ * @param {?object} callback.metadata - Metadata of the zone from the API.
+ * @param {object} callback.apiResponse - Raw API response.
+ *
+ * @example
+ * zone.getMetadata(function(err, metadata, apiResponse) {});
+ */
+Zone.prototype.getMetadata = function(callback) {
+ var self = this;
+ var path = '/managedZones/' + this.name;
+
+ this.makeReq_('GET', path, null, null, function(err, resp) {
+ if (err) {
+ callback(err, null, resp);
+ return;
+ }
+
+ self.metadata = resp;
+
+ callback(null, self.metadata, resp);
+ });
+};
+
+/**
+ * Get the list of records for this zone.
+ *
+ * @param {object=} query - The query object.
+ * @param {string} query.name - Restricts the list to return only records with
+ * this fully qualified domain name.
+ * @param {string} query.type - Restricts the list to return only records of
+ * this type. If present, the "name" parameter must also be present.
+ * @param {number} query.maxResults - Maximum number of results to be returned.
+ * @param {string} query.pageToken - The page token.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An API error.
+ * @param {?module:dns/record[]} callback.records - An array of
+ * {module:dns/record} objects.
+ * @param {?object} callback.nextQuery - A query object representing the next
+ * page of results.
+ * @param {object} callback.apiResponse - Raw API response.
+ *
+ * @example
+ * var callback = function(err, records, nextQuery, apiResponse) {
+ * if (!err) {
+ * // records is an array of Record objects.
+ * }
+ *
+ * if (nextQuery) {
+ * zone.getRecords(nextQuery, callback);
+ * }
+ * };
+ *
+ * zone.getRecords(callback);
+ *
+ * //-
+ * // Provide a query for further customization.
+ * //-
+ * // Get the namespace records for example.com.
+ * var query = {
+ * name: 'example.com.',
+ * type: 'NS'
+ * };
+ *
+ * zone.getRecords(query, callback);
+ *
+ * //-
+ * // Get the records from your zone as a readable object stream.
+ * //-
+ * zone.getRecords()
+ * .on('error', console.error)
+ * .on('data', function(record) {
+ * // record is a Record object.
+ * })
+ * .on('end', function() {
+ * // All records retrieved.
+ * });
+ *
+ * //-
+ * // If you anticipate many results, you can end a stream early to prevent
+ * // unnecessary processing and API requests.
+ * //-
+ * zone.getRecords()
+ * .on('data', function(change) {
+ * this.end();
+ * });
+ *
+ * //-
+ * // If you only want records of a specific type or types, provide them in
+ * // place of the query object.
+ * //-
+ * zone.getRecords('ns', function(err, records) {
+ * if (!err) {
+ * // records is an array of NS Record objects in your zone.
+ * }
+ * });
+ *
+ * //-
+ * // You can also specify multiple record types.
+ * //-
+ * zone.getRecords(['ns', 'a', 'cname'], function(err, records) {
+ * if (!err) {
+ * // records is an array of NS, A, and CNAME records in your zone.
+ * }
+ * });
+ */
+Zone.prototype.getRecords = function(query, callback) {
+ var self = this;
+
+ if (util.is(query, 'function')) {
+ callback = query;
+ query = {};
+ }
+
+ if (util.is(query, 'string') || util.is(query, 'array')) {
+ var filterByTypes_ = {};
+
+ // For faster lookups, store the record types the user wants in an object.
+ util.arrayize(query).forEach(function(type) {
+ filterByTypes_[type.toUpperCase()] = true;
+ });
+
+ query = {
+ filterByTypes_: filterByTypes_
+ };
+ }
+
+ var requestQuery = extend({}, query);
+ delete requestQuery.filterByTypes_;
+
+ var path = '/managedZones/' + this.name + '/rrsets';
+ this.makeReq_('GET', path, requestQuery, true, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var records = (resp.rrsets || []).map(function(record) {
+ return self.record(record.type, record);
+ });
+
+ if (query.filterByTypes_) {
+ records = records.filter(function(record) {
+ return query.filterByTypes_[record.type];
+ });
+ }
+
+ var nextQuery = null;
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, query, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ callback(null, records, nextQuery, resp);
+ });
+};
+
+/**
+ * Copy the records from a zone file into this zone.
+ *
+ * @param {string} localPath - The fully qualified path to the zone file.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An API or file system error.
+ * @param {?module:dns/change} callback.change - A {module:dns/change} object.
+ * @param {?object} callback.apiResponse - Raw API response.
+ *
+ * @example
+ * var zoneFilename = '/Users/dave/zonefile.zone';
+ *
+ * zone.import(zoneFilename, function(err, change, apiResponse) {
+ * if (!err) {
+ * // The change was created successfully.
+ * }
+ * });
+ */
+Zone.prototype.import = function(localPath, callback) {
+ var self = this;
+
+ fs.readFile(localPath, 'utf-8', function(err, file) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ var parsedZonefile = zonefile.parse(file);
+
+ var recordsToCreate = [];
+ function addRecordToCreate(record) {
+ var recordInstance = new Record.fromZoneRecord_(self, recordType, record);
+ recordsToCreate.push(recordInstance);
+ }
+
+ for (var recordType in parsedZonefile) {
+ var recordTypeSet = util.arrayize(parsedZonefile[recordType]);
+ recordTypeSet.forEach(addRecordToCreate);
+ }
+
+ self.addRecords(recordsToCreate, callback);
+ });
+};
+
+/**
+ * A {module:dns/record} object can be used to construct a record you want to
+ * add to your zone, or to refer to an existing one.
+ *
+ * Note that using this method will not itself make any API requests. You will
+ * use the object returned in other API calls, for example to add a record to
+ * your zone or to delete an existing one.
+ *
+ * @param {string} type - The type of record to construct or the type of record
+ * you are referencing.
+ * @param {object} metadata - The metadata of this record.
+ * @param {string} metadata.name - The name of the record, e.g.
+ * `www.example.com.`.
+ * @param {string[]} metadata.data - Defined in
+ * [RFC 1035, section 5](https://goo.gl/9EiM0e) and
+ * [RFC 1034, section 3.6.1](https://goo.gl/Hwhsu9).
+ * @param {number} metadata.ttl - Seconds that the resource is cached by
+ * resolvers.
+ * @return {module:dns/record}
+ *
+ * @example
+ * //-
+ * // Reference an existing record to delete from your zone.
+ * //-
+ * var oldARecord = zone.record('a', {
+ * name: 'example.com.',
+ * data: '1.2.3.4',
+ * ttl: 86400
+ * });
+ *
+ * //-
+ * // Construct a record to add to your zone.
+ * //-
+ * var newARecord = zone.record('a', {
+ * name: 'example.com.',
+ * data: '5.6.7.8',
+ * ttl: 86400
+ * });
+ *
+ * //-
+ * // Use these records together to create a change.
+ * //-
+ * zone.createChange({
+ * add: newARecord,
+ * delete: oldARecord
+ * }, function(err, change, apiResponse) {});
+ */
+Zone.prototype.record = function(type, metadata) {
+ return new Record(this, type, metadata);
+};
+
+/**
+ * Provide a record type that should be deleted and replaced with other records.
+ *
+ * This is not an atomic request. Two API requests are made
+ * (one to get records of the type that you've requested, then another to
+ * replace them), which means the operation is not atomic and could result in
+ * unexpected changes.
+ *
+ * @param {string|string[]} recordTypes - Type(s) of records to replace.
+ * @param {module:dns/record|module:dns/record[]} newRecords - The record
+ * objects to add.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An API error.
+ * @param {?module:dns/change} callback.change - A {module:dns/change} object.
+ * @param {?object} callback.apiResponse - Raw API response.
+ *
+ * @example
+ * var newNs1Record = zone.record('ns', {
+ * name: 'example.com.',
+ * data: 'ns-cloud1.googledomains.com.',
+ * ttl: 86400
+ * });
+ *
+ * var newNs2Record = zone.record('ns', {
+ * name: 'example.com.',
+ * data: 'ns-cloud2.googledomains.com.',
+ * ttl: 86400
+ * });
+ *
+ * var newNsRecords = [
+ * newNs1Record,
+ * newNs2Record
+ * ];
+ *
+ * zone.replaceRecords('ns', newNsRecords, function(err, change, apiResponse) {
+ * if (!err) {
+ * // The change was created successfully.
+ * }
+ * });
+ */
+Zone.prototype.replaceRecords = function(recordType, newRecords, callback) {
+ var self = this;
+
+ this.getRecords(recordType, function(err, recordsToDelete) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ self.createChange({
+ add: newRecords,
+ delete: recordsToDelete
+ }, callback);
+ });
+};
+
+/**
+ * Delete records from the zone matching an array of types.
+ *
+ * @private
+ *
+ * @param {string[]} recordTypes - Types of records to delete. Ex: 'NS', 'A'.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An API error.
+ * @param {?module:dns/change} callback.change - A {module:dns/change} object.
+ * @param {?object} callback.apiResponse - Raw API response.
+ *
+ * @example
+ * zone.deleteRecordsByType_(['NS', 'A'], function(err, change, apiResponse) {
+ * if (!err) {
+ * // The change was created successfully.
+ * }
+ * });
+ */
+Zone.prototype.deleteRecordsByType_ = function(recordTypes, callback) {
+ var self = this;
+
+ this.getRecords(recordTypes, function(err, records) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ if (records.length === 0) {
+ callback();
+ return;
+ }
+
+ self.deleteRecords(records, 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(Zone, ['getChanges', 'getRecords']);
+
+module.exports = Zone;
diff --git a/lib/index.js b/lib/index.js
index 0a03374ac49..dedd6f70f80 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -32,6 +32,12 @@ var BigQuery = require('./bigquery');
*/
var Datastore = require('./datastore');
+/**
+ * @type {module:dns}
+ * @private
+ */
+var DNS = require('./dns');
+
/**
* @type {module:pubsub}
* @private
@@ -122,6 +128,10 @@ function gcloud(config) {
return new BigQuery(util.extendGlobalConfig(config, options));
},
datastore: new Datastore(config),
+ dns: function(options) {
+ options = options || {};
+ return new DNS(util.extendGlobalConfig(config, options));
+ },
pubsub: function(options) {
options = options || {};
return new PubSub(util.extendGlobalConfig(config, options));
@@ -176,6 +186,8 @@ gcloud.bigquery = BigQuery;
*/
gcloud.datastore = Datastore;
+gcloud.dns = DNS;
+
/**
* **Experimental**
*
diff --git a/package.json b/package.json
index bda8f7ee671..25a0c518b64 100644
--- a/package.json
+++ b/package.json
@@ -52,6 +52,7 @@
"buffer-equal": "0.0.1",
"concat-stream": "^1.5.0",
"configstore": "^1.0.0",
+ "dns-zonefile": "0.1.9",
"duplexify": "^3.2.0",
"extend": "^2.0.0",
"google-auth-library": "^0.9.4",
diff --git a/scripts/docs.sh b/scripts/docs.sh
index 24a477ad27c..49020e714d4 100755
--- a/scripts/docs.sh
+++ b/scripts/docs.sh
@@ -21,6 +21,11 @@
./node_modules/.bin/dox < lib/bigquery/job.js > docs/json/master/bigquery/job.json &
./node_modules/.bin/dox < lib/bigquery/table.js > docs/json/master/bigquery/table.json &
+./node_modules/.bin/dox < lib/dns/change.js > docs/json/master/dns/change.json &
+./node_modules/.bin/dox < lib/dns/index.js > docs/json/master/dns/index.json &
+./node_modules/.bin/dox < lib/dns/record.js > docs/json/master/dns/record.json &
+./node_modules/.bin/dox < lib/dns/zone.js > docs/json/master/dns/zone.json &
+
./node_modules/.bin/dox < lib/datastore/dataset.js > docs/json/master/datastore/dataset.json &
./node_modules/.bin/dox < lib/datastore/index.js > docs/json/master/datastore/index.json &
./node_modules/.bin/dox < lib/datastore/query.js > docs/json/master/datastore/query.json &
diff --git a/system-test/data/zonefile.zone b/system-test/data/zonefile.zone
new file mode 100644
index 00000000000..9b123e1c583
--- /dev/null
+++ b/system-test/data/zonefile.zone
@@ -0,0 +1,2 @@
+{DNS_DOMAIN} 21600 IN SPF "v=spf1" "mx:{DNS_DOMAIN}" "-all"
+{DNS_DOMAIN} 21600 IN TXT "google-site-verification=xxxxxxxxxxxxYYYYYYXXX"
diff --git a/system-test/dns.js b/system-test/dns.js
new file mode 100644
index 00000000000..97f8de94c3f
--- /dev/null
+++ b/system-test/dns.js
@@ -0,0 +1,393 @@
+/**
+ * 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 async = require('async');
+var fs = require('fs');
+var tmp = require('tmp');
+var uuid = require('node-uuid');
+
+var env = require('./env.js');
+var DNS = require('../lib/dns');
+var util = require('../lib/common/util.js');
+
+var dns = new DNS(env);
+var DNS_DOMAIN = process.env.GCLOUD_TESTS_DNS_DOMAIN;
+
+// Only run the tests if there is a domain to test with.
+(DNS_DOMAIN ? describe : describe.skip)('dns', function() {
+ var ZONE;
+ var ZONENAME = 'test-zone-' + uuid.v4().substr(0, 18);
+
+ var records = {};
+
+ function createRecords() {
+ records.a = ZONE.record('a', {
+ ttl: 86400,
+ name: DNS_DOMAIN,
+ data: '1.2.3.4'
+ });
+
+ records.aaaa = ZONE.record('aaaa', {
+ ttl: 86400,
+ name: DNS_DOMAIN,
+ data: '2607:f8b0:400a:801::1005'
+ });
+
+ records.cname = ZONE.record('cname', {
+ ttl: 86400,
+ name: 'mail.' + DNS_DOMAIN,
+ data: 'example.com.'
+ });
+
+ records.mx = ZONE.record('mx', {
+ ttl: 86400,
+ name: DNS_DOMAIN,
+ data: [
+ '10 mail.' + DNS_DOMAIN,
+ '20 mail2.' + DNS_DOMAIN
+ ]
+ });
+
+ records.naptr = ZONE.record('naptr', {
+ ttl: 300,
+ name: '2.1.2.1.5.5.5.0.7.7.1.e164.arpa.',
+ data: [
+ '100 10 \"u\" \"sip+E2U\" \"!^.*$!sip:information@foo.se!i\" .',
+ '102 10 \"u\" \"smtp+E2U\" \"!^.*$!mailto:information@foo.se!i\" .'
+ ]
+ });
+
+ records.ns = ZONE.record('ns', {
+ ttl: 86400,
+ name: DNS_DOMAIN,
+ data: 'ns-cloud1.googledomains.com.'
+ });
+
+ records.ptr = ZONE.record('ptr', {
+ ttl: 60,
+ name: '2.1.0.10.in-addr.arpa.',
+ data: 'server.' + DNS_DOMAIN
+ });
+
+ records.soa = ZONE.record('soa', {
+ ttl: 21600,
+ name: DNS_DOMAIN,
+ data: [
+ 'ns-cloud1.googledomains.com.',
+ 'dns-admin.google.com.',
+ '1 21600 3600 1209600 300'
+ ].join(' ')
+ });
+
+ records.spf = ZONE.record('spf', {
+ ttl: 21600,
+ name: DNS_DOMAIN,
+ data: 'v=spf1 mx:' + DNS_DOMAIN.replace(/.$/, '') + ' -all'
+ });
+
+ records.srv = ZONE.record('srv', {
+ ttl: 21600,
+ name: 'sip.' + DNS_DOMAIN,
+ data: '0 5 5060 sip.' + DNS_DOMAIN
+ });
+
+ records.txt = ZONE.record('txt', {
+ ttl: 21600,
+ name: DNS_DOMAIN,
+ data: 'google-site-verification=xxxxxxxxxxxxYYYYYYXXX'
+ });
+ }
+
+ before(function(done) {
+ dns.getZones(function(err, zones) {
+ if (err) {
+ done(err);
+ return;
+ }
+
+ async.each(zones, util.exec('delete', { force: true }), function(err) {
+ if (err) {
+ done(err);
+ return;
+ }
+
+ dns.createZone(ZONENAME, { dnsName: DNS_DOMAIN }, function(err, zone) {
+ assert.ifError(err);
+ ZONE = zone;
+ createRecords();
+ done();
+ });
+ });
+ });
+ });
+
+ after(function(done) {
+ ZONE.delete({ force: true }, done);
+ });
+
+ it('should create a zone', function(done) {
+ var tempName = 'test-zone-' + uuid.v4().substr(0, 18);
+
+ dns.createZone(tempName, { dnsName: DNS_DOMAIN }, function(err, zone) {
+ assert.ifError(err);
+ assert.equal(zone.name, tempName);
+ zone.delete(done);
+ });
+ });
+
+ it('should return 0 or more zones', function(done) {
+ dns.getZones(function(err, zones) {
+ assert.ifError(err);
+ assert(zones.length >= 0);
+ done();
+ });
+ });
+
+ describe('Zones', function() {
+ it('should get the metadata for a zone', function(done) {
+ ZONE.getMetadata(function(err, metadata) {
+ assert.ifError(err);
+ assert.equal(metadata.name, ZONENAME);
+ done();
+ });
+ });
+
+ it('should delete a zone', function(done) {
+ var name = 'test-zone-' + uuid.v4().substr(0, 18);
+
+ dns.createZone(name, { dnsName: DNS_DOMAIN }, function(err, zone) {
+ assert.ifError(err);
+ zone.delete(done);
+ });
+ });
+
+ it('should support all types of records', function(done) {
+ var recordsToCreate = [
+ records.a,
+ records.aaaa,
+ records.cname,
+ records.mx,
+ // records.naptr,
+ records.ns,
+ // records.ptr,
+ records.soa,
+ records.spf,
+ records.srv,
+ records.txt
+ ];
+
+ ZONE.replaceRecords(['ns', 'soa'], recordsToCreate, done);
+ });
+
+ it('should import records from a zone file', function(done) {
+ var zoneFilename = require.resolve('./data/zonefile.zone');
+ var zoneFileTemplate = fs.readFileSync(zoneFilename, 'utf-8');
+ zoneFileTemplate = util.format(zoneFileTemplate, {
+ DNS_DOMAIN: DNS_DOMAIN
+ });
+
+ tmp.setGracefulCleanup();
+ tmp.file(function _tempFileCreated(err, tmpFilePath) {
+ assert.ifError(err);
+
+ fs.writeFileSync(tmpFilePath, zoneFileTemplate, 'utf-8');
+
+ ZONE.empty(function(err) {
+ assert.ifError(err);
+
+ ZONE.import(tmpFilePath, function(err) {
+ assert.ifError(err);
+
+ ZONE.getRecords(['spf', 'txt'], function(err, records) {
+ assert.ifError(err);
+
+ var spfRecord = records.filter(function(record) {
+ return record.type === 'SPF';
+ })[0];
+
+ assert.strictEqual(
+ spfRecord.toJSON().rrdatas[0],
+ '"v=spf1" "mx:' + DNS_DOMAIN + '" "-all"'
+ );
+
+ var txtRecord = records.filter(function(record) {
+ return record.type === 'TXT';
+ })[0];
+
+ assert.strictEqual(
+ txtRecord.toJSON().rrdatas[0],
+ '"google-site-verification=xxxxxxxxxxxxYYYYYYXXX"'
+ );
+
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ it('should export records to a zone file', function(done) {
+ tmp.setGracefulCleanup();
+ tmp.file(function tempFileCreated(err, tmpFilename) {
+ assert.ifError(err);
+
+ async.series([
+ function(next) {
+ ZONE.empty(next);
+ },
+
+ function(next) {
+ var recordsToCreate = [
+ records.spf,
+ records.srv
+ ];
+
+ ZONE.addRecords(recordsToCreate, next);
+ },
+
+ function(next) {
+ ZONE.export(tmpFilename, next);
+ }
+ ], done);
+ });
+ });
+
+ describe('changes', function() {
+ it('should create a change', function(done) {
+ var record = ZONE.record('srv', {
+ ttl: 3600,
+ name: DNS_DOMAIN,
+ data: '10 0 5222 127.0.0.1.'
+ });
+
+ ZONE.createChange({ add: record }, function(err, change) {
+ assert.ifError(err);
+
+ var addition = change.metadata.additions[0];
+ delete addition.kind;
+ assert.deepEqual(addition, record.toJSON());
+
+ done();
+ });
+ });
+
+ it('should get a list of changes', function(done) {
+ ZONE.getChanges(function(err, changes) {
+ assert.ifError(err);
+ assert(changes.length >= 0);
+ done();
+ });
+ });
+
+ it('should get metadata', function(done) {
+ ZONE.getChanges(function(err, changes) {
+ assert.ifError(err);
+
+ var change = changes[0];
+ var expectedMetadata = change.metadata;
+
+ change.getMetadata(function(err, metadata) {
+ assert.ifError(err);
+
+ delete metadata.status;
+ delete expectedMetadata.status;
+ assert.deepEqual(metadata, expectedMetadata);
+
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ describe('Records', function() {
+ it('should return 0 or more records', function(done) {
+ ZONE.getRecords(function(err, records) {
+ assert.ifError(err);
+ assert(records.length >= 0);
+ done();
+ });
+ });
+
+ it('should cursor through records by type', function(done) {
+ var newRecords = [
+ ZONE.record('cname', {
+ ttl: 86400,
+ name: '1.' + DNS_DOMAIN,
+ data: DNS_DOMAIN
+ }),
+ ZONE.record('cname', {
+ ttl: 86400,
+ name: '2.' + DNS_DOMAIN,
+ data: DNS_DOMAIN
+ })
+ ];
+
+ ZONE.replaceRecords('cname', newRecords, function(err) {
+ assert.ifError(err);
+
+ var callback = function(err, records, nextQuery) {
+ if (nextQuery) {
+ ZONE.getRecords(nextQuery, callback);
+ return;
+ }
+
+ ZONE.deleteRecords(newRecords, done);
+ };
+
+ ZONE.getRecords({
+ types: 'cname',
+ maxResults: 2
+ }, callback);
+ });
+ });
+
+ it('should replace records', function(done) {
+ var name = 'test-zone-' + uuid.v4().substr(0, 18);
+
+ dns.createZone(name, { dnsName: DNS_DOMAIN }, function(err, zone) {
+ assert.ifError(err);
+
+ zone.getRecords('ns', function(err, originalRecords) {
+ assert.ifError(err);
+
+ var originalData = originalRecords[0].data;
+
+ var newRecord = zone.record('ns', {
+ ttl: 3600,
+ name: DNS_DOMAIN,
+ data: ['ns1.nameserver.net.', 'ns2.nameserver.net.']
+ });
+
+ zone.replaceRecords('ns', newRecord, function(err, change) {
+ assert.ifError(err);
+
+ var deleted = change.metadata.deletions[0].rrdatas;
+ var added = change.metadata.additions[0].rrdatas;
+
+ assert.deepEqual(deleted, originalData);
+ assert.deepEqual(added, newRecord.data);
+
+ done();
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/test/common/util.js b/test/common/util.js
index 38376f5bcce..f2658f4118f 100644
--- a/test/common/util.js
+++ b/test/common/util.js
@@ -932,6 +932,36 @@ describe('common/util', function() {
});
});
+ describe('exec', function() {
+ it('should execute the function specified', function() {
+ var people = [
+ {
+ getName: function() { return 'Stephen'; }
+ },
+ {
+ getName: function() { return 'Dave'; }
+ }
+ ];
+
+ assert.deepEqual(people.map(util.exec('getName')), ['Stephen', 'Dave']);
+ });
+
+ it('should accept arguments', function() {
+ var people = [
+ {
+ getName: function(prefix) { return prefix + ' Stephen'; }
+ },
+ {
+ getName: function(prefix) { return prefix + ' Dave'; }
+ }
+ ];
+
+ var expectedNames = ['Mr. Stephen', 'Mr. Dave'];
+
+ assert.deepEqual(people.map(util.exec('getName', 'Mr.')), expectedNames);
+ });
+ });
+
describe('prop', function() {
it('should return objects that match the property name', function() {
var people = [
diff --git a/test/dns/change.js b/test/dns/change.js
new file mode 100644
index 00000000000..a950ad64954
--- /dev/null
+++ b/test/dns/change.js
@@ -0,0 +1,130 @@
+/**
+ * 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 Change = require('../../lib/dns/change.js');
+var util = require('../../lib/common/util.js');
+
+describe('Change', function() {
+ var ZONE = {
+ name: 'zone-name',
+ dns: {
+ makeReq_: util.noop
+ }
+ };
+
+ var CHANGE_ID = 'change-id';
+
+ var change;
+
+ beforeEach(function() {
+ change = new Change(ZONE, CHANGE_ID);
+ });
+
+ describe('instantiation', function() {
+ it('should localize the zone name', function() {
+ assert.strictEqual(change.zoneName, ZONE.name);
+ });
+
+ it('should localize the id', function() {
+ assert.strictEqual(change.id, CHANGE_ID);
+ });
+
+ it('should create a makeReq_ function from the Zone', function(done) {
+ var zone = extend({}, ZONE, {
+ dns: {
+ makeReq_: function() {
+ assert.strictEqual(this, zone.dns);
+ done();
+ }
+ }
+ });
+
+ new Change(zone, CHANGE_ID).makeReq_();
+ });
+ });
+
+ describe('getMetadata', function() {
+ it('should make the correct API request', function(done) {
+ change.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+
+ var expectedPath = util.format('/managedZones/{z}/changes/{c}', {
+ z: ZONE.name,
+ c: CHANGE_ID
+ });
+ assert.strictEqual(path, expectedPath);
+
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ change.getMetadata(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ change.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error and API response', function(done) {
+ change.getMetadata(function(err, metadata, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var metadata = { e: 'f', g: 'h' };
+
+ beforeEach(function() {
+ change.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, metadata, metadata);
+ };
+ });
+
+ it('should update the metadata', function(done) {
+ change.getMetadata(function(err) {
+ assert.ifError(err);
+ assert.strictEqual(change.metadata, metadata);
+ done();
+ });
+ });
+
+ it('should execute callback with metadata & API resp', function(done) {
+ change.getMetadata(function(err, metadata_, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(metadata_, metadata);
+ assert.strictEqual(apiResponse_, metadata);
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/test/dns/index.js b/test/dns/index.js
new file mode 100644
index 00000000000..2b5ab1e2038
--- /dev/null
+++ b/test/dns/index.js
@@ -0,0 +1,405 @@
+/**
+ * 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 extended = false;
+var fakeStreamRouter = {
+ extend: function(Class, methods) {
+ if (Class.name !== 'DNS') {
+ return;
+ }
+
+ extended = true;
+ methods = util.arrayize(methods);
+ assert.equal(Class.name, 'DNS');
+ assert.deepEqual(methods, ['getZones']);
+ }
+};
+
+var makeAuthorizedRequestFactoryOverride;
+var fakeUtil = extend({}, util, {
+ makeAuthorizedRequestFactory: function() {
+ if (makeAuthorizedRequestFactoryOverride) {
+ return makeAuthorizedRequestFactoryOverride.apply(null, arguments);
+ } else {
+ return util.makeAuthorizedRequestFactory.apply(null, arguments);
+ }
+ }
+});
+
+function FakeZone() {
+ this.calledWith_ = arguments;
+}
+
+describe('DNS', function() {
+ var DNS;
+ var dns;
+
+ var PROJECT_ID = 'project-id';
+
+ before(function() {
+ mockery.registerMock('../common/stream-router.js', fakeStreamRouter);
+ mockery.registerMock('../common/util.js', fakeUtil);
+ mockery.registerMock('./zone.js', FakeZone);
+ mockery.enable({
+ useCleanCache: true,
+ warnOnUnregistered: false
+ });
+
+ DNS = require('../../lib/dns/index.js');
+ });
+
+ after(function() {
+ mockery.deregisterAll();
+ mockery.disable();
+ });
+
+ beforeEach(function() {
+ makeAuthorizedRequestFactoryOverride = null;
+
+ dns = new DNS({
+ projectId: PROJECT_ID
+ });
+ });
+
+ describe('instantiation', function() {
+ it('should extend the correct methods', function() {
+ assert(extended); // See `fakeStreamRouter.extend`
+ });
+
+ it('should throw if an ID is not provided', function() {
+ assert.throws(function() {
+ new DNS();
+ }, /Sorry, we cannot connect/);
+ });
+
+ 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/ndev.clouddns.readwrite',
+ 'https://www.googleapis.com/auth/cloud-platform'
+ ]
+ });
+ return done;
+ };
+
+ var dns = new DNS(options);
+ dns.makeAuthorizedRequest_();
+ });
+
+ it('should localize the projectId', function() {
+ assert.equal(dns.projectId_, PROJECT_ID);
+ });
+ });
+
+ describe('createZone', function() {
+ var zoneName = 'zone-name';
+ var config = { dnsName: 'dns-name' };
+
+ it('should throw if a zone name is not provided', function() {
+ assert.throws(function() {
+ dns.createZone();
+ }, /A zone name is required/);
+ });
+
+ it('should throw if a zone dnsname is not provided', function() {
+ assert.throws(function() {
+ dns.createZone(zoneName);
+ }, /A zone dnsName is required/);
+
+ assert.throws(function() {
+ dns.createZone(zoneName, {});
+ }, /A zone dnsName is required/);
+ });
+
+ it('should use a provided description', function(done) {
+ var cfg = extend({}, config, { description: 'description' });
+
+ dns.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(body.description, cfg.description);
+ done();
+ };
+
+ dns.createZone(zoneName, cfg, assert.ifError);
+ });
+
+ it('should default a description to ""', function(done) {
+ dns.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(body.description, '');
+ done();
+ };
+
+ dns.createZone(zoneName, config, assert.ifError);
+ });
+
+ it('should make the correct API request', function(done) {
+ dns.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'POST');
+ assert.strictEqual(path, '/managedZones');
+ assert.strictEqual(query, null);
+
+ var expectedBody = extend({}, config, {
+ name: zoneName,
+ description: ''
+ });
+ assert.deepEqual(body, expectedBody);
+
+ done();
+ };
+
+ dns.createZone(zoneName, config, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ dns.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error and API response', function(done) {
+ dns.createZone(zoneName, config, function(err, zone, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(zone, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = { name: zoneName };
+ var zone = {};
+
+ beforeEach(function() {
+ dns.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+
+ dns.zone = function() {
+ return zone;
+ };
+ });
+
+ it('should create a zone from the response', function(done) {
+ dns.zone = function(name) {
+ assert.strictEqual(name, apiResponse.name);
+ setImmediate(done);
+ return zone;
+ };
+
+ dns.createZone(zoneName, config, assert.ifError);
+ });
+
+ it('should execute callback with zone and API response', function(done) {
+ dns.createZone(zoneName, config, function(err, zone_, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(zone_, zone);
+ assert.strictEqual(apiResponse_, apiResponse);
+
+ done();
+ });
+ });
+
+ it('should set the metadata to the response', function(done) {
+ dns.createZone(zoneName, config, function(err, zone) {
+ assert.strictEqual(zone.metadata, apiResponse);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('getZones', function() {
+ it('should make the correct request', function(done) {
+ var query = { a: 'b', c: 'd' };
+
+ dns.makeReq_ = function(method, path, query_, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '/managedZones');
+ assert.strictEqual(query, query);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ dns.getZones(query, assert.ifError);
+ });
+
+ it('should use an empty query if one was not provided', function(done) {
+ dns.makeReq_ = function(method, path, query) {
+ assert.equal(Object.keys(query).length, 0);
+ done();
+ };
+
+ dns.getZones(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ dns.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error and API response', function(done) {
+ dns.getZones({}, function(err, zones, nextQuery, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(zones, null);
+ assert.strictEqual(nextQuery, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var zone = { name: 'zone-1', a: 'b', c: 'd' };
+ var apiResponse = { managedZones: [zone] };
+
+ beforeEach(function() {
+ dns.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+
+ dns.zone = function() {
+ return zone;
+ };
+ });
+
+ it('should create zones from the response', function(done) {
+ dns.zone = function(zoneName) {
+ assert.strictEqual(zoneName, zone.name);
+ setImmediate(done);
+ return zone;
+ };
+
+ dns.getZones({}, assert.ifError);
+ });
+
+ it('should set a nextQuery if necessary', function(done) {
+ var apiResponseWithNextPageToken = extend({}, apiResponse, {
+ nextPageToken: 'next-page-token'
+ });
+
+ var query = { a: 'b', c: 'd' };
+ var originalQuery = extend({}, query);
+
+ dns.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponseWithNextPageToken);
+ };
+
+ dns.getZones(query, function(err, zones, nextQuery) {
+ assert.ifError(err);
+
+ // Check the original query wasn't modified.
+ assert.deepEqual(query, originalQuery);
+
+ assert.deepEqual(nextQuery, extend({}, query, {
+ pageToken: apiResponseWithNextPageToken.nextPageToken
+ }));
+
+ done();
+ });
+ });
+
+ it('should execute callback with zones and API response', function(done) {
+ dns.getZones({}, function(err, zones, nextQuery, apiResponse_) {
+ assert.ifError(err);
+
+ assert.strictEqual(zones[0], zone);
+ assert.strictEqual(nextQuery, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+
+ done();
+ });
+ });
+
+ it('should assign metadata to zones', function(done) {
+ dns.getZones({}, function(err, zones) {
+ assert.ifError(err);
+ assert.strictEqual(zones[0].metadata, zone);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('zone', function() {
+ it('should throw if a name is not provided', function() {
+ assert.throws(function() {
+ dns.zone();
+ }, /A zone name is required/);
+ });
+
+ it('should return a Zone', function() {
+ var newZoneName = 'new-zone-name';
+ var newZone = dns.zone(newZoneName);
+
+ assert(newZone instanceof FakeZone);
+ assert.strictEqual(newZone.calledWith_[0], dns);
+ assert.strictEqual(newZone.calledWith_[1], newZoneName);
+ });
+ });
+
+ describe('makeReq_', function() {
+ it('should make correct authorized request', function(done) {
+ var method = 'POST';
+ var path = '/';
+ var query = 'query';
+ var body = 'body';
+
+ dns.makeAuthorizedRequest_ = function(reqOpts, callback) {
+ assert.equal(reqOpts.method, method);
+ assert.equal(reqOpts.qs, query);
+
+ var baseUri = 'https://www.googleapis.com/dns/v1/';
+ assert.equal(reqOpts.uri, baseUri + 'projects/' + PROJECT_ID + path);
+
+ assert.equal(reqOpts.json, body);
+
+ callback();
+ };
+
+ dns.makeReq_(method, path, query, body, done);
+ });
+ });
+});
diff --git a/test/dns/record.js b/test/dns/record.js
new file mode 100644
index 00000000000..854fa6081cf
--- /dev/null
+++ b/test/dns/record.js
@@ -0,0 +1,330 @@
+/**
+ * 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 Record = require('../../lib/dns/record.js');
+var util = require('../../lib/common/util.js');
+
+describe('Record', function() {
+ var record;
+
+ var ZONE = {
+ deleteRecords: util.noop
+ };
+ var TYPE = 'A';
+ var METADATA = {
+ name: 'name',
+ data: [],
+ ttl: 86400
+ };
+
+ beforeEach(function() {
+ record = new Record(ZONE, TYPE, METADATA);
+ });
+
+ describe('instantiation', function() {
+ it('should localize the zone instance', function() {
+ assert.strictEqual(record.zone_, ZONE);
+ });
+
+ it('should localize the type', function() {
+ assert.strictEqual(record.type, TYPE);
+ });
+
+ it('should localize the metadata', function() {
+ assert.strictEqual(record.metadata, METADATA);
+ });
+
+ it('should assign the parsed metadata', function() {
+ var parsedMetadata = record.toJSON();
+ delete parsedMetadata.rrdatas;
+
+ for (var prop in parsedMetadata) {
+ assert.strictEqual(record[prop], parsedMetadata[prop]);
+ }
+ });
+
+ it('should re-assign rrdatas to data', function() {
+ var originalRrdatas = [];
+
+ var recordThatHadRrdatas = new Record(ZONE, TYPE, {
+ rrdatas: originalRrdatas
+ });
+
+ assert.strictEqual(recordThatHadRrdatas.rrdatas, undefined);
+ assert.strictEqual(recordThatHadRrdatas.data, originalRrdatas);
+ });
+ });
+
+ describe('fromZoneRecord_', function() {
+ describe('a', function() {
+ var aRecord = {
+ ip: '0.0.0.0',
+ name: 'name',
+ ttl: 86400
+ };
+
+ var expectedData = aRecord.ip;
+
+ it('should parse an A record', function() {
+ var record = Record.fromZoneRecord_(ZONE, 'a', aRecord);
+
+ assert.strictEqual(record.type, 'A');
+ assert.deepEqual(record.metadata.data, expectedData);
+ assert.strictEqual(record.metadata.name, aRecord.name);
+ assert.strictEqual(record.metadata.ttl, aRecord.ttl);
+ });
+ });
+
+ describe('aaaa', function() {
+ var aaaaRecord = {
+ ip: '2607:f8b0:400a:801::1005',
+ name: 'name',
+ ttl: 86400
+ };
+
+ var expectedData = aaaaRecord.ip;
+
+ it('should parse an AAAA record', function() {
+ var record = Record.fromZoneRecord_(ZONE, 'aaaa', aaaaRecord);
+
+ assert.strictEqual(record.type, 'AAAA');
+ assert.strictEqual(record.metadata.data, expectedData);
+ assert.strictEqual(record.metadata.name, aaaaRecord.name);
+ assert.strictEqual(record.metadata.ttl, aaaaRecord.ttl);
+ });
+ });
+
+ describe('cname', function() {
+ var cnameRecord = {
+ alias: 'example.com.',
+ name: 'name',
+ ttl: 86400
+ };
+
+ var expectedData = cnameRecord.alias;
+
+ it('should parse a CNAME record', function() {
+ var record = Record.fromZoneRecord_(ZONE, 'cname', cnameRecord);
+
+ assert.strictEqual(record.type, 'CNAME');
+ assert.strictEqual(record.metadata.data, expectedData);
+ assert.strictEqual(record.metadata.name, cnameRecord.name);
+ assert.strictEqual(record.metadata.ttl, cnameRecord.ttl);
+ });
+ });
+
+ describe('mx', function() {
+ var mxRecord = {
+ preference: 0,
+ host: 'mail',
+ name: 'name',
+ ttl: 86400
+ };
+
+ var expectedData = mxRecord.preference + ' ' + mxRecord.host;
+
+ it('should parse an MX record', function() {
+ var record = Record.fromZoneRecord_(ZONE, 'mx', mxRecord);
+
+ assert.strictEqual(record.type, 'MX');
+ assert.strictEqual(record.metadata.data, expectedData);
+ assert.strictEqual(record.metadata.name, mxRecord.name);
+ assert.strictEqual(record.metadata.ttl, mxRecord.ttl);
+ });
+ });
+
+ describe('ns', function() {
+ var nsRecord = {
+ host: 'example.com',
+ name: 'name',
+ ttl: 86400
+ };
+
+ var expectedData = nsRecord.host;
+
+ it('should parse an NS record', function() {
+ var record = Record.fromZoneRecord_(ZONE, 'ns', nsRecord);
+
+ assert.strictEqual(record.type, 'NS');
+ assert.strictEqual(record.metadata.data, expectedData);
+ assert.strictEqual(record.metadata.name, nsRecord.name);
+ assert.strictEqual(record.metadata.ttl, nsRecord.ttl);
+ });
+ });
+
+ describe('soa', function() {
+ var soaRecord = {
+ mname: 'ns1.nameserver.net.',
+ rname: 'hostmaster.mydomain.com.',
+ serial: 86400,
+ retry: 600,
+ refresh: 3600,
+ expire: 604800,
+ minimum: 86400,
+ name: 'name',
+ ttl: 86400
+ };
+
+ var expectedData = [
+ soaRecord.mname,
+ soaRecord.rname,
+ soaRecord.serial,
+ soaRecord.retry,
+ soaRecord.refresh,
+ soaRecord.expire,
+ soaRecord.minimum
+ ].join(' ');
+
+ it('should parse an SOA record', function() {
+ var record = Record.fromZoneRecord_(ZONE, 'soa', soaRecord);
+
+ assert.strictEqual(record.type, 'SOA');
+ assert.strictEqual(record.metadata.data, expectedData);
+ assert.strictEqual(record.metadata.name, soaRecord.name);
+ assert.strictEqual(record.metadata.ttl, soaRecord.ttl);
+ });
+ });
+
+ describe('spf', function() {
+ var spfRecord = {
+ data: '"v=spf1" "mx:example.com"',
+ name: 'name',
+ ttl: 86400
+ };
+
+ var expectedData = spfRecord.data;
+
+ it('should parse an SPF record', function() {
+ var record = Record.fromZoneRecord_(ZONE, 'spf', spfRecord);
+
+ assert.strictEqual(record.type, 'SPF');
+ assert.strictEqual(record.metadata.data, expectedData);
+ assert.strictEqual(record.metadata.name, spfRecord.name);
+ assert.strictEqual(record.metadata.ttl, spfRecord.ttl);
+ });
+ });
+
+ describe('srv', function() {
+ var srvRecord = {
+ priority: 10,
+ weight: 0,
+ port: 5222,
+ target: 'jabber',
+ name: 'name',
+ ttl: 86400
+ };
+
+ var expectedData = [
+ srvRecord.priority,
+ srvRecord.weight,
+ srvRecord.port,
+ srvRecord.target
+ ].join(' ');
+
+ it('should parse an SRV record', function() {
+ var record = Record.fromZoneRecord_(ZONE, 'srv', srvRecord);
+
+ assert.strictEqual(record.type, 'SRV');
+ assert.strictEqual(record.metadata.data, expectedData);
+ assert.strictEqual(record.metadata.name, srvRecord.name);
+ assert.strictEqual(record.metadata.ttl, srvRecord.ttl);
+ });
+ });
+
+ describe('txt', function() {
+ var txtRecord = {
+ txt: 'txt-record-txt',
+ name: 'name',
+ ttl: 86400
+ };
+
+ var expectedData = txtRecord.txt;
+
+ it('should parse a TXT record', function() {
+ var record = Record.fromZoneRecord_(ZONE, 'txt', txtRecord);
+
+ assert.strictEqual(record.type, 'TXT');
+ assert.strictEqual(record.metadata.data, expectedData);
+ assert.strictEqual(record.metadata.name, txtRecord.name);
+ assert.strictEqual(record.metadata.ttl, txtRecord.ttl);
+ });
+ });
+ });
+
+ describe('delete', function() {
+ it('should call zone.deleteRecords', function(done) {
+ record.zone_.deleteRecords = function(records, callback) {
+ assert.strictEqual(records, record);
+ callback();
+ };
+
+ record.delete(done);
+ });
+ });
+
+ describe('toJSON', function() {
+ it('should format the data for the API', function() {
+ var expectedRecord = extend({}, METADATA, {
+ type: 'A',
+ rrdatas: METADATA.data
+ });
+ delete expectedRecord.data;
+
+ assert.deepEqual(record.toJSON(), expectedRecord);
+ });
+ });
+
+ describe('toString', function() {
+ it('should format the data for a zonefile', function() {
+ var jsonRecord = extend({}, METADATA, {
+ type: TYPE,
+ rrdatas: ['example.com.', 'example2.com.']
+ });
+
+ record.toJSON = function() {
+ return jsonRecord;
+ };
+
+ var expectedRecordString = [
+ [
+ jsonRecord.name,
+ jsonRecord.ttl,
+ 'IN',
+ TYPE,
+ jsonRecord.rrdatas[0]
+ ].join(' '),
+
+ [
+ jsonRecord.name,
+ jsonRecord.ttl,
+ 'IN',
+ TYPE,
+ jsonRecord.rrdatas[1]
+ ].join(' ')
+ ].join('\n');
+
+ // That's a bunch of silliness, but it generates simply:
+ // name 86400 IN A example.com.
+ // name 86400 IN A example2.com.
+
+ assert.strictEqual(record.toString(), expectedRecordString);
+ });
+ });
+});
diff --git a/test/dns/zone.js b/test/dns/zone.js
new file mode 100644
index 00000000000..f5a016e5a15
--- /dev/null
+++ b/test/dns/zone.js
@@ -0,0 +1,977 @@
+/**
+ * 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 parseOverride;
+var fakeDnsZonefile = {
+ parse: function() {
+ return (parseOverride || util.noop).apply(null, arguments);
+ }
+};
+
+var writeFileOverride;
+var readFileOverride;
+var fakeFs = {
+ readFile: function() {
+ return (readFileOverride || util.noop).apply(null, arguments);
+ },
+ writeFile: function() {
+ return (writeFileOverride || util.noop).apply(null, arguments);
+ }
+};
+
+var extended = false;
+var fakeStreamRouter = {
+ extend: function(Class, methods) {
+ if (Class.name !== 'Zone') {
+ return;
+ }
+
+ extended = true;
+ methods = util.arrayize(methods);
+ assert.equal(Class.name, 'Zone');
+ assert.deepEqual(methods, ['getChanges', 'getRecords']);
+ }
+};
+
+function FakeChange() {
+ this.calledWith_ = arguments;
+}
+
+function FakeRecord() {
+ this.calledWith_ = arguments;
+}
+FakeRecord.fromZoneRecord_ = function() {
+ var record = new FakeRecord();
+ record.calledWith_ = arguments;
+ return record;
+};
+
+describe('Zone', function() {
+ var Zone;
+ var zone;
+
+ var DNS = {
+ makeReq_: function() {}
+ };
+ var ZONE_NAME = 'zone-name';
+
+ before(function() {
+ mockery.registerMock('dns-zonefile', fakeDnsZonefile);
+ mockery.registerMock('fs', fakeFs);
+ mockery.registerMock('../common/stream-router.js', fakeStreamRouter);
+ mockery.registerMock('./change.js', FakeChange);
+ mockery.registerMock('./record.js', FakeRecord);
+ mockery.enable({
+ useCleanCache: true,
+ warnOnUnregistered: false
+ });
+
+ Zone = require('../../lib/dns/zone.js');
+ });
+
+ after(function() {
+ mockery.deregisterAll();
+ mockery.disable();
+ });
+
+ beforeEach(function() {
+ parseOverride = null;
+ readFileOverride = null;
+ writeFileOverride = null;
+ zone = new Zone(DNS, ZONE_NAME);
+ });
+
+ describe('instantiation', function() {
+ it('should extend the correct methods', function() {
+ assert(extended); // See `fakeStreamRouter.extend`
+ });
+
+ it('should localize the DNS instance', function() {
+ assert.strictEqual(zone.dns, DNS);
+ });
+
+ it('should localize the name', function() {
+ assert.strictEqual(zone.name, ZONE_NAME);
+ });
+
+ it('should create a makeReq_ function', function(done) {
+ var dns = {
+ makeReq_: function() {
+ assert.strictEqual(this, dns);
+ done();
+ }
+ };
+
+ var zone = new Zone(dns, ZONE_NAME);
+ zone.makeReq_();
+ });
+ });
+
+ describe('addRecords', function() {
+ it('should create a change with additions', function(done) {
+ var records = ['a', 'b', 'c'];
+
+ zone.createChange = function(options, callback) {
+ assert.strictEqual(options.add, records);
+ callback();
+ };
+
+ zone.addRecords(records, done);
+ });
+ });
+
+ describe('change', function() {
+ it('should throw if an ID is not provided', function() {
+ assert.throws(function() {
+ zone.change();
+ }, /A change id is required/);
+ });
+
+ it('should return a Change object', function() {
+ var changeId = 'change-id';
+
+ var change = zone.change(changeId);
+
+ assert(change instanceof FakeChange);
+ assert.strictEqual(change.calledWith_[0], zone);
+ assert.strictEqual(change.calledWith_[1], changeId);
+ });
+ });
+
+ describe('createChange', function() {
+ it('should throw error if add or delete is not provided', function() {
+ assert.throws(function() {
+ zone.createChange({}, util.noop);
+ }, /Cannot create a change with no additions or deletions/);
+ });
+
+ it('should parse and rename add to additions', function(done) {
+ var recordsToAdd = [
+ { toJSON: function() { return 'a'; } },
+ { toJSON: function() { return 'a'; } }
+ ];
+ var expectedAdditions = ['a', 'a'];
+
+ zone.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(body.add, undefined);
+ assert.deepEqual(body.additions, expectedAdditions);
+ done();
+ };
+
+ zone.createChange({ add: recordsToAdd }, assert.ifError);
+ });
+
+ it('should parse and rename delete to deletions', function(done) {
+ var recordsToDelete = [
+ { toJSON: function() { return 'a'; } },
+ { toJSON: function() { return 'a'; } }
+ ];
+ var expectedDeletions = ['a', 'a'];
+
+ zone.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(body.delete, undefined);
+ assert.deepEqual(body.deletions, expectedDeletions);
+ done();
+ };
+
+ zone.createChange({ delete: recordsToDelete }, assert.ifError);
+ });
+
+ it('should make correct API request', function(done) {
+ zone.makeReq_ = function(method, path, query) {
+ assert.strictEqual(method, 'POST');
+ assert.strictEqual(path, '/managedZones/' + ZONE_NAME + '/changes');
+ assert.strictEqual(query, null);
+ done();
+ };
+
+ zone.createChange({ add: [] }, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ zone.createChange({ add: [] }, function(err, change, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = { id: 1, a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should execute callback with Change & API response', function(done) {
+ var change = {};
+
+ zone.change = function(id) {
+ assert.strictEqual(id, apiResponse.id);
+ return change;
+ };
+
+ zone.createChange({ add: [] }, function(err, change_, apiResponse_) {
+ assert.ifError(err);
+
+ assert.strictEqual(change_, change);
+ assert.strictEqual(change_.metadata, apiResponse);
+
+ assert.strictEqual(apiResponse_, apiResponse);
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('delete', function() {
+ describe('force', function() {
+ it('should empty the zone', function(done) {
+ zone.empty = function() {
+ done();
+ };
+
+ zone.delete({ force: true }, assert.ifError);
+ });
+
+ it('should try to delete again after emptying', function(done) {
+ zone.makeReq_ = function() {
+ done();
+ };
+
+ zone.empty = function(callback) {
+ callback();
+ };
+
+ zone.delete({ force: true }, assert.ifError);
+ });
+ });
+
+ it('should make the correct API request', function(done) {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+ var ignoreThisArgument = { e: 'f', g: 'h' };
+
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ assert.strictEqual(method, 'DELETE');
+ assert.strictEqual(path, '/managedZones/' + ZONE_NAME);
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+ callback(error, apiResponse, ignoreThisArgument);
+ };
+
+ zone.delete(function(err, apiResponse_) {
+ assert.strictEqual(arguments.length, 2);
+ assert.strictEqual(err, error);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('deleteRecords', function() {
+ it('should delete records by type if a string is given', function(done) {
+ var recordsToDelete = 'ns';
+
+ zone.deleteRecordsByType_ = function(types, callback) {
+ assert.deepEqual(types, [recordsToDelete]);
+ callback();
+ };
+
+ zone.deleteRecords(recordsToDelete, done);
+ });
+
+ it('should create a change if record objects given', function(done) {
+ var recordsToDelete = { a: 'b', c: 'd' };
+
+ zone.createChange = function(options, callback) {
+ assert.deepEqual(options.delete, [recordsToDelete]);
+ callback();
+ };
+
+ zone.deleteRecords(recordsToDelete, done);
+ });
+ });
+
+ describe('empty', function() {
+ it('should get all records', function(done) {
+ zone.getRecords = function() {
+ done();
+ };
+
+ zone.empty(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+
+ beforeEach(function() {
+ zone.getRecords = function(callback) {
+ callback(error);
+ };
+ });
+
+ it('should execute callback with error', function(done) {
+ zone.empty(function(err) {
+ assert.strictEqual(err, error);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var records = [
+ { type: 'A' },
+ { type: 'AAAA' },
+ { type: 'CNAME' },
+ { type: 'MX' },
+ { type: 'NAPTR' },
+ { type: 'NS' },
+ { type: 'PTR' },
+ { type: 'SOA' },
+ { type: 'SPF' },
+ { type: 'SRV' },
+ { type: 'TXT' }
+ ];
+
+ var expectedRecordsToDelete = records.filter(function(record) {
+ return record.type !== 'NS' && record.type !== 'SOA';
+ });
+
+ beforeEach(function() {
+ zone.getRecords = function(callback) {
+ callback(null, records);
+ };
+ });
+
+ it('should execute callback if no records matched', function(done) {
+ zone.getRecords = function(callback) {
+ callback(null, []);
+ };
+
+ zone.empty(done);
+ });
+
+ it('should delete non-NS and non-SOA records', function(done) {
+ zone.deleteRecords = function(recordsToDelete, callback) {
+ assert.deepEqual(recordsToDelete, expectedRecordsToDelete);
+ callback();
+ };
+
+ zone.empty(done);
+ });
+ });
+ });
+
+ describe('export', function() {
+ var path = './zonefile';
+
+ var records = [
+ { toString: function() { return 'a'; } },
+ { toString: function() { return 'a'; } },
+ { toString: function() { return 'a'; } },
+ { toString: function() { return 'a'; } },
+ ];
+
+ var expectedZonefileContents = 'a\na\na\na';
+
+ beforeEach(function() {
+ zone.getRecords = function(callback) {
+ callback(null, records);
+ };
+ });
+
+ describe('get records', function() {
+ describe('error', function() {
+ var error = new Error('Error.');
+
+ it('should execute callback with error', function(done) {
+ zone.getRecords = function(callback) {
+ callback(error);
+ };
+
+ zone.export(path, function(err) {
+ assert.strictEqual(err, error);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ it('should get all records', function(done) {
+ zone.getRecords = function() {
+ done();
+ };
+
+ zone.export(path, assert.ifError);
+ });
+ });
+ });
+
+ describe('write file', function() {
+ it('should write correct zone file', function(done) {
+ writeFileOverride = function(path_, content, encoding) {
+ assert.strictEqual(path_, path);
+ assert.strictEqual(content, expectedZonefileContents);
+ assert.strictEqual(encoding, 'utf-8');
+
+ done();
+ };
+
+ zone.export(path, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+
+ beforeEach(function() {
+ writeFileOverride = function(path, content, encoding, callback) {
+ callback(error);
+ };
+ });
+
+ it('should execute the callback with an error', function(done) {
+ zone.export(path, function(err) {
+ assert.strictEqual(err, error);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ beforeEach(function() {
+ writeFileOverride = function(path, content, encoding, callback) {
+ callback();
+ };
+ });
+
+ it('should execute the callback', function(done) {
+ zone.export(path, function(err) {
+ assert.ifError(err);
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ describe('getChanges', function() {
+ it('should accept only a callback', function(done) {
+ zone.makeReq_ = function(method, path, query) {
+ assert.strictEqual(Object.keys(query).length, 0);
+ done();
+ };
+
+ zone.getChanges(assert.ifError);
+ });
+
+ it('should accept a sort', function(done) {
+ var query = { sort: 'desc' };
+
+ zone.makeReq_ = function(method, path, query) {
+ assert.strictEqual(query.sortOrder, 'descending');
+ assert.strictEqual(query.sort, undefined);
+
+ done();
+ };
+
+ zone.getChanges(query, assert.ifError);
+ });
+
+ it('should make the correct API request', function(done) {
+ var query = { a: 'b', c: 'd' };
+
+ zone.makeReq_ = function(method, path, query_, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '/managedZones/' + ZONE_NAME + '/changes');
+ assert.strictEqual(query_, query);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ zone.getChanges(query, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ zone.getChanges({}, function(err, changes, nextQuery, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = {
+ changes: [{ id: 1 }]
+ };
+
+ beforeEach(function() {
+ zone.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
+ };
+
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponseWithNextPageToken);
+ };
+
+ zone.getChanges({}, function(err, changes, nextQuery) {
+ assert.ifError(err);
+
+ assert.deepEqual(nextQuery, expectedNextQuery);
+
+ done();
+ });
+ });
+
+ it('should execute callback with Changes & API response', function(done) {
+ var change = {};
+
+ zone.change = function(id) {
+ assert.strictEqual(id, apiResponse.changes[0].id);
+ return change;
+ };
+
+ zone.getChanges({}, function(err, changes, nextQuery, apiResponse_) {
+ assert.ifError(err);
+
+ assert.strictEqual(changes[0], change);
+ assert.strictEqual(changes[0].metadata, apiResponse.changes[0]);
+
+ assert.strictEqual(apiResponse_, apiResponse);
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('getMetadata', function() {
+ it('should make the correct API request', function(done) {
+ zone.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '/managedZones/' + ZONE_NAME);
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ zone.getMetadata(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error and API response', function(done) {
+ zone.getMetadata(function(err, metadata, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should update the metadata to the API response', function(done) {
+ zone.getMetadata(function(err) {
+ assert.ifError(err);
+ assert.strictEqual(zone.metadata, apiResponse);
+ done();
+ });
+ });
+
+ it('should exec callback with metadata and API response', function(done) {
+ zone.getMetadata(function(err, metadata, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(metadata, apiResponse);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('getRecords', function() {
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ zone.getChanges({}, function(err, changes, nextQuery, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = {
+ rrsets: [{ type: 'NS' }]
+ };
+
+ beforeEach(function() {
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should execute callback with nextQuery if necessary', function(done) {
+ var nextPageToken = 'next-page-token';
+ var apiResponseWithNextPageToken = extend({}, apiResponse, {
+ nextPageToken: nextPageToken
+ });
+ var expectedNextQuery = { pageToken: nextPageToken };
+
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponseWithNextPageToken);
+ };
+
+ zone.getRecords({}, function(err, records, nextQuery) {
+ assert.ifError(err);
+
+ assert.deepEqual(nextQuery, expectedNextQuery);
+
+ done();
+ });
+ });
+
+ it('should execute callback with Records & API response', function(done) {
+ var record = {};
+
+ zone.record = function(type, recordObject) {
+ assert.strictEqual(type, apiResponse.rrsets[0].type);
+ assert.strictEqual(recordObject, apiResponse.rrsets[0]);
+ return record;
+ };
+
+ zone.getRecords({}, function(err, records, nextQuery, apiResponse_) {
+ assert.ifError(err);
+
+ assert.strictEqual(records[0], record);
+
+ assert.strictEqual(apiResponse_, apiResponse);
+
+ done();
+ });
+ });
+
+ describe('filtering', function() {
+ it('should accept a string type', function(done) {
+ var types = ['MX', 'CNAME'];
+
+ zone.getRecords(types, function(err, records) {
+ assert.ifError(err);
+
+ assert.strictEqual(records.length, 0);
+
+ done();
+ });
+ });
+
+ it('should accept an array of types', function(done) {
+ var type = 'MX';
+
+ zone.getRecords(type, function(err, records) {
+ assert.ifError(err);
+
+ assert.strictEqual(records.length, 0);
+
+ done();
+ });
+ });
+
+ it('should not send filterByTypes_ in API request', function(done) {
+ zone.makeReq_ = function(method, path, query) {
+ assert.strictEqual(query.filterByTypes_, undefined);
+ done();
+ };
+
+ zone.getRecords('NS', assert.ifError);
+ });
+ });
+ });
+ });
+
+ describe('import', function() {
+ var path = './zonefile';
+
+ it('should read from the file', function(done) {
+ readFileOverride = function(path_, encoding) {
+ assert.strictEqual(path, path);
+ assert.strictEqual(encoding, 'utf-8');
+ done();
+ };
+
+ zone.import(path, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+
+ beforeEach(function() {
+ readFileOverride = function(path, encoding, callback) {
+ callback(error);
+ };
+ });
+
+ it('should execute the callback', function(done) {
+ zone.import(path, function(err) {
+ assert.strictEqual(err, error);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var recordType = 'ns';
+ var parsedZonefile = {};
+ parsedZonefile[recordType] = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ parseOverride = function() {
+ return parsedZonefile;
+ };
+
+ readFileOverride = function(path, encoding, callback) {
+ callback();
+ };
+ });
+
+ it('should add records', function(done) {
+ zone.addRecords = function(recordsToCreate, callback) {
+ assert.strictEqual(recordsToCreate.length, 1);
+
+ var recordToCreate = recordsToCreate[0];
+
+ assert(recordToCreate instanceof FakeRecord);
+
+ var args = recordToCreate.calledWith_;
+ assert.strictEqual(args[0], zone);
+ assert.strictEqual(args[1], recordType);
+ assert.strictEqual(args[2], parsedZonefile[recordType]);
+
+ callback();
+ };
+
+ zone.import(path, done);
+ });
+ });
+ });
+
+ describe('record', function() {
+ it('should return a Record object', function() {
+ var type = 'a';
+ var metadata = { a: 'b', c: 'd' };
+
+ var record = zone.record(type, metadata);
+
+ assert(record instanceof FakeRecord);
+
+ var args = record.calledWith_;
+ assert.strictEqual(args[0], zone);
+ assert.strictEqual(args[1], type);
+ assert.strictEqual(args[2], metadata);
+ });
+ });
+
+ describe('replaceRecords', function() {
+ it('should get records', function(done) {
+ var recordType = 'ns';
+
+ zone.getRecords = function(recordType_) {
+ assert.strictEqual(recordType_, recordType);
+ done();
+ };
+
+ zone.replaceRecords(recordType, [], assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+
+ beforeEach(function() {
+ zone.getRecords = function(recordType, callback) {
+ callback(error);
+ };
+ });
+
+ it('should execute callback with error', function(done) {
+ zone.replaceRecords('a', [], function(err) {
+ assert.strictEqual(err, error);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var recordsToCreate = [
+ { a: 'b', c: 'd' },
+ { a: 'b', c: 'd' },
+ { a: 'b', c: 'd' }
+ ];
+
+ var recordsToDelete = [
+ { a: 'b', c: 'd' },
+ { a: 'b', c: 'd' },
+ { a: 'b', c: 'd' }
+ ];
+
+ beforeEach(function() {
+ zone.getRecords = function(recordType, callback) {
+ callback(null, recordsToDelete);
+ };
+ });
+
+ it('should create a change', function(done) {
+ zone.createChange = function(options, callback) {
+ assert.strictEqual(options.add, recordsToCreate);
+ assert.strictEqual(options.delete, recordsToDelete);
+
+ callback();
+ };
+
+ zone.replaceRecords('a', recordsToCreate, done);
+ });
+ });
+ });
+
+ describe('deleteRecordsByType_', function() {
+ it('should get records', function(done) {
+ var recordType = 'ns';
+
+ zone.getRecords = function(recordType_) {
+ assert.strictEqual(recordType_, recordType);
+ done();
+ };
+
+ zone.deleteRecordsByType_(recordType, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+
+ beforeEach(function() {
+ zone.getRecords = function(recordType, callback) {
+ callback(error);
+ };
+ });
+
+ it('should execute callback with error', function(done) {
+ zone.deleteRecordsByType_('a', function(err) {
+ assert.strictEqual(err, error);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var recordsToDelete = [
+ { a: 'b', c: 'd' },
+ { a: 'b', c: 'd' },
+ { a: 'b', c: 'd' }
+ ];
+
+ beforeEach(function() {
+ zone.getRecords = function(recordType, callback) {
+ callback(null, recordsToDelete);
+ };
+ });
+
+ it('should execute callback if no records matched', function(done) {
+ zone.getRecords = function(recordType, callback) {
+ callback(null, []);
+ };
+
+ zone.deleteRecordsByType_('a', done);
+ });
+
+ it('should delete records', function(done) {
+ zone.deleteRecords = function(records, callback) {
+ assert.strictEqual(records, recordsToDelete);
+
+ callback();
+ };
+
+ zone.deleteRecordsByType_('a', done);
+ });
+ });
+ });
+});