diff --git a/package-lock.json b/package-lock.json index e3c86edb2..22501abcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -829,6 +829,35 @@ } } }, + "@sinonjs/commons": { + "version": "1.3.0", + "resolved": "https://eis.jfrog.io/eis/api/npm/npm/@sinonjs/commons/-/commons-1.3.0.tgz", + "integrity": "sha1-UKJ1QBa28wqZTO2m2aCow2rdqEk=", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.1.0", + "resolved": "https://eis.jfrog.io/eis/api/npm/npm/@sinonjs/formatio/-/formatio-3.1.0.tgz", + "integrity": "sha1-asnR6xghmE2ExJlnJuRdFkbYzOU=", + "dev": true, + "requires": { + "@sinonjs/samsam": "^2 || ^3" + } + }, + "@sinonjs/samsam": { + "version": "3.0.2", + "resolved": "https://eis.jfrog.io/eis/api/npm/npm/@sinonjs/samsam/-/samsam-3.0.2.tgz", + "integrity": "sha1-ME+zO9VYWgst+KTIAfy0f6hNjkM=", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.0.2", + "array-from": "^2.1.1", + "lodash.get": "^4.4.2" + } + }, "JSONStream": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz", @@ -997,6 +1026,12 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "array-from": { + "version": "2.1.1", + "resolved": "https://eis.jfrog.io/eis/api/npm/npm/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, "array-uniq": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", @@ -1208,6 +1243,11 @@ "tweetnacl": "^0.14.3" } }, + "bindings": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.1.tgz", + "integrity": "sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew==" + }, "bl": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", @@ -2460,6 +2500,12 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, + "diff": { + "version": "3.5.0", + "resolved": "https://eis.jfrog.io/eis/api/npm/npm/diff/-/diff-3.5.0.tgz", + "integrity": "sha1-gAwN0eCov7yVg1wgKtIg/jF+WhI=", + "dev": true + }, "dijkstrajs": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.1.tgz", @@ -3269,7 +3315,7 @@ }, "fs-extra": { "version": "0.6.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.6.4.tgz", + "resolved": "http://registry.npmjs.org/fs-extra/-/fs-extra-0.6.4.tgz", "integrity": "sha1-9G8MdbeEH40gCzNIzU1pHVoJnRU=", "dev": true, "requires": { @@ -4293,6 +4339,12 @@ } } }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://eis.jfrog.io/eis/api/npm/npm/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -4510,6 +4562,21 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "isemail": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz", + "integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==", + "requires": { + "punycode": "2.x.x" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4602,6 +4669,23 @@ "resolved": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz", "integrity": "sha1-lR9he9WhlCIO0GcLm4KowOxcYiQ=" }, + "joi": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz", + "integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==", + "requires": { + "hoek": "5.x.x", + "isemail": "3.x.x", + "topo": "3.x.x" + }, + "dependencies": { + "hoek": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", + "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" + } + } + }, "js-base64": { "version": "2.4.5", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.5.tgz", @@ -4719,6 +4803,12 @@ } } }, + "just-extend": { + "version": "4.0.2", + "resolved": "https://eis.jfrog.io/eis/api/npm/npm/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha1-8/R/ffyg+YnFVBCn68iFSwcQivw=", + "dev": true + }, "jwa": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz", @@ -4887,6 +4977,12 @@ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://eis.jfrog.io/eis/api/npm/npm/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, "lodash.groupby": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", @@ -4913,6 +5009,12 @@ "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.5.tgz", "integrity": "sha1-euTsJXMC/XkNVXyxDJcQDYV7AFY=" }, + "lolex": { + "version": "3.0.0", + "resolved": "https://eis.jfrog.io/eis/api/npm/npm/lolex/-/lolex-3.0.0.tgz", + "integrity": "sha1-8E7hqKoT9g8avXsOj0IT7HLsGT4=", + "dev": true + }, "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -5295,7 +5397,7 @@ "dependencies": { "underscore": { "version": "1.8.3", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "resolved": "http://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=", "dev": true } @@ -5532,6 +5634,14 @@ } } }, + "namecheap": { + "version": "github:joshuakarjala/node-namecheap#464a9528b7ded3ee2520c2688bc98cbffb08e603", + "from": "github:joshuakarjala/node-namecheap#464a952", + "requires": { + "request": "*", + "xml2json": "*" + } + }, "nan": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", @@ -5549,6 +5659,42 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, + "nise": { + "version": "1.4.8", + "resolved": "https://eis.jfrog.io/eis/api/npm/npm/nise/-/nise-1.4.8.tgz", + "integrity": "sha1-zpHDHobPmyxMrEnX/Nf1Z3m/1rA=", + "dev": true, + "requires": { + "@sinonjs/formatio": "^3.1.0", + "just-extend": "^4.0.2", + "lolex": "^2.3.2", + "path-to-regexp": "^1.7.0", + "text-encoding": "^0.6.4" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://eis.jfrog.io/eis/api/npm/npm/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "lolex": { + "version": "2.7.5", + "resolved": "https://eis.jfrog.io/eis/api/npm/npm/lolex/-/lolex-2.7.5.tgz", + "integrity": "sha1-ETAB1Wv8fgLVbjYpHMXEE9GqBzM=", + "dev": true + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://eis.jfrog.io/eis/api/npm/npm/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, "nock": { "version": "9.3.2", "resolved": "https://registry.npmjs.org/nock/-/nock-9.3.2.tgz", @@ -5583,6 +5729,22 @@ } } }, + "node-expat": { + "version": "2.3.17", + "resolved": "https://registry.npmjs.org/node-expat/-/node-expat-2.3.17.tgz", + "integrity": "sha512-mNTxY/GMiZGayqdKZXyf6lJR7OM1JqyL0EISjE4XF7Ov7+X4zJjmlnfxCi6Gml90IEOyiYBcyJg9MHDsDp6YHw==", + "requires": { + "bindings": "^1.2.1", + "nan": "^2.10.0" + }, + "dependencies": { + "nan": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz", + "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==" + } + } + }, "node-forge": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", @@ -7997,6 +8159,32 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, + "sinon": { + "version": "7.2.2", + "resolved": "https://eis.jfrog.io/eis/api/npm/npm/sinon/-/sinon-7.2.2.tgz", + "integrity": "sha1-OI7KvUL6k8WSv8cdNacIlNWgygc=", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.2.0", + "@sinonjs/formatio": "^3.1.0", + "@sinonjs/samsam": "^3.0.2", + "diff": "^3.5.0", + "lolex": "^3.0.0", + "nise": "^1.4.7", + "supports-color": "^5.5.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://eis.jfrog.io/eis/api/npm/npm/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "sntp": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", @@ -8614,6 +8802,12 @@ } } }, + "text-encoding": { + "version": "0.6.4", + "resolved": "https://eis.jfrog.io/eis/api/npm/npm/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", + "dev": true + }, "through2": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", @@ -8636,6 +8830,21 @@ "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" }, + "topo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz", + "integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==", + "requires": { + "hoek": "6.x.x" + }, + "dependencies": { + "hoek": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.2.tgz", + "integrity": "sha512-6qhh/wahGYZHFSFw12tBbJw5fsAhhwrrG/y3Cs0YMTv2WzMnL0oLPnQJjv1QJvEfylRSOFuP+xCu+tdx0tD16Q==" + } + } + }, "tough-cookie": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", @@ -8952,6 +9161,23 @@ } } }, + "xml2json": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/xml2json/-/xml2json-0.11.2.tgz", + "integrity": "sha512-ZJpHpPOL0T5lOvAHMnWm59iQOPqNtam5t2TMUllWZ1k5Wm8L5YyvQnkeaVnRKCvDwY5EumqXWyOjjMdQVz272A==", + "requires": { + "hoek": "^4.2.1", + "joi": "^13.1.2", + "node-expat": "^2.3.15" + }, + "dependencies": { + "hoek": { + "version": "4.2.1", + "resolved": "http://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" + } + } + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", diff --git a/package.json b/package.json index fe384366a..232175818 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "morgan": "^1.9.0", "multiparty": "^4.1.4", "mysql": "^2.15.0", + "namecheap": "github:joshuakarjala/node-namecheap#464a952", "nodemailer": "^4.6.5", "nodemailer-smtp-transport": "^2.7.4", "oauth2orize": "^1.11.0", @@ -87,7 +88,8 @@ "mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git", "nock": "^9.0.14", "node-sass": "^4.6.1", - "recursive-readdir": "^2.2.2" + "recursive-readdir": "^2.2.2", + "sinon": "^7.2.2" }, "scripts": { "migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up", diff --git a/src/dns/namecheap.js b/src/dns/namecheap.js new file mode 100644 index 000000000..0ac98115d --- /dev/null +++ b/src/dns/namecheap.js @@ -0,0 +1,261 @@ +'use strict'; + +exports = module.exports = { + upsert: upsert, + get: get, + del: del, + waitForDns: require('./waitfordns.js'), + verifyDnsConfig: verifyDnsConfig +}; + +var assert = require('assert'), + debug = require('debug')('box:dns/namecheap'), + dns = require('../native-dns.js'), + DomainsError = require('../domains.js').DomainsError, + Namecheap = require('namecheap'), + sysinfo = require('../sysinfo.js'), + util = require('util'); + +var namecheap; + +function formatError(response) { + return util.format('NameCheap DNS error [%s] %j', response.code, response.message); +} + +// The keys that NameCheap returns us and the keys we need to provide it differ, so we need to map them properly +function mapHosts(hosts) { + for (var i = 0; i < hosts.length; i++) { + let curHost = hosts[i]; + if (curHost.Name && !curHost.HostName) { + curHost.HostName = curHost.Name; + delete curHost.Name; + } + + if (curHost.Type && !curHost.RecordType) { + curHost.RecordType = curHost.Type; + delete curHost.Type; + } + } + + return hosts; +} + +function getInternal(dnsConfig, zoneName, subdomain, type, callback) { + assert.strictEqual(typeof dnsConfig, 'object'); + assert.strictEqual(typeof zoneName, 'string'); + assert.strictEqual(typeof subdomain, 'string'); + assert.strictEqual(typeof type, 'string'); + assert.strictEqual(typeof callback, 'function'); + + if (!namecheap) { + // We haven't initialized our namecheap instance yet + // Note that for all NameCheap calls to go through properly, the public IP returned by the getPublicIp method below must be whitelisted on NameCheap's API dashboard + namecheap = new Namecheap(dnsConfig.username, dnsConfig.apiKey, sysinfo.getPublicIp()); + namecheap.setUsername(dnsConfig.username); + } + + namecheap.domains.dns.getHosts(zoneName, function (err, res) { + if (err) { + return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(err))); + } + + debug('entire getInternal response: %j', res); + + return callback(null, res['DomainDNSGetHostsResult']['host']); + }); +} + +function setInternal(zoneName, hosts, callback) { + let mappedHosts = mapHosts(hosts); + namecheap.domains.dns.setHosts(zoneName, mappedHosts, function (err, res) { + if (err) { + return callback(err); + } + + return callback(null, res); + }); +} + +function upsert(dnsConfig, zoneName, subdomain, type, values, callback) { + assert.strictEqual(typeof dnsConfig, 'object'); + assert.strictEqual(typeof zoneName, 'string'); + assert.strictEqual(typeof subdomain, 'string'); + assert.strictEqual(typeof type, 'string'); + assert(util.isArray(values)); + assert.strictEqual(typeof callback, 'function'); + + subdomain = subdomain || '@'; + + debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values); + + getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) { + if (error) { + return callback(error); + } + + // Array to keep track of records that need to be inserted + let toInsert = []; + + for (var i = 0; i < values.length; i++) { + let curValue = values[i]; + let wasUpdate = false; + + for (var j = 0; j < result.length; j++) { + let curHost = result[j]; + + if (curHost.Type === type && curHost.Name === subdomain) { + // Updating an already existing host + wasUpdate = true; + if (type === "MX") { + curHost.MXPref = curValue.split(' ')[0]; + curHost.Address = curValue.split(' ')[1]; + } else { + curHost.Address = curValue; + } + } + } + + // We don't have this host at all yet, let's push to toInsert array + if (!wasUpdate) { + let newRecord = { + RecordType: type, + HostName: subdomain, + Address: curValue + }; + + // Special case for MX records + if (type === "MX") { + newRecord.MXPref = curValue.split(' ')[0]; + newRecord.Address = curValue.split(' ')[1]; + } + + toInsert.push(newRecord); + + } + } + + let toUpsert = result.concat(toInsert); + + setInternal(zoneName, toUpsert, function (err, result) { + if (err) { + return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(err))); + } else { + return callback(null); + } + }); + }); +} + +function get(dnsConfig, zoneName, subdomain, type, callback) { + assert.strictEqual(typeof dnsConfig, 'object'); + assert.strictEqual(typeof zoneName, 'string'); + assert.strictEqual(typeof subdomain, 'string'); + assert.strictEqual(typeof type, 'string'); + assert.strictEqual(typeof callback, 'function'); + + subdomain = subdomain || '@'; + + getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) { + if (error) return callback(error); + + // We need to filter hosts to ones with this subdomain and type + let actualHosts = result.filter((host) => host.Type === type && host.Name === subdomain); + + // We only return the value string + var tmp = actualHosts.map(function (record) { return record.Address; }); + + debug('get: %j', tmp); + + return callback(null, tmp); + }); +} + +function del(dnsConfig, zoneName, subdomain, type, values, callback) { + assert.strictEqual(typeof dnsConfig, 'object'); + assert.strictEqual(typeof zoneName, 'string'); + assert.strictEqual(typeof subdomain, 'string'); + assert.strictEqual(typeof type, 'string'); + assert(util.isArray(values)); + assert.strictEqual(typeof callback, 'function'); + + debug('del: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values); + + getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) { + if (error) return callback(error); + + if (result.length === 0) return callback(null); + + let removed = false; + + for (var i = 0; i < values.length; i++) { + let curValue = values[i]; + + for (var j = 0; j < result.length; j++) { + let curHost = result[i]; + + if (curHost.Type === type && curHost.Name === subdomain && curHost.Address === curValue) { + removed = true; + + result.splice(i, 1); // Remove element from result array + } + } + } + + if (removed) { + // Only set hosts if we actually removed a host + setInternal(zoneName, result, function (err, result) { + if (err) { + return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(err))); + } else { + return callback(null); + } + }); + } else { + return callback(null); + } + }); +} + +function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) { + assert.strictEqual(typeof dnsConfig, 'object'); + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof zoneName, 'string'); + assert.strictEqual(typeof ip, 'string'); + assert.strictEqual(typeof callback, 'function'); + + if (!dnsConfig.username || typeof dnsConfig.username !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'username must be a non-empty string')); + if (!dnsConfig.apiKey || typeof dnsConfig.apiKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiKey must be a non-empty string')); + + var credentials = { + username: dnsConfig.username, + apKey: dnsConfig.apiKey + }; + + if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here + + dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) { + if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain')); + if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers')); + + if (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('.registrar-servers.com') === -1) { + debug('verifyDnsConfig: %j does not contains NC NS', nameservers); + return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to NameCheap')); + } + + const testSubdomain = 'cloudrontestdns'; + + upsert(dnsConfig, zoneName, testSubdomain, 'A', [ip], function (error, changeId) { + if (error) return callback(error); + + debug('verifyDnsConfig: Test A record added with change id %s', changeId); + + del(dnsConfig, zoneName, testSubdomain, 'A', [ip], function (error) { + if (error) return callback(error); + + debug('verifyDnsConfig: Test A record removed again'); + + callback(null, dnsConfig); + }); + }); + }); +} diff --git a/src/domains.js b/src/domains.js index ec1c3003c..8323e28e1 100644 --- a/src/domains.js +++ b/src/domains.js @@ -91,6 +91,7 @@ function api(provider) { case 'gandi': return require('./dns/gandi.js'); case 'godaddy': return require('./dns/godaddy.js'); case 'namecom': return require('./dns/namecom.js'); + case 'namecheap': return require('./dns/namecheap.js'); case 'noop': return require('./dns/noop.js'); case 'manual': return require('./dns/manual.js'); case 'wildcard': return require('./dns/wildcard.js'); diff --git a/src/test/dns-test.js b/src/test/dns-test.js index 503ede231..2814929dd 100644 --- a/src/test/dns-test.js +++ b/src/test/dns-test.js @@ -13,7 +13,9 @@ var async = require('async'), database = require('../database.js'), domains = require('../domains.js'), expect = require('expect.js'), + namecheap = require('namecheap'), nock = require('nock'), + sinon = require('sinon'), util = require('util'); var DOMAIN_0 = { @@ -55,7 +57,7 @@ describe('dns provider', function () { }); it('upsert succeeds', function (done) { - domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) { + domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) { expect(error).to.eql(null); done(); @@ -73,7 +75,7 @@ describe('dns provider', function () { }); it('del succeeds', function (done) { - domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) { + domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) { expect(error).to.eql(null); done(); @@ -114,7 +116,7 @@ describe('dns provider', function () { .post('/v2/domains/' + DOMAIN_0.zoneName + '/records') .reply(201, { domain_record: DOMAIN_RECORD_0 }); - domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) { + domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) { expect(error).to.eql(null); expect(req1.isDone()).to.be.ok(); expect(req2.isDone()).to.be.ok(); @@ -158,12 +160,12 @@ describe('dns provider', function () { var req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) .get('/v2/domains/' + DOMAIN_0.zoneName + '/records') - .reply(200, { domain_records: [ DOMAIN_RECORD_0, DOMAIN_RECORD_1 ] }); + .reply(200, { domain_records: [DOMAIN_RECORD_0, DOMAIN_RECORD_1] }); var req2 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) .put('/v2/domains/' + DOMAIN_0.zoneName + '/records/' + DOMAIN_RECORD_1.id) .reply(200, { domain_record: DOMAIN_RECORD_1_NEW }); - domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ DOMAIN_RECORD_1_NEW.data ], function (error) { + domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [DOMAIN_RECORD_1_NEW.data], function (error) { expect(error).to.eql(null); expect(req1.isDone()).to.be.ok(); expect(req2.isDone()).to.be.ok(); @@ -237,7 +239,7 @@ describe('dns provider', function () { var req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) .get('/v2/domains/' + DOMAIN_0.zoneName + '/records') - .reply(200, { domain_records: [ DOMAIN_RECORD_0, DOMAIN_RECORD_1, DOMAIN_RECORD_2 ] }); + .reply(200, { domain_records: [DOMAIN_RECORD_0, DOMAIN_RECORD_1, DOMAIN_RECORD_2] }); var req2 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) .put('/v2/domains/' + DOMAIN_0.zoneName + '/records/' + DOMAIN_RECORD_1.id) .reply(200, { domain_record: DOMAIN_RECORD_1_NEW }); @@ -248,7 +250,7 @@ describe('dns provider', function () { .post('/v2/domains/' + DOMAIN_0.zoneName + '/records') .reply(201, { domain_record: DOMAIN_RECORD_2_NEW }); - domains.upsertDnsRecords('', DOMAIN_0.domain, 'TXT', [ DOMAIN_RECORD_2_NEW.data, DOMAIN_RECORD_1_NEW.data, DOMAIN_RECORD_3_NEW.data ], function (error) { + domains.upsertDnsRecords('', DOMAIN_0.domain, 'TXT', [DOMAIN_RECORD_2_NEW.data, DOMAIN_RECORD_1_NEW.data, DOMAIN_RECORD_3_NEW.data], function (error) { expect(error).to.eql(null); expect(req1.isDone()).to.be.ok(); expect(req2.isDone()).to.be.ok(); @@ -284,7 +286,7 @@ describe('dns provider', function () { var req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) .get('/v2/domains/' + DOMAIN_0.zoneName + '/records') - .reply(200, { domain_records: [ DOMAIN_RECORD_0, DOMAIN_RECORD_1 ] }); + .reply(200, { domain_records: [DOMAIN_RECORD_0, DOMAIN_RECORD_1] }); domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) { expect(error).to.eql(null); @@ -322,7 +324,7 @@ describe('dns provider', function () { var req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) .get('/v2/domains/' + DOMAIN_0.zoneName + '/records') - .reply(200, { domain_records: [ DOMAIN_RECORD_0, DOMAIN_RECORD_1 ] }); + .reply(200, { domain_records: [DOMAIN_RECORD_0, DOMAIN_RECORD_1] }); var req2 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; }) .delete('/v2/domains/' + DOMAIN_0.zoneName + '/records/' + DOMAIN_RECORD_1.id) .reply(204, {}); @@ -361,9 +363,9 @@ describe('dns provider', function () { var req1 = nock(GODADDY_API) .put('/' + DOMAIN_0.zoneName + '/records/A/test', DOMAIN_RECORD_0) - .reply(200, { }); + .reply(200, {}); - domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) { + domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) { expect(error).to.eql(null); expect(req1.isDone()).to.be.ok(); @@ -413,7 +415,7 @@ describe('dns provider', function () { var req2 = nock(GODADDY_API) .put('/' + DOMAIN_0.zoneName + '/records/A/test', DOMAIN_RECORD_1) - .reply(200, { }); + .reply(200, {}); domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) { expect(error).to.eql(null); @@ -443,14 +445,14 @@ describe('dns provider', function () { var DOMAIN_RECORD_0 = { 'rrset_ttl': 300, - 'rrset_values': [ '1.2.3.4' ] + 'rrset_values': ['1.2.3.4'] }; var req1 = nock(GANDI_API) .put('/domains/' + DOMAIN_0.zoneName + '/records/test/A', DOMAIN_RECORD_0) .reply(201, { message: 'Zone Record Created' }); - domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) { + domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) { expect(error).to.eql(null); expect(req1.isDone()).to.be.ok(); @@ -465,7 +467,7 @@ describe('dns provider', function () { 'rrset_type': 'A', 'rrset_ttl': 600, 'rrset_name': 'test', - 'rrset_values': [ '1.2.3.4' ] + 'rrset_values': ['1.2.3.4'] }; var req1 = nock(GANDI_API) @@ -488,7 +490,7 @@ describe('dns provider', function () { var req2 = nock(GANDI_API) .delete('/domains/' + DOMAIN_0.zoneName + '/records/test/A') - .reply(204, { }); + .reply(204, {}); domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) { expect(error).to.eql(null); @@ -531,7 +533,7 @@ describe('dns provider', function () { .post(`/domains/${DOMAIN_0.zoneName}/records`, DOMAIN_RECORD_0) .reply(200, {}); - domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) { + domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) { expect(error).to.eql(null); expect(req1.isDone()).to.be.ok(); expect(req2.isDone()).to.be.ok(); @@ -552,7 +554,7 @@ describe('dns provider', function () { var req1 = nock(NAMECOM_API) .get(`/domains/${DOMAIN_0.zoneName}/records`) - .reply(200, { records: [ DOMAIN_RECORD_0 ] }); + .reply(200, { records: [DOMAIN_RECORD_0] }); domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) { expect(error).to.eql(null); @@ -578,7 +580,7 @@ describe('dns provider', function () { var req1 = nock(NAMECOM_API) .get(`/domains/${DOMAIN_0.zoneName}/records`) - .reply(200, { records: [ DOMAIN_RECORD_0 ] }); + .reply(200, { records: [DOMAIN_RECORD_0] }); var req2 = nock(NAMECOM_API) .delete(`/domains/${DOMAIN_0.zoneName}/records/${DOMAIN_RECORD_0.id}`) @@ -594,6 +596,603 @@ describe('dns provider', function () { }); }); + describe('namecheap', function () { + let sandbox = require('sinon').createSandbox(); + + let username = 'namecheapuser'; + let apiKey = 'API_KEY'; + + before(function (done) { + DOMAIN_0.provider = 'namecheap'; + DOMAIN_0.config = { + username, + apiKey + }; + + domains.update(DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE, done); + }); + + after(function() { + sandbox.restore(); + }); + + it('upsert non-existing record succeeds', function (done) { + + let getHostsReturn = { + "Type": "namecheap.domains.dns.getHosts", + "DomainDNSGetHostsResult": { + "Domain": "example-dns-test.com", + "EmailType": "FWD", + "IsUsingOurDNS": "true", + "host": [ + { + "HostId": "614433", + "Name": "www", + "Type": "CNAME", + "Address": "parkingpage.namecheap.com.", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "", + "FriendlyName": "CNAME Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + }, + { + "HostId": "614432", + "Name": "@", + "Type": "URL", + "Address": "http://www.example-dns-test.com/", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "URL Forwarding", + "FriendlyName": "URL Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + } + ] + } + }; + + let setInternalExpect = [ + { + "HostId": "614433", + "HostName": "www", + "RecordType": "CNAME", + "Address": "parkingpage.namecheap.com.", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "", + "FriendlyName": "CNAME Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + }, + { + "HostId": "614432", + "HostName": "@", + "RecordType": "URL", + "Address": "http://www.example-dns-test.com/", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "URL Forwarding", + "FriendlyName": "URL Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + }, + { + RecordType: 'A', + HostName: 'test', + Address: '1.2.3.4' + } + ]; + + let getHostsFake = sinon.fake.yields(null, getHostsReturn); + let setHostsFake = sinon.fake.yields(null, true); + let mockObj = { + dns: { + getHosts: getHostsFake, + setHosts: setHostsFake + } + }; + + sandbox.stub(namecheap.prototype, "domains").value(mockObj); + + domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + + expect(setHostsFake.calledOnce).to.eql(true); + expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true); + + done(); + }); + }); + + it('upsert multiple non-existing records succeeds', function (done) { + + let getHostsReturn = { + "Type": "namecheap.domains.dns.getHosts", + "DomainDNSGetHostsResult": { + "Domain": "example-dns-test.com", + "EmailType": "FWD", + "IsUsingOurDNS": "true", + "host": [ + { + "HostId": "614433", + "Name": "www", + "Type": "CNAME", + "Address": "parkingpage.namecheap.com.", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "", + "FriendlyName": "CNAME Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + }, + { + "HostId": "614432", + "Name": "@", + "Type": "URL", + "Address": "http://www.example-dns-test.com/", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "URL Forwarding", + "FriendlyName": "URL Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + } + ] + } + }; + + let setInternalExpect = [ + { + "HostId": "614433", + "HostName": "www", + "RecordType": "CNAME", + "Address": "parkingpage.namecheap.com.", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "", + "FriendlyName": "CNAME Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + }, + { + "HostId": "614432", + "HostName": "@", + "RecordType": "URL", + "Address": "http://www.example-dns-test.com/", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "URL Forwarding", + "FriendlyName": "URL Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + }, + { + RecordType: 'TXT', + HostName: 'test', + Address: '1.2.3.4' + }, + { + RecordType: 'TXT', + HostName: 'test', + Address: '2.3.4.5' + }, + { + RecordType: 'TXT', + HostName: 'test', + Address: '3.4.5.6' + } + ]; + + let getHostsFake = sinon.fake.yields(null, getHostsReturn); + let setHostsFake = sinon.fake.yields(null, true); + let mockObj = { + dns: { + getHosts: getHostsFake, + setHosts: setHostsFake + } + }; + + sandbox.stub(namecheap.prototype, "domains").value(mockObj); + + domains.upsertDnsRecords('test', DOMAIN_0.domain, 'TXT', ['1.2.3.4', '2.3.4.5', '3.4.5.6'], function (error) { + expect(error).to.eql(null); + + expect(setHostsFake.calledOnce).to.eql(true); + expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true); + + done(); + }); + }); + + it('upsert multiple non-existing MX records succeeds', function (done) { + + let getHostsReturn = { + "Type": "namecheap.domains.dns.getHosts", + "DomainDNSGetHostsResult": { + "Domain": "example-dns-test.com", + "EmailType": "FWD", + "IsUsingOurDNS": "true", + "host": [ + { + "HostId": "614433", + "Name": "www", + "Type": "CNAME", + "Address": "parkingpage.namecheap.com.", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "", + "FriendlyName": "CNAME Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + }, + { + "HostId": "614432", + "Name": "@", + "Type": "URL", + "Address": "http://www.example-dns-test.com/", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "URL Forwarding", + "FriendlyName": "URL Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + } + ] + } + }; + + let setInternalExpect = [ + { + "HostId": "614433", + "HostName": "www", + "RecordType": "CNAME", + "Address": "parkingpage.namecheap.com.", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "", + "FriendlyName": "CNAME Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + }, + { + "HostId": "614432", + "HostName": "@", + "RecordType": "URL", + "Address": "http://www.example-dns-test.com/", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "URL Forwarding", + "FriendlyName": "URL Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + }, + { + RecordType: 'MX', + HostName: 'test', + Address: '1.2.3.4', + MXPref: '10' + }, + { + RecordType: 'MX', + HostName: 'test', + Address: '2.3.4.5', + MXPref: '20' + }, + { + RecordType: 'MX', + HostName: 'test', + Address: '3.4.5.6', + MXPref: '30' + } + ]; + + let getHostsFake = sinon.fake.yields(null, getHostsReturn); + let setHostsFake = sinon.fake.yields(null, true); + let mockObj = { + dns: { + getHosts: getHostsFake, + setHosts: setHostsFake + } + }; + + sandbox.stub(namecheap.prototype, "domains").value(mockObj); + + domains.upsertDnsRecords('test', DOMAIN_0.domain, 'MX', ['10 1.2.3.4', '20 2.3.4.5', '30 3.4.5.6'], function (error) { + expect(error).to.eql(null); + + expect(setHostsFake.calledOnce).to.eql(true); + expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true); + + done(); + }); + }); + + it('upsert existing record succeeds', function (done) { + + let getHostsReturn = { + "Type": "namecheap.domains.dns.getHosts", + "DomainDNSGetHostsResult": { + "Domain": "example-dns-test.com", + "EmailType": "FWD", + "IsUsingOurDNS": "true", + "host": [ + { + "HostId": "614433", + "Name": "www", + "Type": "CNAME", + "Address": DOMAIN_0.domain, + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "", + "FriendlyName": "CNAME Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + }, + { + "HostId": "614432", + "Name": "@", + "Type": "URL", + "Address": "http://www.example-dns-test.com/", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "URL Forwarding", + "FriendlyName": "URL Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + } + ] + } + }; + + let setInternalExpect = [ + { + "HostId": "614433", + "HostName": "www", + "RecordType": "CNAME", + "Address": "1.2.3.4", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "", + "FriendlyName": "CNAME Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + }, + { + "HostId": "614432", + "HostName": "@", + "RecordType": "URL", + "Address": "http://www.example-dns-test.com/", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "URL Forwarding", + "FriendlyName": "URL Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + } + ]; + + let getHostsFake = sinon.fake.yields(null, getHostsReturn); + let setHostsFake = sinon.fake.yields(null, true); + let mockObj = { + dns: { + getHosts: getHostsFake, + setHosts: setHostsFake + } + }; + + sandbox.stub(namecheap.prototype, "domains").value(mockObj); + + domains.upsertDnsRecords('www', DOMAIN_0.domain, 'CNAME', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + + expect(setHostsFake.calledOnce).to.eql(true); + expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true); + + done(); + }); + }); + + it('get succeeds', function(done) { + let getHostsReturn = { + "Type": "namecheap.domains.dns.getHosts", + "DomainDNSGetHostsResult": { + "Domain": "example-dns-test.com", + "EmailType": "FWD", + "IsUsingOurDNS": "true", + "host": [ + { + "HostId": "614433", + "Name": "www", + "Type": "CNAME", + "Address": "1.2.3.4", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "", + "FriendlyName": "CNAME Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + }, + { + "HostId": "614432", + "Name": "test", + "Type": "A", + "Address": "1.2.3.4", + "MXPref": "10", + "TTL": "1800", + "FriendlyName": "A Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + }, + { + "HostId": "614431", + "Name": "test", + "Type": "A", + "Address": "2.3.4.5", + "MXPref": "10", + "TTL": "1800", + "FriendlyName": "A Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + } + ] + } + }; + + let getHostsFake = sinon.fake.yields(null, getHostsReturn); + let mockObj = { + dns: { + getHosts: getHostsFake + } + }; + + sandbox.stub(namecheap.prototype, "domains").value(mockObj); + + domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) { + expect(error).to.eql(null); + + expect(result).to.be.an(Array); + expect(result.length).to.eql(2); + expect(getHostsFake.calledOnce).to.eql(true); + expect(result).to.eql(['1.2.3.4', '2.3.4.5']); + + done(); + }); + }); + + it('del succeeds', function (done) { + + let getHostsReturn = { + "Type": "namecheap.domains.dns.getHosts", + "DomainDNSGetHostsResult": { + "Domain": "example-dns-test.com", + "EmailType": "FWD", + "IsUsingOurDNS": "true", + "host": [ + { + "HostId": "614433", + "Name": "www", + "Type": "CNAME", + "Address": "1.2.3.4", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "", + "FriendlyName": "CNAME Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + }, + { + "HostId": "614432", + "Name": "@", + "Type": "URL", + "Address": "http://www.example-dns-test.com/", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "URL Forwarding", + "FriendlyName": "URL Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + } + ] + } + }; + + let setInternalExpect = [ + { + "HostId": "614432", + "HostName": "@", + "RecordType": "URL", + "Address": "http://www.example-dns-test.com/", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "URL Forwarding", + "FriendlyName": "URL Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + } + ]; + + let getHostsFake = sinon.fake.yields(null, getHostsReturn); + let setHostsFake = sinon.fake.yields(null, true); + let mockObj = { + dns: { + getHosts: getHostsFake, + setHosts: setHostsFake + } + }; + + sandbox.stub(namecheap.prototype, "domains").value(mockObj); + + domains.removeDnsRecords('www', DOMAIN_0.domain, 'CNAME', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + + expect(setHostsFake.calledOnce).to.eql(true); + expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true); + + done(); + }); + }); + + it('del succeeds w/ non-present host', function (done) { + + let getHostsReturn = { + "Type": "namecheap.domains.dns.getHosts", + "DomainDNSGetHostsResult": { + "Domain": "example-dns-test.com", + "EmailType": "FWD", + "IsUsingOurDNS": "true", + "host": [ + { + "HostId": "614433", + "Name": "www", + "Type": "CNAME", + "Address": "parkingpage.namecheap.com.", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "", + "FriendlyName": "CNAME Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + }, + { + "HostId": "614432", + "Name": "@", + "Type": "URL", + "Address": "http://www.example-dns-test.com/", + "MXPref": "10", + "TTL": "1800", + "AssociatedAppTitle": "URL Forwarding", + "FriendlyName": "URL Record", + "IsActive": "true", + "IsDDNSEnabled": "false" + } + ] + } + }; + + let getHostsFake = sinon.fake.yields(null, getHostsReturn); + let setHostsFake = sinon.fake.yields(null, true); + let mockObj = { + dns: { + getHosts: getHostsFake, + setHosts: setHostsFake + } + }; + + sandbox.stub(namecheap.prototype, "domains").value(mockObj); + + domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) { + expect(error).to.eql(null); + + expect(setHostsFake.notCalled).to.eql(true); + + done(); + }); + }); + + }); + describe('route53', function () { // do not clear this with [] but .length = 0 so we don't loose the reference in mockery var awsAnswerQueue = []; @@ -632,12 +1231,12 @@ describe('dns provider', function () { MaxItems: '100' }; - function mockery (queue) { - return function(options, callback) { + function mockery(queue) { + return function (options, callback) { expect(options).to.be.an(Object); var elem = queue.shift(); - if (!util.isArray(elem)) throw(new Error('Mock answer required')); + if (!util.isArray(elem)) throw (new Error('Mock answer required')); // if no callback passed, return a req object with send(); if (typeof callback !== 'function') { @@ -690,7 +1289,7 @@ describe('dns provider', function () { } }]); - domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) { + domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) { expect(error).to.eql(null); expect(awsAnswerQueue.length).to.eql(0); @@ -708,7 +1307,7 @@ describe('dns provider', function () { } }]); - domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) { + domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) { expect(error).to.eql(null); expect(awsAnswerQueue.length).to.eql(0); @@ -726,7 +1325,7 @@ describe('dns provider', function () { } }]); - domains.upsertDnsRecords('', DOMAIN_0.domain, 'TXT', [ 'first', 'second', 'third' ], function (error) { + domains.upsertDnsRecords('', DOMAIN_0.domain, 'TXT', ['first', 'second', 'third'], function (error) { expect(error).to.eql(null); expect(awsAnswerQueue.length).to.eql(0); @@ -791,12 +1390,12 @@ describe('dns provider', function () { } }; - function mockery (queue) { - return function() { + function mockery(queue) { + return function () { var callback = arguments[--arguments.length]; var elem = queue.shift(); - if (!util.isArray(elem)) throw(new Error('Mock answer required')); + if (!util.isArray(elem)) throw (new Error('Mock answer required')); // if no callback passed, return a req object with send(); if (typeof callback !== 'function') { @@ -823,7 +1422,7 @@ describe('dns provider', function () { zone.deleteRecords = mockery(recordQueue || zoneQueue); return zone; } - HOSTED_ZONES = [ fakeZone(DOMAIN_0.domain), fakeZone('cloudron.us') ]; + HOSTED_ZONES = [fakeZone(DOMAIN_0.domain), fakeZone('cloudron.us')]; _OriginalGCDNS = GCDNS.prototype.getZones; GCDNS.prototype.getZones = mockery(zoneQueue); @@ -838,10 +1437,10 @@ describe('dns provider', function () { it('upsert non-existing record succeeds', function (done) { zoneQueue.push([null, HOSTED_ZONES]); // getZone - zoneQueue.push([null, [ ]]); // getRecords - zoneQueue.push([null, {id: '1'}]); + zoneQueue.push([null, []]); // getRecords + zoneQueue.push([null, { id: '1' }]); - domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) { + domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) { expect(error).to.eql(null); expect(zoneQueue.length).to.eql(0); @@ -851,10 +1450,10 @@ describe('dns provider', function () { it('upsert existing record succeeds', function (done) { zoneQueue.push([null, HOSTED_ZONES]); - zoneQueue.push([null, [GCDNS().zone('test').record('A', {'name': 'test', data:['5.6.7.8'], ttl: 1})]]); - zoneQueue.push([null, {id: '2'}]); + zoneQueue.push([null, [GCDNS().zone('test').record('A', { 'name': 'test', data: ['5.6.7.8'], ttl: 1 })]]); + zoneQueue.push([null, { id: '2' }]); - domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) { + domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) { expect(error).to.eql(null); expect(zoneQueue.length).to.eql(0); @@ -864,10 +1463,10 @@ describe('dns provider', function () { it('upsert multiple record succeeds', function (done) { zoneQueue.push([null, HOSTED_ZONES]); - zoneQueue.push([null, [ ]]); // getRecords - zoneQueue.push([null, {id: '3'}]); + zoneQueue.push([null, []]); // getRecords + zoneQueue.push([null, { id: '3' }]); - domains.upsertDnsRecords('', DOMAIN_0.domain, 'TXT', [ 'first', 'second', 'third' ], function (error) { + domains.upsertDnsRecords('', DOMAIN_0.domain, 'TXT', ['first', 'second', 'third'], function (error) { expect(error).to.eql(null); expect(zoneQueue.length).to.eql(0); @@ -877,7 +1476,7 @@ describe('dns provider', function () { it('get succeeds', function (done) { zoneQueue.push([null, HOSTED_ZONES]); - zoneQueue.push([null, [GCDNS().zone('test').record('A', {'name': 'test', data:['1.2.3.4', '5.6.7.8'], ttl: 1})]]); + zoneQueue.push([null, [GCDNS().zone('test').record('A', { 'name': 'test', data: ['1.2.3.4', '5.6.7.8'], ttl: 1 })]]); domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) { expect(error).to.eql(null); @@ -892,8 +1491,8 @@ describe('dns provider', function () { it('del succeeds', function (done) { zoneQueue.push([null, HOSTED_ZONES]); - zoneQueue.push([null, [GCDNS().zone('test').record('A', {'name': 'test', data:['5.6.7.8'], ttl: 1})]]); - zoneQueue.push([null, {id: '5'}]); + zoneQueue.push([null, [GCDNS().zone('test').record('A', { 'name': 'test', data: ['5.6.7.8'], ttl: 1 })]]); + zoneQueue.push([null, { id: '5' }]); domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) { expect(error).to.eql(null);