From 1a15fcdad529ab87a991090f520a309b9aae80b1 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Wed, 8 Aug 2018 20:00:04 -0400 Subject: [PATCH 01/15] Rewrite Uptime Robot status --- lib/all-badge-examples.js | 5 - lib/error-helper.js | 8 +- server.js | 66 ----------- services/base.js | 9 +- services/errors.js | 15 +++ .../uptimerobot/uptimerobot-status.service.js | 105 ++++++++++++++++++ services/uptimerobot/uptimerobot.tester.js | 16 ++- 7 files changed, 144 insertions(+), 80 deletions(-) create mode 100644 services/uptimerobot/uptimerobot-status.service.js diff --git a/lib/all-badge-examples.js b/lib/all-badge-examples.js index 79bfab146fe5a..23437462a3e1c 100644 --- a/lib/all-badge-examples.js +++ b/lib/all-badge-examples.js @@ -1671,11 +1671,6 @@ const allBadgeExamples = [ previewUri: '/swagger/valid/2.0/https/raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v2.0/json/petstore-expanded.json.svg', }, - { - title: 'Uptime Robot status', - previewUri: - '/uptimerobot/status/m778918918-3e92c097147760ee39d02d36.svg', - }, { title: 'Uptime Robot ratio (30 days)', previewUri: diff --git a/lib/error-helper.js b/lib/error-helper.js index 0fed84ae322cf..e66c885549e7f 100644 --- a/lib/error-helper.js +++ b/lib/error-helper.js @@ -1,6 +1,10 @@ 'use strict' -const { NotFound, InvalidResponse } = require('../services/errors') +const { + NotFound, + InvalidResponse, + Inaccessible, +} = require('../services/errors') const checkErrorResponse = function( badgeData, @@ -29,6 +33,8 @@ checkErrorResponse.asPromise = function({ notFoundMessage } = {}) { return async function({ buffer, res }) { if (res.statusCode === 404) { throw new NotFound({ prettyMessage: notFoundMessage }) + } else if (res.statusCode >= 500) { + throw new Inaccessible() } else if (res.statusCode !== 200) { const underlying = Error( `Got status code ${res.statusCode} (expected 200)` diff --git a/server.js b/server.js index bb3a29f201ccd..6055b21eb9a6a 100644 --- a/server.js +++ b/server.js @@ -6691,72 +6691,6 @@ cache(function(data, match, sendBadge, request) { }); })); -// Uptime Robot status integration. -// API documentation : https://uptimerobot.com/api -camp.route(/^\/uptimerobot\/status\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var monitorApiKey = match[1]; // eg, m778918918-3e92c097147760ee39d02d36 - var format = match[2]; - var badgeData = getBadgeData('status', data); - var options = { - method: 'POST', - json: true, - body: { - "api_key": monitorApiKey, - "format": "json", - }, - uri: 'https://api.uptimerobot.com/v2/getMonitors', - }; - // A monitor API key must start with "m" - if (monitorApiKey.substring(0, "m".length) !== "m") { - badgeData.text[1] = 'must use a monitor key'; - sendBadge(format, badgeData); - return; - } - request(options, function(err, res, json) { - if (err !== null || res.statusCode >= 500 || typeof json !== 'object') { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - if (json.stat === 'fail') { - badgeData.text[1] = 'vendor error'; - if (json.error && typeof json.error.message === 'string') { - badgeData.text[1] = json.error.message; - } - badgeData.colorscheme = 'lightgrey'; - sendBadge(format, badgeData); - return; - } - var status = json.monitors[0].status; - if (status === 0) { - badgeData.text[1] = 'paused'; - badgeData.colorscheme = 'yellow'; - } else if (status === 1) { - badgeData.text[1] = 'not checked yet'; - badgeData.colorscheme = 'yellowgreen'; - } else if (status === 2) { - badgeData.text[1] = 'up'; - badgeData.colorscheme = 'brightgreen'; - } else if (status === 8) { - badgeData.text[1] = 'seems down'; - badgeData.colorscheme = 'orange'; - } else if (status === 9) { - badgeData.text[1] = 'down'; - badgeData.colorscheme = 'red'; - } else { - badgeData.text[1] = 'invalid'; - badgeData.colorscheme = 'lightgrey'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - // Uptime Robot ratio integration. // API documentation : https://uptimerobot.com/api camp.route(/^\/uptimerobot\/ratio(\/[^/]+)?\/(.*)\.(svg|png|gif|jpg|json)$/, diff --git a/services/base.js b/services/base.js index a5450c6ea6a0d..319ab3952206c 100644 --- a/services/base.js +++ b/services/base.js @@ -1,7 +1,12 @@ 'use strict' const Joi = require('joi') -const { NotFound, InvalidResponse, Inaccessible } = require('./errors') +const { + NotFound, + InvalidResponse, + Inaccessible, + InvalidParameter, +} = require('./errors') const queryString = require('query-string') const { makeLogo, @@ -144,7 +149,7 @@ class BaseService { try { return await this.handle(namedParams, queryParams) } catch (error) { - if (error instanceof NotFound) { + if (error instanceof NotFound || error instanceof InvalidParameter) { return { message: error.prettyMessage, color: 'red', diff --git a/services/errors.js b/services/errors.js index 68131efc35837..ad8c0707740f4 100644 --- a/services/errors.js +++ b/services/errors.js @@ -69,8 +69,23 @@ class Inaccessible extends ShieldsRuntimeError { } } +class InvalidParameter extends ShieldsRuntimeError { + get name() { + return 'InvalidParameter' + } + get defaultPrettyMessage() { + return 'invalid parameter' + } + + constructor(props) { + const message = 'Invalid Parameter' + super(props, message) + } +} + module.exports = { NotFound, InvalidResponse, Inaccessible, + InvalidParameter, } diff --git a/services/uptimerobot/uptimerobot-status.service.js b/services/uptimerobot/uptimerobot-status.service.js new file mode 100644 index 0000000000000..57e6a164686a6 --- /dev/null +++ b/services/uptimerobot/uptimerobot-status.service.js @@ -0,0 +1,105 @@ +'use strict' + +const Joi = require('joi') +const { BaseJsonService } = require('../base') +const { InvalidParameter, InvalidResponse } = require('../errors') + +// https://uptimerobot.com/api +// POST getMonitors +const errorResponseSchema = Joi.object({ + stat: Joi.equal('fail').required(), + error: Joi.object({ + message: Joi.string(), + }), +}).required() + +const successResponseSchema = Joi.object({ + stat: Joi.equal('ok').required(), + monitors: Joi.array() + .length(1) + .items( + Joi.object({ + status: Joi.equal(0, 1, 2, 8, 9).required(), + }) + ) + .required(), +}).required() + +const schema = Joi.alternatives().try([ + errorResponseSchema, + successResponseSchema, +]) + +module.exports = class UptimeRobotStatus extends BaseJsonService { + static get category() { + return 'other' + } + + static get defaultBadgeData() { + return { + label: 'status', + } + } + + static get url() { + return { + base: 'uptimerobot/status', + format: '(.*)', + capture: ['monitorApiKey'], + } + } + + static get examples() { + return [ + { + title: 'Uptime Robot status', + previewUrl: 'm778918918-3e92c097147760ee39d02d36', + }, + ] + } + + async handle({ monitorApiKey }) { + // A monitor API key must start with "m". + if (!monitorApiKey.startsWith('m')) { + throw new InvalidParameter({ + prettyMessage: 'must use a monitor-specific api key', + }) + } + + const { stat, error = {}, monitors } = await this._requestJson({ + schema, + url: 'https://api.uptimerobot.com/v2/getMonitors', + options: { + method: 'POST', + headers: { + 'cache-control': 'no-cache', + 'content-type': 'application/x-www-form-urlencoded', + }, + form: { + api_key: monitorApiKey, + format: 'json', + }, + }, + }) + + if (stat === 'fail') { + const { message } = error + throw new InvalidResponse({ prettyMessage: message }) + } + + switch (monitors[0].status) { + case 0: + return { message: 'paused', color: 'yellow' } + case 1: + return { message: 'not checked yet', color: 'yellowgreen' } + case 2: + return { message: 'up', color: 'brightgreen' } + case 8: + return { message: 'seems down', color: 'orange' } + case 9: + return { message: 'down', color: 'red' } + default: + throw Error('Should not get here due to validation') + } + } +} diff --git a/services/uptimerobot/uptimerobot.tester.js b/services/uptimerobot/uptimerobot.tester.js index 37d4563ec5e80..b539aedddc423 100644 --- a/services/uptimerobot/uptimerobot.tester.js +++ b/services/uptimerobot/uptimerobot.tester.js @@ -3,8 +3,12 @@ const Joi = require('joi') const ServiceTester = require('../service-tester') -const isUptimeStatus = Joi.string().regex( - /^(paused|not checked yet|up|seems down|down)$/ +const isUptimeStatus = Joi.string().valid( + 'paused', + 'not checked yet', + 'up', + 'seems down', + 'down' ) const { isPercentage } = require('../test-validators') const { invalidJSON } = require('../response-fixtures') @@ -27,7 +31,7 @@ t.create('Uptime Robot: Status (invalid, correct format)') t.create('Uptime Robot: Status (invalid, incorrect format)') .get('/status/not-a-service.json') - .expectJSON({ name: 'status', value: 'must use a monitor key' }) + .expectJSON({ name: 'status', value: 'must use a monitor-specific api key' }) t.create('Uptime Robot: Status (unspecified error)') .get('/status/m778918918-3e92c097147760ee39d02d36.json') @@ -36,7 +40,7 @@ t.create('Uptime Robot: Status (unspecified error)') .post('/v2/getMonitors') .reply(200, '{"stat": "fail"}') ) - .expectJSON({ name: 'status', value: 'vendor error' }) + .expectJSON({ name: 'status', value: 'invalid' }) t.create('Uptime Robot: Status (connection error)') .get('/status/m778918918-3e92c097147760ee39d02d36.json') @@ -59,7 +63,7 @@ t.create('Uptime Robot: Status (unexpected response, valid json)') .post('/v2/getMonitors') .reply(200, '[]') ) - .expectJSON({ name: 'status', value: 'invalid' }) + .expectJSON({ name: 'status', value: 'invalid json response' }) t.create('Uptime Robot: Status (unexpected response, invalid json)') .get('/status/m778918918-3e92c097147760ee39d02d36.json') @@ -68,7 +72,7 @@ t.create('Uptime Robot: Status (unexpected response, invalid json)') .post('/v2/getMonitors') .reply(invalidJSON) ) - .expectJSON({ name: 'status', value: 'inaccessible' }) + .expectJSON({ name: 'status', value: 'unparseable json response' }) t.create('Uptime Robot: Percentage (valid)') .get('/ratio/m778918918-3e92c097147760ee39d02d36.json') From 5969533d1cc91fb6e4ef209700338190c77cdf46 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Wed, 8 Aug 2018 23:03:11 -0400 Subject: [PATCH 02/15] Add debug logging --- lib/logging-symbols.js | 14 ++++++++++ services/base.js | 63 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 lib/logging-symbols.js diff --git a/lib/logging-symbols.js b/lib/logging-symbols.js new file mode 100644 index 0000000000000..2a8a6b9161332 --- /dev/null +++ b/lib/logging-symbols.js @@ -0,0 +1,14 @@ +'use strict' + +module.exports = { + chef: '\uD83D\uDC69\u200D\uD83C\uDF73', + ticket: '\uD83C\uDF9F ', + crayon: '\uD83D\uDD8D ', + stop: '\uD83D\uDED1', + bomb: '\uD83D\uDCA3', + shield: '\uD83D\uDEE1 ', + shrug: '\uD83E\uDD37', + bathtub: '\uD83D\uDEC0', + bowAndArrow: '\uD83C\uDFF9', + bullseye: '\uD83C\uDFAF', +} diff --git a/services/base.js b/services/base.js index 319ab3952206c..d9dc1192c2c76 100644 --- a/services/base.js +++ b/services/base.js @@ -15,6 +15,7 @@ const { setBadgeColor, } = require('../lib/badge-data') const { checkErrorResponse, asJson } = require('../lib/error-helper') +const sym = require('../lib/logging-symbols') class BaseService { constructor({ sendAndCacheRequest }, { handleInternalErrors }) { @@ -39,6 +40,14 @@ class BaseService { // Metadata + /** + * When true, enable debugging on this service. Useful for troubleshooting + * a live server or in conjunction with `.only()` chained onto a service test. + */ + static get debug() { + return false + } + /** * Name of the category to sort this badge into (eg. "build"). Used to sort * the badges on the main shields.io website. @@ -146,10 +155,19 @@ class BaseService { } async invokeHandler(namedParams, queryParams) { + const { debug } = this.constructor + if (debug) { + console.log(sym.chef, 'Service class', this.constructor.name) + console.log(sym.ticket, 'Named params', namedParams) + console.log(sym.crayon, 'Query params', queryParams) + } try { return await this.handle(namedParams, queryParams) } catch (error) { if (error instanceof NotFound || error instanceof InvalidParameter) { + if (debug) { + console.log(sym.stop, 'Handled error', error) + } return { message: error.prettyMessage, color: 'red', @@ -158,18 +176,28 @@ class BaseService { error instanceof InvalidResponse || error instanceof Inaccessible ) { + if (debug) { + console.log(sym.stop, 'Handled error', error) + } return { message: error.prettyMessage, color: 'lightgray', } } else if (this._handleInternalErrors) { - console.log(error) + if (debug) { + console.log(sym.bomb, 'Unhandled internal error', error) + } else { + console.log(error) + } return { label: 'shields', message: 'internal error', color: 'lightgray', } } else { + if (debug) { + console.log(sym.bomb, 'Unhandled internal error', error) + } throw error } } @@ -219,6 +247,7 @@ class BaseService { } static register(camp, handleRequest, { handleInternalErrors }) { + const { debug } = this const ServiceClass = this // In a static context, "this" is the class. camp.route( @@ -237,6 +266,9 @@ class BaseService { namedParams, queryParams ) + if (debug) { + console.log(sym.shield, 'Service data', serviceData) + } const badgeData = this._makeBadgeData(queryParams, serviceData) // Assumes the final capture group is the extension @@ -250,34 +282,59 @@ class BaseService { class BaseJsonService extends BaseService { static _validate(json, schema) { + const { debug } = this const { error, value } = Joi.validate(json, schema, { allowUnknown: true, stripUnknown: true, }) if (error) { + if (debug) { + console.log(sym.shrug, 'Response did not match schema', error.message) + } throw new InvalidResponse({ prettyMessage: 'invalid json response', underlyingError: error, }) } else { + if (debug) { + console.log(sym.bathtub, 'JSON after validation', value) + } return value } } async _requestJson({ schema, url, options = {}, notFoundMessage }) { + const { debug } = this.constructor if (!schema || !schema.isJoi) { throw Error('A Joi schema is required') } - return this._sendAndCacheRequest(url, { + const mergedOptions = { ...{ headers: { Accept: 'application/json' } }, ...options, - }) + } + if (debug) { + console.log(sym.bowAndArrow, 'request URL', url) + console.log(sym.bowAndArrow, 'request options', mergedOptions) + } + return this._sendAndCacheRequest(url, mergedOptions) + .then(({ res, buffer }) => { + if (debug) { + console.log(sym.bullseye, 'Status code', res.statusCode) + } + return { res, buffer } + }) .then( checkErrorResponse.asPromise( notFoundMessage ? { notFoundMessage: notFoundMessage } : undefined ) ) .then(asJson) + .then(json => { + if (debug) { + console.log(sym.bullseye, 'JSON before validation', json) + } + return json + }) .then(json => this.constructor._validate(json, schema)) } } From 34b25f96fc5982ac2a46522a6d0a1bc844196874 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Wed, 8 Aug 2018 23:16:22 -0400 Subject: [PATCH 03/15] Rewrote Uptime Robot ratio and refactor --- lib/all-badge-examples.js | 10 -- server.js | 66 ------------- services/uptimerobot/uptimerobot-base.js | 97 +++++++++++++++++++ .../uptimerobot/uptimerobot-ratio.service.js | 48 +++++++++ .../uptimerobot/uptimerobot-status.service.js | 74 ++------------ services/uptimerobot/uptimerobot.tester.js | 10 +- 6 files changed, 160 insertions(+), 145 deletions(-) create mode 100644 services/uptimerobot/uptimerobot-base.js create mode 100644 services/uptimerobot/uptimerobot-ratio.service.js diff --git a/lib/all-badge-examples.js b/lib/all-badge-examples.js index 23437462a3e1c..bd5a860276fb6 100644 --- a/lib/all-badge-examples.js +++ b/lib/all-badge-examples.js @@ -1671,16 +1671,6 @@ const allBadgeExamples = [ previewUri: '/swagger/valid/2.0/https/raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v2.0/json/petstore-expanded.json.svg', }, - { - title: 'Uptime Robot ratio (30 days)', - previewUri: - '/uptimerobot/ratio/m778918918-3e92c097147760ee39d02d36.svg', - }, - { - title: 'Uptime Robot ratio (7 days)', - previewUri: - '/uptimerobot/ratio/7/m778918918-3e92c097147760ee39d02d36.svg', - }, { title: 'Eclipse Marketplace', previewUri: '/eclipse-marketplace/favorites/notepad4e.svg', diff --git a/server.js b/server.js index 6055b21eb9a6a..2171b833b47d3 100644 --- a/server.js +++ b/server.js @@ -6691,72 +6691,6 @@ cache(function(data, match, sendBadge, request) { }); })); -// Uptime Robot ratio integration. -// API documentation : https://uptimerobot.com/api -camp.route(/^\/uptimerobot\/ratio(\/[^/]+)?\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var numberOfDays = match[1]; // eg, 7, null if querying 30 - var monitorApiKey = match[2]; // eg, m778918918-3e92c097147760ee39d02d36 - var format = match[3]; - var badgeData = getBadgeData('uptime', data); - if (numberOfDays) { - numberOfDays = numberOfDays.slice(1); - } else { - numberOfDays = '30'; - } - var options = { - method: 'POST', - json: true, - body: { - "api_key": monitorApiKey, - "custom_uptime_ratios": numberOfDays, - "format": "json", - }, - uri: 'https://api.uptimerobot.com/v2/getMonitors', - }; - // A monitor API key must start with "m" - if (monitorApiKey.substring(0, "m".length) !== "m") { - badgeData.text[1] = 'must use a monitor key'; - sendBadge(format, badgeData); - return; - } - request(options, function(err, res, json) { - if (err !== null || res.statusCode >= 500 || typeof json !== 'object') { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - if (json.stat === 'fail') { - badgeData.text[1] = 'vendor error'; - if (json.error && typeof json.error.message === 'string') { - badgeData.text[1] = json.error.message; - } - badgeData.colorscheme = 'lightgrey'; - sendBadge(format, badgeData); - return; - } - var percent = parseFloat(json.monitors[0].custom_uptime_ratio); - badgeData.text[1] = percent + '%'; - if (percent <= 10) { - badgeData.colorscheme = 'red'; - } else if (percent <= 30) { - badgeData.colorscheme = 'yellow'; - } else if (percent <= 50) { - badgeData.colorscheme = 'yellowgreen'; - } else if (percent <= 70) { - badgeData.colorscheme = 'green'; - } else { - badgeData.colorscheme = 'brightgreen'; - } - sendBadge(format, badgeData); - } catch (e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - // Discord integration camp.route(/^\/discord\/([^/]+)\.(svg|png|gif|jpg|json)$/, cache((data, match, sendBadge, request) => { diff --git a/services/uptimerobot/uptimerobot-base.js b/services/uptimerobot/uptimerobot-base.js new file mode 100644 index 0000000000000..b4494556abf93 --- /dev/null +++ b/services/uptimerobot/uptimerobot-base.js @@ -0,0 +1,97 @@ +'use strict' + +const Joi = require('joi') +const { BaseJsonService } = require('../base') +const { InvalidParameter, InvalidResponse } = require('../errors') + +// https://uptimerobot.com/api +// POST getMonitors +const errorResponse = Joi.object({ + stat: Joi.equal('fail').required(), + error: Joi.object({ + message: Joi.string(), + }).default({}), +}).required() + +const monitor = Joi.object({ + status: Joi.equal(0, 1, 2, 8, 9).required(), +}) + +const monitorWithUptime = monitor.keys({ + custom_uptime_ratio: Joi.string() + .regex(/^\d*\.\d{3}$/) + .required(), +}) + +const singleMonitorResponse = Joi.alternatives( + errorResponse, + Joi.object({ + stat: Joi.equal('ok').required(), + monitors: Joi.array() + .length(1) + .items(monitor) + .required(), + }).required() +) + +const singleMonitorResponseWithUptime = Joi.alternatives( + errorResponse, + Joi.object({ + stat: Joi.equal('ok').required(), + monitors: Joi.array() + .length(1) + .items(monitorWithUptime) + .required(), + }).required() +) + +module.exports = class UptimeRobotBase extends BaseJsonService { + static get category() { + return 'other' + } + + static ensureIsMonitorApiKey(value) { + // A monitor API key must start with "m". + if (!value.startsWith('m')) { + throw new InvalidParameter({ + prettyMessage: 'must use a monitor-specific api key', + }) + } + } + + async fetch({ monitorApiKey, numberOfDays }) { + this.constructor.ensureIsMonitorApiKey(monitorApiKey) + + let opts, schema + if (numberOfDays) { + opts = { custom_uptime_ratios: numberOfDays } + schema = singleMonitorResponseWithUptime + } else { + opts = {} + schema = singleMonitorResponse + } + const { stat, error, monitors } = await this._requestJson({ + schema, + url: 'https://api.uptimerobot.com/v2/getMonitors', + options: { + method: 'POST', + headers: { + 'cache-control': 'no-cache', + 'content-type': 'application/x-www-form-urlencoded', + }, + form: { + api_key: monitorApiKey, + format: 'json', + ...opts, + }, + }, + }) + + if (stat === 'fail') { + const { message } = error + throw new InvalidResponse({ prettyMessage: message || 'service error' }) + } + + return { monitors } + } +} diff --git a/services/uptimerobot/uptimerobot-ratio.service.js b/services/uptimerobot/uptimerobot-ratio.service.js new file mode 100644 index 0000000000000..f8d7c584f42c0 --- /dev/null +++ b/services/uptimerobot/uptimerobot-ratio.service.js @@ -0,0 +1,48 @@ +'use strict' + +const { colorScale } = require('../../lib/color-formatters') +const UptimeRobotBase = require('./uptimerobot-base') + +const ratioColor = colorScale([10, 30, 50, 70]) + +module.exports = class UptimeRobotRatio extends UptimeRobotBase { + static get defaultBadgeData() { + return { + label: 'uptime', + } + } + + static get url() { + return { + base: 'uptimerobot/ratio', + format: '(?:([\\d+])/)?(.*)', + capture: ['numberOfDays', 'monitorApiKey'], + } + } + + static get examples() { + return [ + { + title: 'Uptime Robot ratio (30 days)', + previewUrl: 'm778918918-3e92c097147760ee39d02d36', + }, + { + title: 'Uptime Robot ratio (7 days)', + previewUrl: '7/m778918918-3e92c097147760ee39d02d36', + }, + ] + } + + static async render({ ratio }) { + return { + message: `${ratio}%`, + color: ratioColor(ratio), + } + } + + async handle({ numberOfDays = 30, monitorApiKey }) { + const { monitors } = await this.fetch({ monitorApiKey, numberOfDays }) + const ratio = Number.parseFloat(monitors[0].custom_uptime_ratio) + return this.constructor.render({ ratio }) + } +} diff --git a/services/uptimerobot/uptimerobot-status.service.js b/services/uptimerobot/uptimerobot-status.service.js index 57e6a164686a6..50e5106ea0317 100644 --- a/services/uptimerobot/uptimerobot-status.service.js +++ b/services/uptimerobot/uptimerobot-status.service.js @@ -1,40 +1,8 @@ 'use strict' -const Joi = require('joi') -const { BaseJsonService } = require('../base') -const { InvalidParameter, InvalidResponse } = require('../errors') - -// https://uptimerobot.com/api -// POST getMonitors -const errorResponseSchema = Joi.object({ - stat: Joi.equal('fail').required(), - error: Joi.object({ - message: Joi.string(), - }), -}).required() - -const successResponseSchema = Joi.object({ - stat: Joi.equal('ok').required(), - monitors: Joi.array() - .length(1) - .items( - Joi.object({ - status: Joi.equal(0, 1, 2, 8, 9).required(), - }) - ) - .required(), -}).required() - -const schema = Joi.alternatives().try([ - errorResponseSchema, - successResponseSchema, -]) - -module.exports = class UptimeRobotStatus extends BaseJsonService { - static get category() { - return 'other' - } +const UptimeRobotBase = require('./uptimerobot-base') +module.exports = class UptimeRobotStatus extends UptimeRobotBase { static get defaultBadgeData() { return { label: 'status', @@ -58,36 +26,8 @@ module.exports = class UptimeRobotStatus extends BaseJsonService { ] } - async handle({ monitorApiKey }) { - // A monitor API key must start with "m". - if (!monitorApiKey.startsWith('m')) { - throw new InvalidParameter({ - prettyMessage: 'must use a monitor-specific api key', - }) - } - - const { stat, error = {}, monitors } = await this._requestJson({ - schema, - url: 'https://api.uptimerobot.com/v2/getMonitors', - options: { - method: 'POST', - headers: { - 'cache-control': 'no-cache', - 'content-type': 'application/x-www-form-urlencoded', - }, - form: { - api_key: monitorApiKey, - format: 'json', - }, - }, - }) - - if (stat === 'fail') { - const { message } = error - throw new InvalidResponse({ prettyMessage: message }) - } - - switch (monitors[0].status) { + static async render({ status }) { + switch (status) { case 0: return { message: 'paused', color: 'yellow' } case 1: @@ -102,4 +42,10 @@ module.exports = class UptimeRobotStatus extends BaseJsonService { throw Error('Should not get here due to validation') } } + + async handle({ monitorApiKey }) { + const { monitors } = await this.fetch({ monitorApiKey }) + const { status } = monitors[0] + return this.constructor.render({ status }) + } } diff --git a/services/uptimerobot/uptimerobot.tester.js b/services/uptimerobot/uptimerobot.tester.js index b539aedddc423..cce9f1a383923 100644 --- a/services/uptimerobot/uptimerobot.tester.js +++ b/services/uptimerobot/uptimerobot.tester.js @@ -40,7 +40,7 @@ t.create('Uptime Robot: Status (unspecified error)') .post('/v2/getMonitors') .reply(200, '{"stat": "fail"}') ) - .expectJSON({ name: 'status', value: 'invalid' }) + .expectJSON({ name: 'status', value: 'service error' }) t.create('Uptime Robot: Status (connection error)') .get('/status/m778918918-3e92c097147760ee39d02d36.json') @@ -98,7 +98,7 @@ t.create('Uptime Robot: Percentage (invalid, correct format)') t.create('Uptime Robot: Percentage (invalid, incorrect format)') .get('/ratio/not-a-service.json') - .expectJSON({ name: 'uptime', value: 'must use a monitor key' }) + .expectJSON({ name: 'uptime', value: 'must use a monitor-specific api key' }) t.create('Uptime Robot: Percentage (unspecified error)') .get('/ratio/m778918918-3e92c097147760ee39d02d36.json') @@ -107,7 +107,7 @@ t.create('Uptime Robot: Percentage (unspecified error)') .post('/v2/getMonitors') .reply(200, '{"stat": "fail"}') ) - .expectJSON({ name: 'uptime', value: 'vendor error' }) + .expectJSON({ name: 'uptime', value: 'service error' }) t.create('Uptime Robot: Percentage (connection error)') .get('/ratio/m778918918-3e92c097147760ee39d02d36.json') @@ -130,7 +130,7 @@ t.create('Uptime Robot: Percentage (unexpected response, valid json)') .post('/v2/getMonitors') .reply(200, '[]') ) - .expectJSON({ name: 'uptime', value: 'invalid' }) + .expectJSON({ name: 'uptime', value: 'invalid json response' }) t.create('Uptime Robot: Percentage (unexpected response, invalid json)') .get('/ratio/m778918918-3e92c097147760ee39d02d36.json') @@ -139,4 +139,4 @@ t.create('Uptime Robot: Percentage (unexpected response, invalid json)') .post('/v2/getMonitors') .reply(invalidJSON) ) - .expectJSON({ name: 'uptime', value: 'inaccessible' }) + .expectJSON({ name: 'uptime', value: 'unparseable json response' }) From cb3d36bcaf26f5cd0f7daa2410684b43d5b03db6 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Fri, 10 Aug 2018 16:08:16 -0400 Subject: [PATCH 04/15] Remove obsolete --- lib/logging-symbols.js | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 lib/logging-symbols.js diff --git a/lib/logging-symbols.js b/lib/logging-symbols.js deleted file mode 100644 index 2a8a6b9161332..0000000000000 --- a/lib/logging-symbols.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict' - -module.exports = { - chef: '\uD83D\uDC69\u200D\uD83C\uDF73', - ticket: '\uD83C\uDF9F ', - crayon: '\uD83D\uDD8D ', - stop: '\uD83D\uDED1', - bomb: '\uD83D\uDCA3', - shield: '\uD83D\uDEE1 ', - shrug: '\uD83E\uDD37', - bathtub: '\uD83D\uDEC0', - bowAndArrow: '\uD83C\uDFF9', - bullseye: '\uD83C\uDFAF', -} From 69300524f0e362ab701725d559edbb9cafa73feb Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Fri, 10 Aug 2018 16:09:08 -0400 Subject: [PATCH 05/15] Reset / cleanup --- services/base.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/services/base.js b/services/base.js index 74f7754cde08a..aef4c833eb447 100644 --- a/services/base.js +++ b/services/base.js @@ -18,7 +18,6 @@ const { setBadgeColor, } = require('../lib/badge-data') const { checkErrorResponse, asJson } = require('../lib/error-helper') - // Config is loaded globally but it would be better to inject it. To do that, // there needs to be one instance of the service created at registration time, // which gets the config injected into it, instead of one instance per request. @@ -51,14 +50,6 @@ class BaseService { // Metadata - /** - * When true, enable debugging on this service. Useful for troubleshooting - * a live server or in conjunction with `.only()` chained onto a service test. - */ - static get debug() { - return false - } - /** * Name of the category to sort this badge into (eg. "build"). Used to sort * the badges on the main shields.io website. @@ -324,7 +315,6 @@ class BaseService { class BaseJsonService extends BaseService { static _validate(json, schema) { - const { debug } = this const { error, value } = Joi.validate(json, schema, { allowUnknown: true, stripUnknown: true, From f6ad187fec773d0d2bc819d4dce01eea89684181 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Fri, 10 Aug 2018 16:15:53 -0400 Subject: [PATCH 06/15] InvalidParameter: New error type --- services/base.js | 9 +++++++-- services/errors.js | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/services/base.js b/services/base.js index 67e510b8c499c..aef4c833eb447 100644 --- a/services/base.js +++ b/services/base.js @@ -4,7 +4,12 @@ const Joi = require('joi') // See available emoji at http://emoji.muan.co/ const emojic = require('emojic') const chalk = require('chalk') -const { NotFound, InvalidResponse, Inaccessible } = require('./errors') +const { + NotFound, + InvalidResponse, + Inaccessible, + InvalidParameter, +} = require('./errors') const queryString = require('query-string') const { makeLogo, @@ -164,7 +169,7 @@ class BaseService { try { return await this.handle(namedParams, queryParams) } catch (error) { - if (error instanceof NotFound) { + if (error instanceof NotFound || error instanceof InvalidParameter) { logTrace('outbound', emojic.noGoodWoman, 'Handled error', error) return { message: error.prettyMessage, diff --git a/services/errors.js b/services/errors.js index 68131efc35837..ad8c0707740f4 100644 --- a/services/errors.js +++ b/services/errors.js @@ -69,8 +69,23 @@ class Inaccessible extends ShieldsRuntimeError { } } +class InvalidParameter extends ShieldsRuntimeError { + get name() { + return 'InvalidParameter' + } + get defaultPrettyMessage() { + return 'invalid parameter' + } + + constructor(props) { + const message = 'Invalid Parameter' + super(props, message) + } +} + module.exports = { NotFound, InvalidResponse, Inaccessible, + InvalidParameter, } From dcaebb69a64efcc5371f4b600a33e800d40c659e Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Fri, 10 Aug 2018 16:16:07 -0400 Subject: [PATCH 07/15] Return inaccessible for 5xx errors from services --- lib/error-helper.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/error-helper.js b/lib/error-helper.js index 0fed84ae322cf..e66c885549e7f 100644 --- a/lib/error-helper.js +++ b/lib/error-helper.js @@ -1,6 +1,10 @@ 'use strict' -const { NotFound, InvalidResponse } = require('../services/errors') +const { + NotFound, + InvalidResponse, + Inaccessible, +} = require('../services/errors') const checkErrorResponse = function( badgeData, @@ -29,6 +33,8 @@ checkErrorResponse.asPromise = function({ notFoundMessage } = {}) { return async function({ buffer, res }) { if (res.statusCode === 404) { throw new NotFound({ prettyMessage: notFoundMessage }) + } else if (res.statusCode >= 500) { + throw new Inaccessible() } else if (res.statusCode !== 200) { const underlying = Error( `Got status code ${res.statusCode} (expected 200)` From f9597447f789feb99a955a6227d749f71243c514 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Fri, 10 Aug 2018 16:51:30 -0400 Subject: [PATCH 08/15] Add test for Inaccessible on 5xx --- lib/error-helper.js | 10 +++++----- lib/error-helper.spec.js | 29 +++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/lib/error-helper.js b/lib/error-helper.js index e66c885549e7f..71aee42843607 100644 --- a/lib/error-helper.js +++ b/lib/error-helper.js @@ -31,15 +31,15 @@ const checkErrorResponse = function( checkErrorResponse.asPromise = function({ notFoundMessage } = {}) { return async function({ buffer, res }) { + const underlyingError = Error( + `Got status code ${res.statusCode} (expected 200)` + ) if (res.statusCode === 404) { throw new NotFound({ prettyMessage: notFoundMessage }) } else if (res.statusCode >= 500) { - throw new Inaccessible() + throw new Inaccessible({ underlyingError }) } else if (res.statusCode !== 200) { - const underlying = Error( - `Got status code ${res.statusCode} (expected 200)` - ) - throw new InvalidResponse({ underlyingError: underlying }) + throw new InvalidResponse({ underlyingError }) } return { buffer, res } } diff --git a/lib/error-helper.spec.js b/lib/error-helper.spec.js index 35729f06df695..55328b9ee9259 100644 --- a/lib/error-helper.spec.js +++ b/lib/error-helper.spec.js @@ -3,7 +3,11 @@ const chai = require('chai') const { assert, expect } = chai const { checkErrorResponse } = require('./error-helper') -const { NotFound, InvalidResponse } = require('../services/errors') +const { + NotFound, + InvalidResponse, + Inaccessible, +} = require('../services/errors') chai.use(require('chai-as-promised')) @@ -86,8 +90,8 @@ describe('async error handler', function() { }) }) - context('when status is 500', function() { - const res = { statusCode: 500 } + context('when status is 499', function() { + const res = { statusCode: 499 } it('throws InvalidResponse', async function() { try { @@ -96,10 +100,27 @@ describe('async error handler', function() { } catch (e) { expect(e).to.be.an.instanceof(InvalidResponse) expect(e.message).to.equal( - 'Invalid Response: Got status code 500 (expected 200)' + 'Invalid Response: Got status code 499 (expected 200)' ) expect(e.prettyMessage).to.equal('invalid') } }) }) + + context('when status is 5xx', function() { + const res = { statusCode: 503 } + + it('throws Inaccessible', async function() { + try { + await checkErrorResponse.asPromise()({ res }) + expect.fail('Expected to throw') + } catch (e) { + expect(e).to.be.an.instanceof(Inaccessible) + expect(e.message).to.equal( + 'Inaccessible: Got status code 503 (expected 200)' + ) + expect(e.prettyMessage).to.equal('inaccessible') + } + }) + }) }) From 38eedb1daee70f60caf5de9dfe6ee9b5eda0a4d7 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Fri, 10 Aug 2018 16:52:27 -0400 Subject: [PATCH 09/15] Tweak text --- lib/error-helper.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/error-helper.spec.js b/lib/error-helper.spec.js index 55328b9ee9259..92f33cef83062 100644 --- a/lib/error-helper.spec.js +++ b/lib/error-helper.spec.js @@ -90,7 +90,7 @@ describe('async error handler', function() { }) }) - context('when status is 499', function() { + context('when status is 4xx', function() { const res = { statusCode: 499 } it('throws InvalidResponse', async function() { From 9ad127e441a1ff6b9351ed614a520ec7e308f32a Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Fri, 10 Aug 2018 17:00:23 -0400 Subject: [PATCH 10/15] Add tests for named error types --- services/base.spec.js | 80 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 5 deletions(-) diff --git a/services/base.spec.js b/services/base.spec.js index 5faf615d7eec2..68a86e5802701 100644 --- a/services/base.spec.js +++ b/services/base.spec.js @@ -4,6 +4,12 @@ const { expect } = require('chai') const { test, given, forCases } = require('sazerac') const sinon = require('sinon') +const { + NotFound, + Inaccessible, + InvalidResponse, + InvalidParameter, +} = require('./errors') const { BaseService } = require('./base') require('../lib/register-chai-plugins.spec') @@ -125,7 +131,7 @@ describe('BaseService', () => { }) }) - describe('Error handling', function() { + describe.only('Error handling', function() { it('Handles internal errors', async function() { const serviceInstance = new DummyService( {}, @@ -134,15 +140,79 @@ describe('BaseService', () => { serviceInstance.handle = () => { throw Error("I've made a huge mistake") } - const serviceData = await serviceInstance.invokeHandler({ - namedParamA: 'bar.bar.bar', - }) - expect(serviceData).to.deep.equal({ + expect( + await serviceInstance.invokeHandler({ + namedParamA: 'bar.bar.bar', + }) + ).to.deep.equal({ color: 'lightgray', label: 'shields', message: 'internal error', }) }) + + describe('Handles known subtypes of ShieldsInternalError', function() { + let serviceInstance + beforeEach(function() { + serviceInstance = new DummyService({}, {}) + }) + + it('handles NotFound errors', async function() { + serviceInstance.handle = () => { + throw new NotFound() + } + expect( + await serviceInstance.invokeHandler({ + namedParamA: 'bar.bar.bar', + }) + ).to.deep.equal({ + color: 'red', + message: 'not found', + }) + }) + + it('handles Inaccessible errors', async function() { + serviceInstance.handle = () => { + throw new Inaccessible() + } + expect( + await serviceInstance.invokeHandler({ + namedParamA: 'bar.bar.bar', + }) + ).to.deep.equal({ + color: 'lightgray', + message: 'inaccessible', + }) + }) + + it('handles InvalidResponse errors', async function() { + serviceInstance.handle = () => { + throw new InvalidResponse() + } + expect( + await serviceInstance.invokeHandler({ + namedParamA: 'bar.bar.bar', + }) + ).to.deep.equal({ + color: 'lightgray', + message: 'invalid', + }) + }) + + it('handles InvalidParameter errors', async function() { + serviceInstance.handle = () => { + throw new InvalidParameter() + } + expect( + await serviceInstance.invokeHandler({ + namedParamA: 'bar.bar.bar', + }) + ).to.deep.equal({ + color: 'red', + message: 'invalid parameter', + }) + }) + }) }) describe('_makeBadgeData', function() { From b11cf843c658f260af60db7628fd7093f795515c Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Fri, 10 Aug 2018 17:10:54 -0400 Subject: [PATCH 11/15] Fix import --- services/uptimerobot/uptimerobot-base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/uptimerobot/uptimerobot-base.js b/services/uptimerobot/uptimerobot-base.js index b4494556abf93..a83cc1545a48c 100644 --- a/services/uptimerobot/uptimerobot-base.js +++ b/services/uptimerobot/uptimerobot-base.js @@ -1,7 +1,7 @@ 'use strict' const Joi = require('joi') -const { BaseJsonService } = require('../base') +const BaseJsonService = require('../base') const { InvalidParameter, InvalidResponse } = require('../errors') // https://uptimerobot.com/api From cbd714f03e79b855b90e2233fe33c9dc43a8e3f3 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Fri, 10 Aug 2018 17:13:48 -0400 Subject: [PATCH 12/15] =?UTF-8?q?Oops,=20that=E2=80=99s=20not=20merged=20y?= =?UTF-8?q?et?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/uptimerobot/uptimerobot-base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/uptimerobot/uptimerobot-base.js b/services/uptimerobot/uptimerobot-base.js index a83cc1545a48c..b4494556abf93 100644 --- a/services/uptimerobot/uptimerobot-base.js +++ b/services/uptimerobot/uptimerobot-base.js @@ -1,7 +1,7 @@ 'use strict' const Joi = require('joi') -const BaseJsonService = require('../base') +const { BaseJsonService } = require('../base') const { InvalidParameter, InvalidResponse } = require('../errors') // https://uptimerobot.com/api From 49efc10a8c6c45f100bea88e62ac8bbf51d4f6cf Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Sat, 11 Aug 2018 11:17:50 -0400 Subject: [PATCH 13/15] Update for #1889 --- services/uptimerobot/uptimerobot-base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/uptimerobot/uptimerobot-base.js b/services/uptimerobot/uptimerobot-base.js index b4494556abf93..33f8b9929b6f1 100644 --- a/services/uptimerobot/uptimerobot-base.js +++ b/services/uptimerobot/uptimerobot-base.js @@ -1,7 +1,7 @@ 'use strict' const Joi = require('joi') -const { BaseJsonService } = require('../base') +const BaseJsonService = require('../base-json') const { InvalidParameter, InvalidResponse } = require('../errors') // https://uptimerobot.com/api From 8460d412a4901f27db83cbf16f222e439db19b06 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Sun, 12 Aug 2018 09:54:03 -0400 Subject: [PATCH 14/15] Fix category after #1903 --- lib/all-badge-examples.js | 15 --------------- services/uptimerobot/uptimerobot-ratio.service.js | 4 ++++ .../uptimerobot/uptimerobot-status.service.js | 4 ++++ 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/lib/all-badge-examples.js b/lib/all-badge-examples.js index 2bf00a133c59b..c50fce816880e 100644 --- a/lib/all-badge-examples.js +++ b/lib/all-badge-examples.js @@ -1466,21 +1466,6 @@ const allBadgeExamples = [ keywords: ['website'], documentation: websiteDoc, }, - { - title: 'Uptime Robot status', - previewUri: - '/uptimerobot/status/m778918918-3e92c097147760ee39d02d36.svg', - }, - { - title: 'Uptime Robot ratio (30 days)', - previewUri: - '/uptimerobot/ratio/m778918918-3e92c097147760ee39d02d36.svg', - }, - { - title: 'Uptime Robot ratio (7 days)', - previewUri: - '/uptimerobot/ratio/7/m778918918-3e92c097147760ee39d02d36.svg', - }, ], }, { diff --git a/services/uptimerobot/uptimerobot-ratio.service.js b/services/uptimerobot/uptimerobot-ratio.service.js index f8d7c584f42c0..4aad743646d8a 100644 --- a/services/uptimerobot/uptimerobot-ratio.service.js +++ b/services/uptimerobot/uptimerobot-ratio.service.js @@ -6,6 +6,10 @@ const UptimeRobotBase = require('./uptimerobot-base') const ratioColor = colorScale([10, 30, 50, 70]) module.exports = class UptimeRobotRatio extends UptimeRobotBase { + static get category() { + return 'monitoring' + } + static get defaultBadgeData() { return { label: 'uptime', diff --git a/services/uptimerobot/uptimerobot-status.service.js b/services/uptimerobot/uptimerobot-status.service.js index 50e5106ea0317..c951d22750971 100644 --- a/services/uptimerobot/uptimerobot-status.service.js +++ b/services/uptimerobot/uptimerobot-status.service.js @@ -3,6 +3,10 @@ const UptimeRobotBase = require('./uptimerobot-base') module.exports = class UptimeRobotStatus extends UptimeRobotBase { + static get category() { + return 'monitoring' + } + static get defaultBadgeData() { return { label: 'status', From 7c92323e3f44bf4fa260e069c37348b95268d7a8 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Mon, 13 Aug 2018 00:34:57 -0400 Subject: [PATCH 15/15] Define the (correct) category in the superclass --- services/uptimerobot/uptimerobot-base.js | 2 +- services/uptimerobot/uptimerobot-ratio.service.js | 4 ---- services/uptimerobot/uptimerobot-status.service.js | 4 ---- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/services/uptimerobot/uptimerobot-base.js b/services/uptimerobot/uptimerobot-base.js index 33f8b9929b6f1..28d7061e61972 100644 --- a/services/uptimerobot/uptimerobot-base.js +++ b/services/uptimerobot/uptimerobot-base.js @@ -47,7 +47,7 @@ const singleMonitorResponseWithUptime = Joi.alternatives( module.exports = class UptimeRobotBase extends BaseJsonService { static get category() { - return 'other' + return 'monitoring' } static ensureIsMonitorApiKey(value) { diff --git a/services/uptimerobot/uptimerobot-ratio.service.js b/services/uptimerobot/uptimerobot-ratio.service.js index 4aad743646d8a..f8d7c584f42c0 100644 --- a/services/uptimerobot/uptimerobot-ratio.service.js +++ b/services/uptimerobot/uptimerobot-ratio.service.js @@ -6,10 +6,6 @@ const UptimeRobotBase = require('./uptimerobot-base') const ratioColor = colorScale([10, 30, 50, 70]) module.exports = class UptimeRobotRatio extends UptimeRobotBase { - static get category() { - return 'monitoring' - } - static get defaultBadgeData() { return { label: 'uptime', diff --git a/services/uptimerobot/uptimerobot-status.service.js b/services/uptimerobot/uptimerobot-status.service.js index c951d22750971..50e5106ea0317 100644 --- a/services/uptimerobot/uptimerobot-status.service.js +++ b/services/uptimerobot/uptimerobot-status.service.js @@ -3,10 +3,6 @@ const UptimeRobotBase = require('./uptimerobot-base') module.exports = class UptimeRobotStatus extends UptimeRobotBase { - static get category() { - return 'monitoring' - } - static get defaultBadgeData() { return { label: 'status',