diff --git a/CHANGELOG.md b/CHANGELOG.md index d275a93..3c8b335 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# 2.0.0 / 2017-02-09 + +* Format synthetics.json file to make it human readable +* Add Developer documentation +* Add Contribution guide +* Add ability to update alerting +* Add command to get available locations +* Add ability to update synthetics configuration +* Add support for non-SCRIPTED_BROWSER synthetics +* Better handling of New Relic errors + # 1.0.1 / 2017-01-04 * Updated README diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..03852d1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via github issues before making the change. + +Please note we have a code of conduct, please follow it in all your interactions with the project. + +## Pull Request Process + +* Update the README.md with details of changes to the interface. +* Ensure the travis build is passing for the given change (npm test). +* Increase the version numbers in any examples files and the README.md to the new version that this pull request would represent. The versioning scheme we use is http://semver.org/ +* Ensure any correlated issue numbers are included in the pull request. + +## Code of Conduct + +http://contributor-covenant.org/version/1/2/0/ + diff --git a/DEVELOPING.md b/DEVELOPING.md new file mode 100644 index 0000000..3fc179a --- /dev/null +++ b/DEVELOPING.md @@ -0,0 +1,43 @@ +# Development information + +## Prerequisites + +* node.js v6 or higher + +## Code Structure + +* bin/ + * command line interface and command line parsing code. bin/synthmanager.js is the entry point. yargs is used to do the command line parsing. +* lib/ + * code that implements the command line functionality. +* test/ + * tests. The tests use mocha, chai and testdouble.js. +* index.js + * library entrypoint that provides webdriver functionality for running tests locally. + +## Command Tasks + +### Linting + +``` +gulp lint +``` + +### Testing + +``` +gulp test +``` + +### Lint and Test in one Tasks + +``` +gulp +``` + +or + +``` +npm test +``` + diff --git a/README.md b/README.md index 6e57284..210f56b 100644 --- a/README.md +++ b/README.md @@ -66,26 +66,31 @@ $ synthmanager update --name "New Synthetic Name" ### Create a new synthetic ``` -synthmanager create --name --file +synthmanager create ``` + Create a synthetic in New Relic and a file to contain the synthetic code. The local file will be created in the directory the command was run. This is where you will put your test code. * --name - Name of synthetic. This is the name used in New Relic as well as how it should be refered to by other commands -* --file - Filename where the synthethics code should go. This file will be created under the 'synthetics' directory. The file should not already exist. +* --filename - Filename where the synthethics code should go. This file will be created under the 'synthetics' directory. The file should not already exist (required for SCRIPT_BROWSER and SCRIPT_API synthetics). * --frequency - Frequency to run the synthetic in minutes. This should be an integer. Possible values are: 1, 5, 10, 15, 30, 60, 360, 720, or 1440. The default is 10. * --locations - Locations to run the synthetic. This can be specified multiple times to specify multiple locations. +* --type - Type of synthetic to create. Possible values are: SIMPLE, BROWSER, SCRIPT_BROWSER, SCRIPT_API. +* --uri - URI that synthetic should check (required for SIMPLE and BROWSER synthetics). +* --emails - Email to send synthetic alerts to (can be specified multiple times). ### Update New Relic with synthetics code ``` -synthmanager update --name +synthmanager update ``` Update New Relic with the latest synthetic code for the specified synthetic. * --name - name of synthetic to update. This should be the name used when the synthetic was created. +* --filename - filename of synthetic to update. ### Import a synthetic from New Relic @@ -93,9 +98,12 @@ Update New Relic with the latest synthetic code for the specified synthetic. In order to import a New Relic Synthetic, the synthetic id is needed. This can be obtained from the New Relic Synthetics website. Navigate to the Synthetic to import and the id will be the last part of the URL (It is made up of 5 hexidecimal numbers separated by dashes). ``` -synthmanager import --name --id --file +synthmanager import ``` +--id - ID of synthetic in New Relic (this is part of the url when viewing the synthetic in New Relic) +--filename - File to store the synthetic code. + Import an existing synthetic from New Relic. ### Global options @@ -106,6 +114,38 @@ These options can be used with any command: * --verbose - Provide verbose logging output. * --debug - Provide debug logging output. +### See a list of available loctions + +Synthetics run from specified locations. You can get a list of available locations from the following command. + +``` +synthmanager locations +``` + +### Change synthetics configuration + +A synthetics configuration can be changed with the following command: + +``` +synthmanager config +``` + +The synthetic to change must be specified with one of the following options: + +* --name - name of synthetic to change. +* --id - id of synthetic to change. + +The following configuration changes can be made: + +* --frequency - Frequency to run the synthetic in minutes. This should be an integer. Possible values are: 1, 5, 10, 15, 30, 60, 360, 720, or 1440. The default is 10. +* --locations - Locations to run the synthetic. This can be specified multiple times to specify multiple locations. +* --uri - URI that synthetic should check (only for SIMPLE and BROWSER synthetics). +* --status - Is the synthetic enabled? (possible values: "ENABLED", "DISABLED", "MUTED") +* --rename - Change the name of the synthetic. +* --addemail - Add the specified email to alerting for the synthetic (this option can be specified multiple times). +* --rmemail - Remove the specified email from alerting for the synthetic. + + ## Configuration Configuration options can be changed by adding a 'synthetics.config.json' file in the base of the project. diff --git a/bin/cmds/config.js b/bin/cmds/config.js new file mode 100644 index 0000000..12d2334 --- /dev/null +++ b/bin/cmds/config.js @@ -0,0 +1,98 @@ +const dependencies = require('../../lib/dependency'); +const logger = require('winston'); +const _ = require('lodash'); + +exports.command = 'config'; +exports.desc = 'Change configuration options of a synthetic'; +exports.builder = { + name: { + alias: 'n', + desc: 'Name of synthetic to configure', + type: 'string', + }, + id: { + alias: 'i', + desc: 'Id of synthetic to configure', + type: 'string', + }, + frequency: { + desc: 'Frequency to run synthetic(in minutes)', + choices: [1, 5, 10, 15, 30, 60, 360, 720, 1440], + type: 'number', + }, + locations: { + desc: 'Locations to run synthetic', + type: 'array', + }, + uri: { + alias: 'u', + desc: 'URI for synthetic', + type: 'string', + }, + status: { + alias: 's', + desc: 'Is the synthetic enabled?', + choices: ['ENABLED', 'DISABLED', 'MUTED'], + }, + rename: { + desc: 'New name to use for synthetic', + type: 'string', + }, + addemail: { + desc: 'Add emails to alerting for synthetics (parameter can be specified multiple times)', + type: 'array', + }, + rmemail: { + desc: 'Remove email from alerting for synthetics', + type: 'string', + }, +} + +function validate(argv) { + if (_.isNil(argv.name) && _.isNil(argv.id)) { + throw new Error('ERROR: Either name or id must be specified'); + } + + const allOptions = [argv.frequency, argv.locations, argv.uri, argv.status, argv.rename, argv.addemail, argv.rmemail]; + + if (_.every(allOptions, _.isNil)) { + throw new Error('Error: No changes specified'); + } +} + +exports.handler = function (argv) { + require('../../lib/config/LoggingConfig')(argv); + + validate(argv); + + const config = require('../../lib/config/SyntheticsConfig').getConfig(argv); + + logger.verbose('Config: ' + argv.name + ':' + argv.id); + logger.verbose(argv); + + const changeConfigOrchestrator = dependencies(config).changeConfigOrchestrator; + + if (!_.isNil(argv.id)) { + changeConfigOrchestrator.changeConfigurationById( + argv.id, + argv.frequency, + argv.locations, + argv.uri, + argv.status, + argv.rename, + argv.addemail, + argv.rmemail + ); + } else if (!_.isNil(argv.name)) { + changeConfigOrchestrator.changeConfigurationByName( + argv.name, + argv.frequency, + argv.locations, + argv.uri, + argv.status, + argv.rename, + argv.addemail, + argv.rmemail + ); + } +} \ No newline at end of file diff --git a/bin/cmds/create.js b/bin/cmds/create.js index cdf537a..769727c 100644 --- a/bin/cmds/create.js +++ b/bin/cmds/create.js @@ -1,5 +1,6 @@ const dependencies = require('../../lib/dependency'); const logger = require('winston'); +const _ = require('lodash'); exports.command = 'create'; exports.desc = 'Create new synthetics monitor'; @@ -11,27 +12,64 @@ exports.builder = { }, filename: { alias: 'f', - desc: 'Filename to place synthetic code', - demand: 1 + desc: 'Filename to place synthetic code (require for SCRIPT_API and SCRIPT_BROWSER synthetics)' }, type: { alias: 't', desc: 'Type of synthetic to create', + choices: ['SIMPLE', 'BROWSER', 'SCRIPT_BROWSER', 'SCRIPT_API'], default: 'SCRIPT_BROWSER' }, frequency: { - desc: 'Frequency to run synthetic', - default: 10 + desc: 'Frequency to run synthetic(in minutes)', + choices: [1, 5, 10, 15, 30, 60, 360, 720, 1440], + default: 10, + type: 'number' }, locations: { desc: 'Locations to run synthetic', - default: ['AWS_US_WEST_1'] + default: ['AWS_US_WEST_1'], + type: 'array' + }, + uri: { + alias: 'u', + desc: 'URI for synthetic (required for SIMPLE and BROWSER synthetics)', + type: 'string' }, + emails: { + alias: 'e', + desc: 'Emails to send synthetic alerts to (can be specified multiple times)', + type: 'array', + } +} + +function validate(argv) { + if ((argv.type === 'SIMPLE') || (argv.type === 'BROWSER')) { + if (_.isNil(argv.uri)) { + throw new Error('ERROR: Missing uri argument'); + } + + if (!_.isNil(argv.filename)) { + throw new Error('ERROR: Unexpected filename argument'); + } + } + + if ((argv.type ==='SCRIPT_API') || (argv.type === 'SCRIPT_BROWSER')) { + if (_.isNil(argv.filename)) { + throw new Error('ERROR: Missing filename argument'); + } + + if (!_.isNil(argv.uri)) { + throw new Error('ERROR: Unexpected uri argument'); + } + } } exports.handler = function (argv) { require('../../lib/config/LoggingConfig')(argv); + validate(argv); + const config = require('../../lib/config/SyntheticsConfig').getConfig(argv); logger.verbose('Create: ' + argv.name + ':' + argv.filename); @@ -42,6 +80,9 @@ exports.handler = function (argv) { argv.locations, argv.type, argv.frequency, - argv.filename + argv.filename, + null, + argv.uri, + argv.emails ); } \ No newline at end of file diff --git a/bin/cmds/locations.js b/bin/cmds/locations.js new file mode 100644 index 0000000..d92f12c --- /dev/null +++ b/bin/cmds/locations.js @@ -0,0 +1,17 @@ +const dependencies = require('../../lib/dependency'); +const logger = require('winston'); +const _ = require('lodash'); + +exports.command = 'locations'; +exports.desc = 'List available locations for synthetics to run'; + +exports.handler = function (argv) { + require('../../lib/config/LoggingConfig')(argv); + + const config = require('../../lib/config/SyntheticsConfig').getConfig(argv); + + logger.verbose('Locations'); + logger.verbose(argv); + + dependencies(config).listLocationsOrchestrator.listLocations(); +} \ No newline at end of file diff --git a/lib/dependency.js b/lib/dependency.js index f5d1742..995d7a6 100644 --- a/lib/dependency.js +++ b/lib/dependency.js @@ -12,6 +12,8 @@ const syntheticsListFileServiceFactory = require('./service/SyntheticsListFileSe const createMonitorOrchestratorFactory = require('./orchestrator/CreateMonitorOrchestrator'); const updateMonitorOrchestratorFactory = require('./orchestrator/UpdateMonitorOrchestrator'); const importMonitorOrchestratorFactory = require('./orchestrator/ImportMonitorOrchestrator'); +const listLocationsOrchestratorFactory = require('./orchestrator/ListLocationsOrchestrator'); +const changeConfigOrchestratorFactory = require('./orchestrator/ChangeConfigOrchestrator'); module.exports = (config) => { const fileService = fileServiceFactory(fs, mkdirp); @@ -43,6 +45,14 @@ module.exports = (config) => { newRelicService, defaults ); + const listLocationsOrchestrator = listLocationsOrchestratorFactory( + newRelicService + ); + const changeConfigOrchestrator = changeConfigOrchestratorFactory( + newRelicService, + syntheticsListFileService + ); + return { fileService: fileService, @@ -51,6 +61,8 @@ module.exports = (config) => { syntheticsListFileService: syntheticsListFileService, createMonitorOrchestrator: createMonitorOrchestrator, updateMonitorOrchestrator: updateMonitorOrchestrator, - importMonitorOrchestrator: importMonitorOrchestrator + importMonitorOrchestrator: importMonitorOrchestrator, + listLocationsOrchestrator: listLocationsOrchestrator, + changeConfigOrchestrator: changeConfigOrchestrator, }; }; diff --git a/lib/orchestrator/ChangeConfigOrchestrator.js b/lib/orchestrator/ChangeConfigOrchestrator.js new file mode 100644 index 0000000..344fdaa --- /dev/null +++ b/lib/orchestrator/ChangeConfigOrchestrator.js @@ -0,0 +1,91 @@ +const async = require('async'); +const logger = require('winston'); +const _ = require('lodash'); + +class ChangeConfigOrchestrator { + constructor(newRelicService, syntheticsListFileService) { + this.newRelicService = newRelicService; + this.syntheticsListFileService = syntheticsListFileService; + } + + changeConfigurationById(id, frequency, locations, uri, status, rename, addAlertEmails, rmAlertEmail, callback) { + logger.verbose('ChangeConfigOrchestrator.changeConfigurationById'); + + async.parallel([ + ((next) => { + if (!_.isNil(frequency) || + !_.isNil(locations) || + !_.isNil(uri) || + !_.isNil(status) || + !_.isNil(rename) + ) { + this.newRelicService.updateMonitorSettings( + id, + frequency, + locations, + uri, + status, + rename, + next + ); + } + }).bind(this), + + ((next) => { + if (!_.isNil(addAlertEmails)) { + this.newRelicService.addAlertEmails( + id, + addAlertEmails, + next + ); + } + }).bind(this), + + ((next) => { + if (!_.isNil(rmAlertEmail)) { + this.newRelicService.removeAlertEmail(id, rmAlertEmail, next); + } + }).bind(this) + + ], (err) => { + if (err) { + if (callback) { + callback(err); + } else { + logger.error(err); + throw new Error(err); + } + } + }); + + } + + changeConfigurationByName(name, frequency, locations, uri, status, rename, addAlertEmails, rmAlertEmail) { + logger.verbose('ChangeConfigOrchestrator.changeConfigurationByName'); + + async.waterfall([ + ((next) => { + this.syntheticsListFileService.getSynthetic(name, (syntheticInfo, err) => { + if (err) { next(err); } + + const syntheticId = syntheticInfo.id; + next(null, syntheticId); + }); + }).bind(this), + + ((id, next) => { + this.changeConfigurationById(id, frequency, locations, uri, status, rename, addAlertEmails, rmAlertEmail, next); + }).bind(this), + + ], (err) => { + if (err) { + logger.error(err); + throw new Error(err); + } + }); + } +} + +module.exports = (newRelicService, syntheticsListFileService) => { + return new ChangeConfigOrchestrator(newRelicService, syntheticsListFileService); +}; \ No newline at end of file diff --git a/lib/orchestrator/CreateMonitorOrchestrator.js b/lib/orchestrator/CreateMonitorOrchestrator.js index 5c80ed6..649b31b 100644 --- a/lib/orchestrator/CreateMonitorOrchestrator.js +++ b/lib/orchestrator/CreateMonitorOrchestrator.js @@ -7,17 +7,21 @@ function syntheticUrlToId(syntheticUrl) { return _.last(syntheticUrl.split('/')); } -function createSyntheticFile(syntheticsFileService, filename, initialContent, callback) { - syntheticsFileService.exists(filename, (exists) => { - if (!exists) { - logger.verbose('Synthetics file does not exist, creating: ' + filename); - syntheticsFileService.createFile(filename, initialContent, function (nBytes, err) { - callback(err); - }); - } else { - callback(null); - } - }); +function createSyntheticFile(syntheticsFileService, type, filename, initialContent, callback) { + if ((type === 'SIMPLE') || (type === 'BROWSER')) { + callback(); + } else { + syntheticsFileService.exists(filename, (exists) => { + if (!exists) { + logger.verbose('Synthetics file does not exist, creating: ' + filename); + syntheticsFileService.createFile(filename, initialContent, function (nBytes, err) { + callback(err); + }); + } else { + callback(null); + } + }); + } } function createSyntheticInNewRelic( @@ -29,27 +33,39 @@ function createSyntheticInNewRelic( frequency, filename, status, + uri, + emails, callback ) { async.waterfall([ (next) => { - newRelicService.createSynthetic(name, locations, frequency, status, (syntheticUrl, err) => { + newRelicService.createSynthetic(name, locations, frequency, status, type, (syntheticUrl, err) => { if (err) { return next(err); } const syntheticId = syntheticUrlToId(syntheticUrl); next(null, syntheticId); - }); + }, + uri); }, (id, next) => { logger.debug('Adding Synthetic to file: ' + id + ' ' + filename); syntheticsListFileService.addSynthetic(id, name, filename, (err) => { - next(err); + next(err, id); }); + }, + + (id, next) => { + if (!_.isNil(emails)) { + logger.debug('Adding alerting for synthetic: ' + id); + newRelicService.addAlertEmails(id, emails, next); + } else { + next(); + } } + ], (err) => { - if (err) { throw err; } - callback(); + callback(err); }); } @@ -61,20 +77,23 @@ class CreateMonitorOrchestrator { this.defaults = defaults; } - createNewMonitor(name, locations, type, frequency, filename, callback) { + createNewMonitor(name, locations, type, frequency, filename, callback, uri, alertEmails) { logger.verbose('CreateMonitorOrchestrator.createNewMonitor: start'); logger.debug( 'name: ' + name + ', locations: ' + locations + ', type: ' + type + ', frequency: ' + frequency + - ', filename: ' + filename + ', filename: ' + filename + + ', uri: ' + uri + + ', emails: ' + alertEmails ); async.parallel([ function (next) { createSyntheticFile( this.syntheticsFileService, + type, filename, this.defaults.syntheticsContent, next @@ -91,20 +110,21 @@ class CreateMonitorOrchestrator { frequency, filename, 'ENABLED', + uri, + alertEmails, next ); }.bind(this) ], (err) => { - if (err) { throw err; } + if (err) { logger.error(err); throw err; } logger.verbose('CreateMonitorOrchestrator.createNewMonitor: complete'); - if (callback !== undefined) { + if (!_.isNil(callback)) { callback(); } }); } - } module.exports = (syntheticsFileService, newRelicService, syntheticsListFileService, defaults) => { diff --git a/lib/orchestrator/ListLocationsOrchestrator.js b/lib/orchestrator/ListLocationsOrchestrator.js new file mode 100644 index 0000000..550885c --- /dev/null +++ b/lib/orchestrator/ListLocationsOrchestrator.js @@ -0,0 +1,25 @@ +const logger = require('winston'); +const _ = require('lodash'); + +class ListLocationsOrchestrator { + constructor(newRelicService) { + this.newRelicService = newRelicService; + } + + listLocations() { + logger.verbose('ListLocationsOrchestrator.listLocations: start'); + + this.newRelicService.getAvailableLocations((err, locationList) => { + if (err) { logger.error(err); throw err; } + + _.forEach(locationList, (location) => { + console.log('%s: %s', location.label, location.name); + }); + }); + } + +} + +module.exports = (newRelicService) => { + return new ListLocationsOrchestrator(newRelicService); +}; \ No newline at end of file diff --git a/lib/service/NewRelicService.js b/lib/service/NewRelicService.js index 8db51e4..6873e5e 100644 --- a/lib/service/NewRelicService.js +++ b/lib/service/NewRelicService.js @@ -1,9 +1,14 @@ const _ = require('lodash'); const logger = require('winston'); +const htmlToText = require('html-to-text'); const SYNTHETICS_HOST = 'synthetics.newrelic.com'; const MONITORS_ENDPOINT = '/synthetics/api/v3/monitors'; const MONITORS_URL = 'https://' + SYNTHETICS_HOST + MONITORS_ENDPOINT; +const LOCATIONS_ENDPOINT = '/synthetics/api/v1/locations'; +const LOCATIONS_URL = 'https://' + SYNTHETICS_HOST + LOCATIONS_ENDPOINT; +const ALERTS_ENDPOINT = '/synthetics/api/v1/monitors'; +const ALERTS_URL = 'https://' + SYNTHETICS_HOST + ALERTS_ENDPOINT; class SyntheticsService { constructor(apikey, request) { @@ -26,7 +31,7 @@ class SyntheticsService { } _sendRequestToNewRelic(options, expectedResponse, operation, callback) { - logger.verbose('SyntheticsService._sendRequestToNewRelic: start: ' + operation); + logger.verbose('NewRelicService._sendRequestToNewRelic: start: ' + operation); logger.debug('Options: ' + JSON.stringify(options)); if (this.apikey === undefined) { @@ -43,7 +48,24 @@ class SyntheticsService { if (response.statusCode !== expectedResponse) { logger.error('Error %s synthetic: %d', operation, response.statusCode); - return callback('Error ' + operation + ' synthetic: ' + response.statusCode); + logger.debug(response.headers); + logger.debug(body); + + const _getError = (response, body) => { + const responseType = response.headers['content-type']; + logger.debug('content-type: ' + responseType); + if (responseType === 'application/json') { + return _.reduce(JSON.parse(body).errors, (acc, err) => { + return acc + ' : ' + err.error; + }, 'New Relic Response'); + } else if (responseType.indexOf('text/html') != -1) { + return 'New Relic Response : ' + htmlToText.fromString(body); + } + + return 'Unknown Error'; + }; + + return callback('Error ' + operation + ' synthetic: ' + response.statusCode + '; ' + _getError(response, body)); } return callback(null, body, response); @@ -52,7 +74,7 @@ class SyntheticsService { } getSynthetic(id, callback) { - logger.verbose('SyntheticsService.getSynthetic: ' + id); + logger.verbose('NewRelicService.getSynthetic: ' + id); const options = this._getOptions({ url: MONITORS_URL + '/' + id @@ -73,17 +95,29 @@ class SyntheticsService { ); } - createSynthetic(name, locations, frequency, status, callback) { - logger.verbose('SyntheticsService.createSynthetic: ' + name); + createSynthetic(name, locations, frequency, status, type, callback, uri) { + logger.verbose('NewRelicService.createSynthetic: ' + name); const paramObject = { name: name, - type: 'SCRIPT_BROWSER', + type: type, frequency: frequency, locations: locations, status: status }; + logger.debug('parameters: %s', JSON.stringify(paramObject)); + + + if ((type === 'SIMPLE') || (type === 'BROWSER')) { + logger.debug('uri: ' + uri); + if (_.isNil(uri)) { + return callback(null, 'Error: Missing uri parameter'); + } + + paramObject.uri = uri; + } + const options = this._getOptions({ url: MONITORS_URL, method: 'POST', @@ -105,7 +139,7 @@ class SyntheticsService { } getMonitorScript(id, callback) { - logger.verbose('SyntheticsService.getMonitorScript: ' + id); + logger.verbose('NewRelicService.getMonitorScript: ' + id); const options = this._getOptions({ url: 'http://' + SYNTHETICS_HOST + MONITORS_ENDPOINT +'/' + id + '/script', @@ -128,7 +162,7 @@ class SyntheticsService { } updateMonitorScript(id, base64Script, callback) { - logger.verbose('SyntheticsService.updateMonitorScript: ' + id); + logger.verbose('NewRelicService.updateMonitorScript: ' + id); const requestParam = { scriptText: base64Script @@ -153,6 +187,130 @@ class SyntheticsService { } ); } + + getAvailableLocations(callback) { + logger.verbose('NewRelicService.getAvailableLocation'); + + const options = this._getOptions({ + url: LOCATIONS_URL + }); + + this._sendRequestToNewRelic( + options, + 200, + 'getting location values', + (err, body, response) => { + if (err) { + return callback(err); + } + + logger.debug(body); + + return callback(null, JSON.parse(body)); + } + ); + } + + updateMonitorSettings(id, frequency, locations, uri, status, rename, callback) { + logger.verbose('NewRelicService.updateMonitorSettings: ' + id); + + var requestParam = {}; + + if (!_.isNil(frequency)) { + requestParam.frequency = frequency; + } + + if (!_.isNil(locations)) { + requestParam.locations = locations; + } + + if (!_.isNil(uri)) { + requestParam.uri = uri; + } + + if (!_.isNil(status)) { + requestParam.status = status; + } + + if (!_.isNil(rename)) { + requestParam.name = rename; + } + + logger.debug('request parameters: ' + JSON.stringify(requestParam)); + + const options = this._getOptions({ + url: MONITORS_URL + '/' + id, + method: 'PATCH', + body: JSON.stringify(requestParam) + }); + + this._sendRequestToNewRelic( + options, + 204, + 'updating synthetic configuration', + (err, body, response) => { + if (err) { + return callback(err); + } + + return callback(); + } + ); + } + + addAlertEmails(id, emails, callback) { + logger.verbose('NewRelicService.addAlertEmails: ' + id); + + const requestParam = { + count: emails.length, + emails: emails + }; + + logger.debug('require parameters: ' + JSON.stringify(requestParam)); + + const options = this._getOptions({ + url: ALERTS_URL + '/' + id + '/notifications', + method: 'POST', + body: JSON.stringify(requestParam), + }); + + this._sendRequestToNewRelic( + options, + 204, + 'adding alert emails', + (err, body, response) => { + if (err) { + return callback(err); + } + + return callback(); + } + ); + } + + removeAlertEmail(id, email, callback) { + logger.verbose('NewRelicService.removeAlertEmail: ' + id); + + logger.debug('email: ' + email); + + const options = this._getOptions({ + url: ALERTS_URL + '/' + id + '/notifications/' + encodeURIComponent(email), + method: 'DELETE' + }); + + this._sendRequestToNewRelic( + options, + 204, + 'removing alert email', + (err, body, response) => { + if (err) { + return callback(err); + } + + return callback(); + } + ); + } } module.exports = (apikey, request) => { return new SyntheticsService(apikey, request); }; \ No newline at end of file diff --git a/lib/service/SyntheticsListFileService.js b/lib/service/SyntheticsListFileService.js index 76d1741..8259ce4 100644 --- a/lib/service/SyntheticsListFileService.js +++ b/lib/service/SyntheticsListFileService.js @@ -37,7 +37,7 @@ function readSyntheticsListFile(syntheticsListFile, fileService, callback) { function writeSyntheticsListFile(syntheticsListFile, syntheticJson, fileService, callback) { fileService.writeFile( syntheticsListFile, - JSON.stringify(syntheticJson), + JSON.stringify(syntheticJson, null, 4), function (bytesWritten, err) { logger.debug("Write Synthetics List file complete"); callback(err); @@ -53,7 +53,7 @@ function addSyntheticAndWrite( fileService, callback ) { - readSyntheticsListFile(syntheticsListFile, fileService, function (syntheticJson, err) { + readSyntheticsListFile(syntheticsListFile, fileService, (syntheticJson, err) => { if (err) { return callback(err); } if (syntheticJson[syntheticName] !== undefined) { @@ -61,10 +61,13 @@ function addSyntheticAndWrite( } syntheticJson[syntheticName] = { - id: syntheticId, - filename: syntheticFilename + id: syntheticId }; + if (!_.isNil(syntheticFilename)) { + syntheticJson[syntheticName].filename = syntheticFilename; + } + writeSyntheticsListFile(syntheticsListFile, syntheticJson, fileService, function (err) { return callback(err); }); @@ -116,7 +119,8 @@ class SyntheticsListFileService { logger.verbose('SyntheticsListFileService.getSynthetic: start'); logger.debug('syntheticName: %s', syntheticName); - readSyntheticsListFile(this.syntheticsListFile, this.fileService, function (syntheticsJson) { + readSyntheticsListFile(this.syntheticsListFile, this.fileService, (syntheticsJson, err) => { + if (err) { return callback(null, err); } if (syntheticsJson[syntheticName] === undefined) { return callback(null, 'Could not find info for synthetic: ' + syntheticName); } diff --git a/package.json b/package.json index 07a90c9..d1fb77a 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "async": "2.1.2", + "html-to-text": "^3.0.0", "lodash": "4.17.1", "mkdirp": "0.5.1", "request": "2.79.0", @@ -32,5 +33,9 @@ "mocha": "3.1.2", "testdouble": "1.9.0", "testdouble-chai": "0.4.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/yodle/new-relic-synthetics-manager.git" } } diff --git a/test/lib/orchestrator/ChangeConfigOrchestratorTest.js b/test/lib/orchestrator/ChangeConfigOrchestratorTest.js new file mode 100644 index 0000000..850e204 --- /dev/null +++ b/test/lib/orchestrator/ChangeConfigOrchestratorTest.js @@ -0,0 +1,248 @@ +const td = require('testdouble'); +const chai = require('chai'); +const tdChai = require('testdouble-chai'); + +chai.should(); +chai.use(tdChai(td)); + +const changeConfigOrchestratorFactory = require('../../../lib/orchestrator/ChangeConfigOrchestrator'); + +describe('ChangeConfigOrchestrator', () => { + const expectedId = 'syntheticId'; + const expectedFrequency = 5; + const expectedLocations = ['location1', 'location2']; + const expectedUri = 'http://newuri.com'; + const expectedStatus = 'ENABLED'; + const expectedNewName = 'newSyntheticName'; + + it ('call to NR to change configuration by id', () => { + + const newRelicServiceMock = { + updateMonitorSettings: td.function() + } + + td.when(newRelicServiceMock.updateMonitorSettings( + td.matchers.isA(String), + td.matchers.isA(Number), + td.matchers.isA(Array), + td.matchers.isA(String), + td.matchers.isA(String), + td.matchers.isA(String), + td.callback + )).thenCallback(); + + const syntheticsListFileService = {}; + + const changeConfigOrchestrator = changeConfigOrchestratorFactory(newRelicServiceMock, syntheticsListFileService); + + changeConfigOrchestrator.changeConfigurationById( + expectedId, expectedFrequency, expectedLocations, expectedUri, expectedStatus, expectedNewName, null, null, (err) => { + newRelicServiceMock.updateMonitorSettings.should.have.been.calledWith( + expectedId, + expectedFrequency, + expectedLocations, + expectedUri, + expectedStatus, + expectedNewName, + td.callback + ); + } + ); + }); + + it ('should show an error when NR has an error by id', () => { + const expectedError = 'error with NR'; + + const newRelicServiceMock = { + updateMonitorSettings: td.function() + } + + td.when(newRelicServiceMock.updateMonitorSettings( + td.matchers.isA(String), + td.matchers.isA(Number), + td.matchers.isA(Array), + td.matchers.isA(String), + td.matchers.isA(String), + td.matchers.isA(String), + td.callback + )).thenCallback(expectedError); + + const syntheticsListFileService = {}; + + const changeConfigOrchestrator = changeConfigOrchestratorFactory(newRelicServiceMock, syntheticsListFileService); + + (() => { + changeConfigOrchestrator.changeConfigurationById( + expectedId, expectedFrequency, expectedLocations, expectedUri, expectedStatus, expectedNewName + ); + }).should.throw(expectedError); + }); + + it ('should show an error when NR has an error by id using callback', () => { + const expectedError = 'error with NR'; + + const newRelicServiceMock = { + updateMonitorSettings: td.function() + } + + td.when(newRelicServiceMock.updateMonitorSettings( + td.matchers.isA(String), + td.matchers.isA(Number), + td.matchers.isA(Array), + td.matchers.isA(String), + td.matchers.isA(String), + td.matchers.isA(String), + td.callback + )).thenCallback(expectedError); + + const syntheticsListFileService = {}; + + const changeConfigOrchestrator = changeConfigOrchestratorFactory(newRelicServiceMock, syntheticsListFileService); + + changeConfigOrchestrator.changeConfigurationById( + expectedId, expectedFrequency, expectedLocations, expectedUri, expectedStatus, expectedNewName, null, null, (err) => { + err.should.be.equal(expectedError); + } + ); + }); + + it ('call to NR to change configuration by name', () => { + const expectedName = 'syntheticName'; + + const newRelicServiceMock = { + updateMonitorSettings: td.function() + } + + td.when(newRelicServiceMock.updateMonitorSettings( + td.matchers.isA(String), + td.matchers.isA(Number), + td.matchers.isA(Array), + td.matchers.isA(String), + td.matchers.isA(String), + td.matchers.isA(String), + td.callback + )).thenCallback(); + + const syntheticsListFileService = { + getSynthetic: td.function() + }; + + td.when(syntheticsListFileService.getSynthetic( + expectedName, + td.callback + )).thenCallback({id: expectedId}); + + const changeConfigOrchestrator = changeConfigOrchestratorFactory(newRelicServiceMock, syntheticsListFileService); + + changeConfigOrchestrator.changeConfigurationByName( + expectedName, + expectedFrequency, + expectedLocations, + expectedUri, + expectedStatus, + expectedNewName + ); + + newRelicServiceMock.updateMonitorSettings.should.have.been.calledWith( + expectedId, + expectedFrequency, + expectedLocations, + expectedUri, + expectedStatus, + expectedNewName, + td.callback + ); + }); + + it ('should show an error when synthetic name cannot be found', () => { + const expectedName = 'syntheticName'; + const expectedError = 'cannot find synthetic'; + + const newRelicServiceMock = { + updateMonitorSettings: td.function() + } + + td.when(newRelicServiceMock.updateMonitorSettings( + td.matchers.isA(String), + td.matchers.isA(Number), + td.matchers.isA(Array), + td.matchers.isA(String), + td.matchers.isA(String), + td.matchers.isA(String), + td.callback + )).thenCallback(expectedError); + + const syntheticsListFileService = { + getSynthetic: td.function() + }; + + td.when(syntheticsListFileService.getSynthetic( + expectedName, + td.callback + )).thenCallback(null, expectedError); + + const changeConfigOrchestrator = changeConfigOrchestratorFactory(newRelicServiceMock, syntheticsListFileService); + + (() => { + changeConfigOrchestrator.changeConfigurationByName( + expectedName, expectedFrequency, expectedLocations, expectedUri, expectedStatus, expectedNewName + ); + }).should.throw(expectedError); + }); + + it ('call to NR to add alert emails by id', () => { + const expectedEmails = ['email1@email.com', 'email2@email.com']; + + const newRelicServiceMock = { + addAlertEmails: td.function() + } + + td.when(newRelicServiceMock.addAlertEmails( + td.matchers.isA(String), + td.matchers.isA(Array), + td.callback + )).thenCallback(); + + const syntheticsListFileService = {}; + + const changeConfigOrchestrator = changeConfigOrchestratorFactory(newRelicServiceMock, syntheticsListFileService); + + changeConfigOrchestrator.changeConfigurationById( + expectedId, null, null, null, null, null, expectedEmails, null, (err) => { + newRelicServiceMock.addAlertEmails.should.have.been.calledWith( + expectedId, + expectedEmails, + td.callback + ); + } + ); + }); + + it ('call to NR to remove alert email by id', () => { + const expectedEmail = 'email1@email.com'; + + const newRelicServiceMock = { + removeAlertEmail: td.function() + } + + td.when(newRelicServiceMock.removeAlertEmail( + td.matchers.isA(String), + td.matchers.isA(String), + td.callback + )).thenCallback(); + + const syntheticsListFileService = {}; + + const changeConfigOrchestrator = changeConfigOrchestratorFactory(newRelicServiceMock, syntheticsListFileService); + + changeConfigOrchestrator.changeConfigurationById( + expectedId, null, null, null, null, null, null, expectedEmail, (err) => { + newRelicServiceMock.removeAlertEmail.should.have.been.calledWith( + expectedId, + expectedEmail, + td.callback + ); + } + ); + }); +}); \ No newline at end of file diff --git a/test/lib/orchestrator/CreateMonitorOrchestratorTest.js b/test/lib/orchestrator/CreateMonitorOrchestratorTest.js index b30032f..8f33c57 100644 --- a/test/lib/orchestrator/CreateMonitorOrchestratorTest.js +++ b/test/lib/orchestrator/CreateMonitorOrchestratorTest.js @@ -95,7 +95,9 @@ describe('CreateMonitorOrchestrator', function () { td.matchers.isA(Array), td.matchers.isA(Number), td.matchers.isA(String), - td.callback + td.matchers.isA(String), + td.callback, + undefined )).thenCallback('http://newrelic/id'); td.when(syntheticsFileServiceMock.exists(expectedFilename, td.callback)).thenCallback(true); td.when(syntheticsListFileServiceMock.addSynthetic( @@ -116,7 +118,9 @@ describe('CreateMonitorOrchestrator', function () { locations, frequency, 'ENABLED', - td.callback + type, + td.callback, + undefined ); }); }); @@ -138,7 +142,9 @@ describe('CreateMonitorOrchestrator', function () { td.matchers.isA(Array), td.matchers.isA(Number), td.matchers.isA(String), - td.callback + td.matchers.isA(String), + td.callback, + undefined )).thenCallback(null, expectedError); td.when(syntheticsFileServiceMock.exists(expectedFilename, td.callback)).thenCallback(true); td.when(syntheticsListFileServiceMock.addSynthetic( @@ -165,4 +171,88 @@ describe('CreateMonitorOrchestrator', function () { }).should.throw(expectedError); }); + it ('should not create a synthetics file for SIMPLE synthetics', () => { + const type = 'SIMPLE'; + const syntheticsFileServiceMock = { + exists: td.function(), + createFile: td.function() + }; + + const createMonitorOrchestrator = createMonitorOrchestratorFactory( + syntheticsFileServiceMock, + newRelicServiceMock, + syntheticsListFileServiceMock, + defaultsMock + ); + + createMonitorOrchestrator.createNewMonitor(monitorName, locations, type, frequency, expectedFilename); + + syntheticsFileServiceMock.exists.should.not.have.been.called; + }); + + it ('should call into new relic for adding alerting', function () { + const expectedId = 'syntheticId'; + const alertEmails = ['email@domain.com']; + + const syntheticsFileServiceMock = { + exists: td.function(), + createFile: td.function() + }; + + const newRelicServiceMock = { + createSynthetic: td.function(), + addAlertEmails: td.function() + }; + + td.when(newRelicServiceMock.createSynthetic( + td.matchers.isA(String), + td.matchers.isA(Array), + td.matchers.isA(Number), + td.matchers.isA(String), + td.matchers.isA(String), + td.callback, + td.matchers.anything() + )).thenCallback('http://newrelic/' + expectedId); + td.when(syntheticsFileServiceMock.exists( + td.matchers.isA(String), + td.callback + )).thenCallback(true); + td.when(syntheticsListFileServiceMock.addSynthetic( + td.matchers.isA(String), + td.matchers.isA(String), + td.matchers.isA(String), + td.callback + )).thenCallback(); + td.when(newRelicServiceMock.addAlertEmails( + td.matchers.isA(String), + td.matchers.isA(Array), + td.callback + )).thenCallback(); + + const createMonitorOrchestrator = createMonitorOrchestratorFactory( + syntheticsFileServiceMock, + newRelicServiceMock, + syntheticsListFileServiceMock, + defaultsMock + ); + + createMonitorOrchestrator.createNewMonitor(monitorName, locations, type, frequency, expectedFilename, () => { + newRelicServiceMock.createSynthetic.should.have.been.calledWith( + monitorName, + locations, + frequency, + 'ENABLED', + type, + td.callback, + null + ); + newRelicServiceMock.addAlertEmails.should.have.been.calledWith( + expectedId, + alertEmails, + td.callback + ); + }, + null, + alertEmails); + }); }); \ No newline at end of file diff --git a/test/lib/orchestrator/ListLocationsOrchestratorTest.js b/test/lib/orchestrator/ListLocationsOrchestratorTest.js new file mode 100644 index 0000000..71a538d --- /dev/null +++ b/test/lib/orchestrator/ListLocationsOrchestratorTest.js @@ -0,0 +1,40 @@ +const td = require('testdouble'); +const chai = require('chai'); +const tdChai = require('testdouble-chai'); + +chai.should(); +chai.use(tdChai(td)); + +const listLocationsOrchestratorFactory = require('../../../lib/orchestrator/ListLocationsOrchestrator'); + +describe('ListLocationsOrchestrator', () => { + it ('should get a list of locations from NR', () => { + const expectedLocations = [{label: "location", name: "name" }, {label: "location2", name: "name2"}]; + + const newRelicServiceMock = { + getAvailableLocations: (callback) => { + callback(null, expectedLocations); + } + } + + const listLocationsOrchestrator = listLocationsOrchestratorFactory(newRelicServiceMock); + + listLocationsOrchestrator.listLocations(); + }); + + it ('should throw an error if NR throws an error', () => { + const expectedError = "Could not list locations"; + + const newRelicServiceMock = { + getAvailableLocations: (callback) => { + callback(expectedError); + } + } + + const listLocationsOrchestrator = listLocationsOrchestratorFactory(newRelicServiceMock); + + (() => { + listLocationsOrchestrator.listLocations(); + }).should.throw(expectedError); + }); +}); \ No newline at end of file diff --git a/test/lib/service/NewRelicSerivceTest.js b/test/lib/service/NewRelicSerivceTest.js index f2e0419..52c4882 100644 --- a/test/lib/service/NewRelicSerivceTest.js +++ b/test/lib/service/NewRelicSerivceTest.js @@ -14,6 +14,7 @@ describe('NewRelicService', () => { const expectedFrequency = 10; const expectedStatus = 'DISABLED'; const expectedUrl = 'http://newrelic/id'; + const expectedType = 'SCRIPTED_BROWSER'; const requestMock = { write: td.function(), @@ -42,6 +43,7 @@ describe('NewRelicService', () => { expectedLocations, expectedFrequency, expectedStatus, + expectedType, (syntheticUrl) => { syntheticUrl.should.equals(expectedUrl); } @@ -72,6 +74,7 @@ describe('NewRelicService', () => { expectedLocations, expectedFrequency, expectedStatus, + expectedType, (syntheticUrl, err) => { err.should.equal(expectedStatusMessage); } @@ -106,16 +109,21 @@ describe('NewRelicService', () => { const expectedId = 'syntheticId'; const expectedContent = 'new relic synthetic content'; const expectedStatusCode = 500; + const errorMessage = 'error updating synthetic'; + const errorHtml = '

' + errorMessage + '

'; const requestMock = td.function(); const responseMock = { - statusCode: expectedStatusCode + statusCode: expectedStatusCode, + headers: { + 'content-type': 'other' + } }; td.when(requestMock( td.matchers.isA(Object), td.callback - )).thenCallback(null, responseMock); + )).thenCallback(null, responseMock, errorHtml); const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); @@ -123,7 +131,7 @@ describe('NewRelicService', () => { expectedId, expectedContent, (err) => { - err.should.equal('Error updating code for synthetic: ' + expectedStatusCode); + err.should.equal('Error updating code for synthetic: ' + expectedStatusCode + '; Unknown Error'); } ); }); @@ -150,21 +158,26 @@ describe('NewRelicService', () => { const expectedStatusCode = 404; const expectedSyntheticId = 'syntheticId'; const expectedError = 'Error retrieving synthetic: ' + expectedStatusCode; + const errorMessage = 'Cannot find synthetic'; + const errorHtml = '

' + errorMessage + '

'; const requestMock = td.function(); const responseMock = { - statusCode: expectedStatusCode + statusCode: expectedStatusCode, + headers: { + 'content-type': 'text/html' + } }; td.when(requestMock( td.matchers.isA(Object), td.callback - )).thenCallback(null, responseMock); + )).thenCallback(null, responseMock, errorHtml); const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); newRelicService.getSynthetic(expectedSyntheticId, (body, err) => { - err.should.be.equal(expectedError); + err.should.be.equal(expectedError + '; New Relic Response : ' + errorMessage); }); }); @@ -254,4 +267,283 @@ describe('NewRelicService', () => { err.should.equal(expectedError); }); }); + + it ('should fail to create a SIMPLE synthetic without a uri', () => { + const type = 'SIMPLE'; + const requestMock = td.function(); + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.createSynthetic( + expectedName, + expectedLocations, + expectedFrequency, + expectedStatus, + type, + (syntheticUrl, err) => { + err.should.equals('Error: Missing uri parameter'); + } + ); + }); + + it ('should fail to create a BROWSER synthetic without a uri', () => { + const type = 'BROWSER'; + const requestMock = td.function(); + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.createSynthetic( + expectedName, + expectedLocations, + expectedFrequency, + expectedStatus, + type, + (syntheticUrl, err) => { + err.should.equals('Error: Missing uri parameter'); + } + ); + }); + + it ('should POST to NR when creating a SIMPLE synthetic', () => { + const type = 'SIMPLE'; + const uri = 'http://simple.uri.com/'; + + const responseMock = { + statusCode: 201, + headers: { + location: expectedUrl + } + }; + + const requestMock = td.function(); + + td.when(requestMock( + td.matchers.isA(Object), + td.callback + )).thenCallback(null, responseMock); + + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.createSynthetic( + expectedName, + expectedLocations, + expectedFrequency, + expectedStatus, + type, + (syntheticUrl) => { + syntheticUrl.should.equals(expectedUrl); + }, + uri + ); + }); + + it ('should GET to NR when listing locations', () => { + const expectedLocationsList = JSON.stringify({ locations: "list of locations" }); + const responseMock = { + statusCode: 200 + }; + + const requestMock = td.function(); + + td.when(requestMock( + td.matchers.isA(Object), + td.callback + )).thenCallback(null, responseMock, expectedLocationsList); + + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.getAvailableLocations( + (err, locationsList) => { + JSON.stringify(locationsList).should.be.equal(expectedLocationsList); + } + ); + }); + + it ('should fail if NR throws an error when getting locations', () => { + const errorMessage = 'error getting locations'; + + const responseMock = { + statusCode: 500, + headers: { + 'content-type': 'application/json' + } + }; + + const expectedBody = JSON.stringify({errors: [{error: errorMessage}]}); + + const requestMock = td.function(); + + td.when(requestMock( + td.matchers.isA(Object), + td.callback + )).thenCallback(null, responseMock, expectedBody); + + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.getAvailableLocations( + (err, locationsList) => { + err.should.be.equal('Error getting location values synthetic: 500; New Relic Response : ' + errorMessage); + } + ); + }); + + it ('should PATCH to NR when changing configuration', () => { + const expectedId = 'syntheticId'; + const frequency = 5; + const locations = ['location1', 'location2']; + const uri = 'http://theuri.com'; + const status = 'ENABLED'; + const newName = 'syntheticName'; + const responseMock = { + statusCode: 204 + }; + + const requestMock = td.function(); + + td.when(requestMock( + td.matchers.isA(Object), + td.callback + )).thenCallback(null, responseMock, null); + + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.updateMonitorSettings( + expectedId, + frequency, + locations, + uri, + status, + newName, + (err) => { + requestMock.should.have.been.calledWith( + td.matchers.isA(Object), + td.callback + ); + } + ); + }); + + it ('should return error when NR errors on synthetic config change', () => { + const expectedId = 'syntheticId'; + const frequency = 5; + const locations = null; + const uri = 'http://theuri.com'; + const status = null; + const newName = 'syntheticName'; + const expectedError = 'error changing synthetic config'; + const responseMock = { + statusCode: 500 + }; + + const requestMock = td.function(); + + td.when(requestMock( + td.matchers.isA(Object), + td.callback + )).thenCallback(expectedError, responseMock, null); + + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.updateMonitorSettings( + expectedId, + frequency, + locations, + uri, + status, + newName, + (err) => { + err.should.be.equal(expectedError); + } + ); + }); + + it ('should POST to NR when adding alert emails', () => { + const expectedId = 'syntheticId'; + const expectedEmails = ['email1@email.com', 'email2@email.com']; + const responseMock = { + statusCode: 204 + }; + + const requestMock = td.function(); + + td.when(requestMock( + td.matchers.isA(Object), + td.callback + )).thenCallback(null, responseMock, null); + + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.addAlertEmails(expectedId, expectedEmails, (err) => { + requestMock.should.have.been.calledWith( + td.matchers.isA(Object), + td.callback + ); + }); + }); + + it ('should return error when NR errors on adding emails', () => { + const expectedId = 'syntheticId'; + const expectedEmails = ['email1@email.com', 'email2@email.com']; + const expectedError = 'error changing synthetic config'; + const responseMock = { + statusCode: 500 + }; + + const requestMock = td.function(); + + td.when(requestMock( + td.matchers.isA(Object), + td.callback + )).thenCallback(expectedError, responseMock, null); + + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.addAlertEmails(expectedId, expectedEmails, (err) => { + err.should.be.equal(expectedError); + }); + }); + + it ('should POST to NR when removing alert emails', () => { + const expectedId = 'syntheticId'; + const expectedEmail = 'email1@email.com'; + const responseMock = { + statusCode: 204 + }; + + const requestMock = td.function(); + + td.when(requestMock( + td.matchers.isA(Object), + td.callback + )).thenCallback(null, responseMock, null); + + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.removeAlertEmail(expectedId, expectedEmail, (err) => { + requestMock.should.have.been.calledWith( + td.matchers.isA(Object), + td.callback + ); + }); + }); + + it ('should return error when NR errors on removing emails', () => { + const expectedId = 'syntheticId'; + const expectedEmail = 'email1@email.com'; + const expectedError = 'error changing synthetic config'; + const responseMock = { + statusCode: 500 + }; + + const requestMock = td.function(); + + td.when(requestMock( + td.matchers.isA(Object), + td.callback + )).thenCallback(expectedError, responseMock, null); + + const newRelicService = newRelicServiceFactory(expectedApiKey, requestMock); + + newRelicService.removeAlertEmail(expectedId, expectedEmail, (err) => { + err.should.be.equal(expectedError); + }); + }); }); \ No newline at end of file diff --git a/test/lib/service/SyntheticsListFileServiceTest.js b/test/lib/service/SyntheticsListFileServiceTest.js index d48e01b..bb7edda 100644 --- a/test/lib/service/SyntheticsListFileServiceTest.js +++ b/test/lib/service/SyntheticsListFileServiceTest.js @@ -251,4 +251,110 @@ describe('SyntheticsListFileService', function () { err.should.equal('Could not find info for synthetic filename: ' + unknownSyntheticFilename); }); }); + + it ('should not require a filename for some synthetics', () => { + const fileServiceMock = { + exists: function (filename, callback) { + filename.should.equal(expectedSyntheticsListFile); + callback(true); + }, + writeFile: td.function(), + getFileContent: function (filename, callback) { + filename.should.equal(expectedSyntheticsListFile); + callback('{}'); + } + }; + + td.when(fileServiceMock.writeFile( + expectedSyntheticsListFile, + td.matchers.contains(expectedSyntheticId), + td.callback + )).thenCallback(null); + + const syntheticsListFileService = syntheticsListFileServiceFactory(expectedSyntheticsListFile, fileServiceMock); + + syntheticsListFileService.addSynthetic(expectedSyntheticId, expectedSynthetic, null, () => { + td.verify(fileServiceMock.writeFile( + expectedSyntheticsListFile, + td.matchers.contains(expectedSyntheticId), + td.callback + )); + }); + }); + + it ('should fail if unable to create Synthetics List File', function () { + const expectedError = 'error writing file'; + + const fileServiceMock = { + exists: function (filename, callback) { + filename.should.equals(expectedSyntheticsListFile); + callback(false); + }, + writeFile: td.function(), + getFileContent: td.function() + } + + td.when(fileServiceMock.writeFile( + td.matchers.isA(String), + td.matchers.isA(String), + td.callback + )).thenCallback(null, expectedError); + + td.when(fileServiceMock.getFileContent( + td.matchers.isA(String), + td.callback + )).thenCallback('{}'); + + const syntheticsListFileService = syntheticsListFileServiceFactory(expectedSyntheticsListFile, fileServiceMock); + + syntheticsListFileService.addSynthetic(expectedSyntheticId, expectedSynthetic, expectedSyntheticFilename, (err) => { + err.should.equals(expectedError); + }); + }); + + it ('should fail getSynthetic if synthetics file cannot be read', () => { + const expectedError = 'error reading file'; + const fileServiceMock = { + exists: function (filename, callback) { + filename.should.equal(expectedSyntheticsListFile); + callback(true); + }, + writeFile: td.function(), + getFileContent: td.function() + }; + + td.when(fileServiceMock.getFileContent( + td.matchers.isA(String), + td.callback + )).thenCallback(null, expectedError); + + const syntheticsListFileService = syntheticsListFileServiceFactory(expectedSyntheticsListFile, fileServiceMock); + + syntheticsListFileService.getSynthetic(expectedSynthetic, (syntheticInfo, err) => { + err.should.equals(expectedError); + }); + }); + + it ('should fail addSynthetic if synthetics file cannot be read', () => { + const expectedError = 'error reading file'; + const fileServiceMock = { + exists: function (filename, callback) { + filename.should.equal(expectedSyntheticsListFile); + callback(true); + }, + writeFile: td.function(), + getFileContent: td.function() + }; + + td.when(fileServiceMock.getFileContent( + td.matchers.isA(String), + td.callback + )).thenCallback(null, expectedError); + + const syntheticsListFileService = syntheticsListFileServiceFactory(expectedSyntheticsListFile, fileServiceMock); + + syntheticsListFileService.addSynthetic(expectedSyntheticId, expectedSynthetic, expectedSyntheticFilename, (err) => { + err.should.equals(expectedError); + }); + }); }); \ No newline at end of file