From 30884601c6716b4dc09d319cf0c967233488e088 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 4 Nov 2019 21:03:21 -0700 Subject: [PATCH] v3.1.0: reduce scope of manager API --- .prettierrc | 2 +- README.md | 357 +++++++++++++-------- bin/init.js | 35 +++ bin/tmpl/manager.test.tmpl.js | 25 ++ bin/tmpl/manager.tmpl.js | 78 +++++ package-lock.json | 39 ++- package.json | 56 ++-- tester.js | 562 +++++++++++++++++++++++++--------- tests/index.js | 26 +- 9 files changed, 856 insertions(+), 324 deletions(-) create mode 100644 bin/init.js create mode 100644 bin/tmpl/manager.test.tmpl.js create mode 100644 bin/tmpl/manager.tmpl.js diff --git a/.prettierrc b/.prettierrc index 2c5ec4a..bb2db00 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,7 @@ { "bracketSpacing": true, "printWidth": 80, - "singleQuote": true, + "singleQuote": false, "tabWidth": 4, "trailingComma": "none", "useTabs": false diff --git a/README.md b/README.md index d43196c..c113dfa 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,246 @@ # [greenlock-manager-test.js](https://git.rootprojects.org/root/greenlock-manager-test.js) -A simple test suite for Greenlock manager plugins. +A simple test suite for Greenlock v3 manager plugins. # Greenlock Manager -A greenlock manager is just a set of a few callbacks to keeps track of: +A Greenlock Manager is responsible for tracking which domains +belong on a certificate, when they are scheduled for renewal, +and if they have been deleted. -- **Default settings** that apply to all sites such as - - `subscriberEmail` - - `agreeToTerms` - - `store` (the account key and ssl certificate store) -- **Site settings** such as - - `subject` (ex: example.com) - - `altnames` (ex: example.com,www.example.com) - - `renewAt` (ex: '45d') - - `challenges` (plugins for 'http-01', 'dns-01', etc) - -The **callbacks** are: - -- `set({ subject, altnames, renewAt })` to save site details -- `find({ subject, altnames, renewBefore })` which returns a list of matching sites (perhaps all sites) -- `remove({ subject })` which marks a site as deleted -- `defaults()` which either **gets** or **sets** the global configs that apply to all sites - -When do they get called? Well, whenever they need to. - -# Some Terminology - -- `subject` refers to the **primary domain** on an SSL certificate -- `altnames` refers to the list of **domain names** on the certificate (including the subject) -- `renewAt` is a pre-calculated value based on `expiresAt` or `issuedAt` on the certificate - -Those are the only values you really have to worry about. - -The rest you can make up for your own needs, or they're just opaque values you'll get from Greenlock. - -# Do you want to build a plugin? - -You can start _really_ simple: just make a file that exports a `create()` function: - -## A great first, failing plugin: - -`my-plugin.js`: +It consists of two required functions: ```js -'use strict'; - -var MyManager = module.exports; - -MyManager.create = function(options) { - console.log('The tests will make me stronger'); - return {}; -}; +set({ subject, altnames, renewAt, deletedAt }); ``` -## The test suite from heaven - -You write your test file, run it, -and then you get a play-by-play of what to do. - +```js +get({ servername }); ``` + +However, if you implement `find({ subject, servernames, renewBefore })` (optional), +you don't have to implement `get()`. + +
+Usage Details +# How to use your plugin + +The **Right Way**: + +```bash +npm install --save greenlack +npx greenlock init --manager ./path-or-npm-name.js --manager-xxxx 'sets xxxx' --manager-yyyy 'set yyyy' +``` + +That creates a `.greenlockrc`, which is essentially the same as doing this: + +```js +var Greenlock = require("greenlock"); +var greenlock = Greenlock.create({ + // ... + + manager: "./path-or-npm-name.js", + xxxx: "sets xxxx", + yyyy: "sets yyyy", + packageRoot: __dirname +}); +``` + +## Why no require? + +Okay, so you **expect** it to look like this: + +```js +var Greenlock = require("greenlock"); +var greenlock = Greenlock.create({ + // WRONG!! + manager: require("./path-or-npm-name.js").create({ + someOptionYouWant: true + }) +}); +``` + +**NOPE**! + +Greenlock is designed to so that the CLI tools, Web API, and JavaScript API +can all work interdepedently, indpendently. + +Therefore the configuration has to go into serializable JSON rather than +executable JavaScript. + +
+ +# Quick Start + +If you want to write a manager, +the best way to start is by using one of the provided templates. + +```bash npm install --save-dev greenlock-manager-test +npx greenlock-manager-init ``` -`test.js`: +It will generate a bare bones manager that passes the tests, +(skipping all optional features), and a test file: +
+manager.js ```js -'use strict'; +"use strict"; + +var Manager = module.exports; +var db = {}; + +Manager.create = function(opts) { +var manager = {}; + + // + // REQUIRED (basic issuance) + // + + // Get + manager.get = async function({ servername, wildname }) { + // Required: find the certificate with the subject of `servername` + // Optional (multi-domain certs support): find a certificate with `servername` as an altname + // Optional (wildcard support): find a certificate with `wildname` as an altname + + // { subject, altnames, renewAt, deletedAt, challenges, ... } + return db[servername] || db[wildname]; + }; + + // Set + manager.set = async function(opts) { + // { subject, altnames, renewAt, deletedAt } + // Required: updated `renewAt` and `deletedAt` for certificate matching `subject` + + var site = db[opts.subject] || {}; + db[opts.subject] = Object.assign(site, opts); + return null; + }; + + // + // Optional (Fully Automatic Renewal) + // + /* + manager.find = async function(opts) { + // { subject, servernames, altnames, renewBefore } + + return [{ subject, altnames, renewAt, deletedAt }]; + }; + //*/ + + // + // Optional (Special Remove Functionality) + // The default behavior is to set `deletedAt` + // + /* + manager.remove = async function(opts) { + return mfs.remove(opts); + }; + //*/ + + // + // Optional (special settings save) + // Implemented here because this module IS the fallback + // + /* + manager.defaults = async function(opts) { + if (opts) { + return setDefaults(opts); + } + return getDefaults(); + }; + //*/ + + // + // Optional (for common deps and/or async initialization) + // + /* + manager.init = async function(deps) { + manager.request = deps.request; + return null; + }; + //*/ + + return manager; -var Tester = require('greenlock-manager-test'); -var MyManager = require('./'); -var myConfigOptions = { - someApiTokenForMyManager: 'xxx' }; -Tester.test(MyManager, myConfigOptions) - .then(function() { - console.log('All Tests Passed'); +```` +
+
+manager.test.js +```js +"use strict"; + +var Tester = require("greenlock-manager-test"); + +var Manager = require("./manager.js"); +var config = { + configFile: "greenlock-manager-test.delete-me.json" +}; + +Tester.test(Manager, config) + .then(function(features) { + console.info("PASS"); + console.info(); + console.info("Optional Feature Support:"); + features.forEach(function(feature) { + console.info(feature.supported ? "✓ (YES)" : "✘ (NO) ", feature.description); + }); + console.info(); }) .catch(function(err) { - console.error('Oops... something bad happened:'); - console.error(err); + console.error("Oops, you broke it. Here are the details:"); + console.error(err.stack); + console.error(); + console.error("That's all I know."); }); +```` + +
+ +```bash +node manager.test.js ``` -You just follow the error messages and, which a little help from this README, -bam!, you get a working plugin. It's insane! +```txt +PASS: get({ servername, wildname }) +PASS: set({ subject }) -# The lazy, hacky way. +Optional Feature Support: +✘ (NO) Multiple Domains per Certificate +✘ (NO) Wildcard Certificates +✘ (NO) Fully Automatic Renewal +``` -If you're going to publish a module, you should pass the full test suite. +# Optional Features -If not, eh, you can be lazy. +If you're publishing a module to the community, +you should implement the full test suite (and it's not that hard). -## Bare minimum... - -At a bare minimum, you must implement `find()` to return an array of `{ subject, altnames }`. - -For example: +If you're only halfway through, you should note +which features are supported and which aren't. ```js -function find(argsToIgnore) { - return Promise.resolve([ - { subject: 'example.com', altnames: ['example.com', 'www.example.com'] } - ]); -} +find({ subject, servernames, renewBefore }); +defaults({ subscriberEmail, agreeToTerms, challenges, store, ... }); +defaults(); // as getter ``` -If that's absolutely all that you do, all of the other methods will be implemented around `greenlock-manager-fs`. +- `find()` is used to get the full list of sites, for continuous fully automatic renewal. +- `defaults()` exists so that the global config can be saved in the same place as the per-site config. +- a proper `get()` should be able to search not just primary domains, but altnames as well. + +Additionally, you're manager may need an init or a _real_ delete - rather than just using `set({ deletedAt })`: + +```js +init({ request }); +remove({ subject }); +``` + +
+Full Implementation # The Right Way™ @@ -122,7 +255,7 @@ module.exports.create = function(options) { manager.set = async function(siteConfig) { // You can see in the tests a sample of common values, // but you don't really need to worry about it. - var subject = siteConfig; + var subject = siteConfig.subject; // Cherry pick what you like for indexing / search, and JSONify the rest return mergeOrCreateSite(subject, siteConfig); @@ -130,38 +263,31 @@ module.exports.create = function(options) { // find the things you've saved before - manager.find = async function({ subject, altnames, renewBefore }) { + manager.get = async function({ servername }) { + return getSiteByAltname(servername); + } + manager.find = async function({ subject, servernames, renewBefore }) { var results = []; var gotten = {}; if (subject) { var site = await getSiteBySubject(subject); - if (site) { - results.push(site); - gotten[site.subject] = true; + if (site && site.subject === subject) { + return [site]; } } - if (altnames) { - var sites = await getSiteByAltnames(subject); - sites.forEach(function() {}); - if (site) { - if (!gotten[site.subject]) { - results.push(site); + if (severnames) { + return await Promise.all(servernames.map(function (altname) { + var site = getSiteByAltname(subject); + if (site && !gotten[site.subject]) { gotten[site.subject] = true; + return site; } - } + }); } - if (subject || altnames) { - return results; - } - - if (renewBefore) { - return getSitesThatShouldBeRenewedBefore(renewBefore); - } - - return getAllSites(); + return getSitesThatShouldBeRenewedBefore(renewBefore || Infinity); }; // delete a site config @@ -198,35 +324,4 @@ module.exports.create = function(options) { }; ``` -# How to use your plugin - -The **Right Way**: - -```js -var Greenlock = require('greenlock'); -var greenlock = Greenlock.create({ - manager: '/absolute/path/to/manager' - someOptionYouWant: true, -}); -``` - -## Why no require? - -Okay, so you **expect** it to look like this: - -```js -var Greenlock = require('greenlock'); -var greenlock = Greenlock.create({ - // WRONG!! - manager: require('./relative/path/to/manager').create({ - someOptionYouWant: true - }) -}); -``` - -**NOPE**! - -It just has to do with some plugin architecture decisions around making the configuration -serializable. - -I may go back and add the other way, but this is how it is right now. +
diff --git a/bin/init.js b/bin/init.js new file mode 100644 index 0000000..eda5cd1 --- /dev/null +++ b/bin/init.js @@ -0,0 +1,35 @@ +"use strict"; + +var fs = require("fs"); +var path = require("path"); + +function tmpl() { + var src = path.join(__dirname, "tmpl/manager.tmpl.js"); + var dst = path.join(process.cwd(), "./manager.js"); + + try { + fs.accessSync(dst); + console.warn("skip 'manager.js': already exists"); + return; + } catch (e) { + fs.writeFileSync(dst, fs.readFileSync(src, "utf8"), "utf8"); + console.info("wrote 'manager.js'"); + } +} + +function tmplTest() { + var srcTest = path.join(__dirname, "tmpl/manager.test.tmpl.js"); + var dstTest = path.join(process.cwd(), "./manager.test.js"); + + try { + fs.accessSync(dstTest); + console.warn("skip 'manager.test.js': already exists"); + return; + } catch (e) { + fs.writeFileSync(dstTest, fs.readFileSync(srcTest, "utf8"), "utf8"); + console.info("wrote 'manager.test.js'"); + } +} + +tmpl(); +tmplTest(); diff --git a/bin/tmpl/manager.test.tmpl.js b/bin/tmpl/manager.test.tmpl.js new file mode 100644 index 0000000..048b627 --- /dev/null +++ b/bin/tmpl/manager.test.tmpl.js @@ -0,0 +1,25 @@ +"use strict"; + +var Tester = require("greenlock-manager-test"); + +var Manager = require("./manager.js"); +var config = { + configFile: "greenlock-manager-test.delete-me.json" +}; + +Tester.test(Manager, config) + .then(function(features) { + console.info("PASS"); + console.info(); + console.info("Optional Feature Support:"); + features.forEach(function(feature) { + console.info(feature.supported ? "✓ (YES)" : "✘ (NO) ", feature.description); + }); + console.info(); + }) + .catch(function(err) { + console.error("Oops, you broke it. Here are the details:"); + console.error(err.stack); + console.error(); + console.error("That's all I know."); + }); diff --git a/bin/tmpl/manager.tmpl.js b/bin/tmpl/manager.tmpl.js new file mode 100644 index 0000000..2c09d3e --- /dev/null +++ b/bin/tmpl/manager.tmpl.js @@ -0,0 +1,78 @@ +"use strict"; + +var Manager = module.exports; +var db = {}; + +Manager.create = function(opts) { + var manager = {}; + + // + // REQUIRED (basic issuance) + // + + // Get + manager.get = async function({ servername, wildname }) { + // Required: find the certificate with the subject of `servername` + // Optional (multi-domain certs support): find a certificate with `servername` as an altname + // Optional (wildcard support): find a certificate with `wildname` as an altname + + // { subject, altnames, renewAt, deletedAt, challenges, ... } + return db[servername] || db[wildname]; + }; + + // Set + manager.set = async function(opts) { + // { subject, altnames, renewAt, deletedAt } + // Required: updated `renewAt` and `deletedAt` for certificate matching `subject` + + var site = db[opts.subject] || {}; + db[opts.subject] = Object.assign(site, opts); + return null; + }; + + // + // Optional (Fully Automatic Renewal) + // + /* + manager.find = async function(opts) { + // { subject, servernames, altnames, renewBefore } + + return [{ subject, altnames, renewAt, deletedAt }]; + }; + //*/ + + // + // Optional (Special Remove Functionality) + // The default behavior is to set `deletedAt` + // + /* + manager.remove = async function(opts) { + return mfs.remove(opts); + }; + //*/ + + // + // Optional (special settings save) + // Implemented here because this module IS the fallback + // + /* + manager.defaults = async function(opts) { + if (opts) { + return setDefaults(opts); + } + return getDefaults(); + }; + //*/ + + // + // Optional (for common deps and/or async initialization) + // + /* + manager.init = async function(deps) { + manager.request = deps.request; + return null; + }; + //*/ + + return manager; +}; diff --git a/package-lock.json b/package-lock.json index b32efa6..7e5fa0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,32 @@ { - "name": "greenlock-manager-test", - "version": "3.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@root/request": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@root/request/-/request-1.4.1.tgz", - "integrity": "sha512-2zSP1v9VhJ3gvm4oph0C4BYCoM3Sj84/Wx4iKdt0IbqbJzfON04EodBq5dsV65UxO/aHZciUBwY2GCZcHqaTYg==" + "name": "greenlock-manager-test", + "version": "3.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@root/mkdirp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@root/mkdirp/-/mkdirp-1.0.0.tgz", + "integrity": "sha512-hxGAYUx5029VggfG+U9naAhQkoMSXtOeXtbql97m3Hi6/sQSRL/4khKZPyOF6w11glyCOU38WCNLu9nUcSjOfA==" + }, + "@root/request": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@root/request/-/request-1.4.1.tgz", + "integrity": "sha512-2zSP1v9VhJ3gvm4oph0C4BYCoM3Sj84/Wx4iKdt0IbqbJzfON04EodBq5dsV65UxO/aHZciUBwY2GCZcHqaTYg==" + }, + "greenlock-manager-fs": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/greenlock-manager-fs/-/greenlock-manager-fs-3.0.5.tgz", + "integrity": "sha512-r/q+tEFuDwklfzPfiGhcIrHuJxMrppC+EseESpu5f0DMokh+1iZVm9nGC/VE7/7GETdOYfEYhhQkmspsi8Gr/A==", + "requires": { + "@root/mkdirp": "^1.0.0", + "safe-replace": "^1.1.0" + } + }, + "safe-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-replace/-/safe-replace-1.1.0.tgz", + "integrity": "sha512-9/V2E0CDsKs9DWOOwJH7jYpSl9S3N05uyevNjvsnDauBqRowBPOyot1fIvV5N2IuZAbYyvrTXrYFVG0RZInfFw==" + } } - } } diff --git a/package.json b/package.json index 3154783..14a6a47 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,32 @@ { - "name": "greenlock-manager-test", - "version": "3.0.0", - "description": "A simple test suite for Greenlock manager plugins.", - "main": "tester.js", - "scripts": { - "test": "node tests" - }, - "files": [ - "*.js", - "lib" - ], - "repository": { - "type": "git", - "url": "https://git.rootprojects.org/root/greenlock-manager-test.js" - }, - "keywords": [ - "Greenlock", - "manager", - "plugin" - ], - "author": "AJ ONeal (https://coolaj86.com/)", - "license": "MPL-2.0", - "dependencies": { - "@root/request": "^1.4.1", - "greenlock-manager-fs": "^3.0.0" - } + "name": "greenlock-manager-test", + "version": "3.1.0", + "description": "A simple test suite for Greenlock manager plugins.", + "main": "tester.js", + "scripts": { + "test": "node tests" + }, + "bin": { + "greenlock-manager-init": "bin/init.js" + }, + "files": [ + "*.js", + "bin", + "lib" + ], + "repository": { + "type": "git", + "url": "https://git.rootprojects.org/root/greenlock-manager-test.js" + }, + "keywords": [ + "Greenlock", + "manager", + "plugin" + ], + "author": "AJ ONeal (https://coolaj86.com/)", + "license": "MPL-2.0", + "dependencies": { + "@root/request": "^1.4.1", + "greenlock-manager-fs": "^3.0.0" + } } diff --git a/tester.js b/tester.js index 10e16ad..2522651 100644 --- a/tester.js +++ b/tester.js @@ -1,165 +1,441 @@ -'use strict'; +"use strict"; -var request = require('@root/request'); +var request = require("@root/request"); + +// For most tests +var siteSubject = "xx.com"; +var siteAltname = "www.foo.xx.com"; +var siteWildname = "*.xx.com"; +var siteMatch = "foo.xx.com"; +var domains = [siteSubject, siteAltname, siteWildname]; + +// Similar, but non-matching subjects +var noExistWild = "*.foo.xx.com"; +var noExistAlt = "bar.xx.com"; + +// For wildcard-as-subject test +var siteWildnameNet = "*.xx.net"; +var siteMatchNet = "foo.xx.net"; -var domains = ['example.com', 'www.example.com']; module.exports.test = async function(pkg, config) { - if ('function' !== typeof pkg.create) { - throw new Error( - 'must have a create function that accepts a single options object' - ); - } + if ("function" !== typeof pkg.create) { + throw new Error( + "must have a create function that accepts a single options object" + ); + } - var manager = pkg.create(config); + var features = { + altnames: false, + wildcard: false, + renewal: false + }; + var manager = pkg.create(config); + var initVal; - if (manager.init) { - await manager.init({ - request: request - }); - } else { - console.warn( - 'WARN: should have an init(deps) function which returns a promise' - ); - } + if (manager.init) { + initVal = await manager.init({ + request: request + }); + if (!initVal && initVal !== null) { + console.warn( + "WARN: `init()` returned `undefined`, but should return `null`" + ); + } + } + console.info("PASS: init(deps)"); - await manager.set({ - subject: domains[0], - altnames: domains - }); + await manager.set({ + subject: siteSubject, + altnames: domains + }); + var site = await manager.get({ + servername: siteSubject + // *.com is an invalid wildname + }); + if (!site || site.subject !== siteSubject) { + throw new Error( + "set({ subject: '" + + siteSubject + + "'}), but could not `get()` or `find()` it" + ); + } - await manager.find({}).then(function(results) { - if (!results.length) { - console.log(results); - throw new Error('should have found all managed sites'); - } - }); - console.log('PASS: set'); + // + // Test for altname support + // + site = await get({ + servername: siteAltname, + wildname: untame(siteAltname) + }); + if (site) { + if (site.subject !== siteSubject) { + throw new Error("found incorrect site"); + } + features.altnames = true; + } else { + console.warn("WARN: Does not support altnames."); + console.warn( + " (searched for %s but did not find site '%s')", + siteAltname, + domains.join(" ") + ); + } - await manager.find({ subject: 'www.example.com' }).then(function(results) { - if (results.length) { - console.log(results); - throw new Error( - "shouldn't find what doesn't exist, exactly, by subject" - ); - } - }); + // + // Test for wildcard support + // + if (features.altnames) { + // Set the wildcard as an altname + site = await get({ + servername: siteMatch, + wildname: siteWildname + }); + if (site) { + if (site.subject !== siteSubject) { + throw new Error( + "found %s when looking for %s", + site.subject, + siteSubject + ); + } + features.wildcard = true; + } else { + console.warn("WARN: Does not support wildcard domains."); + console.warn( + " (searched for %s but did not find site %s)", + siteMatch, + siteSubject + ); + } + } + // Set the wildcard as the subject + await manager.set({ + subject: siteWildnameNet, + altnames: [siteWildnameNet] + }); + site = await get({ + servername: siteMatchNet, + wildname: siteWildnameNet + }); + if (site) { + if (site.subject !== siteWildnameNet) { + throw new Error("found incorrect site"); + } + features.wildcard = true; + } else { + if (features.wildcard) { + throw new Error( + "searched for wildcard subject " + + siteWildnameNet + + " but did not find it" + ); + } + if (!features.altnames) { + console.warn( + "WARN: Does not support wildcard domains as certificate subjects." + ); + console.warn( + " (searched for %s as %s but did not find site %s)", + siteMatchNet, + siteWildnameNet, + siteWildnameNet + ); + } + } + await remove({ subject: siteWildnameNet }); - await manager - .find({ altnames: ['www.example.com'] }) - .then(function(results) { - if (!results.length) { - console.log(results); - throw new Error('should have found sites matching altname'); - } - }); + var wasSet = false; + if (manager.find) { + await manager.find({}).then(function(results) { + if (!results.length) { + //console.error(results); + throw new Error("should have found all managed sites"); + } + wasSet = results.some(function(site) { + return site.subject === siteSubject; + }); + if (!wasSet) { + throw new Error("should have found " + siteSubject); + } + }); + } - await manager.find({ altnames: ['*.example.com'] }).then(function(results) { - if (results.length) { - console.log(results); - throw new Error( - 'should only find an exact (literal) wildcard match' - ); - } - }); - console.log('PASS: find'); + if (manager.get) { + await manager.get({ servername: siteSubject }).then(function(site) { + if (!site || site.subject !== siteSubject) { + throw new Error("should have found " + siteSubject); + } + wasSet = true; + }); + if (features.altnames) { + wasSet = false; + await manager.get({ servername: siteAltname }).then(function(site) { + if (!site || site.subject !== siteSubject) { + throw new Error("should have found " + siteAltname); + } + }); + await manager + .get({ servername: siteMatch, wildname: siteWildname }) + .then(function(site) { + if (!site || site.subject !== siteSubject) { + throw new Error( + "did not find " + + siteMatch + + ", which matches " + + siteWildname + ); + } + wasSet = true; + }); + } + console.info("PASS: get({ servername, wildname })"); + } else { + console.info("[skip] get({ servername, wildname }) not implemented"); + } - await manager.remove({ subject: '*.example.com' }).then(function(result) { - if (result) { - throw new Error( - 'should not return prior object when deleting non-existing site' - ); - } - }); + if (wasSet) { + console.info("PASS: set({ subject })"); + } else { + throw new Error("neither `get()` nor `find()` was implemented"); + } - await manager.remove({ subject: 'www.example.com' }).then(function(result) { - if (result) { - throw new Error( - 'should not return prior object when deleting non-existing site' - ); - } - }); + if (manager.find) { + await manager.find({ subject: siteAltname }).then(function(results) { + if (results.length) { + console.error(results); + throw new Error( + "shouldn't find what doesn't exist, exactly, by subject" + ); + } + }); - await manager.remove({ subject: 'example.com' }).then(function(result) { - if (!result || !result.subject || !result.altnames) { - throw new Error('should return prior object when deleting site'); - } - }); + await manager + .find({ servernames: [siteAltname], altnames: [siteAltname] }) + .then(function(results) { + if (!results.length) { + console.error(results); + throw new Error("should have found sites matching altname"); + } + }); + console.info("PASS: find({ servernames, renewBefore })"); + } else { + console.info( + "[skip] find({ servernames, renewBefore }) not implemented" + ); + } - await manager - .find({ altnames: ['example.com', 'www.example.com'] }) - .then(function(results) { - if (results.length) { - console.log(results); - throw new Error('should not find deleted sites'); - } - }); - console.log('PASS: remove'); + await remove({ subject: noExistWild }).then(function(result) { + if (result) { + console.error(siteWildname, result); + throw new Error( + "should not return prior object when deleting non-existing wildcard domain: " + + noExistWild + ); + } + }); - var originalInput = { - serverKeyType: 'RSA-2048', - accountKeyType: 'P-256', - subscriberEmail: 'jon@example.com', - agreeToTerms: true, - store: { module: '/path/to/store-module', foo: 'foo' }, - challenges: { - 'http-01': { module: '/path/to/http-01-module', bar: 'bar' }, - 'dns-01': { module: '/path/to/dns-01-module', baz: 'baz' }, - 'tls-alpn-01': { - module: '/path/to/tls-alpn-01-module', - qux: 'quux' - } - }, - customerEmail: 'jane@example.com' - }; - //var backup = JSON.parse(JSON.stringify(originalInput)); - var configUpdate = { - renewOffset: '45d', - renewStagger: '12h', - subscriberEmail: 'pat@example.com' - }; + await remove({ subject: noExistAlt }).then(function(result) { + if (result) { + throw new Error( + "should not return prior object when deleting non-existing site: " + + noExistAlt + ); + } + }); - var internalConfig; - await manager.defaults().then(function(result) { - internalConfig = result; - if (!result) { - throw new Error( - 'should at least return an empty object, perhaps one with some defaults set' - ); - } - }); + await remove({ subject: siteWildname }).then(function(result) { + if (result) { + throw new Error("should not delete by wildname: " + siteWildname); + } + }); - await manager.defaults(originalInput).then(function(result) { - // can't say much... what _should_ this return? - // probably nothing? or maybe the full config object? - if (internalConfig === result) { - console.warn( - 'WARN: should return a new copy, not the same internal object' - ); - } - if (originalInput === result) { - console.warn( - 'WARN: should probably return a copy, not the original input' - ); - } - }); + await remove({ subject: siteAltname }).then(function(result) { + if (result) { + throw new Error("should not delete by altname: " + siteAltname); + } + }); - await manager.defaults().then(function(result) { - if (originalInput === result) { - console.warn('WARN: should probably return a copy, not the prior input'); - } - }); + await remove({ subject: siteSubject }).then(function(result) { + if (!result || !result.subject || !result.altnames) { + throw new Error("should return prior object when deleting site"); + } + }); + if (!manager.remove) { + console.info( + "[skip] remove() not implemented - using set({ deletedAt }) instead" + ); + } - await manager.defaults(configUpdate).then(function() { - if (originalInput.renewOffset) { - console.warn('WARN: should probably modify the prior input'); - } - }); + await manager.set({ subject: siteSubject, altnames: domains.slice(0, 2) }); + if (manager.find) { + await manager + .find({ servernames: [noExistWild], altnames: [noExistWild] }) + .then(function(results) { + if (results.length) { + console.error(results); + throw new Error( + "should only find an exact (literal) wildcard match" + ); + } + }); + } + await remove({ subject: siteSubject }).then(function(result) { + if (!result || !result.subject || !result.altnames) { + console.error( + "Could not find", + siteSubject, + "to delete it:", + result + ); + throw new Error("should return prior object when deleting site"); + } + }); - await manager.defaults().then(function(result) { - if (!result.subscriberEmail || !result.renewOffset) { - throw new Error('should merge config values together'); - } - }); + if (manager.find) { + await manager + .find({ servernames: domains, altnames: domains }) + .then(function(results) { + if (results.length) { + console.error(results); + throw new Error("should not find() deleted sites"); + } + }); + } else { + await get({ servername: siteAltname }).then(function(result) { + if (result) { + console.error(result); + throw new Error("should not get() deleted sites"); + } + }); + } + console.info("PASS: remove({ subject })"); - console.log('PASS: defaults'); + var originalInput = { + serverKeyType: "RSA-2048", + accountKeyType: "P-256", + subscriberEmail: "jon@example.com", + agreeToTerms: true, + store: { module: "/path/to/store-module", foo: "foo" }, + challenges: { + "http-01": { module: "/path/to/http-01-module", bar: "bar" }, + "dns-01": { module: "/path/to/dns-01-module", baz: "baz" }, + "tls-alpn-01": { + module: "/path/to/tls-alpn-01-module", + qux: "quux" + } + }, + customerEmail: "jane@example.com" + }; + //var backup = JSON.parse(JSON.stringify(originalInput)); + var configUpdate = { + renewOffset: "45d", + renewStagger: "12h", + subscriberEmail: "pat@example.com" + }; + + var internalConfig; + if (manager.defaults) { + await manager.defaults().then(function(result) { + internalConfig = result; + if (!result) { + throw new Error( + "should at least return an empty object, perhaps one with some defaults set" + ); + } + }); + + await manager.defaults(originalInput).then(function(result) { + // can't say much... what _should_ this return? + // probably nothing? or maybe the full config object? + if (internalConfig === result) { + console.warn( + "WARN: should return a new copy, not the same internal object" + ); + } + if (originalInput === result) { + console.warn( + "WARN: should probably return a copy, not the original input" + ); + } + }); + + await manager.defaults().then(function(result) { + if (originalInput === result) { + console.warn( + "WARN: should probably return a copy, not the prior input" + ); + } + }); + + await manager.defaults(configUpdate).then(function() { + if (originalInput.renewOffset) { + console.warn("WARN: should probably modify the prior input"); + } + }); + console.info("PASS: defaults(conf)"); + + await manager.defaults().then(function(result) { + if (!result.subscriberEmail || !result.renewOffset) { + throw new Error("should merge config values together"); + } + }); + console.info("PASS: defaults()"); + } else { + console.info( + "[skip] defaults({ store, challenges, ... }) not implemented" + ); + } + + features.renewal = !!manager.find; + var featureNames = { + altnames: "Multiple Domains per Certificate", + wildcard: + "Wildcard Certificates" + + (features.altnames ? "" : " (subject only)"), + renewal: "Fully Automatic Renewal" + }; + return Object.keys(features).map(function(k) { + return { + name: k, + description: featureNames[k], + supported: features[k] + }; + }); + + function get(opts) { + if (manager.get) { + opts.servername = opts.servername || opts.subject; + delete opts.subject; + return manager.get(opts); + } else { + return manager.find(opts); + } + } + + function remove(opts) { + if (manager.remove) { + return manager.remove(opts); + } else { + return get(opts).then(function(site) { + // get matches servername, but remove should only match subject + if (site && site.subject === opts.servername) { + site.deletedAt = Date.now(); + return manager.set(site).then(function() { + return site; + }); + } + return null; + }); + } + } + + function untame(str) { + return ( + "*." + + str + .split(".") + .slice(1) + .join(".") + ); + } }; diff --git a/tests/index.js b/tests/index.js index 515b615..30d97e6 100644 --- a/tests/index.js +++ b/tests/index.js @@ -1,19 +1,19 @@ -'use strict'; +"use strict"; -var Tester = require('../'); +var Tester = require("../"); -var Manager = require('greenlock-manager-fs'); +var Manager = require("greenlock-manager-fs"); var config = { - configFile: 'greenlock-manager-test.delete-me.json' + configFile: "greenlock-manager-test.delete-me.json" }; Tester.test(Manager, config) - .then(function() { - console.log('PASS: Known-good test module passes'); - }) - .catch(function(err) { - console.error('Oops, you broke it. Here are the details:'); - console.error(err.stack); - console.error(); - console.error("That's all I know."); - }); + .then(function() { + console.log("PASS: Known-good test module passes"); + }) + .catch(function(err) { + console.error("Oops, you broke it. Here are the details:"); + console.error(err.stack); + console.error(); + console.error("That's all I know."); + });