greenlock-manager-test.js/README.md

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>