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
|
||||
|
||||
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