333 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			333 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # [greenlock-manager-test.js](https://git.rootprojects.org/root/greenlock-manager-test.js)
 | |
| 
 | |
| A simple test suite for Greenlock v3 manager plugins.
 | |
| 
 | |
| # Greenlock Manager
 | |
| 
 | |
| 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.
 | |
| 
 | |
| It consists of two required functions:
 | |
| 
 | |
| ```js
 | |
| set({ subject, altnames, renewAt, deletedAt });
 | |
| ```
 | |
| 
 | |
| ```js
 | |
| get({ servername });
 | |
| ```
 | |
| 
 | |
| However, if you implement `find({ subject, servernames, renewBefore })` (optional),
 | |
| you don't have to implement `get()`.
 | |
| 
 | |
| <details>
 | |
| <summary>Usage Details</summary>
 | |
| # 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.
 | |
| 
 | |
| </details>
 | |
| 
 | |
| # 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
 | |
| ```
 | |
| 
 | |
| It will generate a bare bones manager that passes the tests,
 | |
| (skipping all optional features), and a test file:
 | |
| 
 | |
| <details>
 | |
| <summary>manager.js</summary>
 | |
| 
 | |
| ```js
 | |
| "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;
 | |
| };
 | |
| ```
 | |
| 
 | |
| </details>
 | |
| 
 | |
| <details>
 | |
| <summary>manager.test.js</summary>
 | |
| 
 | |
| ```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, you broke it. Here are the details:");
 | |
|         console.error(err.stack);
 | |
|         console.error();
 | |
|         console.error("That's all I know.");
 | |
|     });
 | |
| ```
 | |
| 
 | |
| </details>
 | |
| 
 | |
| ```bash
 | |
| node manager.test.js
 | |
| ```
 | |
| 
 | |
| ```txt
 | |
| PASS:  get({ servername, wildname })
 | |
| PASS:  set({ subject })
 | |
| 
 | |
| Optional Feature Support:
 | |
| ✘ (NO)  Multiple Domains per Certificate
 | |
| ✘ (NO)  Wildcard Certificates
 | |
| ✘ (NO)  Fully Automatic Renewal
 | |
| ```
 | |
| 
 | |
| # Optional Features
 | |
| 
 | |
| If you're publishing a module to the community,
 | |
| you should implement the full test suite (and it's not that hard).
 | |
| 
 | |
| If you're only halfway through, you should note
 | |
| which features are supported and which aren't.
 | |
| 
 | |
| ```js
 | |
| find({ subject, servernames, renewBefore });
 | |
| defaults({ subscriberEmail, agreeToTerms, challenges, store, ... });
 | |
| defaults(); // as getter
 | |
| ```
 | |
| 
 | |
| -   `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 });
 | |
| ```
 | |
| 
 | |
| <details>
 | |
| <summary>Full Implementation</summary>
 | |
| 
 | |
| # The Right Way™
 | |
| 
 | |
| If you want to publish a module to the community you should do a slightly better job:
 | |
| 
 | |
| ```js
 | |
| module.exports.create = function(options) {
 | |
|     var manager = {};
 | |
| 
 | |
|     // add some things to... wherever you save things
 | |
| 
 | |
|     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.subject;
 | |
| 
 | |
|         // Cherry pick what you like for indexing / search, and JSONify the rest
 | |
|         return mergeOrCreateSite(subject, siteConfig);
 | |
|     };
 | |
| 
 | |
|     // find the things you've saved before
 | |
| 
 | |
|     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 && site.subject === subject) {
 | |
|                 return [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;
 | |
|                 }
 | |
|             });
 | |
|         }
 | |
| 
 | |
|         return getSitesThatShouldBeRenewedBefore(renewBefore || Infinity);
 | |
|     };
 | |
| 
 | |
|     // delete a site config
 | |
| 
 | |
|     manager.remove = async function({ subject }) {
 | |
|         // set deletedAt to a value, or actually delete it - however you like
 | |
|         return mergeOrCreateSite(subject, { deletedAt: Date.now() });
 | |
|     };
 | |
| 
 | |
|     // get / set global things
 | |
| 
 | |
|     manager.defaults = async function(options) {
 | |
|         if (!options) {
 | |
|             return getDefaultConfigValues();
 | |
|         }
 | |
| 
 | |
|         return mergeDefaultConfigValues(options);
 | |
|     };
 | |
| 
 | |
|     // optional, if you need it
 | |
| 
 | |
|     manager.init = async function(deps) {
 | |
|         // a place to do some init, if you need it
 | |
| 
 | |
|         return doMyInit();
 | |
| 
 | |
|         // Also, `deps` will have some common dependencies
 | |
|         // than many modules need, such as `request`.
 | |
|         // This cuts down on stray dependencies, and helps
 | |
|         // with browser compatibility.
 | |
| 
 | |
|         request = deps.request;
 | |
|     };
 | |
| };
 | |
| ```
 | |
| 
 | |
| </details>
 |