v3.0.0: a nice test suite for Greenlock Managers
This commit is contained in:
parent
6b56a973f0
commit
742ef6159a
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"printWidth": 80,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"useTabs": false
|
||||||
|
}
|
228
README.md
228
README.md
|
@ -1,3 +1,229 @@
|
||||||
# greenlock-manager-test.js
|
# greenlock-manager-test.js
|
||||||
|
|
||||||
A simple test suite for Greenlock manager plugins.
|
A simple test suite for Greenlock manager plugins.
|
||||||
|
|
||||||
|
# Greenlock Manager
|
||||||
|
|
||||||
|
A greenlock manager is just a set of a few callbacks to keeps track of:
|
||||||
|
|
||||||
|
- **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
|
||||||
|
|
||||||
|
# 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`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var MyManager = module.exports;
|
||||||
|
MyManager.create = function(options) {
|
||||||
|
console.log('The tests will make me stronger');
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## The test suite from heaven
|
||||||
|
|
||||||
|
You write your test file, run it,
|
||||||
|
and then you get a play-by-play of what to do.
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install --save-dev greenlock-manager-test
|
||||||
|
```
|
||||||
|
|
||||||
|
`test.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var Tester = require('greenlock-manager-test');
|
||||||
|
var MyManager = require('./');
|
||||||
|
var myConfigOptions = {
|
||||||
|
someApiTokenForMyManager: 'xxx'
|
||||||
|
};
|
||||||
|
|
||||||
|
Tester.test(MyManager, myConfigOptions)
|
||||||
|
.then(function() {
|
||||||
|
console.log('All Tests Passed');
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
console.error('Oops... something bad happened:');
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
You just follow the error messages and, which a little help from this README,
|
||||||
|
bam!, you get a working plugin. It's insane!
|
||||||
|
|
||||||
|
# The lazy, hacky way.
|
||||||
|
|
||||||
|
If you're going to publish a module, you should pass the full test suite.
|
||||||
|
|
||||||
|
If not, eh, you can be lazy.
|
||||||
|
|
||||||
|
## Bare minimum...
|
||||||
|
|
||||||
|
At a bare minimum, you must implement `find()` to return an array of `{ subject, altnames }`.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function find(argsToIgnore) {
|
||||||
|
return Promise.resolve([
|
||||||
|
{ subject: 'example.com', altnames: ['example.com', 'www.example.com'] }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If that's absolutely all that you do, all of the other methods will be implemented around `greenlock-manager-fs`.
|
||||||
|
|
||||||
|
# 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;
|
||||||
|
|
||||||
|
// Cherry pick what you like for indexing / search, and JSONify the rest
|
||||||
|
return mergeOrCreateSite(subject, siteConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
// find the things you've saved before
|
||||||
|
|
||||||
|
manager.find = async function({ subject, altnames, renewBefore }) {
|
||||||
|
var results = [];
|
||||||
|
var gotten = {};
|
||||||
|
|
||||||
|
if (subject) {
|
||||||
|
var site = await getSiteBySubject(subject);
|
||||||
|
if (site) {
|
||||||
|
results.push(site);
|
||||||
|
gotten[site.subject] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (altnames) {
|
||||||
|
var sites = await getSiteByAltnames(subject);
|
||||||
|
sites.forEach(function() {});
|
||||||
|
if (site) {
|
||||||
|
if (!gotten[site.subject]) {
|
||||||
|
results.push(site);
|
||||||
|
gotten[site.subject] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subject || altnames) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renewBefore) {
|
||||||
|
return getSitesThatShouldBeRenewedBefore(renewBefore);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getAllSites();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"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=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"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 <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@root/request": "^1.4.1",
|
||||||
|
"greenlock-manager-fs": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var request = require('@root/request');
|
||||||
|
|
||||||
|
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'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var manager = pkg.create(config);
|
||||||
|
|
||||||
|
if (manager.init) {
|
||||||
|
await manager.init({
|
||||||
|
request: request
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
'WARN: should have an init(deps) function which returns a promise'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await manager.set({
|
||||||
|
subject: domains[0],
|
||||||
|
altnames: domains
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
await manager.remove({ subject: '*.example.com' }).then(function(result) {
|
||||||
|
if (result) {
|
||||||
|
throw new Error(
|
||||||
|
'should not return prior object when deleting non-existing site'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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({ 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');
|
||||||
|
|
||||||
|
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;
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.defaults().then(function(result) {
|
||||||
|
if (!result.subscriberEmail || !result.renewOffset) {
|
||||||
|
throw new Error('should merge config values together');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('PASS: defaults');
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var Tester = require('../');
|
||||||
|
|
||||||
|
var Manager = require('greenlock-manager-fs');
|
||||||
|
var config = {
|
||||||
|
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.");
|
||||||
|
});
|
Loading…
Reference in New Issue