v3.0.0: a nice test suite for Greenlock Managers

This commit is contained in:
AJ ONeal 2019-10-31 00:07:33 -06:00
parent 6b56a973f0
commit 742ef6159a
6 changed files with 460 additions and 1 deletions

8
.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"bracketSpacing": true,
"printWidth": 80,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "none",
"useTabs": false
}

228
README.md
View File

@ -1,3 +1,229 @@
# 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.

13
package-lock.json generated Normal file
View File

@ -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=="
}
}
}

28
package.json Normal file
View File

@ -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"
}
}

165
tester.js Normal file
View File

@ -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');
};

19
tests/index.js Normal file
View File

@ -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.");
});