diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..c7f96f7b --- /dev/null +++ b/.eslintignore @@ -0,0 +1,5 @@ +node_modules +dist +.ebextensions +.elasticbeanstalk +coverage \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index 348ce0f0..ad8484f2 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,6 +8,21 @@ "mocha": true }, "rules": { + "no-param-reassign": 0, + "global-require": 0, + "func-names": 0, + "no-mixed-operators": 0, + "consistent-return": 0, + "no-unused-expressions": 0, + "newline-per-chained-call": 0, + "no-underscore-dangle": 0, + "import/no-extraneous-dependencies": ["error", {"devDependencies": true, "optionalDependencies": false, "peerDependencies": false}], + "max-len": ["error", { + "code": 120, + "ignoreComments": true, + "ignoreTrailingComments": true, + "tabWidth": 2 + }], "valid-jsdoc": ["error", { "requireReturn": true, "requireReturnType": true, diff --git a/config/sample.local.js b/config/sample.local.js index 19441b79..ae17f31e 100644 --- a/config/sample.local.js +++ b/config/sample.local.js @@ -1,34 +1,34 @@ // force using test.json for unit tests -var config +let config; if (process.env.NODE_ENV === 'test') { - config = require('./test.json') + config = require('./test.json'); } else { config = { - "authSecret": "secret", - "logLevel": "debug", - "captureLogs": "false", - "logentriesToken": "", - "rabbitmqURL": "amqp://dockerhost:5672", - "fileServiceEndpoint": "https://api.topcoder-dev.com/v3/files/", - "topicServiceEndpoint": "https://api.topcoder-dev.com/v4/topics/", - "directProjectServiceEndpoint": "https://api.topcoder-dev.com/v3/direct", - "userServiceUrl": "https://api.topcoder-dev.com/v3/users", - "connectProjectsUrl": "https://connect.topcoder-dev.com/projects/", - "salesforceLead" : { - "webToLeadUrl": 'https://www.salesforce.com/servlet/servlet.WebToLead?encoding=UTF-8', - "orgId": "00D2C0000000dO6", - "projectNameFieldId": "title", - "projectDescFieldId": "description", - "projectLinkFieldId": "URL", - "projectIdFieldId" : "00N2C000000Vxxx" - }, - "dbConfig": { - "masterUrl": "postgres://coder:mysecretpassword@dockerhost:5432/projectsdb", - "maxPoolSize": 50, - "minPoolSize": 4, - "idleTimeout": 1000 - } - } + authSecret: 'secret', + logLevel: 'debug', + captureLogs: 'false', + logentriesToken: '', + rabbitmqURL: 'amqp://dockerhost:5672', + fileServiceEndpoint: 'https://api.topcoder-dev.com/v3/files/', + topicServiceEndpoint: 'https://api.topcoder-dev.com/v4/topics/', + directProjectServiceEndpoint: 'https://api.topcoder-dev.com/v3/direct', + userServiceUrl: 'https://api.topcoder-dev.com/v3/users', + connectProjectsUrl: 'https://connect.topcoder-dev.com/projects/', + salesforceLead: { + webToLeadUrl: 'https://www.salesforce.com/servlet/servlet.WebToLead?encoding=UTF-8', + orgId: '00D2C0000000dO6', + projectNameFieldId: 'title', + projectDescFieldId: 'description', + projectLinkFieldId: 'URL', + projectIdFieldId: '00N2C000000Vxxx', + }, + dbConfig: { + masterUrl: 'postgres://coder:mysecretpassword@dockerhost:5432/projectsdb', + maxPoolSize: 50, + minPoolSize: 4, + idleTimeout: 1000, + }, + }; } -module.exports = config +module.exports = config; diff --git a/migrations/sync.js b/migrations/sync.js index 5b6f79c1..f682c6b0 100644 --- a/migrations/sync.js +++ b/migrations/sync.js @@ -1,20 +1,20 @@ -'use strict' - /** * Sync the database models to db tables. */ +import winston from 'winston'; + /** * Make sure we are in development mode * @type {String} */ // process.env.NODE_ENV = 'development' -require('./dist/models').default.sequelize.sync({ force: true }) +require('../dist/models').default.sequelize.sync({ force: true }) .then(() => { - console.log('Database synced successfully') - process.exit() + winston.info('Database synced successfully'); + process.exit(); }).catch((err) => { - console.error('Error syncing database', err) - process.exit(1) - }) + winston.error('Error syncing database', err); + process.exit(1); + }); diff --git a/newrelic.js b/newrelic.js index 2694e216..c207db64 100644 --- a/newrelic.js +++ b/newrelic.js @@ -1,4 +1,4 @@ -'use strict' + /** * New Relic agent configuration. @@ -6,13 +6,13 @@ * See lib/config.defaults.js in the agent distribution for a more complete * description of configuration variables and their potential values. */ -var appName = "tc-projects-service" +let appName = 'tc-projects-service'; if (process.env.NODE_ENV === 'development') { - appName += "-dev" + appName += '-dev'; } else if (process.env.NODE_ENV === 'qa') { - appName += "-qa" + appName += '-qa'; } else { - appName += '-prod' + appName += '-prod'; } exports.config = { @@ -30,6 +30,6 @@ exports.config = { * issues with the agent, 'info' and higher will impose the least overhead on * production applications. */ - level: 'info' - } -} + level: 'info', + }, +}; diff --git a/package.json b/package.json index 62f41d39..81c6069e 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "scripts": { "build": "babel src -d dist --presets es2015", "sync": "node migrations/sync.js", - "lint": "./node_modules/.bin/eslint src", + "lint": "./node_modules/.bin/eslint .", + "lint:fix": "./node_modules/.bin/eslint . --fix || true", "prestart": "npm run -s build", "start": "node dist", "start:dev": "NODE_ENV=local PORT=8001 nodemon -w src --exec \"babel-node src --presets es2015\" | ./node_modules/.bin/bunyan", @@ -47,7 +48,8 @@ "pg": "^4.5.5", "pg-native": "^1.10.0", "sequelize": "^3.23.0", - "tc-core-library-js": "^1.0.8" + "tc-core-library-js": "^1.0.8", + "winston": "^2.3.1" }, "devDependencies": { "babel-cli": "^6.9.0", diff --git a/src/events/projects/index.js b/src/events/projects/index.js index c2499b18..1cf66edf 100644 --- a/src/events/projects/index.js +++ b/src/events/projects/index.js @@ -1,6 +1,4 @@ -import config from 'config'; -import querystring from 'querystring'; -import util from '../../util'; + /** * Creates a lead in salesforce for the connect project. diff --git a/src/index.js b/src/index.js index 505da7a2..41304c76 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,5 @@ -import models from './models'; - // include newrelic if (process.env.NODE_ENV !== 'test' && process.env.NODE_ENV !== 'local') { require('newrelic'); @@ -11,6 +9,7 @@ const app = require('./app'); /** * Handle server shutdown gracefully + * @return {Void} This function returns void */ function gracefulShutdown() { app.services.pubsub.disconnect() diff --git a/src/middlewares/checkRole.js b/src/middlewares/checkRole.js index 1144e647..a903d896 100644 --- a/src/middlewares/checkRole.js +++ b/src/middlewares/checkRole.js @@ -7,11 +7,12 @@ * @version 1.0 */ import config from 'config'; + const util = require('tc-core-library-js').util(config); module.exports = function (roleName) { return function (req, res, next) { - if (!req.authUser || !Array.isArray(req.authUser.roles) || req.authUser.roles.indexOf(roleName) == -1) { + if (!req.authUser || !Array.isArray(req.authUser.roles) || req.authUser.roles.indexOf(roleName) === -1) { return res.status(403) .json(util.wrapErrorResponse(req.id, 403, 'You are not allowed to perform this action.')); } diff --git a/src/mocks/addBillingAccount.js b/src/mocks/addBillingAccount.js index 3af425e9..615bb528 100644 --- a/src/mocks/addBillingAccount.js +++ b/src/mocks/addBillingAccount.js @@ -1,3 +1,6 @@ +/* eslint-disable max-len */ +import winston from 'winston'; + const http = require('https'); const options = { @@ -23,7 +26,7 @@ const req = http.request(options, (res) => { res.on('end', () => { const body = Buffer.concat(chunks); - console.log(body.toString()); + winston.info(body.toString()); }); }); req.write('{\n "billingAccountId": 123456789\n}'); diff --git a/src/mocks/addCopilot.js b/src/mocks/addCopilot.js index c6b17ec7..622f5ed5 100644 --- a/src/mocks/addCopilot.js +++ b/src/mocks/addCopilot.js @@ -1,3 +1,6 @@ +/* eslint-disable max-len */ +import winston from 'winston'; + const http = require('https'); const options = { @@ -23,7 +26,7 @@ const req = http.request(options, (res) => { res.on('end', () => { const body = Buffer.concat(chunks); - console.log(body.toString()); + winston.info(body.toString()); }); }); req.write('{\n "copilotUserId": 123456789\n}'); diff --git a/src/mocks/createProject.js b/src/mocks/createProject.js index 4aca9b40..e232d61d 100644 --- a/src/mocks/createProject.js +++ b/src/mocks/createProject.js @@ -1,3 +1,6 @@ +/* eslint-disable max-len */ +import winston from 'winston'; + const http = require('https'); const options = { @@ -23,7 +26,7 @@ const req = http.request(options, (res) => { res.on('end', () => { const body = Buffer.concat(chunks); - console.log(body.toString()); + winston.info(body.toString()); }); }); diff --git a/src/mocks/direct.js b/src/mocks/direct.js index 4dabcfdc..094fdfa8 100644 --- a/src/mocks/direct.js +++ b/src/mocks/direct.js @@ -1,17 +1,20 @@ -import express from 'express'; +import express, { Router } from 'express'; import _ from 'lodash'; import bodyParser from 'body-parser'; import config from 'config'; import coreLib from 'tc-core-library-js'; import expressRequestId from 'express-request-id'; -import Router from 'express'; import https from 'https'; import path from 'path'; import fs from 'fs'; -config.version = 'v3'; + const util = require('tc-core-library-js').util(config); +const jwtAuth = require('tc-core-library-js').middleware.jwtAuthenticator; + +config.version = 'v3'; + const app = express(); app.use(bodyParser.urlencoded({ extended: false, @@ -33,10 +36,9 @@ app.use(coreLib.middleware.logger(null, logger)); app.logger = logger; const router = Router(); -const jwtAuth = require('tc-core-library-js').middleware.jwtAuthenticator; router.all('/v3/direct/projects*', jwtAuth()); -let projectId = 2; +const projectId = 2; const projects = { 1: { projectName: 'test direct project1', @@ -55,7 +57,7 @@ router.route('/v3/direct/projects') }) .post((req, res) => { app.logger.info({ body: req.body }, 'create direct project'); - const newId = projectId++; + const newId = projectId + 1; req.body.id = newId; projects[newId] = req.body; res.json(util.wrapResponse(req.id, { projectId: newId })); @@ -63,47 +65,48 @@ router.route('/v3/direct/projects') router.route('/v3/direct/projects/:projectId(\\d+)/billingaccount') .post((req, res) => { - const projectId = req.params.projectId; - app.logger.info({ body: req.body, projectId }, 'add billingaccount to Project'); - if (projects[projectId]) { - projects[projectId] = _.merge(projects[projectId], req.body); - res.json(util.wrapResponse(req.id, { billingAccountName: `mock account name for ${req.body.billingAccountId}` })); + const pId = req.params.projectId; + app.logger.info({ body: req.body, pId }, 'add billingaccount to Project'); + if (projects[pId]) { + projects[pId] = _.merge(projects[pId], req.body); + res.json(util.wrapResponse(req.id, { billingAccountName: 'mock account name for ' + + `${req.body.billingAccountId}` })); } else { - res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${projectId}`)); + res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${pId}`)); } }); router.route('/v3/direct/projects/:projectId(\\d+)/copilot') .post((req, res) => { - const projectId = req.params.projectId; - app.logger.info({ body: req.body, projectId }, 'add copilot to Project'); - if (projects[projectId]) { - projects[projectId] = _.merge(projects[projectId], req.body); - res.json(util.wrapResponse(req.id, { copilotProjectId: projectId })); + const pId = req.params.projectId; + app.logger.info({ body: req.body, pId }, 'add copilot to Project'); + if (projects[pId]) { + projects[pId] = _.merge(projects[pId], req.body); + res.json(util.wrapResponse(req.id, { copilotProjectId: pId })); } else { - res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${projectId}`)); + res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${pId}`)); } }) .delete((req, res) => { - const projectId = req.params.projectId; - app.logger.info({ body: req.body, projectId }, 'remove copilot from Project'); - if (projects[projectId]) { - projects[projectId] = _.omit(projects[projectId], 'copilotUserId'); + const pId = req.params.projectId; + app.logger.info({ body: req.body, pId }, 'remove copilot from Project'); + if (projects[pId]) { + projects[pId] = _.omit(projects[pId], 'copilotUserId'); res.json(util.wrapResponse(req.id, true)); } else { - res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${projectId}`)); + res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${pId}`)); } }); router.route('/v3/direct/projects/:projectId(\\d+)/permissions') .post((req, res) => { - const projectId = req.params.projectId; - app.logger.info({ body: req.body, projectId }, 'add permissions to Project'); - if (projects[projectId]) { + const pId = req.params.projectId; + app.logger.info({ body: req.body, pId }, 'add permissions to Project'); + if (projects[pId]) { res.json(); } else { - res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${projectId}`)); + res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${pId}`)); } }); diff --git a/src/models/project.js b/src/models/project.js index d565e72c..d9462579 100644 --- a/src/models/project.js +++ b/src/models/project.js @@ -1,9 +1,10 @@ +/* eslint-disable valid-jsdoc */ -import { PROJECT_TYPE, PROJECT_STATUS, PROJECT_MEMBER_ROLE } from '../constants'; import _ from 'lodash'; +import { PROJECT_TYPE, PROJECT_STATUS, PROJECT_MEMBER_ROLE } from '../constants'; module.exports = function (sequelize, DataTypes) { - var Project = sequelize.define('Project', { + const Project = sequelize.define('Project', { id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, directProjectId: DataTypes.BIGINT, billingAccountId: DataTypes.BIGINT, @@ -65,15 +66,17 @@ module.exports = function (sequelize, DataTypes) { return this.findAll({ where: { $or: [ - ['EXISTS(SELECT * FROM "project_members" WHERE "deletedAt" IS NULL AND "projectId" = "Project".id AND "userId" = ? )', userId], - ['"Project".status=? AND NOT EXISTS(SELECT * FROM "project_members" WHERE "deletedAt" IS NULL AND "projectId" = "Project".id AND "role" = ? )', + ['EXISTS(SELECT * FROM "project_members" WHERE "deletedAt" ' + + 'IS NULL AND "projectId" = "Project".id AND "userId" = ? )', userId], + ['"Project".status=? AND NOT EXISTS(SELECT * FROM "project_members" WHERE ' + + ' "deletedAt" IS NULL AND "projectId" = "Project".id AND "role" = ? )', PROJECT_STATUS.REVIEWED, PROJECT_MEMBER_ROLE.COPILOT], ], }, attributes: ['id'], raw: true, }) - .then(res => _.map(res, 'id')); + .then(res => _.map(res, 'id')); }, /** * Get direct project id @@ -142,7 +145,8 @@ module.exports = function (sequelize, DataTypes) { .then((count) => { count = count[0].count; // select project attributes - return sequelize.query(`SELECT ${attributesStr} FROM projects WHERE ${query} ORDER BY ${orderStr} LIMIT ${parameters.limit} OFFSET ${parameters.offset}`, + return sequelize.query(`SELECT ${attributesStr} FROM projects WHERE ${query} ORDER BY ` + + ` ${orderStr} LIMIT ${parameters.limit} OFFSET ${parameters.offset}`, { type: sequelize.QueryTypes.SELECT, logging: (str) => { log.debug(str); }, raw: true, diff --git a/src/permissions/project.delete.js b/src/permissions/project.delete.js index b532798c..27ece695 100644 --- a/src/permissions/project.delete.js +++ b/src/permissions/project.delete.js @@ -1,14 +1,16 @@ /* globals Promise */ +import _ from 'lodash'; import util from '../util'; import models from '../models'; import { USER_ROLE, PROJECT_MEMBER_ROLE } from '../constants'; -import _ from 'lodash'; /** * Super admin, Topcoder Managers are allowed to edit any project * Rest can add members only if they are currently part of the project team. + * @param {Object} req the express request instance + * @return {Promise} Returns a promise */ module.exports = req => new Promise((resolve, reject) => { const projectId = _.parseInt(req.params.projectId); diff --git a/src/permissions/project.edit.js b/src/permissions/project.edit.js index 62854066..f794353c 100644 --- a/src/permissions/project.edit.js +++ b/src/permissions/project.edit.js @@ -1,14 +1,15 @@ -/* globals Promise */ +import _ from 'lodash'; import util from '../util'; import models from '../models'; import { USER_ROLE } from '../constants'; -import _ from 'lodash'; /** * Super admin, Topcoder Managers are allowed to edit any project * Rest can add members only if they are currently part of the project team. + * @param {Object} req the express request instance + * @return {Promise} Returns a promise */ module.exports = req => new Promise((resolve, reject) => { const projectId = _.parseInt(req.params.projectId); diff --git a/src/permissions/project.view.js b/src/permissions/project.view.js index 26e64740..4a70b116 100644 --- a/src/permissions/project.view.js +++ b/src/permissions/project.view.js @@ -1,15 +1,15 @@ -/* globals Promise */ - +import _ from 'lodash'; import util from '../util'; import models from '../models'; import { USER_ROLE } from '../constants'; -import _ from 'lodash'; /** * Super admin, Topcoder Managers are allowed to view any projects * Co-pilots can view projects they are part of or if no other co-pilot has been * assigned. Others can only view projcets that they are part of. + * @param {Object} req the express request instance + * @return {Promise} Returns a promise */ module.exports = req => new Promise((resolve, reject) => { const projectId = _.parseInt(req.params.projectId); diff --git a/src/permissions/projectMember.delete.js b/src/permissions/projectMember.delete.js index 9a07c185..5541db18 100644 --- a/src/permissions/projectMember.delete.js +++ b/src/permissions/projectMember.delete.js @@ -1,14 +1,15 @@ -/* globals Promise */ - +import _ from 'lodash'; import util from '../util'; import models from '../models'; import { USER_ROLE, PROJECT_MEMBER_ROLE } from '../constants'; -import _ from 'lodash'; + /** * Super admin, Topcoder Managers are allowed to edit any project * Rest can add members only if they are currently part of the project team. + * @param {Object} req the express request instance + * @return {Promise} Returns a promise */ module.exports = req => new Promise((resolve, reject) => { diff --git a/src/routes/attachments/create.js b/src/routes/attachments/create.js index 53abd5a7..43a1e8d0 100644 --- a/src/routes/attachments/create.js +++ b/src/routes/attachments/create.js @@ -8,10 +8,9 @@ import _ from 'lodash'; import config from 'config'; import Joi from 'joi'; import path from 'path'; - +import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; -import { middleware as tcMiddleware } from 'tc-core-library-js'; const permissions = tcMiddleware.permissions; diff --git a/src/routes/attachments/create.spec.js b/src/routes/attachments/create.spec.js index 98617cb2..b3a7532e 100644 --- a/src/routes/attachments/create.spec.js +++ b/src/routes/attachments/create.spec.js @@ -2,7 +2,7 @@ import chai from 'chai'; import sinon from 'sinon'; import request from 'supertest'; - +import winston from 'winston'; import server from '../../app'; import models from '../../models'; import util from '../../util'; @@ -100,7 +100,7 @@ describe('Project Attachments', () => { const stub = sinon.stub(util, 'getHttpClient', () => mockHttpClient); // mock util s3FileTransfer util.s3FileTransfer = (req, source, dest) => { - console.log(source, dest); + winston.info(`source is ${source}, dest is ${dest}`); return Promise.resolve(true); }; request(server) @@ -113,18 +113,15 @@ describe('Project Attachments', () => { .expect(201) .end((err, res) => { if (err) { - console.log(err); + winston.error('unexpected error', err); return done(err); } const resJson = res.body.result.content; should.exist(resJson); - - postSpy.should.have.been.calledOnce; getSpy.should.have.been.calledOnce; stub.restore(); - console.log(JSON.stringify(resJson, null, 2)); resJson.title.should.equal('Spec.pdf'); resJson.downloadUrl.should.exist; resJson.projectId.should.equal(project1.id); diff --git a/src/routes/attachments/delete.js b/src/routes/attachments/delete.js index a76b918e..b5aa452c 100644 --- a/src/routes/attachments/delete.js +++ b/src/routes/attachments/delete.js @@ -2,12 +2,11 @@ // import validate from 'express-validation' import _ from 'lodash'; - -import models from '../../models'; -import fileService from '../../services/fileService'; import { middleware as tcMiddleware, } from 'tc-core-library-js'; +import models from '../../models'; +import fileService from '../../services/fileService'; /** * API to delete a project member. diff --git a/src/routes/attachments/delete.spec.js b/src/routes/attachments/delete.spec.js index 8634f941..0dc92d78 100644 --- a/src/routes/attachments/delete.spec.js +++ b/src/routes/attachments/delete.spec.js @@ -1,6 +1,5 @@ import _ from 'lodash'; -import chai from 'chai'; import sinon from 'sinon'; import request from 'supertest'; @@ -11,9 +10,8 @@ import testUtil from '../../tests/util'; describe('Project Attachments delete', () => { - let project1, - member1, - attachment; + let project1; + let attachment; beforeEach((done) => { testUtil.clearDb() .then(() => { @@ -37,23 +35,20 @@ describe('Project Attachments delete', () => { isPrimary: true, createdBy: 1, updatedBy: 1, - }).then((pm) => { - member1 = pm; - return models.ProjectAttachment.create({ - projectId: project1.id, - title: 'test.txt', - description: 'blah', - contentType: 'application/unknown', - size: 12312, - category: null, - filePath: 'https://media.topcoder.com/projects/1/test.txt', - createdBy: 1, - updatedBy: 1, - }).then((a1) => { - attachment = a1; - done(); - }); - }); + }).then(() => models.ProjectAttachment.create({ + projectId: project1.id, + title: 'test.txt', + description: 'blah', + contentType: 'application/unknown', + size: 12312, + category: null, + filePath: 'https://media.topcoder.com/projects/1/test.txt', + createdBy: 1, + updatedBy: 1, + }).then((a1) => { + attachment = a1; + done(); + })); }); }); }); diff --git a/src/routes/attachments/update.js b/src/routes/attachments/update.js index 3cb7e40e..65ec5938 100644 --- a/src/routes/attachments/update.js +++ b/src/routes/attachments/update.js @@ -2,12 +2,11 @@ import validate from 'express-validation'; import _ from 'lodash'; import Joi from 'joi'; - -import models from '../../models'; -import util from '../../util'; import { middleware as tcMiddleware, } from 'tc-core-library-js'; +import models from '../../models'; +import util from '../../util'; /** * API to update a project member. @@ -45,9 +44,10 @@ module.exports = [ }) .then((resp) => { const affectedCount = resp.shift(); - if (affectedCount == 0) { + if (affectedCount === 0) { // handle 404 - const err = new Error(`project attachment not found for project id ${projectId} and member id ${attachmentId}`); + const err = new Error('project attachment not found for project id ' + + `${projectId} and member id ${attachmentId}`); err.status = 404; return Promise.reject(err); } diff --git a/src/routes/attachments/update.spec.js b/src/routes/attachments/update.spec.js index abcf17fe..4001dd95 100644 --- a/src/routes/attachments/update.spec.js +++ b/src/routes/attachments/update.spec.js @@ -4,16 +4,14 @@ import sinon from 'sinon'; import request from 'supertest'; import models from '../../models'; -import util from '../../util'; import server from '../../app'; import testUtil from '../../tests/util'; const should = chai.should(); describe('Project Attachments update', () => { - let project1, - member1, - attachment; + let project1; + let attachment; beforeEach((done) => { testUtil.clearDb() .then(() => { @@ -37,23 +35,20 @@ describe('Project Attachments update', () => { isPrimary: true, createdBy: 1, updatedBy: 1, - }).then((pm) => { - member1 = pm; - return models.ProjectAttachment.create({ - projectId: project1.id, - title: 'test.txt', - description: 'blah', - contentType: 'application/unknown', - size: 12312, - category: null, - filePath: 'https://media.topcoder.com/projects/1/test.txt', - createdBy: 1, - updatedBy: 1, - }).then((a1) => { - attachment = a1; - done(); - }); - }); + }).then(() => models.ProjectAttachment.create({ + projectId: project1.id, + title: 'test.txt', + description: 'blah', + contentType: 'application/unknown', + size: 12312, + category: null, + filePath: 'https://media.topcoder.com/projects/1/test.txt', + createdBy: 1, + updatedBy: 1, + }).then((a1) => { + attachment = a1; + done(); + })); }); }); }); diff --git a/src/routes/index.js b/src/routes/index.js index 8889c61b..8db4fe11 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -21,6 +21,7 @@ router.get('/_health', (req, res) => { // All project service endpoints need authentication const jwtAuth = require('tc-core-library-js').middleware.jwtAuthenticator; + router.all('/v4/projects*', jwtAuth()); // Register all the routes diff --git a/src/routes/projectMembers/create.js b/src/routes/projectMembers/create.js index 805728f6..64f2dfe7 100644 --- a/src/routes/projectMembers/create.js +++ b/src/routes/projectMembers/create.js @@ -3,11 +3,10 @@ import validate from 'express-validation'; import _ from 'lodash'; import Joi from 'joi'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; -import { PROJECT_MEMBER_ROLE } from '../../constants'; -import { middleware as tcMiddleware } from 'tc-core-library-js'; -import { EVENT } from '../../constants'; +import { PROJECT_MEMBER_ROLE, EVENT } from '../../constants'; /** * API to add a project member. @@ -20,7 +19,8 @@ const addMemberValidations = { param: Joi.object().keys({ userId: Joi.number().required(), isPrimary: Joi.boolean(), - role: Joi.any().valid(PROJECT_MEMBER_ROLE.CUSTOMER, PROJECT_MEMBER_ROLE.MANAGER, PROJECT_MEMBER_ROLE.COPILOT).required(), + role: Joi.any().valid(PROJECT_MEMBER_ROLE.CUSTOMER, PROJECT_MEMBER_ROLE.MANAGER, + PROJECT_MEMBER_ROLE.COPILOT).required(), }).required(), }, }; diff --git a/src/routes/projectMembers/create.spec.js b/src/routes/projectMembers/create.spec.js index bd25f5fe..b0893a03 100644 --- a/src/routes/projectMembers/create.spec.js +++ b/src/routes/projectMembers/create.spec.js @@ -12,8 +12,8 @@ import testUtil from '../../tests/util'; const should = chai.should(); describe('Project Members create', () => { - let project1, - project2; + let project1; + let project2; before((done) => { testUtil.clearDb() .then(() => { diff --git a/src/routes/projectMembers/delete.js b/src/routes/projectMembers/delete.js index 27e13f17..e1b761b9 100644 --- a/src/routes/projectMembers/delete.js +++ b/src/routes/projectMembers/delete.js @@ -1,9 +1,9 @@ import _ from 'lodash'; - -import models from '../../models'; +import winston from 'winston'; import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../models'; import { EVENT, PROJECT_MEMBER_ROLE } from '../../constants'; /** @@ -29,7 +29,7 @@ module.exports = [ err.status = 404; return Promise.reject(err); } - return member.destroy({ logging: console.log }); + return member.destroy({ logging: winston.info }); }) .then(member => member.save()) // if primary co-pilot is removed promote the next co-pilot to primary #43 diff --git a/src/routes/projectMembers/delete.spec.js b/src/routes/projectMembers/delete.spec.js index d209e63c..f34158c7 100644 --- a/src/routes/projectMembers/delete.spec.js +++ b/src/routes/projectMembers/delete.spec.js @@ -12,9 +12,9 @@ import testUtil from '../../tests/util'; const should = chai.should(); describe('Project members delete', () => { - let project1, - member1, - member2; + let project1; + let member1; + let member2; beforeEach((done) => { testUtil.clearDb() .then(() => { @@ -106,7 +106,8 @@ describe('Project members delete', () => { role: 'copilot', isPrimary: true, }; - server.services.pubsub.publish.calledWith('project.member.removed', sinon.match(removedMember)).should.be.true; + server.services.pubsub.publish.calledWith('project.member.removed', + sinon.match(removedMember)).should.be.true; done(); // models.ProjectMember // .count({where: { projectId: project1.id, deletedAt: { $eq: null } }}) @@ -164,9 +165,13 @@ describe('Project members delete', () => { role: 'copilot', isPrimary: true, }; - server.services.pubsub.publish.calledWith('project.member.removed', sinon.match(removedMember)).should.be.true; + server.services.pubsub.publish.calledWith('project.member.removed', + sinon.match(removedMember)).should.be.true; // validate the primary copilot - models.ProjectMember.findAll({ paranoid: true, where: { projectId: project1.id, role: 'copilot', isPrimary: true } }) + models.ProjectMember.findAll({ paranoid: true, + where: { projectId: project1.id, + role: 'copilot', + isPrimary: true } }) .then((members) => { should.exist(members); members.length.should.equal(1); @@ -213,7 +218,8 @@ describe('Project members delete', () => { role: 'manager', isPrimary: true, }; - server.services.pubsub.publish.calledWith('project.member.removed', sinon.match(removedMember)).should.be.true; + server.services.pubsub.publish.calledWith('project.member.removed', + sinon.match(removedMember)).should.be.true; postSpy.should.have.been.calledOnce; done(); }); @@ -254,7 +260,8 @@ describe('Project members delete', () => { role: 'manager', isPrimary: true, }; - server.services.pubsub.publish.calledWith('project.member.removed', sinon.match(removedMember)).should.be.true; + server.services.pubsub.publish.calledWith('project.member.removed', + sinon.match(removedMember)).should.be.true; postSpy.should.not.have.been.calledOnce; done(); }); diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index ac89f67c..d4370fae 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -2,12 +2,10 @@ import validate from 'express-validation'; import _ from 'lodash'; import Joi from 'joi'; - +import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; import { EVENT, PROJECT_MEMBER_ROLE } from '../../constants'; -import { middleware as tcMiddleware } from 'tc-core-library-js'; -import directProject from '../../services/directProject'; /** * API to update a project member. @@ -18,7 +16,8 @@ const updateProjectMemberValdiations = { body: { param: Joi.object().keys({ isPrimary: Joi.boolean(), - role: Joi.any().valid(PROJECT_MEMBER_ROLE.CUSTOMER, PROJECT_MEMBER_ROLE.MANAGER, PROJECT_MEMBER_ROLE.COPILOT).required(), + role: Joi.any().valid(PROJECT_MEMBER_ROLE.CUSTOMER, PROJECT_MEMBER_ROLE.MANAGER, + PROJECT_MEMBER_ROLE.COPILOT).required(), }), }, }; @@ -31,21 +30,22 @@ module.exports = [ * Update a projectMember if the user has access */ (req, res, next) => { - let projectMember, - updatedProps = req.body.param; + let projectMember; + let updatedProps = req.body.param; const projectId = _.parseInt(req.params.projectId); const memberRecordId = _.parseInt(req.params.id); updatedProps = _.pick(updatedProps, ['isPrimary', 'role']); let previousValue; - let newValue; + // let newValue; models.sequelize.transaction(() => models.ProjectMember.findOne({ where: { id: memberRecordId, projectId }, }) .then((_member) => { if (!_member) { // handle 404 - const err = new Error(`project member not found for project id ${projectId} and member id ${memberRecordId}`); + const err = new Error(`project member not found for project id ${projectId} ` + + `and member id ${memberRecordId}`); err.status = 404; return Promise.reject(err); } @@ -53,7 +53,7 @@ module.exports = [ projectMember = _member; previousValue = _.clone(projectMember.get({ plain: true })); _.assign(projectMember, updatedProps); - newValue = projectMember.get({ plain: true }); + // newValue = projectMember.get({ plain: true }); // no updates if no change if (updatedProps.role === previousValue.role && diff --git a/src/routes/projectMembers/update.spec.js b/src/routes/projectMembers/update.spec.js index 799a2d5d..37fefadf 100644 --- a/src/routes/projectMembers/update.spec.js +++ b/src/routes/projectMembers/update.spec.js @@ -11,10 +11,10 @@ import testUtil from '../../tests/util'; const should = chai.should(); describe('Project members update', () => { - let project1, - member1, - member2, - member3; + let project1; + let member1; + let member2; + let member3; beforeEach((done) => { testUtil.clearDb() .then(() => { @@ -156,7 +156,8 @@ describe('Project members update', () => { const result = res.body.result; result.success.should.be.false; result.status.should.equal(404); - result.content.message.should.equal(`project member not found for project id ${project1.id} and member id 999999`); + result.content.message.should.equal('project member not found for project id' + + ` ${project1.id} and member id 999999`); done(); }); }); diff --git a/src/routes/projects/create.spec.js b/src/routes/projects/create.spec.js index 477e9ecb..0dd1784d 100644 --- a/src/routes/projects/create.spec.js +++ b/src/routes/projects/create.spec.js @@ -3,6 +3,7 @@ import _ from 'lodash'; import chai from 'chai'; import sinon from 'sinon'; import request from 'supertest'; +import winston from 'winston'; import util from '../../util'; import server from '../../app'; @@ -13,7 +14,7 @@ const should = chai.should(); sinon.stub(RabbitMQService.prototype, 'init', () => {}); sinon.stub(RabbitMQService.prototype, 'publish', () => { - console.log('publish called'); + winston.info('publish called'); }); describe('Project create', () => { diff --git a/src/routes/projects/delete.js b/src/routes/projects/delete.js index d5f03aaa..a2ea2b19 100644 --- a/src/routes/projects/delete.js +++ b/src/routes/projects/delete.js @@ -1,11 +1,8 @@ - -// import validate from 'express-validation' import _ from 'lodash'; -import { EVENT } from '../../constants.js'; -import models from '../../models'; -import fileService from '../../services/fileService'; import { middleware as tcMiddleware } from 'tc-core-library-js'; +import { EVENT } from '../../constants'; +import models from '../../models'; /** * API to delete a project member. diff --git a/src/routes/projects/delete.spec.js b/src/routes/projects/delete.spec.js index 99587089..aeae3e17 100644 --- a/src/routes/projects/delete.spec.js +++ b/src/routes/projects/delete.spec.js @@ -1,21 +1,13 @@ -import _ from 'lodash'; -import chai from 'chai'; -import sinon from 'sinon'; import request from 'supertest'; import models from '../../models'; -import util from '../../util'; import server from '../../app'; import testUtil from '../../tests/util'; describe('Project delete test', () => { - let project1, - owner, - teamMember, - manager, - copilot; + let project1; beforeEach((done) => { testUtil.clearDb() .then(() => { @@ -71,11 +63,7 @@ describe('Project delete test', () => { }), ]; Promise.all(promises) - .then((res) => { - owner = res[0]; - manager = res[2]; - copilot = res[3]; - teamMember = res[4]; + .then(() => { done(); }); }); @@ -103,7 +91,7 @@ describe('Project delete test', () => { Authorization: `Bearer ${testUtil.jwts.member}`, }) .expect(204) - .end((err, resp) => { + .end((err) => { if (err) { return done(err); } diff --git a/src/routes/projects/get.js b/src/routes/projects/get.js index 8150715c..077f06e4 100644 --- a/src/routes/projects/get.js +++ b/src/routes/projects/get.js @@ -2,11 +2,9 @@ /* globals Promise */ import _ from 'lodash'; - +import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import util from '../../util'; -import { middleware as tcMiddleware } from 'tc-core-library-js'; - /** /** diff --git a/src/routes/projects/get.spec.js b/src/routes/projects/get.spec.js index b8004a0c..73317b19 100644 --- a/src/routes/projects/get.spec.js +++ b/src/routes/projects/get.spec.js @@ -11,8 +11,8 @@ import testUtil from '../../tests/util'; const should = chai.should(); describe('GET Project', () => { - let project1, - project2; + let project1; + let project2; before((done) => { testUtil.clearDb() .then(() => { diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index eeadb656..5545b921 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -98,7 +98,7 @@ module.exports = [ // handle filters let filters = util.parseQueryFilter(req.query.filter); let sort = req.query.sort ? decodeURIComponent(req.query.sort) : 'createdAt'; - if (sort && sort.indexOf(' ') == -1) { + if (sort && sort.indexOf(' ') === -1) { sort += ' asc'; } const sortableProps = [ diff --git a/src/routes/projects/list.spec.js b/src/routes/projects/list.spec.js index a69a8c2c..80739536 100644 --- a/src/routes/projects/list.spec.js +++ b/src/routes/projects/list.spec.js @@ -2,6 +2,7 @@ import chai from 'chai'; import request from 'supertest'; +import winston from 'winston'; import models from '../../models'; import server from '../../app'; @@ -11,10 +12,11 @@ const should = chai.should(); /** * Add full text index for projects. + * @return {Promise} returns the promise */ function addFullTextIndex() { if (models.sequelize.options.dialect !== 'postgres') { - console.log('Not creating search index, must be using POSTGRES to do this'); + winston.info('Not creating search index, must be using POSTGRES to do this'); return; } @@ -22,26 +24,29 @@ function addFullTextIndex() { .query('ALTER TABLE projects ADD COLUMN "projectFullText" text;') .then(() => models.sequelize .query('UPDATE projects SET "projectFullText" = lower(' + - 'name || \' \' || coalesce(description, \'\') || \' \' || coalesce(details#>>\'{utm, code}\', \'\'));')).then(() => models.sequelize + 'name || \' \' || coalesce(description, \'\') || \' \' || coalesce(details#>>\'{utm, code}\', \'\'));')) + .then(() => models.sequelize .query('CREATE EXTENSION IF NOT EXISTS pg_trgm;')).then(() => models.sequelize - .query('CREATE INDEX project_text_search_idx ON projects USING GIN("projectFullText" gin_trgm_ops);')).then(() => models.sequelize + .query('CREATE INDEX project_text_search_idx ON projects USING GIN("projectFullText" gin_trgm_ops);')) + .then(() => models.sequelize .query('CREATE OR REPLACE FUNCTION project_text_update_trigger() RETURNS trigger AS $$ ' + 'begin ' + 'new."projectFullText" := ' + - 'lower(new.name || \' \' || coalesce(new.description, \'\') || \' \' || coalesce(new.details#>>\'{utm, code}\', \'\')); ' + + 'lower(new.name || \' \' || coalesce(new.description, \'\') || \' \' || ' + + ' coalesce(new.details#>>\'{utm, code}\', \'\')); ' + 'return new; ' + 'end ' + '$$ LANGUAGE plpgsql;')).then(() => models.sequelize .query('DROP TRIGGER IF EXISTS project_text_update ON projects;')).then(() => models.sequelize .query('CREATE TRIGGER project_text_update BEFORE INSERT OR UPDATE ON projects' + ' FOR EACH ROW EXECUTE PROCEDURE project_text_update_trigger();')).catch((err) => { - console.log('Failed: ', err); + winston.error('Failed: ', err); }); } describe('LIST Project', () => { - let project1, - project2; + let project1; + let project2; before((done) => { testUtil.clearDb() .then(() => addFullTextIndex()) @@ -173,7 +178,8 @@ describe('LIST Project', () => { }); }); - it('should return the project when project that is in reviewed state AND does not yet have a co-pilot assigned', (done) => { + it('should return the project when project that is in reviewed state AND does not yet' + + 'have a co-pilot assigned', (done) => { request(server) .get('/v4/projects') .set({ diff --git a/src/routes/projects/update.js b/src/routes/projects/update.js index 4c0aa7e8..9b4e391c 100644 --- a/src/routes/projects/update.js +++ b/src/routes/projects/update.js @@ -112,7 +112,8 @@ module.exports = [ let updatedProps = req.body.param; const projectId = _.parseInt(req.params.projectId); // prune any fields that cannot be updated directly - updatedProps = _.omit(updatedProps, ['createdBy', 'createdAt', 'updatedBy', 'updatedAt', 'id', 'directProjectId']); + updatedProps = _.omit(updatedProps, ['createdBy', 'createdAt', 'updatedBy', 'updatedAt', + 'id', 'directProjectId']); let previousValue; models.sequelize.transaction(() => models.Project.findOne({ @@ -153,7 +154,8 @@ module.exports = [ _.isUndefined(_.find(members, m => m.userId === req.authUser.userId && matchRole(m.role))) ) { - const err = new Error('Only assigned topcoder-managers or topcoder admins should be allowed to launch a project'); + const err = new Error('Only assigned topcoder-managers or topcoder admins should be allowed ' + + 'to launch a project'); err.status = 403; return Promise.reject(err); } diff --git a/src/routes/projects/update.spec.js b/src/routes/projects/update.spec.js index d27031af..e149dfc7 100644 --- a/src/routes/projects/update.spec.js +++ b/src/routes/projects/update.spec.js @@ -13,9 +13,9 @@ import { PROJECT_STATUS } from '../../constants'; const should = chai.should(); describe('Project', () => { - let project1, - project2, - project3; + let project1; + let project2; + let project3; beforeEach((done) => { testUtil.clearDb(done); }); @@ -588,11 +588,11 @@ describe('Project', () => { }) .expect('Content-Type', /json/) .expect(200) - .end((err, res) => { - if (err) { - return done(err); + .end((error, resp) => { + if (error) { + return done(error); } - resJson = res.body.result.content; + resJson = resp.body.result.content; should.exist(resJson); should.not.exist(resJson.bookmarks); server.services.pubsub.publish.calledWith('project.updated').should.be.true; diff --git a/src/services/fileService.js b/src/services/fileService.js index fdbc56cb..f725e771 100644 --- a/src/services/fileService.js +++ b/src/services/fileService.js @@ -1,14 +1,14 @@ - -import util from '../util'; -import config from 'config'; /** * Service methods to handle direct project. */ +import config from 'config'; +import util from '../util'; + /** * Build custom http client for request - * @param req request - * @returns custom http client + * @param {Object} req request + * @return {Object} custom http client * @private */ function _getHttpClient(req) { @@ -31,6 +31,10 @@ function _getHttpClient(req) { export default { /** * Delete file from S3 using fileservice + * + * @param {Object} req the request + * @param {String} filePath the file path + * @return {Void} the function returns void */ deleteFile(req, filePath) { _getHttpClient(req).delete('', { params: { filter: `filePath%3D${filePath}` } }); diff --git a/src/services/index.js b/src/services/index.js index 9eaaabc6..a24a17c8 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -6,6 +6,11 @@ import RabbitMQService from './rabbitmq'; /** * Responsible for establishing connections to all external services * Also has a hook to load mock services for unit testing. + * + * @param {Object} app the app object + * @param {Object} logger the logger to use + * + * @return {Void} the function returns void */ module.exports = (app, logger) => { app.services = app.service || {}; diff --git a/src/tests/seed.js b/src/tests/seed.js index 54fb5100..a00dd433 100644 --- a/src/tests/seed.js +++ b/src/tests/seed.js @@ -1,4 +1,6 @@ +import winston from 'winston'; import models from '../models'; + models.sequelize.sync({ force: true }) .then(() => models.Project.bulkCreate([{ @@ -102,7 +104,7 @@ models.sequelize.sync({ force: true }) return Promise.all(operations); }) .then(() => { - console.log('Success'); + winston.info('Success'); process.exit(0); }) - .catch(err => console.log('Failed: ', err)); + .catch(err => winston.error('Failed: ', err)); diff --git a/src/tests/util.js b/src/tests/util.js index 4387755b..3035a1bc 100644 --- a/src/tests/util.js +++ b/src/tests/util.js @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ import models from '../models'; diff --git a/src/util.js b/src/util.js index 463e5387..f1312396 100644 --- a/src/util.js +++ b/src/util.js @@ -15,13 +15,15 @@ import querystring from 'querystring'; import config from 'config'; const util = _.cloneDeep(require('tc-core-library-js').util(config)); + _.assignIn(util, { /** * Handle error - * @param defaultMessage the default error message - * @param err the err - * @param next the next function - * @returns next function with error + * @param {String} msg the default error message + * @param {Error} err the err + * @param {Object} req the request + * @param {Function} next the next function + * @returns {Function} next function with error */ handleError: (msg, err, req, next) => { req.log.error({ @@ -37,9 +39,9 @@ _.assignIn(util, { }, /** * Validates if filters are valid - * @param {object} filters object with filters - * @param {array} validValues valid filter values - * @return {boolean} + * @param {object} filters object with filters + * @param {array} validValues valid filter values + * @return {boolean} true if filters are valid otherwise false */ isValidFilter: (filters, validValues) => { let valid = true; @@ -64,8 +66,9 @@ _.assignIn(util, { /** * Parses query fields and groups them per table - * @param {array} queryFields list of query fields - * @return {object} + * @param {array} queryFields list of query fields + * @param {Object} allowedFields the allowed fields + * @return {object} the parsed fields */ parseFields: (queryFields, allowedFields) => { const fields = _.cloneDeep(allowedFields); @@ -85,9 +88,9 @@ _.assignIn(util, { }, /** - * [description] - * @param {[type]} queryFilter [description] - * @return {[type]} [description] + * Parse the query filters + * @param {String} queryFilter the query filter string + * @return {Object} the parsed array */ parseQueryFilter: (queryFilter) => { queryFilter = querystring.parse(queryFilter); @@ -134,9 +137,9 @@ _.assignIn(util, { /** * retrieve download urls for all attachments - * @param {[type]} req original request - * @param {[type]} attachments list of attachments to retrieve urls for - * @return {[type]} [description] + * @param {Object} req original request + * @param {String} filePath the file path + * @return {String} the download url */ getFileDownloadUrl: (req, filePath) => { if (!filePath) { @@ -195,7 +198,8 @@ _.assignIn(util, { getSystemUserToken: (logger, id = 'system') => { const httpClient = util.getHttpClient({ id, log: logger }); const url = `${config.get('identityServiceEndpoint')}authorizations`; - const formData = `clientId=${config.get('systemUserClientId')}&secret=${encodeURIComponent(config.get('systemUserClientSecret'))}`; + const formData = `clientId=${config.get('systemUserClientId')}&` + + `secret=${encodeURIComponent(config.get('systemUserClientSecret'))}`; return httpClient.post(url, formData, { timeout: 4000, @@ -208,11 +212,11 @@ _.assignIn(util, { /** * Fetches the topcoder user details using the given JWT token. * - * @param userId id of the user to be fetched - * @param jwtToken JWT token of the admin user or JWT token of the user to be fecthed - * @param logger logger to be used for logging purposes + * @param {Number} userId id of the user to be fetched + * @param {String} jwtToken JWT token of the admin user or JWT token of the user to be fecthed + * @param {Object} logger logger to be used for logging purposes * - * @return promise which resolves to the user's information + * @return {Promise} promise which resolves to the user's information */ getTopcoderUser: (userId, jwtToken, logger) => { const httpClient = util.getHttpClient({ id: `userService_${userId}`, log: logger }); @@ -222,7 +226,7 @@ _.assignIn(util, { httpClient.defaults.headers.common.Authorization = `Bearer ${jwtToken}`; return httpClient.get(`${config.userServiceUrl}/${userId}`).then((response) => { if (response.data && response.data.result - && response.data.result.status == 200 && response.data.result.content) { + && response.data.result.status === 200 && response.data.result.content) { return response.data.result.content; } return null;