├── .eslintrc.json ├── .jshintrc ├── .travis.yml ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── code_of_conduct.md ├── example ├── checks └── index.js ├── lib ├── healthchecks.js └── index.hbs ├── package.json └── test ├── .eslintrc.json ├── checks ├── default └── empty ├── checks_test.js ├── helpers └── server.js ├── onfail_test.js ├── pingdom_test.js └── user_test.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "es6": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "rules": { 9 | "arrow-spacing": 2, 10 | "block-spacing": 2, 11 | "brace-style": 2, 12 | "camelcase": 2, 13 | "comma-spacing": [2, { "before": false, "after": true }], 14 | "comma-style": [2, "last"], 15 | "consistent-return": 2, 16 | "consistent-this": [2, "self"], 17 | "curly": [2, "multi-or-nest"], 18 | "default-case": 2, 19 | "dot-location": [2, "property"], 20 | "dot-notation": 2, 21 | "eol-last": 2, 22 | "eqeqeq": [2, "smart"], 23 | "func-style": [2, "declaration"], 24 | "global-require": 2, 25 | "handle-callback-err": [2, "error"], 26 | "indent": [2, 2, { "SwitchCase": 1 }], 27 | "key-spacing": [2, { "beforeColon": false, "afterColon": true, "mode": "minimum", "align": "value" }], 28 | "max-depth": [2, 3], 29 | "max-nested-callbacks": [2, 3], 30 | "max-params": [2, 3], 31 | "max-statements": [2, 30], 32 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 33 | "new-parens": 2, 34 | "no-arrow-condition": 2, 35 | "no-caller": 2, 36 | "no-catch-shadow": 2, 37 | "no-const-assign": 2, 38 | "no-dupe-class-members": 2, 39 | "no-console": 2, 40 | "no-eval": 2, 41 | "no-extra-bind": 2, 42 | "no-fallthrough": 2, 43 | "no-labels": 2, 44 | "no-lonely-if": 2, 45 | "no-loop-func": 2, 46 | "no-magic-numbers": 0, 47 | "no-negated-condition": 2, 48 | "no-nested-ternary": 2, 49 | "no-new": 2, 50 | "no-octal-escape": 2, 51 | "no-octal": 2, 52 | "no-param-reassign": [2, { "props": true }], 53 | "no-process-exit": 2, 54 | "no-proto": 2, 55 | "no-return-assign": 2, 56 | "no-self-compare": 2, 57 | "no-sequences": 2, 58 | "no-shadow": [2, { "builtinGlobals": true }], 59 | "no-spaced-func": 2, 60 | "no-this-before-super": 2, 61 | "no-throw-literal": 2, 62 | "no-trailing-spaces": 2, 63 | "no-unneeded-ternary": 2, 64 | "no-unused-expressions": 2, 65 | "no-use-before-define": [2, "nofunc"], 66 | "no-var": 2, 67 | "no-void": 2, 68 | "no-with": 2, 69 | "object-curly-spacing": [2, "always"], 70 | "one-var": [2, "never"], 71 | "operator-linebreak": [2, "after"], 72 | "quote-props": [2, "consistent-as-needed"], 73 | "quotes": [2, "single", "avoid-escape"], 74 | "prefer-const": 2, 75 | "prefer-spread": 2, 76 | "prefer-template": 2, 77 | "radix": 2, 78 | "require-yield": 2, 79 | "semi": [2, "always"], 80 | "semi-spacing": 2, 81 | "sort-vars": 2, 82 | "space-after-keywords": [2, "always"], 83 | "space-before-blocks": [2, "always"], 84 | "space-before-function-paren": [2, "never"], 85 | "space-before-keywords": [2, "always"], 86 | "space-infix-ops": 2, 87 | "space-return-throw-case": 2, 88 | "space-unary-ops": 2, 89 | "spaced-comment": [2, "always"], 90 | "wrap-iife": 2, 91 | "valid-jsdoc": 1, 92 | "yoda": 2 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "camelcase": true, 4 | "freeze": true, 5 | "immed": true, 6 | "indent": 2, 7 | "latedef": "nofunc", 8 | "newcap": true, 9 | "nonew": true, 10 | "undef": true, 11 | "unused": true, 12 | "maxparams": 4, 13 | "maxdepth": 3, 14 | "node": true, 15 | "globals": { 16 | "Promise": false 17 | }, 18 | "overrides": { 19 | "test/*": { 20 | "mocha": true 21 | } 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5.0" 4 | - "4.0" 5 | notifications: 6 | email: 7 | on_success: always 8 | on_failure: always 9 | sudo: false 10 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Assaf Arkin (https://labnotes.org) 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Version 1.7.3 2016-06-21 2 | 3 | CHANGED code cleanups and linting 4 | 5 | 6 | ## Version 1.7.2 2015-12-15 7 | 8 | ADDED checks for HTTPS URLs now set `X-Forwarded-Proto` 9 | 10 | 11 | ## Version 1.7.1 2015-12-09 12 | 13 | FIXED erroneous package pushed to npm 14 | 15 | 16 | ## Version 1.7.0 2015-12-09 17 | 18 | FIXED always run checks against `localAddress` and `localPort` 19 | 20 | FIXED checking subdomains works again 21 | 22 | CHANGED dropped short-lived support for SSL 23 | 24 | CHANGED follow up to 10 redirects 25 | 26 | 27 | ## Version 1.6.1 2015-11-09 28 | 29 | FIXED follow redirects when performing checks 30 | 31 | ADDED skip SSL certificate validation via `strictSSL` option 32 | 33 | 34 | ## Version 1.5.0 2015-05-16 35 | 36 | CHANGED show response time in high resolution (including nanoseconds) 37 | 38 | 39 | ## Version 1.4.4 2015-04-16 40 | 41 | CHANGED show response time in seconds, 2 decimal places 42 | 43 | 44 | ## Version 1.4.3 2015-03-27 45 | 46 | FIXED debug logging check requests 47 | 48 | 49 | ## Version 1.4.2 2015-03-27 50 | 51 | FIXED connection.encrypted is now socket.encrypted (0.12/io.js) 52 | 53 | 54 | ## Version 1.4.1 2015-03-27 55 | 56 | FIXED maintain the same connection protocol (in addition to IP/port) 57 | 58 | 59 | ## Version 1.4.0 2015-03-24 60 | 61 | CHANGED run checks against localAddress/localPort 62 | 63 | 64 | ## Version 1.3.1 2015-03-23 65 | 66 | ADDED send user-agent header, some servers require this 67 | 68 | 69 | ## Version 1.3.0 2015-03-23 70 | 71 | CHANGED module can be used as Node request handler, does not need Express 72 | 73 | 74 | ## Version 1.2.5 2015-02-11 75 | 76 | CHANGED better styling for elapsed time 77 | 78 | 79 | ## Version 1.2.4 2015-02-11 80 | 81 | ADDED show request elapsed time 82 | 83 | 84 | ## Version 1.2.3 2015-01-19 85 | 86 | CHANGED outcome.toString() no shows error message or status code 87 | 88 | 89 | ## Version 1.2.2 2015-01-12 90 | 91 | FIXED verify result page shows consolidated results 92 | 93 | 94 | ## Version 1.2.1 2015-01-12 95 | 96 | CHANGED consolidate multiple checks for the same URL 97 | 98 | FIXED did not accept timeout option 99 | 100 | 101 | ## Version 1.2.0 2015-01-11 102 | 103 | CHANGED options `onfailed` replaced with `onFailed` which accepts an array of 104 | check results (not just URLs). 105 | 106 | Each failed check reported to `onFailed` is an object with the following 107 | properties: 108 | 109 | `url` -- The absolute URL 110 | `reason` -- One of 'error', 'timeout', 'statusCode' or 'body' 111 | `error` -- Connection or timeout error 112 | `timeout` -- True if failed due to timeout 113 | `statusCode` -- HTTP status code (if no error) 114 | `body` -- Response body 115 | 116 | 117 | ## Version 1.1.1 2014-12-08 118 | 119 | CHANGED only show each URL once, and sort alphabetically 120 | 121 | 122 | ## Version 1.1.0 2014-12-03 123 | 124 | ADDED onfailed option: called with list of failed checks 125 | 126 | ADDED If the client sends an X-Request-Id header when making a healthcheck 127 | request, that header is sent to all checked resources 128 | 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please note that this project is released with a [Contributor Code of Conduct](code_of_conduct.md). By participating in this project you agree to abide by its terms. 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2015 Broadly Inc 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Healthchecks 2 | 3 | Express middleware that runs health checks on your application. Allows you to 4 | manage a list of healthchecks as plain text files, keep them in source control 5 | next to the application. Provides a single endpoint you can access from a 6 | mobile device, or from a monitoring service. 7 | 8 | 9 | ## What and Why? 10 | 11 | A health check is a simple ping to a resource of your web application that 12 | checks that your application is up and running, responding to network requests, 13 | and behaving correctly. 14 | 15 | It can be as simple as pinging the home page of a web site, checking that the 16 | page includes the company's name in the title. 17 | 18 | If you have a complex application, there are multiple things that can break 19 | independently, and so you want a good health coverage by checking multiple 20 | resources (see [What Should I Check?](#what-should-i-check)). 21 | 22 | If your application has got one page that's accessing the database, and another 23 | page that just storing requests in a queue, you want to check both[1]. 24 | 25 | Whether you're using a service like Pingdom or internal tool like Nagios, if 26 | you store your checks there, funny thing is they never get updated when you roll 27 | out new features. 28 | 29 | You want checks to be part of the code base, in source control, right next to 30 | the application code that's getting checked, where it can be versioned and code 31 | reviewed. 32 | 33 | And that's what this module does. It lets you write your checks as a plain text 34 | file that lives in the same repository as your application. 35 | 36 | And it gives you a single endpoint that you can open in a browser, to see a list 37 | of all passed or failed checks. The same endpoint you can also use with a 38 | monitoring service like Pingdom or Nagios. 39 | 40 | 41 | ## Checks File 42 | 43 | The checks file lists one or more resources to check, and the expected content 44 | of each resource (which may be empty). 45 | 46 | For example: 47 | 48 | ``` 49 | # Check a web page 50 | / My Amazing App 51 | 52 | # Check stylesheets and scripts 53 | /stylesheets/index.css .body 54 | /scripts/index.js "use strict" 55 | 56 | # Check the image exist 57 | /images/logo.png 58 | ``` 59 | 60 | All URLs are relative to the server on which they are deployed, but must consist 61 | of at least an absolute pathname. You can include the hostname and protocol if 62 | you need to, for example, if your tests are mounted at `http://example.com` and 63 | you want to test `static.example.com` and test the HTTPS admin page: 64 | 65 | ``` 66 | # Check a different hostname than example.com 67 | //static.example.com/logo.png 68 | 69 | # Check with a different protocol and hostname 70 | https://admin.example.com Admin Dashboard 71 | ``` 72 | 73 | The expected content is matched literally against the body of the HTTP response. 74 | Only 2xx responses are considered successful (however, redirects are followed). 75 | 76 | 77 | ## Usage 78 | 79 | Install: 80 | 81 | ```bash 82 | npm install --save healthchecks 83 | ``` 84 | 85 | Include the checks file with your web server, then configure the middleware to 86 | read the checks file, for example: 87 | 88 | ```javascript 89 | const healthchecks = require('healthchecks'); 90 | 91 | // This file contains all the checks 92 | const CHECKS_FILE = __dirname + '/checks'; 93 | 94 | // Mount the middleware at /_healthchecks 95 | server.use('/_healthchecks', healthchecks(CHECKS_FILE)); 96 | ``` 97 | 98 | Now point your monitoring at `http://example.com/_healthchecks`. 99 | 100 | You can also open this page with your browser to see a list of passing and 101 | failed tests. 102 | 103 | **Note** this endpoint is publicly accessible. You can use another middleware 104 | to add access control, or add this feature and send us a pull request. 105 | 106 | You can initialize the middleware with the checks file name, or with an object 107 | containing the following options: 108 | 109 | * `filename` -- The name of the checks file 110 | * `onFailed` -- Called with array of failed checks 111 | * `timeout` -- Timeout slow responses 112 | 113 | You can specify the timeout in milliseconds or as a string, e.g. "3s" for 3 114 | seconds. 115 | 116 | Each failed check reported to `onFailed` is an object with the following 117 | properties: 118 | 119 | * `url` -- The absolute URL 120 | * `reason` -- One of `'error'`, `'timeout'`, `'statusCode'` or `'body'` 121 | * `error` -- Connection or timeout error 122 | * `timeout` -- True if failed due to timeout 123 | * `statusCode` -- HTTP status code (if no error) 124 | * `body` -- Response body 125 | 126 | For convenience, the value of the `reason` property is the name of one of the 127 | other properties. Also, when you call `toString()` you get a URL with the 128 | reason, e.g. `"http://example.com => statusCode"`. 129 | 130 | For example: 131 | 132 | ```javascript 133 | const options = { 134 | filename: CHECKS_FILE, 135 | timeout: '5s', // 5 seconds, can also pass duration in milliseconds 136 | onFailed: function(checks) { 137 | checks.forEach(function(check) { 138 | log('The following check failed:', check.url, 'reason:', check.reason); 139 | // ... or ... 140 | log('The following check failed: %s', check); 141 | }); 142 | } 143 | }; 144 | server.use('/_healthchecks', healthchecks(options); 145 | ``` 146 | 147 | 148 | ## What Should I Check? 149 | 150 | > Anything that can go wrong will go wrong. 151 | > 152 | > -- Murphy's law 153 | 154 | If two parts of your application can fail independently, you want to check both. 155 | 156 | If you have static and dynamic page, you want to check both. 157 | 158 | If different pages use different database servers, you want to check them all. 159 | 160 | If some page uses a caching servers, you want to check that as well. 161 | 162 | If another page uses a 3rd party API, also check that. 163 | 164 | If your page is composed of multiple modules that can fail independently (say 165 | shopping cart and product list), you want to check each module. 166 | 167 | If something can fail that's not an HTML page, you want to check that as well. 168 | 169 | Stylesheets? Check. Client-side scripts? Checks. Images? Checks. 170 | 171 | If they are pre-processed, you want to check what was generated. 172 | 173 | If it's served by a 3rd party CDN, don't skip this check. 174 | 175 | If your application dynamically generates links (to pages, CSS, JS), check those 176 | as well. 177 | 178 | You can have too many checks, but most likely your problem is you don't have 179 | enough! 180 | 181 | 182 | ## License 183 | 184 | [MIT License](LICENSE) Copyright (c) 2014 Broadly Inc 185 | 186 | -------------------------------------------------------------------------------- /code_of_conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 12 | 13 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 14 | 15 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/) 16 | -------------------------------------------------------------------------------- /example/checks: -------------------------------------------------------------------------------- 1 | # Check the main website 2 | / Get Better Online Reviews 3 | 4 | # Check the stylesheet 5 | /stylesheets/site.css @font-face 6 | 7 | # Check the scripts 8 | /scripts/site use strict 9 | 10 | # Check a sub-domain 11 | //embed.broadly.com/525c733fa3af69000000000e/reviews Sonya Z 12 | 13 | # Check HTTP access 14 | http://broadly.com/ Get Better Online Reviews 15 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable */ 2 | // To run this example: 3 | // 4 | // DEBUG=healthchecks node example/index.js 5 | // 6 | // And in separate terminal window: 7 | // 8 | // open http://localhost:4004/_healthchecks 9 | 10 | const express = require('express'); 11 | const healthchecks = require('..'); 12 | 13 | 14 | const server = express(); 15 | server.use('/_healthchecks', healthchecks(__dirname + '/checks')); 16 | 17 | server.listen(4004, function() { 18 | console.log('Healthchecks example listening on port', 4004); 19 | console.log(''); 20 | console.log('open http://localhost:4004/_healthchecks'); 21 | }); 22 | -------------------------------------------------------------------------------- /lib/healthchecks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // Exports an Express middleware factory. 3 | // 4 | // Example: 5 | // 6 | // const healthchecks = require('healthchecks'); 7 | // const CHECKS_FILE = './checks'; 8 | // 9 | // server.use('/_healthchecks', healthchecks(CHECKS_FILE)); 10 | // 11 | // If you want to change the check timeout, use named argument: 12 | // 13 | // const options = { 14 | // filename: CHECKS_FILE, 15 | // timeout: '5s' // 5 seconds, can also pass duration in milliseconds 16 | // }; 17 | // server.use('/_healthchecks', healthchecks(options); 18 | 19 | 20 | const assert = require('assert'); 21 | const debug = require('debug')('healthchecks'); 22 | const File = require('fs'); 23 | const Handlebars = require('handlebars'); 24 | const hrTime = require('pretty-hrtime'); 25 | const HTTP = require('http'); 26 | const ms = require('ms'); 27 | const Path = require('path'); 28 | const URL = require('url'); 29 | 30 | 31 | // Default timeout for checks; a slow server is a failed server. 32 | const DEFAULT_TIMEOUT = '3s'; 33 | 34 | // HTTP status codes which we follow as redirects. 35 | const REDIRECT_STATUSES = [301, 302, 303, 307]; 36 | 37 | // The maximum amount of redirects we will follow. 38 | const MAX_REDIRECT_COUNT = 10; 39 | 40 | 41 | // Represents a check outcome with the properties url, reason, etc. 42 | // 43 | // url - Checked URL 44 | // elapsed - In milliseconds 45 | // error - Protocol error 46 | // statusCode - HTTP status code 47 | // body - Response body 48 | // expected - Expected response body (array) 49 | function Outcome(args) { 50 | this.url = args.url; 51 | this.elapsed = hrTime(args.elapsed); 52 | if (args.error && args.error.code === 'ETIMEDOUT') { 53 | this.reason = 'timeout'; 54 | this.timeout = true; 55 | } else if (args.error) { 56 | this.reason = 'error'; 57 | this.error = args.error; 58 | } else { 59 | 60 | this.statusCode = args.statusCode; 61 | this.body = args.body; 62 | if (this.statusCode < 200 || this.statusCode >= 400) 63 | this.reason = 'statusCode'; 64 | else { 65 | 66 | const expected = args.expected; 67 | const allMatching = expected.every(text => ~args.body.indexOf(text)); 68 | if (!allMatching) 69 | this.reason = 'body'; 70 | 71 | } 72 | } 73 | this.log(); 74 | } 75 | 76 | 77 | // Log outcome, visible when DEBUG=healthchecks 78 | Outcome.prototype.log = function() { 79 | switch (this.reason) { 80 | case 'error': { 81 | debug('%s: Server responded with error %s', this.url, this.error); 82 | break; 83 | } 84 | case 'timeout': { 85 | debug('%s: Server response timeout', this.url); 86 | break; 87 | } 88 | case 'statusCode': { 89 | break; 90 | } 91 | case 'body': { 92 | debug('%s: Server response did not contain expected text', this.url); 93 | break; 94 | } 95 | // no default 96 | } 97 | }; 98 | 99 | 100 | // Easy way to log check outcome, shows the URL and the reason check failed 101 | Outcome.prototype.toString = function() { 102 | switch (this.reason) { 103 | case 'error': { 104 | return `${this.url} => ${this.error.message}`; 105 | } 106 | case 'statusCode': { 107 | return `${this.url} => ${this.statusCode}`; 108 | } 109 | case undefined: { 110 | debug('%s: Server responded with status code %s', this.url, this.statusCode); 111 | return this.url; 112 | } 113 | default: { 114 | return `${this.url} => ${this.reason}`; 115 | } 116 | } 117 | }; 118 | 119 | 120 | // Our Handlerbars instance. 121 | const handlebars = Handlebars.create(); 122 | 123 | 124 | // Checks the status of the given relative URL. 125 | // This URL, and all possible redirects, are resolved using 126 | // the resolver function. 127 | function runCheck(args) { 128 | const url = args.url; 129 | const headers = args.headers; 130 | const resolver = args.resolver; 131 | const timeout = args.timeout; 132 | const redirects = args.redirects || 0; 133 | 134 | const loopbackURL = resolver(url); 135 | const request = { 136 | host: loopbackURL.host, 137 | port: loopbackURL.port, 138 | path: loopbackURL.path, 139 | headers: Object.assign({}, headers, { 140 | 'Host': loopbackURL.hostname, 141 | 'X-Forwarded-Proto': loopbackURL.protocol.replace(/:$/, '') 142 | }) 143 | }; 144 | 145 | return get(request, timeout) 146 | .then(function(response) { 147 | const isRedirect = ~REDIRECT_STATUSES.indexOf(response.statusCode); 148 | if (isRedirect) { 149 | 150 | const redirectLoop = redirects >= MAX_REDIRECT_COUNT; 151 | if (redirectLoop) 152 | throw new Error('too many redirects'); 153 | else { 154 | const redirectURL = response.headers.location; 155 | const sameDomain = withinSameDomain(resolver(redirectURL), loopbackURL); 156 | if (sameDomain) 157 | return runCheck({ url: redirectURL, headers, resolver, timeout, redirects: redirects + 1 }); 158 | else 159 | return response; 160 | } 161 | 162 | } else 163 | return response; 164 | }); 165 | } 166 | 167 | 168 | // Makes an promisified HTTP GET request. 169 | // 170 | // On success, promise is resolved with { statusCode, headers, body }. 171 | // On error or timeout, promise is rejected with error. 172 | // 173 | function get(request, timeout) { 174 | return new Promise(function(resolve, reject) { 175 | HTTP.get(request) 176 | .on('error', reject) 177 | .on('response', function(response) { 178 | const buffers = []; 179 | response.on('data', function(buffer) { 180 | buffers.push(buffer); 181 | }); 182 | response.on('end', function() { 183 | const body = Buffer.concat(buffers).toString(); 184 | resolve({ 185 | statusCode: response.statusCode, 186 | headers: response.headers, 187 | body: body 188 | }); 189 | }); 190 | }); 191 | 192 | setTimeout(function() { 193 | const error = new Error('ETIMEDOUT'); 194 | error.code = 'ETIMEDOUT'; 195 | reject(error); 196 | }, timeout); 197 | }); 198 | } 199 | 200 | 201 | function withinSameDomain(from, to) { 202 | const fromHost = from.hostname; 203 | const toHost = to.hostname; 204 | return fromHost === toHost || 205 | fromHost.endsWith(`.${toHost}`) || 206 | toHost.endsWith(`.${fromHost}`); 207 | } 208 | 209 | 210 | // The check function will run all checks in parallel, and resolve to an object 211 | // with the properties: 212 | // passed - A list of all check URLs that passed 213 | // failed - A list of all check URLs that failed 214 | function checkFunction(url, requestID) { 215 | const protocol = URL.parse(url).protocol; 216 | const hostname = URL.parse(url).hostname; 217 | const port = URL.parse(url).port; 218 | 219 | const checks = this.checks; 220 | const timeout = this.timeout; 221 | 222 | // Given a relative URL in string form, returns a parsed URL 223 | // which points to the local server's IP address and port 224 | // and has the right hostname property to use in the Host header. 225 | function loopbackResolve(relativeURL) { 226 | const absoluteURL = URL.parse(URL.resolve(`${protocol}//localhost/`, relativeURL)); 227 | const loopbackURL = Object.assign({}, absoluteURL, { 228 | hostname: absoluteURL.hostname, 229 | host: hostname, 230 | port: port 231 | }); 232 | return loopbackURL; 233 | } 234 | 235 | // Each check resolves into an outcome object 236 | const allChecks = Object.keys(checks).map(function(checkURL) { 237 | const expected = checks[checkURL]; 238 | 239 | // We need to make an HTTP/S request to the current server, 240 | // based on the hostname/port passed to us, 241 | // so the HTTP check would go to http://localhost:80/ or some such URL. 242 | const headers = { 243 | 'User-Agent': 'Mozilla/5.0 (compatible) Healthchecks http://broadly.com', 244 | 'X-Request-Id': requestID || '' 245 | }; 246 | 247 | const start = process.hrtime(); 248 | return runCheck({ url: checkURL, headers, resolver: loopbackResolve, timeout }) 249 | .then(function(response) { 250 | const elapsed = process.hrtime(start); 251 | const outcome = new Outcome({ url: checkURL, expected, statusCode: response.statusCode, body: response.body, elapsed }); 252 | return outcome; 253 | }) 254 | .catch(function(error) { 255 | const elapsed = process.hrtime(start); 256 | const outcome = new Outcome({ url: checkURL, expected, error, elapsed }); 257 | return outcome; 258 | }); 259 | }); 260 | 261 | 262 | // Run all checks in parallel 263 | const allOutcomes = Promise.all(allChecks); 264 | 265 | // Reduce into an object with the passed and failed lists of URLs 266 | const passedAndFailed = allOutcomes 267 | .then(function(outcomes) { 268 | return { 269 | passed: outcomes.filter(outcome => !outcome.reason ), 270 | failed: outcomes.filter(outcome => outcome.reason ) 271 | }; 272 | }); 273 | 274 | // Returns the promise 275 | return passedAndFailed; 276 | } 277 | 278 | 279 | // Read the checks file and returns a check function (see checkFunction). 280 | function readChecks(filename, timeout) { 281 | const checks = File.readFileSync(filename, 'utf-8') 282 | .split(/[\n\r]+/) // Split into lines 283 | .map(line => line.trim()) // Ignore leading/trailing spaces 284 | .filter(line => line.length) // Ignore empty lines 285 | .filter(line => line[0] !== '#') // Ignore comments 286 | .filter(line => !/^\w+=/.test(line)) // Ignore name = value pairs 287 | .map(function(line) { // Split line to URL + expected value 288 | const match = line.match(/^(\S+)\s*(.*)/); 289 | return { 290 | url: match[1], 291 | expected: match[2] 292 | }; 293 | }) 294 | .map(function(check) { // Valid URLs only 295 | // URLs may be relative to the server, so contain an absolute path 296 | const url = URL.parse(check.url); 297 | assert(url.pathname && url.pathname[0] === '/', 'Check URL must have absolute pathname'); 298 | assert(!url.protocol || /^https?:$/.test(url.protocol), 'Check URL may only use HTTP/S protocol'); 299 | return check; 300 | }) 301 | .reduce(function(memo, check) { 302 | const url = check.url; 303 | memo[url] = memo[url] || []; // eslint-disable-line 304 | if (check.expected) 305 | memo[url].push(check.expected); 306 | return memo; 307 | }, {}); 308 | 309 | // Returns a check function that will use these checks / settings 310 | const context = { 311 | checks: checks, 312 | timeout: timeout 313 | }; 314 | debug('Added %d checks', checks.length); 315 | 316 | return checkFunction.bind(context); 317 | } 318 | 319 | 320 | // Returns a comparer function suitable for Array.sort(). 321 | function compareProperty(propName) { 322 | return function(a, b) { 323 | if (a[propName] < b[propName]) 324 | return -1; 325 | if (a[propName] > b[propName]) 326 | return 1; 327 | return 0; 328 | }; 329 | } 330 | 331 | 332 | // Respond with 200 only if all checks passed 333 | // Respond with 500 if any check fail 334 | // Respond with 404 if there are no checks to run 335 | function statusCodeFromOutcomes(passed, failed) { 336 | const anyFailed = failed.length > 0; 337 | const anyPassed = passed.length > 0; 338 | if (anyFailed) 339 | return 500; 340 | else if (anyPassed) 341 | return 200; 342 | else 343 | return 404; 344 | } 345 | 346 | 347 | // Call this function to configure and return the middleware. 348 | module.exports = function healthchecks(options) { 349 | assert(options, 'Missing options'); 350 | 351 | // Pass filename as first argument or named option 352 | const filename = typeof (options) === 'string' ? options : options.filename; 353 | assert(filename, 'Missing checks filename'); 354 | 355 | // Pass timeout as named option, or use default 356 | const timeoutArg = (typeof (options) === 'object' && options.timeout) || DEFAULT_TIMEOUT; 357 | // If timeout argument is a string (e.g. "3d"), convert to milliseconds 358 | const timeout = (typeof (timeoutArg) === 'string') ? ms(timeoutArg) : parseInt(timeoutArg, 10); 359 | 360 | const onFailed = options.onFailed || function() {}; 361 | 362 | // Read all checks form the file and returns a checking function 363 | const runChecks = readChecks(filename, timeout); 364 | 365 | // Load Handlebars template for rendering results 366 | const template = File.readFileSync(Path.join(__dirname, '/index.hbs'), 'utf-8'); 367 | const render = handlebars.compile(template); 368 | 369 | 370 | // Return the Express middleware 371 | return function(req, res) { 372 | 373 | const requestID = req.headers['x-request-id']; 374 | 375 | // We use local address/port to health check this server, e.g. the checks 376 | // may say //www.example.com/ but in development we connect to 377 | // 127.0.0.1:5000 378 | const protocol = req.socket.encrypted ? 'https:' : 'http:'; 379 | const hostname = req.socket.localAddress; 380 | const port = req.socket.localPort; 381 | const url = URL.format({ protocol, hostname, port }); 382 | 383 | // Run all checks 384 | debug('Running against %s://%s:%d with request-ID %s', protocol, hostname, port, requestID); 385 | runChecks(url, requestID) 386 | .then(function(outcomes) { 387 | debug('%d passed and %d failed', outcomes.passed.length, outcomes.failed.length); 388 | 389 | const passed = outcomes.passed.sort(compareProperty('url')); 390 | const failed = outcomes.failed.sort(compareProperty('url')); 391 | const statusCode = statusCodeFromOutcomes(passed, failed); 392 | 393 | const html = render({ passed, failed }); 394 | res.writeHeader(statusCode); 395 | res.write(html); 396 | res.end(); 397 | 398 | if (failed.length > 0) 399 | onFailed(failed); 400 | }); 401 | }; 402 | 403 | }; 404 | 405 | 406 | -------------------------------------------------------------------------------- /lib/index.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 33 | Healthchecks 34 | 35 | 36 | {{#if failed}} 37 |
38 |

Failed

39 | 44 |
45 | {{/if}} 46 | 47 |
48 | {{#if passed}} 49 |

Passed

50 | 55 | {{else}} 56 |

No tests passed

57 | {{/if}} 58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "healthchecks", 3 | "version": "1.7.3", 4 | "engines": { 5 | "node": ">=4.0.0" 6 | }, 7 | "scripts": { 8 | "lint": "eslint .", 9 | "test": "mocha && npm run lint" 10 | }, 11 | "main": "lib/healthchecks", 12 | "dependencies": { 13 | "debug": "^2.2.0", 14 | "handlebars": "^4.0.5", 15 | "ms": "^0.7.1", 16 | "pretty-hrtime": "^1.0.2" 17 | }, 18 | "devDependencies": { 19 | "eslint": "^1.10.3", 20 | "express": "^4.12", 21 | "mocha": "^2.3.4", 22 | "request": "^2.67.0", 23 | "zombie": "^4.0" 24 | }, 25 | "description": "Express middleware that runs health checks on your application", 26 | "homepage": "https://www.npmjs.com/package/healthchecks", 27 | "license": "MIT", 28 | "repository": { 29 | "type": "git", 30 | "url": "http://github.com/broadly/node-healthchecks" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "no-magic-numbers": 0, 7 | "max-nested-callbacks": [2, 10] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/checks/default: -------------------------------------------------------------------------------- 1 | # Test the response errors 2 | /error 3 | 4 | # Test the response timeout 5 | /timeout 6 | 7 | # Test the response has specific status code 8 | /status 9 | 10 | # Test the response contains expected text 11 | # And that multiple check results are consolidated 12 | /expected foo and bar 13 | /expected foo 14 | /expected bar 15 | 16 | # Test a redirect 17 | /redirect 18 | 19 | # Test a subdomain 20 | //admin.localhost/subdomain 21 | 22 | # Test HTTPS requests 23 | https://localhost/ssl-required 24 | 25 | # Used by Dokku, ignored by healthchecks 26 | WAIT=5 27 | -------------------------------------------------------------------------------- /test/checks/empty: -------------------------------------------------------------------------------- 1 | # Empty checks list. This should always 500. 2 | -------------------------------------------------------------------------------- /test/checks_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const assert = require('assert'); 3 | const healthchecks = require('..'); 4 | const Path = require('path'); 5 | const request = require('request'); 6 | const server = require('./helpers/server'); 7 | 8 | 9 | describe('Empty checks file', function() { 10 | 11 | before(function(done) { 12 | const filename = Path.join(__dirname, 'checks/empty'); 13 | server.use('/_healthchecks.empty', healthchecks(filename)); 14 | server.ready(done); 15 | }); 16 | 17 | 18 | it('should receive response with status 404', function(done) { 19 | request('http://localhost:3000/_healthchecks.empty', function(error, response) { 20 | assert.equal(response.statusCode, 404); 21 | done(error); 22 | }); 23 | }); 24 | 25 | }); 26 | 27 | 28 | describe('Missing checks file', function() { 29 | 30 | it('should fail setting up middleware', function(done) { 31 | assert.throws(function() { 32 | const filename = Path.join(__dirname, 'checks/invalid'); 33 | server.use('/_healthchecks.invalid', healthchecks(filename)); 34 | }); 35 | done(); 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /test/helpers/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const express = require('express'); 3 | const healthchecks = require('../..'); 4 | const Path = require('path'); 5 | 6 | 7 | const server = express(); 8 | module.exports = server; 9 | 10 | server.enable('trust proxy'); 11 | 12 | // We have two test suites that want to run the same test server. 13 | // 14 | // Instead of calling listen() they call ready(), and get notified when the 15 | // server is ready to receive requests. Server only started once. 16 | let listening = false; 17 | server.ready = function(callback) { 18 | if (listening) 19 | setImmediate(callback); 20 | else { 21 | server.listen(3000, function() { 22 | listening = true; 23 | callback(); 24 | }); 25 | } 26 | }; 27 | 28 | 29 | server.use('/_healthchecks', healthchecks({ 30 | filename: Path.join(__dirname, '../checks/default'), 31 | onFailed: function(failed) { 32 | server.emit('failed', failed); 33 | } 34 | })); 35 | 36 | // Test the response errors 37 | server.locals.error = false; 38 | server.get('/error', function(req, res) { 39 | if (server.locals.error) 40 | res.socket.destroy(); 41 | else 42 | res.status(204).send(''); 43 | }); 44 | 45 | // Test the response timeout 46 | server.locals.timeout = 0; 47 | server.get('/timeout', function(req, res) { 48 | setTimeout(function() { 49 | res.send(''); 50 | }, server.locals.timeout); 51 | }); 52 | 53 | // Test the response has specific status code 54 | server.locals.status = 200; 55 | server.get('/status', function(req, res) { 56 | res.status(server.locals.status).send(''); 57 | }); 58 | 59 | // Test the response contains expected text 60 | server.locals.expected = 'Expected to see foo and bar'; 61 | server.get('/expected', function(req, res) { 62 | res.send(server.locals.expected); 63 | }); 64 | 65 | // Test redirects 66 | server.locals.redirect = false; 67 | server.get('/redirect', function(req, res) { 68 | if (server.locals.redirect) 69 | res.redirect(server.locals.redirect); 70 | else 71 | res.status(204).send(''); 72 | }); 73 | 74 | // Test subdomains 75 | server.locals.subdomain = 'admin'; 76 | server.get('/subdomain', function(req, res) { 77 | const subdomain = req.headers.host.split('.')[0]; 78 | 79 | if (subdomain === server.locals.subdomain) 80 | res.status(200).send(''); 81 | else 82 | res.status(404).send(''); 83 | }); 84 | 85 | // Test HTTPS 86 | server.get('/ssl-required', function(req, res) { 87 | if (req.protocol === 'https') 88 | res.status(200).send(''); 89 | else 90 | res.status(403).send(''); 91 | }); 92 | -------------------------------------------------------------------------------- /test/onfail_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const assert = require('assert'); 3 | const ms = require('ms'); 4 | const request = require('request'); 5 | const server = require('./helpers/server'); 6 | 7 | 8 | const checksURL = 'http://localhost:3000/_healthchecks'; 9 | 10 | 11 | describe('Runs checks', function() { 12 | 13 | before(function(done) { 14 | server.ready(done); 15 | }); 16 | 17 | describe('all healthy', function() { 18 | 19 | function notified() { 20 | assert(false, 'Should not have called onFailed'); 21 | } 22 | 23 | before(function() { 24 | server.addListener('failed', notified); 25 | }); 26 | 27 | it('should not be notified', function(done) { 28 | request(checksURL, function() { 29 | done(); 30 | }); 31 | }); 32 | 33 | after(function() { 34 | server.removeListener('failed', notified); 35 | }); 36 | }); 37 | 38 | 39 | describe('URL not accessible', function() { 40 | before(function() { 41 | server.locals.error = true; 42 | }); 43 | 44 | it('should be notified of a failed test', function(done) { 45 | server.once('failed', function(failed) { 46 | assert.equal(failed.length, 1); 47 | assert.equal(failed[0].url, '/error'); 48 | assert.equal(failed[0].reason, 'error'); 49 | 50 | assert(failed[0].error); 51 | assert(!failed[0].timeout); 52 | assert(!failed[0].statusCode); 53 | assert(!failed[0].body); 54 | 55 | assert.equal(failed[0].toString(), '/error => socket hang up'); 56 | done(); 57 | }); 58 | request(checksURL); 59 | }); 60 | 61 | after(function() { 62 | server.locals.error = false; 63 | }); 64 | }); 65 | 66 | 67 | describe('response times out', function() { 68 | before(function() { 69 | server.locals.timeout = ms('3s'); 70 | }); 71 | 72 | it('should be notified of a failed test', function(done) { 73 | this.timeout(ms('4s')); 74 | 75 | server.once('failed', function(failed) { 76 | assert.equal(failed.length, 1); 77 | assert.equal(failed[0].url, '/timeout'); 78 | assert.equal(failed[0].reason, 'timeout'); 79 | 80 | assert(!failed[0].error); 81 | assert(failed[0].timeout); 82 | assert(!failed[0].statusCode); 83 | assert(!failed[0].body); 84 | 85 | assert.equal(failed[0].toString(), '/timeout => timeout'); 86 | done(); 87 | }); 88 | 89 | request(checksURL); 90 | }); 91 | 92 | after(function() { 93 | server.locals.timeout = 0; 94 | }); 95 | }); 96 | 97 | 98 | describe('response is 400', function() { 99 | before(function() { 100 | server.locals.status = 400; 101 | }); 102 | 103 | it('should be notified of a failed test', function(done) { 104 | server.once('failed', function(failed) { 105 | assert.equal(failed.length, 1); 106 | assert.equal(failed[0].url, '/status'); 107 | assert.equal(failed[0].reason, 'statusCode'); 108 | 109 | assert(!failed[0].error); 110 | assert(!failed[0].timeout); 111 | assert.equal(failed[0].statusCode, 400); 112 | assert(!failed[0].body); 113 | 114 | assert.equal(failed[0].toString(), '/status => 400'); 115 | done(); 116 | }); 117 | request(checksURL); 118 | }); 119 | 120 | after(function() { 121 | server.locals.status = 200; 122 | }); 123 | }); 124 | 125 | 126 | describe('response missing expected content', function() { 127 | before(function() { 128 | server.locals.expected = 'Expected to see foo and also bar'; 129 | }); 130 | 131 | it('should be notified of a failed test', function(done) { 132 | server.once('failed', function(failed) { 133 | assert.equal(failed.length, 1); 134 | assert.equal(failed[0].url, '/expected'); 135 | assert.equal(failed[0].reason, 'body'); 136 | 137 | assert(!failed[0].error); 138 | assert(!failed[0].timeout); 139 | assert.equal(failed[0].statusCode, 200); 140 | assert.equal(failed[0].body, 'Expected to see foo and also bar'); 141 | 142 | assert.equal(failed[0].toString(), '/expected => body'); 143 | done(); 144 | }); 145 | request(checksURL); 146 | }); 147 | 148 | after(function() { 149 | server.locals.expected = 'Expected to see foo and bar'; 150 | }); 151 | }); 152 | 153 | 154 | describe('subdomain not accessible', function() { 155 | before(function() { 156 | server.locals.subdomain = ''; 157 | }); 158 | 159 | it('should be notified of a failed test', function(done) { 160 | server.once('failed', function(failed) { 161 | assert.equal(failed[0].url, '//admin.localhost/subdomain'); 162 | assert.equal(failed[0].reason, 'statusCode'); 163 | 164 | assert(!failed[0].error); 165 | assert(!failed[0].timeout); 166 | assert.equal(failed[0].statusCode, 404); 167 | 168 | assert.equal(failed[0].toString(), '//admin.localhost/subdomain => 404'); 169 | done(); 170 | }); 171 | request(checksURL); 172 | }); 173 | 174 | after(function() { 175 | server.locals.subdomain = 'admin'; 176 | }); 177 | }); 178 | 179 | 180 | }); 181 | 182 | 183 | -------------------------------------------------------------------------------- /test/pingdom_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const assert = require('assert'); 3 | const ms = require('ms'); 4 | const request = require('request'); 5 | const server = require('./helpers/server'); 6 | 7 | 8 | const checksURL = 'http://localhost:3000/_healthchecks'; 9 | 10 | 11 | describe('Pingdom run checks', function() { 12 | 13 | before(function(done) { 14 | server.ready(done); 15 | }); 16 | 17 | 18 | describe('all healthy', function() { 19 | 20 | it('should receive response with status 200', function(done) { 21 | request(checksURL, function(error, response) { 22 | assert.equal(response.statusCode, 200); 23 | done(error); 24 | }); 25 | }); 26 | }); 27 | 28 | 29 | describe('URL not accessible', function() { 30 | before(function() { 31 | server.locals.error = true; 32 | }); 33 | 34 | it('should receive response with status 500', function(done) { 35 | request(checksURL, function(error, response) { 36 | assert.equal(response.statusCode, 500); 37 | done(error); 38 | }); 39 | }); 40 | 41 | after(function() { 42 | server.locals.error = false; 43 | }); 44 | }); 45 | 46 | 47 | describe('response times out', function() { 48 | before(function() { 49 | server.locals.timeout = ms('3s'); 50 | }); 51 | 52 | it('should receive response with status 500', function(done) { 53 | this.timeout(ms('4s')); 54 | 55 | request(checksURL, function(error, response) { 56 | assert.equal(response.statusCode, 500); 57 | done(error); 58 | }); 59 | }); 60 | 61 | after(function() { 62 | server.locals.timeout = 0; 63 | }); 64 | }); 65 | 66 | 67 | describe('response is 400', function() { 68 | before(function() { 69 | server.locals.status = 400; 70 | }); 71 | 72 | it('should receive response with status 500', function(done) { 73 | request(checksURL, function(error, response) { 74 | assert.equal(response.statusCode, 500); 75 | done(error); 76 | }); 77 | }); 78 | 79 | after(function() { 80 | server.locals.status = 200; 81 | }); 82 | }); 83 | 84 | 85 | describe('response missing expected content', function() { 86 | before(function() { 87 | server.locals.expected = 'Expected to see foo and also bar'; 88 | }); 89 | 90 | it('should receive response with status 500', function(done) { 91 | request(checksURL, function(error, response) { 92 | assert.equal(response.statusCode, 500); 93 | done(error); 94 | }); 95 | }); 96 | 97 | after(function() { 98 | server.locals.expected = 'Expected to see foo and bar'; 99 | }); 100 | }); 101 | 102 | 103 | describe('subdomain not accessible', function() { 104 | before(function() { 105 | server.locals.subdomain = ''; 106 | }); 107 | 108 | it('should receive response with status 500', function(done) { 109 | request(checksURL, function(error, response) { 110 | assert.equal(response.statusCode, 500); 111 | done(error); 112 | }); 113 | }); 114 | 115 | after(function() { 116 | server.locals.subdomain = 'admin'; 117 | }); 118 | }); 119 | 120 | 121 | }); 122 | -------------------------------------------------------------------------------- /test/user_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Browser = require('zombie'); 3 | const ms = require('ms'); 4 | const server = require('./helpers/server'); 5 | 6 | 7 | const checksURL = 'http://localhost:3000/_healthchecks'; 8 | 9 | 10 | describe('User runs checks', function() { 11 | 12 | const browser = new Browser(); 13 | 14 | before(function(done) { 15 | server.ready(done); 16 | }); 17 | 18 | describe('all healthy', function() { 19 | 20 | it('should see a list of passing tests', function(done) { 21 | browser.visit(checksURL, function() { 22 | browser.assert.text('h1', 'Passed'); 23 | browser.assert.text('.passed li:nth-of-type(1)', /\/\/admin.localhost\/subdomain \d[.\d]* ms/); 24 | browser.assert.text('.passed li:nth-of-type(2)', /error \d[.\d]* ms/); 25 | browser.assert.text('.passed li:nth-of-type(3)', /expected \d[.\d]* ms/); 26 | browser.assert.text('.passed li:nth-of-type(4)', /redirect \d[.\d]* ms/); 27 | browser.assert.text('.passed li:nth-of-type(5)', /status \d[.\d]* ms/); 28 | browser.assert.text('.passed li:nth-of-type(6)', /timeout \d[.\d]* ms/); 29 | browser.assert.text('.passed li:nth-of-type(7)', /https:\/\/localhost\/ssl-required \d[.\d]* ms/); 30 | done(); 31 | }); 32 | }); 33 | }); 34 | 35 | describe('redirect', function() { 36 | describe('to healthy page', function() { 37 | before(function() { 38 | server.locals.redirect = '/status'; 39 | }); 40 | 41 | it('should see a list of passing tests', function(done) { 42 | browser.visit(checksURL, function() { 43 | browser.assert.text('h1', 'Passed'); 44 | browser.assert.text('.passed li:nth-of-type(1)', /\/\/admin.localhost\/subdomain \d[.\d]* ms/); 45 | browser.assert.text('.passed li:nth-of-type(2)', /error \d[.\d]* ms/); 46 | browser.assert.text('.passed li:nth-of-type(3)', /expected \d[.\d]* ms/); 47 | browser.assert.text('.passed li:nth-of-type(4)', /redirect \d[.\d]* ms/); 48 | browser.assert.text('.passed li:nth-of-type(5)', /status \d[.\d]* ms/); 49 | browser.assert.text('.passed li:nth-of-type(6)', /timeout \d[.\d]* ms/); 50 | done(); 51 | }); 52 | }); 53 | 54 | after(function() { 55 | server.locals.redirect = false; 56 | }); 57 | }); 58 | 59 | describe('to page with errors', function() { 60 | before(function() { 61 | server.locals.redirect = '/error'; 62 | server.locals.error = true; 63 | }); 64 | 65 | it('should see a failed test', function(done) { 66 | browser.visit(checksURL, function() { 67 | browser.assert.text('h1', 'FailedPassed'); 68 | browser.assert.elements('.failed li', 2); 69 | browser.assert.text('.failed li:nth-of-type(2)', /redirect => socket hang up \d[.\d]* ms/); 70 | browser.assert.elements('.passed li', 5); 71 | done(); 72 | }); 73 | }); 74 | 75 | after(function() { 76 | server.locals.redirect = false; 77 | server.locals.error = false; 78 | }); 79 | }); 80 | 81 | describe('loop', function() { 82 | before(function() { 83 | server.locals.redirect = '/redirect'; 84 | }); 85 | 86 | it('should see a failed test', function(done) { 87 | browser.visit(checksURL, function() { 88 | browser.assert.text('h1', 'FailedPassed'); 89 | browser.assert.text('.failed li:nth-of-type(1)', /redirect => too many redirects \d[.\d]* .s/); 90 | done(); 91 | }); 92 | }); 93 | 94 | after(function() { 95 | server.locals.redirect = false; 96 | }); 97 | }); 98 | 99 | describe('to a subdomain', function() { 100 | before(function() { 101 | server.locals.redirect = 'https://subdomain.localhost/error'; 102 | }); 103 | 104 | describe('healthy', function() { 105 | it('should see a passing test', function(done) { 106 | browser.visit(checksURL, function() { 107 | browser.assert.text('h1', 'Passed'); 108 | browser.assert.text('.passed li:nth-of-type(4)', /redirect \d[.\d]* ms/); 109 | done(); 110 | }); 111 | }); 112 | 113 | }); 114 | 115 | describe('failing', function() { 116 | before(function() { 117 | server.locals.error = true; 118 | }); 119 | 120 | it('should see a failing test', function(done) { 121 | browser.visit(checksURL, function() { 122 | browser.assert.text('h1', 'FailedPassed'); 123 | browser.assert.text('.failed li:nth-of-type(2)', /redirect => socket hang up \d[.\d]* ms/); 124 | done(); 125 | }); 126 | }); 127 | 128 | after(function() { 129 | server.locals.error = false; 130 | }); 131 | }); 132 | 133 | after(function() { 134 | server.locals.redirect = false; 135 | }); 136 | }); 137 | 138 | describe('to a different domain', function() { 139 | before(function() { 140 | server.locals.redirect = 'https://www.google.com'; 141 | }); 142 | 143 | it('should see a passing test', function(done) { 144 | browser.visit(checksURL, function() { 145 | browser.assert.text('h1', 'Passed'); 146 | browser.assert.text('.passed li:nth-of-type(4)', /redirect \d[.\d]* ms/); 147 | done(); 148 | }); 149 | }); 150 | 151 | after(function() { 152 | server.locals.redirect = false; 153 | }); 154 | }); 155 | 156 | describe('to HTTPS page', function() { 157 | before(function() { 158 | server.locals.redirect = 'https://localhost/ssl-required'; 159 | }); 160 | 161 | it('should see a passing test', function(done) { 162 | browser.visit(checksURL, function() { 163 | browser.assert.text('h1', 'Passed'); 164 | browser.assert.text('.passed li:nth-of-type(4)', /redirect \d[.\d]* ms/); 165 | done(); 166 | }); 167 | }); 168 | 169 | after(function() { 170 | server.locals.redirect = false; 171 | }); 172 | }); 173 | }); 174 | 175 | 176 | describe('URL not accessible', function() { 177 | before(function() { 178 | server.locals.error = true; 179 | }); 180 | 181 | it('should see a failed test', function(done) { 182 | browser.visit(checksURL, function() { 183 | browser.assert.text('h1', 'FailedPassed'); 184 | browser.assert.elements('.failed li', 1); 185 | browser.assert.text('.failed li:nth-of-type(1)', /error => socket hang up \d[.\d]* ms/); 186 | browser.assert.elements('.passed li', 6); 187 | done(); 188 | }); 189 | }); 190 | 191 | after(function() { 192 | server.locals.error = false; 193 | }); 194 | }); 195 | 196 | 197 | describe('response times out', function() { 198 | before(function() { 199 | server.locals.timeout = ms('3s'); 200 | }); 201 | 202 | it('should see a failed test', function(done) { 203 | this.timeout(ms('4s')); 204 | 205 | browser.visit(checksURL, function() { 206 | browser.assert.text('h1', 'FailedPassed'); 207 | browser.assert.elements('.failed li', 1); 208 | browser.assert.text('.failed li:nth-of-type(1)', /timeout => timeout \d[.\d]* s/); 209 | browser.assert.elements('.passed li', 6); 210 | done(); 211 | }); 212 | }); 213 | 214 | after(function() { 215 | server.locals.timeout = 0; 216 | }); 217 | }); 218 | 219 | 220 | describe('response is 400', function() { 221 | before(function() { 222 | server.locals.status = 400; 223 | }); 224 | 225 | it('should see a failed test', function(done) { 226 | browser.visit(checksURL, function() { 227 | browser.assert.text('h1', 'FailedPassed'); 228 | browser.assert.elements('.failed li', 1); 229 | browser.assert.text('.failed li:nth-of-type(1)', /status => 400 \d[.\d]* ms/); 230 | browser.assert.elements('.passed li', 6); 231 | done(); 232 | }); 233 | }); 234 | 235 | after(function() { 236 | server.locals.status = 200; 237 | }); 238 | }); 239 | 240 | 241 | describe('response missing expected content', function() { 242 | before(function() { 243 | server.locals.expected = 'Expected to see foo and also bar'; 244 | }); 245 | 246 | it('should see a failed test', function(done) { 247 | browser.visit(checksURL, function() { 248 | browser.assert.text('h1', 'FailedPassed'); 249 | browser.assert.elements('.failed li', 1); 250 | browser.assert.text('.failed li:nth-of-type(1)', /expected => body \d[.\d]* ms/); 251 | browser.assert.elements('.passed li', 6); 252 | done(); 253 | }); 254 | }); 255 | 256 | after(function() { 257 | server.locals.expected = 'Expected to see foo and bar'; 258 | }); 259 | }); 260 | 261 | 262 | describe('subdomain not accessible', function() { 263 | before(function() { 264 | server.locals.subdomain = ''; 265 | }); 266 | 267 | it('should see a failed test', function(done) { 268 | browser.visit(checksURL, function() { 269 | browser.assert.text('h1', 'FailedPassed'); 270 | browser.assert.elements('.failed li', 1); 271 | browser.assert.text('.failed li:nth-of-type(1)', /\/\/admin.localhost\/subdomain => 404 \d[.\d]* ms/); 272 | browser.assert.elements('.passed li', 6); 273 | done(); 274 | }); 275 | }); 276 | 277 | after(function() { 278 | server.locals.subdomain = 'admin'; 279 | }); 280 | }); 281 | 282 | }); 283 | 284 | --------------------------------------------------------------------------------