greenlock-manager-test.js/README.md

333 lines
8.0 KiB
Markdown
Raw Permalink Normal View History

2019-10-31 06:12:01 +00:00
# [greenlock-manager-test.js](https://git.rootprojects.org/root/greenlock-manager-test.js)
2019-10-31 04:56:52 +00:00
2019-11-05 04:03:21 +00:00
A simple test suite for Greenlock v3 manager plugins.
# Greenlock Manager
2019-11-05 04:03:21 +00:00
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.
2019-11-05 04:03:21 +00:00
It consists of two required functions:
2019-11-05 04:03:21 +00:00
```js
set({ subject, altnames, renewAt, deletedAt });
```
2019-11-05 04:03:21 +00:00
```js
get({ servername });
```
2019-11-05 04:03:21 +00:00
However, if you implement `find({ subject, servernames, renewBefore })` (optional),
you don't have to implement `get()`.
2019-10-31 06:09:09 +00:00
2019-11-05 04:03:21 +00:00
<details>
<summary>Usage Details</summary>
# How to use your plugin
2019-11-05 04:03:21 +00:00
The **Right Way**:
2019-11-05 04:03:21 +00:00
```bash
npm install --save greenlack
npx greenlock init --manager ./path-or-npm-name.js --manager-xxxx 'sets xxxx' --manager-yyyy 'set yyyy'
```
2019-11-05 04:03:21 +00:00
That creates a `.greenlockrc`, which is essentially the same as doing this:
2019-11-05 04:03:21 +00:00
```js
var Greenlock = require("greenlock");
var greenlock = Greenlock.create({
// ...
2019-11-05 04:03:21 +00:00
manager: "./path-or-npm-name.js",
xxxx: "sets xxxx",
yyyy: "sets yyyy",
packageRoot: __dirname
});
```
2019-11-05 04:03:21 +00:00
## Why no require?
2019-11-05 04:03:21 +00:00
Okay, so you **expect** it to look like this:
```js
2019-11-05 04:03:21 +00:00
var Greenlock = require("greenlock");
var greenlock = Greenlock.create({
// WRONG!!
manager: require("./path-or-npm-name.js").create({
someOptionYouWant: true
})
});
```
2019-11-05 04:03:21 +00:00
**NOPE**!
2019-10-31 06:09:09 +00:00
2019-11-05 04:03:21 +00:00
Greenlock is designed to so that the CLI tools, Web API, and JavaScript API
can all work interdepedently, indpendently.
2019-11-05 04:03:21 +00:00
Therefore the configuration has to go into serializable JSON rather than
executable JavaScript.
2019-11-05 04:03:21 +00:00
</details>
2019-11-05 04:03:21 +00:00
# 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
2019-11-05 04:03:21 +00:00
npx greenlock-manager-init
```
2019-11-05 04:03:21 +00:00
It will generate a bare bones manager that passes the tests,
(skipping all optional features), and a test file:
<details>
<summary>manager.js</summary>
2019-11-05 04:04:39 +00:00
2019-11-05 04:03:21 +00:00
```js
"use strict";
var Manager = module.exports;
var db = {};
Manager.create = function(opts) {
2019-11-05 04:04:39 +00:00
var manager = {};
2019-11-05 04:03:21 +00:00
//
// 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;
};
2019-11-05 04:04:39 +00:00
```
2019-11-05 04:03:21 +00:00
</details>
2019-11-05 04:04:39 +00:00
2019-11-05 04:03:21 +00:00
<details>
<summary>manager.test.js</summary>
2019-11-05 04:04:39 +00:00
```js
2019-11-05 04:03:21 +00:00
"use strict";
2019-11-05 04:03:21 +00:00
var Tester = require("greenlock-manager-test");
var Manager = require("./manager.js");
var config = {
configFile: "greenlock-manager-test.delete-me.json"
};
2019-11-05 04:03:21 +00:00
Tester.test(Manager, config)
.then(function(features) {
console.info("PASS");
console.info();
console.info("Optional Feature Support:");
features.forEach(function(feature) {
2019-11-05 04:04:39 +00:00
console.info(
feature.supported ? "✓ (YES)" : "✘ (NO) ",
feature.description
);
2019-11-05 04:03:21 +00:00
});
console.info();
})
.catch(function(err) {
2019-11-05 04:03:21 +00:00
console.error("Oops, you broke it. Here are the details:");
console.error(err.stack);
console.error();
console.error("That's all I know.");
});
2019-11-05 04:04:39 +00:00
```
2019-11-05 04:03:21 +00:00
</details>
```bash
node manager.test.js
```
2019-11-05 04:03:21 +00:00
```txt
PASS: get({ servername, wildname })
PASS: set({ subject })
2019-11-05 04:03:21 +00:00
Optional Feature Support:
✘ (NO) Multiple Domains per Certificate
✘ (NO) Wildcard Certificates
✘ (NO) Fully Automatic Renewal
```
2019-11-05 04:03:21 +00:00
# Optional Features
2019-11-05 04:03:21 +00:00
If you're publishing a module to the community,
you should implement the full test suite (and it's not that hard).
2019-11-05 04:03:21 +00:00
If you're only halfway through, you should note
which features are supported and which aren't.
2019-11-05 04:03:21 +00:00
```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.
2019-11-05 04:03:21 +00:00
Additionally, you're manager may need an init or a _real_ delete - rather than just using `set({ deletedAt })`:
```js
2019-11-05 04:03:21 +00:00
init({ request });
remove({ subject });
```
2019-11-05 04:03:21 +00:00
<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.
2019-11-05 04:03:21 +00:00
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
2019-11-05 04:03:21 +00:00
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);
2019-11-05 04:03:21 +00:00
if (site && site.subject === subject) {
return [site];
}
}
2019-11-05 04:03:21 +00:00
if (severnames) {
return await Promise.all(servernames.map(function (altname) {
var site = getSiteByAltname(subject);
if (site && !gotten[site.subject]) {
gotten[site.subject] = true;
2019-11-05 04:03:21 +00:00
return site;
}
2019-11-05 04:03:21 +00:00
});
}
2019-11-05 04:03:21 +00:00
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;
};
};
```
2019-11-05 04:03:21 +00:00
</details>