Compare commits
	
		
			4 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e159a51984 | |||
| 63f2f02da9 | |||
| 30884601c6 | |||
| 19b571f088 | 
| @ -1,7 +1,7 @@ | ||||
| { | ||||
|   "bracketSpacing": true, | ||||
|   "printWidth": 80, | ||||
|   "singleQuote": true, | ||||
|   "singleQuote": false, | ||||
|   "tabWidth": 4, | ||||
|   "trailingComma": "none", | ||||
|   "useTabs": false | ||||
|  | ||||
							
								
								
									
										392
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										392
									
								
								README.md
									
									
									
									
									
								
							| @ -1,113 +1,251 @@ | ||||
| # greenlock-manager-test.js | ||||
| # [greenlock-manager-test.js](https://git.rootprojects.org/root/greenlock-manager-test.js) | ||||
| 
 | ||||
| A simple test suite for Greenlock manager plugins. | ||||
| A simple test suite for Greenlock v3 manager plugins. | ||||
| 
 | ||||
| # Greenlock Manager | ||||
| 
 | ||||
| A greenlock manager is just a set of a few callbacks to keeps track of: | ||||
| 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. | ||||
| 
 | ||||
| -   **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 | ||||
| 
 | ||||
| When do they get called? Well, whenever they need to. | ||||
| 
 | ||||
| # 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`: | ||||
| It consists of two required functions: | ||||
| 
 | ||||
| ```js | ||||
| 'use strict'; | ||||
| 
 | ||||
| var MyManager = module.exports; | ||||
| 
 | ||||
| MyManager.create = function(options) { | ||||
|     console.log('The tests will make me stronger'); | ||||
|     return {}; | ||||
| }; | ||||
| set({ subject, altnames, renewAt, deletedAt }); | ||||
| ``` | ||||
| 
 | ||||
| ## 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'; | ||||
| get({ servername }); | ||||
| ``` | ||||
| 
 | ||||
| var Tester = require('greenlock-manager-test'); | ||||
| var MyManager = require('./'); | ||||
| var myConfigOptions = { | ||||
|     someApiTokenForMyManager: 'xxx' | ||||
| }; | ||||
| However, if you implement `find({ subject, servernames, renewBefore })` (optional), | ||||
| you don't have to implement `get()`. | ||||
| 
 | ||||
| Tester.test(MyManager, myConfigOptions) | ||||
|     .then(function() { | ||||
|         console.log('All Tests Passed'); | ||||
|     }) | ||||
|     .catch(function(err) { | ||||
|         console.error('Oops... something bad happened:'); | ||||
|         console.error(err); | ||||
| <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 | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| You just follow the error messages and, which a little help from this README, | ||||
| bam!, you get a working plugin. It's insane! | ||||
| ## Why no require? | ||||
| 
 | ||||
| # 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: | ||||
| Okay, so you **expect** it to look like this: | ||||
| 
 | ||||
| ```js | ||||
| function find(argsToIgnore) { | ||||
|     return Promise.resolve([ | ||||
|         { subject: 'example.com', altnames: ['example.com', 'www.example.com'] } | ||||
|     ]); | ||||
| } | ||||
| var Greenlock = require("greenlock"); | ||||
| var greenlock = Greenlock.create({ | ||||
|     // WRONG!! | ||||
|     manager: require("./path-or-npm-name.js").create({ | ||||
|         someOptionYouWant: true | ||||
|     }) | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| If that's absolutely all that you do, all of the other methods will be implemented around `greenlock-manager-fs`. | ||||
| **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™ | ||||
| 
 | ||||
| @ -122,7 +260,7 @@ module.exports.create = function(options) { | ||||
|     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; | ||||
|         var subject = siteConfig.subject; | ||||
| 
 | ||||
|         // Cherry pick what you like for indexing / search, and JSONify the rest | ||||
|         return mergeOrCreateSite(subject, siteConfig); | ||||
| @ -130,38 +268,31 @@ module.exports.create = function(options) { | ||||
| 
 | ||||
|     // find the things you've saved before | ||||
| 
 | ||||
|     manager.find = async function({ subject, altnames, renewBefore }) { | ||||
|     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) { | ||||
|                 results.push(site); | ||||
|             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; | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         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(); | ||||
|         return getSitesThatShouldBeRenewedBefore(renewBefore || Infinity); | ||||
|     }; | ||||
| 
 | ||||
|     // delete a site config | ||||
| @ -198,35 +329,4 @@ module.exports.create = function(options) { | ||||
| }; | ||||
| ``` | ||||
| 
 | ||||
| # 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. | ||||
| </details> | ||||
|  | ||||
							
								
								
									
										36
									
								
								bin/init.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										36
									
								
								bin/init.js
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,36 @@ | ||||
| #!/usr/bin/env node
 | ||||
| "use strict"; | ||||
| 
 | ||||
| var fs = require("fs"); | ||||
| var path = require("path"); | ||||
| 
 | ||||
| function tmpl() { | ||||
|     var src = path.join(__dirname, "tmpl/manager.tmpl.js"); | ||||
|     var dst = path.join(process.cwd(), "./manager.js"); | ||||
| 
 | ||||
|     try { | ||||
|         fs.accessSync(dst); | ||||
|         console.warn("skip  'manager.js': already exists"); | ||||
|         return; | ||||
|     } catch (e) { | ||||
|         fs.writeFileSync(dst, fs.readFileSync(src, "utf8"), "utf8"); | ||||
|         console.info("wrote 'manager.js'"); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function tmplTest() { | ||||
|     var srcTest = path.join(__dirname, "tmpl/manager.test.tmpl.js"); | ||||
|     var dstTest = path.join(process.cwd(), "./manager.test.js"); | ||||
| 
 | ||||
|     try { | ||||
|         fs.accessSync(dstTest); | ||||
|         console.warn("skip  'manager.test.js': already exists"); | ||||
|         return; | ||||
|     } catch (e) { | ||||
|         fs.writeFileSync(dstTest, fs.readFileSync(srcTest, "utf8"), "utf8"); | ||||
|         console.info("wrote 'manager.test.js'"); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| tmpl(); | ||||
| tmplTest(); | ||||
							
								
								
									
										25
									
								
								bin/tmpl/manager.test.tmpl.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								bin/tmpl/manager.test.tmpl.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | ||||
| "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."); | ||||
|     }); | ||||
							
								
								
									
										78
									
								
								bin/tmpl/manager.tmpl.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								bin/tmpl/manager.tmpl.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,78 @@ | ||||
| "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; | ||||
| }; | ||||
							
								
								
									
										21
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										21
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -1,13 +1,32 @@ | ||||
| { | ||||
|     "name": "greenlock-manager-test", | ||||
|   "version": "3.0.0", | ||||
|     "version": "3.1.1", | ||||
|     "lockfileVersion": 1, | ||||
|     "requires": true, | ||||
|     "dependencies": { | ||||
|         "@root/mkdirp": { | ||||
|             "version": "1.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/@root/mkdirp/-/mkdirp-1.0.0.tgz", | ||||
|             "integrity": "sha512-hxGAYUx5029VggfG+U9naAhQkoMSXtOeXtbql97m3Hi6/sQSRL/4khKZPyOF6w11glyCOU38WCNLu9nUcSjOfA==" | ||||
|         }, | ||||
|         "@root/request": { | ||||
|             "version": "1.4.1", | ||||
|             "resolved": "https://registry.npmjs.org/@root/request/-/request-1.4.1.tgz", | ||||
|             "integrity": "sha512-2zSP1v9VhJ3gvm4oph0C4BYCoM3Sj84/Wx4iKdt0IbqbJzfON04EodBq5dsV65UxO/aHZciUBwY2GCZcHqaTYg==" | ||||
|         }, | ||||
|         "greenlock-manager-fs": { | ||||
|             "version": "3.0.5", | ||||
|             "resolved": "https://registry.npmjs.org/greenlock-manager-fs/-/greenlock-manager-fs-3.0.5.tgz", | ||||
|             "integrity": "sha512-r/q+tEFuDwklfzPfiGhcIrHuJxMrppC+EseESpu5f0DMokh+1iZVm9nGC/VE7/7GETdOYfEYhhQkmspsi8Gr/A==", | ||||
|             "requires": { | ||||
|                 "@root/mkdirp": "^1.0.0", | ||||
|                 "safe-replace": "^1.1.0" | ||||
|             } | ||||
|         }, | ||||
|         "safe-replace": { | ||||
|             "version": "1.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/safe-replace/-/safe-replace-1.1.0.tgz", | ||||
|             "integrity": "sha512-9/V2E0CDsKs9DWOOwJH7jYpSl9S3N05uyevNjvsnDauBqRowBPOyot1fIvV5N2IuZAbYyvrTXrYFVG0RZInfFw==" | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,13 +1,17 @@ | ||||
| { | ||||
|     "name": "greenlock-manager-test", | ||||
|   "version": "3.0.0", | ||||
|     "version": "3.1.1", | ||||
|     "description": "A simple test suite for Greenlock manager plugins.", | ||||
|     "main": "tester.js", | ||||
|     "scripts": { | ||||
|         "test": "node tests" | ||||
|     }, | ||||
|     "bin": { | ||||
|         "greenlock-manager-init": "bin/init.js" | ||||
|     }, | ||||
|     "files": [ | ||||
|         "*.js", | ||||
|         "bin", | ||||
|         "lib" | ||||
|     ], | ||||
|     "repository": { | ||||
|  | ||||
							
								
								
									
										388
									
								
								tester.js
									
									
									
									
									
								
							
							
						
						
									
										388
									
								
								tester.js
									
									
									
									
									
								
							| @ -1,43 +1,208 @@ | ||||
| 'use strict'; | ||||
| "use strict"; | ||||
| 
 | ||||
| var request = require('@root/request'); | ||||
| var request = require("@root/request"); | ||||
| 
 | ||||
| // For most tests
 | ||||
| var siteSubject = "xx.com"; | ||||
| var siteAltname = "www.foo.xx.com"; | ||||
| var siteWildname = "*.xx.com"; | ||||
| var siteMatch = "foo.xx.com"; | ||||
| var domains = [siteSubject, siteAltname, siteWildname]; | ||||
| 
 | ||||
| // Similar, but non-matching subjects
 | ||||
| var noExistWild = "*.foo.xx.com"; | ||||
| var noExistAlt = "bar.xx.com"; | ||||
| 
 | ||||
| // For wildcard-as-subject test
 | ||||
| var siteWildnameNet = "*.xx.net"; | ||||
| var siteMatchNet = "foo.xx.net"; | ||||
| 
 | ||||
| var domains = ['example.com', 'www.example.com']; | ||||
| module.exports.test = async function(pkg, config) { | ||||
| 	if ('function' !== typeof pkg.create) { | ||||
|     if ("function" !== typeof pkg.create) { | ||||
|         throw new Error( | ||||
| 			'must have a create function that accepts a single options object' | ||||
|             "must have a create function that accepts a single options object" | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     var features = { | ||||
|         altnames: false, | ||||
|         wildcard: false, | ||||
|         renewal: false | ||||
|     }; | ||||
|     var manager = pkg.create(config); | ||||
|     var initVal; | ||||
| 
 | ||||
|     if (manager.init) { | ||||
| 		await manager.init({ | ||||
|         initVal = await manager.init({ | ||||
|             request: request | ||||
|         }); | ||||
| 	} else { | ||||
|         if (!initVal && initVal !== null) { | ||||
|             console.warn( | ||||
| 			'WARN: should have an init(deps) function which returns a promise' | ||||
|                 "WARN: `init()` returned `undefined`, but should return `null`" | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|     console.info("PASS:  init(deps)"); | ||||
| 
 | ||||
|     await manager.set({ | ||||
|         subject: siteSubject, | ||||
|         altnames: domains | ||||
|     }); | ||||
|     var site = await manager.get({ | ||||
|         servername: siteSubject | ||||
|         // *.com is an invalid wildname
 | ||||
|     }); | ||||
|     if (!site || site.subject !== siteSubject) { | ||||
|         throw new Error( | ||||
|             "set({ subject: '" + | ||||
|                 siteSubject + | ||||
|                 "'}), but could not `get()` or `find()` it" | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
| 	await manager.set({ | ||||
| 		subject: domains[0], | ||||
| 		altnames: domains | ||||
|     //
 | ||||
|     // Test for altname support
 | ||||
|     //
 | ||||
|     site = await get({ | ||||
|         servername: siteAltname, | ||||
|         wildname: untame(siteAltname) | ||||
|     }); | ||||
|     if (site) { | ||||
|         if (site.subject !== siteSubject) { | ||||
|             throw new Error("found incorrect site"); | ||||
|         } | ||||
|         features.altnames = true; | ||||
|     } else { | ||||
|         console.warn("WARN: Does not support altnames."); | ||||
|         console.warn( | ||||
|             "      (searched for %s but did not find site '%s')", | ||||
|             siteAltname, | ||||
|             domains.join(" ") | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     //
 | ||||
|     // Test for wildcard support
 | ||||
|     //
 | ||||
|     if (features.altnames) { | ||||
|         // Set the wildcard as an altname
 | ||||
|         site = await get({ | ||||
|             servername: siteMatch, | ||||
|             wildname: siteWildname | ||||
|         }); | ||||
|         if (site) { | ||||
|             if (site.subject !== siteSubject) { | ||||
|                 throw new Error( | ||||
|                     "found %s when looking for %s", | ||||
|                     site.subject, | ||||
|                     siteSubject | ||||
|                 ); | ||||
|             } | ||||
|             features.wildcard = true; | ||||
|         } else { | ||||
|             console.warn("WARN: Does not support wildcard domains."); | ||||
|             console.warn( | ||||
|                 "      (searched for %s but did not find site %s)", | ||||
|                 siteMatch, | ||||
|                 siteSubject | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|     // Set the wildcard as the subject
 | ||||
|     await manager.set({ | ||||
|         subject: siteWildnameNet, | ||||
|         altnames: [siteWildnameNet] | ||||
|     }); | ||||
|     site = await get({ | ||||
|         servername: siteMatchNet, | ||||
|         wildname: siteWildnameNet | ||||
|     }); | ||||
|     if (site) { | ||||
|         if (site.subject !== siteWildnameNet) { | ||||
|             throw new Error("found incorrect site"); | ||||
|         } | ||||
|         features.wildcard = true; | ||||
|     } else { | ||||
|         if (features.wildcard) { | ||||
|             throw new Error( | ||||
|                 "searched for wildcard subject " + | ||||
|                     siteWildnameNet + | ||||
|                     " but did not find it" | ||||
|             ); | ||||
|         } | ||||
|         if (!features.altnames) { | ||||
|             console.warn( | ||||
|                 "WARN: Does not support wildcard domains as certificate subjects." | ||||
|             ); | ||||
|             console.warn( | ||||
|                 "      (searched for %s as %s but did not find site %s)", | ||||
|                 siteMatchNet, | ||||
|                 siteWildnameNet, | ||||
|                 siteWildnameNet | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|     await remove({ subject: siteWildnameNet }); | ||||
| 
 | ||||
|     var wasSet = false; | ||||
|     if (manager.find) { | ||||
|         await manager.find({}).then(function(results) { | ||||
|             if (!results.length) { | ||||
| 			console.log(results); | ||||
| 			throw new Error('should have found all managed sites'); | ||||
|                 //console.error(results);
 | ||||
|                 throw new Error("should have found all managed sites"); | ||||
|             } | ||||
|             wasSet = results.some(function(site) { | ||||
|                 return site.subject === siteSubject; | ||||
|             }); | ||||
|             if (!wasSet) { | ||||
|                 throw new Error("should have found " + siteSubject); | ||||
|             } | ||||
|         }); | ||||
| 	console.log('PASS: set'); | ||||
|     } | ||||
| 
 | ||||
| 	await manager.find({ subject: 'www.example.com' }).then(function(results) { | ||||
|     if (manager.get) { | ||||
|         await manager.get({ servername: siteSubject }).then(function(site) { | ||||
|             if (!site || site.subject !== siteSubject) { | ||||
|                 throw new Error("should have found " + siteSubject); | ||||
|             } | ||||
|             wasSet = true; | ||||
|         }); | ||||
|         if (features.altnames) { | ||||
|             wasSet = false; | ||||
|             await manager.get({ servername: siteAltname }).then(function(site) { | ||||
|                 if (!site || site.subject !== siteSubject) { | ||||
|                     throw new Error("should have found " + siteAltname); | ||||
|                 } | ||||
|             }); | ||||
|             await manager | ||||
|                 .get({ servername: siteMatch, wildname: siteWildname }) | ||||
|                 .then(function(site) { | ||||
|                     if (!site || site.subject !== siteSubject) { | ||||
|                         throw new Error( | ||||
|                             "did not find " + | ||||
|                                 siteMatch + | ||||
|                                 ", which matches " + | ||||
|                                 siteWildname | ||||
|                         ); | ||||
|                     } | ||||
|                     wasSet = true; | ||||
|                 }); | ||||
|         } | ||||
|         console.info("PASS:  get({ servername, wildname })"); | ||||
|     } else { | ||||
|         console.info("[skip] get({ servername, wildname }) not implemented"); | ||||
|     } | ||||
| 
 | ||||
|     if (wasSet) { | ||||
|         console.info("PASS:  set({ subject })"); | ||||
|     } else { | ||||
|         throw new Error("neither `get()` nor `find()` was implemented"); | ||||
|     } | ||||
| 
 | ||||
|     if (manager.find) { | ||||
|         await manager.find({ subject: siteAltname }).then(function(results) { | ||||
|             if (results.length) { | ||||
| 			console.log(results); | ||||
|                 console.error(results); | ||||
|                 throw new Error( | ||||
|                     "shouldn't find what doesn't exist, exactly, by subject" | ||||
|                 ); | ||||
| @ -45,85 +210,136 @@ module.exports.test = async function(pkg, config) { | ||||
|         }); | ||||
| 
 | ||||
|         await manager | ||||
| 		.find({ altnames: ['www.example.com'] }) | ||||
|             .find({ servernames: [siteAltname], altnames: [siteAltname] }) | ||||
|             .then(function(results) { | ||||
|                 if (!results.length) { | ||||
| 				console.log(results); | ||||
| 				throw new Error('should have found sites matching altname'); | ||||
|                     console.error(results); | ||||
|                     throw new Error("should have found sites matching altname"); | ||||
|                 } | ||||
|             }); | ||||
|         console.info("PASS:  find({ servernames, renewBefore })"); | ||||
|     } else { | ||||
|         console.info( | ||||
|             "[skip] find({ servernames, renewBefore }) not implemented" | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
| 	await manager.find({ altnames: ['*.example.com'] }).then(function(results) { | ||||
| 		if (results.length) { | ||||
| 			console.log(results); | ||||
|     await remove({ subject: noExistWild }).then(function(result) { | ||||
|         if (result) { | ||||
|             console.error(siteWildname, result); | ||||
|             throw new Error( | ||||
| 				'should only find an exact (literal) wildcard match' | ||||
|                 "should not return prior object when deleting non-existing wildcard domain: " + | ||||
|                     noExistWild | ||||
|             ); | ||||
|         } | ||||
|     }); | ||||
| 	console.log('PASS: find'); | ||||
| 
 | ||||
| 	await manager.remove({ subject: '*.example.com' }).then(function(result) { | ||||
|     await remove({ subject: noExistAlt }).then(function(result) { | ||||
|         if (result) { | ||||
|             throw new Error( | ||||
| 				'should not return prior object when deleting non-existing site' | ||||
|                 "should not return prior object when deleting non-existing site: " + | ||||
|                     noExistAlt | ||||
|             ); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
| 	await manager.remove({ subject: 'www.example.com' }).then(function(result) { | ||||
|     await remove({ subject: siteWildname }).then(function(result) { | ||||
|         if (result) { | ||||
| 			throw new Error( | ||||
| 				'should not return prior object when deleting non-existing site' | ||||
| 			); | ||||
|             throw new Error("should not delete by wildname: " + siteWildname); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
| 	await manager.remove({ subject: 'example.com' }).then(function(result) { | ||||
|     await remove({ subject: siteAltname }).then(function(result) { | ||||
|         if (result) { | ||||
|             throw new Error("should not delete by altname: " + siteAltname); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     await remove({ subject: siteSubject }).then(function(result) { | ||||
|         if (!result || !result.subject || !result.altnames) { | ||||
| 			throw new Error('should return prior object when deleting site'); | ||||
|             throw new Error("should return prior object when deleting site"); | ||||
|         } | ||||
|     }); | ||||
|     if (!manager.remove) { | ||||
|         console.info( | ||||
|             "[skip] remove() not implemented - using set({ deletedAt }) instead" | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     await manager.set({ subject: siteSubject, altnames: domains.slice(0, 2) }); | ||||
|     if (manager.find) { | ||||
|         await manager | ||||
| 		.find({ altnames: ['example.com', 'www.example.com'] }) | ||||
|             .find({ servernames: [noExistWild], altnames: [noExistWild] }) | ||||
|             .then(function(results) { | ||||
|                 if (results.length) { | ||||
| 				console.log(results); | ||||
| 				throw new Error('should not find deleted sites'); | ||||
|                     console.error(results); | ||||
|                     throw new Error( | ||||
|                         "should only find an exact (literal) wildcard match" | ||||
|                     ); | ||||
|                 } | ||||
|             }); | ||||
| 	console.log('PASS: remove'); | ||||
|     } | ||||
|     await remove({ subject: siteSubject }).then(function(result) { | ||||
|         if (!result || !result.subject || !result.altnames) { | ||||
|             console.error( | ||||
|                 "Could not find", | ||||
|                 siteSubject, | ||||
|                 "to delete it:", | ||||
|                 result | ||||
|             ); | ||||
|             throw new Error("should return prior object when deleting site"); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     if (manager.find) { | ||||
|         await manager | ||||
|             .find({ servernames: domains, altnames: domains }) | ||||
|             .then(function(results) { | ||||
|                 if (results.length) { | ||||
|                     console.error(results); | ||||
|                     throw new Error("should not find() deleted sites"); | ||||
|                 } | ||||
|             }); | ||||
|     } else { | ||||
|         await get({ servername: siteAltname }).then(function(result) { | ||||
|             if (result) { | ||||
|                 console.error(result); | ||||
|                 throw new Error("should not get() deleted sites"); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|     console.info("PASS:  remove({ subject })"); | ||||
| 
 | ||||
|     var originalInput = { | ||||
| 		serverKeyType: 'RSA-2048', | ||||
| 		accountKeyType: 'P-256', | ||||
| 		subscriberEmail: 'jon@example.com', | ||||
|         serverKeyType: "RSA-2048", | ||||
|         accountKeyType: "P-256", | ||||
|         subscriberEmail: "jon@example.com", | ||||
|         agreeToTerms: true, | ||||
| 		store: { module: '/path/to/store-module', foo: 'foo' }, | ||||
|         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' | ||||
|             "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' | ||||
|         customerEmail: "jane@example.com" | ||||
|     }; | ||||
|     //var backup = JSON.parse(JSON.stringify(originalInput));
 | ||||
|     var configUpdate = { | ||||
| 		renewOffset: '45d', | ||||
| 		renewStagger: '12h', | ||||
| 		subscriberEmail: 'pat@example.com' | ||||
|         renewOffset: "45d", | ||||
|         renewStagger: "12h", | ||||
|         subscriberEmail: "pat@example.com" | ||||
|     }; | ||||
| 
 | ||||
|     var internalConfig; | ||||
|     if (manager.defaults) { | ||||
|         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' | ||||
|                     "should at least return an empty object, perhaps one with some defaults set" | ||||
|                 ); | ||||
|             } | ||||
|         }); | ||||
| @ -133,33 +349,93 @@ module.exports.test = async function(pkg, config) { | ||||
|             // probably nothing? or maybe the full config object?
 | ||||
|             if (internalConfig === result) { | ||||
|                 console.warn( | ||||
| 				'WARN: should return a new copy, not the same internal object' | ||||
|                     "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' | ||||
|                     "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'); | ||||
|                 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'); | ||||
|                 console.warn("WARN: should probably modify the prior input"); | ||||
|             } | ||||
|         }); | ||||
|         console.info("PASS:  defaults(conf)"); | ||||
| 
 | ||||
|         await manager.defaults().then(function(result) { | ||||
|             if (!result.subscriberEmail || !result.renewOffset) { | ||||
| 			throw new Error('should merge config values together'); | ||||
|                 throw new Error("should merge config values together"); | ||||
|             } | ||||
|         }); | ||||
|         console.info("PASS:  defaults()"); | ||||
|     } else { | ||||
|         console.info( | ||||
|             "[skip] defaults({ store, challenges, ... }) not implemented" | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
| 	console.log('PASS: defaults'); | ||||
|     features.renewal = !!manager.find; | ||||
|     var featureNames = { | ||||
|         altnames: "Multiple Domains per Certificate", | ||||
|         wildcard: | ||||
|             "Wildcard Certificates" + | ||||
|             (features.altnames ? "" : " (subject only)"), | ||||
|         renewal: "Fully Automatic Renewal" | ||||
|     }; | ||||
|     return Object.keys(features).map(function(k) { | ||||
|         return { | ||||
|             name: k, | ||||
|             description: featureNames[k], | ||||
|             supported: features[k] | ||||
|         }; | ||||
|     }); | ||||
| 
 | ||||
|     function get(opts) { | ||||
|         if (manager.get) { | ||||
|             opts.servername = opts.servername || opts.subject; | ||||
|             delete opts.subject; | ||||
|             return manager.get(opts); | ||||
|         } else { | ||||
|             return manager.find(opts); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     function remove(opts) { | ||||
|         if (manager.remove) { | ||||
|             return manager.remove(opts); | ||||
|         } else { | ||||
|             return get(opts).then(function(site) { | ||||
|                 // get matches servername, but remove should only match subject
 | ||||
|                 if (site && site.subject === opts.servername) { | ||||
|                     site.deletedAt = Date.now(); | ||||
|                     return manager.set(site).then(function() { | ||||
|                         return site; | ||||
|                     }); | ||||
|                 } | ||||
|                 return null; | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     function untame(str) { | ||||
|         return ( | ||||
|             "*." + | ||||
|             str | ||||
|                 .split(".") | ||||
|                 .slice(1) | ||||
|                 .join(".") | ||||
|         ); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| @ -1,18 +1,18 @@ | ||||
| 'use strict'; | ||||
| "use strict"; | ||||
| 
 | ||||
| var Tester = require('../'); | ||||
| var Tester = require("../"); | ||||
| 
 | ||||
| var Manager = require('greenlock-manager-fs'); | ||||
| var Manager = require("greenlock-manager-fs"); | ||||
| var config = { | ||||
| 	configFile: 'greenlock-manager-test.delete-me.json' | ||||
|     configFile: "greenlock-manager-test.delete-me.json" | ||||
| }; | ||||
| 
 | ||||
| Tester.test(Manager, config) | ||||
|     .then(function() { | ||||
| 		console.log('PASS: Known-good test module passes'); | ||||
|         console.log("PASS: Known-good test module passes"); | ||||
|     }) | ||||
|     .catch(function(err) { | ||||
| 		console.error('Oops, you broke it. Here are the details:'); | ||||
|         console.error("Oops, you broke it. Here are the details:"); | ||||
|         console.error(err.stack); | ||||
|         console.error(); | ||||
|         console.error("That's all I know."); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user