Mobrovac has submitted this change and it was merged. ( https://gerrit.wikimedia.org/r/391985 )
Change subject: Update the service template to 0.5.3 ...................................................................... Update the service template to 0.5.3 This updates to the latest version of ServiceTemplateNode (0.5.3) A forced merged with --allow-unrelated-histories was necessary. eslint rules are temporarily disabled in various files not specific to the ServiceTemplate and will be fixed in a follow up (076e54f) Bug: T151395 Change-Id: Ie0d99fc300aa3978056cd96666124383cd76cb11 --- A .eslintrc.yml M .gitignore M .travis.yml M app.js M config.prod.yaml M lib/EventProcessor.js M lib/EventSource.js M lib/api-util.js M lib/score-pages.js A lib/swagger-ui.js M lib/util.js M package.json M routes/info.js M routes/root.js M routes/trending-v1.js M server.js M test/features/app/app.js M test/features/app/spec.js A test/features/info/info.js M test/features/v1/trending.js M test/index.js M test/utils/logStream.js M test/utils/server.js 23 files changed, 582 insertions(+), 398 deletions(-) Approvals: Mobrovac: Verified; Looks good to me, approved jenkins-bot: Verified diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..9e2c225 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1 @@ +extends: 'eslint-config-node-services' \ No newline at end of file diff --git a/.gitignore b/.gitignore index b09b5ed..850a956 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ Dockerfile .idea/ coverage +config.yaml node_modules npm-debug.log diff --git a/.travis.yml b/.travis.yml index f33adc7..b6a9e5d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ sudo: false node_js: - - "0.10" - - "0.12" - "4" - - "5" + - "6" + - "8" + - "node" diff --git a/app.js b/app.js index 7f5cf81..ed8ae1d 100644 --- a/app.js +++ b/app.js @@ -1,20 +1,18 @@ 'use strict'; - -require('core-js/shim'); - -var http = require('http'); -var BBPromise = require('bluebird'); -var express = require('express'); -var compression = require('compression'); -var bodyParser = require('body-parser'); -var fs = BBPromise.promisifyAll(require('fs')); -var sUtil = require('./lib/util'); -var apiUtil = require('./lib/api-util'); -var packageInfo = require('./package.json'); -var yaml = require('js-yaml'); -var EventSource = require('./lib/EventSource'); -var EventProcessor = require('./lib/EventProcessor'); +const http = require('http'); +const BBPromise = require('bluebird'); +const express = require('express'); +const compression = require('compression'); +const bodyParser = require('body-parser'); +const fs = BBPromise.promisifyAll(require('fs')); +const sUtil = require('./lib/util'); +const apiUtil = require('./lib/api-util'); +const packageInfo = require('./package.json'); +const yaml = require('js-yaml'); +const addShutdown = require('http-shutdown'); +const EventSource = require('./lib/EventSource'); +const EventProcessor = require('./lib/EventProcessor'); /** @@ -25,7 +23,7 @@ function initApp(options) { // the main application object - var app = express(); + const app = express(); // get the options and make them available in the app app.logger = options.logger; // the logging device @@ -43,8 +41,8 @@ if (app.conf.compression_level === undefined) { app.conf.compression_level = 3; } if (app.conf.cors === undefined) { app.conf.cors = '*'; } if (app.conf.csp === undefined) { - app.conf.csp = - "default-src 'self'; object-src 'none'; media-src *; img-src *; style-src *; frame-ancestors 'self'"; + // eslint-disable-next-line max-len + app.conf.csp = "default-src 'self'; object-src 'none'; media-src *; img-src *; style-src *; frame-ancestors 'self'"; } // set outgoing proxy @@ -64,26 +62,26 @@ // set up header whitelisting for logging if (!app.conf.log_header_whitelist) { app.conf.log_header_whitelist = [ - 'cache-control', 'content-type', 'content-length', 'if-match', - 'user-agent', 'x-request-id' + 'cache-control', 'content-type', 'content-length', 'if-match', + 'user-agent', 'x-request-id' ]; } - app.conf.log_header_whitelist = new RegExp('^(?:' + app.conf.log_header_whitelist.map(function(item) { + app.conf.log_header_whitelist = new RegExp(`^(?:${app.conf.log_header_whitelist.map((item) => { return item.trim(); - }).join('|') + ')$', 'i'); + }).join('|')})$`, 'i'); // set up the request templates for the APIs apiUtil.setupApiTemplates(app); // set up the spec if (!app.conf.spec) { - app.conf.spec = __dirname + '/spec.yaml'; + app.conf.spec = `${__dirname}/spec.yaml`; } if (app.conf.spec.constructor !== Object) { try { app.conf.spec = yaml.safeLoad(fs.readFileSync(app.conf.spec)); } catch (e) { - app.logger.log('warn/spec', 'Could not load the spec: ' + e); + app.logger.log('warn/spec', `Could not load the spec: ${e}`); app.conf.spec = {}; } } @@ -103,7 +101,7 @@ } // set the CORS and CSP headers - app.all('*', function(req, res, next) { + app.all('*', (req, res, next) => { if (app.conf.cors !== false) { res.header('access-control-allow-origin', app.conf.cors); res.header('access-control-allow-headers', 'accept, x-requested-with, content-type'); @@ -143,45 +141,46 @@ /** * Loads all routes declared in routes/ into the app * @param {Application} app the application object to load routes into - * @returns {bluebird} a promise resolving to the app object + * @return {bluebird} a promise resolving to the app object */ function loadRoutes(app) { // get the list of files in routes/ - return fs.readdirAsync(__dirname + '/routes').map(function(fname) { - return BBPromise.try(function() { + return fs.readdirAsync(`${__dirname}/routes`).map((fname) => { + return BBPromise.try(() => { // ... and then load each route // but only if it's a js file if (!/\.js$/.test(fname)) { return undefined; } // import the route file - var route = require(__dirname + '/routes/' + fname); + const route = require(`${__dirname}/routes/${fname}`); return route(app); - }).then(function(route) { + }).then((route) => { if (route === undefined) { return undefined; } // check that the route exports the object we need - if (route.constructor !== Object || !route.path || !route.router || !(route.api_version || route.skip_domain)) { - throw new TypeError('routes/' + fname + ' does not export the correct object!'); + if (route.constructor !== Object || !route.path || !route.router + || !(route.api_version || route.skip_domain)) { + throw new TypeError(`routes/${fname} does not export the correct object!`); } // normalise the path to be used as the mount point if (route.path[0] !== '/') { - route.path = '/' + route.path; + route.path = `/${route.path}`; } if (route.path[route.path.length - 1] !== '/') { - route.path = route.path + '/'; + route.path = `${route.path}/`; } if (!route.skip_domain) { - route.path = '/:domain/v' + route.api_version + route.path; + route.path = `/:domain/v${route.api_version}${route.path}`; } // wrap the route handlers with Promise.try() blocks sUtil.wrapRouteHandlers(route, app); // all good, use that route app.use(route.path, route.router); }); - }).then(function() { + }).then(() => { // catch errors sUtil.setErrorHandler(app); // route loading is now complete, return the app object @@ -218,40 +217,48 @@ editStream.on('error', (e) => { app.logger.log('error/edit_stream', 'Error in edit stream: ' + e); }); - process.on('exit', () => { - clearInterval(app._metricsReporter); - editStream.close(); - }); + app.editStream = editStream; return app; } /** * Creates and start the service's web server * @param {Application} app the app object to use in the service - * @returns {bluebird} a promise creating the web server + * @return {bluebird} a promise creating the web server */ function createServer(app) { // return a promise which creates an HTTP server, // attaches the app to it, and starts accepting // incoming client requests - var server; - return new BBPromise(function(resolve) { + let server; + return new BBPromise((resolve) => { server = http.createServer(app).listen( app.conf.port, app.conf.interface, resolve ); - }).then(function() { + server = addShutdown(server); + }).then(() => { app.logger.log('info', - 'Worker ' + process.pid + ' listening on ' + (app.conf.interface || '*') + ':' + app.conf.port); + `Worker ${process.pid} listening on ${app.conf.interface || '*'}:${app.conf.port}`); // Don't delay incomplete packets for 40ms (Linux default) on // pipelined HTTP sockets. We write in large chunks or buffers, so // lack of coalescing should not be an issue here. - server.on("connection", function(socket) { + server.on("connection", (socket) => { socket.setNoDelay(true); }); + + // Clean up on exit + const server_close = () => { + try { + clearInterval(app._metricsReporter); + app.editStream.close(); + } catch (e) { /* Ignore any errors */ } + }; + server.on('close', server_close); + process.on('exit', server_close); return server; }); @@ -270,9 +277,9 @@ return initApp(options) .then(createEditStream) .then(loadRoutes) - .then(function(app) { + .then((app) => { // serve static files from static/ - app.use('/static', express.static(__dirname + '/static')); + app.use('/static', express.static(`${__dirname}/static`)); return app; }).then(createServer); diff --git a/config.prod.yaml b/config.prod.yaml index b58c8cf..dfbe263 100644 --- a/config.prod.yaml +++ b/config.prod.yaml @@ -89,3 +89,4 @@ min_editors: 3 min_edits: 6 broker_list: localhost:9092 + diff --git a/lib/EventProcessor.js b/lib/EventProcessor.js index 29f483d..9bd734a 100644 --- a/lib/EventProcessor.js +++ b/lib/EventProcessor.js @@ -1,4 +1,6 @@ "use strict"; + +/* eslint-disable */ const EventEmitter = require('events').EventEmitter; /** diff --git a/lib/EventSource.js b/lib/EventSource.js index 2c82247..34f9f33 100644 --- a/lib/EventSource.js +++ b/lib/EventSource.js @@ -1,5 +1,6 @@ "use strict"; +/* eslint-disable */ const kafka = require('node-rdkafka'); const EventEmitter = require('events').EventEmitter; const os = require('os'); diff --git a/lib/api-util.js b/lib/api-util.js index 67f1710..cef871e 100644 --- a/lib/api-util.js +++ b/lib/api-util.js @@ -1,35 +1,32 @@ 'use strict'; - -var preq = require('preq'); -var sUtil = require('./util'); -var Template = require('swagger-router').Template; - -var HTTPError = sUtil.HTTPError; +const preq = require('preq'); +const sUtil = require('./util'); +const Template = require('swagger-router').Template; +const HTTPError = sUtil.HTTPError; /** * Calls the MW API with the supplied query as its body - * - * @param {Object} app the application object + * @param {!Object} app the application object * @param {string} domain the domain to issue the request to - * @param {Object} query an object with all the query parameters for the MW API - * @return {Promise} a promise resolving as the response object from the MW API + * @param {?Object} query an object with all the query parameters for the MW API + * @return {!Promise} a promise resolving as the response object from the MW API */ function mwApiGet(app, domain, query) { query = query || {}; query.continue = query.continue || ''; - var request = app.mwapi_tpl.expand({ + const request = app.mwapi_tpl.expand({ request: { - params: { domain: domain }, + params: { domain }, headers: { 'user-agent': app.conf.user_agent }, - query: query + query } }); - return preq(request).then(function(response) { + return preq(request).then((response) => { if (response.status < 200 || response.status > 399) { // there was an error when calling the upstream service, propagate that throw new HTTPError({ @@ -47,16 +44,15 @@ /** * Calls the REST API with the supplied domain, path and request parameters - * - * @param {Object} app the application object + * @param {!Object} app the application object * @param {string} domain the domain to issue the request for - * @param {string} path the REST API path to contact without the leading slash - * @param {Object} [restReq={}] the object containing the REST request details - * @param {string} [restReq.method=get] the request method - * @param {Object} [restReq.query={}] the query string to send, if any - * @param {Object} [restReq.headers={}] the request headers to send - * @param {Object} [restReq.body=null] the body of the request, if any - * @return {Promise} a promise resolving as the response object from the REST API + * @param {!string} path the REST API path to contact without the leading slash + * @param {?Object} [restReq={}] the object containing the REST request details + * @param {?string} [restReq.method=get] the request method + * @param {?Object} [restReq.query={}] the query string to send, if any + * @param {?Object} [restReq.headers={}] the request headers to send + * @param {?Object} [restReq.body=null] the body of the request, if any + * @return {!Promise} a promise resolving as the response object from the REST API * */ function restApiGet(app, domain, path, restReq) { @@ -64,10 +60,10 @@ restReq = restReq || {}; path = path[0] === '/' ? path.slice(1) : path; - var request = app.restbase_tpl.expand({ + const request = app.restbase_tpl.expand({ request: { method: restReq.method, - params: { domain: domain, path: path }, + params: { domain, path }, query: restReq.query, headers: Object.assign({ 'user-agent': app.conf.user_agent }, restReq.headers), body: restReq.body @@ -81,8 +77,7 @@ /** * Sets up the request templates for MW and RESTBase API requests - * - * @param {Application} app the application object + * @param {!Application} app the application object */ function setupApiTemplates(app) { @@ -114,8 +109,8 @@ module.exports = { - mwApiGet: mwApiGet, - restApiGet: restApiGet, - setupApiTemplates: setupApiTemplates + mwApiGet, + restApiGet, + setupApiTemplates }; diff --git a/lib/score-pages.js b/lib/score-pages.js index 0c0e842..a5891ee 100644 --- a/lib/score-pages.js +++ b/lib/score-pages.js @@ -1,5 +1,6 @@ "use strict"; +/* eslint-disable */ var scorer = require('wikipedia-edits-scorer'); /** diff --git a/lib/swagger-ui.js b/lib/swagger-ui.js new file mode 100644 index 0000000..9e39ff5 --- /dev/null +++ b/lib/swagger-ui.js @@ -0,0 +1,79 @@ +'use strict'; + + +const BBPromise = require('bluebird'); +const fs = BBPromise.promisifyAll(require('fs')); +const path = require('path'); +const HTTPError = require('../lib/util.js').HTTPError; + + +// Swagger-ui helpfully exporting the absolute path of its dist directory +const docRoot = `${require('swagger-ui').dist}/`; + +function processRequest(app, req, res) { + + const reqPath = req.query.path || '/index.html'; + const filePath = path.join(docRoot, reqPath); + + // Disallow relative paths. + // Test relies on docRoot ending on a slash. + if (filePath.substring(0, docRoot.length) !== docRoot) { + throw new HTTPError({ + status: 404, + type: 'not_found', + title: 'File not found', + detail: `${reqPath} could not be found.` + }); + } + + return fs.readFileAsync(filePath) + .then((body) => { + if (reqPath === '/index.html') { + body = body.toString() + .replace(/((?:src|href)=['"])/g, '$1?doc&path=') + // Some self-promotion + .replace(/<a id="logo".*?<\/a>/, + `<a id="logo" href="${app.info.homepage}">${app.info.name}</a>`) + .replace(/<title>[^<]*<\/title>/, `<title>${app.info.name}</title>`) + // Replace the default url with ours, switch off validation & + // limit the size of documents to apply syntax highlighting to + .replace(/docExpansion: "none"/, 'docExpansion: "list", ' + + 'validatorUrl: null, ' + + 'highlightSizeThreshold: 10000') + .replace(/ url: url,/, 'url: "/?spec",'); + } + + let contentType = 'text/html'; + if (/\.js$/.test(reqPath)) { + contentType = 'text/javascript'; + body = body.toString() + .replace(/underscore-min\.map/, '?doc&path=lib/underscore-min.map'); + } else if (/\.png$/.test(reqPath)) { + contentType = 'image/png'; + } else if (/\.map$/.test(reqPath)) { + contentType = 'application/json'; + } else if (/\.ttf$/.test(reqPath)) { + contentType = 'application/x-font-ttf'; + } else if (/\.css$/.test(reqPath)) { + contentType = 'text/css'; + body = body.toString().replace(/\.\.\/(images|fonts)\//g, '?doc&path=$1/'); + } + + res.setHeader('Content-Type', contentType); + res.setHeader('content-security-policy', "default-src 'none'; " + + "script-src 'self' 'unsafe-inline'; connect-src *; " + + "style-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'self';"); + res.send(body.toString()); + }) + .catch({ code: 'ENOENT' }, () => { + res.status(404) + .type('not_found') + .send('not found'); + }); + +} + +module.exports = { + processRequest +}; + diff --git a/lib/util.js b/lib/util.js index 16fc4a1..cb3d587 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,57 +1,50 @@ 'use strict'; -var BBPromise = require('bluebird'); -var util = require('util'); -var express = require('express'); -var uuid = require('cassandra-uuid'); -var bunyan = require('bunyan'); +const BBPromise = require('bluebird'); +const express = require('express'); +const uuid = require('cassandra-uuid'); +const bunyan = require('bunyan'); /** * Error instance wrapping HTTP error responses */ -function HTTPError(response) { +class HTTPError extends Error { - Error.call(this); - Error.captureStackTrace(this, HTTPError); + constructor(response) { + super(); + Error.captureStackTrace(this, HTTPError); - if (response.constructor !== Object) { - // just assume this is just the error message - var msg = response; - response = { - status: 500, - type: 'internal_error', - title: 'InternalError', - detail: msg - }; + if (response.constructor !== Object) { + // just assume this is just the error message + response = { + status: 500, + type: 'internal_error', + title: 'InternalError', + detail: response + }; + } + + this.name = this.constructor.name; + this.message = `${response.status}`; + if (response.type) { + this.message += `: ${response.type}`; + } + + Object.assign(this, response); } - - this.name = this.constructor.name; - this.message = response.status + ''; - if (response.type) { - this.message += ': ' + response.type; - } - - for (var key in response) { - this[key] = response[key]; - } - } - -util.inherits(HTTPError, Error); - /** * Generates an object suitable for logging out of a request object - * - * @param {Request} req the request - * @param {RegExp} whitelistRE the RegExp used to filter headers - * @return {Object} an object containing the key components of the request + * @param {!Request} req the request + * @param {?RegExp} whitelistRE the RegExp used to filter headers + * @return {!Object} an object containing the key components of the request */ function reqForLog(req, whitelistRE) { - var ret = { + const ret = { url: req.originalUrl, headers: {}, method: req.method, @@ -63,7 +56,7 @@ }; if (req.headers && whitelistRE) { - Object.keys(req.headers).forEach(function(hdr) { + Object.keys(req.headers).forEach((hdr) => { if (whitelistRE.test(hdr)) { ret.headers[hdr] = req.headers[hdr]; } @@ -76,20 +69,19 @@ /** - * Serialises an error object in a form suitable for logging - * - * @param {Error} err error to serialise - * @return {Object} the serialised version of the error + * Serialises an error object in a form suitable for logging. + * @param {!Error} err error to serialise + * @return {!Object} the serialised version of the error */ function errForLog(err) { - var ret = bunyan.stdSerializers.err(err); + const ret = bunyan.stdSerializers.err(err); ret.status = err.status; ret.type = err.type; ret.detail = err.detail; // log the stack trace only for 500 errors - if (Number.parseInt(ret.status) !== 500) { + if (Number.parseInt(ret.status, 10) !== 500) { ret.stack = undefined; } @@ -98,9 +90,8 @@ } /** - * Generates a unique request ID - * - * @return {String} the generated request ID + * Generates a unique request ID. + * @return {!String} the generated request ID */ function generateRequestId() { @@ -114,37 +105,34 @@ * promised try blocks so as to allow catching all errors, * regardless of whether a handler returns/uses promises * or not. - * - * @param {Object} route the object containing the router and path to bind it to - * @param {Application} app the application object + * @param {!Object} route the object containing the router and path to bind it to + * @param {!Application} app the application object */ function wrapRouteHandlers(route, app) { - route.router.stack.forEach(function(routerLayer) { - var path = (route.path + routerLayer.route.path.slice(1)) + route.router.stack.forEach((routerLayer) => { + let path = (route.path + routerLayer.route.path.slice(1)) .replace(/\/:/g, '/--') .replace(/^\//, '') - .replace(/[\/?]+$/, ''); + .replace(/[/?]+$/, ''); path = app.metrics.normalizeName(path || 'root'); - routerLayer.route.stack.forEach(function(layer) { - var origHandler = layer.handle; + routerLayer.route.stack.forEach((layer) => { + const origHandler = layer.handle; layer.handle = function(req, res, next) { - var startTime = Date.now(); - BBPromise.try(function() { - return origHandler(req, res, next); - }) + const startTime = Date.now(); + BBPromise.try(() => origHandler(req, res, next)) .catch(next) - .finally(function() { - var statusCode = parseInt(res.statusCode) || 500; + .finally(() => { + let statusCode = parseInt(res.statusCode, 10) || 500; if (statusCode < 100 || statusCode > 599) { statusCode = 500; } - var statusClass = Math.floor(statusCode / 100) + 'xx'; - var stat = path + '.' + req.method + '.'; + const statusClass = `${Math.floor(statusCode / 100)}xx`; + const stat = `${path}.${req.method}.`; app.metrics.endTiming([ stat + statusCode, stat + statusClass, - stat + 'ALL' + `${stat}ALL` ], startTime); }); }; @@ -155,24 +143,22 @@ /** - * Generates an error handler for the given applications - * and installs it. Usage: - * - * @param {Application} app the application object to add the handler to + * Generates an error handler for the given applications and installs it. + * @param {!Application} app the application object to add the handler to */ function setErrorHandler(app) { - app.use(function(err, req, res, next) { - var errObj; + app.use((err, req, res, next) => { + let errObj; // ensure this is an HTTPError object if (err.constructor === HTTPError) { errObj = err; } else if (err instanceof Error) { // is this an HTTPError defined elsewhere? (preq) if (err.constructor.name === 'HTTPError') { - var o = { status: err.status }; + const o = { status: err.status }; if (err.body && err.body.constructor === Object) { - Object.keys(err.body).forEach(function(key) { + Object.keys(err.body).forEach((key) => { o[key] = err.body[key]; }); } else { @@ -211,18 +197,17 @@ // some set 'message' or 'description' instead of 'detail' errObj.detail = errObj.detail || errObj.message || errObj.description || ''; // adjust the log level based on the status code - var level = 'error'; - if (Number.parseInt(errObj.status) < 400) { + let level = 'error'; + if (Number.parseInt(errObj.status, 10) < 400) { level = 'trace'; - } else if (Number.parseInt(errObj.status) < 500) { + } else if (Number.parseInt(errObj.status, 10) < 500) { level = 'info'; } // log the error - (req.logger || app.logger).log(level + '/' + - (errObj.component ? errObj.component : errObj.status), - errForLog(errObj)); + const component = (errObj.component ? errObj.component : errObj.status); + (req.logger || app.logger).log(`${level}/${component}`, errForLog(errObj)); // let through only non-sensitive info - var respBody = { + const respBody = { status: errObj.status, type: errObj.type, title: errObj.title, @@ -238,46 +223,45 @@ /** * Creates a new router with some default options. - * - * @param {Object} [opts] additional options to pass to express.Router() - * @return {Router} a new router object + * @param {?Object} [opts] additional options to pass to express.Router() + * @return {!Router} a new router object */ function createRouter(opts) { - var options = { + const options = { mergeParams: true }; if (opts && opts.constructor === Object) { - Object.keys(opts).forEach(function(key) { - options[key] = opts[key]; - }); + Object.assign(options, opts); } - return express.Router(options); + return new express.Router(options); } /** - * Adds logger to the request and logs it - * - * @param {*} req request object - * @param {Application} app application object + * Adds logger to the request and logs it. + * @param {!*} req request object + * @param {!Application} app application object */ function initAndLogRequest(req, app) { req.headers = req.headers || {}; req.headers['x-request-id'] = req.headers['x-request-id'] || generateRequestId(); - req.logger = app.logger.child({ request_id: req.headers['x-request-id'], request: reqForLog(req, app.conf.log_header_whitelist) }); + req.logger = app.logger.child({ + request_id: req.headers['x-request-id'], + request: reqForLog(req, app.conf.log_header_whitelist) + }); req.logger.log('trace/req', { msg: 'incoming request' }); } module.exports = { - HTTPError: HTTPError, - initAndLogRequest: initAndLogRequest, - wrapRouteHandlers: wrapRouteHandlers, - setErrorHandler: setErrorHandler, + HTTPError, + initAndLogRequest, + wrapRouteHandlers, + setErrorHandler, router: createRouter }; diff --git a/package.json b/package.json index 40b3d40..9d8d083 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,15 @@ { "name": "trending-edits", - "version": "0.1.0", + "version": "0.1.1", "description": "A trending REST API service based on edits", "homepage": "https://www.mediawiki.org/wiki/Reading/Web/Projects/Edit_based_trending_service", "main": "./app.js", "scripts": { "start": "service-runner", - "test": "mocha && nsp check", + "test": "PREQ_CONNECT_TIMEOUT=15 mocha && nsp check", "docker-start": "service-runner docker-start", "docker-test": "service-runner docker-test", + "test-build": "service-runner docker-test && service-runner build --deploy-repo --force", "coverage": "istanbul cover _mocha -- -R spec" }, "repository": { @@ -20,31 +21,42 @@ "bugs": { "url": "https://phabricator.wikimedia.org/tag/reading_web_trending_service/" }, + "keywords": [ + "Trending", + "API", + "edits", + "MediaWiki" + ], "dependencies": { "bluebird": "^3.5.1", "body-parser": "^1.18.2", "bunyan": "^1.8.12", "cassandra-uuid": "^0.0.2", "compression": "^1.7.1", - "core-js": "^2.5.1", "domino": "^1.0.30", - "express": "^4.14.0", + "express": "^4.16.2", "js-yaml": "^3.10.0", - "preq": "^0.4.10", - "service-runner": "^2.2.5", - "swagger-router": "^0.4.6", + "preq": "^0.5.3", + "service-runner": "^2.4.2", + "swagger-router": "^0.7.1", + "swagger-ui": "git+https://github.com/wikimedia/swagger-ui#master", + "http-shutdown": "^1.2.0", "node-rdkafka": "^1.0.6", "wikipedia-edits-scorer": "^1.5.7" }, "devDependencies": { "extend": "^3.0.1", - "istanbul": "^0.4.4", - "jscs": "^3.0.7", - "mocha": "^2.5.3", - "mocha-jscs": "^5.0.1", + "istanbul": "^0.4.5", + "mocha": "^4.0.1", "mocha-jshint": "^2.3.1", "mocha-lcov-reporter": "^1.3.0", - "nsp": "^2.6.1" + "nsp": "^2.8.1", + "mocha-eslint": "^3.0.1", + "eslint": "^3.12.0", + "eslint-config-node-services": "^2.0.2", + "eslint-config-wikimedia": "^0.4.0", + "eslint-plugin-json": "^1.2.0", + "eslint-plugin-jsdoc": "^3.0.0" }, "deploy": { "node": "6.11.1", diff --git a/routes/info.js b/routes/info.js index 79b82e3..832af0d 100644 --- a/routes/info.js +++ b/routes/info.js @@ -1,25 +1,25 @@ 'use strict'; -var sUtil = require('../lib/util'); +const sUtil = require('../lib/util'); /** * The main router object */ -var router = sUtil.router(); +const router = sUtil.router(); /** * The main application object reported when this module is require()d */ -var app; +let app; /** * GET / * Gets some basic info about this service */ -router.get('/', function(req, res) { +router.get('/', (req, res) => { // simple sync return res.json({ @@ -36,7 +36,7 @@ * GET /name * Gets the service's name as defined in package.json */ -router.get('/name', function(req, res) { +router.get('/name', (req, res) => { // simple return res.json({ name: app.info.name }); @@ -48,7 +48,7 @@ * GET /version * Gets the service's version as defined in package.json */ -router.get('/version', function(req, res) { +router.get('/version', (req, res) => { // simple return res.json({ version: app.info.version }); @@ -61,28 +61,28 @@ * Redirects to the service's home page if one is given, * returns a 404 otherwise */ -router.all('/home', function(req, res) { +router.all('/home', (req, res) => { - var home = app.info.homepage; + const home = app.info.homepage; if (home && /^http/.test(home)) { // we have a home page URI defined, so send it res.redirect(301, home); } else { // no URI defined for the home page, error out - res.status(404).end('No home page URL defined for ' + app.info.name); + res.status(404).end(`No home page URL defined for ${app.info.name}`); } }); -module.exports = function(appObj) { +module.exports = (appObj) => { app = appObj; return { path: '/_info', skip_domain: true, - router: router + router }; }; diff --git a/routes/root.js b/routes/root.js index 31d5c40..83ee521 100644 --- a/routes/root.js +++ b/routes/root.js @@ -1,29 +1,30 @@ 'use strict'; -var sUtil = require('../lib/util'); +const sUtil = require('../lib/util'); +const swaggerUi = require('../lib/swagger-ui'); /** * The main router object */ -var router = sUtil.router(); +const router = sUtil.router(); /** * The main application object reported when this module is require()d */ -var app; +let app; /** * GET /robots.txt * Instructs robots no indexing should occur on this domain. */ -router.get('/robots.txt', function(req, res) { +router.get('/robots.txt', (req, res) => { res.set({ 'User-agent': '*', - Disallow: '/' + 'Disallow': '/' }).end(); }); @@ -31,28 +32,30 @@ /** * GET / - * Main entry point. Currently it only responds if the spec query + * Main entry point. Currently it only responds if the spec or doc query * parameter is given, otherwise lets the next middleware handle it */ -router.get('/', function(req, res, next) { +router.get('/', (req, res, next) => { - if (!(req.query || {}).hasOwnProperty('spec')) { - next(); - } else { + if ({}.hasOwnProperty.call(req.query || {}, 'spec')) { res.json(app.conf.spec); + } else if ({}.hasOwnProperty.call(req.query || {}, 'doc')) { + return swaggerUi.processRequest(app, req, res); + } else { + next(); } }); -module.exports = function(appObj) { +module.exports = (appObj) => { app = appObj; return { path: '/', skip_domain: true, - router: router + router }; }; diff --git a/routes/trending-v1.js b/routes/trending-v1.js index 050b4a5..aec72a6 100644 --- a/routes/trending-v1.js +++ b/routes/trending-v1.js @@ -1,6 +1,6 @@ 'use strict'; - +/* eslint-disable */ var BBPromise = require('bluebird'); var preq = require('preq'); var domino = require('domino'); diff --git a/server.js b/server.js index a45b9ce..2a5ac56 100755 --- a/server.js +++ b/server.js @@ -8,5 +8,5 @@ // (config.yaml by default, specify other path with -c). It requires the // module(s) specified in the config 'services' section (app.js in this // example). -var ServiceRunner = require('service-runner'); +const ServiceRunner = require('service-runner'); new ServiceRunner().start(); diff --git a/test/features/app/app.js b/test/features/app/app.js index d30b94e..3cef837 100644 --- a/test/features/app/app.js +++ b/test/features/app/app.js @@ -1,33 +1,37 @@ 'use strict'; -var preq = require('preq'); -var assert = require('../../utils/assert.js'); -var server = require('../../utils/server.js'); +const preq = require('preq'); +const assert = require('../../utils/assert.js'); +const server = require('../../utils/server.js'); +if (!server.stopHookAdded) { + server.stopHookAdded = true; + after(() => server.stop()); +} describe('express app', function() { - this.timeout(20000); + this.timeout(20000); // eslint-disable-line no-invalid-this - before(function () { return server.start(); }); + before(() => { return server.start(); }); - it('should get robots.txt', function() { + it('should get robots.txt', () => { return preq.get({ - uri: server.config.uri + 'robots.txt' - }).then(function(res) { + uri: `${server.config.uri}robots.txt` + }).then((res) => { assert.deepEqual(res.status, 200); - assert.deepEqual(res.headers['disallow'], '/'); + assert.deepEqual(res.headers.disallow, '/'); }); }); - it('should set CORS headers', function() { - if(server.config.service.conf.cors === false) { + it('should set CORS headers', () => { + if (server.config.service.conf.cors === false) { return true; } return preq.get({ - uri: server.config.uri + 'robots.txt' - }).then(function(res) { + uri: `${server.config.uri}robots.txt` + }).then((res) => { assert.deepEqual(res.status, 200); assert.deepEqual(res.headers['access-control-allow-origin'], '*'); assert.deepEqual(!!res.headers['access-control-allow-headers'], true); @@ -35,13 +39,13 @@ }); }); - it('should set CSP headers', function() { - if(server.config.service.conf.csp === false) { + it('should set CSP headers', () => { + if (server.config.service.conf.csp === false) { return true; } return preq.get({ - uri: server.config.uri + 'robots.txt' - }).then(function(res) { + uri: `${server.config.uri}robots.txt` + }).then((res) => { assert.deepEqual(res.status, 200); assert.deepEqual(res.headers['x-xss-protection'], '1; mode=block'); assert.deepEqual(res.headers['x-content-type-options'], 'nosniff'); @@ -52,29 +56,29 @@ }); }); - it.skip('should get static content gzipped', function() { + it.skip('should get static content gzipped', () => { return preq.get({ - uri: server.config.uri + 'static/index.html', + uri: `${server.config.uri}static/index.html`, headers: { 'accept-encoding': 'gzip, deflate' } - }).then(function(res) { + }).then((res) => { // check that the response is gzip-ed assert.deepEqual(res.headers['content-encoding'], 'gzip', 'Expected gzipped contents!'); }); }); - it('should get static content uncompressed', function() { + it('should get static content uncompressed', () => { return preq.get({ - uri: server.config.uri + 'static/index.html', + uri: `${server.config.uri}static/index.html`, headers: { 'accept-encoding': '' } - }).then(function(res) { + }).then((res) => { // check that the response is gzip-ed - assert.deepEqual(res.headers['content-encoding'], undefined, 'Did not expect gzipped contents!'); + const contentEncoding = res.headers['content-encoding']; + assert.deepEqual(contentEncoding, undefined, 'Did not expect gzipped contents!'); }); }); - }); diff --git a/test/features/app/spec.js b/test/features/app/spec.js index 570fb40..013be6b 100644 --- a/test/features/app/spec.js +++ b/test/features/app/spec.js @@ -1,25 +1,28 @@ 'use strict'; +const preq = require('preq'); +const assert = require('../../utils/assert.js'); +const server = require('../../utils/server.js'); +const URI = require('swagger-router').URI; +const yaml = require('js-yaml'); +const fs = require('fs'); -var preq = require('preq'); -var assert = require('../../utils/assert.js'); -var server = require('../../utils/server.js'); -var URI = require('swagger-router').URI; -var yaml = require('js-yaml'); -var fs = require('fs'); - +if (!server.stopHookAdded) { + server.stopHookAdded = true; + after(() => server.stop()); +} function staticSpecLoad() { - var spec; - var myService = server.config.conf.services[server.config.conf.services.length - 1].conf; - var specPath = __dirname + '/../../../' + (myService.spec ? myService.spec : 'spec.yaml'); + let spec; + const myService = server.config.conf.services[server.config.conf.services.length - 1].conf; + const specPath = `${__dirname}/../../../${myService.spec ? myService.spec : 'spec.yaml'}`; try { spec = yaml.safeLoad(fs.readFileSync(specPath)); - } catch(e) { + } catch (e) { // this error will be detected later, so ignore it - spec = {paths: {}, 'x-default-params': {}}; + spec = { paths: {}, 'x-default-params': {} }; } return spec; @@ -29,30 +32,32 @@ function validateExamples(pathStr, defParams, mSpec) { - var uri = new URI(pathStr, {}, true); + const uri = new URI(pathStr, {}, true); - if(!mSpec) { + if (!mSpec) { try { uri.expand(defParams); return true; - } catch(e) { - throw new Error('Missing parameter for route ' + pathStr + ' : ' + e.message); + } catch (e) { + throw new Error(`Missing parameter for route ${pathStr} : ${e.message}`); } } - if(!Array.isArray(mSpec)) { - throw new Error('Route ' + pathStr + ' : x-amples must be an array!'); + if (!Array.isArray(mSpec)) { + throw new Error(`Route ${pathStr} : x-amples must be an array!`); } - mSpec.forEach(function(ex, idx) { - if(!ex.title) { - throw new Error('Route ' + pathStr + ', example ' + idx + ': title missing!'); + mSpec.forEach((ex, idx) => { + if (!ex.title) { + throw new Error(`Route ${pathStr}, example ${idx}: title missing!`); } ex.request = ex.request || {}; try { uri.expand(Object.assign({}, defParams, ex.request.params || {})); - } catch(e) { - throw new Error('Route ' + pathStr + ', example ' + idx + ' (' + ex.title + '): missing parameter: ' + e.message); + } catch (e) { + throw new Error( + `Route ${pathStr}, example ${idx} (${ex.title}): missing parameter: ${e.message}` + ); } }); @@ -64,10 +69,10 @@ function constructTestCase(title, path, method, request, response) { return { - title: title, + title, request: { uri: server.config.uri + (path[0] === '/' ? path.substr(1) : path), - method: method, + method, headers: request.headers || {}, query: request.query, body: request.body, @@ -85,31 +90,30 @@ function constructTests(paths, defParams) { - var ret = []; + const ret = []; - Object.keys(paths).forEach(function(pathStr) { - Object.keys(paths[pathStr]).forEach(function(method) { - var p = paths[pathStr][method]; - var uri; - if(p.hasOwnProperty('x-monitor') && !p['x-monitor']) { + Object.keys(paths).forEach((pathStr) => { + Object.keys(paths[pathStr]).forEach((method) => { + const p = paths[pathStr][method]; + if ({}.hasOwnProperty.call(p, 'x-monitor') && !p['x-monitor']) { return; } - uri = new URI(pathStr, {}, true); - if(!p['x-amples']) { + const uri = new URI(pathStr, {}, true); + if (!p['x-amples']) { ret.push(constructTestCase( pathStr, - uri.toString({params: defParams}), + uri.toString({ params: defParams }), method, {}, {} )); return; } - p['x-amples'].forEach(function(ex) { + p['x-amples'].forEach((ex) => { ex.request = ex.request || {}; ret.push(constructTestCase( ex.title, - uri.toString({params: Object.assign({}, defParams, ex.request.params || {})}), + uri.toString({ params: Object.assign({}, defParams, ex.request.params || {}) }), method, ex.request, ex.response || {} @@ -125,51 +129,52 @@ function cmp(result, expected, errMsg) { - if(expected === null || expected === undefined) { + if (expected === null || expected === undefined) { // nothing to expect, so we can return return true; } - if(result === null || result === undefined) { + if (result === null || result === undefined) { result = ''; } - if(expected.constructor === Object) { - Object.keys(expected).forEach(function(key) { - var val = expected[key]; - assert.deepEqual(result.hasOwnProperty(key), true, 'Body field ' + key + ' not found in response!'); - cmp(result[key], val, key + ' body field mismatch!'); + if (expected.constructor === Object) { + Object.keys(expected).forEach((key) => { + const val = expected[key]; + assert.deepEqual({}.hasOwnProperty.call(result, key), true, + `Body field ${key} not found in response!`); + cmp(result[key], val, `${key} body field mismatch!`); }); return true; - } else if(expected.constructor === Array) { - if(result.constructor !== Array) { + } else if (expected.constructor === Array) { + if (result.constructor !== Array) { assert.deepEqual(result, expected, errMsg); return true; } // only one item in expected - compare them all - if(expected.length === 1 && result.length > 1) { - result.forEach(function(item) { + if (expected.length === 1 && result.length > 1) { + result.forEach((item) => { cmp(item, expected[0], errMsg); }); return true; } // more than one item expected, check them one by one - if(expected.length !== result.length) { + if (expected.length !== result.length) { assert.deepEqual(result, expected, errMsg); return true; } - expected.forEach(function(item, idx) { + expected.forEach((item, idx) => { cmp(result[idx], item, errMsg); }); return true; } - if(expected.length > 1 && expected[0] === '/' && expected[expected.length - 1] === '/') { - if((new RegExp(expected.slice(1, -1))).test(result)) { + if (expected.length > 1 && expected[0] === '/' && expected[expected.length - 1] === '/') { + if ((new RegExp(expected.slice(1, -1))).test(result)) { return true; } - } else if(expected.length === 0 && result.length === 0) { + } else if (expected.length === 0 && result.length === 0) { return true; - } else if(result === expected || result.startsWith(expected)) { + } else if (result === expected || result.startsWith(expected)) { return true; } @@ -181,32 +186,35 @@ function validateTestResponse(testCase, res) { - var expRes = testCase.response; + const expRes = testCase.response; // check the status assert.status(res, expRes.status); // check the headers - Object.keys(expRes.headers).forEach(function(key) { - var val = expRes.headers[key]; - assert.deepEqual(res.headers.hasOwnProperty(key), true, 'Header ' + key + ' not found in response!'); - cmp(res.headers[key], val, key + ' header mismatch!'); + Object.keys(expRes.headers).forEach((key) => { + const val = expRes.headers[key]; + assert.deepEqual({}.hasOwnProperty.call(res.headers, key), true, + `Header ${key} not found in response!`); + cmp(res.headers[key], val, `${key} header mismatch!`); }); // check the body - if(!expRes.body) { + if (!expRes.body) { return true; } res.body = res.body || ''; - if(Buffer.isBuffer(res.body)) { res.body = res.body.toString(); } - if(expRes.body.constructor !== res.body.constructor) { - if(expRes.body.constructor === String) { + if (Buffer.isBuffer(res.body)) { res.body = res.body.toString(); } + if (expRes.body.constructor !== res.body.constructor) { + if (expRes.body.constructor === String) { res.body = JSON.stringify(res.body); } else { res.body = JSON.parse(res.body); } } // check that the body type is the same - if(expRes.body.constructor !== res.body.constructor) { - throw new Error('Expected a body of type ' + expRes.body.constructor + ' but gotten ' + res.body.constructor); + if (expRes.body.constructor !== res.body.constructor) { + throw new Error( + `Expected body type ${expRes.body.constructor} but got ${res.body.constructor}` + ); } // compare the bodies @@ -220,19 +228,19 @@ describe('Swagger spec', function() { // the variable holding the spec - var spec = staticSpecLoad(); + let spec = staticSpecLoad(); // default params, if given - var defParams = spec['x-default-params'] || {}; + let defParams = spec['x-default-params'] || {}; - this.timeout(20000); + this.timeout(20000); // eslint-disable-line no-invalid-this - before(function () { + before(() => { return server.start(); }); - it('get the spec', function() { - return preq.get(server.config.uri + '?spec') - .then(function(res) { + it('get the spec', () => { + return preq.get(`${server.config.uri}?spec`) + .then((res) => { assert.status(200); assert.contentType(res, 'application/json'); assert.notDeepEqual(res.body, undefined, 'No body received!'); @@ -240,25 +248,24 @@ }); }); - it('spec validation', function() { - if(spec['x-default-params']) { + it('spec validation', () => { + if (spec['x-default-params']) { defParams = spec['x-default-params']; } // check the high-level attributes - ['info', 'swagger', 'paths'].forEach(function(prop) { - assert.deepEqual(!!spec[prop], true, 'No ' + prop + ' field present!'); + ['info', 'swagger', 'paths'].forEach((prop) => { + assert.deepEqual(!!spec[prop], true, `No ${prop} field present!`); }); // no paths - no love assert.deepEqual(!!Object.keys(spec.paths), true, 'No paths given in the spec!'); // now check each path - Object.keys(spec.paths).forEach(function(pathStr) { - var path; + Object.keys(spec.paths).forEach((pathStr) => { assert.deepEqual(!!pathStr, true, 'A path cannot have a length of zero!'); - path = spec.paths[pathStr]; - assert.deepEqual(!!Object.keys(path), true, 'No methods defined for path: ' + pathStr); - Object.keys(path).forEach(function(method) { - var mSpec = path[method]; - if(mSpec.hasOwnProperty('x-monitor') && !mSpec['x-monitor']) { + const path = spec.paths[pathStr]; + assert.deepEqual(!!Object.keys(path), true, `No methods defined for path: ${pathStr}`); + Object.keys(path).forEach((method) => { + const mSpec = path[method]; + if ({}.hasOwnProperty.call(mSpec, 'x-monitor') && !mSpec['x-monitor']) { return; } validateExamples(pathStr, defParams, mSpec['x-amples']); @@ -266,20 +273,19 @@ }); }); - describe('routes', function() { + describe('routes', () => { - constructTests(spec.paths, defParams).forEach(function(testCase) { - it(testCase.title, function() { + constructTests(spec.paths, defParams).forEach((testCase) => { + it(testCase.title, () => { return preq(testCase.request) - .then(function(res) { + .then((res) => { validateTestResponse(testCase, res); - }, function(err) { + }, (err) => { validateTestResponse(testCase, err); }); }); }); }); - }); diff --git a/test/features/info/info.js b/test/features/info/info.js new file mode 100644 index 0000000..65b8551 --- /dev/null +++ b/test/features/info/info.js @@ -0,0 +1,73 @@ +'use strict'; + + +const preq = require('preq'); +const assert = require('../../utils/assert.js'); +const server = require('../../utils/server.js'); + +if (!server.stopHookAdded) { + server.stopHookAdded = true; + after(() => server.stop()); +} + +describe('service information', function() { + + this.timeout(20000); + + before(() => { return server.start(); }); + + // common URI prefix for info tests + const infoUri = `${server.config.uri}_info/`; + + // common function used for generating requests + // and checking their return values + function checkRet(fieldName) { + return preq.get({ + uri: infoUri + fieldName + }).then((res) => { + // check the returned Content-Type header + assert.contentType(res, 'application/json'); + // the status as well + assert.status(res, 200); + // finally, check the body has the specified field + assert.notDeepEqual(res.body, undefined, 'No body returned!'); + assert.notDeepEqual(res.body[fieldName], undefined, `No ${fieldName} field returned!`); + }); + } + + it('should get the service name', () => { + return checkRet('name'); + }); + + it('should get the service version', () => { + return checkRet('version'); + }); + + it('should redirect to the service home page', () => { + return preq.get({ + uri: `${infoUri}home`, + followRedirect: false + }).then((res) => { + // check the status + assert.status(res, 301); + }); + }); + + it('should get the service info', () => { + return preq.get({ + uri: infoUri + }).then((res) => { + // check the status + assert.status(res, 200); + // check the returned Content-Type header + assert.contentType(res, 'application/json'); + // inspect the body + assert.notDeepEqual(res.body, undefined, 'No body returned!'); + assert.notDeepEqual(res.body.name, undefined, 'No name field returned!'); + assert.notDeepEqual(res.body.version, undefined, 'No version field returned!'); + assert.notDeepEqual(res.body.description, undefined, 'No description field returned!'); + assert.notDeepEqual(res.body.home, undefined, 'No home field returned!'); + }); + }); +}); + diff --git a/test/features/v1/trending.js b/test/features/v1/trending.js index 3f1a4a2..0ec81ae 100644 --- a/test/features/v1/trending.js +++ b/test/features/v1/trending.js @@ -6,6 +6,11 @@ const server = require('../../utils/server.js'); const fixtures = require('../../utils/fixtures'); +if (!server.stopHookAdded) { + server.stopHookAdded = true; + after(() => server.stop()); +} + describe('trending', function() { this.timeout(20000); diff --git a/test/index.js b/test/index.js index c360f7d..b145c8a 100644 --- a/test/index.js +++ b/test/index.js @@ -3,4 +3,9 @@ // Run jshint as part of normal testing require('mocha-jshint')(); -require("mocha-jscs")(); +require('mocha-eslint')([ + 'lib', + 'routes' +], { + timeout: 10000 +}); diff --git a/test/utils/logStream.js b/test/utils/logStream.js index 842df59..f8e292d 100644 --- a/test/utils/logStream.js +++ b/test/utils/logStream.js @@ -4,65 +4,65 @@ function logStream(logStdout) { - var log = []; - var parrot = bunyan.createLogger({ - name: 'test-logger', - level: 'warn' - }); + var log = []; + var parrot = bunyan.createLogger({ + name: 'test-logger', + level: 'warn' + }); - function write(chunk, encoding, callback) { - try { - var entry = JSON.parse(chunk); - var levelMatch = /^(\w+)/.exec(entry.levelPath); - if (logStdout && levelMatch) { - var level = levelMatch[1]; - if (parrot[level]) { - parrot[level](entry); - } + function write(chunk, encoding, callback) { + try { + var entry = JSON.parse(chunk); + var levelMatch = /^(\w+)/.exec(entry.levelPath); + if (logStdout && levelMatch) { + var level = levelMatch[1]; + if (parrot[level]) { + parrot[level](entry); } - } catch (e) { - console.error('something went wrong trying to parrot a log entry', e, chunk); } - - log.push(chunk); + } catch (e) { + console.error('something went wrong trying to parrot a log entry', e, chunk); } - // to implement the stream writer interface - function end(chunk, encoding, callback) { + log.push(chunk); + } + + // to implement the stream writer interface + function end(chunk, encoding, callback) { + } + + function get() { + return log; + } + + function slice() { + + var begin = log.length; + var end = null; + + function halt() { + if (end === null) { + end = log.length; + } } function get() { - return log; - } - - function slice() { - - var begin = log.length; - var end = null; - - function halt() { - if (end === null) { - end = log.length; - } - } - - function get() { - return log.slice(begin, end); - } - - return { - halt: halt, - get: get - }; - + return log.slice(begin, end); } return { - write: write, - end: end, - slice: slice, - get: get + halt: halt, + get: get }; + + } + + return { + write: write, + end: end, + slice: slice, + get: get + }; } module.exports = logStream; diff --git a/test/utils/server.js b/test/utils/server.js index ba0d718..8a67d3f 100644 --- a/test/utils/server.js +++ b/test/utils/server.js @@ -5,24 +5,24 @@ /* global describe, it, before, beforeEach, after, afterEach */ -var BBPromise = require('bluebird'); -var ServiceRunner = require('service-runner'); -var logStream = require('./logStream'); -var fs = require('fs'); -var assert = require('./assert'); -var yaml = require('js-yaml'); -var extend = require('extend'); +const BBPromise = require('bluebird'); +const ServiceRunner = require('service-runner'); +const logStream = require('./logStream'); +const fs = require('fs'); +const assert = require('./assert'); +const yaml = require('js-yaml'); +const extend = require('extend'); // set up the configuration -var config = { - conf: yaml.safeLoad(fs.readFileSync(__dirname + '/../../config.yaml')) +let config = { + conf: yaml.safeLoad(fs.readFileSync(`${__dirname}/../../config.yaml`)) }; // build the API endpoint URI by supposing the actual service // is the last one in the 'services' list in the config file -var myServiceIdx = config.conf.services.length - 1; -var myService = config.conf.services[myServiceIdx]; -config.uri = 'http://localhost:' + myService.conf.port + '/'; +const myServiceIdx = config.conf.services.length - 1; +const myService = config.conf.services[myServiceIdx]; +config.uri = `http://localhost:${myService.conf.port}/`; config.service = myService; // no forking, run just one process when testing config.conf.num_workers = 0; @@ -33,11 +33,11 @@ stream: logStream() }; // make a deep copy of it for later reference -var origConfig = extend(true, {}, config); +const origConfig = extend(true, {}, config); -var stop = function() { return BBPromise.resolve(); }; -var options = null; -var runner = new ServiceRunner(); +module.exports.stop = () => { return BBPromise.resolve(); }; +let options = null; +const runner = new ServiceRunner(); function start(_options) { @@ -45,18 +45,23 @@ _options = _options || {}; if (!assert.isDeepEqual(options, _options)) { - console.log('server options changed; restarting'); - return stop().then(function() { + console.log('starting test server'); // eslint-disable-line no-console + return module.exports.stop().then(() => { options = _options; // set up the config config = extend(true, {}, origConfig); extend(true, config.conf.services[myServiceIdx].conf, options); return runner.start(config.conf) - .then(function() { - stop = function () { - console.log('stopping test server'); - return runner.stop().then(function() { - stop = function() { return BBPromise.resolve(); }; + .then((serviceReturns) => { + module.exports.stop = () => { + console.log('stopping test server'); // eslint-disable-line no-console + serviceReturns.forEach(servers => + servers.forEach(server => + server.shutdown())); + return runner.stop().then(() => { + module.exports.stop = () => { + return BBPromise.resolve(); + }; }); }; return true; @@ -65,7 +70,6 @@ } else { return BBPromise.resolve(); } - } module.exports.config = config; -- To view, visit https://gerrit.wikimedia.org/r/391985 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Ie0d99fc300aa3978056cd96666124383cd76cb11 Gerrit-PatchSet: 4 Gerrit-Project: mediawiki/services/trending-edits Gerrit-Branch: master Gerrit-Owner: Jdlrobson <jrob...@wikimedia.org> Gerrit-Reviewer: BearND <bsitzm...@wikimedia.org> Gerrit-Reviewer: Jdlrobson <jrob...@wikimedia.org> Gerrit-Reviewer: Mholloway <mhollo...@wikimedia.org> Gerrit-Reviewer: Mobrovac <mobro...@wikimedia.org> Gerrit-Reviewer: Ppchelko <ppche...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits