From 60f3ce00e4b03e30cc45e9a9081e5e44c3311ebb Mon Sep 17 00:00:00 2001 From: Ritesh Sangwan Date: Thu, 2 Mar 2017 00:07:46 +0530 Subject: [PATCH 1/8] TOPCODER CONNECT PROJECTS API ENHANCEMENTS WINNING SUBMISSION (#45) * update readme, update node js version * fix #34 * fix #37, keep project history * move project history update to core API's from event handlers * fix #43 * add unit tests * update direct mock service * update swagger.yaml * add sync script * update postman * update readme * update tests * filter members by copilot role, when promoting copilot --- README.md | 7 +- package.json | 1 + postman.json | 1604 +++++++++++++++------- src/events/projectMembers/index.js | 99 +- src/mocks/direct.js | 11 + src/models/project.js | 1 + src/models/projectHistory.js | 22 + src/routes/attachments/create.js | 2 +- src/routes/projectMembers/create.js | 2 +- src/routes/projectMembers/delete.js | 86 +- src/routes/projectMembers/delete.spec.js | 144 +- src/routes/projectMembers/update.spec.js | 115 +- src/routes/projects/create.js | 4 +- src/routes/projects/list.js | 2 +- src/routes/projects/update.js | 48 +- src/routes/projects/update.spec.js | 250 +++- src/services/directProject.js | 10 + swagger.yaml | 153 ++- sync.js | 20 + 19 files changed, 2003 insertions(+), 578 deletions(-) create mode 100644 src/models/projectHistory.js create mode 100644 sync.js diff --git a/README.md b/README.md index 57fb6ad8..7d06f1dd 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Microservice to manage CRUD operations for all things Projects. ### Local Development * We use docker-compose for running dependencies locally. Instructions for Docker compose setup - https://docs.docker.com/compose/install/ -* Nodejs 5.10.1 - consider using [nvm](https://github.com/creationix/nvm) or equivalent to manage your node version +* Nodejs 6.9.4 - consider using [nvm](https://github.com/creationix/nvm) or equivalent to manage your node version * Install [libpg](https://www.npmjs.com/package/pg-native) * Install node dependencies `npm install | ./node_modules/.bin/bunyan` @@ -25,6 +25,8 @@ npm run -s build > NODE_ENV=development node -e "require('./dist/models').default.sequelize.sync({force: true}).then((res)=> console.log('Success: ', res)).catch((err)=> console.log('Failed: ', err));" ``` +Other simple approach to create tables is to run `npm run sync` from root of project. This additional script is added to make the above task simpler. + #### Redis Docker compose command will start a local redis instance as well. You should be able to connect to this instance using url `$(docker-machine ip):6379` @@ -51,5 +53,8 @@ You may replace 172.17.0.1 with your docker0 IP. You can paste **swagger.yaml** to [swagger editor](http://editor.swagger.io/) or import **postman.json** to verify endpoints. +#### Deploying without docker +If you don't want to use docker to deploy to localhost. You can simply run `npm run start` from root of project. This should start the server on default port `3000`. + ### Deployment Using awsebcli - http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/eb-cli3-install.html diff --git a/package.json b/package.json index d0fee265..76477cd9 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "prestart": "npm run -s build", "seed": "babel-node src/tests/seed.js --presets es2015", "direct": "babel-node src/mocks/direct.js --presets es2015", + "sync": "node sync.js", "lint": "./node_modules/.bin/eslint src" }, "repository": { diff --git a/postman.json b/postman.json index 41c30408..ab4617db 100644 --- a/postman.json +++ b/postman.json @@ -1,525 +1,1123 @@ { - "id": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "name": "tc-project-service ", - "description": "", - "order": [], - "folders": [ + "variables": [], + "info": { + "name": "tc-project-service ", + "_postman_id": "85538c36-6562-9d05-bec5-b04dc8f33ef9", + "description": "", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + }, + "item": [ { - "id": "a56012d4-9aaf-1df7-4fe7-6cef64d91a22", - "name": "bookmarks", - "description": "", - "order": [ - "e146bef2-7ea0-aebb-aa27-54785b64765d", - "53d2c743-75a3-da8b-5da6-a2c123291b73", - "e798a2e0-09c3-160a-5401-30748e2066b0", - "bfb954da-8007-81f6-50e0-33570ac72320", - "db09f872-d175-4066-52c4-77ee81aca14f", - "9ca83b2c-eb99-0a21-774f-ebbd32862db6", - "60784e18-9bcb-e5f8-3ce5-7ef440aa5ad4", - "de9780e3-b6c1-9a78-eb3e-6bb9fb26aeea" - ], - "owner": 0 - }, - { - "id": "a555eab3-f75e-6d88-daba-7d5a5f92c56f", - "name": "issue1", - "description": "", - "order": [ - "9db67567-033c-3d07-cdb5-6e5db4bde1c9" - ], - "owner": 0, - "collectionId": "160ba566-2572-1f51-d620-c7615f3411e6" - }, - { - "id": "434cd818-a881-e83f-0e92-4f8545a43942", - "name": "issue10", - "description": "", - "order": [ - "018056b4-f539-e267-e895-2fee42a75f88", - "cc4c20b8-b153-57c9-0e8b-55a01b080e82" - ], - "owner": 0, - "collectionId": "160ba566-2572-1f51-d620-c7615f3411e6" - }, - { - "id": "c44adfb1-dfd4-fa39-e11d-82d239e82292", - "name": "issue5", - "description": "", - "order": [ - "0e98aa1a-e66f-de89-e273-79b54a29ba72", - "6a963b29-f7aa-5ed1-d6f9-b315090af2d4", - "662b20de-1060-9e37-77e8-b54946e7c3a2" - ], - "owner": 0 - }, - { - "id": "47bf002c-126b-0994-8439-b1108b7d049a", - "name": "issue8", - "description": "", - "order": [ - "37f501b2-e28e-de3e-2ad0-9cf5ad8c415b", - "6fb063f5-2316-f835-c0ea-34326343e9a2", - "0ec296ec-0cff-125e-8b82-a85895f714f1", - "7a07ab7f-3142-c2fc-24a6-7de562084767", - "f3a6be9f-9c57-5c48-3019-5f3381fcbbb2", - "378b26c1-6d02-98e0-3b58-53d4b817391b", - "3537eb17-9a0a-02f0-d48e-457e628a4cc3" - ], - "owner": 0, - "collectionId": "160ba566-2572-1f51-d620-c7615f3411e6" - } - ], - "timestamp": 1470532482194, - "owner": 0, - "public": false, - "published": false, - "requests": [ - { - "id": "018056b4-f539-e267-e895-2fee42a75f88", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts\nContent-Type: application/json\n", - "url": "http://localhost:3000/v4/projects/3/members/5", - "preRequestScript": null, - "pathVariables": {}, - "method": "PATCH", - "data": [], - "dataMode": "raw", - "version": 2, - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1470657192407, - "name": "wrong role", - "description": "", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "responses": [], - "rawModeData": " {\n \"param\": {\n \"role\": \"wrong\"\n }\n } ", - "folder": "434cd818-a881-e83f-0e92-4f8545a43942" - }, - { - "id": "0e98aa1a-e66f-de89-e273-79b54a29ba72", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts\nContent-Type: application/json\n", - "url": "http://localhost:3000/v4/projects/1", - "preRequestScript": null, - "pathVariables": {}, - "method": "PATCH", - "data": [], - "dataMode": "raw", - "version": 2, - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1470655466654, - "name": "launch a project by topcoder managers ", - "description": "", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "responses": [], - "rawModeData": "{\n \n \"param\":{\n \"name\": \"updatedProject name\",\n \"status\": \"active\"\n }\n}", - "folder": "c44adfb1-dfd4-fa39-e11d-82d239e82292" - }, - { - "id": "0ec296ec-0cff-125e-8b82-a85895f714f1", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts\nContent-Type: application/json\n", - "url": "http://localhost:3000/v4/projects/1/members", - "preRequestScript": null, - "pathVariables": {}, - "method": "POST", - "data": [], - "dataMode": "raw", - "version": 2, - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1470656347172, - "name": "Response error from direct project service", - "description": "", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "responses": [], - "rawModeData": "{\n \"param\": {\n \"userId\": 2, \n \"role\": \"copilot\"\n }\n}", - "folder": "47bf002c-126b-0994-8439-b1108b7d049a" - }, - { - "id": "3537eb17-9a0a-02f0-d48e-457e628a4cc3", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts\nContent-Type: application/json\n", - "url": "http://localhost:3000/v4/projects/2/members/4", - "preRequestScript": null, - "pathVariables": {}, - "method": "DELETE", - "data": [], - "dataMode": "raw", - "version": 2, - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1470656184824, - "name": "Delete co-pilot when a co-pilot is removed from a project", - "description": "", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "responses": [], - "rawModeData": "", - "folder": "47bf002c-126b-0994-8439-b1108b7d049a" - }, - { - "id": "378b26c1-6d02-98e0-3b58-53d4b817391b", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts\nContent-Type: application/json\n", - "url": "http://localhost:3000/v4/projects/2", - "preRequestScript": null, - "pathVariables": {}, - "method": "PATCH", - "data": [], - "dataMode": "raw", - "version": 2, - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1470656225362, - "name": " Sync billing account id with direct", - "description": "", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "responses": [], - "rawModeData": "{\n \"param\": {\n \"billingAccountId\": 9999, \n \"name\": \"new project name\"\n }\n}", - "folder": "47bf002c-126b-0994-8439-b1108b7d049a" - }, - { - "id": "37f501b2-e28e-de3e-2ad0-9cf5ad8c415b", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts\nContent-Type: application/json\n", - "url": "https://localhost:8443/v3/direct/projects", - "preRequestScript": null, - "pathVariables": {}, - "method": "GET", - "data": [], - "dataMode": "raw", - "version": 2, - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1470615101332, - "name": "mock direct projects", - "description": "", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "responses": [], - "rawModeData": " {\n \"param\": {\n \"role\": \"copilot\",\n \"isPrimary\": true\n }\n } ", - "folder": "47bf002c-126b-0994-8439-b1108b7d049a" - }, - { - "id": "53d2c743-75a3-da8b-5da6-a2c123291b73", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts\nContent-Type: application/json\n", - "url": "http://localhost:3000/v4/projects", - "pathVariables": {}, - "preRequestScript": null, - "method": "POST", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "data": [], - "dataMode": "raw", - "name": " Create project with valid bookmarks", - "description": "", - "descriptionFormat": "html", - "time": 1471670842546, - "version": 2, - "responses": [], - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "folder": "a56012d4-9aaf-1df7-4fe7-6cef64d91a22", - "rawModeData": "{\n \"param\": {\n \"type\": \"generic\",\n \"description\": \"test project\",\n \"details\": {},\n \"bookmarks\":[{\n \"title\":\"title1\",\n \"address\":\"address1\"\n },{\n \"title\":\"title2\",\n \"address\":\"address2\"\n }],\n \"billingAccountId\": 123,\n \"name\": \"test project1\"\n }\n}" - }, - { - "id": "60784e18-9bcb-e5f8-3ce5-7ef440aa5ad4", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts\nContent-Type: application/json\n", - "url": "http://localhost:3000/v4/projects/2", - "pathVariables": {}, - "preRequestScript": null, - "method": "PATCH", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "data": [], - "dataMode": "raw", - "name": "Update project with invalid bookmarks", - "description": "", - "descriptionFormat": "html", - "time": 1471670874088, - "version": 2, - "responses": [], - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "folder": "a56012d4-9aaf-1df7-4fe7-6cef64d91a22", - "rawModeData": "{\n \"param\": {\n \"billingAccountId\": 9999, \n \"name\": \"new project name2\",\n \"bookmarks\":3\n }\n}" - }, - { - "id": "662b20de-1060-9e37-77e8-b54946e7c3a2", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJjb3BpbG90Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMiIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.tY_eE9fjtKQ_Hp9XPwmhwMaaTdOYKoR09tdGgvZ8RLw\nContent-Type: application/json\n", - "url": "http://localhost:3000/v4/projects/1", - "preRequestScript": null, - "pathVariables": {}, - "method": "PATCH", - "data": [], - "dataMode": "raw", - "version": 2, - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1470561983805, - "name": "launch a project by copilot", - "description": "", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "responses": [], - "rawModeData": "{\n \n \"param\":{\n \"name\": \"updatedProject name\",\n \"status\": \"active\"\n }\n}", - "folder": "c44adfb1-dfd4-fa39-e11d-82d239e82292" - }, - { - "id": "6a963b29-f7aa-5ed1-d6f9-b315090af2d4", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6W10sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMSIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.p13tStpp0A1RJjYJ2axSKCTx7lyWIS3kYtCvs8u88WM\nContent-Type: application/json\n", - "url": "http://localhost:3000/v4/projects/1", - "preRequestScript": null, - "pathVariables": {}, - "method": "PATCH", - "data": [], - "dataMode": "raw", - "version": 2, - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1470574753479, - "name": "launch a project by member", - "description": "", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "responses": [], - "rawModeData": "{\n \n \"param\":{\n \"name\": \"updatedProject name\",\n \"status\": \"active\"\n }\n}", - "folder": "c44adfb1-dfd4-fa39-e11d-82d239e82292" - }, - { - "id": "6fb063f5-2316-f835-c0ea-34326343e9a2", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts\nContent-Type: application/json\n", - "url": "http://localhost:3000/v4/projects", - "preRequestScript": null, - "pathVariables": {}, - "method": "POST", - "data": [], - "dataMode": "raw", - "version": 2, - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1470625658104, - "name": " Create direct project when a new project is successfully created", - "description": "", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "responses": [], - "rawModeData": "{\n \"param\": {\n \"type\": \"generic\",\n \"description\": \"test project\",\n \"details\": {},\n \"billingAccountId\": 123,\n \"name\": \"test project1\"\n }\n}", - "folder": "47bf002c-126b-0994-8439-b1108b7d049a" - }, - { - "id": "7a07ab7f-3142-c2fc-24a6-7de562084767", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts\nContent-Type: application/json\n", - "url": "http://localhost:3000/v4/projects/2/members", - "preRequestScript": null, - "pathVariables": {}, - "method": "POST", - "data": [], - "dataMode": "raw", - "version": 2, - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1470655602176, - "name": " Add co-pilot when a co-pilot is added to a project", - "description": "", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "responses": [], - "rawModeData": "{\n \"param\": {\n \"userId\": 2, \n \"role\": \"copilot\"\n }\n}", - "folder": "47bf002c-126b-0994-8439-b1108b7d049a" - }, - { - "id": "9ca83b2c-eb99-0a21-774f-ebbd32862db6", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts\nContent-Type: application/json\n", - "url": "http://localhost:3000/v4/projects/2", - "pathVariables": {}, - "preRequestScript": null, - "method": "PATCH", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "data": [], - "dataMode": "raw", - "name": "Delete project all bookmarks null", + "name": "Project Members", "description": "", - "descriptionFormat": "html", - "time": 1471670868007, - "version": 2, - "responses": [], - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "folder": "a56012d4-9aaf-1df7-4fe7-6cef64d91a22", - "rawModeData": "{\n \"param\": {\n \"billingAccountId\": 9999, \n \"name\": \"new project name2\",\n \"bookmarks\":null\n }\n}" + "item": [ + { + "name": "Create project member with no payload", + "request": { + "url": "http://localhost:3000/v4/projects/1/members", + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "Request payload is mandatory while creating project. If no request payload is specified this should result in 422 status code." + }, + "response": [] + }, + { + "name": "Create project member with invalid userId", + "request": { + "url": "http://localhost:3000/v4/projects/1/members", + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"role\": \"copilot\"\n}" + }, + "description": "Certain fields are mandatory while creating project. If invalid fields are specified this should result in 422 status code." + }, + "response": [] + }, + { + "name": "Create project member with valid values", + "request": { + "url": "http://localhost:3000/v4/projects/1/members", + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"param\": {\n\t\t\"role\": \"copilot\",\n\t\t\"userId\": 40051331,\n\t\t\"isPrimary\": true\n\t}\n}" + }, + "description": "If the request payload is valid, than project member should be created." + }, + "response": [] + }, + { + "name": "Create project member, if user already registered", + "request": { + "url": "http://localhost:3000/v4/projects/1/members", + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"param\": {\n\t\t\"role\": \"copilot\",\n\t\t\"userId\": 40051331,\n\t\t\"isPrimary\": true\n\t}\n}" + }, + "description": "If the request payload is valid and user is already registered with the specified role than this should result in 400." + }, + "response": [] + }, + { + "name": "Create project manager with valid values", + "request": { + "url": "http://localhost:3000/v4/projects/1/members", + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"param\": {\n\t\t\"role\": \"manager\",\n\t\t\"userId\": 40051330,\n\t\t\"isPrimary\": true\n\t}\n}" + }, + "description": "If the request payload is valid, than project manager should be added. This should sync with the direct project is project is associated with direct project." + }, + "response": [] + }, + { + "name": "Update project member", + "request": { + "url": "http://localhost:3000/v4/projects/1/members/1", + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"param\": {\n\t\t\"role\": \"copilot\",\n\t\t\"isPrimary\": true\n\t}\n}" + }, + "description": "Update a project's member." + }, + "response": [] + }, + { + "name": "Delete project member", + "request": { + "url": "http://localhost:3000/v4/projects/2/members/2", + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "Delete a project's member" + }, + "response": [] + } + ] }, { - "id": "9db67567-033c-3d07-cdb5-6e5db4bde1c9", - "headers": "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBjb3BpbG90Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOjQwMDUxMzMyLCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwiaWF0IjoxNDcwNjIwMDQ0fQ.HUhRM1G6jwBt8Kv2slPYndhVUdYXXBAH174fPzqpFME\n", - "url": "http://localhost:3000/v4/projects", - "preRequestScript": null, - "pathVariables": {}, - "method": "GET", - "data": null, - "dataMode": "params", - "version": 2, - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1470655431639, - "name": "get projects with copilot token", - "description": "", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "responses": [], - "folder": "a555eab3-f75e-6d88-daba-7d5a5f92c56f" - }, - { - "id": "bfb954da-8007-81f6-50e0-33570ac72320", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts\nContent-Type: application/json\n", - "url": "http://localhost:3000/v4/projects/2", - "pathVariables": {}, - "preRequestScript": null, - "method": "GET", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "data": [], - "dataMode": "raw", - "name": "get project", - "description": "", - "descriptionFormat": "html", - "time": 1471670855697, - "version": 2, - "responses": [], - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "folder": "a56012d4-9aaf-1df7-4fe7-6cef64d91a22", - "rawModeData": "{\n \"param\": {\n \"billingAccountId\": 9999, \n \"name\": \"new project name\"\n }\n}" + "name": "Projects", + "description": "Requests for all things projects.", + "item": [ + { + "name": "Create project without payload", + "request": { + "url": "http://localhost:3000/v4/projects", + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNCIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.amNzJ6PknW8V1ZDrVxJAd-SxcyfYhTf80IBc1sH-vF8", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\n}" + }, + "description": "Request body is mandatory while creating project. If invalid request body is supplied this should return 422 status code." + }, + "response": [] + }, + { + "name": "Create project without valid name", + "request": { + "url": "http://localhost:3000/v4/projects", + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNCIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.amNzJ6PknW8V1ZDrVxJAd-SxcyfYhTf80IBc1sH-vF8", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"param\": {\n\t}\n}" + }, + "description": "Certain fields are mandatory while creating project. If invalid request body is supplied this should return 422 status code." + }, + "response": [] + }, + { + "name": "Create project with valid values", + "request": { + "url": "http://localhost:3000/v4/projects", + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNCIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.amNzJ6PknW8V1ZDrVxJAd-SxcyfYhTf80IBc1sH-vF8", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"param\": {\n\t\t\"name\": \"test project\",\n\t\t\"description\": \"Hello I am a test project\",\n\t\t\"type\": \"generic\"\n\t}\n}" + }, + "description": "Valid request body. Project should be created successfully." + }, + "response": [] + }, + { + "name": "Get project by id", + "request": { + "url": "http://localhost:3000/v4/projects/1", + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "description": "" + } + ], + "body": {}, + "description": "Get a project by id. project members and attachments should also be returned." + }, + "response": [] + }, + { + "name": "Get project by id and request specific fields", + "request": { + "url": "http://localhost:3000/v4/projects/1?fields=id,name,description,members.id,members.projectId", + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "description": "" + } + ], + "body": {}, + "description": "Get a project by id. project members and attachments should also be returned. Only those fields which are specified should be returned." + }, + "response": [] + }, + { + "name": "List projects", + "request": { + "url": "http://localhost:3000/v4/projects", + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "description": "" + } + ], + "body": {}, + "description": "List all the project with no filter. Default sort and limits are applied." + }, + "response": [] + }, + { + "name": "List projects with limit and offset", + "request": { + "url": "http://localhost:3000/v4/projects?limit=1&offset=1", + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "description": "" + } + ], + "body": {}, + "description": "List all the project with no filter. Limit of 1 and offset of 1 is applied" + }, + "response": [] + }, + { + "name": "List projects with filters applied", + "request": { + "url": "http://localhost:3000/v4/projects?filter=type%3Dgeneric", + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "description": "" + } + ], + "body": {}, + "description": "List all the project with filters applied. The filter string should be url encoded. Default limit and offset is applicable" + }, + "response": [] + }, + { + "name": "List projects with sort applied", + "request": { + "url": "http://localhost:3000/v4/projects?sort=type%20desc", + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "description": "" + } + ], + "body": {}, + "description": "List all the project with custom sort and no filter. Default sort and limits are applied. The sort string has to be url encoded. Sort is of type `key asc|desc`" + }, + "response": [] + }, + { + "name": "List projects and return specific fields", + "request": { + "url": "http://localhost:3000/v4/projects?fields=id,name,description", + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "description": "" + } + ], + "body": {}, + "description": "List all the project with no filter. Default sort and limits are applied. The fields to return is specified as comma separated list. Only those fields should be returned." + }, + "response": [] + }, + { + "name": "DELETE project by id", + "request": { + "url": "http://localhost:3000/v4/projects/3", + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "description": "" + } + ], + "body": {}, + "description": "Delete a project by id" + }, + "response": [] + }, + { + "name": "Update project", + "request": { + "url": "http://localhost:3000/v4/projects/1", + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNCIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.amNzJ6PknW8V1ZDrVxJAd-SxcyfYhTf80IBc1sH-vF8", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"param\": {\n\t\t\"name\": \"project name updated\"\n\t}\n}" + }, + "description": "Update the project name. Name should be updated successfully." + }, + "response": [] + }, + { + "name": "Update project 403", + "request": { + "url": "http://localhost:3000/v4/projects/2", + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMSIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.IgPq3dcPH-WJXQAytjF_4fJbx3gtsee1U3vmqGIGoUA", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"param\": {\n\t\t\"name\": \"project name updated\"\n\t}\n}" + }, + "description": "Update the project name. If user don't have permission to the project than it should return 403." + }, + "response": [] + }, + { + "name": "Update project 404", + "request": { + "url": "http://localhost:3000/v4/projects/10", + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNCIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.amNzJ6PknW8V1ZDrVxJAd-SxcyfYhTf80IBc1sH-vF8", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"param\": {\n\t\t\"name\": \"project name updated\"\n\t}\n}" + }, + "description": "Update the project name. If project is not found than this result in 404 status code." + }, + "response": [] + }, + { + "name": "Update project status to cancelled", + "request": { + "url": "http://localhost:3000/v4/projects/1", + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNCIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.amNzJ6PknW8V1ZDrVxJAd-SxcyfYhTf80IBc1sH-vF8", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"param\": {\n\t\t\"status\": \"cancelled\"\n\t}\n}" + }, + "description": "Update the project status. While cancelling the project `cancelReason` is mandatory. If no `cancelReason` is supplied this should result in 422 status code." + }, + "response": [] + }, + { + "name": "Update project status to cancelled with cancelReason", + "request": { + "url": "http://localhost:3000/v4/projects/1", + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNCIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.amNzJ6PknW8V1ZDrVxJAd-SxcyfYhTf80IBc1sH-vF8", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"param\": {\n\t\t\"status\": \"cancelled\",\n\t\t\"cancelReason\": \"price/cost\"\n\t}\n}" + }, + "description": "Update the project status. While cancelling the project `cancelReason` is mandatory." + }, + "response": [] + }, + { + "name": "Move project out of cancel state.", + "request": { + "url": "http://localhost:3000/v4/projects/1", + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNCIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.amNzJ6PknW8V1ZDrVxJAd-SxcyfYhTf80IBc1sH-vF8", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"param\": {\n\t\t\"status\": \"active\"\n\t}\n}" + }, + "description": "Move a project out of cancel state. Only admin and manager is allowed to do so." + }, + "response": [] + }, + { + "name": "Move project out of cancel state 403", + "request": { + "url": "http://localhost:3000/v4/projects/1", + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMSIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.IgPq3dcPH-WJXQAytjF_4fJbx3gtsee1U3vmqGIGoUA", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"param\": {\n\t\t\"status\": \"active\"\n\t}\n}" + }, + "description": "Move a project out of cancel state. Only admin and manager is allowed to do so." + }, + "response": [] + } + ] }, { - "id": "cc4c20b8-b153-57c9-0e8b-55a01b080e82", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts\nContent-Type: application/json\n", - "url": "http://localhost:3000/v4/projects/3/members/5", - "preRequestScript": null, - "pathVariables": {}, - "method": "PATCH", - "data": [], - "dataMode": "raw", - "version": 2, - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1470657179934, - "name": "editing project member roles & primary option", - "description": "", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "responses": [], - "rawModeData": " {\n \"param\": {\n \"role\": \"manager\",\n \"isPrimary\": true\n }\n } ", - "folder": "434cd818-a881-e83f-0e92-4f8545a43942" - }, - { - "id": "db09f872-d175-4066-52c4-77ee81aca14f", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts\nContent-Type: application/json\n", - "url": "http://localhost:3000/v4/projects/2", - "pathVariables": {}, - "preRequestScript": null, - "method": "PATCH", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "data": [], - "dataMode": "raw", - "name": "Update project with bookmarks", + "name": "bookmarks", "description": "", - "descriptionFormat": "html", - "time": 1471670861583, - "version": 2, - "responses": [], - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "folder": "a56012d4-9aaf-1df7-4fe7-6cef64d91a22", - "rawModeData": "{\n \"param\": {\n \"billingAccountId\": 9999, \n \"name\": \"new project name\",\n \"bookmarks\":[{\n \"title\":\"title1\",\n \"address\":\"address1\"\n },{\n \"title\":\"title2\",\n \"address\":\"address2\"\n }]\n }\n}" + "item": [ + { + "name": " Create project without bookmarks", + "request": { + "url": "http://localhost:3000/v4/projects", + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"param\": {\n \"type\": \"generic\",\n \"description\": \"test project\",\n \"details\": {},\n \"billingAccountId\": 123,\n \"name\": \"test project1\"\n }\n}" + }, + "description": "" + }, + "response": [] + }, + { + "name": " Create project with valid bookmarks", + "request": { + "url": "http://localhost:3000/v4/projects", + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"param\": {\n \"type\": \"generic\",\n \"description\": \"test project\",\n \"details\": {},\n \"bookmarks\":[{\n \"title\":\"title1\",\n \"address\":\"address1\"\n },{\n \"title\":\"title2\",\n \"address\":\"address2\"\n }],\n \"billingAccountId\": 123,\n \"name\": \"test project1\"\n }\n}" + }, + "description": "" + }, + "response": [] + }, + { + "name": " Create project with invalid bookmarks", + "request": { + "url": "http://localhost:3000/v4/projects", + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"param\": {\n \"type\": \"generic\",\n \"description\": \"test project\",\n \"details\": {},\n \"bookmarks\":[{\n \"title\":\"title1\",\n \"invalid\":3,\n \"address\":\"address1\"\n },{\n \"title\":\"title2\",\n \"address\":\"address2\"\n }],\n \"billingAccountId\": 123,\n \"name\": \"test project1\"\n }\n}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "get project", + "request": { + "url": "http://localhost:3000/v4/projects/2", + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"param\": {\n \"billingAccountId\": 9999, \n \"name\": \"new project name\"\n }\n}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Update project with bookmarks", + "request": { + "url": "http://localhost:3000/v4/projects/2", + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"param\": {\n \"billingAccountId\": 9999, \n \"name\": \"new project name\",\n \"bookmarks\":[{\n \"title\":\"title1\",\n \"address\":\"address1\"\n },{\n \"title\":\"title2\",\n \"address\":\"address2\"\n }]\n }\n}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Delete project all bookmarks null", + "request": { + "url": "http://localhost:3000/v4/projects/2", + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"param\": {\n \"billingAccountId\": 9999, \n \"name\": \"new project name2\",\n \"bookmarks\":null\n }\n}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Update project with invalid bookmarks", + "request": { + "url": "http://localhost:3000/v4/projects/2", + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"param\": {\n \"billingAccountId\": 9999, \n \"name\": \"new project name2\",\n \"bookmarks\":3\n }\n}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "get projects with admin token", + "request": { + "url": "http://localhost:3000/v4/projects", + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "description": "" + } + ], + "body": {}, + "description": "" + }, + "response": [] + } + ] }, { - "id": "de9780e3-b6c1-9a78-eb3e-6bb9fb26aeea", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts\n", - "url": "http://localhost:3000/v4/projects", - "pathVariables": {}, - "preRequestScript": null, - "method": "GET", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "data": null, - "dataMode": "params", - "name": "get projects with admin token", + "name": "issue1", "description": "", - "descriptionFormat": "html", - "time": 1471671128898, - "version": 2, - "responses": [], - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "folder": "a56012d4-9aaf-1df7-4fe7-6cef64d91a22" + "item": [ + { + "name": "get projects with copilot token", + "request": { + "url": "http://localhost:3000/v4/projects", + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBjb3BpbG90Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOjQwMDUxMzMyLCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwiaWF0IjoxNDcwNjIwMDQ0fQ.HUhRM1G6jwBt8Kv2slPYndhVUdYXXBAH174fPzqpFME", + "description": "" + } + ], + "body": {}, + "description": "" + }, + "response": [] + } + ] }, { - "id": "e146bef2-7ea0-aebb-aa27-54785b64765d", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts\nContent-Type: application/json\n", - "url": "http://localhost:3000/v4/projects", - "pathVariables": {}, - "preRequestScript": null, - "method": "POST", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "data": [], - "dataMode": "raw", - "name": " Create project without bookmarks", + "name": "issue10", "description": "", - "descriptionFormat": "html", - "time": 1471670834661, - "version": 2, - "responses": [], - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "folder": "a56012d4-9aaf-1df7-4fe7-6cef64d91a22", - "rawModeData": "{\n \"param\": {\n \"type\": \"generic\",\n \"description\": \"test project\",\n \"details\": {},\n \"billingAccountId\": 123,\n \"name\": \"test project1\"\n }\n}" + "item": [ + { + "name": "wrong role", + "request": { + "url": "http://localhost:3000/v4/projects/3/members/5", + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": " {\n \"param\": {\n \"role\": \"wrong\"\n }\n } " + }, + "description": "" + }, + "response": [] + }, + { + "name": "editing project member roles & primary option", + "request": { + "url": "http://localhost:3000/v4/projects/1/members/1", + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": " {\n \"param\": {\n \"role\": \"manager\",\n \"isPrimary\": true\n }\n } " + }, + "description": "" + }, + "response": [] + } + ] }, { - "id": "e798a2e0-09c3-160a-5401-30748e2066b0", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts\nContent-Type: application/json\n", - "url": "http://localhost:3000/v4/projects", - "pathVariables": {}, - "preRequestScript": null, - "method": "POST", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "data": [], - "dataMode": "raw", - "name": " Create project with invalid bookmarks", + "name": "issue5", "description": "", - "descriptionFormat": "html", - "time": 1471670849223, - "version": 2, - "responses": [], - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "folder": "a56012d4-9aaf-1df7-4fe7-6cef64d91a22", - "rawModeData": "{\n \"param\": {\n \"type\": \"generic\",\n \"description\": \"test project\",\n \"details\": {},\n \"bookmarks\":[{\n \"title\":\"title1\",\n \"invalid\":3,\n \"address\":\"address1\"\n },{\n \"title\":\"title2\",\n \"address\":\"address2\"\n }],\n \"billingAccountId\": 123,\n \"name\": \"test project1\"\n }\n}" + "item": [ + { + "name": "launch a project by topcoder managers ", + "request": { + "url": "http://localhost:3000/v4/projects/1", + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \n \"param\":{\n \"name\": \"updatedProject name\",\n \"status\": \"active\"\n }\n}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "launch a project by member", + "request": { + "url": "http://localhost:3000/v4/projects/1", + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6W10sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMSIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.p13tStpp0A1RJjYJ2axSKCTx7lyWIS3kYtCvs8u88WM", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \n \"param\":{\n \"name\": \"updatedProject name\",\n \"status\": \"active\"\n }\n}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "launch a project by copilot", + "request": { + "url": "http://localhost:3000/v4/projects/1", + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJjb3BpbG90Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMiIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.tY_eE9fjtKQ_Hp9XPwmhwMaaTdOYKoR09tdGgvZ8RLw", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \n \"param\":{\n \"name\": \"updatedProject name\",\n \"status\": \"active\"\n }\n}" + }, + "description": "" + }, + "response": [] + } + ] }, { - "id": "f3a6be9f-9c57-5c48-3019-5f3381fcbbb2", - "headers": "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts\nContent-Type: application/json\n", - "url": "http://localhost:3000/v4/projects/2/members/4", - "preRequestScript": null, - "pathVariables": {}, - "method": "PATCH", - "data": [], - "dataMode": "raw", - "version": 2, - "tests": null, - "currentHelper": "normal", - "helperAttributes": {}, - "time": 1470657251192, - "name": "remove copilot from direct project when editing project member role", + "name": "issue8", "description": "", - "collectionId": "170de2c0-0caa-c77b-d761-336f4d4fbd28", - "responses": [], - "rawModeData": " {\n \"param\": {\n \"role\": \"customer\",\n \"isPrimary\": true\n }\n } ", - "folder": "47bf002c-126b-0994-8439-b1108b7d049a" + "item": [ + { + "name": "mock direct projects", + "request": { + "url": "https://localhost:8443/v3/direct/projects", + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": " {\n \"param\": {\n \"role\": \"copilot\",\n \"isPrimary\": true\n }\n } " + }, + "description": "" + }, + "response": [] + }, + { + "name": " Create direct project when a new project is successfully created", + "request": { + "url": "http://localhost:3000/v4/projects", + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"param\": {\n \"type\": \"generic\",\n \"description\": \"test project\",\n \"details\": {},\n \"billingAccountId\": 123,\n \"name\": \"test project1\"\n }\n}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Response error from direct project service", + "request": { + "url": "http://localhost:3000/v4/projects/1/members", + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"param\": {\n \"userId\": 2, \n \"role\": \"copilot\"\n }\n}" + }, + "description": "" + }, + "response": [] + }, + { + "name": " Add co-pilot when a co-pilot is added to a project", + "request": { + "url": "http://localhost:3000/v4/projects/2/members", + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"param\": {\n \"userId\": 2, \n \"role\": \"copilot\"\n }\n}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "remove copilot from direct project when editing project member role", + "request": { + "url": "http://localhost:3000/v4/projects/2/members/4", + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": " {\n \"param\": {\n \"role\": \"customer\",\n \"isPrimary\": true\n }\n } " + }, + "description": "" + }, + "response": [] + }, + { + "name": " Sync billing account id with direct", + "request": { + "url": "http://localhost:3000/v4/projects/2", + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"param\": {\n \"billingAccountId\": 9999, \n \"name\": \"new project name\"\n }\n}" + }, + "description": "" + }, + "response": [] + }, + { + "name": "Delete co-pilot when a co-pilot is removed from a project", + "request": { + "url": "http://localhost:3000/v4/projects/2/members/4", + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "description": "" + }, + { + "key": "Content-Type", + "value": "application/json", + "description": "" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "description": "" + }, + "response": [] + } + ] } ] -} +} \ No newline at end of file diff --git a/src/events/projectMembers/index.js b/src/events/projectMembers/index.js index 5b0e0602..0ffb1750 100644 --- a/src/events/projectMembers/index.js +++ b/src/events/projectMembers/index.js @@ -44,7 +44,54 @@ const projectMemberAddedHandler = (logger, msg, channel) => { }) } else { logger.info('project not associated with a direct project, skipping') - ack(msg) + channel.ack(msg) + } + }) + .catch(err => { + // if the message has been redelivered dont attempt to reprocess it + logger.error('Error retrieving project', err, msg) + channel.nack(msg, false, !msg.fields.redelivered) + }) + } else if (newMember.role === PROJECT_MEMBER_ROLE.MANAGER) { + // if manager is added we have to add the manager to direct project + return models.Project.getDirectProjectId(newMember.projectId) + .then(directProjectId => { + if (directProjectId) { + // retrieve system user token + return util.getSystemUserToken(logger) + .then(token => { + const req = { + id: origRequestId, + log: logger, + headers: { + authorization: `Bearer ${token}` + } + } + return directProject.editProjectPermissions(req, directProjectId, { + permissions: [ + { + userId: newMember.userId, + permissionType: { + permissionTypeId: 3, + name: 'project_full' + }, + studio: false + } + ] + }) + .then(resp => { + logger.debug('added manager to direct') + // acknowledge + channel.ack(msg) + }) + }) + .catch(err => { + logger.error('Error caught while adding manager from direct', err) + channel.nack(msg, false, false) + }) + } else { + logger.info('project not associated with a direct project, skipping') + channel.ack(msg) } }) .catch(err => { @@ -63,7 +110,7 @@ const projectMemberRemovedHandler = (logger, msg, channel) => { const member = JSON.parse(msg.content.toString()) if (member.role === PROJECT_MEMBER_ROLE.COPILOT) { - // Add co-pilot when a co-pilot is added to a project + // Delete co-pilot when a co-pilot is deleted from a project return models.Project.getDirectProjectId(member.projectId) .then(directProjectId => { if (directProjectId) { @@ -100,6 +147,54 @@ const projectMemberRemovedHandler = (logger, msg, channel) => { logger.error('Error retrieving project', err, msg) channel.nack(msg, false, !msg.fields.redelivered) }) + } else if (member.role === PROJECT_MEMBER_ROLE.MANAGER) { + // when a manager is removed from direct project we have to remove manager from direct + return models.Project.getDirectProjectId(member.projectId) + .then(directProjectId => { + if (directProjectId) { + // retrieve system user token + return util.getSystemUserToken(logger) + .then(token => { + const req = { + id: origRequestId, + log: logger, + headers: { + authorization: `Bearer ${token}` + } + } + return directProject.editProjectPermissions(req, directProjectId, { + permissions: [ + { + userId: member.userId, + resourceId: directProjectId, + permissionType: { + permissionTypeId: '', + name: 'project_full' + }, + studio: false + } + ] + }) + .then(resp => { + logger.debug('removed manager from direct') + // acknowledge + channel.ack(msg) + }) + }) + .catch(err => { + logger.error('Error caught while adding manager from direct', err) + channel.nack(msg, false, false) + }) + } else { + logger.info('project not associated with a direct project, skipping') + channel.ack(msg) + } + }) + .catch(err => { + // if the message has been redelivered dont attempt to reprocess it + logger.error('Error retrieving project', err, msg) + channel.nack(msg, false, !msg.fields.redelivered) + }) } else { // nothing to do channel.ack(msg) diff --git a/src/mocks/direct.js b/src/mocks/direct.js index b16697aa..6f8dd5cc 100644 --- a/src/mocks/direct.js +++ b/src/mocks/direct.js @@ -96,6 +96,17 @@ router.route('/v3/direct/projects/:projectId(\\d+)/copilot') } }) +router.route('/v3/direct/projects/:projectId(\\d+)/permissions') + .post((req, res)=>{ + const projectId = req.params.projectId + app.logger.info({body: req.body, projectId: projectId }, 'add permissions to Project') + if(projects[projectId]) { + res.json() + } else { + res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${projectId}`)) + } + }) + app.use(router) // ======================= diff --git a/src/models/project.js b/src/models/project.js index 93bd1fa4..ef728ee8 100644 --- a/src/models/project.js +++ b/src/models/project.js @@ -35,6 +35,7 @@ module.exports = function(sequelize, DataTypes) { }, details: { type: DataTypes.JSON }, challengeEligibility: DataTypes.JSON, + cancelReason: DataTypes.STRING, deletedAt: { type: DataTypes.DATE, allowNull: true }, createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, diff --git a/src/models/projectHistory.js b/src/models/projectHistory.js new file mode 100644 index 00000000..b9edfa22 --- /dev/null +++ b/src/models/projectHistory.js @@ -0,0 +1,22 @@ +'use strict' + +module.exports = function(sequelize, DataTypes) { + var ProjectHistory = sequelize.define('ProjectHistory', { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + projectId: { type: DataTypes.BIGINT, allowNull: false }, + status: { type: DataTypes.STRING, allowNull: false }, + cancelReason: { type: DataTypes.STRING, allowNull: true }, + createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + updatedBy: { type: DataTypes.INTEGER, allowNull: false } + }, { + tableName: 'project_history', + paranoid: false, + timestamps: true, + updatedAt: 'updatedAt', + createdAt: 'createdAt', + indexes: [] + }) + + return ProjectHistory +} diff --git a/src/routes/attachments/create.js b/src/routes/attachments/create.js index 3d30d347..3a9abe19 100644 --- a/src/routes/attachments/create.js +++ b/src/routes/attachments/create.js @@ -25,7 +25,7 @@ const addAttachmentValidations = { filePath: Joi.string().required(), s3Bucket: Joi.string().required(), contentType: Joi.string().required() - }) + }).required() } } diff --git a/src/routes/projectMembers/create.js b/src/routes/projectMembers/create.js index 54880488..94f7c884 100644 --- a/src/routes/projectMembers/create.js +++ b/src/routes/projectMembers/create.js @@ -21,7 +21,7 @@ const addMemberValidations = { userId: Joi.number().required(), isPrimary: Joi.boolean(), role: Joi.any().valid(PROJECT_MEMBER_ROLE.CUSTOMER, PROJECT_MEMBER_ROLE.MANAGER, PROJECT_MEMBER_ROLE.COPILOT).required() - }) + }).required() } } diff --git a/src/routes/projectMembers/delete.js b/src/routes/projectMembers/delete.js index 309f5b99..b1387dd7 100644 --- a/src/routes/projectMembers/delete.js +++ b/src/routes/projectMembers/delete.js @@ -4,7 +4,7 @@ import _ from 'lodash' import models from '../../models' import { middleware as tcMiddleware } from 'tc-core-library-js' -import { EVENT } from '../../constants' +import { EVENT, PROJECT_MEMBER_ROLE } from '../../constants' /** * API to delete a project member. @@ -23,29 +23,65 @@ module.exports = [ return models.ProjectMember.findOne({ where: {id: memberRecordId, projectId: projectId} }) - .then((member) => { - if (!member) { - let err = new Error('Record not found') - err.status = 404 - return Promise.reject(err) - } - return member.destroy({logging: console.log}) - }) - .then(member => { - return member.save() - }) - .then(member => { - // fire event - member = member.get({plain:true}) - req.app.services.pubsub.publish( - EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED, - member, - { correlationId: req.id } - ) - req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED, { req, member }) - res.status(204).json({}) - }) - .catch(err => next(err)) - }) + .then((member) => { + if (!member) { + let err = new Error('Record not found') + err.status = 404 + return Promise.reject(err) + } + return member.destroy({logging: console.log}) + }) + .then(member => { + return member.save() + }) + // if primary co-pilot is removed promote the next co-pilot to primary #43 + .then((member) => { + return new Promise((accept, reject) => { + if (member.role === PROJECT_MEMBER_ROLE.COPILOT && member.isPrimary) { + // find the next copilot + models.ProjectMember.findAll({ + limit: 1, + // return only non-deleted records + paranoid: true, + where: { + projectId: projectId, + role: PROJECT_MEMBER_ROLE.COPILOT + }, + order: [['createdAt', 'ASC']] + }).then((members) => { + if (members && members.length > 0) { + // mark the copilot as primary + const nextMember = members[0] + nextMember.set({ isPrimary: true }) + nextMember.save().then(() => { + accept(member) + }).catch((err) => { + reject(err) + }) + } else { + // no copilot found nothing to do + accept(member) + } + }).catch((err) => { + reject(err) + }) + } else { + // nothing to do + accept(member) + } + }) + }) + }).then((member) => { + // only return the response after transaction is committed + // fire event + member = member.get({plain:true}) + req.app.services.pubsub.publish( + EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED, + member, + { correlationId: req.id } + ) + req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED, { req, member }) + res.status(204).json({}) + }).catch((err) => next(err)) } ] diff --git a/src/routes/projectMembers/delete.spec.js b/src/routes/projectMembers/delete.spec.js index 8fb74636..a0d17c16 100644 --- a/src/routes/projectMembers/delete.spec.js +++ b/src/routes/projectMembers/delete.spec.js @@ -118,7 +118,149 @@ describe('Project members delete', () => { }) }) - it('should return 204 if copilot user is trying to remove a manager', done => { + it('should return 204 if copilot is removed (promote the next copilot to primary)', done => { + models.ProjectMember.bulkCreate([{ + userId: 40051331, + projectId: project1.id, + role: 'copilot', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00' + }, { + userId: 40051333, + projectId: project1.id, + role: 'copilot', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + createdAt: '2016-07-30 00:33:07+00', + updatedAt: '2016-07-30 00:33:07+00' + }, { + userId: 40051335, + projectId: project1.id, + role: 'copilot', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + createdAt: '2016-08-30 00:33:07+00', + updatedAt: '2016-08-30 00:33:07+00' + }]).then(() => { + request(server) + .delete('/v4/projects/' + project1.id + '/members/' + member1.id) + .set({ + 'Authorization': 'Bearer ' + testUtil.jwts.copilot + }) + .expect(204) + .end(function(err) { + if (err) { + return done(err) + } + var removedMember = { + projectId: project1.id, + userId: 40051332, + role: 'copilot', + isPrimary: 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 }}) + .then((members) => { + should.exist(members) + members.length.should.equal(1) + const plain = members[0].get({ plain: true }) + plain.role.should.equal('copilot') + plain.isPrimary.should.equal(true) + plain.userId.should.equal(40051331) + done() + }) + }) + }) + }) + + it('should return 204 if manager is removed from the project', done => { + var mockHttpClient = _.merge(testUtil.mockHttpClient, { + post: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: { } + } + } + }) + }) + var postSpy = sinon.spy(mockHttpClient, 'post') + sandbox.stub(util, 'getHttpClient', () => mockHttpClient) + request(server) + .delete('/v4/projects/' + project1.id + '/members/' + member2.id) + .set({ + 'Authorization': 'Bearer ' + testUtil.jwts.manager + }) + .expect(204) + .end(function(err) { + if (err) { + return done(err) + } + var removedMember = { + projectId: project1.id, + userId: 40051334, + role: 'manager', + isPrimary: true + } + server.services.pubsub.publish.calledWith('project.member.removed', sinon.match(removedMember)).should.be.true + postSpy.should.have.been.calledOnce + done() + }) + }) + + it('should return 204 if manager is removed from the project (without direct project id)', done => { + var mockHttpClient = _.merge(testUtil.mockHttpClient, { + post: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: { } + } + } + }) + }) + var postSpy = sinon.spy(mockHttpClient, 'post') + sandbox.stub(util, 'getHttpClient', () => mockHttpClient) + models.Project.update({ directProjectId: null}, {where: {id: project1.id}}) + .then(() => { + request(server) + .delete('/v4/projects/' + project1.id + '/members/' + member2.id) + .set({ + 'Authorization': 'Bearer ' + testUtil.jwts.manager + }) + .expect(204) + .end(function(err) { + if (err) { + return done(err) + } + var removedMember = { + projectId: project1.id, + userId: 40051334, + role: 'manager', + isPrimary: true + } + server.services.pubsub.publish.calledWith('project.member.removed', sinon.match(removedMember)).should.be.true + postSpy.should.not.have.been.calledOnce + done() + }) + }) + }) + + it('should return 403 if copilot user is trying to remove a manager', done => { request(server) .delete('/v4/projects/' + project1.id + '/members/' + member2.id) .set({ diff --git a/src/routes/projectMembers/update.spec.js b/src/routes/projectMembers/update.spec.js index bb0d6e14..8e2a149a 100644 --- a/src/routes/projectMembers/update.spec.js +++ b/src/routes/projectMembers/update.spec.js @@ -11,7 +11,7 @@ import testUtil from '../../tests/util' const should = chai.should() describe('Project members update', () => { - let project1, member1, member2 + let project1, member1, member2, member3 beforeEach(done => { testUtil.clearDb() .then(() => { @@ -38,7 +38,7 @@ describe('Project members update', () => { createdAt: "2016-06-30 00:33:07+00", updatedAt: "2016-06-30 00:33:07+00" }).then((pm) => { - member1 = pm + member1 = pm.get({ plain: true }) models.ProjectMember.create({ userId: 40051332, projectId: project1.id, @@ -48,10 +48,21 @@ describe('Project members update', () => { updatedBy: 1, createdAt: "2016-06-30 00:33:07+00", updatedAt: "2016-06-30 00:33:07+00" - }).then((pm2) => pm2.reload()) - .then((pm2) => { + }).then((pm2) => { member2 = pm2.get({plain: true}) - done() + models.ProjectMember.create({ + userId: 40051330, + projectId: project1.id, + role: 'copilot', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + createdAt: "2016-06-30 00:33:07+00", + updatedAt: "2016-06-30 00:33:07+00" + }).then((pm3) => { + member3 = pm3.get({ plain: true }) + done() + }) }) }) }) @@ -168,7 +179,6 @@ describe('Project members update', () => { should.exist(resJson) resJson.role.should.equal('customer') resJson.isPrimary.should.be.true - new Date(resJson.updatedAt).valueOf().should.be.equal(new Date(member2.updatedAt).valueOf()) resJson.updatedBy.should.equal(40051332) server.services.pubsub.publish.calledWith('project.member.updated').should.be.true done() @@ -271,6 +281,99 @@ describe('Project members update', () => { }) }) + it('should return 200 if valid user(become manager) and data', done => { + var mockHttpClient = _.merge(testUtil.mockHttpClient, { + post: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: { } + } + } + }) + }) + var postSpy = sinon.spy(mockHttpClient, 'post') + sandbox.stub(util, 'getHttpClient', () => mockHttpClient) + request(server) + .patch('/v4/projects/' + project1.id + '/members/' + member3.id) + .set({ + 'Authorization': 'Bearer ' + testUtil.jwts.manager + }) + .send({ + param: { + "role": "manager", + "isPrimary": false + } + }) + .expect('Content-Type', /json/) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err) + } + const resJson = res.body.result.content + should.exist(resJson) + resJson.role.should.equal('manager') + resJson.isPrimary.should.be.false + resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") + resJson.updatedBy.should.equal(40051334) + postSpy.should.have.been.calledOnce + done() + }) + }) + + it('should return 200 if valid user(become manager) and data (without directProjectId)', done => { + models.Project.update({ directProjectId: null}, {where: {id: project1.id}}) + .then(() => { + var mockHttpClient = _.merge(testUtil.mockHttpClient, { + post: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: { } + } + } + }) + }) + var postSpy = sinon.spy(mockHttpClient, 'post') + sandbox.stub(util, 'getHttpClient', () => mockHttpClient) + request(server) + .patch('/v4/projects/' + project1.id + '/members/' + member3.id) + .set({ + 'Authorization': 'Bearer ' + testUtil.jwts.manager + }) + .send({ + param: { + "role": "manager", + "isPrimary": false + } + }) + .expect('Content-Type', /json/) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err) + } + const resJson = res.body.result.content + should.exist(resJson) + resJson.role.should.equal('manager') + resJson.isPrimary.should.be.false + resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") + resJson.updatedBy.should.equal(40051334) + postSpy.should.not.have.been.calledOnce + done() + }) + }) + }) + it('should return 200 if valid user(become copilot) and data', done => { var mockHttpClient = _.merge(testUtil.mockHttpClient, { post: () => Promise.resolve({ diff --git a/src/routes/projects/create.js b/src/routes/projects/create.js index 6e407516..6aabcfcf 100644 --- a/src/routes/projects/create.js +++ b/src/routes/projects/create.js @@ -42,14 +42,14 @@ const createProjectValdiations = { data: Joi.string().max(300) // TODO - restrict length }).allow(null), // TODO - add more types - type: Joi.any().valid(_.values(PROJECT_TYPE)), + type: Joi.any().valid(_.values(PROJECT_TYPE)).required(), details: Joi.any(), challengeEligibility: Joi.array().items(Joi.object().keys({ role: Joi.string().valid('submitter', 'reviewer', 'copilot'), users: Joi.array().items(Joi.number().positive()), groups: Joi.array().items(Joi.number().positive()) })).allow(null) - }) + }).required() } } diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index 2ce1aa88..84bb5d59 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -111,7 +111,7 @@ module.exports = [ ] if (!util.isValidFilter(filters, ['id', 'status', 'type', 'memberOnly', 'keyword']) || (sort && _.indexOf(sortableProps, sort) < 0)) { - util.handleError('Invalid filters or sort', null, req, next) + return util.handleError('Invalid filters or sort', null, req, next) } // check if user only wants to retrieve projects where he/she is a member const memberOnly = _.get(filters, 'memberOnly', false) diff --git a/src/routes/projects/update.js b/src/routes/projects/update.js index 4f40681c..35d1e494 100644 --- a/src/routes/projects/update.js +++ b/src/routes/projects/update.js @@ -3,7 +3,7 @@ import validate from 'express-validation' import _ from 'lodash' import Joi from 'joi' import models from '../../models' -import { PROJECT_TYPE, PROJECT_STATUS, PROJECT_MEMBER_ROLE, EVENT } from '../../constants' +import { PROJECT_TYPE, PROJECT_STATUS, PROJECT_MEMBER_ROLE, EVENT, USER_ROLE } from '../../constants' import util from '../../util' import directProject from '../../services/directProject' import { middleware as tcMiddleware } from 'tc-core-library-js' @@ -54,7 +54,13 @@ const updateProjectValdiations = { role: Joi.string().valid('submitter', 'reviewer', 'copilot'), users: Joi.array().items(Joi.number().positive()), groups: Joi.array().items(Joi.number().positive()) - })).allow(null) + })).allow(null), + // cancel reason is mandatory when project status is cancelled + cancelReason: Joi.when('status', { + is: PROJECT_STATUS.CANCELLED, + then: Joi.string().required(), + otherwise: Joi.string().optional() + }) }) } } @@ -64,7 +70,6 @@ var validateUpdates = (existingProject, updatedProject) => { var errors = [] switch (existingProject.status) { case PROJECT_STATUS.COMPLETED: - case PROJECT_STATUS.CANCELLED: errors.push(`cannot update a project that is in ${existingProject.status}' state`) break // disabling this check for now. @@ -82,7 +87,7 @@ var validateUpdates = (existingProject, updatedProject) => { module.exports = [ // handles request validations - // validate(updateProjectValdiations), + validate(updateProjectValdiations), permissions('project.edit'), /** * POST projects/ @@ -120,13 +125,15 @@ module.exports = [ }) return Promise.reject(err) } - // Only project manager (user with manager role assigned) should be allowed to transition project status to 'active'. + // Only project manager (user with manager role assigned) or topcoder admin should be allowed + // to transition project status to 'active'. const members = req.context.currentProjectMembers const validRoles = [PROJECT_MEMBER_ROLE.MANAGER, PROJECT_MEMBER_ROLE.MANAGER].map(x => x.toLowerCase()) const matchRole = (role) => _.indexOf(validRoles, role.toLowerCase()) >= 0 - if(updatedProps.status === PROJECT_STATUS.ACTIVE && + if (updatedProps.status === PROJECT_STATUS.ACTIVE && + !util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) && _.isUndefined(_.find(members, (m) => m.userId === req.authUser.userId && matchRole(m.role)))) { - let err = new Error('Only assigned topcoder-managers should be allowed to launch a project') + let err = new Error('Only assigned topcoder-managers or topcoder admins should be allowed to launch a project') err.status = 403 return Promise.reject(err) } @@ -158,6 +165,22 @@ module.exports = [ .then(() => { return project.reload(project.id) }) + // update project history + .then(() => { + return new Promise((accept, reject) => { + // we only want to have project history when project status is updated + if (updatedProps.status && (updatedProps.status !== previousValue.status)) { + models.ProjectHistory.create({ + projectId: project.id, + status: updatedProps.status, + cancelReason: updatedProps.cancelReason, + updatedBy: req.authUser.userId + }).then(() => accept()).catch((err) => reject(err)) + } else { + accept() + } + }) + }) .then(() => { project = project.get({plain: true}) project = _.omit(project, ['deletedAt']) @@ -179,11 +202,10 @@ module.exports = [ // get attachments return util.getProjectAttachments(req, project.id) }) - .then((attachments) => { - project.attachments = attachments - res.json(util.wrapResponse(req.id, project)) - }) - .catch((err) => next(err)) - }) + }).then((attachments) => { + // make sure we only send response after transaction is committed + project.attachments = attachments + res.json(util.wrapResponse(req.id, project)) + }).catch((err) => next(err)) } ] diff --git a/src/routes/projects/update.spec.js b/src/routes/projects/update.spec.js index f49a9f12..70751fed 100644 --- a/src/routes/projects/update.spec.js +++ b/src/routes/projects/update.spec.js @@ -8,6 +8,7 @@ import models from '../../models' import server from '../../app' import testUtil from '../../tests/util' import util from '../../util' +import { PROJECT_STATUS } from '../../constants' var should = chai.should() @@ -97,7 +98,7 @@ describe('Project', () => { request(server) .patch("/v4/projects/" + project1.id) .send(body) - .expect(403,done) + .expect(403, done) }) it('should return 400 if update completed project', done => { @@ -137,7 +138,8 @@ describe('Project', () => { var result = res.body.result result.success.should.be.false result.status.should.equal(403) - result.content.message.should.equal('Only assigned topcoder-managers should be allowed to launch a project') + result.content.message.should.equal('Only assigned topcoder-managers or topcoder admins' + + ' should be allowed to launch a project') done() }) }) @@ -187,6 +189,250 @@ describe('Project', () => { }) }) + it('should return 200 and project history should be updated (status is not set)', done => { + const sbody = _.cloneDeep(body) + // set project status to be updated + sbody.param.status = PROJECT_STATUS.IN_REVIEW + request(server) + .patch("/v4/projects/" + project1.id) + .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .send(sbody) + .expect('Content-Type', /json/) + .expect(200) + .end(function (err,res) { + if (err) { + return done(err) + } + var resJson = res.body.result.content + should.exist(resJson) + resJson.name.should.equal('updatedProject name') + resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") + resJson.updatedBy.should.equal(40051332) + server.services.pubsub.publish.calledWith('project.updated').should.be.true + // validate that project history is updated + models.ProjectHistory.findAll({ + limit: 1, + where: { projectId: project1.id }, + order: [['createdAt', 'DESC']] + }).then((histories) => { + should.exist(histories) + histories.length.should.equal(1) + const history = histories[0].get({ plain: true }) + history.status.should.equal(PROJECT_STATUS.IN_REVIEW) + history.projectId.should.equal(project1.id) + done() + }) + }) + }) + + it('should return 200 and project history should not be updated (status is not updated)', done => { + const sbody = _.cloneDeep(body) + sbody.param.status = PROJECT_STATUS.DRAFT + request(server) + .patch("/v4/projects/" + project1.id) + .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .send(sbody) + .expect('Content-Type', /json/) + .expect(200) + .end(function (err,res) { + if (err) { + return done(err) + } + var resJson = res.body.result.content + should.exist(resJson) + resJson.name.should.equal('updatedProject name') + resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") + resJson.updatedBy.should.equal(40051332) + server.services.pubsub.publish.calledWith('project.updated').should.be.true + // validate that project history is not updated + models.ProjectHistory.findAll({ + where: { projectId: project1.id } + }).then((histories) => { + should.exist(histories) + histories.length.should.equal(0) + done() + }) + }) + }) + + it('should return 422 as cancel reason is mandatory if project status is cancelled', done => { + const sbody = _.cloneDeep(body) + sbody.param.status = PROJECT_STATUS.CANCELLED + request(server) + .patch("/v4/projects/" + project1.id) + .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .send(sbody) + .expect('Content-Type', /json/) + .expect(422) + .end(function (err,res) { + if (err) { + return done(err) + } + const result = res.body.result + result.success.should.be.false + result.status.should.equal(422) + done() + }) + }) + + it('should return 200 and project history should be updated for cancelled project', done => { + const sbody = _.cloneDeep(body) + sbody.param.status = PROJECT_STATUS.CANCELLED + sbody.param.cancelReason = 'price/cost' + request(server) + .patch("/v4/projects/" + project1.id) + .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .send(sbody) + .expect('Content-Type', /json/) + .expect(200) + .end(function (err,res) { + if (err) { + return done(err) + } + var resJson = res.body.result.content + should.exist(resJson) + resJson.name.should.equal('updatedProject name') + resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") + resJson.updatedBy.should.equal(40051332) + server.services.pubsub.publish.calledWith('project.updated').should.be.true + // validate that project history is updated + models.ProjectHistory.findAll({ + where: { projectId: project1.id } + }).then((histories) => { + should.exist(histories) + histories.length.should.equal(1) + const history = histories[0].get({ plain: true }) + history.status.should.equal(PROJECT_STATUS.CANCELLED) + history.projectId.should.equal(project1.id) + history.cancelReason.should.equal('price/cost') + done() + }) + }) + }) + + it('should return 200, manager is allowed to transition project out of cancel status', done => { + models.Project.update({ status: PROJECT_STATUS.CANCELLED}, {where: {id: project1.id}}) + .then(() => { + const sbody = _.cloneDeep(body) + sbody.param.status = PROJECT_STATUS.ACTIVE + request(server) + .patch("/v4/projects/" + project1.id) + .set({"Authorization": "Bearer " + testUtil.jwts.manager}) + .send(sbody) + .expect('Content-Type', /json/) + .expect(200) + .end(function (err,res) { + if (err) { + return done(err) + } + var resJson = res.body.result.content + should.exist(resJson) + resJson.name.should.equal('updatedProject name') + resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") + resJson.updatedBy.should.equal(40051334) + server.services.pubsub.publish.calledWith('project.updated').should.be.true + // validate that project history is updated + models.ProjectHistory.findAll({ + where: { projectId: project1.id } + }).then((histories) => { + should.exist(histories) + histories.length.should.equal(1) + const history = histories[0].get({ plain: true }) + history.status.should.equal(PROJECT_STATUS.ACTIVE) + history.projectId.should.equal(project1.id) + done() + }) + }) + }) + }) + + it('should return 200, admin is allowed to transition project out of cancel status', done => { + models.Project.update({ status: PROJECT_STATUS.CANCELLED}, {where: {id: project1.id}}) + .then(() => { + const sbody = _.cloneDeep(body) + sbody.param.status = PROJECT_STATUS.ACTIVE + request(server) + .patch("/v4/projects/" + project1.id) + .set({"Authorization": "Bearer " + testUtil.jwts.admin}) + .send(sbody) + .expect('Content-Type', /json/) + .expect(200) + .end(function (err,res) { + if (err) { + return done(err) + } + var resJson = res.body.result.content + should.exist(resJson) + resJson.name.should.equal('updatedProject name') + resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") + resJson.updatedBy.should.equal(40051333) + server.services.pubsub.publish.calledWith('project.updated').should.be.true + // validate that project history is updated + models.ProjectHistory.findAll({ + where: { projectId: project1.id } + }).then((histories) => { + should.exist(histories) + histories.length.should.equal(1) + const history = histories[0].get({ plain: true }) + history.status.should.equal(PROJECT_STATUS.ACTIVE) + history.projectId.should.equal(project1.id) + done() + }) + }) + }) + }) + + it('should return 403, copilot is not allowed to transition project out of cancel status', done => { + models.Project.update({ status: PROJECT_STATUS.CANCELLED}, {where: {id: project1.id}}) + .then(() => { + const sbody = _.cloneDeep(body) + sbody.param.status = PROJECT_STATUS.ACTIVE + request(server) + .patch("/v4/projects/" + project1.id) + .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .send(sbody) + .expect('Content-Type', /json/) + .expect(403) + .end(function (err,res) { + if (err) { + return done(err) + } + const result = res.body.result + result.success.should.be.false + result.status.should.equal(403) + done() + }) + }) + }) + + it('should return 200 and project history should not be updated', done => { + request(server) + .patch("/v4/projects/" + project1.id) + .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .send(body) + .expect('Content-Type', /json/) + .expect(200) + .end(function (err,res) { + if (err) { + return done(err) + } + var resJson = res.body.result.content + should.exist(resJson) + resJson.name.should.equal('updatedProject name') + resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") + resJson.updatedBy.should.equal(40051332) + server.services.pubsub.publish.calledWith('project.updated').should.be.true + // validate that project history is not updated + models.ProjectHistory.findAll({ + where: { projectId: project1.id } + }).then((histories) => { + should.exist(histories) + histories.length.should.equal(0) + done() + }) + }) + }) + it('should return 500 if error to sync billing account id', done => { var mockHttpClient = _.merge(testUtil.mockHttpClient, { post: () => Promise.reject(new Error('error message')) diff --git a/src/services/directProject.js b/src/services/directProject.js index 1fb60dd6..9d54dfb8 100644 --- a/src/services/directProject.js +++ b/src/services/directProject.js @@ -64,4 +64,14 @@ export default { */ addBillingAccount: (req, directProjectId, body) => _getHttpClient(req) .post(`/projects/${directProjectId}/billingaccount`, body), + + /** + * Add/remove direct project permissions + * This can be used to add/remove direct project manager + * @param req the request + * @param directProjectId the id of direct project + * @param body the body contains permissions information + */ + editProjectPermissions: (req, directProjectId, body) => _getHttpClient(req) + .post(`/projects/${directProjectId}/permissions`, body), } diff --git a/swagger.yaml b/swagger.yaml index ff0b6b11..e3968d3f 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -26,12 +26,18 @@ paths: tags: - project operationId: findProjects + security: + - Bearer: [] description: Retreive projects that match the filter responses: '422': description: Invalid input schema: $ref: "#/definitions/ErrorModel" + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" '200': description: A list of projects schema: @@ -45,14 +51,15 @@ paths: in: query description: | Url encoded list of Supported filters - - type + - id - status + - type - memberOnly - keyword - name: sort required: false description: | - sort projects by status, name + sort projects by status, name, type, createdAt, updatedAt. Default is createdAt asc in: query type: string post: @@ -83,24 +90,38 @@ paths: /projects/{projectId}: get: description: Retrieve project by id + security: + - Bearer: [] responses: '404': description: Not found schema: $ref: "#/definitions/ErrorModel" + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" '200': description: a project schema: $ref: "#/definitions/ProjectResponse" parameters: - $ref: "#/parameters/projectIdParam" + - name: fields + required: false + type: string + in: query + description: | + Comma separated list of project fields to return. + Can also specify project_members, attachments to get project members and project attachments. + Sub fields of project members and project attachments are also allowed. operationId: getProject patch: operationId: updateProject security: - Bearer: [] - description: Update a project that user has access to. + description: Update a project that user has access to. Managers and admin are able to pull out a project from cancelled state. responses: '403': description: No permission or wrong token @@ -127,9 +148,28 @@ paths: - name: body in: body required: true + description: Only specify those properties that needs to be updated. `cancelReason` is mandatory if status is cancelled schema: $ref: "#/definitions/ProjectBodyParam" + delete: + description: remove an existing project + security: + - Bearer: [] + parameters: + - $ref: "#/parameters/projectIdParam" + responses: + '403': + description: No permission or wrong token + schema: + $ref: "#/definitions/ErrorModel" + '404': + description: If project is not found + schema: + $ref: "#/definitions/ErrorModel" + '204': + description: Project successfully removed + /projects/{projectId}/attachments: post: description: add a new project attachment @@ -148,7 +188,7 @@ paths: schema: $ref: "#/definitions/ErrorModel" '201': - description: Returns the newly created project + description: Returns the newly created project attachment schema: $ref: "#/definitions/NewProjectAttachmentResponse" '422': @@ -166,10 +206,12 @@ paths: - in: path name: id required: true + description: The id of attachment to update type: integer - in: body name: body required: true + description: Specify only those properties that needs to be updated schema: $ref: '#/definitions/NewProjectAttachmentBodyParam' responses: @@ -181,6 +223,10 @@ paths: description: Returns the newly created project schema: $ref: "#/definitions/NewProjectAttachmentResponse" + '404': + description: If project attachment is not found + schema: + $ref: "#/definitions/ErrorModel" '422': description: Invalid input schema: @@ -194,12 +240,17 @@ paths: - in: path name: id required: true + description: The id of attachment to delete type: integer responses: '403': description: No permission or wrong token schema: $ref: "#/definitions/ErrorModel" + '404': + description: If attachment is not found + schema: + $ref: "#/definitions/ErrorModel" '204': description: Attachment successfully removed @@ -231,7 +282,7 @@ paths: /projects/{projectId}/members/{id}: delete: - description: add a new project member + description: Delete a project member security: - Bearer: [] parameters: @@ -303,7 +354,7 @@ parameters: format: int32 limitParam: name: limit - description: "max records to return. Defaults to 10" + description: "max records to return. Defaults to 20" in: query required: false type: integer @@ -369,6 +420,19 @@ definitions: description: type: string description: Project description + billingAccountId: + type: number + format: long + description: the customer billing account id + estimatedPrice: + type: number + format: float + description: The estimated price of the project + terms: + type: array + items: + type: number + format: integer external: type: object description: READ-ONLY, OPTIONAL. Refernce to external task/issue. @@ -382,7 +446,7 @@ definitions: enum: [ "github", "jira", "asana", "other"] data: type: string - description: "250 Char length text blob for customer provided data" + description: "300 Char length text blob for customer provided data" type: type: string description: project type @@ -404,6 +468,8 @@ definitions: properties: campaign: type: string + medium: + type: string source: type: string @@ -420,7 +486,7 @@ definitions: properties: role: type: string - enum: ["submitter", "reviewer", "copilot", "observer"] + enum: ["submitter", "reviewer", "copilot"] users: type: array items: @@ -444,6 +510,33 @@ definitions: description: unique identifier in direct type: integer format: int64 + billingAccountId: + type: integer + format: int64 + description: The customer billing account id + utm: + description: READ-ONLY. Used for tracking + type: object + properties: + campaign: + type: string + medium: + type: string + source: + type: string + estimatedPrice: + type: number + format: float + description: The estimated price of the project + actualPrice: + type: number + format: float + description: The actual price of the project + terms: + type: array + items: + type: number + format: integer name: type: string description: project name @@ -464,20 +557,18 @@ definitions: enum: [ "github", "jira", "asana", "other"] data: type: string - description: "250 Char length text blob for customer provided data" + description: "300 Char length text blob for customer provided data" type: type: string description: project type - enum: ["design", "code", "...from product offerings..."] - tags: - description: Array of tags associated with this project. - type: array - items: - type: integer + enum: ["app_dev", "generic", "visual_prototype", "visual_design"] status: type: string description: current state of the task - enum: ["draft", "active", "paused", "cancelled", "completed"] + enum: ["draft", "in_review", "reviewed", "active", "paused", "cancelled", "completed"] + cancelReason: + type: string + description: If a project is cancelled, define the reason of cancellation challengeEligibility: description: List of eligibility criteria (one entry per role) type: array @@ -557,6 +648,9 @@ definitions: type: number format: int64 description: user identifier + isPrimary: + type: boolean + description: Flag to indicate this member is primary for specified role role: type: string description: member role on specified project @@ -593,11 +687,16 @@ definitions: type: object required: - filePath + - s3Bucket + - title - contentType properties: filePath: type: string description: path where file is stored + s3Bucket: + type: string + description: The s3 bucket of attachment contentType: type: string description: Uploaded file content type @@ -607,6 +706,13 @@ definitions: description: type: string description: Optional description for the attached file. + category: + type: string + description: Category of attachment + size: + type: number + format: float + description: The size of attachment NewProjectAttachmentBodyParam: type: object @@ -615,7 +721,7 @@ definitions: $ref: "#/definitions/NewProjectAttachment" NewProjectAttachmentResponse: - title: Project member object response + title: Project attachment object response type: object properties: id: @@ -641,9 +747,13 @@ definitions: id: type: number description: unique id for the attachment - filePath: + size: + type: number + format: float + description: The size of attachment + category: type: string - description: path where file is stored + description: The category of attachment contentType: type: string description: Uploaded file content type @@ -686,6 +796,9 @@ definitions: type: number format: int64 description: user identifier + isPrimary: + type: boolean + description: Flag to indicate this member is primary for specified role projectId: type: number format: int64 @@ -824,4 +937,4 @@ definitions: content: type: array items: - $ref: "#/definitions/Project" + $ref: "#/definitions/Project" \ No newline at end of file diff --git a/sync.js b/sync.js new file mode 100644 index 00000000..440e9023 --- /dev/null +++ b/sync.js @@ -0,0 +1,20 @@ +'use strict' + +/** + * Sync the database models to db tables. + */ + +/** + * Make sure we are in development mode + * @type {String} + */ +process.env.NODE_ENV = 'development' + +require('./dist/models').default.sequelize.sync({ force: true }) + .then(() => { + console.log('Database synced successfully') + process.exit() + }).catch((err) => { + console.error('Error syncing database', err) + process.exit(1) + }) From 87f052777553ab7cbd97e958d3cb04736f4af674 Mon Sep 17 00:00:00 2001 From: Parth Shah Date: Wed, 1 Mar 2017 16:07:03 -0800 Subject: [PATCH 2/8] fixing lint errors --- .eslintrc | 25 + README.md | 10 +- sync.js => migrations/sync.js | 2 +- package.json | 39 +- postman.json | 236 ++++--- src/app.js | 81 ++- src/constants.js | 44 +- src/events/analytics.js | 106 ++-- src/events/index.js | 24 +- src/events/projectMembers/index.js | 279 +++++---- src/events/projects/index.js | 87 ++- src/index.js | 88 +-- src/middlewares/checkRole.js | 13 +- src/mocks/addBillingAccount.js | 46 +- src/mocks/addCopilot.js | 46 +- src/mocks/createProject.js | 46 +- src/mocks/direct.js | 192 +++--- src/models/index.js | 68 +- src/models/project.js | 318 +++++----- src/models/projectAttachment.js | 28 +- src/models/projectHistory.js | 16 +- src/models/projectMember.js | 50 +- src/permissions/index.js | 28 +- src/permissions/project.delete.js | 38 +- src/permissions/project.edit.js | 34 +- src/permissions/project.view.js | 49 +- src/permissions/projectMember.delete.js | 40 +- src/routes/attachments/create.js | 127 ++-- src/routes/attachments/create.spec.js | 183 +++--- src/routes/attachments/delete.js | 49 +- src/routes/attachments/delete.spec.js | 130 ++-- src/routes/attachments/update.js | 72 ++- src/routes/attachments/update.spec.js | 119 ++-- src/routes/index.js | 76 +-- src/routes/projectMembers/create.js | 80 ++- src/routes/projectMembers/create.spec.js | 215 +++---- src/routes/projectMembers/delete.js | 115 ++-- src/routes/projectMembers/delete.spec.js | 295 ++++----- src/routes/projectMembers/update.js | 114 ++-- src/routes/projectMembers/update.spec.js | 690 ++++++++++---------- src/routes/projects/create.js | 108 ++-- src/routes/projects/create.spec.js | 180 +++--- src/routes/projects/delete.js | 49 +- src/routes/projects/delete.spec.js | 103 +-- src/routes/projects/get.js | 64 +- src/routes/projects/get.spec.js | 223 ++++--- src/routes/projects/list.js | 162 +++-- src/routes/projects/list.spec.js | 598 +++++++++--------- src/routes/projects/update.js | 312 +++++----- src/routes/projects/update.spec.js | 762 +++++++++++------------ src/services/directProject.js | 80 +-- src/services/fileService.js | 40 +- src/services/index.js | 22 +- src/services/rabbitmq.js | 221 +++---- src/services/topicService.js | 67 +- src/tests/seed.js | 216 +++---- src/tests/serviceMocks.js | 20 +- src/tests/util.js | 16 +- src/util.js | 220 ++++--- 59 files changed, 3879 insertions(+), 3882 deletions(-) create mode 100644 .eslintrc rename sync.js => migrations/sync.js (90%) diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..348ce0f0 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,25 @@ +{ + "extends": "airbnb", + "parser": "babel-eslint", + "env": { + "browser": false, + "node": true, + "es6": true, + "mocha": true + }, + "rules": { + "valid-jsdoc": ["error", { + "requireReturn": true, + "requireReturnType": true, + "requireParamDescription": true, + "requireReturnDescription": true + }], + "require-jsdoc": ["error", { + "require": { + "FunctionDeclaration": true, + "MethodDefinition": true, + "ClassDeclaration": true + } + }] + } +} diff --git a/README.md b/README.md index 7d06f1dd..06d23445 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Microservice to manage CRUD operations for all things Projects. * Nodejs 6.9.4 - consider using [nvm](https://github.com/creationix/nvm) or equivalent to manage your node version * Install [libpg](https://www.npmjs.com/package/pg-native) * Install node dependencies -`npm install | ./node_modules/.bin/bunyan` +`npm install` * Start local services ```~/Projects/tc-projects-service @@ -21,12 +21,9 @@ Copy config/sample.local.js as config/local.js, update the properties and accord Once you start your PostgreSQL database through docker, it will create a projectsDB. *To create tables - note this will drop tables if they already exist* ``` -npm run -s build -> NODE_ENV=development node -e "require('./dist/models').default.sequelize.sync({force: true}).then((res)=> console.log('Success: ', res)).catch((err)=> console.log('Failed: ', err));" +NODE_ENV=local npm run sync ``` -Other simple approach to create tables is to run `npm run sync` from root of project. This additional script is added to make the above task simpler. - #### Redis Docker compose command will start a local redis instance as well. You should be able to connect to this instance using url `$(docker-machine ip):6379` @@ -55,6 +52,3 @@ You can paste **swagger.yaml** to [swagger editor](http://editor.swagger.io/) o #### Deploying without docker If you don't want to use docker to deploy to localhost. You can simply run `npm run start` from root of project. This should start the server on default port `3000`. - -### Deployment -Using awsebcli - http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/eb-cli3-install.html diff --git a/sync.js b/migrations/sync.js similarity index 90% rename from sync.js rename to migrations/sync.js index 440e9023..5b6f79c1 100644 --- a/sync.js +++ b/migrations/sync.js @@ -8,7 +8,7 @@ * Make sure we are in development mode * @type {String} */ -process.env.NODE_ENV = 'development' +// process.env.NODE_ENV = 'development' require('./dist/models').default.sequelize.sync({ force: true }) .then(() => { diff --git a/package.json b/package.json index 76477cd9..62f41d39 100644 --- a/package.json +++ b/package.json @@ -7,16 +7,16 @@ "node": ">=6.9" }, "scripts": { - "test": "NODE_ENV=test ./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- --compilers js:babel-core/register $(find src -path '*spec.js*')", - "live-test": "NODE_ENV=test ./node_modules/.bin/mocha -w --compilers js:babel-core/register $(find src -path '*spec.js*')", - "dev": "NODE_ENV=local PORT=8001 nodemon -w src --exec \"babel-node src --presets es2015\" | ./node_modules/.bin/bunyan", "build": "babel src -d dist --presets es2015", - "start": "node dist", + "sync": "node migrations/sync.js", + "lint": "./node_modules/.bin/eslint src", "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", + "test": "NODE_ENV=test ./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- --compilers js:babel-core/register $(find src -path '*spec.js*')", + "test:watch": "NODE_ENV=test ./node_modules/.bin/mocha -w --compilers js:babel-core/register $(find src -path '*spec.js*')", "seed": "babel-node src/tests/seed.js --presets es2015", - "direct": "babel-node src/mocks/direct.js --presets es2015", - "sync": "node sync.js", - "lint": "./node_modules/.bin/eslint src" + "direct": "babel-node src/mocks/direct.js --presets es2015" }, "repository": { "type": "git", @@ -28,25 +28,6 @@ "url": "https://github.com/appirio-tech/tc-projects-service/issues" }, "homepage": "https://github.com/appirio-tech/tc-projects-service#readme", - "eslintConfig": { - "plugins": [], - "extends": "eslint:recommended", - "parserOptions": { - "ecmaVersion": 7, - "sourceType": "module" - }, - "env": { - "node": true, - "mocha": true - }, - "globals": { - "Promise": true - }, - "rules": { - "no-console": 0, - "no-unused-vars": 1 - } - }, "dependencies": { "amqplib": "^0.5.1", "analytics-node": "^2.1.1", @@ -71,12 +52,16 @@ "devDependencies": { "babel-cli": "^6.9.0", "babel-core": "^6.11.4", + "babel-eslint": "^7.1.1", "babel-plugin-add-module-exports": "^0.2.1", "babel-preset-es2015": "^6.9.0", "bunyan": "^1.8.1", "chai": "^3.5.0", "eslint": "^3.2.2", - "eslint-plugin-import": "^1.12.0", + "eslint-config-airbnb": "^14.1.0", + "eslint-plugin-import": "^2.2.0", + "eslint-plugin-jsx-a11y": "^4.0.0", + "eslint-plugin-react": "^6.10.0", "istanbul": "^1.0.0-alpha.2", "mocha": "^2.5.3", "nodemon": "^1.9.1", diff --git a/postman.json b/postman.json index ab4617db..dc38c0d4 100644 --- a/postman.json +++ b/postman.json @@ -2,7 +2,7 @@ "variables": [], "info": { "name": "tc-project-service ", - "_postman_id": "85538c36-6562-9d05-bec5-b04dc8f33ef9", + "_postman_id": "c83bc278-c7e5-9c70-4a25-3e07a4311fc4", "description": "", "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" }, @@ -14,12 +14,12 @@ { "name": "Create project member with no payload", "request": { - "url": "http://localhost:3000/v4/projects/1/members", + "url": "{{api-url}}/v4/projects/1/members", "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -39,12 +39,12 @@ { "name": "Create project member with invalid userId", "request": { - "url": "http://localhost:3000/v4/projects/1/members", + "url": "{{api-url}}/v4/projects/1/members", "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -64,12 +64,12 @@ { "name": "Create project member with valid values", "request": { - "url": "http://localhost:3000/v4/projects/1/members", + "url": "{{api-url}}/v4/projects/1/members", "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -89,12 +89,12 @@ { "name": "Create project member, if user already registered", "request": { - "url": "http://localhost:3000/v4/projects/1/members", + "url": "{{api-url}}/v4/projects/1/members", "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -114,12 +114,12 @@ { "name": "Create project manager with valid values", "request": { - "url": "http://localhost:3000/v4/projects/1/members", + "url": "{{api-url}}/v4/projects/1/members", "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -139,12 +139,12 @@ { "name": "Update project member", "request": { - "url": "http://localhost:3000/v4/projects/1/members/1", + "url": "{{api-url}}/v4/projects/1/members/1", "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -164,12 +164,12 @@ { "name": "Delete project member", "request": { - "url": "http://localhost:3000/v4/projects/2/members/2", + "url": "{{api-url}}/v4/projects/2/members/2", "method": "DELETE", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -195,12 +195,12 @@ { "name": "Create project without payload", "request": { - "url": "http://localhost:3000/v4/projects", + "url": "{{api-url}}/v4/projects", "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNCIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.amNzJ6PknW8V1ZDrVxJAd-SxcyfYhTf80IBc1sH-vF8", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -220,12 +220,12 @@ { "name": "Create project without valid name", "request": { - "url": "http://localhost:3000/v4/projects", + "url": "{{api-url}}/v4/projects", "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNCIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.amNzJ6PknW8V1ZDrVxJAd-SxcyfYhTf80IBc1sH-vF8", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -245,12 +245,12 @@ { "name": "Create project with valid values", "request": { - "url": "http://localhost:3000/v4/projects", + "url": "{{api-url}}/v4/projects", "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNCIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.amNzJ6PknW8V1ZDrVxJAd-SxcyfYhTf80IBc1sH-vF8", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -270,16 +270,19 @@ { "name": "Get project by id", "request": { - "url": "http://localhost:3000/v4/projects/1", + "url": "{{api-url}}/v4/projects/1", "method": "GET", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "value": "Bearer {{jwt-token}}", "description": "" } ], - "body": {}, + "body": { + "mode": "raw", + "raw": "" + }, "description": "Get a project by id. project members and attachments should also be returned." }, "response": [] @@ -287,16 +290,19 @@ { "name": "Get project by id and request specific fields", "request": { - "url": "http://localhost:3000/v4/projects/1?fields=id,name,description,members.id,members.projectId", + "url": "{{api-url}}/v4/projects/1?fields=id,name,description,members.id,members.projectId", "method": "GET", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "value": "Bearer {{jwt-token}}", "description": "" } ], - "body": {}, + "body": { + "mode": "raw", + "raw": "" + }, "description": "Get a project by id. project members and attachments should also be returned. Only those fields which are specified should be returned." }, "response": [] @@ -304,16 +310,19 @@ { "name": "List projects", "request": { - "url": "http://localhost:3000/v4/projects", + "url": "{{api-url}}/v4/projects", "method": "GET", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "value": "Bearer {{jwt-token}}", "description": "" } ], - "body": {}, + "body": { + "mode": "raw", + "raw": "" + }, "description": "List all the project with no filter. Default sort and limits are applied." }, "response": [] @@ -321,16 +330,19 @@ { "name": "List projects with limit and offset", "request": { - "url": "http://localhost:3000/v4/projects?limit=1&offset=1", + "url": "{{api-url}}/v4/projects?limit=1&offset=1", "method": "GET", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "value": "Bearer {{jwt-token}}", "description": "" } ], - "body": {}, + "body": { + "mode": "raw", + "raw": "" + }, "description": "List all the project with no filter. Limit of 1 and offset of 1 is applied" }, "response": [] @@ -338,16 +350,19 @@ { "name": "List projects with filters applied", "request": { - "url": "http://localhost:3000/v4/projects?filter=type%3Dgeneric", + "url": "{{api-url}}/v4/projects?filter=type%3Dgeneric", "method": "GET", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "value": "Bearer {{jwt-token}}", "description": "" } ], - "body": {}, + "body": { + "mode": "raw", + "raw": "" + }, "description": "List all the project with filters applied. The filter string should be url encoded. Default limit and offset is applicable" }, "response": [] @@ -355,16 +370,19 @@ { "name": "List projects with sort applied", "request": { - "url": "http://localhost:3000/v4/projects?sort=type%20desc", + "url": "{{api-url}}/v4/projects?sort=type%20desc", "method": "GET", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "value": "Bearer {{jwt-token}}", "description": "" } ], - "body": {}, + "body": { + "mode": "raw", + "raw": "" + }, "description": "List all the project with custom sort and no filter. Default sort and limits are applied. The sort string has to be url encoded. Sort is of type `key asc|desc`" }, "response": [] @@ -372,16 +390,19 @@ { "name": "List projects and return specific fields", "request": { - "url": "http://localhost:3000/v4/projects?fields=id,name,description", + "url": "{{api-url}}/v4/projects?fields=id,name,description", "method": "GET", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "value": "Bearer {{jwt-token}}", "description": "" } ], - "body": {}, + "body": { + "mode": "raw", + "raw": "" + }, "description": "List all the project with no filter. Default sort and limits are applied. The fields to return is specified as comma separated list. Only those fields should be returned." }, "response": [] @@ -389,16 +410,19 @@ { "name": "DELETE project by id", "request": { - "url": "http://localhost:3000/v4/projects/3", + "url": "{{api-url}}/v4/projects/3", "method": "DELETE", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20iLCJoYW5kbGUiOiJ0ZXN0MSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzMiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoidGVzdEB0b3Bjb2Rlci5jb20iLCJqdGkiOiJiMzNiNzdjZC1iNTJlLTQwZmUtODM3ZS1iZWI4ZTBhZTZhNGEifQ.yG7pd9_UUFGo6XFH7-H6Wd5wWzRivkeZTCyet7IXFso", + "value": "Bearer {{jwt-token}}", "description": "" } ], - "body": {}, + "body": { + "mode": "raw", + "raw": "" + }, "description": "Delete a project by id" }, "response": [] @@ -406,12 +430,12 @@ { "name": "Update project", "request": { - "url": "http://localhost:3000/v4/projects/1", + "url": "{{api-url}}/v4/projects/1", "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNCIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.amNzJ6PknW8V1ZDrVxJAd-SxcyfYhTf80IBc1sH-vF8", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -431,12 +455,12 @@ { "name": "Update project 403", "request": { - "url": "http://localhost:3000/v4/projects/2", + "url": "{{api-url}}/v4/projects/2", "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMSIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.IgPq3dcPH-WJXQAytjF_4fJbx3gtsee1U3vmqGIGoUA", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -456,12 +480,12 @@ { "name": "Update project 404", "request": { - "url": "http://localhost:3000/v4/projects/10", + "url": "{{api-url}}/v4/projects/10", "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNCIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.amNzJ6PknW8V1ZDrVxJAd-SxcyfYhTf80IBc1sH-vF8", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -481,12 +505,12 @@ { "name": "Update project status to cancelled", "request": { - "url": "http://localhost:3000/v4/projects/1", + "url": "{{api-url}}/v4/projects/1", "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNCIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.amNzJ6PknW8V1ZDrVxJAd-SxcyfYhTf80IBc1sH-vF8", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -506,12 +530,12 @@ { "name": "Update project status to cancelled with cancelReason", "request": { - "url": "http://localhost:3000/v4/projects/1", + "url": "{{api-url}}/v4/projects/1", "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNCIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.amNzJ6PknW8V1ZDrVxJAd-SxcyfYhTf80IBc1sH-vF8", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -531,12 +555,12 @@ { "name": "Move project out of cancel state.", "request": { - "url": "http://localhost:3000/v4/projects/1", + "url": "{{api-url}}/v4/projects/1", "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNCIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.amNzJ6PknW8V1ZDrVxJAd-SxcyfYhTf80IBc1sH-vF8", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -556,12 +580,12 @@ { "name": "Move project out of cancel state 403", "request": { - "url": "http://localhost:3000/v4/projects/1", + "url": "{{api-url}}/v4/projects/1", "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMSIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.IgPq3dcPH-WJXQAytjF_4fJbx3gtsee1U3vmqGIGoUA", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -587,12 +611,12 @@ { "name": " Create project without bookmarks", "request": { - "url": "http://localhost:3000/v4/projects", + "url": "{{api-url}}/v4/projects", "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -612,12 +636,12 @@ { "name": " Create project with valid bookmarks", "request": { - "url": "http://localhost:3000/v4/projects", + "url": "{{api-url}}/v4/projects", "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -637,12 +661,12 @@ { "name": " Create project with invalid bookmarks", "request": { - "url": "http://localhost:3000/v4/projects", + "url": "{{api-url}}/v4/projects", "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -662,12 +686,12 @@ { "name": "get project", "request": { - "url": "http://localhost:3000/v4/projects/2", + "url": "{{api-url}}/v4/projects/2", "method": "GET", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -687,12 +711,12 @@ { "name": "Update project with bookmarks", "request": { - "url": "http://localhost:3000/v4/projects/2", + "url": "{{api-url}}/v4/projects/2", "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -712,12 +736,12 @@ { "name": "Delete project all bookmarks null", "request": { - "url": "http://localhost:3000/v4/projects/2", + "url": "{{api-url}}/v4/projects/2", "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -737,12 +761,12 @@ { "name": "Update project with invalid bookmarks", "request": { - "url": "http://localhost:3000/v4/projects/2", + "url": "{{api-url}}/v4/projects/2", "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -762,16 +786,19 @@ { "name": "get projects with admin token", "request": { - "url": "http://localhost:3000/v4/projects", + "url": "{{api-url}}/v4/projects", "method": "GET", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "value": "Bearer {{jwt-token}}", "description": "" } ], - "body": {}, + "body": { + "mode": "raw", + "raw": "" + }, "description": "" }, "response": [] @@ -785,16 +812,19 @@ { "name": "get projects with copilot token", "request": { - "url": "http://localhost:3000/v4/projects", + "url": "{{api-url}}/v4/projects", "method": "GET", "header": [ { "key": "Authorization", - "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBjb3BpbG90Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOjQwMDUxMzMyLCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwiaWF0IjoxNDcwNjIwMDQ0fQ.HUhRM1G6jwBt8Kv2slPYndhVUdYXXBAH174fPzqpFME", + "value": "Bearer {{jwt-token}}", "description": "" } ], - "body": {}, + "body": { + "mode": "raw", + "raw": "" + }, "description": "" }, "response": [] @@ -808,12 +838,12 @@ { "name": "wrong role", "request": { - "url": "http://localhost:3000/v4/projects/3/members/5", + "url": "{{api-url}}/v4/projects/3/members/5", "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -833,12 +863,12 @@ { "name": "editing project member roles & primary option", "request": { - "url": "http://localhost:3000/v4/projects/1/members/1", + "url": "{{api-url}}/v4/projects/1/members/1", "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -864,12 +894,12 @@ { "name": "launch a project by topcoder managers ", "request": { - "url": "http://localhost:3000/v4/projects/1", + "url": "{{api-url}}/v4/projects/1", "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -889,12 +919,12 @@ { "name": "launch a project by member", "request": { - "url": "http://localhost:3000/v4/projects/1", + "url": "{{api-url}}/v4/projects/1", "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6W10sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMSIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.p13tStpp0A1RJjYJ2axSKCTx7lyWIS3kYtCvs8u88WM", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -914,12 +944,12 @@ { "name": "launch a project by copilot", "request": { - "url": "http://localhost:3000/v4/projects/1", + "url": "{{api-url}}/v4/projects/1", "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJjb3BpbG90Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMiIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.tY_eE9fjtKQ_Hp9XPwmhwMaaTdOYKoR09tdGgvZ8RLw", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -950,7 +980,7 @@ "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -970,12 +1000,12 @@ { "name": " Create direct project when a new project is successfully created", "request": { - "url": "http://localhost:3000/v4/projects", + "url": "{{api-url}}/v4/projects", "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -995,12 +1025,12 @@ { "name": "Response error from direct project service", "request": { - "url": "http://localhost:3000/v4/projects/1/members", + "url": "{{api-url}}/v4/projects/1/members", "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -1020,12 +1050,12 @@ { "name": " Add co-pilot when a co-pilot is added to a project", "request": { - "url": "http://localhost:3000/v4/projects/2/members", + "url": "{{api-url}}/v4/projects/2/members", "method": "POST", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -1045,12 +1075,12 @@ { "name": "remove copilot from direct project when editing project member role", "request": { - "url": "http://localhost:3000/v4/projects/2/members/4", + "url": "{{api-url}}/v4/projects/2/members/4", "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -1070,12 +1100,12 @@ { "name": " Sync billing account id with direct", "request": { - "url": "http://localhost:3000/v4/projects/2", + "url": "{{api-url}}/v4/projects/2", "method": "PATCH", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -1095,12 +1125,12 @@ { "name": "Delete co-pilot when a co-pilot is removed from a project", "request": { - "url": "http://localhost:3000/v4/projects/2/members/4", + "url": "{{api-url}}/v4/projects/2/members/4", "method": "DELETE", "header": [ { "key": "Authorization", - "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", + "value": "Bearer {{jwt-token}}", "description": "" }, { @@ -1120,4 +1150,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/app.js b/src/app.js index 96dc6f09..5d6738d2 100644 --- a/src/app.js +++ b/src/app.js @@ -1,15 +1,15 @@ -'use strict' - -import express from 'express' -import _ from 'lodash' -import bodyParser from 'body-parser' -import config from 'config' -import router from './routes' -import permissions from './permissions' -import coreLib from 'tc-core-library-js' -import expressRequestId from 'express-request-id' -import models from './models' -var app = express() +import express 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 './routes'; +import permissions from './permissions'; +import models from './models'; +import analytics from './events/analytics'; + +const app = express(); // ======================= // configuration ========= @@ -17,74 +17,73 @@ var app = express() // instantiate core library // var coreLib = require('tc-core-library-js') - // use body parser so we can get info from POST and/or URL parameters app.use(bodyParser.urlencoded({ - extended: false -})) -app.use(bodyParser.json()) + extended: false, +})); +app.use(bodyParser.json()); // add request Id -var addRequestId = expressRequestId() -app.use(addRequestId) +const addRequestId = expressRequestId(); +app.use(addRequestId); // ======================= // Loger ========= // ======================= -let appName = 'tc-projects-service' +let appName = 'tc-projects-service'; switch (process.env.NODE_ENV.toLowerCase()) { case 'development': - appName += "-dev" - break + appName += '-dev'; + break; case 'qa': - appName += "-qa" - break + appName += '-qa'; + break; case 'production': default: - appName += '-prod' - break + appName += '-prod'; + break; } // init logger -var logger = coreLib.logger({ +const logger = coreLib.logger({ name: appName, - level: _.get(config, "logLevel", 'debug').toLowerCase(), + level: _.get(config, 'logLevel', 'debug').toLowerCase(), captureLogs: config.get('captureLogs'), - logentriesToken: _.get(config, 'logentriesToken', null) -}) -app.use(coreLib.middleware.logger(null, logger)) -app.logger = logger + logentriesToken: _.get(config, 'logentriesToken', null), +}); +app.use(coreLib.middleware.logger(null, logger)); +app.logger = logger; // ======================= // Database ========= // ======================= -logger.info('Registering models ... ', !!models) +logger.info('Registering models ... ', !!models); // ======================= // Analytics // ======================= -const analyticsKey = config.get('analyticsKey') -if (!_.isEmpty(analyticsKey)) - require('./events/analytics')(analyticsKey, app, logger) +const analyticsKey = config.get('analyticsKey'); +if (!_.isEmpty(analyticsKey)) { + analytics(analyticsKey, app, logger); +} // ======================== // Permissions // ======================== // require('app/permissions')() -permissions() +permissions(); // ======================== // Routes // ======================== -app.use(router) -app.routerRef = router +app.use(router); +app.routerRef = router; // ======================= // Initialize services // ======================= -require('./services')(app, logger) - +require('./services')(app, logger); -module.exports = app +module.exports = app; diff --git a/src/constants.js b/src/constants.js index 02fd57ce..85e2d357 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,32 +1,32 @@ export const PROJECT_TYPE = { - APP_DEV : 'app_dev', - GENERIC : 'generic', - VISUAL_PROTOTYPE : 'visual_prototype', - VISUAL_DESIGN : 'visual_design' -} + APP_DEV: 'app_dev', + GENERIC: 'generic', + VISUAL_PROTOTYPE: 'visual_prototype', + VISUAL_DESIGN: 'visual_design', +}; export const PROJECT_STATUS = { - DRAFT : 'draft', - IN_REVIEW : 'in_review', - REVIEWED : 'reviewed', - ACTIVE : 'active', - COMPLETED : 'completed', - PAUSED : 'paused', - CANCELLED : 'cancelled' -} + DRAFT: 'draft', + IN_REVIEW: 'in_review', + REVIEWED: 'reviewed', + ACTIVE: 'active', + COMPLETED: 'completed', + PAUSED: 'paused', + CANCELLED: 'cancelled', +}; export const PROJECT_MEMBER_ROLE = { - MANAGER : 'manager', - CUSTOMER : 'customer', - COPILOT : 'copilot' -} + MANAGER: 'manager', + CUSTOMER: 'customer', + COPILOT: 'copilot', +}; export const USER_ROLE = { TOPCODER_ADMIN: 'administrator', MANAGER: 'Connect Manager', - COPILOT: 'Connect Copilot' -} + COPILOT: 'Connect Copilot', +}; export const EVENT = { @@ -37,6 +37,6 @@ export const EVENT = { PROJECT_DRAFT_CREATED: 'project.draft-created', PROJECT_UPDATED: 'project.updated', - PROJECT_DELETED: 'project.deleted' - } -} + PROJECT_DELETED: 'project.deleted', + }, +}; diff --git a/src/events/analytics.js b/src/events/analytics.js index 10c43dfe..c85901a2 100644 --- a/src/events/analytics.js +++ b/src/events/analytics.js @@ -1,22 +1,22 @@ -import Analytics from 'analytics-node' +import Analytics from 'analytics-node'; +import _ from 'lodash'; +import 'config'; import { EVENT, - PROJECT_STATUS -} from '../constants' -import 'config' -import _ from 'lodash' + PROJECT_STATUS, +} from '../constants'; -const PROJECT_CREATED = 'Project Created' -const PROJECT_SUBMITTED = 'Project Submitted' -const PROJECT_REVIEWED = 'Project Reviewed' -const PROJECT_ACTIVATED = 'Project Acvtivated' -const PROJECT_COMPLETED = 'Project Completed' -const PROJECT_CANCELED = 'Project Canceled' -const PROJECT_DELETED = 'Project Deleted' -const PROJECT_PAUSED = 'Project Paused' -const PROJECT_SPEC_UPDATED = 'Project Spec Updated' -const PROJECT_MEMBER_ADDED = 'Project Member Added' -const PROJECT_MEMBER_REMOVED = 'Project Member Removed' +const PROJECT_CREATED = 'Project Created'; +const PROJECT_SUBMITTED = 'Project Submitted'; +const PROJECT_REVIEWED = 'Project Reviewed'; +const PROJECT_ACTIVATED = 'Project Acvtivated'; +const PROJECT_COMPLETED = 'Project Completed'; +const PROJECT_CANCELED = 'Project Canceled'; +// const PROJECT_DELETED = 'Project Deleted'; +const PROJECT_PAUSED = 'Project Paused'; +const PROJECT_SPEC_UPDATED = 'Project Spec Updated'; +const PROJECT_MEMBER_ADDED = 'Project Member Added'; +const PROJECT_MEMBER_REMOVED = 'Project Member Removed'; const mapEventTypes = { [PROJECT_STATUS.DRAFT]: PROJECT_CREATED, @@ -25,77 +25,73 @@ const mapEventTypes = { [PROJECT_STATUS.ACTIVE]: PROJECT_ACTIVATED, [PROJECT_STATUS.COMPLETED]: PROJECT_COMPLETED, [PROJECT_STATUS.CANCELLED]: PROJECT_CANCELED, - [PROJECT_STATUS.PAUSED]: PROJECT_PAUSED -} - -module.exports = (analyticsKey, app, logger) => { + [PROJECT_STATUS.PAUSED]: PROJECT_PAUSED, +}; +module.exports = (analyticsKey, app) => { // Initialize analytics - const analytics = analyticsKey ? new Analytics(analyticsKey) : null + const analytics = analyticsKey ? new Analytics(analyticsKey) : null; /** * Invokes analytics track and attaches additional props * @param {Object} req Http request * @param {String} eventType analytics event - * @param {Object} props properties to track + * @param {Object} properties properties to track + * @returns {undefined} */ - const track = (req, eventType, props) => { + const track = (req, eventType, properties) => { if (analytics) { - const userId = _.get(req, 'authUser.userId', 'system').toString() + const userId = _.get(req, 'authUser.userId', 'system').toString(); // add referrer ? - const _props = _.assign({}, { referrer: req.get('Referer') }, props) + const props = _.assign({}, { referrer: req.get('Referer') }, properties); const data = { userId, event: eventType, - properties: _props - } - req.log.debug('Analytics tracking:', data) - analytics.track(data) + properties: props, + }; + req.log.debug('Analytics tracking:', data); + analytics.track(data); } + }; - } - - app.on(EVENT.ROUTING_KEY.PROJECT_DRAFT_CREATED, ({req, project}) => { + app.on(EVENT.ROUTING_KEY.PROJECT_DRAFT_CREATED, ({ req, project }) => { track(req, PROJECT_CREATED, { id: project.id, name: project.name, type: project.type, - products: _.get(project, 'products', []) - }) - }) + products: _.get(project, 'products', []), + }); + }); - app.on(EVENT.ROUTING_KEY.PROJECT_DELETED, ({req, id}) => { - track(req, PROJECT_CREATED, { id }) - }) + app.on(EVENT.ROUTING_KEY.PROJECT_DELETED, ({ req, id }) => { + track(req, PROJECT_CREATED, { id }); + }); - app.on(EVENT.ROUTING_KEY.PROJECT_UPDATED, ({req, original, updated }) => { + app.on(EVENT.ROUTING_KEY.PROJECT_UPDATED, ({ req, original, updated }) => { // determine what changed - let eventType + let eventType; if (original.status !== updated.status) { // status was changed - eventType = mapEventTypes[updated.status] + eventType = mapEventTypes[updated.status]; } else if (!_.isEqual(original.details, updated.details)) { // specifications updated - eventType = PROJECT_SPEC_UPDATED + eventType = PROJECT_SPEC_UPDATED; } if (eventType) { track(req, eventType, { id: updated.id, name: updated.name, type: updated.type, - products: _.get(updated, 'products', []) - }) + products: _.get(updated, 'products', []), + }); } + }); - }) - - - app.on(EVENT.ROUTING_KEY.PROJECT_MEMBER_ADDED, ({req, member}) => { - track(req, PROJECT_MEMBER_ADDED, { role: member.role }) - }) - - app.on(EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED, ({req, member}) => { - track(req, PROJECT_MEMBER_REMOVED, { role: member.role }) - }) + app.on(EVENT.ROUTING_KEY.PROJECT_MEMBER_ADDED, ({ req, member }) => { + track(req, PROJECT_MEMBER_ADDED, { role: member.role }); + }); -} + app.on(EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED, ({ req, member }) => { + track(req, PROJECT_MEMBER_REMOVED, { role: member.role }); + }); +}; diff --git a/src/events/index.js b/src/events/index.js index d1220c63..dddcc0b2 100644 --- a/src/events/index.js +++ b/src/events/index.js @@ -1,17 +1,21 @@ -'use strict' + import { - EVENT -} from '../constants' + EVENT, +} from '../constants'; import { - projectCreatedHandler -} from './projects' + projectCreatedHandler, +} from './projects'; import { projectMemberAddedHandler, - projectMemberRemovedHandler -} from './projectMembers' + projectMemberRemovedHandler, +} from './projectMembers'; -export const handlers = { +const handlers = { [EVENT.ROUTING_KEY.PROJECT_DRAFT_CREATED]: projectCreatedHandler, [EVENT.ROUTING_KEY.PROJECT_MEMBER_ADDED]: projectMemberAddedHandler, - [EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED]: projectMemberRemovedHandler -} + [EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED]: projectMemberRemovedHandler, +}; + +export default { + handlers, +}; diff --git a/src/events/projectMembers/index.js b/src/events/projectMembers/index.js index 0ffb1750..2fb4bfa2 100644 --- a/src/events/projectMembers/index.js +++ b/src/events/projectMembers/index.js @@ -1,207 +1,216 @@ -'use strict' - -import _ from 'lodash' -import { - EVENT, - PROJECT_MEMBER_ROLE -} from '../../constants' -import util from '../../util' -import models from '../../models' -import directProject from '../../services/directProject' - +import { PROJECT_MEMBER_ROLE } from '../../constants'; +import util from '../../util'; +import models from '../../models'; +import directProject from '../../services/directProject'; +/** + * Project member added event handler + * @param {Object} logger logger + * @param {Object} msg event payload + * @param {Object} channel channel to ack / nack + * @return {undefined} + */ const projectMemberAddedHandler = (logger, msg, channel) => { - const origRequestId = msg.properties.correlationId - const newMember = JSON.parse(msg.content.toString()) + const origRequestId = msg.properties.correlationId; + const newMember = JSON.parse(msg.content.toString()); if (newMember.role === PROJECT_MEMBER_ROLE.COPILOT) { // Add co-pilot when a co-pilot is added to a project return models.Project.getDirectProjectId(newMember.projectId) - .then(directProjectId => { + .then((directProjectId) => { if (directProjectId) { // retrieve system user token return util.getSystemUserToken(logger) - .then(token => { + .then((token) => { const req = { id: origRequestId, log: logger, headers: { - authorization: `Bearer ${token}` - } - } + authorization: `Bearer ${token}`, + }, + }; return directProject.addCopilot(req, directProjectId, { - copilotUserId: newMember.userId - }) - .then(resp => { - logger.debug('added copilot to direct') + copilotUserId: newMember.userId, + }) + .then(() => { + logger.debug('added copilot to direct'); // acknowledge - channel.ack(msg) - }) + channel.ack(msg); + return undefined; + }); }) - .catch(err => { - logger.error('Error caught while adding co-pilot from direct', err) - channel.nack(msg, false, false) - }) - } else { - logger.info('project not associated with a direct project, skipping') - channel.ack(msg) + .catch((err) => { + logger.error('Error caught while adding co-pilot from direct', err); + channel.nack(msg, false, false); + }); } + logger.info('project not associated with a direct project, skipping'); + channel.ack(msg); + return undefined; }) - .catch(err => { + .catch((err) => { // if the message has been redelivered dont attempt to reprocess it - logger.error('Error retrieving project', err, msg) - channel.nack(msg, false, !msg.fields.redelivered) - }) + logger.error('Error retrieving project', err, msg); + channel.nack(msg, false, !msg.fields.redelivered); + }); } else if (newMember.role === PROJECT_MEMBER_ROLE.MANAGER) { // if manager is added we have to add the manager to direct project return models.Project.getDirectProjectId(newMember.projectId) - .then(directProjectId => { + .then((directProjectId) => { if (directProjectId) { // retrieve system user token return util.getSystemUserToken(logger) - .then(token => { + .then((token) => { const req = { id: origRequestId, log: logger, headers: { - authorization: `Bearer ${token}` - } - } + authorization: `Bearer ${token}`, + }, + }; return directProject.editProjectPermissions(req, directProjectId, { - permissions: [ - { - userId: newMember.userId, - permissionType: { - permissionTypeId: 3, - name: 'project_full' - }, - studio: false - } - ] - }) - .then(resp => { - logger.debug('added manager to direct') - // acknowledge - channel.ack(msg) - }) - }) - .catch(err => { - logger.error('Error caught while adding manager from direct', err) - channel.nack(msg, false, false) + permissions: [ + { + userId: newMember.userId, + permissionType: { + permissionTypeId: 3, + name: 'project_full', + }, + studio: false, + }, + ], + }) + .then(() => { + logger.debug('added manager to direct'); + // acknowledge + channel.ack(msg); + }); }) - } else { - logger.info('project not associated with a direct project, skipping') - channel.ack(msg) + .catch((err) => { + logger.error('Error caught while adding manager from direct', err); + channel.nack(msg, false, false); + }); } + logger.info('project not associated with a direct project, skipping'); + channel.ack(msg); + return undefined; }) - .catch(err => { + .catch((err) => { // if the message has been redelivered dont attempt to reprocess it - logger.error('Error retrieving project', err, msg) - channel.nack(msg, false, !msg.fields.redelivered) - }) - } else { - // nothing to do - channel.ack(msg) + logger.error('Error retrieving project', err, msg); + channel.nack(msg, false, !msg.fields.redelivered); + }); } -} + // nothing to do + channel.ack(msg); + return undefined; +}; +/** + * Project member removed event handler + * @param {Object} logger logger + * @param {Object} msg event payload + * @param {Object} channel channel to ack / nack + * @return {undefined} + */ const projectMemberRemovedHandler = (logger, msg, channel) => { - const origRequestId = msg.properties.correlationId - const member = JSON.parse(msg.content.toString()) + const origRequestId = msg.properties.correlationId; + const member = JSON.parse(msg.content.toString()); if (member.role === PROJECT_MEMBER_ROLE.COPILOT) { // Delete co-pilot when a co-pilot is deleted from a project return models.Project.getDirectProjectId(member.projectId) - .then(directProjectId => { + .then((directProjectId) => { if (directProjectId) { // retrieve system user token return util.getSystemUserToken(logger) - .then(token => { + .then((token) => { const req = { id: origRequestId, log: logger, headers: { - authorization: `Bearer ${token}` - } - } + authorization: `Bearer ${token}`, + }, + }; return directProject.deleteCopilot(req, directProjectId, { - copilotUserId: member.userId - }) - .then(resp => { - logger.debug('removed copilot from direct') - // acknowledge - channel.ack(msg) - }) + copilotUserId: member.userId, + }) + .then(() => { + logger.debug('removed copilot from direct'); + // acknowledge + channel.ack(msg); + return undefined; + }); }) - .catch(err => { - logger.error('Error caught while removing co-pilot from direct', err) - channel.nack(msg, false, false) - }) - } else { - logger.info('project not associated with a direct project, skipping') - channel.ack(msg) + .catch((err) => { + logger.error('Error caught while removing co-pilot from direct', err); + channel.nack(msg, false, false); + }); } + logger.info('project not associated with a direct project, skipping'); + channel.ack(msg); + return undefined; }) - .catch(err => { + .catch((err) => { // if the message has been redelivered dont attempt to reprocess it - logger.error('Error retrieving project', err, msg) - channel.nack(msg, false, !msg.fields.redelivered) - }) + logger.error('Error retrieving project', err, msg); + channel.nack(msg, false, !msg.fields.redelivered); + }); } else if (member.role === PROJECT_MEMBER_ROLE.MANAGER) { // when a manager is removed from direct project we have to remove manager from direct return models.Project.getDirectProjectId(member.projectId) - .then(directProjectId => { + .then((directProjectId) => { if (directProjectId) { // retrieve system user token return util.getSystemUserToken(logger) - .then(token => { + .then((token) => { const req = { id: origRequestId, log: logger, headers: { - authorization: `Bearer ${token}` - } - } + authorization: `Bearer ${token}`, + }, + }; return directProject.editProjectPermissions(req, directProjectId, { - permissions: [ - { - userId: member.userId, - resourceId: directProjectId, - permissionType: { - permissionTypeId: '', - name: 'project_full' - }, - studio: false - } - ] - }) - .then(resp => { - logger.debug('removed manager from direct') - // acknowledge - channel.ack(msg) - }) - }) - .catch(err => { - logger.error('Error caught while adding manager from direct', err) - channel.nack(msg, false, false) + permissions: [ + { + userId: member.userId, + resourceId: directProjectId, + permissionType: { + permissionTypeId: '', + name: 'project_full', + }, + studio: false, + }, + ], + }) + .then(() => { + logger.debug('removed manager from direct'); + // acknowledge + channel.ack(msg); + }); }) - } else { - logger.info('project not associated with a direct project, skipping') - channel.ack(msg) + .catch((err) => { + logger.error('Error caught while adding manager from direct', err); + channel.nack(msg, false, false); + }); } + logger.info('project not associated with a direct project, skipping'); + channel.ack(msg); + return undefined; }) - .catch(err => { + .catch((err) => { // if the message has been redelivered dont attempt to reprocess it - logger.error('Error retrieving project', err, msg) - channel.nack(msg, false, !msg.fields.redelivered) - }) - } else { - // nothing to do - channel.ack(msg) + logger.error('Error retrieving project', err, msg); + channel.nack(msg, false, !msg.fields.redelivered); + }); } -} + // nothing to do + channel.ack(msg); + return undefined; +}; module.exports = { projectMemberAddedHandler, - projectMemberRemovedHandler -} + projectMemberRemovedHandler, +}; diff --git a/src/events/projects/index.js b/src/events/projects/index.js index 638cc3ab..c2499b18 100644 --- a/src/events/projects/index.js +++ b/src/events/projects/index.js @@ -1,55 +1,53 @@ -import _ from 'lodash' -import { - EVENT -} from '../../constants' -import util from '../../util' -import config from 'config' -import querystring from 'querystring' +import config from 'config'; +import querystring from 'querystring'; +import util from '../../util'; /** * Creates a lead in salesforce for the connect project. * - * @param token JWT token of the admin user which would be used to fetch user info - * @param logger logger to be used for logging - * @param project connect project for which lead is to be created - * - * @return promise which resolves to the HTML content where salesforce web to lead form redirects + * @param {String} token JWT token of the admin user which would be used to fetch user info + * @param {String} logger logger to be used for logging + * @param {Object} project connect project for which lead is to be created + * @returns {Promise} promise which resolves to the HTML content where + * salesforce web to lead form redirects */ -const _addSalesforceLead = (token, logger, project) => { - logger.debug('Getting topcoder user with userId: ', project.createdBy) - return util.getTopcoderUser(project.createdBy, token, logger) - .then((userInfo) => { - var httpClient = util.getHttpClient({id: 2,log : logger}) - httpClient.defaults.timeout = 3000 - httpClient.defaults.headers.common['Content-Type'] = 'application/x-www-form-urlencoded' - var data = { - oid: config.get('salesforceLead.orgId'), - first_name: userInfo.firstName, - last_name: userInfo.lastName, - email: userInfo.email - } - data[config.get('salesforceLead.projectIdFieldId')] = project.id - data[config.get('salesforceLead.projectNameFieldId')] = project.name - data[config.get('salesforceLead.projectDescFieldId')] = project.description - data[config.get('salesforceLead.projectLinkFieldId')] = config.get('connectProjectsUrl') + project.id - var body = querystring.stringify(data) - var webToLeadUrl = config.get('salesforceLead.webToLeadUrl') - logger.debug('initiaiting salesforce web to lead call for project: ', project.id) - return httpClient.post(webToLeadUrl, body) - }) -} +// const addSalesforceLead = (token, logger, project) => { +// logger.debug('Getting topcoder user with userId: ', project.createdBy); +// return util.getTopcoderUser(project.createdBy, token, logger) +// .then((userInfo) => { +// const httpClient = util.getHttpClient({ id: 2, log: logger }); +// httpClient.defaults.timeout = 3000; +// httpClient.defaults.headers.common['Content-Type'] = 'application/x-www-form-urlencoded'; +// const data = { +// oid: config.get('salesforceLead.orgId'), +// first_name: userInfo.firstName, +// last_name: userInfo.lastName, +// email: userInfo.email, +// }; +// data[config.get('salesforceLead.projectIdFieldId')] = project.id; +// data[config.get('salesforceLead.projectNameFieldId')] = project.name; +// data[config.get('salesforceLead.projectDescFieldId')] = project.description; +// data[config.get('salesforceLead.projectLinkFieldId')] = +// config.get('connectProjectsUrl') + project.id; +// const body = querystring.stringify(data); +// const webToLeadUrl = config.get('salesforceLead.webToLeadUrl'); +// logger.debug('initiaiting salesforce web to lead call for project: ', project.id); +// return httpClient.post(webToLeadUrl, body); +// }); +// }; /** * Handler for project creation event - * @param {[type]} logger logger to log along with trace id - * @param {[type]} msg event payload - * @param {[type]} channel channel to ack, nack + * @param {Object} logger logger to log along with trace id + * @param {Object} msg event payload + * @param {Object} channel channel to ack, nack + * @returns {undefined} */ -const projectCreatedHandler = (logger, msg, channel) => { +const projectCreatedHandler = (logger, msg, channel) => // disabling salesforce integration for now, // see https://github.com/topcoder-platform/tc-project-service/issues/38 - return channel.ack(msg) + channel.ack(msg); // let project = JSON.parse(msg.content) // return util.getSystemUserToken(logger) // .then(token => { @@ -61,7 +59,8 @@ const projectCreatedHandler = (logger, msg, channel) => { // } // } // return Promise.all([ - // _addSalesforceLead(token, logger, project).then((resp)=> logger.debug('web to lead response:', resp.status)) + // addSalesforceLead(token, logger, project) + // .then((resp)=> logger.debug('web to lead response:', resp.status)) // ]); // }) // .then(() => { @@ -72,8 +71,8 @@ const projectCreatedHandler = (logger, msg, channel) => { // logger.error('Error processing', msg, err) // channel.nack(msg, false, false) // }) -} + module.exports = { - projectCreatedHandler -} + projectCreatedHandler, +}; diff --git a/src/index.js b/src/index.js index 822ae58d..505da7a2 100644 --- a/src/index.js +++ b/src/index.js @@ -1,44 +1,44 @@ -'use strict' - -import models from './models' - -// include newrelic -if (process.env.NODE_ENV !== 'test' && process.env.NODE_ENV !== 'local') { - require('newrelic') -} - -const app = require('./app') - -/** - * Handle server shutdown gracefully - */ -function gracefulShutdown() { - app.services.pubsub.disconnect() - .then(()=> { - app.logger.info('Gracefully shutting down server') - process.exit() - }).catch((err) => { - app.logger.error(err) - }) - // if after - setTimeout(function() { - app.logger.error("Could not close connections in time, forcefully shutting down"); - process.exit() - }, 10*1000); -} -process.on('SIGTERM', gracefulShutdown) -process.on('SIGINT', gracefulShutdown) - -// ======================= -// start the server ====== -// ======================= -var port = process.env.PORT || 3000 // used to create, sign, and verify tokens - -var server = app.listen(port, () => { - app.logger.info("Starting server on PORT: %d", port) - let authz = require('tc-core-library-js').Authorizer - app.logger.info("Registered Policies", authz.getRegisteredPolicies()) - require('express-list-routes')({prefix: '', spacer: 7}, 'APIs:', app.routerRef) -}) - -module.exports = server + + +import models from './models'; + +// include newrelic +if (process.env.NODE_ENV !== 'test' && process.env.NODE_ENV !== 'local') { + require('newrelic'); +} + +const app = require('./app'); + +/** + * Handle server shutdown gracefully + */ +function gracefulShutdown() { + app.services.pubsub.disconnect() + .then(() => { + app.logger.info('Gracefully shutting down server'); + process.exit(); + }).catch((err) => { + app.logger.error(err); + }); + // if after + setTimeout(() => { + app.logger.error('Could not close connections in time, forcefully shutting down'); + process.exit(); + }, 10 * 1000); +} +process.on('SIGTERM', gracefulShutdown); +process.on('SIGINT', gracefulShutdown); + +// ======================= +// start the server ====== +// ======================= +const port = process.env.PORT || 3000; // used to create, sign, and verify tokens + +const server = app.listen(port, () => { + app.logger.info('Starting server on PORT: %d', port); + const authz = require('tc-core-library-js').Authorizer; + app.logger.info('Registered Policies', authz.getRegisteredPolicies()); + require('express-list-routes')({ prefix: '', spacer: 7 }, 'APIs:', app.routerRef); +}); + +module.exports = server; diff --git a/src/middlewares/checkRole.js b/src/middlewares/checkRole.js index 6e0c852e..1144e647 100644 --- a/src/middlewares/checkRole.js +++ b/src/middlewares/checkRole.js @@ -6,16 +6,15 @@ * @author TCDEVELOPER * @version 1.0 */ -import config from 'config' -var util = require('tc-core-library-js').util(config) +import config from 'config'; +const util = require('tc-core-library-js').util(config); -module.exports = function(roleName) { +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.')); - } else { - next() } - } + next(); + }; }; diff --git a/src/mocks/addBillingAccount.js b/src/mocks/addBillingAccount.js index 47fa0a94..3af425e9 100644 --- a/src/mocks/addBillingAccount.js +++ b/src/mocks/addBillingAccount.js @@ -1,30 +1,30 @@ -var http = require("https"); +const http = require('https'); -var options = { - "method": "POST", - "hostname": "localhost", - "port": 8443, - "rejectUnauthorized": false, - "path": "/v3/direct/projects/1/billingaccount", - "headers": { - "Content-Type": "application/json", - "authorization": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", - "cache-control": "no-cache", - "postman-token": "cf9afe9c-dee8-b6bd-ccfb-de528e06b262" - } +const options = { + method: 'POST', + hostname: 'localhost', + port: 8443, + rejectUnauthorized: false, + path: '/v3/direct/projects/1/billingaccount', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts', + 'cache-control': 'no-cache', + 'postman-token': 'cf9afe9c-dee8-b6bd-ccfb-de528e06b262', + }, }; -var req = http.request(options, function (res) { - var chunks = []; +const req = http.request(options, (res) => { + const chunks = []; - res.on("data", function (chunk) { - chunks.push(chunk); - }); + res.on('data', (chunk) => { + chunks.push(chunk); + }); - res.on("end", function () { - var body = Buffer.concat(chunks); - console.log(body.toString()); - }); + res.on('end', () => { + const body = Buffer.concat(chunks); + console.log(body.toString()); + }); }); -req.write("{\n \"billingAccountId\": 123456789\n}"); +req.write('{\n "billingAccountId": 123456789\n}'); req.end(); diff --git a/src/mocks/addCopilot.js b/src/mocks/addCopilot.js index 029620de..c6b17ec7 100644 --- a/src/mocks/addCopilot.js +++ b/src/mocks/addCopilot.js @@ -1,30 +1,30 @@ -var http = require("https"); +const http = require('https'); -var options = { - "method": "POST", - "hostname": "localhost", - "port": 8443, - "rejectUnauthorized": false, - "path": "/v3/direct/projects/1/copilot", - "headers": { - "Content-Type": "application/json", - "authorization": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", - "cache-control": "no-cache", - "postman-token": "cf9afe9c-dee8-b6bd-ccfb-de528e06b262" - } +const options = { + method: 'POST', + hostname: 'localhost', + port: 8443, + rejectUnauthorized: false, + path: '/v3/direct/projects/1/copilot', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts', + 'cache-control': 'no-cache', + 'postman-token': 'cf9afe9c-dee8-b6bd-ccfb-de528e06b262', + }, }; -var req = http.request(options, function (res) { - var chunks = []; +const req = http.request(options, (res) => { + const chunks = []; - res.on("data", function (chunk) { - chunks.push(chunk); - }); + res.on('data', (chunk) => { + chunks.push(chunk); + }); - res.on("end", function () { - var body = Buffer.concat(chunks); - console.log(body.toString()); - }); + res.on('end', () => { + const body = Buffer.concat(chunks); + console.log(body.toString()); + }); }); -req.write("{\n \"copilotUserId\": 123456789\n}"); +req.write('{\n "copilotUserId": 123456789\n}'); req.end(); diff --git a/src/mocks/createProject.js b/src/mocks/createProject.js index 9c9dc5ba..4aca9b40 100644 --- a/src/mocks/createProject.js +++ b/src/mocks/createProject.js @@ -1,31 +1,31 @@ -var http = require("https"); +const http = require('https'); -var options = { - "method": "POST", - "hostname": "localhost", - "port": 8443, - "rejectUnauthorized": false, - "path": "/v3/direct/projects/", - "headers": { - "Content-Type": "application/json", - "authorization": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts", - "cache-control": "no-cache", - "postman-token": "cf9afe9c-dee8-b6bd-ccfb-de528e06b262" - } +const options = { + method: 'POST', + hostname: 'localhost', + port: 8443, + rejectUnauthorized: false, + path: '/v3/direct/projects/', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzMyIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.uiZHiDXF-_KysU5tq-G82oBTYBR0gV_w-svLX_2O6ts', + 'cache-control': 'no-cache', + 'postman-token': 'cf9afe9c-dee8-b6bd-ccfb-de528e06b262', + }, }; -var req = http.request(options, function (res) { - var chunks = []; +const req = http.request(options, (res) => { + const chunks = []; - res.on("data", function (chunk) { - chunks.push(chunk); - }); + res.on('data', (chunk) => { + chunks.push(chunk); + }); - res.on("end", function () { - var body = Buffer.concat(chunks); - console.log(body.toString()); - }); + res.on('end', () => { + const body = Buffer.concat(chunks); + console.log(body.toString()); + }); }); -req.write("{\n \"projectName\": \"Tony Test 1\",\n \"projectDescription\": \"test 1 description\"\n}"); +req.write('{\n "projectName": "Tony Test 1",\n "projectDescription": "test 1 description"\n}'); req.end(); diff --git a/src/mocks/direct.js b/src/mocks/direct.js index 6f8dd5cc..4dabcfdc 100644 --- a/src/mocks/direct.js +++ b/src/mocks/direct.js @@ -1,125 +1,125 @@ -'use strict' -import express 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 app = express() + +import express 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 app = express(); app.use(bodyParser.urlencoded({ - extended: false -})) -app.use(bodyParser.json()) + extended: false, +})); +app.use(bodyParser.json()); // add request Id -const addRequestId = expressRequestId() -app.use(addRequestId) +const addRequestId = expressRequestId(); +app.use(addRequestId); // Logger -const appName = 'tc-mock-direct' -var logger = coreLib.logger({ - name: appName, - level: _.get(config, "logLevel", 'debug').toLowerCase(), - captureLogs: config.get('captureLogs'), - logentriesToken: _.get(config, 'logentriesToken', null) -}) -app.use(coreLib.middleware.logger(null, logger)) -app.logger = logger +const appName = 'tc-mock-direct'; +const logger = coreLib.logger({ + name: appName, + level: _.get(config, 'logLevel', 'debug').toLowerCase(), + captureLogs: config.get('captureLogs'), + logentriesToken: _.get(config, 'logentriesToken', null), +}); +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()) +const router = Router(); +const jwtAuth = require('tc-core-library-js').middleware.jwtAuthenticator; +router.all('/v3/direct/projects*', jwtAuth()); -let projectId = 2 +let projectId = 2; const projects = { - "1": { - projectName: "test direct project1", - projectDescription: "test direct project1", - billingAccountId: 2, - copilotUserId:4, - id: 1 - } -} + 1: { + projectName: 'test direct project1', + projectDescription: 'test direct project1', + billingAccountId: 2, + copilotUserId: 4, + id: 1, + }, +}; // Register all the routes router.route('/v3/direct/projects') - .get((req, res)=>{ - app.logger.info("get direct projects") - res.json(util.wrapResponse(req.id, {projects: projects})) - }) - .post((req, res)=>{ - app.logger.info({body: req.body}, "create direct project") - const newId = projectId++ - req.body.id = newId - projects[newId] = req.body - res.json(util.wrapResponse(req.id, {projectId: newId})) + .get((req, res) => { + app.logger.info('get direct projects'); + res.json(util.wrapResponse(req.id, { projects })); }) + .post((req, res) => { + app.logger.info({ body: req.body }, 'create direct project'); + const newId = projectId++; + req.body.id = newId; + projects[newId] = req.body; + res.json(util.wrapResponse(req.id, { projectId: newId })); + }); router.route('/v3/direct/projects/:projectId(\\d+)/billingaccount') - .post((req, res)=>{ - const projectId = req.params.projectId - app.logger.info({body: req.body, projectId: 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}`})) - } else { - res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${projectId}`)) - } - }) + .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}` })); + } else { + res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${projectId}`)); + } + }); router.route('/v3/direct/projects/:projectId(\\d+)/copilot') - .post((req, res)=>{ - const projectId = req.params.projectId - app.logger.info({body: req.body, projectId: projectId }, "add copilot to Project") - if(projects[projectId]) { - projects[projectId] = _.merge(projects[projectId], req.body) - res.json(util.wrapResponse(req.id, {copilotProjectId: projectId})) - } else { - res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${projectId}`)) - } - }) - .delete((req, res)=>{ - const projectId = req.params.projectId - app.logger.info({body: req.body, projectId: projectId }, "remove copilot from Project") - if(projects[projectId]) { - projects[projectId] = _.omit(projects[projectId], 'copilotUserId') - res.json(util.wrapResponse(req.id, true)) - } else { - res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${projectId}`)) - } + .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 })); + } else { + res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${projectId}`)); + } }) + .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'); + res.json(util.wrapResponse(req.id, true)); + } else { + res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${projectId}`)); + } + }); router.route('/v3/direct/projects/:projectId(\\d+)/permissions') - .post((req, res)=>{ - const projectId = req.params.projectId - app.logger.info({body: req.body, projectId: projectId }, 'add permissions to Project') - if(projects[projectId]) { - res.json() - } else { - res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${projectId}`)) - } - }) + .post((req, res) => { + const projectId = req.params.projectId; + app.logger.info({ body: req.body, projectId }, 'add permissions to Project'); + if (projects[projectId]) { + res.json(); + } else { + res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${projectId}`)); + } + }); -app.use(router) +app.use(router); // ======================= // start the server ====== // ======================= -const port = process.env.DIRECT_PORT || 8443 +const port = process.env.DIRECT_PORT || 8443; const server = https.createServer({ - key: fs.readFileSync(path.join(__dirname, 'key.pem')), - cert: fs.readFileSync(path.join(__dirname,'cert.pem')), - passphrase: '1234' + key: fs.readFileSync(path.join(__dirname, 'key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'cert.pem')), + passphrase: '1234', }, app).listen(port, () => { - app.logger.info("Starting mock direct server on PORT: %d", port) -}) + app.logger.info('Starting mock direct server on PORT: %d', port); +}); -module.exports = server +module.exports = server; diff --git a/src/models/index.js b/src/models/index.js index d37e83cf..59c8c9d2 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -1,52 +1,50 @@ -"use strict" -import fs from 'fs' -import path from 'path' -import config from 'config' -import cls from 'continuation-local-storage' -import pg from 'pg' -import Sequelize from 'sequelize' + +import fs from 'fs'; +import path from 'path'; +import config from 'config'; +import cls from 'continuation-local-storage'; +import pg from 'pg'; +import Sequelize from 'sequelize'; // // BIGINT string bug - https://github.com/sequelize/sequelize/issues/1774 -pg.defaults.parseInt8 = true -delete pg.native +pg.defaults.parseInt8 = true; +delete pg.native; -Sequelize.cls = cls.createNamespace('tc.micro.service') +Sequelize.cls = cls.createNamespace('tc.micro.service'); -var sequelize = new Sequelize(config.get('dbConfig.masterUrl'), { +const sequelize = new Sequelize(config.get('dbConfig.masterUrl'), { logging: false, dialectOptions: { - ssl: false + ssl: false, }, define: { - timestamps: false + timestamps: false, }, freezeTableName: true, pool: { max: config.dbConfig.maxPoolSize, min: config.dbConfig.minPoolSize, - idle: config.dbConfig.idleTimeout - } -}) + idle: config.dbConfig.idleTimeout, + }, +}); -var db = {} +const db = {}; fs .readdirSync(__dirname) - .filter(function(file) { - return (file.indexOf(".") !== 0) && (file !== "index.js") - }) - .forEach(function(file) { - var model = sequelize["import"](path.join(__dirname, file)) - db[model.name] = model - }) - -Object.keys(db).forEach(function(modelName) { - if ("associate" in db[modelName]) { - db[modelName].associate(db) - } -}) - -db.sequelize = sequelize -db.Sequelize = Sequelize - -export default db + .filter(file => (file.indexOf('.') !== 0) && (file !== 'index.js')) + .forEach((file) => { + const model = sequelize.import(path.join(__dirname, file)); + db[model.name] = model; + }); + +Object.keys(db).forEach((modelName) => { + if ('associate' in db[modelName]) { + db[modelName].associate(db); + } +}); + +db.sequelize = sequelize; +db.Sequelize = Sequelize; + +export default db; diff --git a/src/models/project.js b/src/models/project.js index ef728ee8..d565e72c 100644 --- a/src/models/project.js +++ b/src/models/project.js @@ -1,163 +1,157 @@ -'use strict' -import { PROJECT_TYPE, PROJECT_STATUS, PROJECT_MEMBER_ROLE } from '../constants' -import _ from 'lodash' - -module.exports = function(sequelize, DataTypes) { - var Project = sequelize.define('Project', { - id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, - directProjectId: DataTypes.BIGINT, - billingAccountId: DataTypes.BIGINT, - name: { type: DataTypes.STRING, allowNull: false }, - description: DataTypes.TEXT, - external: DataTypes.JSON, - bookmarks: DataTypes.JSON, - utm: { type: DataTypes.JSON, allowNull: true }, - estimatedPrice: { type: DataTypes.DECIMAL(10,2), allowNull: true }, - actualPrice: { type: DataTypes.DECIMAL(10,2), allowNull: true}, - terms: { - type: DataTypes.ARRAY(DataTypes.INTEGER), - allowNull: false, - defaultValue: [] - }, - type: { - type: DataTypes.STRING, - allowNull: false, - validate: { - isIn: [_.values(PROJECT_TYPE)] - } - }, - status: { - type: DataTypes.STRING, - allowNull: false, - validate: { - isIn: [_.values(PROJECT_STATUS)] - } - }, - details: { type: DataTypes.JSON }, - challengeEligibility: DataTypes.JSON, + +import { PROJECT_TYPE, PROJECT_STATUS, PROJECT_MEMBER_ROLE } from '../constants'; +import _ from 'lodash'; + +module.exports = function (sequelize, DataTypes) { + var Project = sequelize.define('Project', { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + directProjectId: DataTypes.BIGINT, + billingAccountId: DataTypes.BIGINT, + name: { type: DataTypes.STRING, allowNull: false }, + description: DataTypes.TEXT, + external: DataTypes.JSON, + bookmarks: DataTypes.JSON, + utm: { type: DataTypes.JSON, allowNull: true }, + estimatedPrice: { type: DataTypes.DECIMAL(10, 2), allowNull: true }, + actualPrice: { type: DataTypes.DECIMAL(10, 2), allowNull: true }, + terms: { + type: DataTypes.ARRAY(DataTypes.INTEGER), + allowNull: false, + defaultValue: [], + }, + type: { + type: DataTypes.STRING, + allowNull: false, + validate: { + isIn: [_.values(PROJECT_TYPE)], + }, + }, + status: { + type: DataTypes.STRING, + allowNull: false, + validate: { + isIn: [_.values(PROJECT_STATUS)], + }, + }, + details: { type: DataTypes.JSON }, + challengeEligibility: DataTypes.JSON, cancelReason: DataTypes.STRING, - deletedAt: { type: DataTypes.DATE, allowNull: true }, - createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, - updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, - createdBy: { type: DataTypes.INTEGER, allowNull: false }, - updatedBy: { type: DataTypes.INTEGER, allowNull: false } - }, { - tableName: 'projects', - timestamps: true, - updatedAt: 'updatedAt', - createdAt: 'createdAt', - deletedAt: 'deletedAt', - indexes: [ - { fields: ['createdAt'] }, - { fields: ['name'] }, - { fields: ['type'] }, - { fields: ['status'] }, - { fields: ['directProjectId'] } - ], - classMethods: { - /* - * @Co-pilots should be able to view projects any of the following conditions are met: - * a. they are registered active project members on the project - * b. any project that is in 'reviewed' state AND does not yet have a co-pilot assigned - * @param userId the id of user - */ - getProjectIdsForCopilot: function(userId) { - 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" = ? )', - PROJECT_STATUS.REVIEWED, PROJECT_MEMBER_ROLE.COPILOT] - ] - }, - attributes:['id'], - raw: true - }) - .then((res) => { - return _.map(res, 'id') - }) - }, - /** - * Get direct project id - * @param id the id of project - */ - getDirectProjectId: function(id) { - return this.findById(id, { - attributes:['directProjectId'], - raw: true - }) - .then((res) => { - return res.directProjectId - }) - }, - associate: (models) => { - Project.hasMany(models.ProjectMember, { as : 'members', foreignKey: 'projectId' }) - Project.hasMany(models.ProjectAttachment, { as : 'attachments', foreignKey: 'projectId' }) - }, - /** - * Search keyword in name, description, details.utm.code - * @param parameters the parameters - * - filters: the filters contains keyword - * - order: the order - * - limit: the limit - * - offset: the offset - * - attributes: the attributes to get - * @param log the request log - * @return the result rows and count - */ - searchText: function(parameters, log) { - // special handling for keyword filter - var query = '1=1 '; - if (_.has(parameters.filters, 'id')) { - if (_.isObject(parameters.filters.id)) { - if (parameters.filters.id['$in'].length === 0) { - parameters.filters.id['$in'].push(-1) - } - query += `AND id IN (${parameters.filters.id['$in']}) `; - } else if(_.isString(parameters.filters.id) || _.isNumber(parameters.filters.id)){ - query += `AND id = ${parameters.filters.id} `; - } - } - if (_.has(parameters.filters, 'status')) { - var statusFilter = parameters.filters.status - if (_.isObject(statusFilter)) { - var statuses = statusFilter['$in'].join("','"); - query += `AND status IN ('${statuses}') `; - } else if(_.isString(statusFilter)){ - query += `AND status ='${statusFilter}'`; - } - } - if (_.has(parameters.filters, 'type')) { - query += `AND type = '${parameters.filters.type}' `; - } - if (_.has(parameters.filters, 'keyword')) { - query += `AND "projectFullText" ~ lower('${parameters.filters.keyword}')`; - } - - let attributesStr = '"' + parameters.attributes.join('","') + '"'; - let orderStr = '"' + parameters.order[0][0] + '" ' + parameters.order[0][1]; - - // select count of projects - return sequelize.query(`SELECT COUNT(1) FROM projects WHERE ${query}`, - { type: sequelize.QueryTypes.SELECT, - logging: (str) => { log.debug(str); }, - raw: true - }) - .then(function(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}`, - { type: sequelize.QueryTypes.SELECT, - logging: (str) => { log.debug(str); }, - raw: true - }) - .then(function(projects) { - return {rows: projects, count: count}; - }); - }); - } - } - }) - - return Project -} + deletedAt: { type: DataTypes.DATE, allowNull: true }, + createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, + createdBy: { type: DataTypes.INTEGER, allowNull: false }, + updatedBy: { type: DataTypes.INTEGER, allowNull: false }, + }, { + tableName: 'projects', + timestamps: true, + updatedAt: 'updatedAt', + createdAt: 'createdAt', + deletedAt: 'deletedAt', + indexes: [ + { fields: ['createdAt'] }, + { fields: ['name'] }, + { fields: ['type'] }, + { fields: ['status'] }, + { fields: ['directProjectId'] }, + ], + classMethods: { + /* + * @Co-pilots should be able to view projects any of the following conditions are met: + * a. they are registered active project members on the project + * b. any project that is in 'reviewed' state AND does not yet have a co-pilot assigned + * @param userId the id of user + */ + getProjectIdsForCopilot(userId) { + 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" = ? )', + PROJECT_STATUS.REVIEWED, PROJECT_MEMBER_ROLE.COPILOT], + ], + }, + attributes: ['id'], + raw: true, + }) + .then(res => _.map(res, 'id')); + }, + /** + * Get direct project id + * @param id the id of project + */ + getDirectProjectId(id) { + return this.findById(id, { + attributes: ['directProjectId'], + raw: true, + }) + .then(res => res.directProjectId); + }, + associate: (models) => { + Project.hasMany(models.ProjectMember, { as: 'members', foreignKey: 'projectId' }); + Project.hasMany(models.ProjectAttachment, { as: 'attachments', foreignKey: 'projectId' }); + }, + /** + * Search keyword in name, description, details.utm.code + * @param parameters the parameters + * - filters: the filters contains keyword + * - order: the order + * - limit: the limit + * - offset: the offset + * - attributes: the attributes to get + * @param log the request log + * @return the result rows and count + */ + searchText(parameters, log) { + // special handling for keyword filter + let query = '1=1 '; + if (_.has(parameters.filters, 'id')) { + if (_.isObject(parameters.filters.id)) { + if (parameters.filters.id.$in.length === 0) { + parameters.filters.id.$in.push(-1); + } + query += `AND id IN (${parameters.filters.id.$in}) `; + } else if (_.isString(parameters.filters.id) || _.isNumber(parameters.filters.id)) { + query += `AND id = ${parameters.filters.id} `; + } + } + if (_.has(parameters.filters, 'status')) { + const statusFilter = parameters.filters.status; + if (_.isObject(statusFilter)) { + const statuses = statusFilter.$in.join("','"); + query += `AND status IN ('${statuses}') `; + } else if (_.isString(statusFilter)) { + query += `AND status ='${statusFilter}'`; + } + } + if (_.has(parameters.filters, 'type')) { + query += `AND type = '${parameters.filters.type}' `; + } + if (_.has(parameters.filters, 'keyword')) { + query += `AND "projectFullText" ~ lower('${parameters.filters.keyword}')`; + } + + const attributesStr = `"${parameters.attributes.join('","')}"`; + const orderStr = `"${parameters.order[0][0]}" ${parameters.order[0][1]}`; + + // select count of projects + return sequelize.query(`SELECT COUNT(1) FROM projects WHERE ${query}`, + { type: sequelize.QueryTypes.SELECT, + logging: (str) => { log.debug(str); }, + raw: true, + }) + .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}`, + { type: sequelize.QueryTypes.SELECT, + logging: (str) => { log.debug(str); }, + raw: true, + }) + .then(projects => ({ rows: projects, count })); + }); + }, + }, + }); + + return Project; +}; diff --git a/src/models/projectAttachment.js b/src/models/projectAttachment.js index 5582d034..c7825fc2 100644 --- a/src/models/projectAttachment.js +++ b/src/models/projectAttachment.js @@ -1,19 +1,19 @@ -'use strict' -module.exports = function(sequelize, DataTypes) { - var ProjectAttachment = sequelize.define('ProjectAttachment', { + +module.exports = function (sequelize, DataTypes) { + const ProjectAttachment = sequelize.define('ProjectAttachment', { id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, title: { type: DataTypes.STRING, allowNull: true }, size: { type: DataTypes.INTEGER, allowNull: true }, // size in MB category: { type: DataTypes.STRING, allowNull: true }, // size in MB - description: { type: DataTypes.STRING, allowNull: true}, + description: { type: DataTypes.STRING, allowNull: true }, filePath: { type: DataTypes.STRING, allowNull: false }, contentType: { type: DataTypes.STRING, allowNull: false }, deletedAt: { type: DataTypes.DATE, allowNull: true }, createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, createdBy: { type: DataTypes.INTEGER, allowNull: false }, - updatedBy: { type: DataTypes.INTEGER, allowNull: false } + updatedBy: { type: DataTypes.INTEGER, allowNull: false }, }, { tableName: 'project_attachments', paranoid: false, @@ -23,17 +23,17 @@ module.exports = function(sequelize, DataTypes) { deletedAt: 'deletedAt', indexes: [], classMethods: { - getActiveProjectAttachments: function(projectId) { + getActiveProjectAttachments(projectId) { return this.findAll({ where: { deletedAt: { $eq: null }, - projectId: projectId + projectId, }, - raw: true - }) - } - } - }) + raw: true, + }); + }, + }, + }); - return ProjectAttachment -} + return ProjectAttachment; +}; diff --git a/src/models/projectHistory.js b/src/models/projectHistory.js index b9edfa22..9b243810 100644 --- a/src/models/projectHistory.js +++ b/src/models/projectHistory.js @@ -1,22 +1,22 @@ -'use strict' -module.exports = function(sequelize, DataTypes) { - var ProjectHistory = sequelize.define('ProjectHistory', { + +module.exports = function (sequelize, DataTypes) { + const ProjectHistory = sequelize.define('ProjectHistory', { id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, projectId: { type: DataTypes.BIGINT, allowNull: false }, status: { type: DataTypes.STRING, allowNull: false }, cancelReason: { type: DataTypes.STRING, allowNull: true }, createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, - updatedBy: { type: DataTypes.INTEGER, allowNull: false } + updatedBy: { type: DataTypes.INTEGER, allowNull: false }, }, { tableName: 'project_history', paranoid: false, timestamps: true, updatedAt: 'updatedAt', createdAt: 'createdAt', - indexes: [] - }) + indexes: [], + }); - return ProjectHistory -} + return ProjectHistory; +}; diff --git a/src/models/projectMember.js b/src/models/projectMember.js index 67fd0b53..a8b858b7 100644 --- a/src/models/projectMember.js +++ b/src/models/projectMember.js @@ -1,24 +1,24 @@ -'use strict' -import _ from 'lodash' -import { PROJECT_MEMBER_ROLE } from '../constants' -module.exports = function(sequelize, DataTypes) { - var ProjectMember = sequelize.define('ProjectMember', { +import _ from 'lodash'; +import { PROJECT_MEMBER_ROLE } from '../constants'; + +module.exports = function (sequelize, DataTypes) { + const ProjectMember = sequelize.define('ProjectMember', { id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, userId: DataTypes.BIGINT, role: { type: DataTypes.STRING, allowNull: false, validate: { - isIn: [_.values(PROJECT_MEMBER_ROLE)] - } + isIn: [_.values(PROJECT_MEMBER_ROLE)], + }, }, isPrimary: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }, deletedAt: { type: DataTypes.DATE, allowNull: true }, createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, createdBy: { type: DataTypes.INTEGER, allowNull: false }, - updatedBy: { type: DataTypes.INTEGER, allowNull: false } + updatedBy: { type: DataTypes.INTEGER, allowNull: false }, }, { tableName: 'project_members', paranoid: true, @@ -29,33 +29,31 @@ module.exports = function(sequelize, DataTypes) { indexes: [ { fields: ['deletedAt'] }, { fields: ['userId'] }, - { fields: ['role'] } + { fields: ['role'] }, ], classMethods: { - getProjectIdsForUser: function(userId) { + getProjectIdsForUser(userId) { return this.findAll({ where: { - deletedAt: { $eq: null}, - userId: userId + deletedAt: { $eq: null }, + userId, }, - attributes:['projectId'], - raw: true - }) - .then((res) => { - return _.without(_.map(res, 'projectId'), null) + attributes: ['projectId'], + raw: true, }) + .then(res => _.without(_.map(res, 'projectId'), null)); }, - getActiveProjectMembers: function(projectId) { + getActiveProjectMembers(projectId) { return this.findAll({ where: { deletedAt: { $eq: null }, - projectId: projectId + projectId, }, - raw: true - }) - } - } - }) + raw: true, + }); + }, + }, + }); - return ProjectMember -} + return ProjectMember; +}; diff --git a/src/permissions/index.js b/src/permissions/index.js index 15a0da3b..5465845a 100644 --- a/src/permissions/index.js +++ b/src/permissions/index.js @@ -1,19 +1,19 @@ -'use strict' -const Authorizer = require('tc-core-library-js').Authorizer + +const Authorizer = require('tc-core-library-js').Authorizer; module.exports = () => { - Authorizer.setDeniedStatusCode(403) + Authorizer.setDeniedStatusCode(403); // anyone can create a project - Authorizer.setPolicy('project.create', true) - Authorizer.setPolicy('project.view', require('./project.view')) - Authorizer.setPolicy('project.edit', require('./project.edit')) - Authorizer.setPolicy('project.delete', require('./project.delete')) - Authorizer.setPolicy('project.addMember', require('./project.view')) - Authorizer.setPolicy('project.removeMember', require('./projectMember.delete')) - Authorizer.setPolicy('project.addAttachment', require('./project.edit')) - Authorizer.setPolicy('project.updateAttachment', require('./project.edit')) - Authorizer.setPolicy('project.removeAttachment', require('./project.edit')) - Authorizer.setPolicy('project.updateMember', require('./project.edit')) -} + Authorizer.setPolicy('project.create', true); + Authorizer.setPolicy('project.view', require('./project.view')); + Authorizer.setPolicy('project.edit', require('./project.edit')); + Authorizer.setPolicy('project.delete', require('./project.delete')); + Authorizer.setPolicy('project.addMember', require('./project.view')); + Authorizer.setPolicy('project.removeMember', require('./projectMember.delete')); + Authorizer.setPolicy('project.addAttachment', require('./project.edit')); + Authorizer.setPolicy('project.updateAttachment', require('./project.edit')); + Authorizer.setPolicy('project.removeAttachment', require('./project.edit')); + Authorizer.setPolicy('project.updateMember', require('./project.edit')); +}; diff --git a/src/permissions/project.delete.js b/src/permissions/project.delete.js index efbdf17b..b532798c 100644 --- a/src/permissions/project.delete.js +++ b/src/permissions/project.delete.js @@ -1,36 +1,32 @@ -'use strict' + /* globals Promise */ -import util from '../util' -import models from '../models' -import { USER_ROLE, PROJECT_MEMBER_ROLE } from '../constants' -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. */ -module.exports = (req) => { - return new Promise((resolve, reject) => { - var projectId = _.parseInt(req.params.projectId) - return models.ProjectMember.getActiveProjectMembers(projectId) +module.exports = req => new Promise((resolve, reject) => { + const projectId = _.parseInt(req.params.projectId); + return models.ProjectMember.getActiveProjectMembers(projectId) .then((members) => { - req.context = req.context || {} - req.context.currentProjectMembers = members + req.context = req.context || {}; + req.context.currentProjectMembers = members; // check if auth user has acecss to this project - let hasAccess = util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) - || !_.isUndefined(_.find(members, (m) => { - return m.userId === req.authUser.userId + const hasAccess = util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) + || !_.isUndefined(_.find(members, m => m.userId === req.authUser.userId && ( m.role === PROJECT_MEMBER_ROLE.CUSTOMER && m.isPrimary - || m.role === PROJECT_MEMBER_ROLE.MANAGER) - })) + || m.role === PROJECT_MEMBER_ROLE.MANAGER))); if (!hasAccess) { // user is not an admin nor is a registered project member - return reject(new Error('You do not have permissions to perform this action')) + return reject(new Error('You do not have permissions to perform this action')); } - return resolve(true) - }) - }) -} + return resolve(true); + }); +}); diff --git a/src/permissions/project.edit.js b/src/permissions/project.edit.js index a7cb42a6..62854066 100644 --- a/src/permissions/project.edit.js +++ b/src/permissions/project.edit.js @@ -1,32 +1,30 @@ -'use strict' + /* globals Promise */ -import util from '../util' -import models from '../models' -import { USER_ROLE } from '../constants' -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. */ -module.exports = (req) => { - return new Promise((resolve, reject) => { - var projectId = _.parseInt(req.params.projectId) - return models.ProjectMember.getActiveProjectMembers(projectId) +module.exports = req => new Promise((resolve, reject) => { + const projectId = _.parseInt(req.params.projectId); + return models.ProjectMember.getActiveProjectMembers(projectId) .then((members) => { - req.context = req.context || {} - req.context.currentProjectMembers = members + req.context = req.context || {}; + req.context.currentProjectMembers = members; // check if auth user has acecss to this project - let hasAccess = util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) + const hasAccess = util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) || util.hasRole(req, USER_ROLE.MANAGER) - || !_.isUndefined(_.find(members, (m) => {return m.userId === req.authUser.userId})) + || !_.isUndefined(_.find(members, m => m.userId === req.authUser.userId)); if (!hasAccess) { // user is not an admin nor is a registered project member - return reject(new Error('You do not have permissions to perform this action')) + return reject(new Error('You do not have permissions to perform this action')); } - return resolve(true) - }) - }) -} + return resolve(true); + }); +}); diff --git a/src/permissions/project.view.js b/src/permissions/project.view.js index 37b785e3..26e64740 100644 --- a/src/permissions/project.view.js +++ b/src/permissions/project.view.js @@ -1,46 +1,43 @@ -'use strict' + /* globals Promise */ -import util from '../util' -import models from '../models' -import { USER_ROLE } from '../constants' -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. */ -module.exports = (req) => { - return new Promise((resolve, reject) => { - const projectId = _.parseInt(req.params.projectId) - const currentUserId = req.authUser.userId - return models.ProjectMember.getActiveProjectMembers(projectId) +module.exports = req => new Promise((resolve, reject) => { + const projectId = _.parseInt(req.params.projectId); + const currentUserId = req.authUser.userId; + return models.ProjectMember.getActiveProjectMembers(projectId) .then((members) => { - req.context = req.context || {} - req.context.currentProjectMembers = members + req.context = req.context || {}; + req.context.currentProjectMembers = members; // check if auth user has acecss to this project - let hasAccess = util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) + const hasAccess = util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) || util.hasRole(req, USER_ROLE.MANAGER) - || !_.isUndefined(_.find(members, (m) => {return m.userId === currentUserId})) + || !_.isUndefined(_.find(members, m => m.userId === currentUserId)); // if user is co-pilot and the project doesn't have any copilots then user can access the project if (util.hasRole(req, USER_ROLE.COPILOT)) { return models.Project.getProjectIdsForCopilot(currentUserId) - .then(ids => { - req.context.accessibleProjectIds = ids - return Promise.resolve(_.indexOf(ids, projectId) > -1) - }) - } else { - return Promise.resolve(hasAccess) + .then((ids) => { + req.context.accessibleProjectIds = ids; + return Promise.resolve(_.indexOf(ids, projectId) > -1); + }); } + return Promise.resolve(hasAccess); }) - .then(hasAccess => { + .then((hasAccess) => { if (!hasAccess) { // user is not an admin nor is a registered project member - return reject(new Error('You do not have permissions to perform this action')) + return reject(new Error('You do not have permissions to perform this action')); } - return resolve(true) - }) - }) -} + return resolve(true); + }); +}); diff --git a/src/permissions/projectMember.delete.js b/src/permissions/projectMember.delete.js index b6e55ed1..9a07c185 100644 --- a/src/permissions/projectMember.delete.js +++ b/src/permissions/projectMember.delete.js @@ -1,39 +1,37 @@ -'use strict' + /* globals Promise */ -import util from '../util' -import models from '../models' -import { USER_ROLE, PROJECT_MEMBER_ROLE } from '../constants' -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. */ -module.exports = (req) => { - return new Promise((resolve, reject) => { - var projectId = _.parseInt(req.params.projectId) - return models.ProjectMember.getActiveProjectMembers(projectId) +module.exports = req => new Promise((resolve, reject) => { + const projectId = _.parseInt(req.params.projectId); + return models.ProjectMember.getActiveProjectMembers(projectId) .then((members) => { - req.context = req.context || {} - req.context.currentProjectMembers = members - const authMember = _.find(members, m => m.userId === req.authUser.userId) - const prjMemberId = _.parseInt(req.params.id) - const memberToBeRemoved = _.find(members, m => m.id === prjMemberId) + req.context = req.context || {}; + req.context.currentProjectMembers = members; + const authMember = _.find(members, m => m.userId === req.authUser.userId); + const prjMemberId = _.parseInt(req.params.id); + const memberToBeRemoved = _.find(members, m => m.id === prjMemberId); // check if auth user has acecss to this project - let hasAccess = util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) + const hasAccess = util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) || authMember && memberToBeRemoved && ( authMember.role === PROJECT_MEMBER_ROLE.MANAGER || authMember.role === PROJECT_MEMBER_ROLE.CUSTOMER && authMember.isPrimary && memberToBeRemoved.role === PROJECT_MEMBER_ROLE.CUSTOMER - || memberToBeRemoved.userId === req.authUser.userId) + || memberToBeRemoved.userId === req.authUser.userId); if (!hasAccess) { // user is not an admin nor is a registered project member - return reject(new Error('You do not have permissions to perform this action')) + return reject(new Error('You do not have permissions to perform this action')); } - return resolve(true) - }) - }) -} + return resolve(true); + }); +}); diff --git a/src/routes/attachments/create.js b/src/routes/attachments/create.js index 3a9abe19..53abd5a7 100644 --- a/src/routes/attachments/create.js +++ b/src/routes/attachments/create.js @@ -1,19 +1,19 @@ -'use strict' + /** * API to handle adding project attachment. * */ -import validate from 'express-validation' -import _ from 'lodash' -import config from 'config' -import Joi from 'joi' -import path from 'path' +import validate from 'express-validation'; +import _ from 'lodash'; +import config from 'config'; +import Joi from 'joi'; +import path from 'path'; -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'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; -const permissions = tcMiddleware.permissions +const permissions = tcMiddleware.permissions; const addAttachmentValidations = { body: { @@ -24,10 +24,10 @@ const addAttachmentValidations = { size: Joi.number().optional(), filePath: Joi.string().required(), s3Bucket: Joi.string().required(), - contentType: Joi.string().required() - }).required() - } -} + contentType: Joi.string().required(), + }).required(), + }, +}; module.exports = [ // handles request validations @@ -37,59 +37,59 @@ module.exports = [ * Add project attachment */ (req, res, next) => { - var data = req.body.param + const data = req.body.param; // default values - var projectId = req.params.projectId + const projectId = req.params.projectId; _.assign(data, { - projectId: projectId, + projectId, createdBy: req.authUser.userId, - updatedBy: req.authUser.userId - }) + updatedBy: req.authUser.userId, + }); // extract file name - var fileName = path.parse(data.filePath).base + const fileName = path.parse(data.filePath).base; // create file path - var filePath = _.join([ + const filePath = _.join([ config.get('projectAttachmentPathPrefix'), data.projectId, config.get('projectAttachmentPathPrefix'), - fileName - ], '/') - var newAttachment = null + fileName, + ], '/'); + let newAttachment = null; // get presigned Url - var httpClient = util.getHttpClient(req) - httpClient.defaults.headers.common['Authorization'] = req.headers.authorization + const httpClient = util.getHttpClient(req); + httpClient.defaults.headers.common.Authorization = req.headers.authorization; - let fileServiceUrl = config.get('fileServiceEndpoint') - if (fileServiceUrl.substr(-1) !== '/') fileServiceUrl += '/' + let fileServiceUrl = config.get('fileServiceEndpoint'); + if (fileServiceUrl.substr(-1) !== '/') fileServiceUrl += '/'; // get pre-signed Url - req.log.debug('requesting presigned Url') - return httpClient.post(fileServiceUrl + 'uploadurl/', { + req.log.debug('requesting presigned Url'); + return httpClient.post(`${fileServiceUrl}uploadurl/`, { param: { - filePath: filePath, + filePath, contentType: data.contentType, - isPublic: false - } + isPublic: false, + }, }) .then((resp) => { - req.log.debug('Presigned Url resp: ', JSON.stringify(resp.data, null, 2)) + req.log.debug('Presigned Url resp: ', JSON.stringify(resp.data, null, 2)); if (resp.status !== 200 || resp.data.result.status !== 200) { - return Promise.reject(new Error(resp.data.result.message)) + return Promise.reject(new Error(resp.data.result.message)); } // store deistination path & url - const destinationUri = `s3://${config.get('attachmentsS3Bucket')}/${filePath}` - const sourceUri = `s3://${data.s3Bucket}/${data.filePath}` - req.log.debug('Moving s3 file') - return util.s3FileTransfer(req, sourceUri, destinationUri) + const destinationUri = `s3://${config.get('attachmentsS3Bucket')}/${filePath}`; + const sourceUri = `s3://${data.s3Bucket}/${data.filePath}`; + req.log.debug('Moving s3 file'); + return util.s3FileTransfer(req, sourceUri, destinationUri); }) .then(() => { // file copied to final destination, create DB record - req.log.debug('creating db record') + req.log.debug('creating db record'); return models.ProjectAttachment .create({ - projectId: projectId, + projectId, createdBy: req.authUser.userId, updatedBy: req.authUser.userId, title: data.title, @@ -97,36 +97,35 @@ module.exports = [ category: data.category || null, description: data.description, contentType: data.contentType, - filePath: filePath - }) + filePath, + }); }) .then((_newAttachment) => { - newAttachment = _newAttachment.get({plain: true}) - req.log.debug('New Attachment record: ', newAttachment) + newAttachment = _newAttachment.get({ plain: true }); + req.log.debug('New Attachment record: ', newAttachment); // retrieve download url for the response - req.log.debug('retrieving download url') - return httpClient.post(fileServiceUrl + 'downloadurl', { + req.log.debug('retrieving download url'); + return httpClient.post(`${fileServiceUrl}downloadurl`, { param: { - filePath: filePath - } - }) + filePath, + }, + }); }) .then((resp) => { - req.log.debug('Retreiving Presigned Url resp: ', JSON.stringify(resp.data, null, 2)) + req.log.debug('Retreiving Presigned Url resp: ', JSON.stringify(resp.data, null, 2)); if (resp.status !== 200 || resp.data.result.status !== 200) { - return Promise.reject(new Error("Unable to fetch pre-signed url")) + return Promise.reject(new Error('Unable to fetch pre-signed url')); } - let response = _.cloneDeep(newAttachment) - response = _.omit(response, ['filePath', 'deletedAt']) + let response = _.cloneDeep(newAttachment); + response = _.omit(response, ['filePath', 'deletedAt']); - response.downloadUrl = resp.data.result.content.preSignedURL - res.status(201).json(util.wrapResponse(req.id, response, 1, 201)) + response.downloadUrl = resp.data.result.content.preSignedURL; + res.status(201).json(util.wrapResponse(req.id, response, 1, 201)); }) - .catch(function(err) { - req.log.error("Error adding attachment", err) - err.status = err.status || 500 - next(err) - }) - - } -] + .catch((err) => { + req.log.error('Error adding attachment', err); + err.status = err.status || 500; + next(err); + }); + }, +]; diff --git a/src/routes/attachments/create.spec.js b/src/routes/attachments/create.spec.js index 8df5a184..98617cb2 100644 --- a/src/routes/attachments/create.spec.js +++ b/src/routes/attachments/create.spec.js @@ -1,26 +1,26 @@ -'use strict' -import chai from 'chai' -import sinon from 'sinon' -import request from 'supertest' -import server from '../../app' -import models from '../../models' -import util from '../../util' -import testUtil from '../../tests/util' +import chai from 'chai'; +import sinon from 'sinon'; +import request from 'supertest'; -var should = chai.should() +import server from '../../app'; +import models from '../../models'; +import util from '../../util'; +import testUtil from '../../tests/util'; -var body = { - title: "Spec.pdf", - description: "", +const should = chai.should(); + +const body = { + title: 'Spec.pdf', + description: '', category: 'appDefinition', - filePath: "projects/1/spec.pdf", - s3Bucket: "submissions-staging-dev", - contentType: "application/pdf" -} + filePath: 'projects/1/spec.pdf', + s3Bucket: 'submissions-staging-dev', + contentType: 'application/pdf', +}; describe('Project Attachments', () => { - var project1 - before(done => { + let project1; + before((done) => { // mocks testUtil.clearDb() .then(() => { @@ -32,9 +32,9 @@ describe('Project Attachments', () => { status: 'draft', details: {}, createdBy: 1, - updatedBy: 1 - }).then(p => { - project1 = p + updatedBy: 1, + }).then((p) => { + project1 = p; // create members models.ProjectMember.create({ userId: 40051332, @@ -42,103 +42,94 @@ describe('Project Attachments', () => { role: 'copilot', isPrimary: true, createdBy: 1, - updatedBy: 1 - }).then(() => done()) - - }) - }) - }) + updatedBy: 1, + }).then(() => done()); + }); + }); + }); - after(done => { - testUtil.clearDb(done) - }) + after((done) => { + testUtil.clearDb(done); + }); describe('POST /projects/{id}/attachments/', () => { - it('should return 403 if user does not have permissions', done => { + it('should return 403 if user does not have permissions', (done) => { request(server) - .post('/v4/projects/' + project1.id + '/attachments/') + .post(`/v4/projects/${project1.id}/attachments/`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.member + Authorization: `Bearer ${testUtil.jwts.member}`, }) .send({ param: body }) .expect('Content-Type', /json/) - .expect(403, done) - }) + .expect(403, done); + }); - it('should return 201 return attachment record', done => { - var mockHttpClient = { + it('should return 201 return attachment record', (done) => { + const mockHttpClient = { defaults: { headers: { common: {} } }, - post: () => { - return new Promise((resolve) => { - return resolve({ + post: () => new Promise(resolve => resolve({ + status: 200, + data: { + status: 200, + result: { + success: true, status: 200, - data: { - status: 200, - result: { - success: true, - status: 200, - content: { - filePath: "tmp/spec.pdf", - preSignedURL: "www.topcoder.com/media/spec.pdf" - } - } - } - }) - }) - }, - get: () => { - return new Promise((resolve) => { - return resolve({ + content: { + filePath: 'tmp/spec.pdf', + preSignedURL: 'www.topcoder.com/media/spec.pdf', + }, + }, + }, + })), + get: () => new Promise(resolve => resolve({ + status: 200, + data: { + result: { + success: true, status: 200, - data: { - result: { - success: true, - status: 200, - content: { - filePath: "tmp/spec.pdf", - preSignedURL: "http://topcoder-media.s3.amazon.com/projects/1/spec.pdf" - } - } - } - }) - }) - } - } - var postSpy = sinon.spy(mockHttpClient, 'post') - var getSpy = sinon.spy(mockHttpClient, 'get') - var stub = sinon.stub(util, 'getHttpClient', () => { return mockHttpClient } ) + content: { + filePath: 'tmp/spec.pdf', + preSignedURL: 'http://topcoder-media.s3.amazon.com/projects/1/spec.pdf', + }, + }, + }, + })), + }; + const postSpy = sinon.spy(mockHttpClient, 'post'); + const getSpy = sinon.spy(mockHttpClient, 'get'); + const stub = sinon.stub(util, 'getHttpClient', () => mockHttpClient); // mock util s3FileTransfer util.s3FileTransfer = (req, source, dest) => { - console.log(source, dest) - return Promise.resolve(true) - } + console.log(source, dest); + return Promise.resolve(true); + }; request(server) - .post('/v4/projects/' + project1.id + '/attachments/') + .post(`/v4/projects/${project1.id}/attachments/`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) .send({ param: body }) .expect('Content-Type', /json/) .expect(201) - .end(function(err, res) { + .end((err, res) => { if (err) { - console.log(err) - return done(err) + console.log(err); + return done(err); } - var resJson = res.body.result.content - should.exist(resJson) + 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) - done() - }) - }) - }) -}) + 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); + done(); + }); + }); + }); +}); diff --git a/src/routes/attachments/delete.js b/src/routes/attachments/delete.js index f4d7ede7..a76b918e 100644 --- a/src/routes/attachments/delete.js +++ b/src/routes/attachments/delete.js @@ -1,48 +1,47 @@ -'use strict' + // import validate from 'express-validation' -import _ from 'lodash' +import _ from 'lodash'; -import models from '../../models' -import fileService from '../../services/fileService' +import models from '../../models'; +import fileService from '../../services/fileService'; import { - middleware as tcMiddleware -} from 'tc-core-library-js' + middleware as tcMiddleware, +} from 'tc-core-library-js'; /** * API to delete a project member. * */ -const permissions = tcMiddleware.permissions +const permissions = tcMiddleware.permissions; module.exports = [ permissions('project.removeAttachment'), (req, res, next) => { - var projectId = _.parseInt(req.params.projectId) - var attachmentId = _.parseInt(req.params.id) + const projectId = _.parseInt(req.params.projectId); + const attachmentId = _.parseInt(req.params.id); - models.sequelize.transaction(() => { + models.sequelize.transaction(() => // soft delete the record - return models.ProjectAttachment.findOne({ - where: { - id: attachmentId, - projectId: projectId - } - }) + models.ProjectAttachment.findOne({ + where: { + id: attachmentId, + projectId, + }, + }) .then((attachment) => { if (!attachment) { - let err = new Error('Record not found') - err.status = 404 - return Promise.reject(err) + const err = new Error('Record not found'); + err.status = 404; + return Promise.reject(err); } - return attachment.destroy() + return attachment.destroy(); }) .then((attachment) => { - fileService.deleteFile(req, attachment.filePath) + fileService.deleteFile(req, attachment.filePath); }) .then(() => res.status(204).json({})) - .catch((err) => next(err)) - }) - } -] + .catch(err => next(err))); + }, +]; diff --git a/src/routes/attachments/delete.spec.js b/src/routes/attachments/delete.spec.js index a4aa1cff..8634f941 100644 --- a/src/routes/attachments/delete.spec.js +++ b/src/routes/attachments/delete.spec.js @@ -1,18 +1,20 @@ -'use strict' -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' +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 Attachments delete', () => { - var project1, member1, attachment - beforeEach(done => { + let project1, + member1, + attachment; + beforeEach((done) => { testUtil.clearDb() .then(() => { models.Project.create({ @@ -24,9 +26,9 @@ describe('Project Attachments delete', () => { status: 'draft', details: {}, createdBy: 1, - updatedBy: 1 - }).then(p => { - project1 = p + updatedBy: 1, + }).then((p) => { + project1 = p; // create members return models.ProjectMember.create({ userId: 40051332, @@ -34,9 +36,9 @@ describe('Project Attachments delete', () => { role: 'copilot', isPrimary: true, createdBy: 1, - updatedBy: 1 + updatedBy: 1, }).then((pm) => { - member1 = pm + member1 = pm; return models.ProjectAttachment.create({ projectId: project1.id, title: 'test.txt', @@ -46,51 +48,51 @@ describe('Project Attachments delete', () => { category: null, filePath: 'https://media.topcoder.com/projects/1/test.txt', createdBy: 1, - updatedBy: 1 + updatedBy: 1, }).then((a1) => { - attachment = a1 - done() - }) - }) - }) - }) - }) + attachment = a1; + done(); + }); + }); + }); + }); + }); - after(done => { - testUtil.clearDb(done) - }) + after((done) => { + testUtil.clearDb(done); + }); describe('DELETE /projects/{id}/attachments/{id}', () => { - var sandbox + let sandbox; beforeEach(() => { - sandbox = sinon.sandbox.create() - }) + sandbox = sinon.sandbox.create(); + }); afterEach(() => { - sandbox.restore() - }) + sandbox.restore(); + }); - it('should return 403 if user does not have permissions', done => { + it('should return 403 if user does not have permissions', (done) => { request(server) - .delete('/v4/projects/' + project1.id + '/attachments/' + attachment.id) + .delete(`/v4/projects/${project1.id}/attachments/${attachment.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.member + Authorization: `Bearer ${testUtil.jwts.member}`, }) - .send({ param: {userId: 1, projectId: project1.id, role: 'customer'}}) - .expect(403, done) - }) + .send({ param: { userId: 1, projectId: project1.id, role: 'customer' } }) + .expect(403, done); + }); - it('should return 404 if attachment was not found', done => { + it('should return 404 if attachment was not found', (done) => { request(server) - .delete('/v4/projects/' + project1.id + '/attachments/8888888') + .delete(`/v4/projects/${project1.id}/attachments/8888888`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) - .send({ param: {userId: 1, projectId: project1.id, role: 'customer'}}) - .expect(404, done) - }) + .send({ param: { userId: 1, projectId: project1.id, role: 'customer' } }) + .expect(404, done); + }); - it('should return 204 if attachment was successfully removed', done => { - var mockHttpClient = _.merge(testUtil.mockHttpClient, { + it('should return 204 if attachment was successfully removed', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { delete: () => Promise.resolve({ status: 200, data: { @@ -99,25 +101,25 @@ describe('Project Attachments delete', () => { result: { success: true, status: 200, - content: true - } - } - }) - }) - var deleteSpy = sinon.spy(mockHttpClient, 'delete') - sandbox.stub(util, 'getHttpClient', () => mockHttpClient ) + content: true, + }, + }, + }), + }); + const deleteSpy = sinon.spy(mockHttpClient, 'delete'); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .delete('/v4/projects/' + project1.id + '/attachments/' + attachment.id) + .delete(`/v4/projects/${project1.id}/attachments/${attachment.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) .expect(204) - .end(err => { + .end((err) => { if (err) { - return done(err) + return done(err); } - deleteSpy.should.have.been.calledOnce - done() + deleteSpy.should.have.been.calledOnce; + done(); // models.ProjectAttachment // .count({}) // .then(count=>{ @@ -125,9 +127,7 @@ describe('Project Attachments delete', () => { // done() // }) // .catch(err => done(err)) - - }) - }) - - }) -}) + }); + }); + }); +}); diff --git a/src/routes/attachments/update.js b/src/routes/attachments/update.js index 8e150a2a..3cb7e40e 100644 --- a/src/routes/attachments/update.js +++ b/src/routes/attachments/update.js @@ -1,27 +1,27 @@ -'use strict' -import validate from 'express-validation' -import _ from 'lodash' -import Joi from 'joi' -import models from '../../models' -import util from '../../util' +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' + middleware as tcMiddleware, +} from 'tc-core-library-js'; /** * API to update a project member. */ -const permissions = tcMiddleware.permissions +const permissions = tcMiddleware.permissions; const updateProjectAttachmentValidation = { body: { param: Joi.object().keys({ title: Joi.string().required(), - description: Joi.string().optional().allow(null).allow('') - }) - } -} + description: Joi.string().optional().allow(null).allow(''), + }), + }, +}; module.exports = [ // handles request validations @@ -31,33 +31,31 @@ module.exports = [ * Update a attachment if the user has access */ (req, res, next) => { - let updatedProps = req.body.param - const projectId = _.parseInt(req.params.projectId) - const attachmentId = _.parseInt(req.params.id) - updatedProps.updatedBy = req.authUser.userId + const updatedProps = req.body.param; + const projectId = _.parseInt(req.params.projectId); + const attachmentId = _.parseInt(req.params.id); + updatedProps.updatedBy = req.authUser.userId; - models.sequelize.transaction(() => { - return models.ProjectAttachment.update(updatedProps, { - where: { - id: attachmentId, - projectId: projectId - }, - returning: true - }) - .then(resp => { - const affectedCount = resp.shift() + models.sequelize.transaction(() => models.ProjectAttachment.update(updatedProps, { + where: { + id: attachmentId, + projectId, + }, + returning: true, + }) + .then((resp) => { + const affectedCount = resp.shift(); if (affectedCount == 0) { // handle 404 - let err = new Error(`project attachment not found for project id ${projectId} and member id ${attachmentId}`) - err.status = 404 - return Promise.reject(err) + const err = new Error(`project attachment not found for project id ${projectId} and member id ${attachmentId}`); + err.status = 404; + return Promise.reject(err); } - const attachment = resp.shift()[0] - req.log.debug('updated project attachment', JSON.stringify(attachment, null, 2)) - res.json(util.wrapResponse(req.id, attachment)) + const attachment = resp.shift()[0]; + req.log.debug('updated project attachment', JSON.stringify(attachment, null, 2)); + res.json(util.wrapResponse(req.id, attachment)); }) - .catch((err) => next(err)) - }) - } -] + .catch(err => next(err))); + }, +]; diff --git a/src/routes/attachments/update.spec.js b/src/routes/attachments/update.spec.js index dd8ad87f..abcf17fe 100644 --- a/src/routes/attachments/update.spec.js +++ b/src/routes/attachments/update.spec.js @@ -1,18 +1,20 @@ -'use strict' -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' +import chai from 'chai'; +import sinon from 'sinon'; +import request from 'supertest'; -var should = chai.should() +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', () => { - var project1, member1, attachment - beforeEach(done => { + let project1, + member1, + attachment; + beforeEach((done) => { testUtil.clearDb() .then(() => { models.Project.create({ @@ -24,9 +26,9 @@ describe('Project Attachments update', () => { status: 'draft', details: {}, createdBy: 1, - updatedBy: 1 - }).then(p => { - project1 = p + updatedBy: 1, + }).then((p) => { + project1 = p; // create members return models.ProjectMember.create({ userId: 40051332, @@ -34,9 +36,9 @@ describe('Project Attachments update', () => { role: 'copilot', isPrimary: true, createdBy: 1, - updatedBy: 1 + updatedBy: 1, }).then((pm) => { - member1 = pm + member1 = pm; return models.ProjectAttachment.create({ projectId: project1.id, title: 'test.txt', @@ -46,68 +48,67 @@ describe('Project Attachments update', () => { category: null, filePath: 'https://media.topcoder.com/projects/1/test.txt', createdBy: 1, - updatedBy: 1 + updatedBy: 1, }).then((a1) => { - attachment = a1 - done() - }) - }) - }) - }) - }) + attachment = a1; + done(); + }); + }); + }); + }); + }); - after(done => { - testUtil.clearDb(done) - }) + after((done) => { + testUtil.clearDb(done); + }); describe('Update /projects/{id}/attachments/{id}', () => { - var sandbox + let sandbox; beforeEach(() => { - sandbox = sinon.sandbox.create() - }) + sandbox = sinon.sandbox.create(); + }); afterEach(() => { - sandbox.restore() - }) + sandbox.restore(); + }); - it('should return 403 if user does not have permissions', done => { + it('should return 403 if user does not have permissions', (done) => { request(server) - .patch('/v4/projects/' + project1.id + '/attachments/' + attachment.id) + .patch(`/v4/projects/${project1.id}/attachments/${attachment.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.member + Authorization: `Bearer ${testUtil.jwts.member}`, }) - .send({ param: {title: 'updated title', description: 'updated description'}}) - .expect(403, done) - }) + .send({ param: { title: 'updated title', description: 'updated description' } }) + .expect(403, done); + }); - it('should return 404 if attachment was not found', done => { + it('should return 404 if attachment was not found', (done) => { request(server) - .patch('/v4/projects/' + project1.id + '/attachments/8888888') + .patch(`/v4/projects/${project1.id}/attachments/8888888`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) - .send({ param: {title: 'updated title', description: 'updated description'}}) - .expect(404, done) - }) + .send({ param: { title: 'updated title', description: 'updated description' } }) + .expect(404, done); + }); - it('should return 200 if attachment was successfully updated', done => { + it('should return 200 if attachment was successfully updated', (done) => { request(server) - .patch('/v4/projects/' + project1.id + '/attachments/' + attachment.id) + .patch(`/v4/projects/${project1.id}/attachments/${attachment.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) - .send({ param: {title: 'updated title', description: 'updated description'}}) + .send({ param: { title: 'updated title', description: 'updated description' } }) .expect(200) .end((err, res) => { if (err) { - return done(err) + return done(err); } - const resJson = res.body.result.content - should.exist(resJson) - resJson.title.should.equal('updated title') - resJson.description.should.equal('updated description') - done() - }) - }) - - }) -}) + const resJson = res.body.result.content; + should.exist(resJson); + resJson.title.should.equal('updated title'); + resJson.description.should.equal('updated description'); + done(); + }); + }); + }); +}); diff --git a/src/routes/index.js b/src/routes/index.js index 4891e001..8889c61b 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,94 +1,94 @@ -'use strict' -import _ from 'lodash' -import validate from 'express-validation' -import { Router } from 'express' -const router = Router() +import _ from 'lodash'; +import validate from 'express-validation'; +import { Router } from 'express'; + +const router = Router(); validate.options({ status: 422, flatten: true, - allowUnknownBody: false -}) + allowUnknownBody: false, +}); // health check router.get('/_health', (req, res) => { // TODO more checks res.status(200).send({ - message: "All-is-well" - }) -}) + message: 'All-is-well', + }); +}); // All project service endpoints need authentication -var jwtAuth = require('tc-core-library-js').middleware.jwtAuthenticator -router.all('/v4/projects*', jwtAuth()) +const jwtAuth = require('tc-core-library-js').middleware.jwtAuthenticator; +router.all('/v4/projects*', jwtAuth()); // Register all the routes router.route('/v4/projects') .post(require('./projects/create')) - .get(require('./projects/list')) + .get(require('./projects/list')); router.route('/v4/projects/:projectId(\\d+)') .get(require('./projects/get')) .patch(require('./projects/update')) - .delete(require('./projects/delete')) + .delete(require('./projects/delete')); router.route('/v4/projects/:projectId(\\d+)/members') - .post(require('./projectMembers/create')) + .post(require('./projectMembers/create')); router.route('/v4/projects/:projectId(\\d+)/members/:id(\\d+)') .delete(require('./projectMembers/delete')) - .patch(require('./projectMembers/update')) + .patch(require('./projectMembers/update')); router.route('/v4/projects/:projectId(\\d+)/attachments') - .post(require('./attachments/create')) + .post(require('./attachments/create')); router.route('/v4/projects/:projectId(\\d+)/attachments/:id(\\d+)') .patch(require('./attachments/update')) - .delete(require('./attachments/delete')) + .delete(require('./attachments/delete')); // register error handler router.use((err, req, res, next) => { // eslint-disable-line no-unused-vars // DO NOT REMOVE next arg.. even though eslint // complains that it is not being used. - let content = {} - let httpStatus = err.status || 500 + const content = {}; + let httpStatus = err.status || 500; // specific for validation errors if (err instanceof validate.ValidationError) { - content.message = err.message + ": " + err.toJSON() - httpStatus = err.status + content.message = `${err.message}: ${err.toJSON()}`; + httpStatus = err.status; } else { - content.message = err.message + content.message = err.message; } - var body = { + const body = { id: req.id, result: { success: false, status: httpStatus, - content: content - } - } + content, + }, + }; // dvalidateelopment error handler // will print stacktrace if (_.indexOf(['development', 'test', 'qa'], process.env.NODE_ENV) > -1) { - body.result.debug = err.stack + body.result.debug = err.stack; if (err.details) { - body.result.details = err.details + body.result.details = err.details; } } - err.status = err.status || 500 - req.log.error(err) + err.status = err.status || 500; + req.log.error(err); res .status(err.status) - .send(body) -}) + .send(body); +}); // catch 404 and forward to error handler router.use((req, res, next) => { - var err = new Error('Not Found') - err.status = 404 - next(err) -}) + const err = new Error('Not Found'); + err.status = 404; + next(err); +}); -module.exports = router +module.exports = router; diff --git a/src/routes/projectMembers/create.js b/src/routes/projectMembers/create.js index 94f7c884..805728f6 100644 --- a/src/routes/projectMembers/create.js +++ b/src/routes/projectMembers/create.js @@ -1,80 +1,76 @@ -'use strict' -import validate from 'express-validation' -import _ from 'lodash' -import Joi from 'joi' -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 validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; +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'; /** * API to add a project member. * */ -const permissions = tcMiddleware.permissions +const permissions = tcMiddleware.permissions; const addMemberValidations = { body: { 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() - }).required() - } -} + role: Joi.any().valid(PROJECT_MEMBER_ROLE.CUSTOMER, PROJECT_MEMBER_ROLE.MANAGER, PROJECT_MEMBER_ROLE.COPILOT).required(), + }).required(), + }, +}; module.exports = [ // handles request validations validate(addMemberValidations), permissions('project.addMember'), (req, res, next) => { - var member = req.body.param - var projectId = _.parseInt(req.params.projectId) + const member = req.body.param; + const projectId = _.parseInt(req.params.projectId); // set defaults _.assign(member, { - projectId: projectId, + projectId, createdBy: req.authUser.userId, - updatedBy: req.authUser.userId - }) - let members = req.context.currentProjectMembers + updatedBy: req.authUser.userId, + }); + const members = req.context.currentProjectMembers; // check if member is already registered - let existingMember = _.find(members, (m) => { - return m.userId === member.userId - }) + const existingMember = _.find(members, m => m.userId === member.userId); if (existingMember) { - let err = new Error('User already registered for role: ' + existingMember.role) - err.status = 400 - return next(err) + const err = new Error(`User already registered for role: ${existingMember.role}`); + err.status = 400; + return next(err); } // check if another member is registered for this role as primary, // if not mark this member as primary if (_.isUndefined(member.isPrimary)) { - member.isPrimary = _.isUndefined(_.find(members, (m) => { - return m.isPrimary && m.role === member.role - })) + member.isPrimary = _.isUndefined(_.find(members, m => m.isPrimary && m.role === member.role)); } - req.log.debug('creating member', member) - let newMember = null + req.log.debug('creating member', member); + let newMember = null; // register member return models.ProjectMember.create(member) - .then(_newMember => { - newMember = _newMember.get({plain: true}) + .then((_newMember) => { + newMember = _newMember.get({ plain: true }); // publish event req.app.services.pubsub.publish( EVENT.ROUTING_KEY.PROJECT_MEMBER_ADDED, newMember, - { correlationId: req.id } - ) - req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_ADDED, { req, member: newMember }) - res.status(201).json(util.wrapResponse(req.id, newMember, 1, 201)) + { correlationId: req.id }, + ); + req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_ADDED, { req, member: newMember }); + res.status(201).json(util.wrapResponse(req.id, newMember, 1, 201)); }) .catch((err) => { - req.log.error('Unable to register ', err) - next(err) - }) - } -] + req.log.error('Unable to register ', err); + next(err); + }); + }, +]; diff --git a/src/routes/projectMembers/create.spec.js b/src/routes/projectMembers/create.spec.js index a1d315a8..bd25f5fe 100644 --- a/src/routes/projectMembers/create.spec.js +++ b/src/routes/projectMembers/create.spec.js @@ -1,19 +1,20 @@ -'use strict' -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' +import _ from 'lodash'; +import chai from 'chai'; +import sinon from 'sinon'; +import request from 'supertest'; -var should = chai.should() +import models from '../../models'; +import util from '../../util'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const should = chai.should(); describe('Project Members create', () => { - var project1, project2 - before(done => { + let project1, + project2; + before((done) => { testUtil.clearDb() .then(() => { models.Project.create({ @@ -25,9 +26,9 @@ describe('Project Members create', () => { status: 'draft', details: {}, createdBy: 1, - updatedBy: 1 - }).then(p => { - project1 = p + updatedBy: 1, + }).then((p) => { + project1 = p; // create members models.ProjectMember.create({ userId: 40051332, @@ -35,8 +36,8 @@ describe('Project Members create', () => { role: 'copilot', isPrimary: true, createdBy: 1, - updatedBy: 1 - }) + updatedBy: 1, + }); }).then(() => models.Project.create({ type: 'generic', @@ -46,103 +47,103 @@ describe('Project Members create', () => { status: 'reviewed', details: {}, createdBy: 1, - updatedBy: 1 - }).then(p2 => { - project2 = p2 - done() - })) - }) - }) + updatedBy: 1, + }).then((p2) => { + project2 = p2; + done(); + })); + }); + }); - after(done => { - testUtil.clearDb(done) - }) + after((done) => { + testUtil.clearDb(done); + }); describe('POST /projects/{id}/members/', () => { - var sandbox + let sandbox; beforeEach(() => { - sandbox = sinon.sandbox.create() - }) + sandbox = sinon.sandbox.create(); + }); afterEach(() => { - sandbox.restore() - }) + sandbox.restore(); + }); - it('should return 403 if user does not have permissions', done => { + it('should return 403 if user does not have permissions', (done) => { request(server) - .post('/v4/projects/' + project1.id + '/members/') + .post(`/v4/projects/${project1.id}/members/`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.member + Authorization: `Bearer ${testUtil.jwts.member}`, }) - .send({ param: {userId: 1, role: 'customer'}}) + .send({ param: { userId: 1, role: 'customer' } }) .expect('Content-Type', /json/) - .expect(403, done) - }) + .expect(403, done); + }); - it('should return 400 if user is already registered', done => { + it('should return 400 if user is already registered', (done) => { request(server) - .post('/v4/projects/' + project1.id + '/members/') + .post(`/v4/projects/${project1.id}/members/`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.admin + Authorization: `Bearer ${testUtil.jwts.admin}`, }) - .send({ param: {userId: 40051332, role: 'customer'}}) + .send({ param: { userId: 40051332, role: 'customer' } }) .expect('Content-Type', /json/) .expect(400) .end((err, res) => { if (err) { - return done(err) + return done(err); } - res.body.result.status.should.equal(400) - done() - }) - }) + res.body.result.status.should.equal(400); + done(); + }); + }); - it('should return 201 and register copilot member for project', done => { + it('should return 201 and register copilot member for project', (done) => { request(server) - .post('/v4/projects/' + project2.id + '/members/') + .post(`/v4/projects/${project2.id}/members/`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) - .send({ param: {userId: 1, role: 'copilot'}}) + .send({ param: { userId: 1, role: 'copilot' } }) .expect('Content-Type', /json/) .expect(201) - .end(function(err, res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var resJson = res.body.result.content - should.exist(resJson) - resJson.role.should.equal('copilot') - resJson.isPrimary.should.be.truthy - resJson.projectId.should.equal(project2.id) - resJson.userId.should.equal(1) - server.services.pubsub.publish.calledWith('project.member.added').should.be.true - done() - }) - }) + const resJson = res.body.result.content; + should.exist(resJson); + resJson.role.should.equal('copilot'); + resJson.isPrimary.should.be.truthy; + resJson.projectId.should.equal(project2.id); + resJson.userId.should.equal(1); + server.services.pubsub.publish.calledWith('project.member.added').should.be.true; + done(); + }); + }); - it('should return 201 and register customer member', done => { + it('should return 201 and register customer member', (done) => { request(server) - .post('/v4/projects/' + project1.id + '/members/') + .post(`/v4/projects/${project1.id}/members/`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) - .send({ param: {userId: 1, role: 'customer'}}) + .send({ param: { userId: 1, role: 'customer' } }) .expect('Content-Type', /json/) .expect(201) - .end(function(err, res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var resJson = res.body.result.content - should.exist(resJson) - resJson.role.should.equal('customer') - resJson.isPrimary.should.be.truthy - resJson.projectId.should.equal(project1.id) - resJson.userId.should.equal(1) - server.services.pubsub.publish.calledWith('project.member.added').should.be.true - done() - }) - }) + const resJson = res.body.result.content; + should.exist(resJson); + resJson.role.should.equal('customer'); + resJson.isPrimary.should.be.truthy; + resJson.projectId.should.equal(project1.id); + resJson.userId.should.equal(1); + server.services.pubsub.publish.calledWith('project.member.added').should.be.true; + done(); + }); + }); /* // TODO this test is no logner valid since updating direct is async @@ -173,8 +174,8 @@ describe('Project Members create', () => { }) */ - it('should return 201 and register copilot member', done => { - var mockHttpClient = _.merge(testUtil.mockHttpClient, { + it('should return 201 and register copilot member', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { post: () => Promise.resolve({ status: 200, data: { @@ -184,37 +185,37 @@ describe('Project Members create', () => { success: true, status: 200, content: { - copilotProjectId: 2 - } - } - } - }) - }) - var postSpy = sinon.spy(mockHttpClient, 'post') + copilotProjectId: 2, + }, + }, + }, + }), + }); + const postSpy = sinon.spy(mockHttpClient, 'post'); // var amqPubSpy = sinon.spy(server.services.pubsub, 'publish') - sandbox.stub(util, 'getHttpClient', () => mockHttpClient ) + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .post('/v4/projects/' + project1.id + '/members/') + .post(`/v4/projects/${project1.id}/members/`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) - .send({ param: {userId: 3, role: 'copilot'}}) + .send({ param: { userId: 3, role: 'copilot' } }) .expect('Content-Type', /json/) .expect(201) - .end(function(err, res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var resJson = res.body.result.content - should.exist(resJson) - resJson.role.should.equal('copilot') - resJson.isPrimary.should.be.truthy - resJson.projectId.should.equal(project1.id) - resJson.userId.should.equal(3) - postSpy.should.have.been.calledOnce - server.services.pubsub.publish.calledWith('project.member.added').should.be.true - done() - }) - }) - }) -}) + const resJson = res.body.result.content; + should.exist(resJson); + resJson.role.should.equal('copilot'); + resJson.isPrimary.should.be.truthy; + resJson.projectId.should.equal(project1.id); + resJson.userId.should.equal(3); + postSpy.should.have.been.calledOnce; + server.services.pubsub.publish.calledWith('project.member.added').should.be.true; + done(); + }); + }); + }); +}); diff --git a/src/routes/projectMembers/delete.js b/src/routes/projectMembers/delete.js index b1387dd7..27e13f17 100644 --- a/src/routes/projectMembers/delete.js +++ b/src/routes/projectMembers/delete.js @@ -1,87 +1,82 @@ -'use strict' -import _ from 'lodash' -import models from '../../models' -import { middleware as tcMiddleware } from 'tc-core-library-js' -import { EVENT, PROJECT_MEMBER_ROLE } from '../../constants' +import _ from 'lodash'; + +import models from '../../models'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import { EVENT, PROJECT_MEMBER_ROLE } from '../../constants'; /** * API to delete a project member. * */ -const permissions = tcMiddleware.permissions +const permissions = tcMiddleware.permissions; module.exports = [ permissions('project.removeMember'), (req, res, next) => { - var projectId = _.parseInt(req.params.projectId) - var memberRecordId = _.parseInt(req.params.id) + const projectId = _.parseInt(req.params.projectId); + const memberRecordId = _.parseInt(req.params.id); - models.sequelize.transaction(() => { + models.sequelize.transaction(() => // soft delete the record - return models.ProjectMember.findOne({ - where: {id: memberRecordId, projectId: projectId} - }) + models.ProjectMember.findOne({ + where: { id: memberRecordId, projectId }, + }) .then((member) => { if (!member) { - let err = new Error('Record not found') - err.status = 404 - return Promise.reject(err) + const err = new Error('Record not found'); + err.status = 404; + return Promise.reject(err); } - return member.destroy({logging: console.log}) - }) - .then(member => { - return member.save() + return member.destroy({ logging: console.log }); }) + .then(member => member.save()) // if primary co-pilot is removed promote the next co-pilot to primary #43 - .then((member) => { - return new Promise((accept, reject) => { - if (member.role === PROJECT_MEMBER_ROLE.COPILOT && member.isPrimary) { + .then(member => new Promise((accept, reject) => { + if (member.role === PROJECT_MEMBER_ROLE.COPILOT && member.isPrimary) { // find the next copilot - models.ProjectMember.findAll({ - limit: 1, + models.ProjectMember.findAll({ + limit: 1, // return only non-deleted records - paranoid: true, - where: { - projectId: projectId, - role: PROJECT_MEMBER_ROLE.COPILOT - }, - order: [['createdAt', 'ASC']] - }).then((members) => { - if (members && members.length > 0) { + paranoid: true, + where: { + projectId, + role: PROJECT_MEMBER_ROLE.COPILOT, + }, + order: [['createdAt', 'ASC']], + }).then((members) => { + if (members && members.length > 0) { // mark the copilot as primary - const nextMember = members[0] - nextMember.set({ isPrimary: true }) - nextMember.save().then(() => { - accept(member) - }).catch((err) => { - reject(err) - }) - } else { + const nextMember = members[0]; + nextMember.set({ isPrimary: true }); + nextMember.save().then(() => { + accept(member); + }).catch((err) => { + reject(err); + }); + } else { // no copilot found nothing to do - accept(member) - } - }).catch((err) => { - reject(err) - }) - } else { + accept(member); + } + }).catch((err) => { + reject(err); + }); + } else { // nothing to do - accept(member) - } - }) - }) - }).then((member) => { + accept(member); + } + }))).then((member) => { // only return the response after transaction is committed // fire event - member = member.get({plain:true}) - req.app.services.pubsub.publish( + member = member.get({ plain: true }); + req.app.services.pubsub.publish( EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED, member, - { correlationId: req.id } - ) - req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED, { req, member }) - res.status(204).json({}) - }).catch((err) => next(err)) - } -] + { correlationId: req.id }, + ); + req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED, { req, member }); + res.status(204).json({}); + }).catch(err => next(err)); + }, +]; diff --git a/src/routes/projectMembers/delete.spec.js b/src/routes/projectMembers/delete.spec.js index a0d17c16..d209e63c 100644 --- a/src/routes/projectMembers/delete.spec.js +++ b/src/routes/projectMembers/delete.spec.js @@ -1,19 +1,21 @@ -'use strict' -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' +import _ from 'lodash'; +import chai from 'chai'; +import sinon from 'sinon'; +import request from 'supertest'; -var should = chai.should() +import models from '../../models'; +import util from '../../util'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const should = chai.should(); describe('Project members delete', () => { - var project1, member1, member2 - beforeEach(done => { + let project1, + member1, + member2; + beforeEach((done) => { testUtil.clearDb() .then(() => { models.Project.create({ @@ -25,9 +27,9 @@ describe('Project members delete', () => { status: 'draft', details: {}, createdBy: 1, - updatedBy: 1 - }).then(p => { - project1 = p + updatedBy: 1, + }).then((p) => { + project1 = p; // create members return models.ProjectMember.create({ userId: 40051332, @@ -35,77 +37,77 @@ describe('Project members delete', () => { role: 'copilot', isPrimary: true, createdBy: 1, - updatedBy: 1 + updatedBy: 1, }).then((pm) => { - member1 = pm + member1 = pm; return models.ProjectMember.create({ userId: 40051334, projectId: project1.id, role: 'manager', isPrimary: true, createdBy: 1, - updatedBy: 1 + updatedBy: 1, }).then((pm2) => { - member2 = pm2 - done() - }) - }) - }) - }) - }) + member2 = pm2; + done(); + }); + }); + }); + }); + }); - after(done => { - testUtil.clearDb(done) - }) + after((done) => { + testUtil.clearDb(done); + }); describe('DELETE /projects/{id}/members/{id}', () => { - var sandbox + let sandbox; beforeEach(() => { - sandbox = sinon.sandbox.create() - }) + sandbox = sinon.sandbox.create(); + }); afterEach(() => { - sandbox.restore() - }) + sandbox.restore(); + }); - it('should return 403 if user does not have permissions', done => { + it('should return 403 if user does not have permissions', (done) => { request(server) - .delete('/v4/projects/' + project1.id + '/members/' + member1.id) + .delete(`/v4/projects/${project1.id}/members/${member1.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.member + Authorization: `Bearer ${testUtil.jwts.member}`, }) - .send({ param: {userId: 1, projectId: project1.id, role: 'customer'}}) - .expect(403, done) - }) + .send({ param: { userId: 1, projectId: project1.id, role: 'customer' } }) + .expect(403, done); + }); - it('should return 403 if user not found', done => { + it('should return 403 if user not found', (done) => { request(server) - .delete('/v4/projects/' + project1.id + '/members/8888888') + .delete(`/v4/projects/${project1.id}/members/8888888`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) - .send({ param: {userId: 1, projectId: project1.id, role: 'customer'}}) - .expect(403, done) - }) + .send({ param: { userId: 1, projectId: project1.id, role: 'customer' } }) + .expect(403, done); + }); - it('should return 204 if copilot user has access to the project', done => { + it('should return 204 if copilot user has access to the project', (done) => { request(server) - .delete('/v4/projects/' + project1.id + '/members/' + member1.id) + .delete(`/v4/projects/${project1.id}/members/${member1.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) .expect(204) - .end(function(err) { + .end((err) => { if (err) { - return done(err) + return done(err); } - var removedMember = { + const removedMember = { projectId: project1.id, userId: 40051332, role: 'copilot', - isPrimary: true - } - server.services.pubsub.publish.calledWith('project.member.removed', sinon.match(removedMember)).should.be.true - done() + isPrimary: 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 } }}) // .then(count=>{ @@ -114,11 +116,10 @@ describe('Project members delete', () => { // done() // }) // .catch(err=>done(err)) + }); + }); - }) - }) - - it('should return 204 if copilot is removed (promote the next copilot to primary)', done => { + it('should return 204 if copilot is removed (promote the next copilot to primary)', (done) => { models.ProjectMember.bulkCreate([{ userId: 40051331, projectId: project1.id, @@ -127,7 +128,7 @@ describe('Project members delete', () => { createdBy: 1, updatedBy: 1, createdAt: '2016-06-30 00:33:07+00', - updatedAt: '2016-06-30 00:33:07+00' + updatedAt: '2016-06-30 00:33:07+00', }, { userId: 40051333, projectId: project1.id, @@ -136,7 +137,7 @@ describe('Project members delete', () => { createdBy: 1, updatedBy: 1, createdAt: '2016-07-30 00:33:07+00', - updatedAt: '2016-07-30 00:33:07+00' + updatedAt: '2016-07-30 00:33:07+00', }, { userId: 40051335, projectId: project1.id, @@ -145,128 +146,128 @@ describe('Project members delete', () => { createdBy: 1, updatedBy: 1, createdAt: '2016-08-30 00:33:07+00', - updatedAt: '2016-08-30 00:33:07+00' + updatedAt: '2016-08-30 00:33:07+00', }]).then(() => { request(server) - .delete('/v4/projects/' + project1.id + '/members/' + member1.id) + .delete(`/v4/projects/${project1.id}/members/${member1.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) .expect(204) - .end(function(err) { + .end((err) => { if (err) { - return done(err) + return done(err); } - var removedMember = { + const removedMember = { projectId: project1.id, userId: 40051332, role: 'copilot', - isPrimary: true - } - server.services.pubsub.publish.calledWith('project.member.removed', sinon.match(removedMember)).should.be.true + isPrimary: 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) - const plain = members[0].get({ plain: true }) - plain.role.should.equal('copilot') - plain.isPrimary.should.equal(true) - plain.userId.should.equal(40051331) - done() - }) - }) - }) - }) + should.exist(members); + members.length.should.equal(1); + const plain = members[0].get({ plain: true }); + plain.role.should.equal('copilot'); + plain.isPrimary.should.equal(true); + plain.userId.should.equal(40051331); + done(); + }); + }); + }); + }); - it('should return 204 if manager is removed from the project', done => { - var mockHttpClient = _.merge(testUtil.mockHttpClient, { - post: () => Promise.resolve({ + it('should return 204 if manager is removed from the project', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + post: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, status: 200, - data: { - id: 'requesterId', - version: 'v3', - result: { - success: true, - status: 200, - content: { } - } - } - }) - }) - var postSpy = sinon.spy(mockHttpClient, 'post') - sandbox.stub(util, 'getHttpClient', () => mockHttpClient) + content: { }, + }, + }, + }), + }); + const postSpy = sinon.spy(mockHttpClient, 'post'); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .delete('/v4/projects/' + project1.id + '/members/' + member2.id) + .delete(`/v4/projects/${project1.id}/members/${member2.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.manager + Authorization: `Bearer ${testUtil.jwts.manager}`, }) .expect(204) - .end(function(err) { + .end((err) => { if (err) { - return done(err) + return done(err); } - var removedMember = { + const removedMember = { projectId: project1.id, userId: 40051334, role: 'manager', - isPrimary: true - } - server.services.pubsub.publish.calledWith('project.member.removed', sinon.match(removedMember)).should.be.true - postSpy.should.have.been.calledOnce - done() - }) - }) + isPrimary: true, + }; + server.services.pubsub.publish.calledWith('project.member.removed', sinon.match(removedMember)).should.be.true; + postSpy.should.have.been.calledOnce; + done(); + }); + }); - it('should return 204 if manager is removed from the project (without direct project id)', done => { - var mockHttpClient = _.merge(testUtil.mockHttpClient, { - post: () => Promise.resolve({ + it('should return 204 if manager is removed from the project (without direct project id)', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + post: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, status: 200, - data: { - id: 'requesterId', - version: 'v3', - result: { - success: true, - status: 200, - content: { } - } - } - }) - }) - var postSpy = sinon.spy(mockHttpClient, 'post') - sandbox.stub(util, 'getHttpClient', () => mockHttpClient) - models.Project.update({ directProjectId: null}, {where: {id: project1.id}}) + content: { }, + }, + }, + }), + }); + const postSpy = sinon.spy(mockHttpClient, 'post'); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + models.Project.update({ directProjectId: null }, { where: { id: project1.id } }) .then(() => { request(server) - .delete('/v4/projects/' + project1.id + '/members/' + member2.id) + .delete(`/v4/projects/${project1.id}/members/${member2.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.manager + Authorization: `Bearer ${testUtil.jwts.manager}`, }) .expect(204) - .end(function(err) { + .end((err) => { if (err) { - return done(err) + return done(err); } - var removedMember = { + const removedMember = { projectId: project1.id, userId: 40051334, role: 'manager', - isPrimary: true - } - server.services.pubsub.publish.calledWith('project.member.removed', sinon.match(removedMember)).should.be.true - postSpy.should.not.have.been.calledOnce - done() - }) - }) - }) + isPrimary: true, + }; + server.services.pubsub.publish.calledWith('project.member.removed', sinon.match(removedMember)).should.be.true; + postSpy.should.not.have.been.calledOnce; + done(); + }); + }); + }); - it('should return 403 if copilot user is trying to remove a manager', done => { + it('should return 403 if copilot user is trying to remove a manager', (done) => { request(server) - .delete('/v4/projects/' + project1.id + '/members/' + member2.id) + .delete(`/v4/projects/${project1.id}/members/${member2.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) - .expect(403, done) - }) - }) -}) + .expect(403, done); + }); + }); +}); diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index e1945184..ac89f67c 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -1,27 +1,27 @@ -'use strict' -import validate from 'express-validation' -import _ from 'lodash' -import Joi from 'joi' -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' +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; + +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. */ -const permissions = tcMiddleware.permissions +const permissions = tcMiddleware.permissions; 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(), + }), + }, +}; module.exports = [ // handles request validations @@ -32,55 +32,54 @@ module.exports = [ */ (req, res, next) => { let projectMember, - updatedProps = req.body.param - const projectId = _.parseInt(req.params.projectId) - const memberRecordId = _.parseInt(req.params.id) - updatedProps = _.pick(updatedProps, ['isPrimary', 'role']) + 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 - models.sequelize.transaction(() => { - return models.ProjectMember.findOne({ - where: { id: memberRecordId, projectId: projectId } - }) + let previousValue; + let newValue; + models.sequelize.transaction(() => models.ProjectMember.findOne({ + where: { id: memberRecordId, projectId }, + }) .then((_member) => { if (!_member) { // handle 404 - let err = new Error(`project member not found for project id ${projectId} and member id ${memberRecordId}`) - err.status = 404 - return Promise.reject(err) + const err = new Error(`project member not found for project id ${projectId} and member id ${memberRecordId}`); + err.status = 404; + return Promise.reject(err); } - projectMember = _member - previousValue = _.clone(projectMember.get({plain: true})) - _.assign(projectMember, updatedProps) - newValue = projectMember.get({plain: true}) + projectMember = _member; + previousValue = _.clone(projectMember.get({ plain: true })); + _.assign(projectMember, updatedProps); + newValue = projectMember.get({ plain: true }); // no updates if no change - if(updatedProps.role === previousValue.role && + if (updatedProps.role === previousValue.role && (_.isUndefined(updatedProps.isPrimary) || updatedProps.isPrimary === previousValue.isPrimary)) { - return Promise.resolve() + return Promise.resolve(); } - projectMember.updatedBy = req.authUser.userId - const operations = [] - operations.push(projectMember.save()) + projectMember.updatedBy = req.authUser.userId; + const operations = []; + operations.push(projectMember.save()); - if(updatedProps.isPrimary){ + if (updatedProps.isPrimary) { // if set as primary, other users with same role should no longer be primary operations.push(models.ProjectMember.update({ isPrimary: false, updatedBy: req.authUser.userId }, - { - where: { - projectId, - isPrimary: true, - role: updatedProps.role, - id: { - $ne: projectMember.id - } - } - })) + { + where: { + projectId, + isPrimary: true, + role: updatedProps.role, + id: { + $ne: projectMember.id, + }, + }, + })); } - return Promise.all(operations) + return Promise.all(operations); }) // .then(() => { // // TODO move this to an event @@ -112,18 +111,17 @@ module.exports = [ // }) .then(() => projectMember.reload(projectMember.id)) .then(() => { - projectMember = projectMember.get({plain: true}) - projectMember = _.omit(projectMember, ['deletedAt']) + projectMember = projectMember.get({ plain: true }); + projectMember = _.omit(projectMember, ['deletedAt']); // emit original and updated project information req.app.services.pubsub.publish( EVENT.ROUTING_KEY.PROJECT_MEMBER_UPDATED, { original: previousValue, updated: projectMember }, - { correlationId: req.id } - ) - req.log.debug('updated project member', projectMember) - res.json(util.wrapResponse(req.id, projectMember)) + { correlationId: req.id }, + ); + req.log.debug('updated project member', projectMember); + res.json(util.wrapResponse(req.id, projectMember)); }) - .catch((err) => next(err)) - }) - } -] + .catch(err => next(err))); + }, +]; diff --git a/src/routes/projectMembers/update.spec.js b/src/routes/projectMembers/update.spec.js index 8e2a149a..799a2d5d 100644 --- a/src/routes/projectMembers/update.spec.js +++ b/src/routes/projectMembers/update.spec.js @@ -1,423 +1,424 @@ -'use strict' -import _ from 'lodash' -import chai from 'chai' -import request from 'supertest' -import sinon from 'sinon' -import models from '../../models' -import server from '../../app' -import util from '../../util' -import testUtil from '../../tests/util' -const should = chai.should() +import _ from 'lodash'; +import chai from 'chai'; +import request from 'supertest'; +import sinon from 'sinon'; +import models from '../../models'; +import server from '../../app'; +import util from '../../util'; +import testUtil from '../../tests/util'; + +const should = chai.should(); describe('Project members update', () => { - let project1, member1, member2, member3 - beforeEach(done => { - testUtil.clearDb() + let project1, + member1, + member2, + member3; + beforeEach((done) => { + testUtil.clearDb() .then(() => { - models.Project.create({ - type: 'generic', - directProjectId: 1, - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - details: {}, - createdBy: 1, - updatedBy: 1 - }).then(p => { - project1 = p + models.Project.create({ + type: 'generic', + directProjectId: 1, + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + }).then((p) => { + project1 = p; // create members + models.ProjectMember.create({ + userId: 40051334, + projectId: project1.id, + role: 'manager', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', + }).then((pm) => { + member1 = pm.get({ plain: true }); + models.ProjectMember.create({ + userId: 40051332, + projectId: project1.id, + role: 'copilot', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', + }).then((pm2) => { + member2 = pm2.get({ plain: true }); models.ProjectMember.create({ - userId: 40051334, - projectId: project1.id, - role: 'manager', - isPrimary: false, - createdBy: 1, - updatedBy: 1, - createdAt: "2016-06-30 00:33:07+00", - updatedAt: "2016-06-30 00:33:07+00" - }).then((pm) => { - member1 = pm.get({ plain: true }) - models.ProjectMember.create({ - userId: 40051332, - projectId: project1.id, - role: 'copilot', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - createdAt: "2016-06-30 00:33:07+00", - updatedAt: "2016-06-30 00:33:07+00" - }).then((pm2) => { - member2 = pm2.get({plain: true}) - models.ProjectMember.create({ - userId: 40051330, - projectId: project1.id, - role: 'copilot', - isPrimary: false, - createdBy: 1, - updatedBy: 1, - createdAt: "2016-06-30 00:33:07+00", - updatedAt: "2016-06-30 00:33:07+00" - }).then((pm3) => { - member3 = pm3.get({ plain: true }) - done() - }) - }) - }) - }) - }) - }) + userId: 40051330, + projectId: project1.id, + role: 'copilot', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', + }).then((pm3) => { + member3 = pm3.get({ plain: true }); + done(); + }); + }); + }); + }); + }); + }); - after(done => { - testUtil.clearDb(done) - }) + after((done) => { + testUtil.clearDb(done); + }); - describe('PUT /projects/{id}/members/{id}', () => { - const body = { - param: { - "role": "manager", - "isPrimary": false - } - } + describe('PUT /projects/{id}/members/{id}', () => { + const body = { + param: { + role: 'manager', + isPrimary: false, + }, + }; - var sandbox - beforeEach(() => { - sandbox = sinon.sandbox.create() - }) - afterEach(() => { - sandbox.restore() - }) + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + afterEach(() => { + sandbox.restore(); + }); - it('should return 403 if user does not have permissions', done => { - request(server) - .patch('/v4/projects/' + project1.id + '/members/' + member2.id) + it('should return 403 if user does not have permissions', (done) => { + request(server) + .patch(`/v4/projects/${project1.id}/members/${member2.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.member + Authorization: `Bearer ${testUtil.jwts.member}`, }) .send(body) - .expect(403, done) - }) + .expect(403, done); + }); - it('should return 422 if no role', done => { - request(server) - .patch('/v4/projects/' + project1.id + '/members/' + member2.id) + it('should return 422 if no role', (done) => { + request(server) + .patch(`/v4/projects/${project1.id}/members/${member2.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.admin + Authorization: `Bearer ${testUtil.jwts.admin}`, }) .send({ param: {} }) - .expect(422, done) - }) + .expect(422, done); + }); - it('should return 422 if role is invalid', done => { - request(server) - .patch('/v4/projects/' + project1.id + '/members/' + member2.id) + it('should return 422 if role is invalid', (done) => { + request(server) + .patch(`/v4/projects/${project1.id}/members/${member2.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.member + Authorization: `Bearer ${testUtil.jwts.member}`, }) .send({ - param: { - "role": "wrong" - } + param: { + role: 'wrong', + }, }) - .expect(422, done) - }) + .expect(422, done); + }); - it('should return 422 if isPrimary is invalid', done => { - request(server) - .patch('/v4/projects/' + project1.id + '/members/' + member2.id) + it('should return 422 if isPrimary is invalid', (done) => { + request(server) + .patch(`/v4/projects/${project1.id}/members/${member2.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.member + Authorization: `Bearer ${testUtil.jwts.member}`, }) .send({ - param: { - "isPrimary": "wrong" - } + param: { + isPrimary: 'wrong', + }, }) - .expect(422, done) - }) + .expect(422, done); + }); - it('should return 404 if not exist id', done => { - request(server) - .patch('/v4/projects/' + project1.id + '/members/999999') + it('should return 404 if not exist id', (done) => { + request(server) + .patch(`/v4/projects/${project1.id}/members/999999`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) .send(body) .expect('Content-Type', /json/) .expect(404) - .end(function (err, res) { - if (err) { - return done(err) - } - 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`) - done() - }) - }) + .end((err, res) => { + if (err) { + return done(err); + } + 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`); + done(); + }); + }); - it('should return 200 if valid user and data(no isPrimary and no updates)', done => { - request(server) - .patch('/v4/projects/' + project1.id + '/members/' + member2.id) + it('should return 200 if valid user and data(no isPrimary and no updates)', (done) => { + request(server) + .patch(`/v4/projects/${project1.id}/members/${member2.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) .send({ - param: { - "role": "customer" - } + param: { + role: 'customer', + }, }) .expect('Content-Type', /json/) .expect(200) - .end(function (err, res) { - if (err) { - return done(err) - } - const resJson = res.body.result.content - should.exist(resJson) - resJson.role.should.equal('customer') - resJson.isPrimary.should.be.true - resJson.updatedBy.should.equal(40051332) - server.services.pubsub.publish.calledWith('project.member.updated').should.be.true - done() - }) + .end((err, res) => { + if (err) { + return done(err); + } + const resJson = res.body.result.content; + should.exist(resJson); + resJson.role.should.equal('customer'); + resJson.isPrimary.should.be.true; + resJson.updatedBy.should.equal(40051332); + server.services.pubsub.publish.calledWith('project.member.updated').should.be.true; + done(); + }); + }); - }) - - it('should return 200 if valid user(not copilot any more) for project without direct project id', done => { - models.Project.update({ directProjectId: null}, {where: {id: project1.id}}) - .then(()=> { - request(server) - .patch('/v4/projects/' + project1.id + '/members/' + member2.id) + it('should return 200 if valid user(not copilot any more) for project without direct project id', (done) => { + models.Project.update({ directProjectId: null }, { where: { id: project1.id } }) + .then(() => { + request(server) + .patch(`/v4/projects/${project1.id}/members/${member2.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) .send(body) .expect('Content-Type', /json/) .expect(200) - .end(function (err, res) { - if (err) { - return done(err) - } - const resJson = res.body.result.content - should.exist(resJson) - resJson.role.should.equal(body.param.role) - resJson.isPrimary.should.be.false - resJson.updatedBy.should.equal(40051332) - server.services.pubsub.publish.calledWith('project.member.updated').should.be.true - done() - }) - } - ) - - }) + .end((err, res) => { + if (err) { + return done(err); + } + const resJson = res.body.result.content; + should.exist(resJson); + resJson.role.should.equal(body.param.role); + resJson.isPrimary.should.be.false; + resJson.updatedBy.should.equal(40051332); + server.services.pubsub.publish.calledWith('project.member.updated').should.be.true; + done(); + }); + }, + ); + }); - it('should return 200 if valid user(not copilot any more) and data', done => { - var mockHttpClient = _.merge(testUtil.mockHttpClient, { - delete: () => Promise.resolve({ - status: 200, - data: { - id: 'requesterId', - version: 'v3', - result: { - success: true, - status: 200, - content: true - } - } - }) - }) - var deleteSpy = sinon.spy(mockHttpClient, 'delete') - sandbox.stub(util, 'getHttpClient', () => mockHttpClient ) - request(server) - .patch('/v4/projects/' + project1.id + '/members/' + member2.id) + it('should return 200 if valid user(not copilot any more) and data', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + delete: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: true, + }, + }, + }), + }); + const deleteSpy = sinon.spy(mockHttpClient, 'delete'); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .patch(`/v4/projects/${project1.id}/members/${member2.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) .send(body) .expect('Content-Type', /json/) .expect(200) - .end(function (err, res) { - if (err) { - return done(err) - } - const resJson = res.body.result.content - should.exist(resJson) - resJson.role.should.equal(body.param.role) - resJson.isPrimary.should.be.false - resJson.updatedBy.should.equal(40051332) - deleteSpy.should.have.been.calledOnce - server.services.pubsub.publish.calledWith('project.member.removed').should.be.true - done() - }) - }) + .end((err, res) => { + if (err) { + return done(err); + } + const resJson = res.body.result.content; + should.exist(resJson); + resJson.role.should.equal(body.param.role); + resJson.isPrimary.should.be.false; + resJson.updatedBy.should.equal(40051332); + deleteSpy.should.have.been.calledOnce; + server.services.pubsub.publish.calledWith('project.member.removed').should.be.true; + done(); + }); + }); - it.skip('should return 500 if error to remove copilot from direct project', done => { - var mockHttpClient = _.merge(testUtil.mockHttpClient, { - delete: () => Promise.reject(new Error('error message')) - }) - var deleteSpy = sinon.spy(mockHttpClient, 'delete') - sandbox.stub(util, 'getHttpClient', () => mockHttpClient ) - request(server) - .patch('/v4/projects/' + project1.id + '/members/' + member2.id) + it.skip('should return 500 if error to remove copilot from direct project', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + delete: () => Promise.reject(new Error('error message')), + }); + const deleteSpy = sinon.spy(mockHttpClient, 'delete'); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .patch(`/v4/projects/${project1.id}/members/${member2.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) .send(body) .expect('Content-Type', /json/) .expect(500) - .end(function (err, res) { - if (err) { - return done(err) - } - const result = res.body.result - result.success.should.be.false - result.status.should.equal(500) - result.content.message.should.equal('error message') - deleteSpy.should.have.been.calledOnce - done() - }) - }) + .end((err, res) => { + if (err) { + return done(err); + } + const result = res.body.result; + result.success.should.be.false; + result.status.should.equal(500); + result.content.message.should.equal('error message'); + deleteSpy.should.have.been.calledOnce; + done(); + }); + }); - it('should return 200 if valid user(become manager) and data', done => { - var mockHttpClient = _.merge(testUtil.mockHttpClient, { - post: () => Promise.resolve({ - status: 200, - data: { - id: 'requesterId', - version: 'v3', - result: { - success: true, - status: 200, - content: { } - } - } - }) - }) - var postSpy = sinon.spy(mockHttpClient, 'post') - sandbox.stub(util, 'getHttpClient', () => mockHttpClient) - request(server) - .patch('/v4/projects/' + project1.id + '/members/' + member3.id) + it('should return 200 if valid user(become manager) and data', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + post: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: { }, + }, + }, + }), + }); + const postSpy = sinon.spy(mockHttpClient, 'post'); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .patch(`/v4/projects/${project1.id}/members/${member3.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.manager + Authorization: `Bearer ${testUtil.jwts.manager}`, }) .send({ - param: { - "role": "manager", - "isPrimary": false - } + param: { + role: 'manager', + isPrimary: false, + }, }) .expect('Content-Type', /json/) .expect(200) - .end(function (err, res) { - if (err) { - return done(err) - } - const resJson = res.body.result.content - should.exist(resJson) - resJson.role.should.equal('manager') - resJson.isPrimary.should.be.false - resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") - resJson.updatedBy.should.equal(40051334) - postSpy.should.have.been.calledOnce - done() - }) - }) + .end((err, res) => { + if (err) { + return done(err); + } + const resJson = res.body.result.content; + should.exist(resJson); + resJson.role.should.equal('manager'); + resJson.isPrimary.should.be.false; + resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); + resJson.updatedBy.should.equal(40051334); + postSpy.should.have.been.calledOnce; + done(); + }); + }); - it('should return 200 if valid user(become manager) and data (without directProjectId)', done => { - models.Project.update({ directProjectId: null}, {where: {id: project1.id}}) + it('should return 200 if valid user(become manager) and data (without directProjectId)', (done) => { + models.Project.update({ directProjectId: null }, { where: { id: project1.id } }) .then(() => { - var mockHttpClient = _.merge(testUtil.mockHttpClient, { - post: () => Promise.resolve({ - status: 200, - data: { - id: 'requesterId', - version: 'v3', - result: { - success: true, - status: 200, - content: { } - } - } - }) - }) - var postSpy = sinon.spy(mockHttpClient, 'post') - sandbox.stub(util, 'getHttpClient', () => mockHttpClient) - request(server) - .patch('/v4/projects/' + project1.id + '/members/' + member3.id) + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + post: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: { }, + }, + }, + }), + }); + const postSpy = sinon.spy(mockHttpClient, 'post'); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .patch(`/v4/projects/${project1.id}/members/${member3.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.manager + Authorization: `Bearer ${testUtil.jwts.manager}`, }) .send({ - param: { - "role": "manager", - "isPrimary": false - } + param: { + role: 'manager', + isPrimary: false, + }, }) .expect('Content-Type', /json/) .expect(200) - .end(function (err, res) { - if (err) { - return done(err) - } - const resJson = res.body.result.content - should.exist(resJson) - resJson.role.should.equal('manager') - resJson.isPrimary.should.be.false - resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") - resJson.updatedBy.should.equal(40051334) - postSpy.should.not.have.been.calledOnce - done() - }) - }) - }) + .end((err, res) => { + if (err) { + return done(err); + } + const resJson = res.body.result.content; + should.exist(resJson); + resJson.role.should.equal('manager'); + resJson.isPrimary.should.be.false; + resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); + resJson.updatedBy.should.equal(40051334); + postSpy.should.not.have.been.calledOnce; + done(); + }); + }); + }); - it('should return 200 if valid user(become copilot) and data', done => { - var mockHttpClient = _.merge(testUtil.mockHttpClient, { - post: () => Promise.resolve({ - status: 200, - data: { - id: 'requesterId', - version: 'v3', - result: { - success: true, - status: 200, - content: { - copilotProjectId: 2 - } - } - } - }) - }) - var postSpy = sinon.spy(mockHttpClient, 'post') - sandbox.stub(util, 'getHttpClient', () => mockHttpClient ) - request(server) - .patch('/v4/projects/' + project1.id + '/members/' + member1.id) + it('should return 200 if valid user(become copilot) and data', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + post: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: { + copilotProjectId: 2, + }, + }, + }, + }), + }); + const postSpy = sinon.spy(mockHttpClient, 'post'); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .patch(`/v4/projects/${project1.id}/members/${member1.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) .send({ - param: { - "role": "copilot", - "isPrimary": true - } + param: { + role: 'copilot', + isPrimary: true, + }, }) .expect('Content-Type', /json/) .expect(200) - .end(function (err, res) { - if (err) { - return done(err) - } - const resJson = res.body.result.content - should.exist(resJson) - resJson.role.should.equal('copilot') - resJson.isPrimary.should.be.true - resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") - resJson.updatedBy.should.equal(40051332) - postSpy.should.have.been.calledOnce - done() + .end((err, res) => { + if (err) { + return done(err); + } + const resJson = res.body.result.content; + should.exist(resJson); + resJson.role.should.equal('copilot'); + resJson.isPrimary.should.be.true; + resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); + resJson.updatedBy.should.equal(40051332); + postSpy.should.have.been.calledOnce; + done(); // models.ProjectMember.findById(member2.id) // .then(pm=> { // pm.isPrimary.should.be.false @@ -426,8 +427,7 @@ describe('Project members update', () => { // done() // }) // .catch(err => done(err)) - }) - }) - }) - -}) + }); + }); + }); +}); diff --git a/src/routes/projects/create.js b/src/routes/projects/create.js index 6aabcfcf..76d61b7b 100644 --- a/src/routes/projects/create.js +++ b/src/routes/projects/create.js @@ -1,13 +1,13 @@ -'use strict' -import validate from 'express-validation' -import _ from 'lodash' -import Joi from 'joi' -import models from '../../models' -import {PROJECT_TYPE, PROJECT_MEMBER_ROLE, PROJECT_STATUS, USER_ROLE, EVENT } from '../../constants' -import util from '../../util' -import directProject from '../../services/directProject' +import validate from 'express-validation'; +import _ from 'lodash'; +import Joi from 'joi'; + +import models from '../../models'; +import { PROJECT_TYPE, PROJECT_MEMBER_ROLE, PROJECT_STATUS, USER_ROLE, EVENT } from '../../constants'; +import util from '../../util'; +import directProject from '../../services/directProject'; /** * API to handle creating a new project. @@ -17,7 +17,7 @@ import directProject from '../../services/directProject' * All Topcoder users are allowed to create a project. * */ -var permissions = require('tc-core-library-js').middleware.permissions +const permissions = require('tc-core-library-js').middleware.permissions; const createProjectValdiations = { body: { @@ -28,18 +28,18 @@ const createProjectValdiations = { utm: Joi.object().keys({ source: Joi.string().allow(null), medium: Joi.string().allow(null), - campaign: Joi.string().allow(null) + campaign: Joi.string().allow(null), }).allow(null), bookmarks: Joi.array().items(Joi.object().keys({ title: Joi.string(), - address: Joi.string() + address: Joi.string(), })).optional().allow(null), estimatedPrice: Joi.number().precision(2).positive().optional().allow(null), terms: Joi.array().items(Joi.number().positive()).optional(), external: Joi.object().keys({ id: Joi.string(), type: Joi.any().valid('github', 'jira', 'asana', 'other'), - data: Joi.string().max(300) // TODO - restrict length + data: Joi.string().max(300), // TODO - restrict length }).allow(null), // TODO - add more types type: Joi.any().valid(_.values(PROJECT_TYPE)).required(), @@ -47,11 +47,11 @@ const createProjectValdiations = { challengeEligibility: Joi.array().items(Joi.object().keys({ role: Joi.string().valid('submitter', 'reviewer', 'copilot'), users: Joi.array().items(Joi.number().positive()), - groups: Joi.array().items(Joi.number().positive()) - })).allow(null) - }).required() - } -} + groups: Joi.array().items(Joi.number().positive()), + })).allow(null), + }).required(), + }, +}; module.exports = [ // handles request validations @@ -62,10 +62,10 @@ module.exports = [ * Create a project if the user has access */ (req, res, next) => { - var project = req.body.param + const project = req.body.param; const userRole = util.hasRole(req, USER_ROLE.MANAGER) ? PROJECT_MEMBER_ROLE.MANAGER - : PROJECT_MEMBER_ROLE.CUSTOMER + : PROJECT_MEMBER_ROLE.CUSTOMER; // set defaults _.defaults(project, { createdBy: req.authUser.userId, @@ -73,8 +73,8 @@ module.exports = [ challengeEligibility: [], bookmarks: [], external: null, - utm: null - }) + utm: null, + }); // override values _.assign(project, { status: PROJECT_STATUS.DRAFT, @@ -86,62 +86,60 @@ module.exports = [ userId: req.authUser.userId, updatedBy: req.authUser.userId, createdBy: req.authUser.userId, - }] - }) + }], + }); models.sequelize.transaction(() => { - var newProject = null + let newProject = null; return models.Project .create(project, { include: [{ model: models.ProjectMember, - as: 'members' - }] + as: 'members', + }], }) .then((_newProject) => { - newProject = _newProject + newProject = _newProject; req.log.debug('new project created (id# %d, name: %s)', - newProject.id, newProject.name) + newProject.id, newProject.name); // create direct project with name and description - var body = { + const body = { projectName: newProject.name, - projectDescription: newProject.description - } + projectDescription: newProject.description, + }; // billingAccountId is optional field - if(newProject.billingAccountId){ - body.billingAccountId = newProject.billingAccountId + if (newProject.billingAccountId) { + body.billingAccountId = newProject.billingAccountId; } return directProject.createDirectProject(req, body) .then((resp) => { - newProject.directProjectId = resp.data.result.content.projectId - return newProject.save() - }) - .then(() => { - return newProject.reload(newProject.id) + newProject.directProjectId = resp.data.result.content.projectId; + return newProject.save(); }) - .catch(err => { + .then(() => newProject.reload(newProject.id)) + .catch((err) => { // log the error and continue - req.log.error('Error creating direct project') - req.log.error(err) - return Promise.resolve() - }) + req.log.error('Error creating direct project'); + req.log.error(err); + return Promise.resolve(); + }); }) .then(() => { - newProject = newProject.get({plain: true}) + newProject = newProject.get({ plain: true }); // remove utm details & deletedAt field - newProject = _.omit(newProject, ['deletedAt', 'utm']) + newProject = _.omit(newProject, ['deletedAt', 'utm']); // add an empty attachments array - newProject.attachments = [] + newProject.attachments = []; req.app.services.pubsub.publish(EVENT.ROUTING_KEY.PROJECT_DRAFT_CREATED, newProject, - { correlationId: req.id } - ) + { correlationId: req.id }, + ); // emit event - req.app.emit(EVENT.ROUTING_KEY.PROJECT_DRAFT_CREATED, {req, project: newProject }) - res.status(201).json(util.wrapResponse(req.id, newProject, 1, 201)) + req.app.emit(EVENT.ROUTING_KEY.PROJECT_DRAFT_CREATED, { req, project: newProject }); + res.status(201).json(util.wrapResponse(req.id, newProject, 1, 201)); }) .catch((err) => { - util.handleError('Error creating project', err, req, next) - }) - }) - } -] + util.handleError('Error creating project', err, req, next); + }); + }); + }, +]; diff --git a/src/routes/projects/create.spec.js b/src/routes/projects/create.spec.js index 045fdbc2..477e9ecb 100644 --- a/src/routes/projects/create.spec.js +++ b/src/routes/projects/create.spec.js @@ -1,100 +1,100 @@ -'use strict' -import _ from 'lodash' -import chai from 'chai' -import sinon from 'sinon' -import request from 'supertest' -import util from '../../util' -import server from '../../app' -import testUtil from '../../tests/util' -import RabbitMQService from '../../services/rabbitmq' +import _ from 'lodash'; +import chai from 'chai'; +import sinon from 'sinon'; +import request from 'supertest'; -var should = chai.should() +import util from '../../util'; +import server from '../../app'; +import testUtil from '../../tests/util'; +import RabbitMQService from '../../services/rabbitmq'; -sinon.stub(RabbitMQService.prototype, 'init', () => {}) +const should = chai.should(); + +sinon.stub(RabbitMQService.prototype, 'init', () => {}); sinon.stub(RabbitMQService.prototype, 'publish', () => { - console.log('publish called') -}) + console.log('publish called'); +}); describe('Project create', () => { - before(done => { - testUtil.clearDb(done) - }) + before((done) => { + testUtil.clearDb(done); + }); - after(done => { - testUtil.clearDb(done) - }) + after((done) => { + testUtil.clearDb(done); + }); describe('POST /projects', () => { - var body = { + const body = { param: { type: 'generic', - description: "test project", + description: 'test project', details: {}, billingAccountId: 1, name: 'test project1', bookmarks: [{ title: 'title1', - address: 'address1' - }] - } - } + address: 'address1', + }], + }, + }; - var sandbox + let sandbox; beforeEach(() => { - sandbox = sinon.sandbox.create() - }) + sandbox = sinon.sandbox.create(); + }); afterEach(() => { - sandbox.restore() - }) + sandbox.restore(); + }); - it('should return 403 if user is not authenticated', done => { + it('should return 403 if user is not authenticated', (done) => { request(server) - .post("/v4/projects") + .post('/v4/projects') .send(body) - .expect(403, done) - }) + .expect(403, done); + }); - it('should return 422 if validations dont pass', done => { - let invalidBody = _.cloneDeep(body) - delete invalidBody.param.name + it('should return 422 if validations dont pass', (done) => { + const invalidBody = _.cloneDeep(body); + delete invalidBody.param.name; request(server) - .post("/v4/projects") + .post('/v4/projects') .set({ - "Authorization": "Bearer " + testUtil.jwts.member + Authorization: `Bearer ${testUtil.jwts.member}`, }) .send(invalidBody) .expect('Content-Type', /json/) - .expect(422, done) - }) + .expect(422, done); + }); - it('should return 201 if error to create direct project', done => { - var mockHttpClient = _.merge(testUtil.mockHttpClient, { - post: () => Promise.reject(new Error('error message')) - }) - sandbox.stub(util, 'getHttpClient', () => mockHttpClient) + it('should return 201 if error to create direct project', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + post: () => Promise.reject(new Error('error message')), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .post("/v4/projects") + .post('/v4/projects') .set({ - "Authorization": "Bearer " + testUtil.jwts.member + Authorization: `Bearer ${testUtil.jwts.member}`, }) .send(body) .expect('Content-Type', /json/) .expect(201) - .end(function(err, res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - const result = res.body.result - result.success.should.be.truthy - result.status.should.equal(201) - server.services.pubsub.publish.calledWith('project.draft-created').should.be.true - done() - }) - }) + const result = res.body.result; + result.success.should.be.truthy; + result.status.should.equal(201); + server.services.pubsub.publish.calledWith('project.draft-created').should.be.true; + done(); + }); + }); - it('should return 201 if valid user and data', done => { - var mockHttpClient = _.merge(testUtil.mockHttpClient, { + it('should return 201 if valid user and data', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { post: () => Promise.resolve({ status: 200, data: { @@ -105,42 +105,42 @@ describe('Project create', () => { status: 200, content: { projectId: 128, - } - } - } - }) - }) - sandbox.stub(util, 'getHttpClient', () => mockHttpClient) + }, + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .post("/v4/projects") + .post('/v4/projects') .set({ - "Authorization": "Bearer " + testUtil.jwts.member + Authorization: `Bearer ${testUtil.jwts.member}`, }) .send(body) .expect('Content-Type', /json/) .expect(201) - .end(function(err, res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var resJson = res.body.result.content - should.exist(resJson) - should.exist(resJson.billingAccountId) - should.exist(resJson.name) - resJson.directProjectId.should.be.eql(128) - resJson.status.should.be.eql('draft') - resJson.type.should.be.eql(body.param.type) - resJson.members.should.have.lengthOf(1) - resJson.members[0].role.should.be.eql('customer') - resJson.members[0].userId.should.be.eql(40051331) - resJson.members[0].projectId.should.be.eql(resJson.id) - resJson.members[0].isPrimary.should.be.truthy - resJson.bookmarks.should.have.lengthOf(1) - resJson.bookmarks[0].title.should.be.eql('title1') - resJson.bookmarks[0].address.should.be.eql('address1') - server.services.pubsub.publish.calledWith('project.draft-created').should.be.true - done() - }) - }) - }) -}) + const resJson = res.body.result.content; + should.exist(resJson); + should.exist(resJson.billingAccountId); + should.exist(resJson.name); + resJson.directProjectId.should.be.eql(128); + resJson.status.should.be.eql('draft'); + resJson.type.should.be.eql(body.param.type); + resJson.members.should.have.lengthOf(1); + resJson.members[0].role.should.be.eql('customer'); + resJson.members[0].userId.should.be.eql(40051331); + resJson.members[0].projectId.should.be.eql(resJson.id); + resJson.members[0].isPrimary.should.be.truthy; + resJson.bookmarks.should.have.lengthOf(1); + resJson.bookmarks[0].title.should.be.eql('title1'); + resJson.bookmarks[0].address.should.be.eql('address1'); + server.services.pubsub.publish.calledWith('project.draft-created').should.be.true; + done(); + }); + }); + }); +}); diff --git a/src/routes/projects/delete.js b/src/routes/projects/delete.js index a201d7ca..d5f03aaa 100644 --- a/src/routes/projects/delete.js +++ b/src/routes/projects/delete.js @@ -1,48 +1,47 @@ -'use strict' + // 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 _ 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'; /** * API to delete a project member. * */ -const permissions = tcMiddleware.permissions +const permissions = tcMiddleware.permissions; module.exports = [ permissions('project.delete'), (req, res, next) => { - var projectId = _.parseInt(req.params.projectId) + const projectId = _.parseInt(req.params.projectId); - models.sequelize.transaction(t => { + models.sequelize.transaction(t => // soft delete the record - return models.Project.destroy({ - where: { id: projectId }, - cascade: true, - transaction: t - }) + models.Project.destroy({ + where: { id: projectId }, + cascade: true, + transaction: t, + }) .then((count) => { if (count === 0) { - let err = new Error('Project not found') - err.status = 404 - next(err) + const err = new Error('Project not found'); + err.status = 404; + next(err); } else { req.app.services.pubsub.publish( EVENT.ROUTING_KEY.PROJECT_DELETED, { id: projectId }, - { correlationId: req.id } - ) + { correlationId: req.id }, + ); // emit event - req.app.emit(EVENT.ROUTING_KEY.PROJECT_DELETED, {req, id: projectId }) - res.status(204).json({}) + req.app.emit(EVENT.ROUTING_KEY.PROJECT_DELETED, { req, id: projectId }); + res.status(204).json({}); } }) - .catch((err) => next(err)) - }) - } -] + .catch(err => next(err))); + }, +]; diff --git a/src/routes/projects/delete.spec.js b/src/routes/projects/delete.spec.js index eab11c53..99587089 100644 --- a/src/routes/projects/delete.spec.js +++ b/src/routes/projects/delete.spec.js @@ -1,18 +1,22 @@ -'use strict' -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' +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', () => { - var project1, owner, teamMember, manager, copilot - beforeEach(done => { + let project1, + owner, + teamMember, + manager, + copilot; + beforeEach((done) => { testUtil.clearDb() .then(() => { models.Project.create({ @@ -24,11 +28,11 @@ describe('Project delete test', () => { status: 'draft', details: {}, createdBy: 1, - updatedBy: 1 - }).then(p => { - project1 = p + updatedBy: 1, + }).then((p) => { + project1 = p; // create members - let promises = [ + const promises = [ // owner models.ProjectMember.create({ userId: 40051331, @@ -36,7 +40,7 @@ describe('Project delete test', () => { role: 'customer', isPrimary: true, createdBy: 1, - updatedBy: 1 + updatedBy: 1, }), // manager models.ProjectMember.create({ @@ -45,7 +49,7 @@ describe('Project delete test', () => { role: 'manager', isPrimary: true, createdBy: 1, - updatedBy: 1 + updatedBy: 1, }), // copilot models.ProjectMember.create({ @@ -54,7 +58,7 @@ describe('Project delete test', () => { role: 'copilot', isPrimary: true, createdBy: 1, - updatedBy: 1 + updatedBy: 1, }), // team member models.ProjectMember.create({ @@ -63,50 +67,49 @@ describe('Project delete test', () => { role: 'customer', isPrimary: false, createdBy: 1, - updatedBy: 1 - }) - ] + updatedBy: 1, + }), + ]; Promise.all(promises) .then((res) => { - owner = res[0] - manager = res[2] - copilot = res[3] - teamMember = res[4] - done() - }) - }) - }) - }) + owner = res[0]; + manager = res[2]; + copilot = res[3]; + teamMember = res[4]; + done(); + }); + }); + }); + }); - after(done => { - testUtil.clearDb(done) - }) + after((done) => { + testUtil.clearDb(done); + }); describe('DELETE /projects/{id}/', () => { - - it('should return 403 if copilot tries to delete the project', done => { + it('should return 403 if copilot tries to delete the project', (done) => { request(server) - .delete('/v4/projects/' + project1.id) + .delete(`/v4/projects/${project1.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) - .expect(403, done) - }) + .expect(403, done); + }); - it('should return 204 if project was successfully removed', done => { + it('should return 204 if project was successfully removed', (done) => { request(server) - .delete('/v4/projects/' + project1.id) + .delete(`/v4/projects/${project1.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.member + Authorization: `Bearer ${testUtil.jwts.member}`, }) .expect(204) - .end(function(err, resp) { + .end((err, resp) => { if (err) { - return done(err) + return done(err); } - server.services.pubsub.publish.calledWith('project.deleted').should.be.true - done() - }) - }) - }) -}) + server.services.pubsub.publish.calledWith('project.deleted').should.be.true; + done(); + }); + }); + }); +}); diff --git a/src/routes/projects/get.js b/src/routes/projects/get.js index d06aa46e..8150715c 100644 --- a/src/routes/projects/get.js +++ b/src/routes/projects/get.js @@ -1,11 +1,11 @@ -'use strict' + /* globals Promise */ -import _ from 'lodash' +import _ from 'lodash'; -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'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; /** @@ -18,9 +18,9 @@ import { middleware as tcMiddleware} from 'tc-core-library-js' */ // var permissions = require('tc-core-library-js').middleware.permissions -const permissions = tcMiddleware.permissions -const PROJECT_ATTRIBUTES = _.without(_.keys(models.Project.rawAttributes), 'utm', 'deletedAt') -const PROJECT_MEMBER_ATTRIBUTES = _.without(_.keys(models.ProjectMember.rawAttributes), 'deletedAt') +const permissions = tcMiddleware.permissions; +const PROJECT_ATTRIBUTES = _.without(_.keys(models.Project.rawAttributes), 'utm', 'deletedAt'); +const PROJECT_MEMBER_ATTRIBUTES = _.without(_.keys(models.ProjectMember.rawAttributes), 'deletedAt'); module.exports = [ permissions('project.view'), @@ -29,52 +29,48 @@ module.exports = [ * Get a project by id */ (req, res, next) => { - const projectId = Number(req.params.projectId) - var fields = req.query.fields - fields = fields ? fields.split(',') : [] + const projectId = Number(req.params.projectId); + let fields = req.query.fields; + fields = fields ? fields.split(',') : []; // parse the fields string to determine what fields are to be returned fields = util.parseFields(fields, { - 'projects': PROJECT_ATTRIBUTES, - 'project_members': PROJECT_MEMBER_ATTRIBUTES - }) - var project + projects: PROJECT_ATTRIBUTES, + project_members: PROJECT_MEMBER_ATTRIBUTES, + }); + let project; return models.Project .find({ where: { id: projectId }, attributes: _.get(fields, 'projects', null), - raw: true + raw: true, }) .then((_project) => { - project = _project + project = _project; if (!project) { // returning 404 - var apiErr = new Error(`project not found for id ${projectId}`) - apiErr.status = 404 - return Promise.reject(apiErr) + const apiErr = new Error(`project not found for id ${projectId}`); + apiErr.status = 404; + return Promise.reject(apiErr); } // check context for project members - project.members = _.map(req.context.currentProjectMembers, (m) => { - return _.pick(m, fields['project_members']) - }) + project.members = _.map(req.context.currentProjectMembers, m => _.pick(m, fields.project_members)); // check if attachments field was requested if (!req.query.fields || _.indexOf(req.query.fields, 'attachments') > -1) { - return util.getProjectAttachments(req, project.id) + return util.getProjectAttachments(req, project.id); } - else { // return null if attachments were not requested. - return Promise.resolve(null) - } + return Promise.resolve(null); }) .then((attachments) => { // if attachments were requested if (attachments) { - project.attachments = attachments + project.attachments = attachments; } - req.log.debug('attachment', project.attachments) + req.log.debug('attachment', project.attachments); - req.log.debug('project', JSON.stringify(project, null, 2)) - res.status(200).json(util.wrapResponse(req.id, project)) + req.log.debug('project', JSON.stringify(project, null, 2)); + res.status(200).json(util.wrapResponse(req.id, project)); }) - .catch((err) => next(err)) - } -] + .catch(err => next(err)); + }, +]; diff --git a/src/routes/projects/get.spec.js b/src/routes/projects/get.spec.js index b23f3a82..b8004a0c 100644 --- a/src/routes/projects/get.spec.js +++ b/src/routes/projects/get.spec.js @@ -1,21 +1,22 @@ -'use strict' -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' +import chai from 'chai'; +import sinon from 'sinon'; +import request from 'supertest'; -var should = chai.should() +import models from '../../models'; +import util from '../../util'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const should = chai.should(); describe('GET Project', () => { - var project1, project2 - before(done => { + let project1, + project2; + before((done) => { testUtil.clearDb() .then(() => { - var p1 = models.Project.create({ + const p1 = models.Project.create({ type: 'generic', billingAccountId: 1, name: 'test1', @@ -23,30 +24,30 @@ describe('GET Project', () => { status: 'draft', details: {}, createdBy: 1, - updatedBy: 1 - }).then(p => { - project1 = p + updatedBy: 1, + }).then((p) => { + project1 = p; // create members - var pm1 = models.ProjectMember.create({ + const pm1 = models.ProjectMember.create({ userId: 40051331, projectId: project1.id, role: 'customer', isPrimary: true, createdBy: 1, - updatedBy: 1 - }) - var pm2 = models.ProjectMember.create({ + updatedBy: 1, + }); + const pm2 = models.ProjectMember.create({ userId: 40051333, projectId: project1.id, role: 'copilot', isPrimary: true, createdBy: 1, - updatedBy: 1 - }) - return Promise.all([pm1, pm2]) - }) + updatedBy: 1, + }); + return Promise.all([pm1, pm2]); + }); - var p2 = models.Project.create({ + const p2 = models.Project.create({ type: 'visual_design', billingAccountId: 1, name: 'test2', @@ -54,87 +55,86 @@ describe('GET Project', () => { status: 'draft', details: {}, createdBy: 1, - updatedBy: 1 - }).then(p => { - project2 = p - }) + updatedBy: 1, + }).then((p) => { + project2 = p; + }); return Promise.all([p1, p2]) - .then(() => done()) - }) - - }) + .then(() => done()); + }); + }); - after(done => { - testUtil.clearDb(done) - }) + after((done) => { + testUtil.clearDb(done); + }); describe('GET /projects/{id}', () => { - it('should return 403 if user is not authenticated', done => { + it('should return 403 if user is not authenticated', (done) => { request(server) - .get('/v4/projects/' + project2.id) - .expect(403, done) - }) + .get(`/v4/projects/${project2.id}`) + .expect(403, done); + }); - it('should return 404 if requested project doesn\'t exist', done => { + it('should return 404 if requested project doesn\'t exist', (done) => { request(server) .get('/v4/projects/14343323') .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.admin + Authorization: `Bearer ${testUtil.jwts.admin}`, }) - .expect(404, done) - }) + .expect(404, done); + }); - it('should return 404 if user does not have access to the project', done => { + it('should return 404 if user does not have access to the project', (done) => { request(server) - .get('/v4/projects/' + project2.id) + .get(`/v4/projects/${project2.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.member + Authorization: `Bearer ${testUtil.jwts.member}`, }) - .expect(403, done) - }) + .expect(403, done); + }); - it('should return the project when registerd member attempts to access the project', done => { + it('should return the project when registerd member attempts to access the project', (done) => { request(server) - .get('/v4/projects/' + project1.id + '/?fields=id%2Cname%2Cstatus%2Cmembers.role%2Cmembers.id%2Cmembers.userId') + .get(`/v4/projects/${project1.id}/?fields=id%2Cname%2Cstatus%2Cmembers.role%2Cmembers.id%2Cmembers.userId`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.member + Authorization: `Bearer ${testUtil.jwts.member}`, }) .expect('Content-Type', /json/) .expect(200) - .end(function(err, res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var resJson = res.body.result.content - should.exist(resJson) - should.not.exist(resJson.deletedAt) - should.not.exist(resJson.billingAccountId) - should.exist(resJson.name) - resJson.status.should.be.eql('draft') - resJson.members.should.have.lengthOf(2) - done() - }) - }) - - it('should return the project for administrator ', done => { + const resJson = res.body.result.content; + should.exist(resJson); + should.not.exist(resJson.deletedAt); + should.not.exist(resJson.billingAccountId); + should.exist(resJson.name); + resJson.status.should.be.eql('draft'); + resJson.members.should.have.lengthOf(2); + done(); + }); + }); + + it('should return the project for administrator ', (done) => { request(server) - .get('/v4/projects/' + project1.id) + .get(`/v4/projects/${project1.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.admin + Authorization: `Bearer ${testUtil.jwts.admin}`, }) .expect('Content-Type', /json/) .expect(200) - .end(function(err, res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var resJson = res.body.result.content - should.exist(resJson) - done() - }) - }) + const resJson = res.body.result.content; + should.exist(resJson); + done(); + }); + }); - it('should return attachment with downloadUrl', done => { + it('should return attachment with downloadUrl', (done) => { models.ProjectAttachment.create({ projectId: project1.id, filePath: 'projects/1/spec.pdf', @@ -142,54 +142,47 @@ describe('GET Project', () => { createdBy: 1, updatedBy: 1, name: 'spec.pdf', - description: 'blah' + description: 'blah', }).then((attachment) => { - var mockHttpClient = { + const mockHttpClient = { defaults: { headers: { common: {} } }, - post: () => { - return new Promise((resolve) => { - return resolve({ + post: () => new Promise(resolve => resolve({ + status: 200, + data: { + result: { status: 200, - data: { - result: { - status: 200, - content: { - filePath: 'projects/1/spec.pdf', - preSignedURL: 'https://www.topcoder-dev.com/downloadUrl' - } - } - } - }) - }) - } - } - var spy = sinon.spy(mockHttpClient, 'post') - var stub = sinon.stub(util, 'getHttpClient', () => { return mockHttpClient } ) + content: { + filePath: 'projects/1/spec.pdf', + preSignedURL: 'https://www.topcoder-dev.com/downloadUrl', + }, + }, + }, + })), + }; + const spy = sinon.spy(mockHttpClient, 'post'); + const stub = sinon.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .get('/v4/projects/' + project1.id) + .get(`/v4/projects/${project1.id}`) .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.admin + Authorization: `Bearer ${testUtil.jwts.admin}`, }) .expect('Content-Type', /json/) .expect(200) - .end(function(err, res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var resJson = res.body.result.content - should.exist(resJson) - spy.should.have.been.calledOnce - resJson.attachments.should.have.lengthOf(1) - resJson.attachments[0].filePath.should.equal(attachment.filePath) - resJson.attachments[0].downloadUrl.should.exist - stub.restore() - done() - }) - - }) - - }) - }) - -}) + const resJson = res.body.result.content; + should.exist(resJson); + spy.should.have.been.calledOnce; + resJson.attachments.should.have.lengthOf(1); + resJson.attachments[0].filePath.should.equal(attachment.filePath); + resJson.attachments[0].downloadUrl.should.exist; + stub.restore(); + done(); + }); + }); + }); + }); +}); diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index 84bb5d59..eeadb656 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -1,11 +1,11 @@ -'use strict' + /* globals Promise */ -import _ from 'lodash' +import _ from 'lodash'; -import models from '../../models' -import { USER_ROLE } from '../../constants' -import util from '../../util' +import models from '../../models'; +import { USER_ROLE } from '../../constants'; +import util from '../../util'; /** * API to handle retrieving projects @@ -16,78 +16,78 @@ import util from '../../util' */ const PROJECT_ATTRIBUTES = _.without(_.keys(models.Project.rawAttributes), 'utm', - 'deletedAt' -) + 'deletedAt', +); const PROJECT_MEMBER_ATTRIBUTES = _.without( _.keys(models.ProjectMember.rawAttributes), - 'deletedAt' -) + 'deletedAt', +); const PROJECT_ATTACHMENT_ATTRIBUTES = _.without( _.keys(models.ProjectAttachment.rawAttributes), - 'deletedAt' + 'deletedAt', -) -var _retrieveProjects = (req, criteria, sort, fields) => { +); +const _retrieveProjects = (req, criteria, sort, fields) => { // order by - const order = sort ? [sort.split(' ')] : [['createdAt', 'asc']] - fields = fields ? fields.split(',') : [] + const order = sort ? [sort.split(' ')] : [['createdAt', 'asc']]; + fields = fields ? fields.split(',') : []; // parse the fields string to determine what fields are to be returned fields = util.parseFields(fields, { - 'projects': PROJECT_ATTRIBUTES, - 'project_members': PROJECT_MEMBER_ATTRIBUTES - }) + projects: PROJECT_ATTRIBUTES, + project_members: PROJECT_MEMBER_ATTRIBUTES, + }); // make sure project.id is part of fields - if (_.indexOf(fields.projects, 'id') < 0) fields.projects.push('id') - let retrieveAttachments = !req.query.fields || req.query.fields.indexOf('attachments') > -1 - let retrieveMembers = !req.query.fields || !!fields.project_members.length + if (_.indexOf(fields.projects, 'id') < 0) fields.projects.push('id'); + const retrieveAttachments = !req.query.fields || req.query.fields.indexOf('attachments') > -1; + const retrieveMembers = !req.query.fields || !!fields.project_members.length; return models.Project.searchText({ - filters: criteria.filters, - order, - limit : criteria.limit, - offset: criteria.offset, - attributes: _.get(fields, 'projects', null) - }, req.log) - .then( ({rows, count}) => { - const projectIds = _.map(rows, 'id') - const promises = [] + filters: criteria.filters, + order, + limit: criteria.limit, + offset: criteria.offset, + attributes: _.get(fields, 'projects', null), + }, req.log) + .then(({ rows, count }) => { + const projectIds = _.map(rows, 'id'); + const promises = []; // retrieve members if (projectIds.length && retrieveMembers) { promises.push( models.ProjectMember.findAll({ attributes: _.get(fields, 'ProjectMembers'), where: { projectId: { in: projectIds } }, - raw: true - }) - ) + raw: true, + }), + ); } if (projectIds.length && retrieveAttachments) { promises.push( models.ProjectAttachment.findAll({ attributes: PROJECT_ATTACHMENT_ATTRIBUTES, where: { projectId: { in: projectIds } }, - raw: true - }) - ) + raw: true, + }), + ); } // return results after promise(s) have resolved return Promise.all(promises) - .then(values => { - const allMembers = retrieveMembers ? values.shift() : [] - const allAttachments = retrieveAttachments ? values.shift() : [] - _.forEach(rows, p => { + .then((values) => { + const allMembers = retrieveMembers ? values.shift() : []; + const allAttachments = retrieveAttachments ? values.shift() : []; + _.forEach(rows, (p) => { // if values length is 1 it could be either attachments or members if (retrieveMembers) { - p.members = _.filter(allMembers, m => m.projectId === p.id) + p.members = _.filter(allMembers, m => m.projectId === p.id); } if (retrieveAttachments) { - p.attachments = _.filter(allAttachments, a => a.projectId === p.id) + p.attachments = _.filter(allAttachments, a => a.projectId === p.id); } - }) - return { rows, count } - }) - }) -} + }); + return { rows, count }; + }); + }); +}; module.exports = [ /** @@ -96,10 +96,10 @@ module.exports = [ */ (req, res, next) => { // handle filters - var filters = util.parseQueryFilter(req.query.filter) - var sort = req.query.sort ? decodeURIComponent(req.query.sort) : 'createdAt' - if (sort && sort.indexOf(" ") == -1) { - sort = sort + ' asc' + let filters = util.parseQueryFilter(req.query.filter); + let sort = req.query.sort ? decodeURIComponent(req.query.sort) : 'createdAt'; + if (sort && sort.indexOf(' ') == -1) { + sort += ' asc'; } const sortableProps = [ 'createdAt', 'createdAt asc', 'createdAt desc', @@ -107,58 +107,52 @@ module.exports = [ 'id', 'id asc', 'id desc', 'status', 'status asc', 'status desc', 'name', 'name asc', 'name desc', - 'type', 'type asc', 'type desc' - ] + 'type', 'type asc', 'type desc', + ]; if (!util.isValidFilter(filters, ['id', 'status', 'type', 'memberOnly', 'keyword']) || (sort && _.indexOf(sortableProps, sort) < 0)) { - return util.handleError('Invalid filters or sort', null, req, next) + return util.handleError('Invalid filters or sort', null, req, next); } // check if user only wants to retrieve projects where he/she is a member - const memberOnly = _.get(filters, 'memberOnly', false) - filters = _.omit(filters, 'memberOnly') + const memberOnly = _.get(filters, 'memberOnly', false); + filters = _.omit(filters, 'memberOnly'); - var criteria = { - filters: filters, - limit: Math.min(req.query.limit || 20, 20), - offset: req.query.offset || 0 - } - req.log.debug(criteria) + const criteria = { + filters, + limit: Math.min(req.query.limit || 20, 20), + offset: req.query.offset || 0, + }; + req.log.debug(criteria); if (!memberOnly && (util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) - || util.hasRole(req, USER_ROLE.MANAGER))) - { + || util.hasRole(req, USER_ROLE.MANAGER))) { // admins & topcoder managers can see all projects return _retrieveProjects(req, criteria, sort, req.query.fields) - .then(result => { - return res.json(util.wrapResponse(req.id, result.rows, result.count)) - }) - .catch(err => next(err)) - } else { + .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) + .catch(err => next(err)); + } // If user requested projects where he/she is a member or // if they are not a copilot then return projects that they are members in. // Copilots can view projects that they are members in or they have // - var getProjectIds = !memberOnly && util.hasRole(req, USER_ROLE.COPILOT) ? + const getProjectIds = !memberOnly && util.hasRole(req, USER_ROLE.COPILOT) ? models.Project.getProjectIdsForCopilot(req.authUser.userId) : - models.ProjectMember.getProjectIdsForUser(req.authUser.userId) - return getProjectIds - .then(accessibleProjectIds => { + models.ProjectMember.getProjectIdsForUser(req.authUser.userId); + return getProjectIds + .then((accessibleProjectIds) => { // filter based on accessible if (_.get(criteria.filters, 'id', null)) { - criteria.filters.id['$in'] = _.intersection( + criteria.filters.id.$in = _.intersection( accessibleProjectIds, - criteria.filters.id['$in'] - ) + criteria.filters.id.$in, + ); } else { - criteria.filters.id = { $in : accessibleProjectIds} + criteria.filters.id = { $in: accessibleProjectIds }; } - return _retrieveProjects(req, criteria, sort, req.query.fields) + return _retrieveProjects(req, criteria, sort, req.query.fields); }) - .then(result => { - return res.json(util.wrapResponse(req.id, result.rows, result.count)) - }) - .catch(err => next(err)) - } - } -] + .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) + .catch(err => next(err)); + }, +]; diff --git a/src/routes/projects/list.spec.js b/src/routes/projects/list.spec.js index 0e8679af..a69a8c2c 100644 --- a/src/routes/projects/list.spec.js +++ b/src/routes/projects/list.spec.js @@ -1,305 +1,293 @@ -'use strict' - -import chai from 'chai' -import request from 'supertest' - -import models from '../../models' -import server from '../../app' -import testUtil from '../../tests/util' - -var should = chai.should() - -/** - * Add full text index for projects. - */ -function addFullTextIndex() { - if(models.sequelize.options.dialect !== 'postgres') { - console.log('Not creating search index, must be using POSTGRES to do this'); - return; - } - - return models.sequelize - .query('ALTER TABLE projects ADD COLUMN "projectFullText" text;') - .then(function() { - return models.sequelize - .query('UPDATE projects SET "projectFullText" = lower(' + - 'name || \' \' || coalesce(description, \'\') || \' \' || coalesce(details#>>\'{utm, code}\', \'\'));'); - }).then(function() { - return models.sequelize - .query('CREATE EXTENSION IF NOT EXISTS pg_trgm;'); - }).then(function() { - return models.sequelize - .query('CREATE INDEX project_text_search_idx ON projects USING GIN("projectFullText" gin_trgm_ops);'); - }).then(function() { - return 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}\', \'\')); ' + - 'return new; ' + - 'end ' + - '$$ LANGUAGE plpgsql;'); - }).then(function() { - return models.sequelize - .query('DROP TRIGGER IF EXISTS project_text_update ON projects;'); - }).then(function() { - return models.sequelize - .query('CREATE TRIGGER project_text_update BEFORE INSERT OR UPDATE ON projects' + - ' FOR EACH ROW EXECUTE PROCEDURE project_text_update_trigger();'); - }).catch(function(err){ - console.log('Failed: ', err); - }); -} - -describe('LIST Project', () => { - var project1, project2 - before(done => { - testUtil.clearDb() - .then(() => addFullTextIndex()) - .then(() => { - var p1 = models.Project.create({ - type: 'generic', - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'active', - details: { - utm: { - code: 'code1' - } - }, - createdBy: 1, - updatedBy: 1 - }).then(p => { - project1 = p - // create members - var pm1 = models.ProjectMember.create({ - userId: 40051331, - projectId: project1.id, - role: 'customer', - isPrimary: true, - createdBy: 1, - updatedBy: 1 - }) - var pm2 = models.ProjectMember.create({ - userId: 40051332, - projectId: project1.id, - role: 'copilot', - isPrimary: true, - createdBy: 1, - updatedBy: 1 - }) - var pa1 = models.ProjectAttachment.create({ - title: 'Spec', - projectId: project1.id, - description: "specification", - filePath: "projects/1/spec.pdf", - contentType: "application/pdf", - createdBy: 1, - updatedBy: 1 - }) - return Promise.all([pm1, pm2, pa1]) - }) - - var p2 = models.Project.create({ - type: 'visual_design', - billingAccountId: 1, - name: 'test2', - description: 'test project2', - status: 'draft', - details: {}, - createdBy: 1, - updatedBy: 1 - }).then(p => { - project2 = p - return models.ProjectMember.create({ - userId: 40051332, - projectId: project2.id, - role: 'copilot', - isPrimary: true, - createdBy: 1, - updatedBy: 1 - }) - }) - var p3 = models.Project.create({ - type: 'visual_design', - billingAccountId: 1, - name: 'test2', - description: 'test project3', - status: 'reviewed', - details: {}, - createdBy: 1, - updatedBy: 1 - }) - return Promise.all([p1, p2, p3]) - .then(() => done()) - }) - }) - - after(done => { - testUtil.clearDb(done) - }) - - describe('GET All /projects/', () => { - it('should return 403 if user is not authenticated', done => { - request(server) - .get('/v4/projects/') - .expect(403, done) - }) - - it('should return 200 and no projects if user does not have access', done => { - request(server) - .get('/v4/projects/?filter=id%3Din%28'+ project2.id + '%29') - .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.member - }) - .expect(200) - .end((err, res) => { - if (err) { - return done(err) - } - res.body.result.content.should.have.lengthOf(0) - done() - }) - }) - - it('should return the project when registerd member attempts to access the project', done => { - request(server) - .get('/v4/projects/?filter=status%3Ddraft') - .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot - }) - .expect('Content-Type', /json/) - .expect(200) - .end(function(err, res) { - if (err) { - return done(err) - } - var resJson = res.body.result.content - res.body.result.metadata.totalCount.should.equal(1) - should.exist(resJson) - resJson.should.have.lengthOf(1) - resJson[0].id.should.equal(project2.id) - 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({ - 'Authorization': 'Bearer ' + testUtil.jwts.copilot - }) - .expect('Content-Type', /json/) - .expect(200) - .end(function(err, res) { - if (err) { - return done(err) - } - var resJson = res.body.result.content - res.body.result.metadata.totalCount.should.equal(3) - should.exist(resJson) - resJson.should.have.lengthOf(3) - done() - }) - }) - - it('should return the project for administrator ', done => { - request(server) - .get('/v4/projects/?fields=id%2Cmembers.id') - .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.admin - }) - .expect('Content-Type', /json/) - .expect(200) - .end(function(err, res) { - if (err) { - return done(err) - } - var resJson = res.body.result.content - should.exist(resJson) - resJson.should.have.lengthOf(3) - done() - }) - }) - - it('should return all projects that match when filtering by name', done => { - request(server) - .get('/v4/projects/?filter=keyword%3Dtest') - .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.admin - }) - .expect('Content-Type', /json/) - .expect(200) - .end(function(err, res) { - if (err) { - return done(err) - } - var resJson = res.body.result.content - should.exist(resJson) - resJson.should.have.lengthOf(3) - done() - }) - }) - - it('should return the project when filtering by keyword, which matches the name', done => { - request(server) - .get('/v4/projects/?filter=keyword%3D1') - .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.admin - }) - .expect('Content-Type', /json/) - .expect(200) - .end(function(err, res) { - if (err) { - return done(err) - } - var resJson = res.body.result.content - should.exist(resJson) - resJson.should.have.lengthOf(1) - resJson[0].name.should.equal('test1') - done() - }) - }) - - it('should return the project when filtering by keyword, which matches the description', done => { - request(server) - .get('/v4/projects/?filter=keyword%3Dproject') - .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.admin - }) - .expect('Content-Type', /json/) - .expect(200) - .end(function(err, res) { - if (err) { - return done(err) - } - var resJson = res.body.result.content - should.exist(resJson) - resJson.should.have.lengthOf(3) - done() - }) - }) - - it('should return the project when filtering by keyword, which matches the details', done => { - request(server) - .get('/v4/projects/?filter=keyword%3Dcode') - .set({ - 'Authorization': 'Bearer ' + testUtil.jwts.admin - }) - .expect('Content-Type', /json/) - .expect(200) - .end(function(err, res) { - if (err) { - return done(err) - } - var resJson = res.body.result.content - should.exist(resJson) - resJson.should.have.lengthOf(1) - resJson[0].name.should.equal('test1') - done() - }) - }) - }) - -}) + + +import chai from 'chai'; +import request from 'supertest'; + +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; + +const should = chai.should(); + +/** + * Add full text index for projects. + */ +function addFullTextIndex() { + if (models.sequelize.options.dialect !== 'postgres') { + console.log('Not creating search index, must be using POSTGRES to do this'); + return; + } + + return models.sequelize + .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 + .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 OR REPLACE FUNCTION project_text_update_trigger() RETURNS trigger AS $$ ' + + 'begin ' + + 'new."projectFullText" := ' + + '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); + }); +} + +describe('LIST Project', () => { + let project1, + project2; + before((done) => { + testUtil.clearDb() + .then(() => addFullTextIndex()) + .then(() => { + const p1 = models.Project.create({ + type: 'generic', + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'active', + details: { + utm: { + code: 'code1', + }, + }, + createdBy: 1, + updatedBy: 1, + }).then((p) => { + project1 = p; + // create members + const pm1 = models.ProjectMember.create({ + userId: 40051331, + projectId: project1.id, + role: 'customer', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }); + const pm2 = models.ProjectMember.create({ + userId: 40051332, + projectId: project1.id, + role: 'copilot', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }); + const pa1 = models.ProjectAttachment.create({ + title: 'Spec', + projectId: project1.id, + description: 'specification', + filePath: 'projects/1/spec.pdf', + contentType: 'application/pdf', + createdBy: 1, + updatedBy: 1, + }); + return Promise.all([pm1, pm2, pa1]); + }); + + const p2 = models.Project.create({ + type: 'visual_design', + billingAccountId: 1, + name: 'test2', + description: 'test project2', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + }).then((p) => { + project2 = p; + return models.ProjectMember.create({ + userId: 40051332, + projectId: project2.id, + role: 'copilot', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }); + }); + const p3 = models.Project.create({ + type: 'visual_design', + billingAccountId: 1, + name: 'test2', + description: 'test project3', + status: 'reviewed', + details: {}, + createdBy: 1, + updatedBy: 1, + }); + return Promise.all([p1, p2, p3]) + .then(() => done()); + }); + }); + + after((done) => { + testUtil.clearDb(done); + }); + + describe('GET All /projects/', () => { + it('should return 403 if user is not authenticated', (done) => { + request(server) + .get('/v4/projects/') + .expect(403, done); + }); + + it('should return 200 and no projects if user does not have access', (done) => { + request(server) + .get(`/v4/projects/?filter=id%3Din%28${project2.id}%29`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200) + .end((err, res) => { + if (err) { + return done(err); + } + res.body.result.content.should.have.lengthOf(0); + done(); + }); + }); + + it('should return the project when registerd member attempts to access the project', (done) => { + request(server) + .get('/v4/projects/?filter=status%3Ddraft') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + return done(err); + } + const resJson = res.body.result.content; + res.body.result.metadata.totalCount.should.equal(1); + should.exist(resJson); + resJson.should.have.lengthOf(1); + resJson[0].id.should.equal(project2.id); + 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({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + return done(err); + } + const resJson = res.body.result.content; + res.body.result.metadata.totalCount.should.equal(3); + should.exist(resJson); + resJson.should.have.lengthOf(3); + done(); + }); + }); + + it('should return the project for administrator ', (done) => { + request(server) + .get('/v4/projects/?fields=id%2Cmembers.id') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + return done(err); + } + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(3); + done(); + }); + }); + + it('should return all projects that match when filtering by name', (done) => { + request(server) + .get('/v4/projects/?filter=keyword%3Dtest') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + return done(err); + } + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(3); + done(); + }); + }); + + it('should return the project when filtering by keyword, which matches the name', (done) => { + request(server) + .get('/v4/projects/?filter=keyword%3D1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + return done(err); + } + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(1); + resJson[0].name.should.equal('test1'); + done(); + }); + }); + + it('should return the project when filtering by keyword, which matches the description', (done) => { + request(server) + .get('/v4/projects/?filter=keyword%3Dproject') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + return done(err); + } + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(3); + done(); + }); + }); + + it('should return the project when filtering by keyword, which matches the details', (done) => { + request(server) + .get('/v4/projects/?filter=keyword%3Dcode') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + return done(err); + } + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(1); + resJson[0].name.should.equal('test1'); + done(); + }); + }); + }); +}); diff --git a/src/routes/projects/update.js b/src/routes/projects/update.js index 35d1e494..4c0aa7e8 100644 --- a/src/routes/projects/update.js +++ b/src/routes/projects/update.js @@ -1,27 +1,39 @@ -'use strict' -import validate from 'express-validation' -import _ from 'lodash' -import Joi from 'joi' -import models from '../../models' -import { PROJECT_TYPE, PROJECT_STATUS, PROJECT_MEMBER_ROLE, EVENT, USER_ROLE } from '../../constants' -import util from '../../util' -import directProject from '../../services/directProject' -import { middleware as tcMiddleware } from 'tc-core-library-js' +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 { + PROJECT_TYPE, + PROJECT_STATUS, + PROJECT_MEMBER_ROLE, + EVENT, + USER_ROLE, +} from '../../constants'; +import util from '../../util'; +import directProject from '../../services/directProject'; + /** * API to handle updating a project. */ -const permissions = tcMiddleware.permissions +const permissions = tcMiddleware.permissions; /** * Customizer used to merge project properties - * Used with lodash mergeWith, recursively merges all values except array type * values which are replaced. + * @param {Object} objValue val + * @param {Object} srcValue val + * @returns {Object} obj */ const mergeCustomizer = (objValue, srcValue) => { - if (_.isArray(objValue)) - return srcValue -} + if (_.isArray(objValue)) { + return srcValue; + } +}; const updateProjectValdiations = { body: { @@ -37,11 +49,11 @@ const updateProjectValdiations = { external: Joi.object().keys({ id: Joi.string(), type: Joi.any().valid('github', 'jira', 'asana', 'other'), - data: Joi.string().max(300) // TODO - restrict length + data: Joi.string().max(300), // TODO - restrict length }).allow(null), bookmarks: Joi.array().items(Joi.object().keys({ title: Joi.string(), - address: Joi.string() + address: Joi.string(), })).optional().allow(null), type: Joi.any().valid(_.values(PROJECT_TYPE)), details: Joi.any(), @@ -53,37 +65,39 @@ const updateProjectValdiations = { challengeEligibility: Joi.array().items(Joi.object().keys({ role: Joi.string().valid('submitter', 'reviewer', 'copilot'), users: Joi.array().items(Joi.number().positive()), - groups: Joi.array().items(Joi.number().positive()) + groups: Joi.array().items(Joi.number().positive()), })).allow(null), // cancel reason is mandatory when project status is cancelled cancelReason: Joi.when('status', { is: PROJECT_STATUS.CANCELLED, then: Joi.string().required(), - otherwise: Joi.string().optional() - }) - }) - } -} + otherwise: Joi.string().optional(), + }), + }), + }, +}; // NOTE- decided to disable all additional checks for now. -var validateUpdates = (existingProject, updatedProject) => { - var errors = [] +const validateUpdates = (existingProject) => { + const errors = []; switch (existingProject.status) { case PROJECT_STATUS.COMPLETED: - errors.push(`cannot update a project that is in ${existingProject.status}' state`) - break - // disabling this check for now. - // case PROJECT_STATUS.DRAFT: - // if (_.get(updatedProject, 'status', '') === 'active') { - // // attempting to launch the project make sure certain - // // properties are set - // if (!updatedProject.billingAccountId && !existingProject.billingAccountId) { - // errors.push('\'billingAccountId\' must be set before activating the project') - // } - // } + errors.push(`cannot update a project that is in ${existingProject.status}' state`); + break; + default: + break; + // disabling this check for now. + // case PROJECT_STATUS.DRAFT: + // if (_.get(updatedProject, 'status', '') === 'active') { + // // attempting to launch the project make sure certain + // // properties are set + // if (!updatedProject.billingAccountId && !existingProject.billingAccountId) { + // errors.push('\'billingAccountId\' must be set before activating the project') + // } + // } } - return errors -} + return errors; +}; module.exports = [ // handles request validations @@ -93,119 +107,125 @@ module.exports = [ * POST projects/ * Create a project if the user has access */ - (req, res, next) => { - var project, - updatedProps = req.body.param - var projectId = _.parseInt(req.params.projectId) + (req, res, next) => { + let project; + 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(() => { - return models.Project.findOne({ - where: { id: projectId }, - lock: { of: models.Project } - }) - .then((_prj) => { - project = _prj - if (!project) { - // handle 404 - let err = new Error(`project not found for id ${projectId}`) - err.status = 404 - return Promise.reject(err) - } - previousValue = _.clone(project.get({plain: true})) - // run additional validations - let validationErrors = validateUpdates(previousValue, updatedProps) - if(validationErrors.length > 0) { - let err = new Error('Unable to update project') - _.assign(err, { - details: JSON.stringify(validationErrors), - status: 400 - }) - return Promise.reject(err) - } - // Only project manager (user with manager role assigned) or topcoder admin should be allowed - // to transition project status to 'active'. - const members = req.context.currentProjectMembers - const validRoles = [PROJECT_MEMBER_ROLE.MANAGER, PROJECT_MEMBER_ROLE.MANAGER].map(x => x.toLowerCase()) - const matchRole = (role) => _.indexOf(validRoles, role.toLowerCase()) >= 0 - if (updatedProps.status === PROJECT_STATUS.ACTIVE && - !util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) && - _.isUndefined(_.find(members, (m) => m.userId === req.authUser.userId && matchRole(m.role)))) { - let err = new Error('Only assigned topcoder-managers or topcoder admins should be allowed to launch a project') - err.status = 403 - return Promise.reject(err) - } + let previousValue; + models.sequelize.transaction(() => models.Project.findOne({ + where: { + id: projectId, + }, + lock: { of: models.Project }, + }) + .then((_prj) => { + project = _prj; + if (!project) { + // handle 404 + const err = new Error(`project not found for id ${projectId}`); + err.status = 404; + return Promise.reject(err); + } + previousValue = _.clone(project.get({ plain: true })); + // run additional validations + const validationErrors = validateUpdates(previousValue, updatedProps); + if (validationErrors.length > 0) { + const err = new Error('Unable to update project'); + _.assign(err, { + details: JSON.stringify(validationErrors), + status: 400, + }); + return Promise.reject(err); + } + // Only project manager (user with manager role assigned) or topcoder + // admin should be allowed to transition project status to 'active'. + const members = req.context.currentProjectMembers; + const validRoles = [ + PROJECT_MEMBER_ROLE.MANAGER, + PROJECT_MEMBER_ROLE.MANAGER, + ].map(x => x.toLowerCase()); + const matchRole = role => _.indexOf(validRoles, role.toLowerCase()) >= 0; + if (updatedProps.status === PROJECT_STATUS.ACTIVE && + !util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) && + _.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'); + err.status = 403; + return Promise.reject(err); + } - // no updates if same + // no updates if same - if (_.isEqual(previousValue, updatedProps)) { - return Promise.resolve() - } - updatedProps.updatedBy = req.authUser.userId - const newValues = _.mergeWith({}, previousValue, updatedProps, mergeCustomizer) - project.set(newValues) - return project.save() - }) - .then(() => { - if (updatedProps.billingAccountId && (previousValue.billingAccountId !== updatedProps.billingAccountId)) { - if(!previousValue.directProjectId){ - return Promise.resolve() - } else { - // if billing account is updated and exist direct project id we should invoke direct project service - return directProject.addBillingAccount(req, previousValue.directProjectId, { - billingAccountId: updatedProps.billingAccountId - }) - } - } else { - return Promise.resolve() + if (_.isEqual(previousValue, updatedProps)) { + return Promise.resolve(); + } + updatedProps.updatedBy = req.authUser.userId; + const newValues = _.mergeWith({}, previousValue, updatedProps, mergeCustomizer); + project.set(newValues); + return project.save(); + }) + .then(() => { + if (updatedProps.billingAccountId && + (previousValue.billingAccountId !== updatedProps.billingAccountId)) { + if (!previousValue.directProjectId) { + return Promise.resolve(); } - }) - .then(() => { - return project.reload(project.id) - }) - // update project history - .then(() => { - return new Promise((accept, reject) => { - // we only want to have project history when project status is updated - if (updatedProps.status && (updatedProps.status !== previousValue.status)) { - models.ProjectHistory.create({ - projectId: project.id, - status: updatedProps.status, - cancelReason: updatedProps.cancelReason, - updatedBy: req.authUser.userId - }).then(() => accept()).catch((err) => reject(err)) - } else { - accept() - } - }) - }) - .then(() => { - project = project.get({plain: true}) - project = _.omit(project, ['deletedAt']) - req.log.debug('updated project', project) - previousValue = _.omit(previousValue, ['deletedAt']) - // publish original and updated project data - req.app.services.pubsub.publish( - EVENT.ROUTING_KEY.PROJECT_UPDATED, - { original: previousValue, updated: project }, - { correlationId: req.id } - ) - req.app.emit(EVENT.ROUTING_KEY.PROJECT_UPDATED, { - req, + // if billing account is updated and exist direct projectId we + // should invoke direct project service + return directProject.addBillingAccount(req, previousValue.directProjectId, { + billingAccountId: updatedProps.billingAccountId, + }); + } + return Promise.resolve(); + }) + .then(() => project.reload(project.id)) + // update project history + .then(() => new Promise((accept, reject) => { + // we only want to have project history when project status is updated + if (updatedProps.status && (updatedProps.status !== previousValue.status)) { + models.ProjectHistory.create({ + projectId: project.id, + status: updatedProps.status, + cancelReason: updatedProps.cancelReason, + updatedBy: req.authUser.userId, + }).then(() => accept()).catch(err => reject(err)); + } else { + accept(); + } + })) + .then(() => { + project = project.get({ plain: true }); + project = _.omit(project, ['deletedAt']); + req.log.debug('updated project', project); + previousValue = _.omit(previousValue, ['deletedAt']); + // publish original and updated project data + req.app.services.pubsub.publish( + EVENT.ROUTING_KEY.PROJECT_UPDATED, { original: previousValue, - updated: project - }) - // check context for project members - project.members = req.context.currentProjectMembers - // get attachments - return util.getProjectAttachments(req, project.id) - }) - }).then((attachments) => { - // make sure we only send response after transaction is committed - project.attachments = attachments - res.json(util.wrapResponse(req.id, project)) - }).catch((err) => next(err)) - } -] + updated: project, + }, { + correlationId: req.id, + }, + ); + req.app.emit(EVENT.ROUTING_KEY.PROJECT_UPDATED, { + req, + original: previousValue, + updated: project, + }); + // check context for project members + project.members = req.context.currentProjectMembers; + // get attachments + return util.getProjectAttachments(req, project.id); + })) + .then((attachments) => { + // make sure we only send response after transaction is committed + project.attachments = attachments; + res.json(util.wrapResponse(req.id, project)); + }) + .catch(err => next(err)); + }, +]; diff --git a/src/routes/projects/update.spec.js b/src/routes/projects/update.spec.js index 70751fed..d27031af 100644 --- a/src/routes/projects/update.spec.js +++ b/src/routes/projects/update.spec.js @@ -1,38 +1,40 @@ -'use strict' -import _ from 'lodash' -import chai from 'chai' -import sinon from 'sinon' -import request from 'supertest' -import models from '../../models' -import server from '../../app' -import testUtil from '../../tests/util' -import util from '../../util' -import { PROJECT_STATUS } from '../../constants' +import _ from 'lodash'; +import chai from 'chai'; +import sinon from 'sinon'; +import request from 'supertest'; -var should = chai.should() +import models from '../../models'; +import server from '../../app'; +import testUtil from '../../tests/util'; +import util from '../../util'; +import { PROJECT_STATUS } from '../../constants'; + +const should = chai.should(); describe('Project', () => { - var project1, project2, project3 - beforeEach(done => { - testUtil.clearDb(done) - }) + let project1, + project2, + project3; + beforeEach((done) => { + testUtil.clearDb(done); + }); - after(done => { - testUtil.clearDb(done) - }) + after((done) => { + testUtil.clearDb(done); + }); describe('PATCH /projects', () => { - var body = { + const body = { param: { - name: 'updatedProject name' - } - } - var sandbox + name: 'updatedProject name', + }, + }; + let sandbox; afterEach(() => { - sandbox.restore() - }) - beforeEach(done => { - sandbox = sinon.sandbox.create() + sandbox.restore(); + }); + beforeEach((done) => { + sandbox = sinon.sandbox.create(); models.Project.bulkCreate([{ type: 'generic', directProjectId: 1, @@ -43,8 +45,8 @@ describe('Project', () => { details: {}, createdBy: 1, updatedBy: 1, - createdAt: "2016-06-30 00:33:07+00", - updatedAt: "2016-06-30 00:33:07+00" + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', }, { type: 'generic', billingAccountId: 2, @@ -54,9 +56,9 @@ describe('Project', () => { details: {}, createdBy: 1, updatedBy: 1, - createdAt: "2016-06-30 00:33:07+00", - updatedAt: "2016-06-30 00:33:07+00" - },{ + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', + }, { type: 'generic', name: 'test3', description: 'test project3', @@ -64,404 +66,404 @@ describe('Project', () => { details: {}, createdBy: 1, updatedBy: 1, - createdAt: "2016-06-30 00:33:07+00", - updatedAt: "2016-06-30 00:33:07+00" + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', }]) - .then(()=> models.Project.findAll()) - .then((projects)=> { - project1 = projects[0] - project2 = projects[1] - project3 = projects[2] + .then(() => models.Project.findAll()) + .then((projects) => { + project1 = projects[0]; + project2 = projects[1]; + project3 = projects[2]; return models.ProjectMember.bulkCreate([{ projectId: project1.id, role: 'copilot', userId: 40051332, createdBy: 1, - updatedBy: 1 - },{ + updatedBy: 1, + }, { projectId: project1.id, role: 'manager', userId: 40051334, createdBy: 1, - updatedBy: 1 + updatedBy: 1, }, { projectId: project2.id, role: 'copilot', userId: 40051332, createdBy: 1, - updatedBy: 1 - }]) - }).then(()=> done()) - }) + updatedBy: 1, + }]); + }).then(() => done()); + }); - it('should return 403 if user is not authenticated', done => { + it('should return 403 if user is not authenticated', (done) => { request(server) - .patch("/v4/projects/" + project1.id) + .patch(`/v4/projects/${project1.id}`) .send(body) - .expect(403, done) - }) + .expect(403, done); + }); - it('should return 400 if update completed project', done => { + it('should return 400 if update completed project', (done) => { request(server) .patch(`/v4/projects/${project2.id}`) - .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) .send(body) .expect('Content-Type', /json/) .expect(400) - .end(function (err, res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var result = res.body.result - result.success.should.be.false - result.status.should.equal(400) - result.content.message.should.equal('Unable to update project') - done() - }) - }) + const result = res.body.result; + result.success.should.be.false; + result.status.should.equal(400); + result.content.message.should.equal('Unable to update project'); + done(); + }); + }); - it('should return 403 if invalid user will launch a project', done => { + it('should return 403 if invalid user will launch a project', (done) => { request(server) .patch(`/v4/projects/${project1.id}`) - .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) .send({ param: { - status: 'active' - } + status: 'active', + }, }) .expect('Content-Type', /json/) .expect(403) - .end(function (err, res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var result = res.body.result - result.success.should.be.false - result.status.should.equal(403) + const result = res.body.result; + result.success.should.be.false; + result.status.should.equal(403); result.content.message.should.equal('Only assigned topcoder-managers or topcoder admins' + - ' should be allowed to launch a project') - done() - }) - }) + ' should be allowed to launch a project'); + done(); + }); + }); - it('should return 200 if topcoder manager user will launch a project', done => { + it('should return 200 if topcoder manager user will launch a project', (done) => { request(server) .patch(`/v4/projects/${project1.id}`) - .set({"Authorization": "Bearer " + testUtil.jwts.manager}) + .set({ Authorization: `Bearer ${testUtil.jwts.manager}` }) .send({ param: { - status: 'active' - } + status: 'active', + }, }) .expect('Content-Type', /json/) .expect(200) - .end(function (err, res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var result = res.body.result - result.success.should.be.true - result.status.should.equal(200) - result.content.status.should.equal('active') - server.services.pubsub.publish.calledWith('project.updated').should.be.true - done() - }) - }) + const result = res.body.result; + result.success.should.be.true; + result.status.should.equal(200); + result.content.status.should.equal('active'); + server.services.pubsub.publish.calledWith('project.updated').should.be.true; + done(); + }); + }); - it('should return 200 if valid user and data', done => { + it('should return 200 if valid user and data', (done) => { request(server) - .patch("/v4/projects/" + project1.id) - .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .patch(`/v4/projects/${project1.id}`) + .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) .send(body) .expect('Content-Type', /json/) .expect(200) - .end(function (err,res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var resJson = res.body.result.content - should.exist(resJson) - resJson.name.should.equal('updatedProject name') - resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") - resJson.updatedBy.should.equal(40051332) - server.services.pubsub.publish.calledWith('project.updated').should.be.true - done() - }) - }) + const resJson = res.body.result.content; + should.exist(resJson); + resJson.name.should.equal('updatedProject name'); + resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); + resJson.updatedBy.should.equal(40051332); + server.services.pubsub.publish.calledWith('project.updated').should.be.true; + done(); + }); + }); - it('should return 200 and project history should be updated (status is not set)', done => { - const sbody = _.cloneDeep(body) + it('should return 200 and project history should be updated (status is not set)', (done) => { + const sbody = _.cloneDeep(body); // set project status to be updated - sbody.param.status = PROJECT_STATUS.IN_REVIEW + sbody.param.status = PROJECT_STATUS.IN_REVIEW; request(server) - .patch("/v4/projects/" + project1.id) - .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .patch(`/v4/projects/${project1.id}`) + .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) .send(sbody) .expect('Content-Type', /json/) .expect(200) - .end(function (err,res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var resJson = res.body.result.content - should.exist(resJson) - resJson.name.should.equal('updatedProject name') - resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") - resJson.updatedBy.should.equal(40051332) - server.services.pubsub.publish.calledWith('project.updated').should.be.true + const resJson = res.body.result.content; + should.exist(resJson); + resJson.name.should.equal('updatedProject name'); + resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); + resJson.updatedBy.should.equal(40051332); + server.services.pubsub.publish.calledWith('project.updated').should.be.true; // validate that project history is updated models.ProjectHistory.findAll({ limit: 1, where: { projectId: project1.id }, - order: [['createdAt', 'DESC']] + order: [['createdAt', 'DESC']], }).then((histories) => { - should.exist(histories) - histories.length.should.equal(1) - const history = histories[0].get({ plain: true }) - history.status.should.equal(PROJECT_STATUS.IN_REVIEW) - history.projectId.should.equal(project1.id) - done() - }) - }) - }) + should.exist(histories); + histories.length.should.equal(1); + const history = histories[0].get({ plain: true }); + history.status.should.equal(PROJECT_STATUS.IN_REVIEW); + history.projectId.should.equal(project1.id); + done(); + }); + }); + }); - it('should return 200 and project history should not be updated (status is not updated)', done => { - const sbody = _.cloneDeep(body) - sbody.param.status = PROJECT_STATUS.DRAFT + it('should return 200 and project history should not be updated (status is not updated)', (done) => { + const sbody = _.cloneDeep(body); + sbody.param.status = PROJECT_STATUS.DRAFT; request(server) - .patch("/v4/projects/" + project1.id) - .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .patch(`/v4/projects/${project1.id}`) + .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) .send(sbody) .expect('Content-Type', /json/) .expect(200) - .end(function (err,res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var resJson = res.body.result.content - should.exist(resJson) - resJson.name.should.equal('updatedProject name') - resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") - resJson.updatedBy.should.equal(40051332) - server.services.pubsub.publish.calledWith('project.updated').should.be.true + const resJson = res.body.result.content; + should.exist(resJson); + resJson.name.should.equal('updatedProject name'); + resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); + resJson.updatedBy.should.equal(40051332); + server.services.pubsub.publish.calledWith('project.updated').should.be.true; // validate that project history is not updated models.ProjectHistory.findAll({ - where: { projectId: project1.id } + where: { projectId: project1.id }, }).then((histories) => { - should.exist(histories) - histories.length.should.equal(0) - done() - }) - }) - }) + should.exist(histories); + histories.length.should.equal(0); + done(); + }); + }); + }); - it('should return 422 as cancel reason is mandatory if project status is cancelled', done => { - const sbody = _.cloneDeep(body) - sbody.param.status = PROJECT_STATUS.CANCELLED + it('should return 422 as cancel reason is mandatory if project status is cancelled', (done) => { + const sbody = _.cloneDeep(body); + sbody.param.status = PROJECT_STATUS.CANCELLED; request(server) - .patch("/v4/projects/" + project1.id) - .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .patch(`/v4/projects/${project1.id}`) + .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) .send(sbody) .expect('Content-Type', /json/) .expect(422) - .end(function (err,res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - const result = res.body.result - result.success.should.be.false - result.status.should.equal(422) - done() - }) - }) + const result = res.body.result; + result.success.should.be.false; + result.status.should.equal(422); + done(); + }); + }); - it('should return 200 and project history should be updated for cancelled project', done => { - const sbody = _.cloneDeep(body) - sbody.param.status = PROJECT_STATUS.CANCELLED - sbody.param.cancelReason = 'price/cost' + it('should return 200 and project history should be updated for cancelled project', (done) => { + const sbody = _.cloneDeep(body); + sbody.param.status = PROJECT_STATUS.CANCELLED; + sbody.param.cancelReason = 'price/cost'; request(server) - .patch("/v4/projects/" + project1.id) - .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .patch(`/v4/projects/${project1.id}`) + .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) .send(sbody) .expect('Content-Type', /json/) .expect(200) - .end(function (err,res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var resJson = res.body.result.content - should.exist(resJson) - resJson.name.should.equal('updatedProject name') - resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") - resJson.updatedBy.should.equal(40051332) - server.services.pubsub.publish.calledWith('project.updated').should.be.true + const resJson = res.body.result.content; + should.exist(resJson); + resJson.name.should.equal('updatedProject name'); + resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); + resJson.updatedBy.should.equal(40051332); + server.services.pubsub.publish.calledWith('project.updated').should.be.true; // validate that project history is updated models.ProjectHistory.findAll({ - where: { projectId: project1.id } + where: { projectId: project1.id }, }).then((histories) => { - should.exist(histories) - histories.length.should.equal(1) - const history = histories[0].get({ plain: true }) - history.status.should.equal(PROJECT_STATUS.CANCELLED) - history.projectId.should.equal(project1.id) - history.cancelReason.should.equal('price/cost') - done() - }) - }) - }) + should.exist(histories); + histories.length.should.equal(1); + const history = histories[0].get({ plain: true }); + history.status.should.equal(PROJECT_STATUS.CANCELLED); + history.projectId.should.equal(project1.id); + history.cancelReason.should.equal('price/cost'); + done(); + }); + }); + }); - it('should return 200, manager is allowed to transition project out of cancel status', done => { - models.Project.update({ status: PROJECT_STATUS.CANCELLED}, {where: {id: project1.id}}) + it('should return 200, manager is allowed to transition project out of cancel status', (done) => { + models.Project.update({ status: PROJECT_STATUS.CANCELLED }, { where: { id: project1.id } }) .then(() => { - const sbody = _.cloneDeep(body) - sbody.param.status = PROJECT_STATUS.ACTIVE + const sbody = _.cloneDeep(body); + sbody.param.status = PROJECT_STATUS.ACTIVE; request(server) - .patch("/v4/projects/" + project1.id) - .set({"Authorization": "Bearer " + testUtil.jwts.manager}) + .patch(`/v4/projects/${project1.id}`) + .set({ Authorization: `Bearer ${testUtil.jwts.manager}` }) .send(sbody) .expect('Content-Type', /json/) .expect(200) - .end(function (err,res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var resJson = res.body.result.content - should.exist(resJson) - resJson.name.should.equal('updatedProject name') - resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") - resJson.updatedBy.should.equal(40051334) - server.services.pubsub.publish.calledWith('project.updated').should.be.true + const resJson = res.body.result.content; + should.exist(resJson); + resJson.name.should.equal('updatedProject name'); + resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); + resJson.updatedBy.should.equal(40051334); + server.services.pubsub.publish.calledWith('project.updated').should.be.true; // validate that project history is updated models.ProjectHistory.findAll({ - where: { projectId: project1.id } + where: { projectId: project1.id }, }).then((histories) => { - should.exist(histories) - histories.length.should.equal(1) - const history = histories[0].get({ plain: true }) - history.status.should.equal(PROJECT_STATUS.ACTIVE) - history.projectId.should.equal(project1.id) - done() - }) - }) - }) - }) + should.exist(histories); + histories.length.should.equal(1); + const history = histories[0].get({ plain: true }); + history.status.should.equal(PROJECT_STATUS.ACTIVE); + history.projectId.should.equal(project1.id); + done(); + }); + }); + }); + }); - it('should return 200, admin is allowed to transition project out of cancel status', done => { - models.Project.update({ status: PROJECT_STATUS.CANCELLED}, {where: {id: project1.id}}) + it('should return 200, admin is allowed to transition project out of cancel status', (done) => { + models.Project.update({ status: PROJECT_STATUS.CANCELLED }, { where: { id: project1.id } }) .then(() => { - const sbody = _.cloneDeep(body) - sbody.param.status = PROJECT_STATUS.ACTIVE + const sbody = _.cloneDeep(body); + sbody.param.status = PROJECT_STATUS.ACTIVE; request(server) - .patch("/v4/projects/" + project1.id) - .set({"Authorization": "Bearer " + testUtil.jwts.admin}) + .patch(`/v4/projects/${project1.id}`) + .set({ Authorization: `Bearer ${testUtil.jwts.admin}` }) .send(sbody) .expect('Content-Type', /json/) .expect(200) - .end(function (err,res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var resJson = res.body.result.content - should.exist(resJson) - resJson.name.should.equal('updatedProject name') - resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") - resJson.updatedBy.should.equal(40051333) - server.services.pubsub.publish.calledWith('project.updated').should.be.true + const resJson = res.body.result.content; + should.exist(resJson); + resJson.name.should.equal('updatedProject name'); + resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); + resJson.updatedBy.should.equal(40051333); + server.services.pubsub.publish.calledWith('project.updated').should.be.true; // validate that project history is updated models.ProjectHistory.findAll({ - where: { projectId: project1.id } + where: { projectId: project1.id }, }).then((histories) => { - should.exist(histories) - histories.length.should.equal(1) - const history = histories[0].get({ plain: true }) - history.status.should.equal(PROJECT_STATUS.ACTIVE) - history.projectId.should.equal(project1.id) - done() - }) - }) - }) - }) + should.exist(histories); + histories.length.should.equal(1); + const history = histories[0].get({ plain: true }); + history.status.should.equal(PROJECT_STATUS.ACTIVE); + history.projectId.should.equal(project1.id); + done(); + }); + }); + }); + }); - it('should return 403, copilot is not allowed to transition project out of cancel status', done => { - models.Project.update({ status: PROJECT_STATUS.CANCELLED}, {where: {id: project1.id}}) + it('should return 403, copilot is not allowed to transition project out of cancel status', (done) => { + models.Project.update({ status: PROJECT_STATUS.CANCELLED }, { where: { id: project1.id } }) .then(() => { - const sbody = _.cloneDeep(body) - sbody.param.status = PROJECT_STATUS.ACTIVE + const sbody = _.cloneDeep(body); + sbody.param.status = PROJECT_STATUS.ACTIVE; request(server) - .patch("/v4/projects/" + project1.id) - .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .patch(`/v4/projects/${project1.id}`) + .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) .send(sbody) .expect('Content-Type', /json/) .expect(403) - .end(function (err,res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - const result = res.body.result - result.success.should.be.false - result.status.should.equal(403) - done() - }) - }) - }) + const result = res.body.result; + result.success.should.be.false; + result.status.should.equal(403); + done(); + }); + }); + }); - it('should return 200 and project history should not be updated', done => { + it('should return 200 and project history should not be updated', (done) => { request(server) - .patch("/v4/projects/" + project1.id) - .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .patch(`/v4/projects/${project1.id}`) + .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) .send(body) .expect('Content-Type', /json/) .expect(200) - .end(function (err,res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var resJson = res.body.result.content - should.exist(resJson) - resJson.name.should.equal('updatedProject name') - resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") - resJson.updatedBy.should.equal(40051332) - server.services.pubsub.publish.calledWith('project.updated').should.be.true + const resJson = res.body.result.content; + should.exist(resJson); + resJson.name.should.equal('updatedProject name'); + resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); + resJson.updatedBy.should.equal(40051332); + server.services.pubsub.publish.calledWith('project.updated').should.be.true; // validate that project history is not updated models.ProjectHistory.findAll({ - where: { projectId: project1.id } + where: { projectId: project1.id }, }).then((histories) => { - should.exist(histories) - histories.length.should.equal(0) - done() - }) - }) - }) + should.exist(histories); + histories.length.should.equal(0); + done(); + }); + }); + }); - it('should return 500 if error to sync billing account id', done => { - var mockHttpClient = _.merge(testUtil.mockHttpClient, { - post: () => Promise.reject(new Error('error message')) - }) - sandbox.stub(util, 'getHttpClient', () => mockHttpClient ) + it('should return 500 if error to sync billing account id', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + post: () => Promise.reject(new Error('error message')), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .patch("/v4/projects/" + project1.id) - .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .patch(`/v4/projects/${project1.id}`) + .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) .send({ param: { - billingAccountId: 123 - } + billingAccountId: 123, + }, }) .expect('Content-Type', /json/) .expect(500) - .end(function (err,res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - const result = res.body.result - result.success.should.be.false - result.status.should.equal(500) - result.content.message.should.equal('error message') - done() - }) - }) + const result = res.body.result; + result.success.should.be.false; + result.status.should.equal(500); + result.content.message.should.equal('error message'); + done(); + }); + }); - it('should return 200 and sync new billing account id', done => { - var mockHttpClient = _.merge(testUtil.mockHttpClient, { + it('should return 200 and sync new billing account id', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { post: () => Promise.resolve({ status: 200, data: { @@ -471,134 +473,132 @@ describe('Project', () => { success: true, status: 200, content: { - billingAccountName: '2' - } - } - } - }) - }) - const postSpy = sinon.spy(mockHttpClient, 'post') - sandbox.stub(util, 'getHttpClient', () => mockHttpClient ) + billingAccountName: '2', + }, + }, + }, + }), + }); + const postSpy = sinon.spy(mockHttpClient, 'post'); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .patch("/v4/projects/" + project1.id) - .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .patch(`/v4/projects/${project1.id}`) + .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) .send({ param: { - billingAccountId: 123 - } + billingAccountId: 123, + }, }) .expect('Content-Type', /json/) .expect(200) - .end(function (err,res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var resJson = res.body.result.content - should.exist(resJson) - resJson.billingAccountId.should.equal(123) - resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") - resJson.updatedBy.should.equal(40051332) - postSpy.should.have.been.calledOnce - server.services.pubsub.publish.calledWith('project.updated').should.be.true - done() - }) - }) + const resJson = res.body.result.content; + should.exist(resJson); + resJson.billingAccountId.should.equal(123); + resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); + resJson.updatedBy.should.equal(40051332); + postSpy.should.have.been.calledOnce; + server.services.pubsub.publish.calledWith('project.updated').should.be.true; + done(); + }); + }); - it('should return 200 and not sync same billing account id', done => { + it('should return 200 and not sync same billing account id', (done) => { request(server) - .patch("/v4/projects/" + project1.id) - .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .patch(`/v4/projects/${project1.id}`) + .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) .send({ param: { - billingAccountId: 1 - } + billingAccountId: 1, + }, }) .expect('Content-Type', /json/) .expect(200) - .end(function (err,res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var resJson = res.body.result.content - should.exist(resJson) - resJson.billingAccountId.should.equal(1) - resJson.billingAccountId.should.equal(1) - server.services.pubsub.publish.calledWith('project.updated').should.be.true - done() - }) - }) + const resJson = res.body.result.content; + should.exist(resJson); + resJson.billingAccountId.should.equal(1); + resJson.billingAccountId.should.equal(1); + server.services.pubsub.publish.calledWith('project.updated').should.be.true; + done(); + }); + }); - it('should return 200 and not sync same billing account id for project without direct project id', done => { + it('should return 200 and not sync same billing account id for project without direct project id', (done) => { request(server) - .patch("/v4/projects/" + project3.id) - .set({"Authorization": "Bearer " + testUtil.jwts.admin}) + .patch(`/v4/projects/${project3.id}`) + .set({ Authorization: `Bearer ${testUtil.jwts.admin}` }) .send({ param: { - billingAccountId: 1 - } + billingAccountId: 1, + }, }) .expect('Content-Type', /json/) .expect(200) - .end(function (err,res) { + .end((err, res) => { if (err) { - return done(err) + return done(err); } - var resJson = res.body.result.content - should.exist(resJson) - resJson.billingAccountId.should.equal(1) - resJson.updatedAt.should.not.equal("2016-06-30 00:33:07+00") - resJson.updatedBy.should.equal(40051333) - server.services.pubsub.publish.calledWith('project.updated').should.be.true - done() - }) - }) + const resJson = res.body.result.content; + should.exist(resJson); + resJson.billingAccountId.should.equal(1); + resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); + resJson.updatedBy.should.equal(40051333); + server.services.pubsub.publish.calledWith('project.updated').should.be.true; + done(); + }); + }); - it.skip('should return 200 and update bookmarks', done => { + it.skip('should return 200 and update bookmarks', (done) => { request(server) - .patch("/v4/projects/" + project1.id) - .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .patch(`/v4/projects/${project1.id}`) + .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) .send({ - param: { - bookmarks:[{ - title:'title1', - address:'address1' - }] - } + param: { + bookmarks: [{ + title: 'title1', + address: 'address1', + }], + }, }) .expect('Content-Type', /json/) .expect(200) - .end(function (err,res) { - if (err) { - return done(err) - } - var resJson = res.body.result.content - should.exist(resJson) - resJson.bookmarks.should.have.lengthOf(1) - resJson.bookmarks[0].title.should.be.eql('title1') - resJson.bookmarks[0].address.should.be.eql('address1') - request(server) - .patch("/v4/projects/" + project1.id) - .set({"Authorization": "Bearer " + testUtil.jwts.copilot}) + .end((err, res) => { + if (err) { + return done(err); + } + let resJson = res.body.result.content; + should.exist(resJson); + resJson.bookmarks.should.have.lengthOf(1); + resJson.bookmarks[0].title.should.be.eql('title1'); + resJson.bookmarks[0].address.should.be.eql('address1'); + request(server) + .patch(`/v4/projects/${project1.id}`) + .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) .send({ - param: { - bookmarks: null - } + param: { + bookmarks: null, + }, }) .expect('Content-Type', /json/) .expect(200) - .end(function (err,res) { - if (err) { - return done(err) - } - resJson = res.body.result.content - should.exist(resJson) - should.not.exist(resJson.bookmarks); - server.services.pubsub.publish.calledWith('project.updated').should.be.true - done() - }) - - }) - }) - }) - -}) + .end((err, res) => { + if (err) { + return done(err); + } + resJson = res.body.result.content; + should.exist(resJson); + should.not.exist(resJson.bookmarks); + server.services.pubsub.publish.calledWith('project.updated').should.be.true; + done(); + }); + }); + }); + }); +}); diff --git a/src/services/directProject.js b/src/services/directProject.js index 9d54dfb8..8957e9b3 100644 --- a/src/services/directProject.js +++ b/src/services/directProject.js @@ -1,77 +1,83 @@ -'use strict' -import util from '../util' -import _ from 'lodash' -import config from 'config' + +import _ from 'lodash'; +import config from 'config'; +import util from '../util'; + /** * Service methods to handle direct project. */ /** * Build custom http client for request - * @param req request - * @returns custom http client + * @param {Object} req request + * @returns {Object} custom http client * @private */ -function _getHttpClient(req) { - var httpClient = util.getHttpClient(req) - httpClient.defaults.headers.common['Authorization'] = req.headers.authorization - httpClient.defaults.headers.common['Content-Type'] = 'application/json' - httpClient.defaults.baseURL = config.get('directProjectServiceEndpoint') - httpClient.defaults.timeout = _.get(config, 'directProjectServiceTimeout', 5000) +function getHttpClient(req) { + const httpClient = util.getHttpClient(req); + httpClient.defaults.headers.common.Authorization = req.headers.authorization; + httpClient.defaults.headers.common['Content-Type'] = 'application/json'; + httpClient.defaults.baseURL = config.get('directProjectServiceEndpoint'); + httpClient.defaults.timeout = _.get(config, 'directProjectServiceTimeout', 5000); httpClient.interceptors.response.use((resp) => { // req.log.debug('resp: ', JSON.stringify(resp.data, null, 2)) if (resp.status !== 200 || resp.data.result.status !== 200) { - req.log.error('error resp: ', JSON.stringify(resp.data, null, 2)) - return Promise.reject(new Error(resp.data.result.content.message)) + req.log.error('error resp: ', JSON.stringify(resp.data, null, 2)); + return Promise.reject(new Error(resp.data.result.content.message)); } - return Promise.resolve(resp) - }) - return httpClient + return Promise.resolve(resp); + }); + return httpClient; } export default { /** * Create direct project - * @param req the request - * @param body the body contains project information + * @param {Object} req request + * @param {Object} body the body contains project information + * @returns {Promise} create direct project promise */ - createDirectProject: (req, body) =>_getHttpClient(req) + createDirectProject: (req, body) => getHttpClient(req) .post('/projects/', body), /** * Add direct project copilot - * @param req the request - * @param directProjectId the id of direct project - * @param body the body contains project copilot information + * @param {Object} req request + * @param {Object} directProjectId the id of direct project + * @param {Object} body the body contains project copilot information + * @returns {Promise} add pilot promise */ - addCopilot: (req, directProjectId, body) => _getHttpClient(req) + addCopilot: (req, directProjectId, body) => getHttpClient(req) .post(`/projects/${directProjectId}/copilot`, body), /** * Remove direct project copilot - * @param req the request - * @param directProjectId the id of direct project - * @param body the body contains project copilot information + * @param {Object} req the request + * @param {Integer} directProjectId the id of direct project + * @param {Object} body the body contains project copilot information + * @returns {Promise} response promise */ - deleteCopilot: (req, directProjectId, body) => _getHttpClient(req) + deleteCopilot: (req, directProjectId, body) => getHttpClient(req) .delete(`/projects/${directProjectId}/copilot`, body), /** * Add billing account for direct project - * @param req the request - * @param directProjectId the id of direct project - * @param body the body contains billing account information + * @param {Object} req the request + * @param {String} directProjectId the id of direct project + * @param {Object} body the body contains billing account information + * @returns {Promise} add billing account promise */ - addBillingAccount: (req, directProjectId, body) => _getHttpClient(req) + addBillingAccount: (req, directProjectId, body) => getHttpClient(req) .post(`/projects/${directProjectId}/billingaccount`, body), /** * Add/remove direct project permissions * This can be used to add/remove direct project manager - * @param req the request - * @param directProjectId the id of direct project - * @param body the body contains permissions information + * @param {Object} req the request + * @param {Integer} directProjectId the id of direct project + * @param {Object} body the body contains permissions information + * @returns {Promise} promise */ - editProjectPermissions: (req, directProjectId, body) => _getHttpClient(req) + editProjectPermissions: (req, directProjectId, body) => getHttpClient(req) .post(`/projects/${directProjectId}/permissions`, body), -} +}; diff --git a/src/services/fileService.js b/src/services/fileService.js index a79f607b..fdbc56cb 100644 --- a/src/services/fileService.js +++ b/src/services/fileService.js @@ -1,6 +1,6 @@ -'use strict' -import util from '../util' -import config from 'config' + +import util from '../util'; +import config from 'config'; /** * Service methods to handle direct project. */ @@ -11,20 +11,20 @@ import config from 'config' * @returns custom http client * @private */ -function _getHttpClient(req){ - var httpClient = util.getHttpClient(req) - httpClient.defaults.headers.common['Authorization'] = req.headers.authorization - httpClient.defaults.baseURL = config.get('fileServiceEndpoint') - httpClient.defaults.timeout = 3000 - httpClient.interceptors.response.use((resp) => { - req.log.debug('resp: ', JSON.stringify(resp.data, null, 2)) - if (resp.status !== 200 || resp.data.result.status !== 200) { - req.log.error('error resp: ', JSON.stringify(resp.data, null, 2)) - return Promise.reject(new Error(resp.data.result.content.message)) - } - return Promise.resolve(resp) - }) - return httpClient +function _getHttpClient(req) { + const httpClient = util.getHttpClient(req); + httpClient.defaults.headers.common.Authorization = req.headers.authorization; + httpClient.defaults.baseURL = config.get('fileServiceEndpoint'); + httpClient.defaults.timeout = 3000; + httpClient.interceptors.response.use((resp) => { + req.log.debug('resp: ', JSON.stringify(resp.data, null, 2)); + if (resp.status !== 200 || resp.data.result.status !== 200) { + req.log.error('error resp: ', JSON.stringify(resp.data, null, 2)); + return Promise.reject(new Error(resp.data.result.content.message)); + } + return Promise.resolve(resp); + }); + return httpClient; } @@ -33,6 +33,6 @@ export default { * Delete file from S3 using fileservice */ deleteFile(req, filePath) { - _getHttpClient(req).delete('', { params: { filter:`filePath%3D${filePath}` }}) - } -} + _getHttpClient(req).delete('', { params: { filter: `filePath%3D${filePath}` } }); + }, +}; diff --git a/src/services/index.js b/src/services/index.js index ac0c5eaa..9eaaabc6 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -1,33 +1,33 @@ -'use strict' -import config from 'config' -import RabbitMQService from './rabbitmq' + +import config from 'config'; +import RabbitMQService from './rabbitmq'; /** * Responsible for establishing connections to all external services * Also has a hook to load mock services for unit testing. */ module.exports = (app, logger) => { - app.services = app.service || {} + app.services = app.service || {}; if (process.env.NODE_ENV.toLowerCase() === 'test') { - require('../tests/serviceMocks')(app) + require('../tests/serviceMocks')(app); } else { // RabbitMQ Initialization - app.services.pubsub = new RabbitMQService(app, logger) + app.services.pubsub = new RabbitMQService(app, logger); // initialize RabbitMQ app.services.pubsub.init( config.get('rabbitmqURL'), config.get('pubsubExchangeName'), - config.get('pubsubQueueName') + config.get('pubsubQueueName'), ) .then(() => { - logger.info('RabbitMQ service initialized') + logger.info('RabbitMQ service initialized'); }) .catch((err) => { - logger.error('Error initializing services', err) + logger.error('Error initializing services', err); // gracefulShutdown() - }) + }); // // elasticsearch // var esConfig = { @@ -46,4 +46,4 @@ module.exports = (app, logger) => { // } // app.services.es = require('elasticsearch').Client(esConfig) } -} +}; diff --git a/src/services/rabbitmq.js b/src/services/rabbitmq.js index 34e1b988..7450fed2 100644 --- a/src/services/rabbitmq.js +++ b/src/services/rabbitmq.js @@ -1,121 +1,122 @@ -'use strict' /* globals Promise */ -import _ from 'lodash' -import { - EventEmitter -} from 'events' -import { - handlers as msgHandlers -} from '../events' -import { - EVENT -} from '../constants' - -// console.log(events) -// const EventEmitter = EventEmitter +import _ from 'lodash'; +import amqplib from 'amqplib'; +import { EventEmitter } from 'events'; +import { handlers as msgHandlers } from '../events'; module.exports = class RabbitMQService extends EventEmitter { + /** + * constructor + * @param {Object} app express app Object + * @param {Object} logger logger object + */ constructor(app, logger) { - super() - EventEmitter.call(this) - this._app = app - this.logger = logger - this._subConn = null - this._pubConn = null - this._subQueue = null + super(); + EventEmitter.call(this); + this.app = app; + this.logger = logger; + this.subscriberCxn = null; + this.publisherCxn = null; + this.subscriberQ = null; } /** * initialize rabbit mq connections / exchanges/ queues etc + * @param {String} rabbitmqURL rabbitmq connection url + * @param {String} exchangeName rabbitmq exchange name + * @param {String} queueName rabbitmq queue name * @return {Promise} Resolved or rejected promise */ init(rabbitmqURL, exchangeName, queueName) { - var self = this - self.exchangeName = exchangeName - self.queueName = queueName - return self._createConnection(rabbitmqURL) + const self = this; + self.rabbitmqURL = rabbitmqURL; + self.exchangeName = exchangeName; + self.queueName = queueName; + return self.createConnection() .then((conn) => { - self.logger.debug('Publisher connection created') - self._pubConn = conn - // subscriber connection - return self._createConnection(rabbitmqURL) + self.logger.debug('Publisher connection created'); + self.publisherCxn = conn; + // subscriber connection + return self.createConnection(); }).then((conn) => { - self.logger.debug('Subscriber connection created') - self._subConn = conn - return self._initSubscriber() + self.logger.debug('Subscriber connection created'); + self.subscriberCxn = conn; + return self.initSubscriber(); }) .catch((err) => { - self.logger.error(err) - }) + self.logger.error(err); + }); } /** * helper function to create a connection to rabbitmq - * @param {string} rabbitUrl url to connect to * @return {promise} promise + * @private */ - _createConnection(rabbitUrl) { - return require('amqplib').connect(rabbitUrl) + createConnection() { + return amqplib.connect(this.rabbitmqURL); } /** * Helper function to handle initializing subscribers * @return {promise} resolved promise + * @private */ - _initSubscriber(handlers) { - var self = this - var channel = null - // create channel to setup exchanges + queues + bindings - // on subscriber connection - return self._subConn.createChannel() + initSubscriber() { + const self = this; + let channel = null; + // create channel to setup exchanges + queues + bindings + // on subscriber connection + return self.subscriberCxn.createChannel() .then((ch) => { // assert / create exchanges - self.logger.debug('Channel created') - channel = ch + self.logger.debug('Channel created'); + channel = ch; return channel.assertExchange(self.exchangeName, 'topic', { - durable: true - }) + durable: true, + }); }).then(() => { // create queue // a single queue for project service will suffice - self.logger.debug('Exchange created') - // with default params - exclusive:false, durable: true, autoDelete: false - return channel.assertQueue(self.queueName) + self.logger.debug('Exchange created'); + // with default params - exclusive:false, durable: true, autoDelete: false + return channel.assertQueue(self.queueName); }).then((qok) => { - self.logger.debug('Queue %s created', self.queueName) - self._subQueue = qok.queue - // bindings for the queue - // all these keys/bindings should be routed to the same queue - const bindings = _.keys(msgHandlers) - self.logger.debug('Adding bindings: ', bindings) - var bindingPromises = _.map(bindings, (rk) => { - return channel.bindQueue(self._subQueue, self.exchangeName, rk) - }) - return Promise.all(bindingPromises) - }).then(() => { - self._subChannel = channel - return channel.consume(self._subQueue, (msg) => { - const key = msg.fields.routingKey - // create a child logger so we can trace with original request id - const _childLogger = self.logger.child({ - requestId: msg.properties.correlationId - }) - _childLogger.debug('Received Message', key, msg.fields) - const handler = msgHandlers[key] + self.logger.debug('Queue %s created', self.queueName); + self.subscriberQ = qok.queue; + // bindings for the queue + // all these keys/bindings should be routed to the same queue + const bindings = _.keys(msgHandlers); + self.logger.debug('Adding bindings: ', bindings); + const bindingPromises = _.map(bindings, rk => + channel.bindQueue(self.subscriberQ, self.exchangeName, rk)); + return Promise.all(bindingPromises); + }) + .then(() => + channel.consume(self.subscriberQ, (msg) => { + const key = msg.fields.routingKey; + // create a child logger so we can trace with original request id + const cLogger = self.logger.child({ + requestId: msg.properties.correlationId, + }); + cLogger.debug('Received Message', key, msg.fields); + const handler = msgHandlers[key]; if (!_.isFunction(handler)) { - _childLogger.error(`Unknown message type: ${key}, NACKing... `) - // channel.nack(msg, false, false) + cLogger.error(`Unknown message type: ${key}, NACKing... `); + // channel.nack(msg, false, false) } else { - handler(_childLogger, msg, channel) + handler(cLogger, msg, channel); } - }) - }).then(() => { - self.logger.info('Waiting for messages .... ') - }).catch((err) => { - // channel.close() - self.logger.error(err) + }), + ) + .then(() => { + self.logger.info('Waiting for messages .... '); }) + .catch((err) => { + // channel.close() + self.logger.error(err); + }); } @@ -126,54 +127,56 @@ module.exports = class RabbitMQService extends EventEmitter { disconnect() { // TODO shutdown channel // shutdown connections - var self = this + const self = this; return new Promise((resolve) => { - var promises = _.map([self._subConn, self._pubConn], (conn) => { - conn.close() - }) + const promises = _.map([self.subscriberCxn, self.publisherCxn], (conn) => { + conn.close(); + }); Promise.all(promises) .then(() => { - self.logger.info('Disconnected from rabbitmq') - resolve() + self.logger.info('Disconnected from rabbitmq'); + resolve(); }).catch((err) => { - self.logger.error('ERROR Closing connection', err) - }) - }) + self.logger.error('ERROR Closing connection', err); + }); + }); } /** * Publish message to default exchange - * @param {string} key routing key - * @param {object} payload message payload + * @param {string} key routing key + * @param {object} payload message payload + * @param {Object} props message properties (optional) + * @returns {Promise} promise */ publish(key, payload, props = {}) { - var channel = null - var self = this - // first create a channel - this is a lightweight connection - return self._pubConn.createChannel() + let channel = null; + const self = this; + // first create a channel - this is a lightweight connection + return self.publisherCxn.createChannel() .then((ch) => { - channel = ch - // make sure the exchance exisits, else create it + channel = ch; + // make sure the exchance exisits, else create it return channel.assertExchange(self.exchangeName, 'topic', { - durable: true - }) + durable: true, + }); }).then(() => { // publish the message - props = _.defaults(props, { - contentType: 'application/json' - }) + const updatedProps = _.defaults(props, { + contentType: 'application/json', + }); channel.publish( self.exchangeName, key, new Buffer(JSON.stringify(payload)), - props - ) - self.logger.debug('Published msg to exchange %s with key: %s', self.exchangeName, key) - return channel.close() + updatedProps, + ); + self.logger.debug('Published msg to exchange %s with key: %s', self.exchangeName, key); + return channel.close(); }) .catch((err) => { - self.logger.error(err) - return channel.close() - }) + self.logger.error(err); + return channel.close(); + }); } -} +}; diff --git a/src/services/topicService.js b/src/services/topicService.js index fa05b95f..204f07bf 100644 --- a/src/services/topicService.js +++ b/src/services/topicService.js @@ -1,48 +1,53 @@ -'use strict' -import util from '../util' -import config from 'config' -import _ from 'lodash' +import config from 'config'; +import util from '../util'; /** * Service methods to handle creating topics */ /** * Build custom http client for request - * @param req request - * @returns custom http client + * @param {Object} req request + * @returns {Promise} custom http client * @private */ -function _getHttpClient(req) { - var httpClient = util.getHttpClient(req) - httpClient.defaults.headers.common['Authorization'] = req.headers.authorization - httpClient.defaults.baseURL = config.get('topicServiceEndpoint') - httpClient.defaults.timeout = 30000 - httpClient.interceptors.response.use(resp => { +function getHttpClient(req) { + const httpClient = util.getHttpClient(req); + httpClient.defaults.headers.common.Authorization = req.headers.authorization; + httpClient.defaults.baseURL = config.get('topicServiceEndpoint'); + httpClient.defaults.timeout = 30000; + httpClient.interceptors.response.use((resp) => { // req.log.debug('resp: ', JSON.stringify(resp.data, null, 2)) if (resp.status !== 200 || resp.data.result.status !== 200) { // req.log.error('error resp: ', JSON.stringify(resp.data, null, 2)) - return Promise.reject(new Error(resp.data.result.content.message)) + return Promise.reject(new Error(resp.data.result.content.message)); } - return Promise.resolve(resp) - }) - return httpClient + return Promise.resolve(resp); + }); + return httpClient; } -export default { +/** + * Create topics in topic service + * @param {Object} req request object + * @param {integer} projectId project id + * @param {String} title title of the post + * @param {String} message message to be posted + * @param {String} tag tag, defaults to PRIMARY + * @return {Promise} returned Promise + */ +function createTopic(req, projectId, title, message, tag = 'PRIMARY') { + return getHttpClient(req) + .post('', { + reference: 'project', + referenceId: projectId.toString(), + tag, + title, + body: message, + }); +} - /** - * Create topics in topic service - */ - createTopic(req, projectId, title, message, tag = 'PRIMARY') { - return _getHttpClient(req) - .post('', { - reference: 'project', - referenceId: projectId.toString(), - tag, - title, - body: message - }) - } -} +export default { + createTopic, +}; diff --git a/src/tests/seed.js b/src/tests/seed.js index 32fffeae..54fb5100 100644 --- a/src/tests/seed.js +++ b/src/tests/seed.js @@ -1,108 +1,108 @@ -import models from '../models' -models.sequelize.sync({force: true}) - .then(() => - models.Project.bulkCreate([{ - type: 'generic', - directProjectId: 9999999, - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'active', - details: {}, - createdBy: 1, - updatedBy: 1 - }, { - type: 'visual_design', - directProjectId: 1, - billingAccountId: 2, - name: 'test2', - description: 'test project2', - status: 'draft', - details: {}, - createdBy: 1, - updatedBy: 1 - }, { - type: 'visual_design', - billingAccountId: 3, - name: 'test2', - description: 'completed project without copilot', - status: 'completed', - details: {}, - createdBy: 1, - updatedBy: 1 - }, { - type: 'generic', - billingAccountId: 4, - name: 'test2', - description: 'draft project without copilot', - status: 'draft', - details: {}, - createdBy: 1, - updatedBy: 1 - }, { - type: 'generic', - billingAccountId: 5, - name: 'test2', - description: 'active project without copilot', - status: 'active', - details: {}, - createdBy: 1, - updatedBy: 1 - }])) - .then(()=> models.Project.findAll()) - .then((projects)=> { - var project1 = projects[0] - var project2 = projects[1] - var operations = [] - operations.push(models.ProjectMember.bulkCreate([{ - userId: 40051331, - projectId: project1.id, - role: 'customer', - isPrimary: false, - createdBy: 1, - updatedBy: 1 - }, { - userId: 40051332, - projectId: project1.id, - role: 'copilot', - isPrimary: false, - createdBy: 1, - updatedBy: 1 - },{ - userId: 40051333, - projectId: project1.id, - role: 'manager', - isPrimary: true, - createdBy: 1, - updatedBy: 1 - }, { - userId: 40051332, - projectId: project2.id, - role: 'copilot', - isPrimary: false, - createdBy: 1, - updatedBy: 1 - }, { - userId: 40051331, - projectId: projects[2].id, - role: 'customer', - isPrimary: false, - createdBy: 1, - updatedBy: 1 - }])) - operations.push(models.ProjectAttachment.create({ - title: 'Spec', - projectId: project1.id, - description: "specification", - filePath: "projects/1/spec.pdf", - contentType: "application/pdf", - createdBy: 1, - updatedBy: 1 - })) - return Promise.all(operations) - }) - .then(()=>{ - console.log('Success') - process.exit(0) - }) - .catch((err)=> console.log('Failed: ', err)) +import models from '../models'; +models.sequelize.sync({ force: true }) + .then(() => + models.Project.bulkCreate([{ + type: 'generic', + directProjectId: 9999999, + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'active', + details: {}, + createdBy: 1, + updatedBy: 1, + }, { + type: 'visual_design', + directProjectId: 1, + billingAccountId: 2, + name: 'test2', + description: 'test project2', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + }, { + type: 'visual_design', + billingAccountId: 3, + name: 'test2', + description: 'completed project without copilot', + status: 'completed', + details: {}, + createdBy: 1, + updatedBy: 1, + }, { + type: 'generic', + billingAccountId: 4, + name: 'test2', + description: 'draft project without copilot', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + }, { + type: 'generic', + billingAccountId: 5, + name: 'test2', + description: 'active project without copilot', + status: 'active', + details: {}, + createdBy: 1, + updatedBy: 1, + }])) + .then(() => models.Project.findAll()) + .then((projects) => { + const project1 = projects[0]; + const project2 = projects[1]; + const operations = []; + operations.push(models.ProjectMember.bulkCreate([{ + userId: 40051331, + projectId: project1.id, + role: 'customer', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }, { + userId: 40051332, + projectId: project1.id, + role: 'copilot', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }, { + userId: 40051333, + projectId: project1.id, + role: 'manager', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }, { + userId: 40051332, + projectId: project2.id, + role: 'copilot', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }, { + userId: 40051331, + projectId: projects[2].id, + role: 'customer', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }])); + operations.push(models.ProjectAttachment.create({ + title: 'Spec', + projectId: project1.id, + description: 'specification', + filePath: 'projects/1/spec.pdf', + contentType: 'application/pdf', + createdBy: 1, + updatedBy: 1, + })); + return Promise.all(operations); + }) + .then(() => { + console.log('Success'); + process.exit(0); + }) + .catch(err => console.log('Failed: ', err)); diff --git a/src/tests/serviceMocks.js b/src/tests/serviceMocks.js index f1c42dff..a8fd2b0d 100644 --- a/src/tests/serviceMocks.js +++ b/src/tests/serviceMocks.js @@ -1,18 +1,18 @@ -'use strict' + /* globals Promise */ -import sinon from 'sinon' -import _ from 'lodash' +import sinon from 'sinon'; +import _ from 'lodash'; module.exports = (app) => { _.assign(app.services, { pubsub: { - publish: () => {} + publish: () => {}, }, es: { - index: () => {} - } - }) - sinon.stub(app.services.pubsub, 'publish', ()=> Promise.resolve(true) ) - sinon.stub(app.services.es, 'index', ()=> Promise.resolve(true) ) -} + index: () => {}, + }, + }); + sinon.stub(app.services.pubsub, 'publish', () => Promise.resolve(true)); + sinon.stub(app.services.es, 'index', () => Promise.resolve(true)); +}; diff --git a/src/tests/util.js b/src/tests/util.js index a1aa0ef0..4387755b 100644 --- a/src/tests/util.js +++ b/src/tests/util.js @@ -1,13 +1,11 @@ -import models from '../models' +import models from '../models'; export default { - clearDb: done => { - return models.sequelize.sync({force: true}) + clearDb: done => models.sequelize.sync({ force: true }) .then(() => { - if (done) done() - }) - }, + if (done) done(); + }), mockHttpClient: { defaults: { headers: { common: {} } }, interceptors: { response: { use: () => {} } }, @@ -22,6 +20,6 @@ export default { // userId = 40051334, roles: [ 'Manager', 'Topcoder User' ],handle: 'test1',email: 'test@topcoder.com' manager: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6InRlc3QxIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNCIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.amNzJ6PknW8V1ZDrVxJAd-SxcyfYhTf80IBc1sH-vF8', // userId = 40051335, [ 'Topcoder User' ],handle: 'member2',email: 'test@topcoder.com' - member2: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6Im1lbWJlcjIiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzM1IiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.2euj7ZV0jWLbMv25USDDySMjdi9N7SomPriGWJdyhUc' - } -} + member2: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6Im1lbWJlcjIiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzM1IiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.2euj7ZV0jWLbMv25USDDySMjdi9N7SomPriGWJdyhUc', + }, +}; diff --git a/src/util.js b/src/util.js index 676b4dd9..463e5387 100644 --- a/src/util.js +++ b/src/util.js @@ -1,4 +1,4 @@ -'use strict' + /* globals Promise */ /* * Copyright (C) 2016 TopCoder Inc., All Rights Reserved. @@ -10,11 +10,11 @@ */ -import _ from 'lodash' -import querystring from 'querystring' -import config from 'config' +import _ from 'lodash'; +import querystring from 'querystring'; +import config from 'config'; -let util = _.cloneDeep(require('tc-core-library-js').util(config)) +const util = _.cloneDeep(require('tc-core-library-js').util(config)); _.assignIn(util, { /** * Handle error @@ -26,14 +26,14 @@ _.assignIn(util, { handleError: (msg, err, req, next) => { req.log.error({ message: msg, - error: err - }) - let apiErr = new Error(msg) + error: err, + }); + const apiErr = new Error(msg); _.assign(apiErr, { status: _.get(err, 'status', 500), - details: _.get(err, 'details', msg) - }) - return next(apiErr) + details: _.get(err, 'details', msg), + }); + return next(apiErr); }, /** * Validates if filters are valid @@ -42,13 +42,13 @@ _.assignIn(util, { * @return {boolean} */ isValidFilter: (filters, validValues) => { - var valid = true + let valid = true; _.each(_.keys(filters), (k) => { if (valid && _.indexOf(validValues, k) < 0) { - valid = false + valid = false; } - }) - return valid + }); + return valid; }, /** * Helper funtion to verify if user has specified role @@ -57,9 +57,9 @@ _.assignIn(util, { * @return {boolean} true/false */ hasRole: (req, role) => { - let roles = _.get(req, 'authUser.roles', []) - roles = roles.map(s => s.toLowerCase()) - return _.indexOf(roles, role.toLowerCase()) >= 0 + let roles = _.get(req, 'authUser.roles', []); + roles = roles.map(s => s.toLowerCase()); + return _.indexOf(roles, role.toLowerCase()) >= 0; }, /** @@ -68,20 +68,20 @@ _.assignIn(util, { * @return {object} */ parseFields: (queryFields, allowedFields) => { - var fields = _.cloneDeep(allowedFields) + const fields = _.cloneDeep(allowedFields); if (queryFields.length) { // remove any inavlid fields - fields['projects'] = _.intersection(queryFields, allowedFields['projects']) - fields['project_members'] = _.filter(queryFields, (f) => { return f.indexOf('members.') === 0}) + fields.projects = _.intersection(queryFields, allowedFields.projects); + fields.project_members = _.filter(queryFields, f => f.indexOf('members.') === 0); // remove members. prefix - fields['project_members'] = _.map(fields['project_members'], (f) => { return f.substring(8) }) + fields.project_members = _.map(fields.project_members, f => f.substring(8)); // remove any errorneous fields - fields['project_members'] = _.intersection(fields['project_members'], allowedFields['project_members']) - if (fields['project_members'].length === 0 && _.indexOf(queryFields, 'members') > -1) { - fields['project_members'] = allowedFields['project_members'] + fields.project_members = _.intersection(fields.project_members, allowedFields.project_members); + if (fields.project_members.length === 0 && _.indexOf(queryFields, 'members') > -1) { + fields.project_members = allowedFields.project_members; } } - return fields + return fields; }, /** @@ -90,18 +90,18 @@ _.assignIn(util, { * @return {[type]} [description] */ parseQueryFilter: (queryFilter) => { - queryFilter = querystring.parse(queryFilter) + queryFilter = querystring.parse(queryFilter); // convert in to array queryFilter = _.mapValues(queryFilter, (val) => { if (val.indexOf('in(') > -1) { - return { $in: val.substring(3, val.length-1).split(',') } + return { $in: val.substring(3, val.length - 1).split(',') }; } - return val - }) + return val; + }); if (queryFilter.id) { - queryFilter.id['$in'] = _.map(queryFilter.id['$in'], _.parseInt) + queryFilter.id.$in = _.map(queryFilter.id.$in, _.parseInt); } - return queryFilter + return queryFilter; }, /** @@ -111,27 +111,25 @@ _.assignIn(util, { * @param {string} dest destination url * @return {promise} promise */ - s3FileTransfer: (req, source, dest) => { - return new Promise((resolve, reject) => { - var cmdStr = _.join([ - 'aws s3 mv', - `"${source}"`, - `"${dest}"`, - '--region us-east-1' - ], ' ') + s3FileTransfer: (req, source, dest) => new Promise((resolve, reject) => { + const cmdStr = _.join([ + 'aws s3 mv', + `"${source}"`, + `"${dest}"`, + '--region us-east-1', + ], ' '); - const exec = require('child_process').exec - exec(cmdStr, (error, stdout, stderr) => { - req.log.debug(`s3FileTransfer: stdout: ${stdout}`) - req.log.debug(`s3FileTransfer: stderr: ${stderr}`) - if (error !== null) { - req.log.error(`exec error: ${error}`) - return reject(error) - } - return resolve({success: true}) - }) - }) - }, + const exec = require('child_process').exec; + exec(cmdStr, (error, stdout, stderr) => { + req.log.debug(`s3FileTransfer: stdout: ${stdout}`); + req.log.debug(`s3FileTransfer: stderr: ${stderr}`); + if (error !== null) { + req.log.error(`exec error: ${error}`); + return reject(error); + } + return resolve({ success: true }); + }); + }), /** @@ -142,72 +140,70 @@ _.assignIn(util, { */ getFileDownloadUrl: (req, filePath) => { if (!filePath) { - return Promise.reject( new Error('file path empty')) + return Promise.reject(new Error('file path empty')); } - let fileServiceUrl = config.get('fileServiceEndpoint') - if (fileServiceUrl.substr(-1) !== '/') fileServiceUrl += '/' + let fileServiceUrl = config.get('fileServiceEndpoint'); + if (fileServiceUrl.substr(-1) !== '/') fileServiceUrl += '/'; // get presigned Url - var httpClient = util.getHttpClient(req) - httpClient.defaults.headers.common['Authorization'] = req.headers.authorization - return httpClient.post(fileServiceUrl + 'downloadurl', { - param: { - filePath: filePath - } - }) + const httpClient = util.getHttpClient(req); + httpClient.defaults.headers.common.Authorization = req.headers.authorization; + return httpClient.post(`${fileServiceUrl}downloadurl`, { + param: { + filePath, + }, + }) .then((resp) => { - req.log.debug('Retreiving Presigned Url resp: ', JSON.stringify(resp.data, null, 2)) + req.log.debug('Retreiving Presigned Url resp: ', JSON.stringify(resp.data, null, 2)); if (resp.status !== 200 || resp.data.result.status !== 200) { - return Promise.reject(new Error("Unable to fetch pre-signed url")) + return Promise.reject(new Error('Unable to fetch pre-signed url')); } return [ filePath, - resp.data.result.content.preSignedURL - ] - }) - }, - getProjectAttachments: (req, projectId) => { - const models = require('./models').default - let attachments = [] - return models.ProjectAttachment.getActiveProjectAttachments(projectId) + resp.data.result.content.preSignedURL, + ]; + }); + }, + getProjectAttachments: (req, projectId) => { + const models = require('./models').default; + let attachments = []; + return models.ProjectAttachment.getActiveProjectAttachments(projectId) .then((_attachments) => { // if attachments were requested if (attachments) { - attachments = _attachments + attachments = _attachments; } else { - return attachments + return attachments; } // TODO consider using redis to cache attachments urls - let promises = [] + const promises = []; _.each(attachments, (a) => { - promises.push(util.getFileDownloadUrl(req, a.filePath)) - }) - return Promise.all(promises) + promises.push(util.getFileDownloadUrl(req, a.filePath)); + }); + return Promise.all(promises); }) .then((result) => { // result is an array of 'tuples' => [[path, url], [path,url]] // convert it to a map for easy lookup - let urls = _.fromPairs(result) + const urls = _.fromPairs(result); _.each(attachments, (a) => { - a.downloadUrl = urls[a.filePath] - }) - return attachments - }) - }, + a.downloadUrl = urls[a.filePath]; + }); + return attachments; + }); + }, - getSystemUserToken: (logger, id='system') => { - const httpClient = util.getHttpClient({id: id, log: logger}) - const url = `${config.get('identityServiceEndpoint')}authorizations` - const formData = `clientId=${config.get('systemUserClientId')}&secret=${encodeURIComponent(config.get('systemUserClientSecret'))}` - return httpClient.post(url, formData, - { - timeout: 4000, - headers: { 'Content-Type': 'application/x-www-form-urlencoded' } - } + 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'))}`; + return httpClient.post(url, formData, + { + timeout: 4000, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }, ) - .then(res => { - return res.data.result.content.token - }) - }, + .then(res => res.data.result.content.token); + }, /** * Fetches the topcoder user details using the given JWT token. @@ -218,20 +214,20 @@ _.assignIn(util, { * * @return promise which resolves to the user's information */ - getTopcoderUser: (userId, jwtToken, logger) => { - var httpClient = util.getHttpClient({id: 'userService_' + userId, log : logger}); - httpClient.defaults.timeout = 3000 - httpClient.defaults.headers.common['Accept'] = 'application/json' - httpClient.defaults.headers.common['Content-Type'] = 'application/json' - httpClient.defaults.headers.common['Authorization'] = 'Bearer ' + jwtToken - return httpClient.get(config.userServiceUrl + '/' + userId).then((response) => { - if (response.data && response.data.result + getTopcoderUser: (userId, jwtToken, logger) => { + const httpClient = util.getHttpClient({ id: `userService_${userId}`, log: logger }); + httpClient.defaults.timeout = 3000; + httpClient.defaults.headers.common.Accept = 'application/json'; + httpClient.defaults.headers.common['Content-Type'] = 'application/json'; + 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) { - return response.data.result.content; - } - return null; - }); - } -}) + return response.data.result.content; + } + return null; + }); + }, +}); -export default util +export default util; From 4a699bf51b016e3c17e605f4b4eabb4dc046ede4 Mon Sep 17 00:00:00 2001 From: Ritesh Sangwan Date: Fri, 3 Mar 2017 00:06:35 +0530 Subject: [PATCH 3/8] fix lint errors (#46) --- .eslintignore | 5 +++ .eslintrc | 15 +++++++ config/sample.local.js | 56 +++++++++++------------ migrations/sync.js | 16 +++---- newrelic.js | 16 +++---- package.json | 6 ++- src/events/projects/index.js | 4 +- src/index.js | 3 +- src/middlewares/checkRole.js | 3 +- src/mocks/addBillingAccount.js | 5 ++- src/mocks/addCopilot.js | 5 ++- src/mocks/createProject.js | 5 ++- src/mocks/direct.js | 57 +++++++++++++----------- src/models/project.js | 16 ++++--- src/permissions/project.delete.js | 4 +- src/permissions/project.edit.js | 5 ++- src/permissions/project.view.js | 6 +-- src/permissions/projectMember.delete.js | 7 +-- src/routes/attachments/create.js | 3 +- src/routes/attachments/create.spec.js | 9 ++-- src/routes/attachments/delete.js | 5 +-- src/routes/attachments/delete.spec.js | 37 +++++++-------- src/routes/attachments/update.js | 10 ++--- src/routes/attachments/update.spec.js | 37 +++++++-------- src/routes/index.js | 1 + src/routes/projectMembers/create.js | 8 ++-- src/routes/projectMembers/create.spec.js | 4 +- src/routes/projectMembers/delete.js | 6 +-- src/routes/projectMembers/delete.spec.js | 23 ++++++---- src/routes/projectMembers/update.js | 18 ++++---- src/routes/projectMembers/update.spec.js | 11 ++--- src/routes/projects/create.spec.js | 3 +- src/routes/projects/delete.js | 7 +-- src/routes/projects/delete.spec.js | 18 ++------ src/routes/projects/get.js | 4 +- src/routes/projects/get.spec.js | 4 +- src/routes/projects/list.js | 2 +- src/routes/projects/list.spec.js | 22 +++++---- src/routes/projects/update.js | 6 ++- src/routes/projects/update.spec.js | 14 +++--- src/services/fileService.js | 14 +++--- src/services/index.js | 5 +++ src/tests/seed.js | 6 ++- src/tests/util.js | 1 + src/util.js | 46 ++++++++++--------- 45 files changed, 300 insertions(+), 258 deletions(-) create mode 100644 .eslintignore 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; From 2099b80c428bcea39a79594b63952a0ed7028258 Mon Sep 17 00:00:00 2001 From: Parth Shah Date: Thu, 2 Mar 2017 10:39:40 -0800 Subject: [PATCH 4/8] Revert "fix lint errors" (#47) --- .eslintignore | 5 --- .eslintrc | 15 ------- config/sample.local.js | 56 +++++++++++------------ migrations/sync.js | 16 +++---- newrelic.js | 16 +++---- package.json | 6 +-- src/events/projects/index.js | 4 +- src/index.js | 3 +- src/middlewares/checkRole.js | 3 +- src/mocks/addBillingAccount.js | 5 +-- src/mocks/addCopilot.js | 5 +-- src/mocks/createProject.js | 5 +-- src/mocks/direct.js | 57 +++++++++++------------- src/models/project.js | 16 +++---- src/permissions/project.delete.js | 4 +- src/permissions/project.edit.js | 5 +-- src/permissions/project.view.js | 6 +-- src/permissions/projectMember.delete.js | 7 ++- src/routes/attachments/create.js | 3 +- src/routes/attachments/create.spec.js | 9 ++-- src/routes/attachments/delete.js | 5 ++- src/routes/attachments/delete.spec.js | 37 ++++++++------- src/routes/attachments/update.js | 10 ++--- src/routes/attachments/update.spec.js | 37 ++++++++------- src/routes/index.js | 1 - src/routes/projectMembers/create.js | 8 ++-- src/routes/projectMembers/create.spec.js | 4 +- src/routes/projectMembers/delete.js | 6 +-- src/routes/projectMembers/delete.spec.js | 23 ++++------ src/routes/projectMembers/update.js | 18 ++++---- src/routes/projectMembers/update.spec.js | 11 +++-- src/routes/projects/create.spec.js | 3 +- src/routes/projects/delete.js | 7 ++- src/routes/projects/delete.spec.js | 18 ++++++-- src/routes/projects/get.js | 4 +- src/routes/projects/get.spec.js | 4 +- src/routes/projects/list.js | 2 +- src/routes/projects/list.spec.js | 22 ++++----- src/routes/projects/update.js | 6 +-- src/routes/projects/update.spec.js | 14 +++--- src/services/fileService.js | 14 +++--- src/services/index.js | 5 --- src/tests/seed.js | 6 +-- src/tests/util.js | 1 - src/util.js | 46 +++++++++---------- 45 files changed, 258 insertions(+), 300 deletions(-) delete mode 100644 .eslintignore diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index c7f96f7b..00000000 --- a/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules -dist -.ebextensions -.elasticbeanstalk -coverage \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index ad8484f2..348ce0f0 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,21 +8,6 @@ "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 ae17f31e..19441b79 100644 --- a/config/sample.local.js +++ b/config/sample.local.js @@ -1,34 +1,34 @@ // force using test.json for unit tests -let config; +var 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 f682c6b0..5b6f79c1 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(() => { - winston.info('Database synced successfully'); - process.exit(); + console.log('Database synced successfully') + process.exit() }).catch((err) => { - winston.error('Error syncing database', err); - process.exit(1); - }); + console.error('Error syncing database', err) + process.exit(1) + }) diff --git a/newrelic.js b/newrelic.js index c207db64..2694e216 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. */ -let appName = 'tc-projects-service'; +var 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 81c6069e..62f41d39 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,7 @@ "scripts": { "build": "babel src -d dist --presets es2015", "sync": "node migrations/sync.js", - "lint": "./node_modules/.bin/eslint .", - "lint:fix": "./node_modules/.bin/eslint . --fix || true", + "lint": "./node_modules/.bin/eslint src", "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", @@ -48,8 +47,7 @@ "pg": "^4.5.5", "pg-native": "^1.10.0", "sequelize": "^3.23.0", - "tc-core-library-js": "^1.0.8", - "winston": "^2.3.1" + "tc-core-library-js": "^1.0.8" }, "devDependencies": { "babel-cli": "^6.9.0", diff --git a/src/events/projects/index.js b/src/events/projects/index.js index 1cf66edf..c2499b18 100644 --- a/src/events/projects/index.js +++ b/src/events/projects/index.js @@ -1,4 +1,6 @@ - +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 41304c76..505da7a2 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,7 @@ +import models from './models'; + // include newrelic if (process.env.NODE_ENV !== 'test' && process.env.NODE_ENV !== 'local') { require('newrelic'); @@ -9,7 +11,6 @@ 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 a903d896..1144e647 100644 --- a/src/middlewares/checkRole.js +++ b/src/middlewares/checkRole.js @@ -7,12 +7,11 @@ * @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 615bb528..3af425e9 100644 --- a/src/mocks/addBillingAccount.js +++ b/src/mocks/addBillingAccount.js @@ -1,6 +1,3 @@ -/* eslint-disable max-len */ -import winston from 'winston'; - const http = require('https'); const options = { @@ -26,7 +23,7 @@ const req = http.request(options, (res) => { res.on('end', () => { const body = Buffer.concat(chunks); - winston.info(body.toString()); + console.log(body.toString()); }); }); req.write('{\n "billingAccountId": 123456789\n}'); diff --git a/src/mocks/addCopilot.js b/src/mocks/addCopilot.js index 622f5ed5..c6b17ec7 100644 --- a/src/mocks/addCopilot.js +++ b/src/mocks/addCopilot.js @@ -1,6 +1,3 @@ -/* eslint-disable max-len */ -import winston from 'winston'; - const http = require('https'); const options = { @@ -26,7 +23,7 @@ const req = http.request(options, (res) => { res.on('end', () => { const body = Buffer.concat(chunks); - winston.info(body.toString()); + console.log(body.toString()); }); }); req.write('{\n "copilotUserId": 123456789\n}'); diff --git a/src/mocks/createProject.js b/src/mocks/createProject.js index e232d61d..4aca9b40 100644 --- a/src/mocks/createProject.js +++ b/src/mocks/createProject.js @@ -1,6 +1,3 @@ -/* eslint-disable max-len */ -import winston from 'winston'; - const http = require('https'); const options = { @@ -26,7 +23,7 @@ const req = http.request(options, (res) => { res.on('end', () => { const body = Buffer.concat(chunks); - winston.info(body.toString()); + console.log(body.toString()); }); }); diff --git a/src/mocks/direct.js b/src/mocks/direct.js index 094fdfa8..4dabcfdc 100644 --- a/src/mocks/direct.js +++ b/src/mocks/direct.js @@ -1,20 +1,17 @@ -import express, { Router } from 'express'; +import express 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'; - -const util = require('tc-core-library-js').util(config); -const jwtAuth = require('tc-core-library-js').middleware.jwtAuthenticator; - config.version = 'v3'; - +const util = require('tc-core-library-js').util(config); const app = express(); app.use(bodyParser.urlencoded({ extended: false, @@ -36,9 +33,10 @@ 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()); -const projectId = 2; +let projectId = 2; const projects = { 1: { projectName: 'test direct project1', @@ -57,7 +55,7 @@ router.route('/v3/direct/projects') }) .post((req, res) => { app.logger.info({ body: req.body }, 'create direct project'); - const newId = projectId + 1; + const newId = projectId++; req.body.id = newId; projects[newId] = req.body; res.json(util.wrapResponse(req.id, { projectId: newId })); @@ -65,48 +63,47 @@ router.route('/v3/direct/projects') router.route('/v3/direct/projects/:projectId(\\d+)/billingaccount') .post((req, res) => { - 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}` })); + 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}` })); } else { - res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${pId}`)); + res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${projectId}`)); } }); router.route('/v3/direct/projects/:projectId(\\d+)/copilot') .post((req, res) => { - 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 })); + 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 })); } else { - res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${pId}`)); + res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${projectId}`)); } }) .delete((req, res) => { - 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'); + 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'); res.json(util.wrapResponse(req.id, true)); } else { - res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${pId}`)); + res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${projectId}`)); } }); router.route('/v3/direct/projects/:projectId(\\d+)/permissions') .post((req, res) => { - const pId = req.params.projectId; - app.logger.info({ body: req.body, pId }, 'add permissions to Project'); - if (projects[pId]) { + const projectId = req.params.projectId; + app.logger.info({ body: req.body, projectId }, 'add permissions to Project'); + if (projects[projectId]) { res.json(); } else { - res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${pId}`)); + res.json(util.wrapErrorResponse(req.id, 404, `Cannot find direct project ${projectId}`)); } }); diff --git a/src/models/project.js b/src/models/project.js index d9462579..d565e72c 100644 --- a/src/models/project.js +++ b/src/models/project.js @@ -1,10 +1,9 @@ -/* eslint-disable valid-jsdoc */ -import _ from 'lodash'; import { PROJECT_TYPE, PROJECT_STATUS, PROJECT_MEMBER_ROLE } from '../constants'; +import _ from 'lodash'; module.exports = function (sequelize, DataTypes) { - const Project = sequelize.define('Project', { + var Project = sequelize.define('Project', { id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, directProjectId: DataTypes.BIGINT, billingAccountId: DataTypes.BIGINT, @@ -66,17 +65,15 @@ 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 @@ -145,8 +142,7 @@ 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 27ece695..b532798c 100644 --- a/src/permissions/project.delete.js +++ b/src/permissions/project.delete.js @@ -1,16 +1,14 @@ /* 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 f794353c..62854066 100644 --- a/src/permissions/project.edit.js +++ b/src/permissions/project.edit.js @@ -1,15 +1,14 @@ +/* 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 4a70b116..26e64740 100644 --- a/src/permissions/project.view.js +++ b/src/permissions/project.view.js @@ -1,15 +1,15 @@ -import _ from 'lodash'; +/* globals Promise */ + 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 5541db18..9a07c185 100644 --- a/src/permissions/projectMember.delete.js +++ b/src/permissions/projectMember.delete.js @@ -1,15 +1,14 @@ -import _ from 'lodash'; +/* globals Promise */ + 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 43a1e8d0..53abd5a7 100644 --- a/src/routes/attachments/create.js +++ b/src/routes/attachments/create.js @@ -8,9 +8,10 @@ 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 b3a7532e..98617cb2 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) => { - winston.info(`source is ${source}, dest is ${dest}`); + console.log(source, dest); return Promise.resolve(true); }; request(server) @@ -113,15 +113,18 @@ describe('Project Attachments', () => { .expect(201) .end((err, res) => { if (err) { - winston.error('unexpected error', err); + console.log(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 b5aa452c..a76b918e 100644 --- a/src/routes/attachments/delete.js +++ b/src/routes/attachments/delete.js @@ -2,11 +2,12 @@ // 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 0dc92d78..8634f941 100644 --- a/src/routes/attachments/delete.spec.js +++ b/src/routes/attachments/delete.spec.js @@ -1,5 +1,6 @@ import _ from 'lodash'; +import chai from 'chai'; import sinon from 'sinon'; import request from 'supertest'; @@ -10,8 +11,9 @@ import testUtil from '../../tests/util'; describe('Project Attachments delete', () => { - let project1; - let attachment; + let project1, + member1, + attachment; beforeEach((done) => { testUtil.clearDb() .then(() => { @@ -35,20 +37,23 @@ describe('Project Attachments delete', () => { isPrimary: true, createdBy: 1, updatedBy: 1, - }).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(); - })); + }).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(); + }); + }); }); }); }); diff --git a/src/routes/attachments/update.js b/src/routes/attachments/update.js index 65ec5938..3cb7e40e 100644 --- a/src/routes/attachments/update.js +++ b/src/routes/attachments/update.js @@ -2,11 +2,12 @@ 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. @@ -44,10 +45,9 @@ 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 4001dd95..abcf17fe 100644 --- a/src/routes/attachments/update.spec.js +++ b/src/routes/attachments/update.spec.js @@ -4,14 +4,16 @@ 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; - let attachment; + let project1, + member1, + attachment; beforeEach((done) => { testUtil.clearDb() .then(() => { @@ -35,20 +37,23 @@ describe('Project Attachments update', () => { isPrimary: true, createdBy: 1, updatedBy: 1, - }).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(); - })); + }).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(); + }); + }); }); }); }); diff --git a/src/routes/index.js b/src/routes/index.js index 8db4fe11..8889c61b 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -21,7 +21,6 @@ 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 64f2dfe7..805728f6 100644 --- a/src/routes/projectMembers/create.js +++ b/src/routes/projectMembers/create.js @@ -3,10 +3,11 @@ 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, EVENT } from '../../constants'; +import { PROJECT_MEMBER_ROLE } from '../../constants'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; +import { EVENT } from '../../constants'; /** * API to add a project member. @@ -19,8 +20,7 @@ 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 b0893a03..bd25f5fe 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; - let project2; + let project1, + project2; before((done) => { testUtil.clearDb() .then(() => { diff --git a/src/routes/projectMembers/delete.js b/src/routes/projectMembers/delete.js index e1b761b9..27e13f17 100644 --- a/src/routes/projectMembers/delete.js +++ b/src/routes/projectMembers/delete.js @@ -1,9 +1,9 @@ import _ from 'lodash'; -import winston from 'winston'; -import { middleware as tcMiddleware } from 'tc-core-library-js'; + import models from '../../models'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; import { EVENT, PROJECT_MEMBER_ROLE } from '../../constants'; /** @@ -29,7 +29,7 @@ module.exports = [ err.status = 404; return Promise.reject(err); } - return member.destroy({ logging: winston.info }); + return member.destroy({ logging: console.log }); }) .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 f34158c7..d209e63c 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; - let member1; - let member2; + let project1, + member1, + member2; beforeEach((done) => { testUtil.clearDb() .then(() => { @@ -106,8 +106,7 @@ 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 } }}) @@ -165,13 +164,9 @@ 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); @@ -218,8 +213,7 @@ 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(); }); @@ -260,8 +254,7 @@ 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 d4370fae..ac89f67c 100644 --- a/src/routes/projectMembers/update.js +++ b/src/routes/projectMembers/update.js @@ -2,10 +2,12 @@ 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. @@ -16,8 +18,7 @@ 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(), }), }, }; @@ -30,22 +31,21 @@ module.exports = [ * Update a projectMember if the user has access */ (req, res, next) => { - let projectMember; - let updatedProps = req.body.param; + let projectMember, + 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 37fefadf..799a2d5d 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; - let member1; - let member2; - let member3; + let project1, + member1, + member2, + member3; beforeEach((done) => { testUtil.clearDb() .then(() => { @@ -156,8 +156,7 @@ 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 0dd1784d..477e9ecb 100644 --- a/src/routes/projects/create.spec.js +++ b/src/routes/projects/create.spec.js @@ -3,7 +3,6 @@ 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'; @@ -14,7 +13,7 @@ const should = chai.should(); sinon.stub(RabbitMQService.prototype, 'init', () => {}); sinon.stub(RabbitMQService.prototype, 'publish', () => { - winston.info('publish called'); + console.log('publish called'); }); describe('Project create', () => { diff --git a/src/routes/projects/delete.js b/src/routes/projects/delete.js index a2ea2b19..d5f03aaa 100644 --- a/src/routes/projects/delete.js +++ b/src/routes/projects/delete.js @@ -1,8 +1,11 @@ + +// import validate from 'express-validation' import _ from 'lodash'; -import { middleware as tcMiddleware } from 'tc-core-library-js'; -import { EVENT } from '../../constants'; +import { EVENT } from '../../constants.js'; import models from '../../models'; +import fileService from '../../services/fileService'; +import { middleware as tcMiddleware } from 'tc-core-library-js'; /** * API to delete a project member. diff --git a/src/routes/projects/delete.spec.js b/src/routes/projects/delete.spec.js index aeae3e17..99587089 100644 --- a/src/routes/projects/delete.spec.js +++ b/src/routes/projects/delete.spec.js @@ -1,13 +1,21 @@ +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; + let project1, + owner, + teamMember, + manager, + copilot; beforeEach((done) => { testUtil.clearDb() .then(() => { @@ -63,7 +71,11 @@ describe('Project delete test', () => { }), ]; Promise.all(promises) - .then(() => { + .then((res) => { + owner = res[0]; + manager = res[2]; + copilot = res[3]; + teamMember = res[4]; done(); }); }); @@ -91,7 +103,7 @@ describe('Project delete test', () => { Authorization: `Bearer ${testUtil.jwts.member}`, }) .expect(204) - .end((err) => { + .end((err, resp) => { if (err) { return done(err); } diff --git a/src/routes/projects/get.js b/src/routes/projects/get.js index 077f06e4..8150715c 100644 --- a/src/routes/projects/get.js +++ b/src/routes/projects/get.js @@ -2,9 +2,11 @@ /* 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 73317b19..b8004a0c 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; - let project2; + let project1, + project2; before((done) => { testUtil.clearDb() .then(() => { diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index 5545b921..eeadb656 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 80739536..a69a8c2c 100644 --- a/src/routes/projects/list.spec.js +++ b/src/routes/projects/list.spec.js @@ -2,7 +2,6 @@ import chai from 'chai'; import request from 'supertest'; -import winston from 'winston'; import models from '../../models'; import server from '../../app'; @@ -12,11 +11,10 @@ const should = chai.should(); /** * Add full text index for projects. - * @return {Promise} returns the promise */ function addFullTextIndex() { if (models.sequelize.options.dialect !== 'postgres') { - winston.info('Not creating search index, must be using POSTGRES to do this'); + console.log('Not creating search index, must be using POSTGRES to do this'); return; } @@ -24,29 +22,26 @@ 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) => { - winston.error('Failed: ', err); + console.log('Failed: ', err); }); } describe('LIST Project', () => { - let project1; - let project2; + let project1, + project2; before((done) => { testUtil.clearDb() .then(() => addFullTextIndex()) @@ -178,8 +173,7 @@ 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 9b4e391c..4c0aa7e8 100644 --- a/src/routes/projects/update.js +++ b/src/routes/projects/update.js @@ -112,8 +112,7 @@ 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({ @@ -154,8 +153,7 @@ 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 e149dfc7..d27031af 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; - let project2; - let project3; + let project1, + project2, + project3; beforeEach((done) => { testUtil.clearDb(done); }); @@ -588,11 +588,11 @@ describe('Project', () => { }) .expect('Content-Type', /json/) .expect(200) - .end((error, resp) => { - if (error) { - return done(error); + .end((err, res) => { + if (err) { + return done(err); } - resJson = resp.body.result.content; + resJson = res.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 f725e771..fdbc56cb 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 {Object} req request - * @return {Object} custom http client + * @param req request + * @returns custom http client * @private */ function _getHttpClient(req) { @@ -31,10 +31,6 @@ 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 a24a17c8..9eaaabc6 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -6,11 +6,6 @@ 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 a00dd433..54fb5100 100644 --- a/src/tests/seed.js +++ b/src/tests/seed.js @@ -1,6 +1,4 @@ -import winston from 'winston'; import models from '../models'; - models.sequelize.sync({ force: true }) .then(() => models.Project.bulkCreate([{ @@ -104,7 +102,7 @@ models.sequelize.sync({ force: true }) return Promise.all(operations); }) .then(() => { - winston.info('Success'); + console.log('Success'); process.exit(0); }) - .catch(err => winston.error('Failed: ', err)); + .catch(err => console.log('Failed: ', err)); diff --git a/src/tests/util.js b/src/tests/util.js index 3035a1bc..4387755b 100644 --- a/src/tests/util.js +++ b/src/tests/util.js @@ -1,4 +1,3 @@ -/* eslint-disable max-len */ import models from '../models'; diff --git a/src/util.js b/src/util.js index f1312396..463e5387 100644 --- a/src/util.js +++ b/src/util.js @@ -15,15 +15,13 @@ import querystring from 'querystring'; import config from 'config'; const util = _.cloneDeep(require('tc-core-library-js').util(config)); - _.assignIn(util, { /** * Handle 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 + * @param defaultMessage the default error message + * @param err the err + * @param next the next function + * @returns next function with error */ handleError: (msg, err, req, next) => { req.log.error({ @@ -39,9 +37,9 @@ _.assignIn(util, { }, /** * Validates if filters are valid - * @param {object} filters object with filters - * @param {array} validValues valid filter values - * @return {boolean} true if filters are valid otherwise false + * @param {object} filters object with filters + * @param {array} validValues valid filter values + * @return {boolean} */ isValidFilter: (filters, validValues) => { let valid = true; @@ -66,9 +64,8 @@ _.assignIn(util, { /** * Parses query fields and groups them per table - * @param {array} queryFields list of query fields - * @param {Object} allowedFields the allowed fields - * @return {object} the parsed fields + * @param {array} queryFields list of query fields + * @return {object} */ parseFields: (queryFields, allowedFields) => { const fields = _.cloneDeep(allowedFields); @@ -88,9 +85,9 @@ _.assignIn(util, { }, /** - * Parse the query filters - * @param {String} queryFilter the query filter string - * @return {Object} the parsed array + * [description] + * @param {[type]} queryFilter [description] + * @return {[type]} [description] */ parseQueryFilter: (queryFilter) => { queryFilter = querystring.parse(queryFilter); @@ -137,9 +134,9 @@ _.assignIn(util, { /** * retrieve download urls for all attachments - * @param {Object} req original request - * @param {String} filePath the file path - * @return {String} the download url + * @param {[type]} req original request + * @param {[type]} attachments list of attachments to retrieve urls for + * @return {[type]} [description] */ getFileDownloadUrl: (req, filePath) => { if (!filePath) { @@ -198,8 +195,7 @@ _.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, @@ -212,11 +208,11 @@ _.assignIn(util, { /** * Fetches the topcoder user details using the given JWT token. * - * @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 + * @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 * - * @return {Promise} promise which resolves to the user's information + * @return promise which resolves to the user's information */ getTopcoderUser: (userId, jwtToken, logger) => { const httpClient = util.getHttpClient({ id: `userService_${userId}`, log: logger }); @@ -226,7 +222,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; From 744ef82caaea9dc542af16eabc4dba461d75008c Mon Sep 17 00:00:00 2001 From: Parth Shah Date: Thu, 2 Mar 2017 11:22:57 -0800 Subject: [PATCH 5/8] updating lint config --- .eslintignore | 7 ++++++ .eslintrc | 3 ++- config/sample.local.js | 56 +++++++++++++++++++++--------------------- migrations/sync.js | 12 ++++----- newrelic.js | 35 -------------------------- package.json | 10 +++----- src/index.js | 15 ++++------- 7 files changed, 52 insertions(+), 86 deletions(-) create mode 100644 .eslintignore delete mode 100644 newrelic.js diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..5eb52385 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,7 @@ +config/local.js +config/sample.local.js +node_modules +dist +.ebextensions +.elasticbeanstalk +coverage diff --git a/.eslintrc b/.eslintrc index 348ce0f0..6b4e8bd2 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,5 +1,5 @@ { - "extends": "airbnb", + "extends": "airbnb-base", "parser": "babel-eslint", "env": { "browser": false, @@ -8,6 +8,7 @@ "mocha": true }, "rules": { + "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.test.js", "**/*.spec.js"]}], "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..bf4f2277 100644 --- a/migrations/sync.js +++ b/migrations/sync.js @@ -1,4 +1,4 @@ -'use strict' + /** * Sync the database models to db tables. @@ -12,9 +12,9 @@ require('./dist/models').default.sequelize.sync({ force: true }) .then(() => { - console.log('Database synced successfully') - process.exit() + console.log('Database synced successfully'); + process.exit(); }).catch((err) => { - console.error('Error syncing database', err) - process.exit(1) - }) + console.error('Error syncing database', err); + process.exit(1); + }); diff --git a/newrelic.js b/newrelic.js deleted file mode 100644 index 2694e216..00000000 --- a/newrelic.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict' - -/** - * New Relic agent configuration. - * - * 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" -if (process.env.NODE_ENV === 'development') { - appName += "-dev" -} else if (process.env.NODE_ENV === 'qa') { - appName += "-qa" -} else { - appName += '-prod' -} - -exports.config = { - /** - * Array of application names. - */ - app_name: [appName], - /** - * Your New Relic license key. - */ - license_key: process.env.NEW_RELIC_LICENSE_KEY, - logging: { - /** - * Level at which to log. 'trace' is most useful to New Relic when diagnosing - * issues with the agent, 'info' and higher will impose the least overhead on - * production applications. - */ - level: 'info' - } -} diff --git a/package.json b/package.json index 62f41d39..450f4b4a 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", @@ -43,7 +44,6 @@ "express-validation": "^0.6.0", "joi": "^8.0.5", "lodash": "^4.16.4", - "newrelic": "^1.27.2", "pg": "^4.5.5", "pg-native": "^1.10.0", "sequelize": "^3.23.0", @@ -57,11 +57,9 @@ "babel-preset-es2015": "^6.9.0", "bunyan": "^1.8.1", "chai": "^3.5.0", - "eslint": "^3.2.2", - "eslint-config-airbnb": "^14.1.0", + "eslint": "^3.16.1", + "eslint-config-airbnb-base": "^11.1.0", "eslint-plugin-import": "^2.2.0", - "eslint-plugin-jsx-a11y": "^4.0.0", - "eslint-plugin-react": "^6.10.0", "istanbul": "^1.0.0-alpha.2", "mocha": "^2.5.3", "nodemon": "^1.9.1", diff --git a/src/index.js b/src/index.js index 505da7a2..4a37f70b 100644 --- a/src/index.js +++ b/src/index.js @@ -1,16 +1,11 @@ - -import models from './models'; - -// include newrelic -if (process.env.NODE_ENV !== 'test' && process.env.NODE_ENV !== 'local') { - require('newrelic'); -} - const app = require('./app'); +const coreLib = require('tc-core-library-js'); +const expressListRoutes = require('express-list-routes'); /** * Handle server shutdown gracefully + * @returns {undefined} */ function gracefulShutdown() { app.services.pubsub.disconnect() @@ -36,9 +31,9 @@ const port = process.env.PORT || 3000; // used to create, sign, and verify token const server = app.listen(port, () => { app.logger.info('Starting server on PORT: %d', port); - const authz = require('tc-core-library-js').Authorizer; + const authz = coreLib.Authorizer; app.logger.info('Registered Policies', authz.getRegisteredPolicies()); - require('express-list-routes')({ prefix: '', spacer: 7 }, 'APIs:', app.routerRef); + expressListRoutes({ prefix: '', spacer: 7 }, 'APIs:', app.routerRef); }); module.exports = server; From c9a14c9e2136184822fd72e89b8dfc7eb67df0b2 Mon Sep 17 00:00:00 2001 From: Ritesh Sangwan Date: Mon, 6 Mar 2017 22:59:42 +0530 Subject: [PATCH 6/8] fix lint errors (#48) --- .eslintrc | 3 +- migrations/sync.js | 3 +- src/events/projects/index.js | 4 +- src/index.js | 1 + src/middlewares/checkRole.js | 12 +- src/mocks/addBillingAccount.js | 8 +- src/mocks/addCopilot.js | 8 +- src/mocks/createProject.js | 8 +- src/mocks/direct.js | 60 +- src/models/project.js | 22 +- src/models/projectAttachment.js | 2 +- src/models/projectHistory.js | 2 +- src/models/projectMember.js | 2 +- src/permissions/index.js | 22 +- src/permissions/project.delete.js | 18 +- src/permissions/project.edit.js | 10 +- src/permissions/project.view.js | 16 +- src/permissions/projectMember.delete.js | 55 +- src/routes/attachments/create.js | 26 +- src/routes/attachments/create.spec.js | 34 +- src/routes/attachments/delete.js | 5 +- src/routes/attachments/delete.spec.js | 53 +- src/routes/attachments/update.js | 35 +- src/routes/attachments/update.spec.js | 52 +- src/routes/index.js | 10 +- src/routes/projectMembers/create.js | 8 +- src/routes/projectMembers/create.spec.js | 207 ++++--- src/routes/projectMembers/delete.js | 21 +- src/routes/projectMembers/delete.spec.js | 256 ++++---- src/routes/projectMembers/update.js | 24 +- src/routes/projectMembers/update.spec.js | 602 ++++++++++--------- src/routes/projects/create.js | 3 +- src/routes/projects/create.spec.js | 56 +- src/routes/projects/delete.js | 7 +- src/routes/projects/delete.spec.js | 27 +- src/routes/projects/get.js | 4 +- src/routes/projects/get.spec.js | 53 +- src/routes/projects/list.js | 13 +- src/routes/projects/list.spec.js | 363 ++++++------ src/routes/projects/update.js | 7 +- src/routes/projects/update.spec.js | 715 +++++++++++++---------- src/services/fileService.js | 18 +- src/services/index.js | 10 +- src/tests/seed.js | 4 +- src/tests/util.js | 1 + src/util.js | 59 +- 46 files changed, 1581 insertions(+), 1348 deletions(-) diff --git a/.eslintrc b/.eslintrc index 6b4e8bd2..1042ccec 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,7 +8,8 @@ "mocha": true }, "rules": { - "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.test.js", "**/*.spec.js"]}], + "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.test.js", "**/*.spec.js", "**/serviceMocks.js"]}], + max-len: ["error", { "ignoreComments": true, code: 120 }], "valid-jsdoc": ["error", { "requireReturn": true, "requireReturnType": true, diff --git a/migrations/sync.js b/migrations/sync.js index bf4f2277..023b2d06 100644 --- a/migrations/sync.js +++ b/migrations/sync.js @@ -1,5 +1,6 @@ +/* eslint-disable no-console */ /** * Sync the database models to db tables. */ @@ -10,7 +11,7 @@ */ // 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(); 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 4a37f70b..4e73d87c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,5 @@ + const app = require('./app'); const coreLib = require('tc-core-library-js'); const expressListRoutes = require('express-list-routes'); diff --git a/src/middlewares/checkRole.js b/src/middlewares/checkRole.js index 1144e647..156d378d 100644 --- a/src/middlewares/checkRole.js +++ b/src/middlewares/checkRole.js @@ -7,14 +7,16 @@ * @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) { +module.exports = function defineCheckRole(roleName) { + return function checkRoleMiddleware(req, res, next) { + 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.')); + .json(util.wrapErrorResponse(req.id, 403, 'You are not allowed to perform this action.')); } - next(); + return next(); }; }; diff --git a/src/mocks/addBillingAccount.js b/src/mocks/addBillingAccount.js index 3af425e9..d53b0ed0 100644 --- a/src/mocks/addBillingAccount.js +++ b/src/mocks/addBillingAccount.js @@ -1,4 +1,7 @@ +/* eslint-disable max-len */ + const http = require('https'); +const _ = require('lodash'); const options = { method: 'POST', @@ -21,10 +24,7 @@ const req = http.request(options, (res) => { chunks.push(chunk); }); - res.on('end', () => { - const body = Buffer.concat(chunks); - console.log(body.toString()); - }); + res.on('end', _.noop); }); req.write('{\n "billingAccountId": 123456789\n}'); req.end(); diff --git a/src/mocks/addCopilot.js b/src/mocks/addCopilot.js index c6b17ec7..6310453d 100644 --- a/src/mocks/addCopilot.js +++ b/src/mocks/addCopilot.js @@ -1,4 +1,7 @@ +/* eslint-disable max-len */ + const http = require('https'); +const _ = require('lodash'); const options = { method: 'POST', @@ -21,10 +24,7 @@ const req = http.request(options, (res) => { chunks.push(chunk); }); - res.on('end', () => { - const body = Buffer.concat(chunks); - console.log(body.toString()); - }); + res.on('end', _.noop); }); req.write('{\n "copilotUserId": 123456789\n}'); req.end(); diff --git a/src/mocks/createProject.js b/src/mocks/createProject.js index 4aca9b40..1f04482b 100644 --- a/src/mocks/createProject.js +++ b/src/mocks/createProject.js @@ -1,4 +1,7 @@ +/* eslint-disable max-len */ + const http = require('https'); +const _ = require('lodash'); const options = { method: 'POST', @@ -21,10 +24,7 @@ const req = http.request(options, (res) => { chunks.push(chunk); }); - res.on('end', () => { - const body = Buffer.concat(chunks); - console.log(body.toString()); - }); + res.on('end', _.noop); }); req.write('{\n "projectName": "Tony Test 1",\n "projectDescription": "test 1 description"\n}'); diff --git a/src/mocks/direct.js b/src/mocks/direct.js index 4dabcfdc..2b7df972 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', @@ -53,9 +55,10 @@ router.route('/v3/direct/projects') app.logger.info('get direct projects'); res.json(util.wrapResponse(req.id, { projects })); }) - .post((req, res) => { + .post((freq, res) => { + const req = freq; 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 +66,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..edaa7999 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', { +module.exports = function defineProject(sequelize, DataTypes) { + 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 @@ -139,10 +142,11 @@ module.exports = function (sequelize, DataTypes) { logging: (str) => { log.debug(str); }, raw: true, }) - .then((count) => { - count = count[0].count; + .then((fcount) => { + const count = fcount[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/models/projectAttachment.js b/src/models/projectAttachment.js index c7825fc2..8fab5508 100644 --- a/src/models/projectAttachment.js +++ b/src/models/projectAttachment.js @@ -1,6 +1,6 @@ -module.exports = function (sequelize, DataTypes) { +module.exports = function defineProjectAttachment(sequelize, DataTypes) { const ProjectAttachment = sequelize.define('ProjectAttachment', { id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, title: { type: DataTypes.STRING, allowNull: true }, diff --git a/src/models/projectHistory.js b/src/models/projectHistory.js index 9b243810..23bdbe93 100644 --- a/src/models/projectHistory.js +++ b/src/models/projectHistory.js @@ -1,6 +1,6 @@ -module.exports = function (sequelize, DataTypes) { +module.exports = function defineProjectHistory(sequelize, DataTypes) { const ProjectHistory = sequelize.define('ProjectHistory', { id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, projectId: { type: DataTypes.BIGINT, allowNull: false }, diff --git a/src/models/projectMember.js b/src/models/projectMember.js index a8b858b7..2ab4c94b 100644 --- a/src/models/projectMember.js +++ b/src/models/projectMember.js @@ -2,7 +2,7 @@ import _ from 'lodash'; import { PROJECT_MEMBER_ROLE } from '../constants'; -module.exports = function (sequelize, DataTypes) { +module.exports = function defineProjectMember(sequelize, DataTypes) { const ProjectMember = sequelize.define('ProjectMember', { id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, userId: DataTypes.BIGINT, diff --git a/src/permissions/index.js b/src/permissions/index.js index 5465845a..f8563d6a 100644 --- a/src/permissions/index.js +++ b/src/permissions/index.js @@ -1,19 +1,23 @@ const Authorizer = require('tc-core-library-js').Authorizer; +const projectView = require('./project.view'); +const projectEdit = require('./project.edit'); +const projectDelete = require('./project.delete'); +const projectMemberDelete = require('./projectMember.delete'); module.exports = () => { Authorizer.setDeniedStatusCode(403); // anyone can create a project Authorizer.setPolicy('project.create', true); - Authorizer.setPolicy('project.view', require('./project.view')); - Authorizer.setPolicy('project.edit', require('./project.edit')); - Authorizer.setPolicy('project.delete', require('./project.delete')); - Authorizer.setPolicy('project.addMember', require('./project.view')); - Authorizer.setPolicy('project.removeMember', require('./projectMember.delete')); - Authorizer.setPolicy('project.addAttachment', require('./project.edit')); - Authorizer.setPolicy('project.updateAttachment', require('./project.edit')); - Authorizer.setPolicy('project.removeAttachment', require('./project.edit')); - Authorizer.setPolicy('project.updateMember', require('./project.edit')); + Authorizer.setPolicy('project.view', projectView); + Authorizer.setPolicy('project.edit', projectEdit); + Authorizer.setPolicy('project.delete', projectDelete); + Authorizer.setPolicy('project.addMember', projectView); + Authorizer.setPolicy('project.removeMember', projectMemberDelete); + Authorizer.setPolicy('project.addAttachment', projectEdit); + Authorizer.setPolicy('project.updateAttachment', projectEdit); + Authorizer.setPolicy('project.removeAttachment', projectEdit); + Authorizer.setPolicy('project.updateMember', projectEdit); }; diff --git a/src/permissions/project.delete.js b/src/permissions/project.delete.js index b532798c..c07479d3 100644 --- a/src/permissions/project.delete.js +++ b/src/permissions/project.delete.js @@ -1,27 +1,29 @@ /* 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} freq the express request instance + * @return {Promise} Returns a promise */ -module.exports = req => new Promise((resolve, reject) => { - const projectId = _.parseInt(req.params.projectId); +module.exports = freq => new Promise((resolve, reject) => { + const projectId = _.parseInt(freq.params.projectId); return models.ProjectMember.getActiveProjectMembers(projectId) .then((members) => { + const req = freq; req.context = req.context || {}; req.context.currentProjectMembers = members; // check if auth user has acecss to this project - const hasAccess = util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) - || !_.isUndefined(_.find(members, m => m.userId === req.authUser.userId - && ( - m.role === PROJECT_MEMBER_ROLE.CUSTOMER && m.isPrimary - || m.role === PROJECT_MEMBER_ROLE.MANAGER))); + const hasAccess = util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) || + !_.isUndefined(_.find(members, m => m.userId === req.authUser.userId && + ((m.role === PROJECT_MEMBER_ROLE.CUSTOMER && m.isPrimary) || + m.role === PROJECT_MEMBER_ROLE.MANAGER))); if (!hasAccess) { // user is not an admin nor is a registered project member diff --git a/src/permissions/project.edit.js b/src/permissions/project.edit.js index 62854066..57d56847 100644 --- a/src/permissions/project.edit.js +++ b/src/permissions/project.edit.js @@ -1,19 +1,21 @@ -/* 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} freq the express request instance + * @return {Promise} Returns a promise */ -module.exports = req => new Promise((resolve, reject) => { - const projectId = _.parseInt(req.params.projectId); +module.exports = freq => new Promise((resolve, reject) => { + const projectId = _.parseInt(freq.params.projectId); return models.ProjectMember.getActiveProjectMembers(projectId) .then((members) => { + const req = freq; req.context = req.context || {}; req.context.currentProjectMembers = members; // check if auth user has acecss to this project diff --git a/src/permissions/project.view.js b/src/permissions/project.view.js index 26e64740..91776d1a 100644 --- a/src/permissions/project.view.js +++ b/src/permissions/project.view.js @@ -1,21 +1,22 @@ -/* 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} freq the express request instance + * @return {Promise} Returns a promise */ -module.exports = req => new Promise((resolve, reject) => { - const projectId = _.parseInt(req.params.projectId); - const currentUserId = req.authUser.userId; +module.exports = freq => new Promise((resolve, reject) => { + const projectId = _.parseInt(freq.params.projectId); + const currentUserId = freq.authUser.userId; return models.ProjectMember.getActiveProjectMembers(projectId) .then((members) => { + const req = freq; req.context = req.context || {}; req.context.currentProjectMembers = members; // check if auth user has acecss to this project @@ -23,7 +24,8 @@ module.exports = req => new Promise((resolve, reject) => { || util.hasRole(req, USER_ROLE.MANAGER) || !_.isUndefined(_.find(members, m => m.userId === currentUserId)); - // if user is co-pilot and the project doesn't have any copilots then user can access the project + // if user is co-pilot and the project doesn't have any copilots then + // user can access the project if (util.hasRole(req, USER_ROLE.COPILOT)) { return models.Project.getProjectIdsForCopilot(currentUserId) .then((ids) => { diff --git a/src/permissions/projectMember.delete.js b/src/permissions/projectMember.delete.js index 9a07c185..634bb557 100644 --- a/src/permissions/projectMember.delete.js +++ b/src/permissions/projectMember.delete.js @@ -1,37 +1,40 @@ - -/* globals Promise */ - +import _ from 'lodash'; import util from '../util'; import models from '../models'; -import { USER_ROLE, PROJECT_MEMBER_ROLE } from '../constants'; -import _ from 'lodash'; +import { + USER_ROLE, + PROJECT_MEMBER_ROLE, +} from '../constants'; + /** * 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} freq the express request instance + * @return {Promise} Returns a promise */ -module.exports = req => new Promise((resolve, reject) => { - const projectId = _.parseInt(req.params.projectId); +module.exports = freq => new Promise((resolve, reject) => { + const projectId = _.parseInt(freq.params.projectId); return models.ProjectMember.getActiveProjectMembers(projectId) - .then((members) => { - req.context = req.context || {}; - req.context.currentProjectMembers = members; - const authMember = _.find(members, m => m.userId === req.authUser.userId); - const prjMemberId = _.parseInt(req.params.id); - const memberToBeRemoved = _.find(members, m => m.id === prjMemberId); - // check if auth user has acecss to this project - const hasAccess = util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) - || authMember && memberToBeRemoved && ( - authMember.role === PROJECT_MEMBER_ROLE.MANAGER - || authMember.role === PROJECT_MEMBER_ROLE.CUSTOMER && authMember.isPrimary - && memberToBeRemoved.role === PROJECT_MEMBER_ROLE.CUSTOMER - || memberToBeRemoved.userId === req.authUser.userId); + .then((members) => { + const req = freq; + req.context = req.context || {}; + req.context.currentProjectMembers = members; + const authMember = _.find(members, m => m.userId === req.authUser.userId); + const prjMemberId = _.parseInt(req.params.id); + const memberToBeRemoved = _.find(members, m => m.id === prjMemberId); + // check if auth user has acecss to this project + const hasAccess = util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) + || (authMember && memberToBeRemoved && (authMember.role === PROJECT_MEMBER_ROLE.MANAGER || + (authMember.role === PROJECT_MEMBER_ROLE.CUSTOMER && authMember.isPrimary && + memberToBeRemoved.role === PROJECT_MEMBER_ROLE.CUSTOMER) || + memberToBeRemoved.userId === req.authUser.userId)); - if (!hasAccess) { - // user is not an admin nor is a registered project member - return reject(new Error('You do not have permissions to perform this action')); - } - return resolve(true); - }); + if (!hasAccess) { + // user is not an admin nor is a registered project member + return reject(new Error('You do not have permissions to perform this action')); + } + return resolve(true); + }); }); diff --git a/src/routes/attachments/create.js b/src/routes/attachments/create.js index 53abd5a7..c0cbedf1 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; @@ -113,19 +112,24 @@ module.exports = [ }) .then((resp) => { req.log.debug('Retreiving Presigned Url resp: ', JSON.stringify(resp.data, null, 2)); - if (resp.status !== 200 || resp.data.result.status !== 200) { - return Promise.reject(new Error('Unable to fetch pre-signed url')); - } - let response = _.cloneDeep(newAttachment); - response = _.omit(response, ['filePath', 'deletedAt']); + return new Promise((accept, reject) => { + if (resp.status !== 200 || resp.data.result.status !== 200) { + reject(new Error('Unable to fetch pre-signed url')); + } else { + let response = _.cloneDeep(newAttachment); + response = _.omit(response, ['filePath', 'deletedAt']); - response.downloadUrl = resp.data.result.content.preSignedURL; - res.status(201).json(util.wrapResponse(req.id, response, 1, 201)); + response.downloadUrl = resp.data.result.content.preSignedURL; + res.status(201).json(util.wrapResponse(req.id, response, 1, 201)); + accept(); + } + }); }) .catch((err) => { req.log.error('Error adding attachment', err); - err.status = err.status || 500; - next(err); + const rerr = err; + rerr.status = rerr.status || 500; + next(rerr); }); }, ]; diff --git a/src/routes/attachments/create.spec.js b/src/routes/attachments/create.spec.js index 98617cb2..2aff8b54 100644 --- a/src/routes/attachments/create.spec.js +++ b/src/routes/attachments/create.spec.js @@ -1,8 +1,7 @@ - +/* eslint-disable no-unused-expressions */ import chai from 'chai'; import sinon from 'sinon'; import request from 'supertest'; - import server from '../../app'; import models from '../../models'; import util from '../../util'; @@ -99,10 +98,7 @@ describe('Project Attachments', () => { const getSpy = sinon.spy(mockHttpClient, 'get'); const stub = sinon.stub(util, 'getHttpClient', () => mockHttpClient); // mock util s3FileTransfer - util.s3FileTransfer = (req, source, dest) => { - console.log(source, dest); - return Promise.resolve(true); - }; + util.s3FileTransfer = () => Promise.resolve(true); request(server) .post(`/v4/projects/${project1.id}/attachments/`) .set({ @@ -113,22 +109,18 @@ describe('Project Attachments', () => { .expect(201) .end((err, res) => { if (err) { - console.log(err); - return done(err); + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + postSpy.should.have.been.calledOnce; + getSpy.should.have.been.calledOnce; + stub.restore(); + resJson.title.should.equal('Spec.pdf'); + resJson.downloadUrl.should.exist; + resJson.projectId.should.equal(project1.id); + done(); } - - 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); - done(); }); }); }); 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..87727788 100644 --- a/src/routes/attachments/delete.spec.js +++ b/src/routes/attachments/delete.spec.js @@ -1,6 +1,5 @@ - +/* eslint-disable no-unused-expressions */ 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(); + })); }); }); }); @@ -116,17 +111,11 @@ describe('Project Attachments delete', () => { .expect(204) .end((err) => { if (err) { - return done(err); + done(err); + } else { + deleteSpy.should.have.been.calledOnce; + done(); } - deleteSpy.should.have.been.calledOnce; - done(); - // models.ProjectAttachment - // .count({}) - // .then(count=>{ - // count.should.equal(0) - // done() - // }) - // .catch(err => done(err)) }); }); }); diff --git a/src/routes/attachments/update.js b/src/routes/attachments/update.js index 3cb7e40e..e5ee7a34 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. @@ -43,19 +42,21 @@ module.exports = [ }, returning: true, }) - .then((resp) => { - const affectedCount = resp.shift(); - if (affectedCount == 0) { - // handle 404 - const err = new Error(`project attachment not found for project id ${projectId} and member id ${attachmentId}`); - err.status = 404; - return Promise.reject(err); - } - - const attachment = resp.shift()[0]; - req.log.debug('updated project attachment', JSON.stringify(attachment, null, 2)); - res.json(util.wrapResponse(req.id, attachment)); - }) - .catch(err => next(err))); + .then(resp => new Promise((accept, reject) => { + const affectedCount = resp.shift(); + if (affectedCount === 0) { + // handle 404 + const err = new Error('project attachment not found for project id ' + + `${projectId} and member id ${attachmentId}`); + err.status = 404; + reject(err); + } else { + const attachment = resp.shift()[0]; + req.log.debug('updated project attachment', JSON.stringify(attachment, null, 2)); + res.json(util.wrapResponse(req.id, attachment)); + accept(); + } + })) + .catch(err => next(err))); }, ]; diff --git a/src/routes/attachments/update.spec.js b/src/routes/attachments/update.spec.js index abcf17fe..3735eca2 100644 --- a/src/routes/attachments/update.spec.js +++ b/src/routes/attachments/update.spec.js @@ -1,19 +1,17 @@ - +/* eslint-disable no-unused-expressions */ 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'; 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(); + })); }); }); }); @@ -101,13 +96,14 @@ describe('Project Attachments update', () => { .expect(200) .end((err, res) => { if (err) { - return done(err); + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.title.should.equal('updated title'); + resJson.description.should.equal('updated description'); + done(); } - const resJson = res.body.result.content; - should.exist(resJson); - resJson.title.should.equal('updated title'); - resJson.description.should.equal('updated description'); - done(); }); }); }); diff --git a/src/routes/index.js b/src/routes/index.js index 8889c61b..10c0558a 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 @@ -77,11 +78,10 @@ router.use((err, req, res, next) => { // eslint-disable-line no-unused-vars body.result.details = err.details; } } - err.status = err.status || 500; - req.log.error(err); - res - .status(err.status) - .send(body); + const rerr = err; + rerr.status = rerr.status || 500; + req.log.error(rerr); + res.status(rerr.status).send(body); }); // catch 404 and forward to error handler 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..0c714775 100644 --- a/src/routes/projectMembers/create.spec.js +++ b/src/routes/projectMembers/create.spec.js @@ -1,4 +1,4 @@ - +/* eslint-disable no-unused-expressions */ import _ from 'lodash'; import chai from 'chai'; import sinon from 'sinon'; @@ -12,47 +12,47 @@ 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(() => { + .then(() => { + models.Project.create({ + type: 'generic', + directProjectId: 1, + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + }).then((p) => { + project1 = p; + // create members + models.ProjectMember.create({ + userId: 40051332, + projectId: project1.id, + role: 'copilot', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }); + }).then(() => models.Project.create({ type: 'generic', - directProjectId: 1, billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', + name: 'test2', + description: 'test project2', + status: 'reviewed', details: {}, createdBy: 1, updatedBy: 1, - }).then((p) => { - project1 = p; - // create members - models.ProjectMember.create({ - userId: 40051332, - projectId: project1.id, - role: 'copilot', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }); - }).then(() => - models.Project.create({ - type: 'generic', - billingAccountId: 1, - name: 'test2', - description: 'test project2', - status: 'reviewed', - details: {}, - createdBy: 1, - updatedBy: 1, - }).then((p2) => { - project2 = p2; - done(); - })); - }); + }).then((p2) => { + project2 = p2; + done(); + })); + }); }); after((done) => { @@ -70,46 +70,62 @@ describe('Project Members create', () => { it('should return 403 if user does not have permissions', (done) => { request(server) - .post(`/v4/projects/${project1.id}/members/`) - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .send({ param: { userId: 1, role: 'customer' } }) - .expect('Content-Type', /json/) - .expect(403, done); + .post(`/v4/projects/${project1.id}/members/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send({ + param: { + userId: 1, + role: 'customer', + }, + }) + .expect('Content-Type', /json/) + .expect(403, done); }); it('should return 400 if user is already registered', (done) => { request(server) - .post(`/v4/projects/${project1.id}/members/`) - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send({ param: { userId: 40051332, role: 'customer' } }) - .expect('Content-Type', /json/) - .expect(400) - .end((err, res) => { - if (err) { - return done(err); - } + .post(`/v4/projects/${project1.id}/members/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ + param: { + userId: 40051332, + role: 'customer', + }, + }) + .expect('Content-Type', /json/) + .expect(400) + .end((err, res) => { + if (err) { + done(err); + } else { res.body.result.status.should.equal(400); done(); - }); + } + }); }); it('should return 201 and register copilot member for project', (done) => { request(server) - .post(`/v4/projects/${project2.id}/members/`) - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send({ param: { userId: 1, role: 'copilot' } }) - .expect('Content-Type', /json/) - .expect(201) - .end((err, res) => { - if (err) { - return done(err); - } + .post(`/v4/projects/${project2.id}/members/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + userId: 1, + role: 'copilot', + }, + }) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + if (err) { + done(err); + } else { const resJson = res.body.result.content; should.exist(resJson); resJson.role.should.equal('copilot'); @@ -118,22 +134,28 @@ describe('Project Members create', () => { resJson.userId.should.equal(1); server.services.pubsub.publish.calledWith('project.member.added').should.be.true; done(); - }); + } + }); }); it('should return 201 and register customer member', (done) => { request(server) - .post(`/v4/projects/${project1.id}/members/`) - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send({ param: { userId: 1, role: 'customer' } }) - .expect('Content-Type', /json/) - .expect(201) - .end((err, res) => { - if (err) { - return done(err); - } + .post(`/v4/projects/${project1.id}/members/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + userId: 1, + role: 'customer', + }, + }) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + if (err) { + done(err); + } else { const resJson = res.body.result.content; should.exist(resJson); resJson.role.should.equal('customer'); @@ -142,7 +164,8 @@ describe('Project Members create', () => { resJson.userId.should.equal(1); server.services.pubsub.publish.calledWith('project.member.added').should.be.true; done(); - }); + } + }); }); /* @@ -195,17 +218,22 @@ describe('Project Members create', () => { // var amqPubSpy = sinon.spy(server.services.pubsub, 'publish') sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .post(`/v4/projects/${project1.id}/members/`) - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send({ param: { userId: 3, role: 'copilot' } }) - .expect('Content-Type', /json/) - .expect(201) - .end((err, res) => { - if (err) { - return done(err); - } + .post(`/v4/projects/${project1.id}/members/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + userId: 3, + role: 'copilot', + }, + }) + .expect('Content-Type', /json/) + .expect(201) + .end((err, res) => { + if (err) { + done(err); + } else { const resJson = res.body.result.content; should.exist(resJson); resJson.role.should.equal('copilot'); @@ -215,7 +243,8 @@ describe('Project Members create', () => { postSpy.should.have.been.calledOnce; server.services.pubsub.publish.calledWith('project.member.added').should.be.true; done(); - }); + } + }); }); }); }); diff --git a/src/routes/projectMembers/delete.js b/src/routes/projectMembers/delete.js index 27e13f17..6471b94b 100644 --- a/src/routes/projectMembers/delete.js +++ b/src/routes/projectMembers/delete.js @@ -1,9 +1,8 @@ import _ from 'lodash'; - -import models from '../../models'; import { middleware as tcMiddleware } from 'tc-core-library-js'; +import models from '../../models'; import { EVENT, PROJECT_MEMBER_ROLE } from '../../constants'; /** @@ -29,7 +28,7 @@ module.exports = [ err.status = 404; return Promise.reject(err); } - return member.destroy({ logging: console.log }); + return member.destroy({ logging: console.log }); // eslint-disable-line no-console }) .then(member => member.save()) // if primary co-pilot is removed promote the next co-pilot to primary #43 @@ -67,15 +66,15 @@ module.exports = [ accept(member); } }))).then((member) => { - // only return the response after transaction is committed - // fire event - member = member.get({ plain: true }); + // only return the response after transaction is committed + // fire event + const pmember = member.get({ plain: true }); req.app.services.pubsub.publish( - EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED, - member, - { correlationId: req.id }, - ); - req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED, { req, member }); + EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED, + pmember, + { correlationId: req.id }, + ); + req.app.emit(EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED, { req, pmember }); res.status(204).json({}); }).catch(err => next(err)); }, diff --git a/src/routes/projectMembers/delete.spec.js b/src/routes/projectMembers/delete.spec.js index d209e63c..e600742d 100644 --- a/src/routes/projectMembers/delete.spec.js +++ b/src/routes/projectMembers/delete.spec.js @@ -1,4 +1,4 @@ - +/* eslint-disable no-unused-expressions */ import _ from 'lodash'; import chai from 'chai'; import sinon from 'sinon'; @@ -12,48 +12,48 @@ 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(() => { - models.Project.create({ - type: 'generic', - directProjectId: 1, - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - details: {}, + .then(() => { + models.Project.create({ + type: 'generic', + directProjectId: 1, + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + }).then((p) => { + project1 = p; + // create members + return models.ProjectMember.create({ + userId: 40051332, + projectId: project1.id, + role: 'copilot', + isPrimary: true, createdBy: 1, updatedBy: 1, - }).then((p) => { - project1 = p; - // create members + }).then((pm) => { + member1 = pm; return models.ProjectMember.create({ - userId: 40051332, + userId: 40051334, projectId: project1.id, - role: 'copilot', + role: 'manager', isPrimary: true, createdBy: 1, updatedBy: 1, - }).then((pm) => { - member1 = pm; - return models.ProjectMember.create({ - userId: 40051334, - projectId: project1.id, - role: 'manager', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }).then((pm2) => { - member2 = pm2; - done(); - }); + }).then((pm2) => { + member2 = pm2; + done(); }); }); }); + }); }); after((done) => { @@ -71,52 +71,67 @@ describe('Project members delete', () => { it('should return 403 if user does not have permissions', (done) => { request(server) - .delete(`/v4/projects/${project1.id}/members/${member1.id}`) - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .send({ param: { userId: 1, projectId: project1.id, role: 'customer' } }) - .expect(403, done); + .delete(`/v4/projects/${project1.id}/members/${member1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send({ + param: { + userId: 1, + projectId: project1.id, + role: 'customer', + }, + }) + .expect(403, done); }); it('should return 403 if user not found', (done) => { request(server) - .delete(`/v4/projects/${project1.id}/members/8888888`) - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send({ param: { userId: 1, projectId: project1.id, role: 'customer' } }) - .expect(403, done); + .delete(`/v4/projects/${project1.id}/members/8888888`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + userId: 1, + projectId: project1.id, + role: 'customer', + }, + }) + .expect(403, done); }); it('should return 204 if copilot user has access to the project', (done) => { request(server) - .delete(`/v4/projects/${project1.id}/members/${member1.id}`) - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .expect(204) - .end((err) => { - if (err) { - return done(err); - } + .delete(`/v4/projects/${project1.id}/members/${member1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(204) + .end((err) => { + if (err) { + done(err); + } else { const removedMember = { projectId: project1.id, userId: 40051332, 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 } }}) - // .then(count=>{ - // console.log(JSON.stringify(count, null, 2)) - // count.length.should.equal(1) - // done() - // }) - // .catch(err=>done(err)) - }); + } + + // models.ProjectMember + // .count({where: { projectId: project1.id, deletedAt: { $eq: null } }}) + // .then(count=>{ + // console.log(JSON.stringify(count, null, 2)) + // count.length.should.equal(1) + // done() + // }) + // .catch(err=>done(err)) + }); }); it('should return 204 if copilot is removed (promote the next copilot to primary)', (done) => { @@ -156,26 +171,37 @@ describe('Project members delete', () => { .expect(204) .end((err) => { if (err) { - return done(err); + done(err); + } else { + const removedMember = { + projectId: project1.id, + userId: 40051332, + role: 'copilot', + isPrimary: 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, + }, + }) + .then((members) => { + should.exist(members); + members.length.should.equal(1); + const plain = members[0].get({ + plain: true, + }); + plain.role.should.equal('copilot'); + plain.isPrimary.should.equal(true); + plain.userId.should.equal(40051331); + done(); + }); } - const removedMember = { - projectId: project1.id, - userId: 40051332, - role: 'copilot', - isPrimary: 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 } }) - .then((members) => { - should.exist(members); - members.length.should.equal(1); - const plain = members[0].get({ plain: true }); - plain.role.should.equal('copilot'); - plain.isPrimary.should.equal(true); - plain.userId.should.equal(40051331); - done(); - }); }); }); }); @@ -190,7 +216,7 @@ describe('Project members delete', () => { result: { success: true, status: 200, - content: { }, + content: {}, }, }, }), @@ -198,25 +224,27 @@ describe('Project members delete', () => { const postSpy = sinon.spy(mockHttpClient, 'post'); sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .delete(`/v4/projects/${project1.id}/members/${member2.id}`) - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .expect(204) - .end((err) => { - if (err) { - return done(err); - } + .delete(`/v4/projects/${project1.id}/members/${member2.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(204) + .end((err) => { + if (err) { + done(err); + } else { const removedMember = { projectId: project1.id, userId: 40051334, 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(); - }); + } + }); }); it('should return 204 if manager is removed from the project (without direct project id)', (done) => { @@ -229,14 +257,20 @@ describe('Project members delete', () => { result: { success: true, status: 200, - content: { }, + content: {}, }, }, }), }); const postSpy = sinon.spy(mockHttpClient, 'post'); sandbox.stub(util, 'getHttpClient', () => mockHttpClient); - models.Project.update({ directProjectId: null }, { where: { id: project1.id } }) + models.Project.update({ + directProjectId: null, + }, { + where: { + id: project1.id, + }, + }) .then(() => { request(server) .delete(`/v4/projects/${project1.id}/members/${member2.id}`) @@ -246,28 +280,30 @@ describe('Project members delete', () => { .expect(204) .end((err) => { if (err) { - return done(err); + done(err); + } else { + const removedMember = { + projectId: project1.id, + userId: 40051334, + role: 'manager', + isPrimary: true, + }; + server.services.pubsub.publish.calledWith('project.member.removed', + sinon.match(removedMember)).should.be.true; + postSpy.should.not.have.been.calledOnce; + done(); } - const removedMember = { - projectId: project1.id, - userId: 40051334, - role: 'manager', - isPrimary: true, - }; - server.services.pubsub.publish.calledWith('project.member.removed', sinon.match(removedMember)).should.be.true; - postSpy.should.not.have.been.calledOnce; - done(); }); }); }); it('should return 403 if copilot user is trying to remove a manager', (done) => { request(server) - .delete(`/v4/projects/${project1.id}/members/${member2.id}`) - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .expect(403, done); + .delete(`/v4/projects/${project1.id}/members/${member2.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403, done); }); }); }); diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js index ac89f67c..47626083 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,11 +53,12 @@ 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 && - (_.isUndefined(updatedProps.isPrimary) || updatedProps.isPrimary === previousValue.isPrimary)) { + (_.isUndefined(updatedProps.isPrimary) || + updatedProps.isPrimary === previousValue.isPrimary)) { return Promise.resolve(); } @@ -67,7 +68,8 @@ module.exports = [ if (updatedProps.isPrimary) { // if set as primary, other users with same role should no longer be primary - operations.push(models.ProjectMember.update({ isPrimary: false, updatedBy: req.authUser.userId }, + operations.push(models.ProjectMember.update({ isPrimary: false, + updatedBy: req.authUser.userId }, { where: { projectId, diff --git a/src/routes/projectMembers/update.spec.js b/src/routes/projectMembers/update.spec.js index 799a2d5d..e45026ad 100644 --- a/src/routes/projectMembers/update.spec.js +++ b/src/routes/projectMembers/update.spec.js @@ -1,4 +1,4 @@ - +/* eslint-disable no-unused-expressions */ import _ from 'lodash'; import chai from 'chai'; import request from 'supertest'; @@ -11,65 +11,71 @@ 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(() => { - models.Project.create({ - type: 'generic', - directProjectId: 1, - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - details: {}, + .then(() => { + models.Project.create({ + type: 'generic', + directProjectId: 1, + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + }).then((p) => { + project1 = p; + // create members + models.ProjectMember.create({ + userId: 40051334, + projectId: project1.id, + role: 'manager', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', + }).then((pm) => { + member1 = pm.get({ + plain: true, + }); + models.ProjectMember.create({ + userId: 40051332, + projectId: project1.id, + role: 'copilot', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', + }).then((pm2) => { + member2 = pm2.get({ + plain: true, + }); + models.ProjectMember.create({ + userId: 40051330, + projectId: project1.id, + role: 'copilot', + isPrimary: false, createdBy: 1, updatedBy: 1, - }).then((p) => { - project1 = p; - // create members - models.ProjectMember.create({ - userId: 40051334, - projectId: project1.id, - role: 'manager', - isPrimary: false, - createdBy: 1, - updatedBy: 1, - createdAt: '2016-06-30 00:33:07+00', - updatedAt: '2016-06-30 00:33:07+00', - }).then((pm) => { - member1 = pm.get({ plain: true }); - models.ProjectMember.create({ - userId: 40051332, - projectId: project1.id, - role: 'copilot', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - createdAt: '2016-06-30 00:33:07+00', - updatedAt: '2016-06-30 00:33:07+00', - }).then((pm2) => { - member2 = pm2.get({ plain: true }); - models.ProjectMember.create({ - userId: 40051330, - projectId: project1.id, - role: 'copilot', - isPrimary: false, - createdBy: 1, - updatedBy: 1, - createdAt: '2016-06-30 00:33:07+00', - updatedAt: '2016-06-30 00:33:07+00', - }).then((pm3) => { - member3 = pm3.get({ plain: true }); - done(); - }); - }); + createdAt: '2016-06-30 00:33:07+00', + updatedAt: '2016-06-30 00:33:07+00', + }).then((pm3) => { + member3 = pm3.get({ + plain: true, }); + done(); }); }); + }); + }); + }); }); after((done) => { @@ -94,125 +100,136 @@ describe('Project members update', () => { it('should return 403 if user does not have permissions', (done) => { request(server) - .patch(`/v4/projects/${project1.id}/members/${member2.id}`) - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .send(body) - .expect(403, done); + .patch(`/v4/projects/${project1.id}/members/${member2.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send(body) + .expect(403, done); }); it('should return 422 if no role', (done) => { request(server) - .patch(`/v4/projects/${project1.id}/members/${member2.id}`) - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .send({ param: {} }) - .expect(422, done); + .patch(`/v4/projects/${project1.id}/members/${member2.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ + param: {}, + }) + .expect(422, done); }); it('should return 422 if role is invalid', (done) => { request(server) - .patch(`/v4/projects/${project1.id}/members/${member2.id}`) - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .send({ - param: { - role: 'wrong', - }, - }) - .expect(422, done); + .patch(`/v4/projects/${project1.id}/members/${member2.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send({ + param: { + role: 'wrong', + }, + }) + .expect(422, done); }); it('should return 422 if isPrimary is invalid', (done) => { request(server) - .patch(`/v4/projects/${project1.id}/members/${member2.id}`) - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .send({ - param: { - isPrimary: 'wrong', - }, - }) - .expect(422, done); + .patch(`/v4/projects/${project1.id}/members/${member2.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .send({ + param: { + isPrimary: 'wrong', + }, + }) + .expect(422, done); }); it('should return 404 if not exist id', (done) => { request(server) - .patch(`/v4/projects/${project1.id}/members/999999`) - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send(body) - .expect('Content-Type', /json/) - .expect(404) - .end((err, res) => { - if (err) { - return done(err); - } - 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`); - done(); - }); + .patch(`/v4/projects/${project1.id}/members/999999`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(404) + .end((err, res) => { + if (err) { + done(err); + } else { + 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`); + done(); + } + }); }); it('should return 200 if valid user and data(no isPrimary and no updates)', (done) => { request(server) - .patch(`/v4/projects/${project1.id}/members/${member2.id}`) - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send({ - param: { - role: 'customer', - }, - }) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } - const resJson = res.body.result.content; - should.exist(resJson); - resJson.role.should.equal('customer'); - resJson.isPrimary.should.be.true; - resJson.updatedBy.should.equal(40051332); - server.services.pubsub.publish.calledWith('project.member.updated').should.be.true; - done(); - }); + .patch(`/v4/projects/${project1.id}/members/${member2.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + role: 'customer', + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.role.should.equal('customer'); + resJson.isPrimary.should.be.true; + resJson.updatedBy.should.equal(40051332); + server.services.pubsub.publish.calledWith('project.member.updated').should.be.true; + done(); + } + }); }); it('should return 200 if valid user(not copilot any more) for project without direct project id', (done) => { - models.Project.update({ directProjectId: null }, { where: { id: project1.id } }) - .then(() => { - request(server) - .patch(`/v4/projects/${project1.id}/members/${member2.id}`) - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send(body) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } - const resJson = res.body.result.content; - should.exist(resJson); - resJson.role.should.equal(body.param.role); - resJson.isPrimary.should.be.false; - resJson.updatedBy.should.equal(40051332); - server.services.pubsub.publish.calledWith('project.member.updated').should.be.true; - done(); - }); - }, - ); + models.Project.update({ + directProjectId: null, + }, { + where: { + id: project1.id, + }, + }) + .then(() => { + request(server) + .patch(`/v4/projects/${project1.id}/members/${member2.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.role.should.equal(body.param.role); + resJson.isPrimary.should.be.false; + resJson.updatedBy.should.equal(40051332); + server.services.pubsub.publish.calledWith('project.member.updated').should.be.true; + done(); + } + }); + }); }); it('should return 200 if valid user(not copilot any more) and data', (done) => { @@ -233,26 +250,27 @@ describe('Project members update', () => { const deleteSpy = sinon.spy(mockHttpClient, 'delete'); sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .patch(`/v4/projects/${project1.id}/members/${member2.id}`) - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send(body) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } - const resJson = res.body.result.content; - should.exist(resJson); - resJson.role.should.equal(body.param.role); - resJson.isPrimary.should.be.false; - resJson.updatedBy.should.equal(40051332); - deleteSpy.should.have.been.calledOnce; - server.services.pubsub.publish.calledWith('project.member.removed').should.be.true; - done(); - }); + .patch(`/v4/projects/${project1.id}/members/${member2.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.role.should.equal(body.param.role); + resJson.isPrimary.should.be.false; + resJson.updatedBy.should.equal(40051332); + deleteSpy.should.have.been.calledOnce; + server.services.pubsub.publish.calledWith('project.member.removed').should.be.true; + done(); + } + }); }); it.skip('should return 500 if error to remove copilot from direct project', (done) => { @@ -262,24 +280,25 @@ describe('Project members update', () => { const deleteSpy = sinon.spy(mockHttpClient, 'delete'); sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .patch(`/v4/projects/${project1.id}/members/${member2.id}`) - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send(body) - .expect('Content-Type', /json/) - .expect(500) - .end((err, res) => { - if (err) { - return done(err); - } - const result = res.body.result; - result.success.should.be.false; - result.status.should.equal(500); - result.content.message.should.equal('error message'); - deleteSpy.should.have.been.calledOnce; - done(); - }); + .patch(`/v4/projects/${project1.id}/members/${member2.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(500) + .end((err, res) => { + if (err) { + done(err); + } else { + const result = res.body.result; + result.success.should.be.false; + result.status.should.equal(500); + result.content.message.should.equal('error message'); + deleteSpy.should.have.been.calledOnce; + done(); + } + }); }); it('should return 200 if valid user(become manager) and data', (done) => { @@ -292,7 +311,7 @@ describe('Project members update', () => { result: { success: true, status: 200, - content: { }, + content: {}, }, }, }), @@ -300,79 +319,87 @@ describe('Project members update', () => { const postSpy = sinon.spy(mockHttpClient, 'post'); sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .patch(`/v4/projects/${project1.id}/members/${member3.id}`) - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .send({ - param: { - role: 'manager', - isPrimary: false, - }, - }) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } - const resJson = res.body.result.content; - should.exist(resJson); - resJson.role.should.equal('manager'); - resJson.isPrimary.should.be.false; - resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); - resJson.updatedBy.should.equal(40051334); - postSpy.should.have.been.calledOnce; - done(); - }); + .patch(`/v4/projects/${project1.id}/members/${member3.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ + param: { + role: 'manager', + isPrimary: false, + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.role.should.equal('manager'); + resJson.isPrimary.should.be.false; + resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); + resJson.updatedBy.should.equal(40051334); + postSpy.should.have.been.calledOnce; + done(); + } + }); }); it('should return 200 if valid user(become manager) and data (without directProjectId)', (done) => { - models.Project.update({ directProjectId: null }, { where: { id: project1.id } }) - .then(() => { - const mockHttpClient = _.merge(testUtil.mockHttpClient, { - post: () => Promise.resolve({ - status: 200, - data: { - id: 'requesterId', - version: 'v3', - result: { - success: true, - status: 200, - content: { }, - }, - }, - }), - }); - const postSpy = sinon.spy(mockHttpClient, 'post'); - sandbox.stub(util, 'getHttpClient', () => mockHttpClient); - request(server) - .patch(`/v4/projects/${project1.id}/members/${member3.id}`) - .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, - }) - .send({ - param: { - role: 'manager', - isPrimary: false, - }, - }) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } - const resJson = res.body.result.content; - should.exist(resJson); - resJson.role.should.equal('manager'); - resJson.isPrimary.should.be.false; - resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); - resJson.updatedBy.should.equal(40051334); - postSpy.should.not.have.been.calledOnce; - done(); - }); - }); + models.Project.update({ + directProjectId: null, + }, { + where: { + id: project1.id, + }, + }) + .then(() => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + post: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: {}, + }, + }, + }), + }); + const postSpy = sinon.spy(mockHttpClient, 'post'); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .patch(`/v4/projects/${project1.id}/members/${member3.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ + param: { + role: 'manager', + isPrimary: false, + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.role.should.equal('manager'); + resJson.isPrimary.should.be.false; + resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); + resJson.updatedBy.should.equal(40051334); + postSpy.should.not.have.been.calledOnce; + done(); + } + }); + }); }); it('should return 200 if valid user(become copilot) and data', (done) => { @@ -395,39 +422,32 @@ describe('Project members update', () => { const postSpy = sinon.spy(mockHttpClient, 'post'); sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .patch(`/v4/projects/${project1.id}/members/${member1.id}`) - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .send({ - param: { - role: 'copilot', - isPrimary: true, - }, - }) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } - const resJson = res.body.result.content; - should.exist(resJson); - resJson.role.should.equal('copilot'); - resJson.isPrimary.should.be.true; - resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); - resJson.updatedBy.should.equal(40051332); - postSpy.should.have.been.calledOnce; - done(); - // models.ProjectMember.findById(member2.id) - // .then(pm=> { - // pm.isPrimary.should.be.false - // pm.updatedAt.should.not.equal("2016-06-30 00:33:07+00") - // pm.updatedBy.should.equal(40051332) - // done() - // }) - // .catch(err => done(err)) - }); + .patch(`/v4/projects/${project1.id}/members/${member1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + role: 'copilot', + isPrimary: true, + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.role.should.equal('copilot'); + resJson.isPrimary.should.be.true; + resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); + resJson.updatedBy.should.equal(40051332); + postSpy.should.have.been.calledOnce; + done(); + } + }); }); }); }); diff --git a/src/routes/projects/create.js b/src/routes/projects/create.js index 76d61b7b..df629acd 100644 --- a/src/routes/projects/create.js +++ b/src/routes/projects/create.js @@ -34,7 +34,8 @@ const createProjectValdiations = { title: Joi.string(), address: Joi.string(), })).optional().allow(null), - estimatedPrice: Joi.number().precision(2).positive().optional().allow(null), + estimatedPrice: Joi.number().precision(2).positive().optional() + .allow(null), terms: Joi.array().items(Joi.number().positive()).optional(), external: Joi.object().keys({ id: Joi.string(), diff --git a/src/routes/projects/create.spec.js b/src/routes/projects/create.spec.js index 477e9ecb..c41fa612 100644 --- a/src/routes/projects/create.spec.js +++ b/src/routes/projects/create.spec.js @@ -1,4 +1,4 @@ - +/* eslint-disable no-unused-expressions */ import _ from 'lodash'; import chai from 'chai'; import sinon from 'sinon'; @@ -12,9 +12,7 @@ import RabbitMQService from '../../services/rabbitmq'; const should = chai.should(); sinon.stub(RabbitMQService.prototype, 'init', () => {}); -sinon.stub(RabbitMQService.prototype, 'publish', () => { - console.log('publish called'); -}); +sinon.stub(RabbitMQService.prototype, 'publish', () => {}); describe('Project create', () => { before((done) => { @@ -83,13 +81,14 @@ describe('Project create', () => { .expect(201) .end((err, res) => { if (err) { - return done(err); + done(err); + } else { + const result = res.body.result; + result.success.should.be.truthy; + result.status.should.equal(201); + server.services.pubsub.publish.calledWith('project.draft-created').should.be.true; + done(); } - const result = res.body.result; - result.success.should.be.truthy; - result.status.should.equal(201); - server.services.pubsub.publish.calledWith('project.draft-created').should.be.true; - done(); }); }); @@ -121,25 +120,26 @@ describe('Project create', () => { .expect(201) .end((err, res) => { if (err) { - return done(err); + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + should.exist(resJson.billingAccountId); + should.exist(resJson.name); + resJson.directProjectId.should.be.eql(128); + resJson.status.should.be.eql('draft'); + resJson.type.should.be.eql(body.param.type); + resJson.members.should.have.lengthOf(1); + resJson.members[0].role.should.be.eql('customer'); + resJson.members[0].userId.should.be.eql(40051331); + resJson.members[0].projectId.should.be.eql(resJson.id); + resJson.members[0].isPrimary.should.be.truthy; + resJson.bookmarks.should.have.lengthOf(1); + resJson.bookmarks[0].title.should.be.eql('title1'); + resJson.bookmarks[0].address.should.be.eql('address1'); + server.services.pubsub.publish.calledWith('project.draft-created').should.be.true; + done(); } - const resJson = res.body.result.content; - should.exist(resJson); - should.exist(resJson.billingAccountId); - should.exist(resJson.name); - resJson.directProjectId.should.be.eql(128); - resJson.status.should.be.eql('draft'); - resJson.type.should.be.eql(body.param.type); - resJson.members.should.have.lengthOf(1); - resJson.members[0].role.should.be.eql('customer'); - resJson.members[0].userId.should.be.eql(40051331); - resJson.members[0].projectId.should.be.eql(resJson.id); - resJson.members[0].isPrimary.should.be.truthy; - resJson.bookmarks.should.have.lengthOf(1); - resJson.bookmarks[0].title.should.be.eql('title1'); - resJson.bookmarks[0].address.should.be.eql('address1'); - server.services.pubsub.publish.calledWith('project.draft-created').should.be.true; - done(); }); }); }); 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..bf9ffd81 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'; +/* eslint-disable no-unused-expressions */ 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,12 +91,13 @@ describe('Project delete test', () => { Authorization: `Bearer ${testUtil.jwts.member}`, }) .expect(204) - .end((err, resp) => { + .end((err) => { if (err) { - return done(err); + done(err); + } else { + server.services.pubsub.publish.calledWith('project.deleted').should.be.true; + done(); } - server.services.pubsub.publish.calledWith('project.deleted').should.be.true; - done(); }); }); }); 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..e1251041 100644 --- a/src/routes/projects/get.spec.js +++ b/src/routes/projects/get.spec.js @@ -1,4 +1,4 @@ - +/* eslint-disable no-unused-expressions */ import chai from 'chai'; import sinon from 'sinon'; import request from 'supertest'; @@ -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(() => { @@ -103,16 +103,17 @@ describe('GET Project', () => { .expect(200) .end((err, res) => { if (err) { - return done(err); + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + should.not.exist(resJson.deletedAt); + should.not.exist(resJson.billingAccountId); + should.exist(resJson.name); + resJson.status.should.be.eql('draft'); + resJson.members.should.have.lengthOf(2); + done(); } - const resJson = res.body.result.content; - should.exist(resJson); - should.not.exist(resJson.deletedAt); - should.not.exist(resJson.billingAccountId); - should.exist(resJson.name); - resJson.status.should.be.eql('draft'); - resJson.members.should.have.lengthOf(2); - done(); }); }); @@ -126,11 +127,12 @@ describe('GET Project', () => { .expect(200) .end((err, res) => { if (err) { - return done(err); + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + done(); } - const resJson = res.body.result.content; - should.exist(resJson); - done(); }); }); @@ -171,16 +173,17 @@ describe('GET Project', () => { .expect(200) .end((err, res) => { if (err) { - return done(err); + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + spy.should.have.been.calledOnce; + resJson.attachments.should.have.lengthOf(1); + resJson.attachments[0].filePath.should.equal(attachment.filePath); + resJson.attachments[0].downloadUrl.should.exist; + stub.restore(); + done(); } - const resJson = res.body.result.content; - should.exist(resJson); - spy.should.have.been.calledOnce; - resJson.attachments.should.have.lengthOf(1); - resJson.attachments[0].filePath.should.equal(attachment.filePath); - resJson.attachments[0].downloadUrl.should.exist; - stub.restore(); - done(); }); }); }); diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index eeadb656..9d083764 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -27,10 +27,10 @@ const PROJECT_ATTACHMENT_ATTRIBUTES = _.without( 'deletedAt', ); -const _retrieveProjects = (req, criteria, sort, fields) => { +const retrieveProjects = (req, criteria, sort, ffields) => { // order by const order = sort ? [sort.split(' ')] : [['createdAt', 'asc']]; - fields = fields ? fields.split(',') : []; + let fields = ffields ? ffields.split(',') : []; // parse the fields string to determine what fields are to be returned fields = util.parseFields(fields, { projects: PROJECT_ATTRIBUTES, @@ -75,7 +75,8 @@ const _retrieveProjects = (req, criteria, sort, fields) => { .then((values) => { const allMembers = retrieveMembers ? values.shift() : []; const allAttachments = retrieveAttachments ? values.shift() : []; - _.forEach(rows, (p) => { + _.forEach(rows, (fp) => { + const p = fp; // if values length is 1 it could be either attachments or members if (retrieveMembers) { p.members = _.filter(allMembers, m => m.projectId === p.id); @@ -98,7 +99,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 = [ @@ -128,7 +129,7 @@ module.exports = [ && (util.hasRole(req, USER_ROLE.TOPCODER_ADMIN) || util.hasRole(req, USER_ROLE.MANAGER))) { // admins & topcoder managers can see all projects - return _retrieveProjects(req, criteria, sort, req.query.fields) + return retrieveProjects(req, criteria, sort, req.query.fields) .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) .catch(err => next(err)); } @@ -150,7 +151,7 @@ module.exports = [ } else { criteria.filters.id = { $in: accessibleProjectIds }; } - return _retrieveProjects(req, criteria, sort, req.query.fields); + return retrieveProjects(req, criteria, sort, req.query.fields); }) .then(result => res.json(util.wrapResponse(req.id, result.rows, result.count))) .catch(err => next(err)); diff --git a/src/routes/projects/list.spec.js b/src/routes/projects/list.spec.js index a69a8c2c..d4787d56 100644 --- a/src/routes/projects/list.spec.js +++ b/src/routes/projects/list.spec.js @@ -1,5 +1,4 @@ - - +/* eslint-disable no-unused-expressions */ import chai from 'chai'; import request from 'supertest'; @@ -11,118 +10,121 @@ 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'); - return; + return null; } return models.sequelize - .query('ALTER TABLE projects ADD COLUMN "projectFullText" text;') + .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 + .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 OR REPLACE FUNCTION project_text_update_trigger() RETURNS trigger AS $$ ' + + 'begin ' + + 'new."projectFullText" := ' + + '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('UPDATE projects SET "projectFullText" = lower(' + - '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 OR REPLACE FUNCTION project_text_update_trigger() RETURNS trigger AS $$ ' + - 'begin ' + - 'new."projectFullText" := ' + - '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); - }); + .query('CREATE TRIGGER project_text_update BEFORE INSERT OR UPDATE ON projects' + + ' FOR EACH ROW EXECUTE PROCEDURE project_text_update_trigger();')); } describe('LIST Project', () => { - let project1, - project2; + let project1; + let project2; before((done) => { testUtil.clearDb() - .then(() => addFullTextIndex()) - .then(() => { - const p1 = models.Project.create({ - type: 'generic', - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'active', - details: { - utm: { - code: 'code1', - }, + .then(() => addFullTextIndex()) + .then(() => { + const p1 = models.Project.create({ + type: 'generic', + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'active', + details: { + utm: { + code: 'code1', }, + }, + createdBy: 1, + updatedBy: 1, + }).then((p) => { + project1 = p; + // create members + const pm1 = models.ProjectMember.create({ + userId: 40051331, + projectId: project1.id, + role: 'customer', + isPrimary: true, createdBy: 1, updatedBy: 1, - }).then((p) => { - project1 = p; - // create members - const pm1 = models.ProjectMember.create({ - userId: 40051331, - projectId: project1.id, - role: 'customer', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }); - const pm2 = models.ProjectMember.create({ - userId: 40051332, - projectId: project1.id, - role: 'copilot', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }); - const pa1 = models.ProjectAttachment.create({ - title: 'Spec', - projectId: project1.id, - description: 'specification', - filePath: 'projects/1/spec.pdf', - contentType: 'application/pdf', - createdBy: 1, - updatedBy: 1, - }); - return Promise.all([pm1, pm2, pa1]); }); - - const p2 = models.Project.create({ - type: 'visual_design', - billingAccountId: 1, - name: 'test2', - description: 'test project2', - status: 'draft', - details: {}, + const pm2 = models.ProjectMember.create({ + userId: 40051332, + projectId: project1.id, + role: 'copilot', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }); + const pa1 = models.ProjectAttachment.create({ + title: 'Spec', + projectId: project1.id, + description: 'specification', + filePath: 'projects/1/spec.pdf', + contentType: 'application/pdf', createdBy: 1, updatedBy: 1, - }).then((p) => { - project2 = p; - return models.ProjectMember.create({ - userId: 40051332, - projectId: project2.id, - role: 'copilot', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }); }); - const p3 = models.Project.create({ - type: 'visual_design', - billingAccountId: 1, - name: 'test2', - description: 'test project3', - status: 'reviewed', - details: {}, + return Promise.all([pm1, pm2, pa1]); + }); + + const p2 = models.Project.create({ + type: 'visual_design', + billingAccountId: 1, + name: 'test2', + description: 'test project2', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + }).then((p) => { + project2 = p; + return models.ProjectMember.create({ + userId: 40051332, + projectId: project2.id, + role: 'copilot', + isPrimary: true, createdBy: 1, updatedBy: 1, }); - return Promise.all([p1, p2, p3]) - .then(() => done()); }); + const p3 = models.Project.create({ + type: 'visual_design', + billingAccountId: 1, + name: 'test2', + description: 'test project3', + status: 'reviewed', + details: {}, + createdBy: 1, + updatedBy: 1, + }); + return Promise.all([p1, p2, p3]) + .then(() => done()); + }); }); after((done) => { @@ -132,48 +134,51 @@ describe('LIST Project', () => { describe('GET All /projects/', () => { it('should return 403 if user is not authenticated', (done) => { request(server) - .get('/v4/projects/') - .expect(403, done); + .get('/v4/projects/') + .expect(403, done); }); it('should return 200 and no projects if user does not have access', (done) => { request(server) - .get(`/v4/projects/?filter=id%3Din%28${project2.id}%29`) - .set({ - Authorization: `Bearer ${testUtil.jwts.member}`, - }) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } + .get(`/v4/projects/?filter=id%3Din%28${project2.id}%29`) + .set({ + Authorization: `Bearer ${testUtil.jwts.member}`, + }) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { res.body.result.content.should.have.lengthOf(0); done(); - }); + } + }); }); it('should return the project when registerd member attempts to access the project', (done) => { request(server) - .get('/v4/projects/?filter=status%3Ddraft') - .set({ - Authorization: `Bearer ${testUtil.jwts.copilot}`, - }) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } + .get('/v4/projects/?filter=status%3Ddraft') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { const resJson = res.body.result.content; res.body.result.metadata.totalCount.should.equal(1); should.exist(resJson); resJson.should.have.lengthOf(1); resJson[0].id.should.equal(project2.id); done(); - }); + } + }); }); - 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({ @@ -183,111 +188,117 @@ describe('LIST Project', () => { .expect(200) .end((err, res) => { if (err) { - return done(err); + done(err); + } else { + const resJson = res.body.result.content; + res.body.result.metadata.totalCount.should.equal(3); + should.exist(resJson); + resJson.should.have.lengthOf(3); + done(); } - const resJson = res.body.result.content; - res.body.result.metadata.totalCount.should.equal(3); - should.exist(resJson); - resJson.should.have.lengthOf(3); - done(); }); }); it('should return the project for administrator ', (done) => { request(server) - .get('/v4/projects/?fields=id%2Cmembers.id') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } + .get('/v4/projects/?fields=id%2Cmembers.id') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { const resJson = res.body.result.content; should.exist(resJson); resJson.should.have.lengthOf(3); done(); - }); + } + }); }); it('should return all projects that match when filtering by name', (done) => { request(server) - .get('/v4/projects/?filter=keyword%3Dtest') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } + .get('/v4/projects/?filter=keyword%3Dtest') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { const resJson = res.body.result.content; should.exist(resJson); resJson.should.have.lengthOf(3); done(); - }); + } + }); }); it('should return the project when filtering by keyword, which matches the name', (done) => { request(server) - .get('/v4/projects/?filter=keyword%3D1') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } + .get('/v4/projects/?filter=keyword%3D1') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { const resJson = res.body.result.content; should.exist(resJson); resJson.should.have.lengthOf(1); resJson[0].name.should.equal('test1'); done(); - }); + } + }); }); it('should return the project when filtering by keyword, which matches the description', (done) => { request(server) - .get('/v4/projects/?filter=keyword%3Dproject') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } + .get('/v4/projects/?filter=keyword%3Dproject') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { const resJson = res.body.result.content; should.exist(resJson); resJson.should.have.lengthOf(3); done(); - }); + } + }); }); it('should return the project when filtering by keyword, which matches the details', (done) => { request(server) - .get('/v4/projects/?filter=keyword%3Dcode') - .set({ - Authorization: `Bearer ${testUtil.jwts.admin}`, - }) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } + .get('/v4/projects/?filter=keyword%3Dcode') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { const resJson = res.body.result.content; should.exist(resJson); resJson.should.have.lengthOf(1); resJson[0].name.should.equal('test1'); done(); - }); + } + }); }); }); }); diff --git a/src/routes/projects/update.js b/src/routes/projects/update.js index 4c0aa7e8..448357f4 100644 --- a/src/routes/projects/update.js +++ b/src/routes/projects/update.js @@ -33,6 +33,7 @@ const mergeCustomizer = (objValue, srcValue) => { if (_.isArray(objValue)) { return srcValue; } + return undefined; }; const updateProjectValdiations = { @@ -112,7 +113,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 +155,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..03e27cc1 100644 --- a/src/routes/projects/update.spec.js +++ b/src/routes/projects/update.spec.js @@ -1,4 +1,4 @@ - +/* eslint-disable no-unused-expressions */ import _ from 'lodash'; import chai from 'chai'; import sinon from 'sinon'; @@ -8,14 +8,16 @@ import models from '../../models'; import server from '../../app'; import testUtil from '../../tests/util'; import util from '../../util'; -import { PROJECT_STATUS } from '../../constants'; +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); }); @@ -69,118 +71,129 @@ describe('Project', () => { createdAt: '2016-06-30 00:33:07+00', updatedAt: '2016-06-30 00:33:07+00', }]) - .then(() => models.Project.findAll()) - .then((projects) => { - project1 = projects[0]; - project2 = projects[1]; - project3 = projects[2]; - return models.ProjectMember.bulkCreate([{ - projectId: project1.id, - role: 'copilot', - userId: 40051332, - createdBy: 1, - updatedBy: 1, - }, { - projectId: project1.id, - role: 'manager', - userId: 40051334, - createdBy: 1, - updatedBy: 1, - }, { - projectId: project2.id, - role: 'copilot', - userId: 40051332, - createdBy: 1, - updatedBy: 1, - }]); - }).then(() => done()); + .then(() => models.Project.findAll()) + .then((projects) => { + project1 = projects[0]; + project2 = projects[1]; + project3 = projects[2]; + return models.ProjectMember.bulkCreate([{ + projectId: project1.id, + role: 'copilot', + userId: 40051332, + createdBy: 1, + updatedBy: 1, + }, { + projectId: project1.id, + role: 'manager', + userId: 40051334, + createdBy: 1, + updatedBy: 1, + }, { + projectId: project2.id, + role: 'copilot', + userId: 40051332, + createdBy: 1, + updatedBy: 1, + }]); + }).then(() => done()); }); it('should return 403 if user is not authenticated', (done) => { request(server) - .patch(`/v4/projects/${project1.id}`) - .send(body) - .expect(403, done); + .patch(`/v4/projects/${project1.id}`) + .send(body) + .expect(403, done); }); it('should return 400 if update completed project', (done) => { request(server) - .patch(`/v4/projects/${project2.id}`) - .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) - .send(body) - .expect('Content-Type', /json/) - .expect(400) - .end((err, res) => { - if (err) { - return done(err); - } + .patch(`/v4/projects/${project2.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(400) + .end((err, res) => { + if (err) { + done(err); + } else { const result = res.body.result; result.success.should.be.false; result.status.should.equal(400); result.content.message.should.equal('Unable to update project'); done(); - }); + } + }); }); it('should return 403 if invalid user will launch a project', (done) => { request(server) - .patch(`/v4/projects/${project1.id}`) - .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) - .send({ - param: { - status: 'active', - }, - }) - .expect('Content-Type', /json/) - .expect(403) - .end((err, res) => { - if (err) { - return done(err); - } + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + status: 'active', + }, + }) + .expect('Content-Type', /json/) + .expect(403) + .end((err, res) => { + if (err) { + done(err); + } else { const result = res.body.result; result.success.should.be.false; result.status.should.equal(403); result.content.message.should.equal('Only assigned topcoder-managers or topcoder admins' + ' should be allowed to launch a project'); done(); - }); + } + }); }); it('should return 200 if topcoder manager user will launch a project', (done) => { request(server) - .patch(`/v4/projects/${project1.id}`) - .set({ Authorization: `Bearer ${testUtil.jwts.manager}` }) - .send({ - param: { - status: 'active', - }, - }) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ + param: { + status: 'active', + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { const result = res.body.result; result.success.should.be.true; result.status.should.equal(200); result.content.status.should.equal('active'); server.services.pubsub.publish.calledWith('project.updated').should.be.true; done(); - }); + } + }); }); it('should return 200 if valid user and data', (done) => { request(server) - .patch(`/v4/projects/${project1.id}`) - .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) - .send(body) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { const resJson = res.body.result.content; should.exist(resJson); resJson.name.should.equal('updatedProject name'); @@ -188,23 +201,29 @@ describe('Project', () => { resJson.updatedBy.should.equal(40051332); server.services.pubsub.publish.calledWith('project.updated').should.be.true; done(); - }); + } + }); }); it('should return 200 and project history should be updated (status is not set)', (done) => { - const sbody = _.cloneDeep(body); - // set project status to be updated - sbody.param.status = PROJECT_STATUS.IN_REVIEW; + const mbody = { + param: { + name: 'updatedProject name', + status: PROJECT_STATUS.IN_REVIEW, + }, + }; request(server) - .patch(`/v4/projects/${project1.id}`) - .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) - .send(sbody) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send(mbody) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { const resJson = res.body.result.content; should.exist(resJson); resJson.name.should.equal('updatedProject name'); @@ -214,32 +233,45 @@ describe('Project', () => { // validate that project history is updated models.ProjectHistory.findAll({ limit: 1, - where: { projectId: project1.id }, - order: [['createdAt', 'DESC']], + where: { + projectId: project1.id, + }, + order: [ + ['createdAt', 'DESC'], + ], }).then((histories) => { should.exist(histories); histories.length.should.equal(1); - const history = histories[0].get({ plain: true }); + const history = histories[0].get({ + plain: true, + }); history.status.should.equal(PROJECT_STATUS.IN_REVIEW); history.projectId.should.equal(project1.id); done(); }); - }); + } + }); }); it('should return 200 and project history should not be updated (status is not updated)', (done) => { - const sbody = _.cloneDeep(body); - sbody.param.status = PROJECT_STATUS.DRAFT; + const mbody = { + param: { + name: 'updatedProject name', + status: PROJECT_STATUS.DRAFT, + }, + }; request(server) - .patch(`/v4/projects/${project1.id}`) - .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) - .send(sbody) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send(mbody) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { const resJson = res.body.result.content; should.exist(resJson); resJson.name.should.equal('updatedProject name'); @@ -248,49 +280,65 @@ describe('Project', () => { server.services.pubsub.publish.calledWith('project.updated').should.be.true; // validate that project history is not updated models.ProjectHistory.findAll({ - where: { projectId: project1.id }, + where: { + projectId: project1.id, + }, }).then((histories) => { should.exist(histories); histories.length.should.equal(0); done(); }); - }); + } + }); }); it('should return 422 as cancel reason is mandatory if project status is cancelled', (done) => { - const sbody = _.cloneDeep(body); - sbody.param.status = PROJECT_STATUS.CANCELLED; + const mbody = { + param: { + name: 'updatedProject name', + status: PROJECT_STATUS.CANCELLED, + }, + }; request(server) - .patch(`/v4/projects/${project1.id}`) - .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) - .send(sbody) - .expect('Content-Type', /json/) - .expect(422) - .end((err, res) => { - if (err) { - return done(err); - } + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send(mbody) + .expect('Content-Type', /json/) + .expect(422) + .end((err, res) => { + if (err) { + done(err); + } else { const result = res.body.result; result.success.should.be.false; result.status.should.equal(422); done(); - }); + } + }); }); it('should return 200 and project history should be updated for cancelled project', (done) => { - const sbody = _.cloneDeep(body); - sbody.param.status = PROJECT_STATUS.CANCELLED; - sbody.param.cancelReason = 'price/cost'; + const mbody = { + param: { + name: 'updatedProject name', + status: PROJECT_STATUS.CANCELLED, + cancelReason: 'price/cost', + }, + }; request(server) - .patch(`/v4/projects/${project1.id}`) - .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) - .send(sbody) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send(mbody) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { const resJson = res.body.result.content; should.exist(resJson); resJson.name.should.equal('updatedProject name'); @@ -299,125 +347,179 @@ describe('Project', () => { server.services.pubsub.publish.calledWith('project.updated').should.be.true; // validate that project history is updated models.ProjectHistory.findAll({ - where: { projectId: project1.id }, + where: { + projectId: project1.id, + }, }).then((histories) => { should.exist(histories); histories.length.should.equal(1); - const history = histories[0].get({ plain: true }); + const history = histories[0].get({ + plain: true, + }); history.status.should.equal(PROJECT_STATUS.CANCELLED); history.projectId.should.equal(project1.id); history.cancelReason.should.equal('price/cost'); done(); }); - }); + } + }); }); it('should return 200, manager is allowed to transition project out of cancel status', (done) => { - models.Project.update({ status: PROJECT_STATUS.CANCELLED }, { where: { id: project1.id } }) + models.Project.update({ + status: PROJECT_STATUS.CANCELLED, + }, { + where: { + id: project1.id, + }, + }) .then(() => { - const sbody = _.cloneDeep(body); - sbody.param.status = PROJECT_STATUS.ACTIVE; + const mbody = { + param: { + name: 'updatedProject name', + status: PROJECT_STATUS.ACTIVE, + }, + }; request(server) .patch(`/v4/projects/${project1.id}`) - .set({ Authorization: `Bearer ${testUtil.jwts.manager}` }) - .send(sbody) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send(mbody) .expect('Content-Type', /json/) .expect(200) .end((err, res) => { if (err) { - return done(err); + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.name.should.equal('updatedProject name'); + resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); + resJson.updatedBy.should.equal(40051334); + server.services.pubsub.publish.calledWith('project.updated').should.be.true; + // validate that project history is updated + models.ProjectHistory.findAll({ + where: { + projectId: project1.id, + }, + }).then((histories) => { + should.exist(histories); + histories.length.should.equal(1); + const history = histories[0].get({ + plain: true, + }); + history.status.should.equal(PROJECT_STATUS.ACTIVE); + history.projectId.should.equal(project1.id); + done(); + }); } - const resJson = res.body.result.content; - should.exist(resJson); - resJson.name.should.equal('updatedProject name'); - resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); - resJson.updatedBy.should.equal(40051334); - server.services.pubsub.publish.calledWith('project.updated').should.be.true; - // validate that project history is updated - models.ProjectHistory.findAll({ - where: { projectId: project1.id }, - }).then((histories) => { - should.exist(histories); - histories.length.should.equal(1); - const history = histories[0].get({ plain: true }); - history.status.should.equal(PROJECT_STATUS.ACTIVE); - history.projectId.should.equal(project1.id); - done(); - }); }); }); }); it('should return 200, admin is allowed to transition project out of cancel status', (done) => { - models.Project.update({ status: PROJECT_STATUS.CANCELLED }, { where: { id: project1.id } }) + models.Project.update({ + status: PROJECT_STATUS.CANCELLED, + }, { + where: { + id: project1.id, + }, + }) .then(() => { - const sbody = _.cloneDeep(body); - sbody.param.status = PROJECT_STATUS.ACTIVE; + const mbody = { + param: { + name: 'updatedProject name', + status: PROJECT_STATUS.ACTIVE, + }, + }; request(server) .patch(`/v4/projects/${project1.id}`) - .set({ Authorization: `Bearer ${testUtil.jwts.admin}` }) - .send(sbody) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send(mbody) .expect('Content-Type', /json/) .expect(200) .end((err, res) => { if (err) { - return done(err); + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.name.should.equal('updatedProject name'); + resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); + resJson.updatedBy.should.equal(40051333); + server.services.pubsub.publish.calledWith('project.updated').should.be.true; + // validate that project history is updated + models.ProjectHistory.findAll({ + where: { + projectId: project1.id, + }, + }).then((histories) => { + should.exist(histories); + histories.length.should.equal(1); + const history = histories[0].get({ + plain: true, + }); + history.status.should.equal(PROJECT_STATUS.ACTIVE); + history.projectId.should.equal(project1.id); + done(); + }); } - const resJson = res.body.result.content; - should.exist(resJson); - resJson.name.should.equal('updatedProject name'); - resJson.updatedAt.should.not.equal('2016-06-30 00:33:07+00'); - resJson.updatedBy.should.equal(40051333); - server.services.pubsub.publish.calledWith('project.updated').should.be.true; - // validate that project history is updated - models.ProjectHistory.findAll({ - where: { projectId: project1.id }, - }).then((histories) => { - should.exist(histories); - histories.length.should.equal(1); - const history = histories[0].get({ plain: true }); - history.status.should.equal(PROJECT_STATUS.ACTIVE); - history.projectId.should.equal(project1.id); - done(); - }); }); }); }); it('should return 403, copilot is not allowed to transition project out of cancel status', (done) => { - models.Project.update({ status: PROJECT_STATUS.CANCELLED }, { where: { id: project1.id } }) + models.Project.update({ + status: PROJECT_STATUS.CANCELLED, + }, { + where: { + id: project1.id, + }, + }) .then(() => { - const sbody = _.cloneDeep(body); - sbody.param.status = PROJECT_STATUS.ACTIVE; + const mbody = { + param: { + name: 'updatedProject name', + status: PROJECT_STATUS.ACTIVE, + }, + }; request(server) .patch(`/v4/projects/${project1.id}`) - .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) - .send(sbody) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send(mbody) .expect('Content-Type', /json/) .expect(403) .end((err, res) => { if (err) { - return done(err); + done(err); + } else { + const result = res.body.result; + result.success.should.be.false; + result.status.should.equal(403); + done(); } - const result = res.body.result; - result.success.should.be.false; - result.status.should.equal(403); - done(); }); }); }); it('should return 200 and project history should not be updated', (done) => { request(server) - .patch(`/v4/projects/${project1.id}`) - .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) - .send(body) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { const resJson = res.body.result.content; should.exist(resJson); resJson.name.should.equal('updatedProject name'); @@ -426,13 +528,16 @@ describe('Project', () => { server.services.pubsub.publish.calledWith('project.updated').should.be.true; // validate that project history is not updated models.ProjectHistory.findAll({ - where: { projectId: project1.id }, + where: { + projectId: project1.id, + }, }).then((histories) => { should.exist(histories); histories.length.should.equal(0); done(); }); - }); + } + }); }); it('should return 500 if error to sync billing account id', (done) => { @@ -441,25 +546,28 @@ describe('Project', () => { }); sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .patch(`/v4/projects/${project1.id}`) - .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) - .send({ - param: { - billingAccountId: 123, - }, - }) - .expect('Content-Type', /json/) - .expect(500) - .end((err, res) => { - if (err) { - return done(err); - } + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + billingAccountId: 123, + }, + }) + .expect('Content-Type', /json/) + .expect(500) + .end((err, res) => { + if (err) { + done(err); + } else { const result = res.body.result; result.success.should.be.false; result.status.should.equal(500); result.content.message.should.equal('error message'); done(); - }); + } + }); }); it('should return 200 and sync new billing account id', (done) => { @@ -482,19 +590,21 @@ describe('Project', () => { const postSpy = sinon.spy(mockHttpClient, 'post'); sandbox.stub(util, 'getHttpClient', () => mockHttpClient); request(server) - .patch(`/v4/projects/${project1.id}`) - .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) - .send({ - param: { - billingAccountId: 123, - }, - }) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + billingAccountId: 123, + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { const resJson = res.body.result.content; should.exist(resJson); resJson.billingAccountId.should.equal(123); @@ -503,48 +613,54 @@ describe('Project', () => { postSpy.should.have.been.calledOnce; server.services.pubsub.publish.calledWith('project.updated').should.be.true; done(); - }); + } + }); }); it('should return 200 and not sync same billing account id', (done) => { request(server) - .patch(`/v4/projects/${project1.id}`) - .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) - .send({ - param: { - billingAccountId: 1, - }, - }) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + billingAccountId: 1, + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { const resJson = res.body.result.content; should.exist(resJson); resJson.billingAccountId.should.equal(1); resJson.billingAccountId.should.equal(1); server.services.pubsub.publish.calledWith('project.updated').should.be.true; done(); - }); + } + }); }); it('should return 200 and not sync same billing account id for project without direct project id', (done) => { request(server) - .patch(`/v4/projects/${project3.id}`) - .set({ Authorization: `Bearer ${testUtil.jwts.admin}` }) - .send({ - param: { - billingAccountId: 1, - }, - }) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } + .patch(`/v4/projects/${project3.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ + param: { + billingAccountId: 1, + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { const resJson = res.body.result.content; should.exist(resJson); resJson.billingAccountId.should.equal(1); @@ -552,53 +668,60 @@ describe('Project', () => { resJson.updatedBy.should.equal(40051333); server.services.pubsub.publish.calledWith('project.updated').should.be.true; done(); - }); + } + }); }); it.skip('should return 200 and update bookmarks', (done) => { request(server) - .patch(`/v4/projects/${project1.id}`) - .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) - .send({ - param: { - bookmarks: [{ - title: 'title1', - address: 'address1', - }], - }, - }) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + bookmarks: [{ + title: 'title1', + address: 'address1', + }], + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { let resJson = res.body.result.content; should.exist(resJson); resJson.bookmarks.should.have.lengthOf(1); resJson.bookmarks[0].title.should.be.eql('title1'); resJson.bookmarks[0].address.should.be.eql('address1'); request(server) - .patch(`/v4/projects/${project1.id}`) - .set({ Authorization: `Bearer ${testUtil.jwts.copilot}` }) - .send({ - param: { - bookmarks: null, - }, - }) - .expect('Content-Type', /json/) - .expect(200) - .end((err, res) => { - if (err) { - return done(err); - } - resJson = res.body.result.content; - should.exist(resJson); - should.not.exist(resJson.bookmarks); - server.services.pubsub.publish.calledWith('project.updated').should.be.true; - done(); - }); - }); + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + bookmarks: null, + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((error, resp) => { + if (error) { + done(error); + } else { + resJson = resp.body.result.content; + should.exist(resJson); + should.not.exist(resJson.bookmarks); + server.services.pubsub.publish.calledWith('project.updated').should.be.true; + done(); + } + }); + } + }); }); }); }); diff --git a/src/services/fileService.js b/src/services/fileService.js index fdbc56cb..944f3e2f 100644 --- a/src/services/fileService.js +++ b/src/services/fileService.js @@ -1,17 +1,17 @@ - -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) { +function getHttpClient(req) { const httpClient = util.getHttpClient(req); httpClient.defaults.headers.common.Authorization = req.headers.authorization; httpClient.defaults.baseURL = config.get('fileServiceEndpoint'); @@ -31,8 +31,12 @@ 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}` } }); + getHttpClient(req).delete('', { params: { filter: `filePath%3D${filePath}` } }); }, }; diff --git a/src/services/index.js b/src/services/index.js index 9eaaabc6..fc0453e2 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -6,11 +6,17 @@ 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} fapp the app object + * @param {Object} logger the logger to use + * + * @return {Void} the function returns void */ -module.exports = (app, logger) => { +module.exports = (fapp, logger) => { + const app = fapp; app.services = app.service || {}; if (process.env.NODE_ENV.toLowerCase() === 'test') { - require('../tests/serviceMocks')(app); + require('../tests/serviceMocks')(app); // eslint-disable-line global-require } else { // RabbitMQ Initialization app.services.pubsub = new RabbitMQService(app, logger); diff --git a/src/tests/seed.js b/src/tests/seed.js index 54fb5100..a1f53c84 100644 --- a/src/tests/seed.js +++ b/src/tests/seed.js @@ -1,4 +1,5 @@ import models from '../models'; + models.sequelize.sync({ force: true }) .then(() => models.Project.bulkCreate([{ @@ -102,7 +103,6 @@ models.sequelize.sync({ force: true }) return Promise.all(operations); }) .then(() => { - console.log('Success'); process.exit(0); }) - .catch(err => console.log('Failed: ', err)); + .catch(() => process.exit(1)); 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..0b1a38b3 100644 --- a/src/util.js +++ b/src/util.js @@ -14,14 +14,19 @@ import _ from 'lodash'; import querystring from 'querystring'; import config from 'config'; +const exec = require('child_process').exec; +const models = require('./models').default; + 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 +42,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 +69,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,12 +91,12 @@ _.assignIn(util, { }, /** - * [description] - * @param {[type]} queryFilter [description] - * @return {[type]} [description] + * Parse the query filters + * @param {String} fqueryFilter the query filter string + * @return {Object} the parsed array */ - parseQueryFilter: (queryFilter) => { - queryFilter = querystring.parse(queryFilter); + parseQueryFilter: (fqueryFilter) => { + let queryFilter = querystring.parse(fqueryFilter); // convert in to array queryFilter = _.mapValues(queryFilter, (val) => { if (val.indexOf('in(') > -1) { @@ -118,8 +124,6 @@ _.assignIn(util, { `"${dest}"`, '--region us-east-1', ], ' '); - - const exec = require('child_process').exec; exec(cmdStr, (error, stdout, stderr) => { req.log.debug(`s3FileTransfer: stdout: ${stdout}`); req.log.debug(`s3FileTransfer: stderr: ${stderr}`); @@ -134,9 +138,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) { @@ -164,7 +168,6 @@ _.assignIn(util, { }); }, getProjectAttachments: (req, projectId) => { - const models = require('./models').default; let attachments = []; return models.ProjectAttachment.getActiveProjectAttachments(projectId) .then((_attachments) => { @@ -185,7 +188,8 @@ _.assignIn(util, { // result is an array of 'tuples' => [[path, url], [path,url]] // convert it to a map for easy lookup const urls = _.fromPairs(result); - _.each(attachments, (a) => { + _.each(attachments, (at) => { + const a = at; a.downloadUrl = urls[a.filePath]; }); return attachments; @@ -195,7 +199,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 +213,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 +227,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; From d934bff02d22c2c870e948b1b5316e759be3d2c6 Mon Sep 17 00:00:00 2001 From: Parth Shah Date: Mon, 6 Mar 2017 10:17:39 -0800 Subject: [PATCH 7/8] updating eslint and cleaning up package.json --- .eslintrc | 2 +- package.json | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.eslintrc b/.eslintrc index 1042ccec..b0115e3f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -9,7 +9,7 @@ }, "rules": { "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.test.js", "**/*.spec.js", "**/serviceMocks.js"]}], - max-len: ["error", { "ignoreComments": true, code: 120 }], + "max-len": ["error", { "ignoreComments": true, "code": 120 }], "valid-jsdoc": ["error", { "requireReturn": true, "requireReturnType": true, diff --git a/package.json b/package.json index 450f4b4a..d66d3594 100644 --- a/package.json +++ b/package.json @@ -7,17 +7,16 @@ "node": ">=6.9" }, "scripts": { - "build": "babel src -d dist --presets es2015", - "sync": "node migrations/sync.js", "lint": "./node_modules/.bin/eslint .", "lint:fix": "./node_modules/.bin/eslint . --fix || true", + "build": "babel src -d dist --presets es2015", + "sync": "node migrations/sync.js", "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", - "test": "NODE_ENV=test ./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- --compilers js:babel-core/register $(find src -path '*spec.js*')", + "test": "npm run lint && NODE_ENV=test ./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- --compilers js:babel-core/register $(find src -path '*spec.js*')", "test:watch": "NODE_ENV=test ./node_modules/.bin/mocha -w --compilers js:babel-core/register $(find src -path '*spec.js*')", - "seed": "babel-node src/tests/seed.js --presets es2015", - "direct": "babel-node src/mocks/direct.js --presets es2015" + "seed": "babel-node src/tests/seed.js --presets es2015" }, "repository": { "type": "git", From decd3c58ecc6348f9ccd5bb6ec212309d86e2d0f Mon Sep 17 00:00:00 2001 From: Parth Shah Date: Mon, 6 Mar 2017 10:24:25 -0800 Subject: [PATCH 8/8] Fixing path --- migrations/sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/sync.js b/migrations/sync.js index 023b2d06..2e5e861a 100644 --- a/migrations/sync.js +++ b/migrations/sync.js @@ -11,7 +11,7 @@ */ // process.env.NODE_ENV = 'development' -require('../dist/models').default.sequelize.sync({ force: true }) +require('../src/models').default.sequelize.sync({ force: true }) .then(() => { console.log('Database synced successfully'); process.exit();