diff --git a/.gitignore b/.gitignore index 0cf4a82a..edd85b28 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ # Created by https://www.gitignore.io/api/node config/local.js +# can be used locally to config some env variables and after apply them using `source .env` +.env ### Node ### # Logs logs diff --git a/README.md b/README.md index c487aab6..60abab9e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Microservice to manage CRUD operations for all things Projects. Copy config/sample.local.js as config/local.js, update the properties and according to your env setup #### Database -Once you start your PostgreSQL database through docker, it will create a projectsDB. +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* ``` NODE_ENV=development npm run sync:db @@ -40,6 +40,33 @@ Run `npm run sync:es` from the root of project to execute the script. **NOTE**: In production these dependencies / services are hosted & managed outside tc-projects-service. +#### Kafka +Kafka must be installed and configured prior starting the application. +Following topics must be created: +``` +notifications.connect.project.updated +notifications.connect.project.files.updated +notifications.connect.project.team.updated +notifications.connect.project.plan.updated +notifications.connect.project.topic.created +notifications.connect.project.topic.updated +notifications.connect.project.post.created +notifications.connect.project.post.edited +``` + +New Kafka related configuration options has been introduced: +``` +"kafkaConfig": { + "hosts": List of Kafka brokers. Default: localhost: 9092 + "clientCert": SSL certificate + "clientCertKey": Certificate key +} +``` +Environment variables: +KAFKA_HOSTS - same as "hosts" +KAFKA_CLIENT_CERT - same as "clientCert" +KAFKA_CLIENT_CERT_KEY - same as "clientCertKey" + ### Test Each of the individual modules/services are unit tested. diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index 82606743..41fc3eb3 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -29,6 +29,11 @@ "maxPoolSize": "DB_MAX_POOL_SIZE", "minPoolSize": "DB_MIN_POOL_SIZE" }, + "kafkaConfig": { + "hosts": "KAFKA_HOSTS", + "clientCert": "KAFKA_CLIENT_CERT", + "clientCertKey": "KAFKA_CLIENT_CERT_KEY" + }, "analyticsKey": "SEGMENT_ANALYTICS_KEY", "VALID_ISSUERS": "VALID_ISSUERS", "jwksUri": "JWKS_URI", diff --git a/config/default.json b/config/default.json index e2b2d11d..79440586 100644 --- a/config/default.json +++ b/config/default.json @@ -33,6 +33,8 @@ "minPoolSize": 4, "idleTimeout": 1000 }, + "kafkaConfig": { + }, "analyticsKey": "", "VALID_ISSUERS": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\"]", "validIssuers": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\"]", diff --git a/migrations/20180910_project_activity.sql b/migrations/20180910_project_activity.sql new file mode 100644 index 00000000..540d5491 --- /dev/null +++ b/migrations/20180910_project_activity.sql @@ -0,0 +1,18 @@ +-- +-- UPDATE EXISTING TABLES: +-- projects: +-- added column `lastActivityAt` +-- added column `lastActivityUserId` + +-- +-- projects + +-- Add new columns +ALTER TABLE projects ADD COLUMN "lastActivityAt" timestamp with time zone; +ALTER TABLE projects ADD COLUMN "lastActivityUserId" character varying(45); +-- Update new colums +UPDATE projects SET "lastActivityAt"="updatedAt" WHERE "lastActivityAt" is NULL; +UPDATE projects SET "lastActivityUserId"=cast("updatedBy" as varchar) WHERE "lastActivityUserId" is NULL; +-- Set not null +ALTER TABLE projects ALTER COLUMN "lastActivityAt" SET NOT NULL; +ALTER TABLE projects ALTER COLUMN "lastActivityUserId" SET NOT NULL; diff --git a/migrations/elasticsearch_sync.js b/migrations/elasticsearch_sync.js index eac45e5f..349be0b6 100644 --- a/migrations/elasticsearch_sync.js +++ b/migrations/elasticsearch_sync.js @@ -281,6 +281,13 @@ function getRequestBody(indexName) { updatedBy: { type: 'integer', }, + lastActivityAt: { + type: 'date', + format: 'strict_date_optional_time||epoch_millis', + }, + lastActivityUserId: { + type: 'string', + }, utm: { properties: { campaign: { diff --git a/package-lock.json b/package-lock.json index 72ec9d9c..228a50d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,23 +13,41 @@ "join-component": "1.1.0" } }, + "@types/bluebird": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.0.tgz", + "integrity": "sha1-JjNHCk6r6aR82aRf2yDtX5NAe8o=" + }, "@types/body-parser": { - "version": "1.16.8", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.16.8.tgz", - "integrity": "sha512-BdN2PXxOFnTXFcyONPW6t0fHjz2fvRZHVMFpaS0wYr+Y8fWEaNOs4V8LEu/fpzQlMx+ahdndgTaGTwPC+J/EeA==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", + "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", + "requires": { + "@types/connect": "3.4.32", + "@types/node": "10.9.4" + } + }, + "@types/connect": { + "version": "3.4.32", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", "requires": { - "@types/express": "4.0.39", - "@types/node": "8.5.1" + "@types/node": "10.9.4" } }, + "@types/events": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", + "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==" + }, "@types/express": { - "version": "4.0.39", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.0.39.tgz", - "integrity": "sha512-dBUam7jEjyuEofigUXCtublUHknRZvcRgITlGsTbFgPvnTwtQUt2NgLakbsf+PsGo/Nupqr3IXCYsOpBpofyrA==", + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.0.tgz", + "integrity": "sha512-TtPEYumsmSTtTetAPXlJVf3kEqb6wZK0bZojpJQrnD/djV4q1oB6QQ8aKvKqwNPACoe02GNiy5zDzcYivR5Z2w==", "requires": { - "@types/body-parser": "1.16.8", - "@types/express-serve-static-core": "4.0.57", - "@types/serve-static": "1.13.1" + "@types/body-parser": "1.17.0", + "@types/express-serve-static-core": "4.16.0", + "@types/serve-static": "1.13.2" } }, "@types/express-jwt": { @@ -37,16 +55,18 @@ "resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-0.0.34.tgz", "integrity": "sha1-/b7kxq9cCiRu8qkz9VGZc8dxfwI=", "requires": { - "@types/express": "4.0.39", + "@types/express": "4.16.0", "@types/express-unless": "0.0.32" } }, "@types/express-serve-static-core": { - "version": "4.0.57", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.0.57.tgz", - "integrity": "sha512-QLAHjdLwEICm3thVbXSKRoisjfgMVI4xJH/HU8F385BR2HI7PmM6ax4ELXf8Du6sLmSpySXMYaI+xc//oQ/IFw==", + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.0.tgz", + "integrity": "sha512-lTeoCu5NxJU4OD9moCgm0ESZzweAx0YqsAcab6OB0EB3+As1OaHtKnaGJvcngQxYsi9UNv0abn4/DRavrRxt4w==", "requires": { - "@types/node": "8.5.1" + "@types/events": "1.2.0", + "@types/node": "10.9.4", + "@types/range-parser": "1.2.2" } }, "@types/express-unless": { @@ -54,7 +74,7 @@ "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.0.32.tgz", "integrity": "sha512-6YpJyFNlDDnPnRjMOvJCoDYlSDDmG/OEEUsPk7yhNkL4G9hUYtgab6vi1CcWsGSSSM0CsvNlWTG+ywAGnvF03g==", "requires": { - "@types/express": "4.0.39" + "@types/express": "4.16.0" } }, "@types/geojson": { @@ -62,22 +82,32 @@ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz", "integrity": "sha512-Xqg/lIZMrUd0VRmSRbCAewtwGZiAk3mEUDvV4op1tGl+LvyPcb/MIOSxTl9z+9+J+R4/vpjiCAT4xeKzH9ji1w==" }, + "@types/lodash": { + "version": "4.14.116", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.116.tgz", + "integrity": "sha512-lRnAtKnxMXcYYXqOiotTmJd74uawNWuPnsnPrrO7HiFuE3npE2iQhfABatbYDyxTNqZNuXzcKGhw37R7RjBFLg==" + }, "@types/mime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz", "integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==" }, "@types/node": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.5.1.tgz", - "integrity": "sha512-SrmAO+NhnsuG/6TychSl2VdxBZiw/d6V+8j+DFo8O3PwFi+QeYXWHhAw+b170aSc6zYab6/PjEWRZHIDN9mNUw==" + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.9.4.tgz", + "integrity": "sha512-fCHV45gS+m3hH17zgkgADUSi2RR1Vht6wOZ0jyHP8rjiQra9f+mIcgwPQHllmDocYOstIEbKlxbFDYlgrTPYqw==" + }, + "@types/range-parser": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.2.tgz", + "integrity": "sha512-HtKGu+qG1NPvYe1z7ezLsyIaXYyi8SoAVqWDZgDQ8dLrsZvSzUNCwZyfX33uhWxL/SU0ZDQZ3nwZ0nimt507Kw==" }, "@types/serve-static": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.1.tgz", - "integrity": "sha512-jDMH+3BQPtvqZVIcsH700Dfi8Q3MIcEx16g/VdxjoqiGR/NntekB10xdBpirMKnPe9z2C5cBmL0vte0YttOr3Q==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", "requires": { - "@types/express-serve-static-core": "4.0.57", + "@types/express-serve-static-core": "4.16.0", "@types/mime": "2.0.0" } }, @@ -347,9 +377,12 @@ "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" }, "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "2.1.2" + } }, "assert-plus": { "version": "1.0.0", @@ -389,9 +422,9 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "auth0-js": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.6.0.tgz", - "integrity": "sha1-2a4wFIBzZtO0ecKtGKNTfz4Mlpk=", + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.7.3.tgz", + "integrity": "sha512-iZAqoN4EbsNCS/3VkFPNb4glTyj8hq57T7gcUF+XH8Rua7hBTUzpb101K9zqcdUIBilIdF9XBLCTJ4JGgZ/oFA==", "requires": { "base64-js": "1.2.1", "idtoken-verifier": "1.2.0", @@ -403,11 +436,11 @@ }, "dependencies": { "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", "requires": { - "ms": "2.0.0" + "ms": "2.1.1" } }, "formidable": { @@ -420,6 +453,11 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", @@ -427,7 +465,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "1.0.2", @@ -454,7 +492,7 @@ "requires": { "component-emitter": "1.2.1", "cookiejar": "2.1.1", - "debug": "3.1.0", + "debug": "3.2.5", "extend": "3.0.1", "form-data": "2.3.1", "formidable": "1.2.1", @@ -481,6 +519,13 @@ "uuid": "3.1.0", "xml2js": "0.4.17", "xmlbuilder": "4.2.1" + }, + "dependencies": { + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + } } }, "aws-sign2": { @@ -489,9 +534,9 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, "axios": { "version": "0.17.1", @@ -1553,7 +1598,7 @@ }, "babel-runtime": { "version": "6.6.1", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.6.1.tgz", + "resolved": "http://registry.npmjs.org/babel-runtime/-/babel-runtime-6.6.1.tgz", "integrity": "sha1-eIuUtvY04luRvWxd9y1GdFevsAA=", "requires": { "core-js": "2.5.1" @@ -1662,9 +1707,9 @@ "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==" }, "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", "optional": true, "requires": { "tweetnacl": "0.14.5" @@ -1676,6 +1721,16 @@ "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=", "dev": true }, + "bin-protocol": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/bin-protocol/-/bin-protocol-3.0.4.tgz", + "integrity": "sha1-RlqdNQb+sOEmtStbIWDZNuFbJ/Q=", + "requires": { + "lodash": "4.17.4", + "long": "3.2.0", + "protocol-buffers-schema": "3.3.2" + } + }, "binary-extensions": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.10.0.tgz", @@ -1717,14 +1772,6 @@ "type-is": "1.6.15" } }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "requires": { - "hoek": "4.2.0" - } - }, "boxen": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.2.2.tgz", @@ -1851,6 +1898,11 @@ } } }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -1943,6 +1995,15 @@ "type-detect": "1.0.0" } }, + "chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "requires": { + "check-error": "1.0.2" + } + }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", @@ -1955,6 +2016,12 @@ "supports-color": "2.0.0" } }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, "check-more-types": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.3.0.tgz", @@ -2201,6 +2268,11 @@ "xdg-basedir": "3.0.0" } }, + "connection-parse": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/connection-parse/-/connection-parse-0.0.7.tgz", + "integrity": "sha1-GOcxiqsGppkmc3KxDFIm0locmmk=" + }, "contains-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", @@ -2298,24 +2370,6 @@ } } }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "requires": { - "boom": "5.2.0" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "requires": { - "hoek": "4.2.0" - } - } - } - }, "crypto-browserify": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-1.0.9.tgz", @@ -2563,12 +2617,13 @@ "dev": true }, "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", "optional": true, "requires": { - "jsbn": "0.1.1" + "jsbn": "0.1.1", + "safer-buffer": "2.1.2" } }, "ecdsa-sig-formatter": { @@ -3113,7 +3168,7 @@ "resolved": "https://registry.npmjs.org/express-request-id/-/express-request-id-1.4.0.tgz", "integrity": "sha1-J3ssCUmAPmgQTJ1Fw+aJNPlr9aI=", "requires": { - "uuid": "3.1.0" + "uuid": "3.3.2" } }, "express-sanitizer": { @@ -3162,9 +3217,9 @@ } }, "fast-deep-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" }, "fast-json-stable-stringify": { "version": "2.0.0", @@ -3907,15 +3962,6 @@ "dev": true, "optional": true }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, "string-width": { "version": "1.0.2", "bundled": true, @@ -3926,6 +3972,15 @@ "strip-ansi": "3.0.1" } }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, "strip-ansi": { "version": "3.0.1", "bundled": true, @@ -4435,21 +4490,21 @@ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", + "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", "requires": { - "ajv": "5.5.1", + "ajv": "5.5.2", "har-schema": "2.0.0" }, "dependencies": { "ajv": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.1.tgz", - "integrity": "sha1-s4u4h22ehr7plJVqBOch6IskjrI=", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", "requires": { "co": "4.6.0", - "fast-deep-equal": "1.0.0", + "fast-deep-equal": "1.1.0", "fast-json-stable-stringify": "2.0.0", "json-schema-traverse": "0.3.1" } @@ -4488,15 +4543,13 @@ "sparkles": "1.0.0" } }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "hashring": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/hashring/-/hashring-3.2.0.tgz", + "integrity": "sha1-/aTv3oqiLNuX+x0qZeiEAeHBRM4=", "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.0", - "sntp": "2.1.0" + "connection-parse": "0.0.7", + "simple-lru-cache": "0.0.2" } }, "hoek": { @@ -4555,7 +4608,7 @@ "requires": { "assert-plus": "1.0.0", "jsprim": "1.4.1", - "sshpk": "1.13.1" + "sshpk": "1.14.2" } }, "iconv-lite": { @@ -4576,11 +4629,11 @@ }, "dependencies": { "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", "requires": { - "ms": "2.0.0" + "ms": "2.1.1" } }, "formidable": { @@ -4593,6 +4646,11 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", @@ -4600,7 +4658,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "1.0.2", @@ -4627,7 +4685,7 @@ "requires": { "component-emitter": "1.2.1", "cookiejar": "2.1.1", - "debug": "3.1.0", + "debug": "3.2.5", "extend": "3.0.1", "form-data": "2.3.1", "formidable": "1.2.1", @@ -5385,16 +5443,16 @@ } }, "jwks-rsa": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.2.1.tgz", - "integrity": "sha512-xg+fw7FOV4eGdDIEMqQJvPLmFv85h4uN+j/GKwJZAxlCrDQpM8ov1F709xKGEp/dG3l4TUxoSOeN6YK7+KpinQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.3.0.tgz", + "integrity": "sha512-9q+d5VffK/FvFAjuXoddrq7zQybFSINV4mcwJJExGKXGyjWWpTt3vsn/aX33aB0heY02LK0qSyicdtRK0gVTig==", "requires": { "@types/express-jwt": "0.0.34", "debug": "2.6.9", - "limiter": "1.1.2", - "lru-memoizer": "1.11.1", + "limiter": "1.1.3", + "lru-memoizer": "1.12.0", "ms": "2.0.0", - "request": "2.83.0" + "request": "2.88.0" } }, "jws": { @@ -5461,7 +5519,7 @@ "dependencies": { "lodash": { "version": "3.9.3", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.9.3.tgz", + "resolved": "http://registry.npmjs.org/lodash/-/lodash-3.9.3.tgz", "integrity": "sha1-AVnoaDL+/8bWHYUrEqlTuZSWvTI=" }, "semver": { @@ -5482,12 +5540,19 @@ } }, "libpq": { - "version": "1.8.7", - "resolved": "https://registry.npmjs.org/libpq/-/libpq-1.8.7.tgz", - "integrity": "sha1-wt6xIeKPf4S9OyRRr/9otmY+dPk=", + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/libpq/-/libpq-1.8.8.tgz", + "integrity": "sha512-0TVzqkbAZZiM8JJy5sagRyXOkvU9zTBlgGX6YdzuWECobc5F81Tp6uuS+djMZrnB5YN4O/ff52hsvXYBRW2gdQ==", "requires": { "bindings": "1.2.1", - "nan": "2.7.0" + "nan": "2.11.0" + }, + "dependencies": { + "nan": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz", + "integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw==" + } } }, "liftoff": { @@ -5522,9 +5587,9 @@ } }, "limiter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.2.tgz", - "integrity": "sha512-JIKZ0xb6fZZYa3deZ0BgXCgX6HgV8Nx3mFGeFHmFWW8Fb2c08e0CyE+G3nalpD0xGvGssjGb1UdFr+PprxZEbw==" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.3.tgz", + "integrity": "sha512-zrycnIMsLw/3ZxTbW7HCez56rcFGecWTx5OZNplzcXUUmJLmoYArC6qdJzmAN5BWiNXGcpjhF9RQ1HSv5zebEw==" }, "load-json-file": { "version": "2.0.0", @@ -5791,6 +5856,11 @@ "integrity": "sha1-fD2mL/yzDw9agKJWbKJORdigHzE=", "dev": true }, + "long": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", + "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=" + }, "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -5819,21 +5889,16 @@ "dev": true }, "lru-memoizer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-1.11.1.tgz", - "integrity": "sha1-BpP2EAWTkUwC4ZK/m42TiEy/UNM=", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-1.12.0.tgz", + "integrity": "sha1-7+ZXBsyKnMZT+A8NWm6jitlQ41I=", "requires": { "lock": "0.1.4", - "lodash": "4.5.1", + "lodash": "4.17.4", "lru-cache": "4.0.2", "very-fast-args": "1.1.0" }, "dependencies": { - "lodash": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.5.1.tgz", - "integrity": "sha1-gOigdMpfOJOmscELKmNkktcQwxY=" - }, "lru-cache": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", @@ -6114,6 +6179,11 @@ "duplexer2": "0.0.2" } }, + "murmur-hash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmur-hash-js/-/murmur-hash-js-1.0.0.tgz", + "integrity": "sha1-UEEEkmnJZjPIZjhpYLL0KJ515bA=" + }, "mute-stream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", @@ -6165,6 +6235,38 @@ "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, + "nice-simple-logger": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/nice-simple-logger/-/nice-simple-logger-1.0.1.tgz", + "integrity": "sha1-D55khSe+e+PkmrdvqMjAmK+VG/Y=", + "requires": { + "lodash": "4.17.4" + } + }, + "no-kafka": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/no-kafka/-/no-kafka-3.2.10.tgz", + "integrity": "sha1-0sq8QwZbSS24wVyiOK6V8WgIGvU=", + "requires": { + "@types/bluebird": "3.5.0", + "@types/lodash": "4.14.116", + "bin-protocol": "3.0.4", + "bluebird": "3.5.1", + "buffer-crc32": "0.2.13", + "hashring": "3.2.0", + "lodash": "4.17.5", + "murmur-hash-js": "1.0.0", + "nice-simple-logger": "1.0.1", + "wrr-pool": "1.1.3" + }, + "dependencies": { + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" + } + } + }, "nodemon": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.12.1.tgz", @@ -6229,9 +6331,9 @@ "dev": true }, "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, "object-assign": { "version": "4.1.1", @@ -6586,7 +6688,7 @@ "resolved": "https://registry.npmjs.org/pg-native/-/pg-native-1.10.1.tgz", "integrity": "sha1-lOYcy7hafzQ2suUmMVx1gRB/5Aw=", "requires": { - "libpq": "1.8.7", + "libpq": "1.8.8", "pg-types": "1.6.0", "readable-stream": "1.0.31" }, @@ -6747,6 +6849,11 @@ "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", "dev": true }, + "protocol-buffers-schema": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.3.2.tgz", + "integrity": "sha512-Xdayp8sB/mU+sUV4G7ws8xtYMGdQnxbeIfLjyO9TZZRJdztBGhlmbI5x1qcY4TG5hBkIKGnc28i7nXxaugu88w==" + }, "proxy-addr": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", @@ -6770,6 +6877,11 @@ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, + "psl": { + "version": "1.1.29", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" + }, "punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", @@ -7129,32 +7241,78 @@ "dev": true }, "request": { - "version": "2.83.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", "requires": { "aws-sign2": "0.7.0", - "aws4": "1.6.0", + "aws4": "1.8.0", "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", + "combined-stream": "1.0.6", + "extend": "3.0.2", "forever-agent": "0.6.1", - "form-data": "2.3.1", - "har-validator": "5.0.3", - "hawk": "6.0.2", + "form-data": "2.3.2", + "har-validator": "5.1.0", "http-signature": "1.2.0", "is-typedarray": "1.0.0", "isstream": "0.1.2", "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", + "mime-types": "2.1.20", + "oauth-sign": "0.9.0", "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", + "qs": "6.5.2", + "safe-buffer": "5.1.2", + "tough-cookie": "2.4.3", "tunnel-agent": "0.6.0", - "uuid": "3.1.0" + "uuid": "3.3.2" + }, + "dependencies": { + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "1.0.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.20" + } + }, + "mime-db": { + "version": "1.36.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", + "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" + }, + "mime-types": { + "version": "2.1.20", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", + "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", + "requires": { + "mime-db": "1.36.0" + } + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } } }, "require-directory": { @@ -7267,6 +7425,11 @@ "integrity": "sha1-gaCY9Efku8P/MxKiQ1IbwGDvWRE=", "optional": true }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "samsam": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz", @@ -7342,7 +7505,7 @@ "shimmer": "1.1.0", "terraformer-wkt-parser": "1.1.2", "toposort-class": "1.0.1", - "uuid": "3.1.0", + "uuid": "3.3.2", "validator": "5.7.0", "wkx": "0.2.0" }, @@ -7553,6 +7716,11 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, + "simple-lru-cache": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/simple-lru-cache/-/simple-lru-cache-0.0.2.tgz", + "integrity": "sha1-1ZzDoZPBpdAyD4Tucy9uRxPlEd0=" + }, "sinon": { "version": "1.17.7", "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.7.tgz", @@ -7592,14 +7760,6 @@ "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", "dev": true }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", - "requires": { - "hoek": "4.2.0" - } - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -7657,17 +7817,18 @@ "dev": true }, "sshpk": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", - "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", "requires": { - "asn1": "0.2.3", + "asn1": "0.2.4", "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", + "bcrypt-pbkdf": "1.0.2", "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", + "ecc-jsbn": "0.1.2", "getpass": "0.1.7", "jsbn": "0.1.1", + "safer-buffer": "2.1.2", "tweetnacl": "0.14.5" } }, @@ -7690,11 +7851,6 @@ "resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.0.tgz", "integrity": "sha1-pB6tGm1ggc63n2WwYZAbbY89HQ8=" }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -7706,10 +7862,10 @@ "strip-ansi": "3.0.1" } }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, "strip-ansi": { "version": "3.0.1", @@ -7922,12 +8078,12 @@ "tc-core-library-js": { "version": "github:appirio-tech/tc-core-library-js#df1f5c1a5578d3d1e475bfb4a7413d9dec25525a", "requires": { - "auth0-js": "9.6.0", + "auth0-js": "9.7.3", "axios": "0.12.0", "bunyan": "1.8.12", "config": "1.27.0", "jsonwebtoken": "7.4.3", - "jwks-rsa": "1.2.1", + "jwks-rsa": "1.3.0", "le_node": "1.7.1", "lodash": "4.17.4", "millisecond": "0.1.2" @@ -7935,7 +8091,7 @@ "dependencies": { "axios": { "version": "0.12.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.12.0.tgz", + "resolved": "http://registry.npmjs.org/axios/-/axios-0.12.0.tgz", "integrity": "sha1-uQewIhzDTsHJ+sGOx/B935V4W6Q=", "requires": { "follow-redirects": "0.0.7" @@ -7943,7 +8099,7 @@ }, "follow-redirects": { "version": "0.0.7", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.7.tgz", + "resolved": "http://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.7.tgz", "integrity": "sha1-NLkLqyqRGqNHVx2pDyK9NuzYqRk=", "requires": { "debug": "2.6.9", @@ -7962,7 +8118,7 @@ }, "joi": { "version": "6.10.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-6.10.1.tgz", + "resolved": "http://registry.npmjs.org/joi/-/joi-6.10.1.tgz", "integrity": "sha1-TVDDGAeRIgAP5fFq8f+OGRe3fgY=", "requires": { "hoek": "2.16.3", @@ -8148,10 +8304,11 @@ } }, "tough-cookie": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", - "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "requires": { + "psl": "1.1.29", "punycode": "1.4.1" }, "dependencies": { @@ -8418,9 +8575,9 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" }, "v8flags": { "version": "2.1.1", @@ -8642,6 +8799,14 @@ "signal-exit": "3.0.2" } }, + "wrr-pool": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wrr-pool/-/wrr-pool-1.1.3.tgz", + "integrity": "sha1-/a0i8uofMDY//l14HPeUl6d/8H4=", + "requires": { + "lodash": "4.17.4" + } + }, "xdg-basedir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", diff --git a/package.json b/package.json index eab997b8..008d7ad1 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "prestart": "npm run -s build", "start": "node dist", "start:dev": "NODE_ENV=development PORT=8001 nodemon -w src --exec \"babel-node src --presets es2015\" | ./node_modules/.bin/bunyan", - "test": "NODE_ENV=test npm run lint && NODE_ENV=test npm run sync:es && NODE_ENV=test npm run sync:db && NODE_ENV=test ./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- --timeout 5000 --compilers js:babel-core/register $(find src -path '*spec.js*')", + "test": "NODE_ENV=test npm run lint && NODE_ENV=test npm run sync:es && NODE_ENV=test npm run sync:db && NODE_ENV=test ./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- --timeout 10000 --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" }, @@ -55,6 +55,7 @@ "memwatch-next": "^0.3.0", "method-override": "^2.3.9", "moment": "^2.22.2", + "no-kafka": "^3.2.10", "pg": "^4.5.5", "pg-native": "^1.10.1", "sequelize": "^3.23.0", @@ -71,6 +72,7 @@ "babel-preset-es2015": "^6.9.0", "bunyan": "^1.8.1", "chai": "^3.5.0", + "chai-as-promised": "^7.1.1", "eslint": "^3.16.1", "eslint-config-airbnb-base": "^11.1.0", "eslint-plugin-import": "^2.2.0", diff --git a/src/constants.js b/src/constants.js index 99ca4e5d..c41703e8 100644 --- a/src/constants.js +++ b/src/constants.js @@ -64,6 +64,7 @@ export const EVENT = { export const BUS_API_EVENT = { PROJECT_CREATED: 'notifications.connect.project.created', + PROJECT_UPDATED: 'notifications.connect.project.updated', PROJECT_SUBMITTED_FOR_REVIEW: 'notifications.connect.project.submittedForReview', PROJECT_APPROVED: 'notifications.connect.project.approved', PROJECT_PAUSED: 'notifications.connect.project.paused', @@ -88,11 +89,13 @@ export const BUS_API_EVENT = { PROJECT_FILE_UPLOADED: 'notifications.connect.project.fileUploaded', PROJECT_SPECIFICATION_MODIFIED: 'notifications.connect.project.specificationModified', PROJECT_PROGRESS_MODIFIED: 'notifications.connect.project.progressModified', + PROJECT_FILES_UPDATED: 'notifications.connect.project.files.updated', + PROJECT_TEAM_UPDATED: 'notifications.connect.project.team.updated', // When phase is added/updated/deleted from the project, // When product is added/deleted from a phase // When product is updated on any field other than specification - PROJECT_PLAN_MODIFIED: 'notifications.connect.project.planModified', + PROJECT_PLAN_UPDATED: 'notifications.connect.project.plan.updated', PROJECT_PLAN_READY: 'notifications.connect.project.planReady', @@ -109,6 +112,12 @@ export const BUS_API_EVENT = { MILESTONE_TRANSITION_COMPLETED: 'notifications.connect.project.phase.milestone.transition.completed', // When milestone is waiting for customers's input MILESTONE_WAITING_CUSTOMER: 'notifications.connect.project.phase.milestone.waiting.customer', + + // TC Message Service events + TOPIC_CREATED: 'notifications.connect.project.topic.created', + TOPIC_UPDATED: 'notifications.connect.project.topic.updated', + POST_CREATED: 'notifications.connect.project.post.created', + POST_UPDATED: 'notifications.connect.project.post.edited', }; export const REGEX = { diff --git a/src/events/busApi.js b/src/events/busApi.js index c2756b5b..ac471a0e 100644 --- a/src/events/busApi.js +++ b/src/events/busApi.js @@ -93,6 +93,19 @@ module.exports = (app, logger) => { initiatorUserId: req.authUser.userId, }, logger); } + + // send PROJECT_UPDATED Kafka message when one of the specified below properties changed + const watchProperties = ['status', 'details', 'name', 'description', 'bookmarks']; + if (!_.isEqual(_.pick(original, watchProperties), + _.pick(updated, watchProperties))) { + createEvent(BUS_API_EVENT.PROJECT_UPDATED, { + projectId: updated.id, + projectName: updated.name, + projectUrl: connectProjectUrl(updated.id), + userId: req.authUser.userId, + initiatorUserId: req.authUser.userId, + }, logger); + } }); /** @@ -126,6 +139,14 @@ module.exports = (app, logger) => { userId: member.userId, initiatorUserId: req.authUser.userId, }, logger); + + createEvent(BUS_API_EVENT.PROJECT_TEAM_UPDATED, { + projectId: project.id, + projectName: project.name, + projectUrl: connectProjectUrl(project.id), + userId: req.authUser.userId, + initiatorUserId: req.authUser.userId, + }, logger); }).catch(err => null); // eslint-disable-line no-unused-vars }); @@ -155,6 +176,14 @@ module.exports = (app, logger) => { userId: member.userId, initiatorUserId: req.authUser.userId, }, logger); + + createEvent(BUS_API_EVENT.PROJECT_TEAM_UPDATED, { + projectId: project.id, + projectName: project.name, + projectUrl: connectProjectUrl(project.id), + userId: req.authUser.userId, + initiatorUserId: req.authUser.userId, + }, logger); } }).catch(err => null); // eslint-disable-line no-unused-vars }); @@ -166,12 +195,13 @@ module.exports = (app, logger) => { logger.debug('receive PROJECT_MEMBER_UPDATED event'); const projectId = _.parseInt(req.params.projectId); - if (updated.isPrimary && !original.isPrimary) { - models.Project.findOne({ - where: { id: projectId }, - }) - .then((project) => { - if (project) { + + models.Project.findOne({ + where: { id: projectId }, + }) + .then((project) => { + if (project) { + if (updated.isPrimary && !original.isPrimary) { createEvent(BUS_API_EVENT.MEMBER_ASSIGNED_AS_OWNER, { projectId, projectName: project.name, @@ -180,8 +210,16 @@ module.exports = (app, logger) => { initiatorUserId: req.authUser.userId, }, logger); } - }).catch(err => null); // eslint-disable-line no-unused-vars - } + + createEvent(BUS_API_EVENT.PROJECT_TEAM_UPDATED, { + projectId: project.id, + projectName: project.name, + projectUrl: connectProjectUrl(project.id), + userId: req.authUser.userId, + initiatorUserId: req.authUser.userId, + }, logger); + } + }).catch(err => null); // eslint-disable-line no-unused-vars }); /** @@ -205,9 +243,62 @@ module.exports = (app, logger) => { userId: req.authUser.userId, initiatorUserId: req.authUser.userId, }, logger); + + createEvent(BUS_API_EVENT.PROJECT_FILES_UPDATED, { + projectId: project.id, + projectName: project.name, + projectUrl: connectProjectUrl(project.id), + userId: req.authUser.userId, + initiatorUserId: req.authUser.userId, + }, logger); }).catch(err => null); // eslint-disable-line no-unused-vars }); + + /** + * PROJECT_ATTACHMENT_UPDATED + */ + app.on(EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_UPDATED, ({ req }) => { + logger.debug('receive PROJECT_ATTACHMENT_UPDATED event'); + + const projectId = _.parseInt(req.params.projectId); + + models.Project.findOne({ + where: { id: projectId }, + }) + .then((project) => { + createEvent(BUS_API_EVENT.PROJECT_FILES_UPDATED, { + projectId: project.id, + projectName: project.name, + projectUrl: connectProjectUrl(project.id), + userId: req.authUser.userId, + initiatorUserId: req.authUser.userId, + }, logger); + }).catch(err => null); // eslint-disable-line no-unused-vars + }); + + /** + * PROJECT_ATTACHMENT_REMOVED + */ + app.on(EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_REMOVED, ({ req }) => { + logger.debug('receive PROJECT_ATTACHMENT_REMOVED event'); + + const projectId = _.parseInt(req.params.projectId); + + models.Project.findOne({ + where: { id: projectId }, + }) + .then((project) => { + createEvent(BUS_API_EVENT.PROJECT_FILES_UPDATED, { + projectId: project.id, + projectName: project.name, + projectUrl: connectProjectUrl(project.id), + userId: req.authUser.userId, + initiatorUserId: req.authUser.userId, + }, logger); + }).catch(err => null); // eslint-disable-line no-unused-vars + }); + /** * If the project is in draft status and the phase is in reviewed status, and it's the * only phase in the project with that status, then send the plan ready event. @@ -249,14 +340,13 @@ module.exports = (app, logger) => { where: { id: projectId }, }) .then((project) => { - createEvent(BUS_API_EVENT.PROJECT_PLAN_MODIFIED, { + createEvent(BUS_API_EVENT.PROJECT_PLAN_UPDATED, { projectId, projectName: project.name, projectUrl: connectProjectUrl(projectId), userId: req.authUser.userId, initiatorUserId: req.authUser.userId, }, logger); - return sendPlanReadyEventIfNeeded(req, project, created); }).catch(err => null); // eslint-disable-line no-unused-vars }); @@ -273,7 +363,7 @@ module.exports = (app, logger) => { where: { id: projectId }, }) .then((project) => { - createEvent(BUS_API_EVENT.PROJECT_PLAN_MODIFIED, { + createEvent(BUS_API_EVENT.PROJECT_PLAN_UPDATED, { projectId, projectName: project.name, projectUrl: connectProjectUrl(projectId), @@ -296,13 +386,18 @@ module.exports = (app, logger) => { where: { id: projectId }, }) .then((project) => { - createEvent(BUS_API_EVENT.PROJECT_PLAN_MODIFIED, { - projectId, - projectName: project.name, - projectUrl: connectProjectUrl(projectId), - userId: req.authUser.userId, - initiatorUserId: req.authUser.userId, - }, logger); + // send PROJECT_PLAN_UPDATED Kafka message when one of the specified below properties changed + const watchProperties = ['spentBudget', 'progress', 'details', 'status', 'budget', 'startDate', 'duration']; + if (!_.isEqual(_.pick(original, watchProperties), + _.pick(updated, watchProperties))) { + createEvent(BUS_API_EVENT.PROJECT_PLAN_UPDATED, { + projectId, + projectName: project.name, + projectUrl: connectProjectUrl(projectId), + userId: req.authUser.userId, + initiatorUserId: req.authUser.userId, + }, logger); + } [ ['spentBudget', BUS_API_EVENT.PROJECT_PHASE_UPDATE_PAYMENT], @@ -333,51 +428,6 @@ module.exports = (app, logger) => { }).catch(err => null); // eslint-disable-line no-unused-vars }); - /** - * PROJECT_PHASE_PRODUCT_ADDED - */ - app.on(EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_ADDED, ({ req, created }) => { // eslint-disable-line no-unused-vars - logger.debug('receive PROJECT_PHASE_PRODUCT_ADDED event'); - - const projectId = _.parseInt(req.params.projectId); - - models.Project.findOne({ - where: { id: projectId }, - }) - .then((project) => { - createEvent(BUS_API_EVENT.PROJECT_PLAN_MODIFIED, { - projectId, - projectName: project.name, - projectUrl: connectProjectUrl(projectId), - userId: req.authUser.userId, - initiatorUserId: req.authUser.userId, - phase: created, - }, logger); - }).catch(err => null); // eslint-disable-line no-unused-vars - }); - - /** - * PROJECT_PHASE_PRODUCT_REMOVED - */ - app.on(EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_REMOVED, ({ req, deleted }) => { // eslint-disable-line no-unused-vars - logger.debug('receive PROJECT_PHASE_PRODUCT_REMOVED event'); - - const projectId = _.parseInt(req.params.projectId); - - models.Project.findOne({ - where: { id: projectId }, - }) - .then((project) => { - createEvent(BUS_API_EVENT.PROJECT_PLAN_MODIFIED, { - projectId, - projectName: project.name, - projectUrl: connectProjectUrl(projectId), - userId: req.authUser.userId, - initiatorUserId: req.authUser.userId, - }, logger); - }).catch(err => null); // eslint-disable-line no-unused-vars - }); - /** * PROJECT_PHASE_PRODUCT_UPDATED */ @@ -403,11 +453,10 @@ module.exports = (app, logger) => { }, logger); } - // Other fields change - const originalWithouDetails = _.omit(original, 'details'); - const updatedWithouDetails = _.omit(updated, 'details'); - if (!_.isEqual(originalWithouDetails, updatedWithouDetails)) { - createEvent(BUS_API_EVENT.PROJECT_PLAN_MODIFIED, { + const watchProperties = ['name', 'estimatedPrice', 'actualPrice', 'details']; + if (!_.isEqual(_.pick(original, watchProperties), + _.pick(updated, watchProperties))) { + createEvent(BUS_API_EVENT.PROJECT_PLAN_UPDATED, { projectId, projectName: project.name, projectUrl: connectProjectUrl(projectId), @@ -481,7 +530,18 @@ module.exports = (app, logger) => { models.Project.findOne({ where: { id: projectId }, }) - .then(project => sendMilestoneNotification(req, {}, created, project)) + .then((project) => { + if (project) { + createEvent(BUS_API_EVENT.PROJECT_PLAN_UPDATED, { + projectId, + projectName: project.name, + projectUrl: connectProjectUrl(projectId), + userId: req.authUser.userId, + initiatorUserId: req.authUser.userId, + }, logger); + } + sendMilestoneNotification(req, {}, created, project); + }) .catch(err => null); // eslint-disable-line no-unused-vars }); @@ -498,6 +558,18 @@ module.exports = (app, logger) => { where: { id: projectId }, }) .then((project) => { + // send PROJECT_UPDATED Kafka message when one of the specified below properties changed + const watchProperties = ['startDate', 'endDate', 'duration', 'details', 'status', 'order']; + if (!_.isEqual(_.pick(original, watchProperties), + _.pick(updated, watchProperties))) { + createEvent(BUS_API_EVENT.PROJECT_PLAN_UPDATED, { + projectId, + projectName: project.name, + projectUrl: connectProjectUrl(projectId), + userId: req.authUser.userId, + initiatorUserId: req.authUser.userId, + }, logger); + } sendMilestoneNotification(req, original, updated, project); logger.debug('cascadedUpdates', cascadedUpdates); @@ -527,4 +599,54 @@ module.exports = (app, logger) => { }) .catch(err => null); // eslint-disable-line no-unused-vars }); + + /** + * MILESTONE_REMOVED. + */ + app.on(EVENT.ROUTING_KEY.MILESTONE_REMOVED, ({ req }) => { + logger.debug('receive MILESTONE_REMOVED event'); + // req.params.projectId is set by validateTimelineIdParam middleware + const projectId = _.parseInt(req.params.projectId); + + models.Project.findOne({ + where: { id: projectId }, + }) + .then((project) => { + if (project) { + createEvent(BUS_API_EVENT.PROJECT_PLAN_UPDATED, { + projectId, + projectName: project.name, + projectUrl: connectProjectUrl(projectId), + userId: req.authUser.userId, + initiatorUserId: req.authUser.userId, + }, logger); + } + }).catch(err => null); // eslint-disable-line no-unused-vars + }); + + app.on(EVENT.ROUTING_KEY.TIMELINE_UPDATED, ({ req, original, updated }) => { + logger.debug('receive TIMELINE_UPDATED event'); + // send PROJECT_UPDATED Kafka message when one of the specified below properties changed + const watchProperties = ['startDate', 'endDate']; + if (!_.isEqual(_.pick(original, watchProperties), + _.pick(updated, watchProperties))) { + // req.params.projectId is set by validateTimelineIdParam middleware + const projectId = _.parseInt(req.params.projectId); + + models.Project.findOne({ + where: { id: projectId }, + }) + .then((project) => { + if (project) { + createEvent(BUS_API_EVENT.PROJECT_PLAN_UPDATED, { + projectId, + projectName: project.name, + projectUrl: connectProjectUrl(projectId), + userId: req.authUser.userId, + initiatorUserId: req.authUser.userId, + }, logger); + } + }).catch(err => null); // eslint-disable-line no-unused-vars + } + }); }; diff --git a/src/events/index.js b/src/events/index.js index fac17d8d..38a1ed00 100644 --- a/src/events/index.js +++ b/src/events/index.js @@ -1,6 +1,7 @@ -import { EVENT } from '../constants'; -import { projectCreatedHandler, projectUpdatedHandler, projectDeletedHandler } from './projects'; +import { EVENT, BUS_API_EVENT } from '../constants'; +import { projectCreatedHandler, projectUpdatedHandler, projectDeletedHandler, + projectUpdatedKafkaHandler } from './projects'; import { projectMemberAddedHandler, projectMemberRemovedHandler, projectMemberUpdatedHandler } from './projectMembers'; import { projectAttachmentAddedHandler, projectAttachmentRemovedHandler, @@ -12,7 +13,7 @@ import { phaseProductAddedHandler, phaseProductRemovedHandler, import { timelineAddedHandler, timelineUpdatedHandler, timelineRemovedHandler } from './timelines'; import { milestoneAddedHandler, milestoneUpdatedHandler, milestoneRemovedHandler } from './milestones'; -export default { +export const rabbitHandlers = { 'project.initial': projectCreatedHandler, [EVENT.ROUTING_KEY.PROJECT_DRAFT_CREATED]: projectCreatedHandler, [EVENT.ROUTING_KEY.PROJECT_UPDATED]: projectUpdatedHandler, @@ -42,3 +43,17 @@ export default { [EVENT.ROUTING_KEY.MILESTONE_REMOVED]: milestoneRemovedHandler, [EVENT.ROUTING_KEY.MILESTONE_UPDATED]: milestoneUpdatedHandler, }; + +export const kafkaHandlers = { + // Events defined by project-service + [BUS_API_EVENT.PROJECT_UPDATED]: projectUpdatedKafkaHandler, + [BUS_API_EVENT.PROJECT_FILES_UPDATED]: projectUpdatedKafkaHandler, + [BUS_API_EVENT.PROJECT_TEAM_UPDATED]: projectUpdatedKafkaHandler, + [BUS_API_EVENT.PROJECT_PLAN_UPDATED]: projectUpdatedKafkaHandler, + + // Events from message-service + [BUS_API_EVENT.TOPIC_CREATED]: projectUpdatedKafkaHandler, + [BUS_API_EVENT.TOPIC_UPDATED]: projectUpdatedKafkaHandler, + [BUS_API_EVENT.POST_CREATED]: projectUpdatedKafkaHandler, + [BUS_API_EVENT.POST_UPDATED]: projectUpdatedKafkaHandler, +}; diff --git a/src/events/projects/index.js b/src/events/projects/index.js index 64a7690a..505d7ddb 100644 --- a/src/events/projects/index.js +++ b/src/events/projects/index.js @@ -2,10 +2,13 @@ * Event handlers for project create, update and delete */ import _ from 'lodash'; +import Joi from 'joi'; import Promise from 'bluebird'; import config from 'config'; import util from '../../util'; +import models from '../../models'; import { createPhaseTopic } from '../projectPhases'; +import { REGEX } from '../../constants'; const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); @@ -128,8 +131,66 @@ const projectDeletedHandler = Promise.coroutine(function* (logger, msg, channel) } }); +/** + * Kafka event handlers + */ + +const payloadSchema = Joi.object().keys({ + projectId: Joi.number().integer().positive().required(), + projectName: Joi.string().required(), + projectUrl: Joi.string().regex(REGEX.URL).required(), + userId: Joi.number().integer().positive().required(), + initiatorUserId: Joi.number().integer().positive().required(), +}).required(); + +/** + * Updates project activity fields. throws exceptions in case of error + * @param {Object} app Application object used to interact with RMQ service + * @param {String} topic Kafka topic + * @param {Object} payload Message payload + * @return {Promise} Promise + */ +async function projectUpdatedKafkaHandler(app, topic, payload) { + // Validate payload + const result = Joi.validate(payload, payloadSchema); + if (result.error) { + throw new Error(result.error); + } + + // Find project by id and update activity. Single update is used as there is no need to wrap it into transaction + const projectId = payload.projectId; + const project = await models.Project.findById(projectId); + if (!project) { + throw new Error(`Project with id ${projectId} not found`); + } + const previousValue = project.get({ plain: true }); + project.lastActivityAt = new Date(); + project.lastActivityUserId = payload.initiatorUserId.toString(); + + await project.save(); + + // first get the existing document and than merge the updated changes and save the new document + try { + const doc = await eClient.get({ index: ES_PROJECT_INDEX, type: ES_PROJECT_TYPE, id: previousValue.id }); + const merged = _.merge(doc._source, project.get({ plain: true })); // eslint-disable-line no-underscore-dangle + // update the merged document + await eClient.update({ + index: ES_PROJECT_INDEX, + type: ES_PROJECT_TYPE, + id: previousValue.id, + body: { + doc: merged, + }, + }); + } catch (error) { + throw Error(`failed to updated project document in elasitcsearch index (projectId: ${previousValue.id})` + + `. Details: '${error}'.`); + } +} + module.exports = { projectCreatedHandler, projectUpdatedHandler, projectDeletedHandler, + projectUpdatedKafkaHandler, }; diff --git a/src/events/projects/index.spec.js b/src/events/projects/index.spec.js new file mode 100644 index 00000000..9067166e --- /dev/null +++ b/src/events/projects/index.spec.js @@ -0,0 +1,148 @@ +/* eslint-disable no-unused-expressions */ +import _ from 'lodash'; +import sinon from 'sinon'; +import chai, { expect } from 'chai'; +import config from 'config'; +import util from '../../util'; +import models from '../../models'; +import { projectUpdatedKafkaHandler } from './index'; +import testUtil from '../../tests/util'; +import server from '../../app'; + +const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName'); +const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType'); +const eClient = util.getElasticSearchClient(); + +chai.use(require('chai-as-promised')); + +describe('projectUpdatedKafkaHandler', () => { + // Any topic name is fine here as routing happens in kafkaConsumer + const topic = 'topic'; + + const validPayload = { + projectId: 1, + projectName: 'test project', + projectUrl: 'http://someurl.com', + userId: 1, + initiatorUserId: 2, + }; + + const mockedApp = { + services: { + pubsub: { + publish: sinon.stub(), + }, + }, + }; + + it('should throw validation exception when payload is empty', async () => { + await expect(projectUpdatedKafkaHandler(mockedApp, topic, {})).to.be.rejectedWith(Error); + }); + + it('should throw validation exception when projectId is not set', async () => { + const payload = _.omit(validPayload, 'projectId'); + await expect(projectUpdatedKafkaHandler(mockedApp, topic, payload)).to.be.rejectedWith(Error); + }); + + it('should throw validation exception when projectName is not set', async () => { + const payload = _.omit(validPayload, 'projectName'); + await expect(projectUpdatedKafkaHandler(mockedApp, mockedApp, topic, payload)) + .to.be.rejectedWith(Error); + }); + + it('should throw validation exception when projectUrl is not set', async () => { + const payload = _.omit(validPayload, 'projectUrl'); + await expect(projectUpdatedKafkaHandler(mockedApp, topic, payload)).to.be.rejectedWith(Error); + }); + + it('should throw validation exception when userId is not set', async () => { + const payload = _.omit(validPayload, 'userId'); + await expect(projectUpdatedKafkaHandler(mockedApp, topic, payload)).to.be.rejectedWith(Error); + }); + + it('should throw validation exception when initiatorUserId is not set', async () => { + const payload = _.omit(validPayload, 'initiatorUserId'); + await expect(projectUpdatedKafkaHandler(mockedApp, topic, payload)).to.be.rejectedWith(Error); + }); + + it('should throw validation exception when projectId is not integer', async () => { + const payload = _.clone(validPayload); + payload.projectId = 'string'; + await expect(projectUpdatedKafkaHandler(mockedApp, topic, payload)).to.be.rejectedWith(Error); + }); + + it('should throw validation exception when projectUrl is not a valid url', async () => { + const payload = _.clone(validPayload); + payload.projectUrl = 'string'; + await expect(projectUpdatedKafkaHandler(mockedApp, topic, payload)).to.be.rejectedWith(Error); + }); + + it('should throw validation exception when userId is not integer', async () => { + const payload = _.clone(validPayload); + payload.userId = 'string'; + await expect(projectUpdatedKafkaHandler(mockedApp, topic, payload)).to.be.rejectedWith(Error); + }); + + it('should throw validation exception when initiatorUserId is not integer', async () => { + const payload = _.clone(validPayload); + payload.initiatorUserId = 'string'; + await expect(projectUpdatedKafkaHandler(mockedApp, topic, payload)).to.be.rejectedWith(Error); + }); + + describe('integration', () => { + let project; + + beforeEach(async () => { + await testUtil.clearDb(); + project = await models.Project.create({ + type: 'generic', + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }); + // add project to ES index + await server.services.es.index({ + index: ES_PROJECT_INDEX, + type: ES_PROJECT_TYPE, + id: project.id, + body: { + doc: project.get({ plain: true }), + }, + }); + }); + + after(async () => { + await testUtil.clearDb(); + }); + + it('should throw exception when project not found by id', async () => { + const payload = _.clone(validPayload); + payload.projectId = 2; + await expect(projectUpdatedKafkaHandler(mockedApp, topic, payload)).to.be + .rejectedWith(Error, 'Project with id 2 not found'); + }); + + it('should update lastActivityAt and lastActivityUserId columns in db', async () => { + await projectUpdatedKafkaHandler(mockedApp, topic, validPayload); + + const updatedProject = await models.Project.findById(project.id); + expect(updatedProject.lastActivityUserId).to.be.eql('2'); + expect(updatedProject.lastActivityAt).to.be.greaterThan(project.lastActivityAt); + }); + + it('should update ES index', async () => { + await projectUpdatedKafkaHandler(mockedApp, topic, validPayload); + + const doc = await eClient.get({ index: ES_PROJECT_INDEX, type: ES_PROJECT_TYPE, id: validPayload.projectId }); + const esProject = doc._source; // eslint-disable-line no-underscore-dangle + expect(esProject.lastActivityUserId).to.be.eql('2'); + expect(new Date(esProject.lastActivityAt)).to.be.greaterThan(project.lastActivityAt); + }); + }); +}); diff --git a/src/models/project.js b/src/models/project.js index 6bb6b66f..7fdf81d0 100644 --- a/src/models/project.js +++ b/src/models/project.js @@ -42,6 +42,10 @@ module.exports = function defineProject(sequelize, DataTypes) { createdBy: { type: DataTypes.INTEGER, allowNull: false }, updatedBy: { type: DataTypes.INTEGER, allowNull: false }, version: { type: DataTypes.STRING(3), allowNull: false, defaultValue: 'v3' }, + lastActivityAt: { type: DataTypes.DATE, allowNull: false }, + // we use string for `lastActivityUserId` because it comes in Kafka messages payloads + // and can be not only user id but also `coderbot`, `system` or some kind of autopilot bot id in the future + lastActivityUserId: { type: DataTypes.STRING, allowNull: false }, }, { tableName: 'projects', paranoid: true, diff --git a/src/routes/admin/project-create-index.js b/src/routes/admin/project-create-index.js index e03a6e2b..d23738ea 100644 --- a/src/routes/admin/project-create-index.js +++ b/src/routes/admin/project-create-index.js @@ -253,6 +253,13 @@ function getRequestBody(indexName, docType) { updatedBy: { type: 'integer', }, + lastActivityAt: { + type: 'date', + format: 'strict_date_optional_time||epoch_millis', + }, + lastActivityUserId: { + type: 'string', + }, userId: { type: 'long', }, diff --git a/src/routes/attachments/create.spec.js b/src/routes/attachments/create.spec.js index 2aff8b54..5cbae3fb 100644 --- a/src/routes/attachments/create.spec.js +++ b/src/routes/attachments/create.spec.js @@ -6,6 +6,8 @@ import server from '../../app'; import models from '../../models'; import util from '../../util'; import testUtil from '../../tests/util'; +import busApi from '../../services/busApi'; +import { BUS_API_EVENT } from '../../constants'; const should = chai.should(); @@ -17,9 +19,46 @@ const body = { s3Bucket: 'submissions-staging-dev', contentType: 'application/pdf', }; + describe('Project Attachments', () => { let project1; - before((done) => { + let postSpy; + let getSpy; + let stub; + let sandbox; + + beforeEach((done) => { + const mockHttpClient = { + defaults: { headers: { common: {} } }, + post: () => new Promise(resolve => resolve({ + status: 200, + data: { + status: 200, + result: { + success: true, + status: 200, + 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, + content: { + filePath: 'tmp/spec.pdf', + preSignedURL: 'http://topcoder-media.s3.amazon.com/projects/1/spec.pdf', + }, + }, + }, + })), + }; + // mocks testUtil.clearDb() .then(() => { @@ -32,6 +71,8 @@ describe('Project Attachments', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { project1 = p; // create members @@ -42,12 +83,20 @@ describe('Project Attachments', () => { isPrimary: true, createdBy: 1, updatedBy: 1, - }).then(() => done()); + }).then(() => { + sandbox = sinon.sandbox.create(); + postSpy = sandbox.spy(mockHttpClient, 'post'); + getSpy = sandbox.spy(mockHttpClient, 'get'); + stub = sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + sandbox.stub(util, 's3FileTransfer').returns(Promise.resolve(true)); + done(); + }); }); }); }); - after((done) => { + afterEach((done) => { + sandbox.restore(); testUtil.clearDb(done); }); @@ -64,41 +113,6 @@ describe('Project Attachments', () => { }); it('should return 201 return attachment record', (done) => { - const mockHttpClient = { - defaults: { headers: { common: {} } }, - post: () => new Promise(resolve => resolve({ - status: 200, - data: { - status: 200, - result: { - success: true, - status: 200, - 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, - 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 = () => Promise.resolve(true); request(server) .post(`/v4/projects/${project1.id}/attachments/`) .set({ @@ -123,5 +137,47 @@ describe('Project Attachments', () => { } }); }); + + describe('Bus api', () => { + let createEventSpy; + + before((done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(() => { + createEventSpy = sandbox.spy(busApi, 'createEvent'); + }); + + it('sends single BUS_API_EVENT.PROJECT_FILES_UPDATED message when attachment added', (done) => { + request(server) + .post(`/v4/projects/${project1.id}/attachments/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ param: body }) + .expect(201) + .end((err) => { + if (err) { + done(err); + } else { + // Wait for app message handler to complete + testUtil.wait(() => { + createEventSpy.calledTwice.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_FILE_UPLOADED); + createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_FILES_UPDATED, sinon.match({ + projectId: project1.id, + projectName: project1.name, + projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`, + userId: 40051333, + initiatorUserId: 40051333, + })).should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/attachments/delete.spec.js b/src/routes/attachments/delete.spec.js index 850b8ca1..b9366d2d 100644 --- a/src/routes/attachments/delete.spec.js +++ b/src/routes/attachments/delete.spec.js @@ -8,6 +8,8 @@ import models from '../../models'; import util from '../../util'; import server from '../../app'; import testUtil from '../../tests/util'; +import busApi from '../../services/busApi'; +import { BUS_API_EVENT } from '../../constants'; describe('Project Attachments delete', () => { @@ -26,6 +28,8 @@ describe('Project Attachments delete', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { project1 = p; // create members @@ -142,5 +146,45 @@ describe('Project Attachments delete', () => { } }); }); + + describe('Bus api', () => { + let createEventSpy; + + before((done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(() => { + createEventSpy = sandbox.spy(busApi, 'createEvent'); + }); + + it('sends single BUS_API_EVENT.PROJECT_FILES_UPDATED message when attachment deleted', (done) => { + request(server) + .delete(`/v4/projects/${project1.id}/attachments/${attachment.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(204) + .end((err) => { + if (err) { + done(err); + } else { + // Wait for app message handler to complete + testUtil.wait(() => { + createEventSpy.calledOnce.should.be.true; + createEventSpy.calledWith(BUS_API_EVENT.PROJECT_FILES_UPDATED, sinon.match({ + projectId: project1.id, + projectName: project1.name, + projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`, + userId: 40051333, + initiatorUserId: 40051333, + })).should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/attachments/update.js b/src/routes/attachments/update.js index 8bebd832..9a67a86a 100644 --- a/src/routes/attachments/update.js +++ b/src/routes/attachments/update.js @@ -62,6 +62,7 @@ module.exports = [ { original: previousValue, updated: updated.get({ plain: true }) }, { correlationId: req.id }, ); + req.app.emit(EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_UPDATED, { req, original: previousValue, updated }); }).catch(err => next(err))); }, ]; diff --git a/src/routes/attachments/update.spec.js b/src/routes/attachments/update.spec.js index 3735eca2..99dd0b15 100644 --- a/src/routes/attachments/update.spec.js +++ b/src/routes/attachments/update.spec.js @@ -6,6 +6,8 @@ import request from 'supertest'; import models from '../../models'; import server from '../../app'; import testUtil from '../../tests/util'; +import busApi from '../../services/busApi'; +import { BUS_API_EVENT } from '../../constants'; const should = chai.should(); @@ -25,6 +27,8 @@ describe('Project Attachments update', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { project1 = p; // create members @@ -106,5 +110,46 @@ describe('Project Attachments update', () => { } }); }); + + describe('Bus api', () => { + let createEventSpy; + + before((done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(() => { + createEventSpy = sandbox.stub(busApi, 'createEvent'); + }); + + it('sends single BUS_API_EVENT.PROJECT_FILES_UPDATED message when attachment updated', (done) => { + request(server) + .patch(`/v4/projects/${project1.id}/attachments/${attachment.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ param: { title: 'updated title', description: 'updated description' } }) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + // Wait for app message handler to complete + testUtil.wait(() => { + createEventSpy.calledOnce.should.be.true; + createEventSpy.calledWith(BUS_API_EVENT.PROJECT_FILES_UPDATED, sinon.match({ + projectId: project1.id, + projectName: project1.name, + projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`, + userId: 40051333, + initiatorUserId: 40051333, + })).should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/milestones/create.spec.js b/src/routes/milestones/create.spec.js index 250e8175..bf652b28 100644 --- a/src/routes/milestones/create.spec.js +++ b/src/routes/milestones/create.spec.js @@ -1,13 +1,16 @@ +/* eslint-disable no-unused-expressions */ /** * Tests for create.js */ import chai from 'chai'; +import sinon from 'sinon'; import request from 'supertest'; import _ from 'lodash'; import server from '../../app'; import testUtil from '../../tests/util'; import models from '../../models'; -import { EVENT } from '../../constants'; +import busApi from '../../services/busApi'; +import { EVENT, BUS_API_EVENT } from '../../constants'; const should = chai.should(); @@ -28,6 +31,8 @@ describe('CREATE milestone', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }, { type: 'generic', @@ -38,6 +43,8 @@ describe('CREATE milestone', () => { details: {}, createdBy: 2, updatedBy: 2, + lastActivityAt: 1, + lastActivityUserId: '1', deletedAt: '2018-05-15T00:00:00Z', }, ], { returning: true }) @@ -604,5 +611,51 @@ describe('CREATE milestone', () => { done(); }); }); + + describe('Bus api', () => { + let createEventSpy; + const sandbox = sinon.sandbox.create(); + + before((done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(() => { + createEventSpy = sandbox.spy(busApi, 'createEvent'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when milestone created', (done) => { + request(server) + .post('/v4/timelines/1/milestones') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send(body) + .expect('Content-Type', /json/) + .expect(201) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledOnce.should.be.true; + createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + projectId: 1, + projectName: 'test1', + projectUrl: 'https://local.topcoder-dev.com/projects/1', + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/milestones/delete.js b/src/routes/milestones/delete.js index 50377c90..45e5a41b 100644 --- a/src/routes/milestones/delete.js +++ b/src/routes/milestones/delete.js @@ -55,6 +55,8 @@ module.exports = [ deleted, { correlationId: req.id }, ); + req.app.emit(EVENT.ROUTING_KEY.MILESTONE_REMOVED, + { req, deleted }); // Write to response res.status(204).end(); diff --git a/src/routes/milestones/delete.spec.js b/src/routes/milestones/delete.spec.js index a82294e9..ee9c41e3 100644 --- a/src/routes/milestones/delete.spec.js +++ b/src/routes/milestones/delete.spec.js @@ -1,14 +1,16 @@ +/* eslint-disable no-unused-expressions */ /** * Tests for delete.js */ import request from 'supertest'; +import sinon from 'sinon'; import chai from 'chai'; import models from '../../models'; import server from '../../app'; import testUtil from '../../tests/util'; -import { EVENT } from '../../constants'; - +import { EVENT, BUS_API_EVENT } from '../../constants'; +import busApi from '../../services/busApi'; const expectAfterDelete = (timelineId, id, err, next) => { if (err) throw err; @@ -50,6 +52,8 @@ describe('DELETE milestone', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }, { type: 'generic', @@ -60,6 +64,8 @@ describe('DELETE milestone', () => { details: {}, createdBy: 2, updatedBy: 2, + lastActivityAt: 1, + lastActivityUserId: '1', deletedAt: '2018-05-15T00:00:00Z', }, ]) @@ -348,5 +354,51 @@ describe('DELETE milestone', () => { .expect(204) .end(err => expectAfterDelete(1, 1, err, done)); }); + + describe('Bus api', () => { + let createEventSpy; + const sandbox = sinon.sandbox.create(); + + before((done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(() => { + createEventSpy = sandbox.spy(busApi, 'createEvent'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + // not testing fields separately as startDate is required parameter, + // thus PROJECT_PLAN_UPDATED will be always sent + it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when milestone removed', (done) => { + request(server) + .delete('/v4/timelines/1/milestones/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(204) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledOnce.should.be.true; + createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + projectId: 1, + projectName: 'test1', + projectUrl: 'https://local.topcoder-dev.com/projects/1', + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/milestones/get.spec.js b/src/routes/milestones/get.spec.js index 919b756d..fb0451d1 100644 --- a/src/routes/milestones/get.spec.js +++ b/src/routes/milestones/get.spec.js @@ -24,6 +24,8 @@ describe('GET milestone', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }, { type: 'generic', @@ -34,6 +36,8 @@ describe('GET milestone', () => { details: {}, createdBy: 2, updatedBy: 2, + lastActivityAt: 1, + lastActivityUserId: '1', deletedAt: '2018-05-15T00:00:00Z', }, ]) diff --git a/src/routes/milestones/list.spec.js b/src/routes/milestones/list.spec.js index 0240ee43..21c07336 100644 --- a/src/routes/milestones/list.spec.js +++ b/src/routes/milestones/list.spec.js @@ -94,6 +94,8 @@ describe('LIST timelines', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }, { type: 'generic', @@ -104,6 +106,8 @@ describe('LIST timelines', () => { details: {}, createdBy: 2, updatedBy: 2, + lastActivityAt: 1, + lastActivityUserId: '1', deletedAt: '2018-05-15T00:00:00Z', }, ]) diff --git a/src/routes/milestones/update.spec.js b/src/routes/milestones/update.spec.js index d6b77bb9..6286c5d4 100644 --- a/src/routes/milestones/update.spec.js +++ b/src/routes/milestones/update.spec.js @@ -1,14 +1,17 @@ +/* eslint-disable no-unused-expressions */ /** * Tests for get.js */ import chai from 'chai'; +import sinon from 'sinon'; import request from 'supertest'; import moment from 'moment'; import _ from 'lodash'; import models from '../../models'; import server from '../../app'; import testUtil from '../../tests/util'; -import { EVENT, MILESTONE_STATUS } from '../../constants'; +import busApi from '../../services/busApi'; +import { EVENT, MILESTONE_STATUS, BUS_API_EVENT } from '../../constants'; const should = chai.should(); @@ -26,6 +29,8 @@ describe('UPDATE Milestone', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }, { type: 'generic', @@ -36,6 +41,8 @@ describe('UPDATE Milestone', () => { details: {}, createdBy: 2, updatedBy: 2, + lastActivityAt: 1, + lastActivityUserId: '1', deletedAt: '2018-05-15T00:00:00Z', }, ]) @@ -1077,5 +1084,141 @@ describe('UPDATE Milestone', () => { .expect(200) .end(done); }); + + describe('Bus api', () => { + let createEventSpy; + const sandbox = sinon.sandbox.create(); + + before((done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(() => { + createEventSpy = sandbox.spy(busApi, 'createEvent'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when milestone duration updated', (done) => { + request(server) + .patch('/v4/timelines/1/milestones/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + duration: 1, + }, + }) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledTwice.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + projectId: 1, + projectName: 'test1', + projectUrl: 'https://local.topcoder-dev.com/projects/1', + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + createEventSpy.secondCall.calledWith(BUS_API_EVENT.TIMELINE_MODIFIED); + done(); + }); + } + }); + }); + + it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when milestone status updated', (done) => { + request(server) + .patch('/v4/timelines/1/milestones/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + status: 'reviewed', + }, + }) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledOnce.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + projectId: 1, + projectName: 'test1', + projectUrl: 'https://local.topcoder-dev.com/projects/1', + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + done(); + }); + } + }); + }); + + it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when milestone order updated', (done) => { + request(server) + .patch('/v4/timelines/1/milestones/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + order: 2, + }, + }) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledOnce.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + projectId: 1, + projectName: 'test1', + projectUrl: 'https://local.topcoder-dev.com/projects/1', + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + done(); + }); + } + }); + }); + + it('should not send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when milestone plannedText updated', (done) => { + request(server) + .patch('/v4/timelines/1/milestones/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + plannedText: 'new text', + }, + }) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.notCalled.should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/phaseProducts/create.spec.js b/src/routes/phaseProducts/create.spec.js index e5fe049d..8f81a28b 100644 --- a/src/routes/phaseProducts/create.spec.js +++ b/src/routes/phaseProducts/create.spec.js @@ -1,10 +1,12 @@ /* eslint-disable no-unused-expressions */ import _ from 'lodash'; +import sinon from 'sinon'; import chai from 'chai'; import request from 'supertest'; import server from '../../app'; import models from '../../models'; import testUtil from '../../tests/util'; +import busApi from '../../services/busApi'; const should = chai.should(); @@ -35,7 +37,7 @@ describe('Phase Products', () => { lastName: 'lName', email: 'some@abc.com', }; - before((done) => { + beforeEach((done) => { // mocks testUtil.clearDb() .then(() => { @@ -48,6 +50,8 @@ describe('Phase Products', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { projectId = p.id; // create members @@ -90,7 +94,7 @@ describe('Phase Products', () => { }); }); - after((done) => { + afterEach((done) => { testUtil.clearDb(done); }); @@ -215,5 +219,44 @@ describe('Phase Products', () => { } }); }); + + describe('Bus api', () => { + let createEventSpy; + const sandbox = sinon.sandbox.create(); + + before((done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(() => { + createEventSpy = sandbox.spy(busApi, 'createEvent'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should not send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when product phase created', (done) => { + request(server) + .post(`/v4/projects/${projectId}/phases/${phaseId}/products`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ param: body }) + .expect('Content-Type', /json/) + .expect(201) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.notCalled.should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/phaseProducts/delete.spec.js b/src/routes/phaseProducts/delete.spec.js index 5f958f4d..69942fa6 100644 --- a/src/routes/phaseProducts/delete.spec.js +++ b/src/routes/phaseProducts/delete.spec.js @@ -1,10 +1,12 @@ /* eslint-disable no-unused-expressions */ import _ from 'lodash'; +import sinon from 'sinon'; import request from 'supertest'; import chai from 'chai'; import server from '../../app'; import models from '../../models'; import testUtil from '../../tests/util'; +import busApi from '../../services/busApi'; const expectAfterDelete = (projectId, phaseId, id, err, next) => { if (err) throw err; @@ -63,7 +65,7 @@ describe('Phase Products', () => { lastName: 'lName', email: 'some@abc.com', }; - before((done) => { + beforeEach((done) => { // mocks testUtil.clearDb() .then(() => { @@ -76,6 +78,8 @@ describe('Phase Products', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { projectId = p.id; // create members @@ -123,7 +127,7 @@ describe('Phase Products', () => { }); }); - after((done) => { + afterEach((done) => { testUtil.clearDb(done); }); @@ -187,5 +191,42 @@ describe('Phase Products', () => { .expect(204) .end(err => expectAfterDelete(projectId, phaseId, productId, err, done)); }); + + describe('Bus api', () => { + let createEventSpy; + const sandbox = sinon.sandbox.create(); + + before((done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(() => { + createEventSpy = sandbox.spy(busApi, 'createEvent'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should not send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when product phase removed', (done) => { + request(server) + .delete(`/v4/projects/${projectId}/phases/${phaseId}/products/${productId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(204) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.notCalled.should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/phaseProducts/get.spec.js b/src/routes/phaseProducts/get.spec.js index d2022f17..a71d8a7f 100644 --- a/src/routes/phaseProducts/get.spec.js +++ b/src/routes/phaseProducts/get.spec.js @@ -51,6 +51,8 @@ describe('Phase Products', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { projectId = p.id; // create members diff --git a/src/routes/phaseProducts/list.spec.js b/src/routes/phaseProducts/list.spec.js index 0b0b46db..8dcd08d2 100644 --- a/src/routes/phaseProducts/list.spec.js +++ b/src/routes/phaseProducts/list.spec.js @@ -57,6 +57,8 @@ describe('Phase Products', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { projectId = p.id; project = p.toJSON(); @@ -94,12 +96,13 @@ describe('Phase Products', () => { }).then((phase) => { phaseId = phase.id; _.assign(body, { phaseId, projectId }); - + project.lastActivityAt = 1; project.phases = [phase.toJSON()]; models.PhaseProduct.create(body).then((product) => { project.phases[0].products = [product.toJSON()]; - + // Overwrite lastActivityAt as otherwise ES fill not be able to parse it + project.lastActivityAt = 1; // Index to ES return server.services.es.index({ index: ES_PROJECT_INDEX, diff --git a/src/routes/phaseProducts/update.spec.js b/src/routes/phaseProducts/update.spec.js index 1f0d91ac..3c35871b 100644 --- a/src/routes/phaseProducts/update.spec.js +++ b/src/routes/phaseProducts/update.spec.js @@ -1,10 +1,13 @@ /* eslint-disable no-unused-expressions */ import _ from 'lodash'; import chai from 'chai'; +import sinon from 'sinon'; import request from 'supertest'; import server from '../../app'; import models from '../../models'; import testUtil from '../../tests/util'; +import busApi from '../../services/busApi'; +import { BUS_API_EVENT } from '../../constants'; const should = chai.should(); @@ -61,6 +64,8 @@ describe('Phase Products', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { projectId = p.id; // create members @@ -208,5 +213,177 @@ describe('Phase Products', () => { } }); }); + + describe('Bus api', () => { + let createEventSpy; + const sandbox = sinon.sandbox.create(); + + before((done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(() => { + createEventSpy = sandbox.spy(busApi, 'createEvent'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when name updated', (done) => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}/products/${productId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + name: 'new name', + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledOnce.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + projectId: 1, + projectName: 'test1', + projectUrl: 'https://local.topcoder-dev.com/projects/1', + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + done(); + }); + } + }); + }); + + it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when estimatedPrice updated', (done) => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}/products/${productId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + estimatedPrice: 123, + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledOnce.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + projectId: 1, + projectName: 'test1', + projectUrl: 'https://local.topcoder-dev.com/projects/1', + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + done(); + }); + } + }); + }); + + it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when actualPrice updated', (done) => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}/products/${productId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + actualPrice: 123, + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledOnce.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + projectId: 1, + projectName: 'test1', + projectUrl: 'https://local.topcoder-dev.com/projects/1', + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + done(); + }); + } + }); + }); + + it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when details updated', (done) => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}/products/${productId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + details: 'something', + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledTwice.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PRODUCT_SPECIFICATION_MODIFIED); + createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + projectId: 1, + projectName: 'test1', + projectUrl: 'https://local.topcoder-dev.com/projects/1', + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + done(); + }); + } + }); + }); + + it('should not send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when type updated', (done) => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}/products/${productId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + type: 'another type', + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.notCalled.should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/phases/create.spec.js b/src/routes/phases/create.spec.js index 2f84160f..8bdcb5d8 100644 --- a/src/routes/phases/create.spec.js +++ b/src/routes/phases/create.spec.js @@ -1,10 +1,15 @@ /* eslint-disable no-unused-expressions */ import _ from 'lodash'; import chai from 'chai'; +import sinon from 'sinon'; import request from 'supertest'; import server from '../../app'; import models from '../../models'; import testUtil from '../../tests/util'; +import busApi from '../../services/busApi'; +import { + BUS_API_EVENT, +} from '../../constants'; const should = chai.should(); @@ -33,6 +38,7 @@ const validatePhase = (resJson, expectedPhase) => { describe('Project Phases', () => { let projectId; + let projectName; const memberUser = { handle: testUtil.getDecodedToken(testUtil.jwts.member).handle, userId: testUtil.getDecodedToken(testUtil.jwts.member).userId, @@ -61,8 +67,11 @@ describe('Project Phases', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { projectId = p.id; + projectName = p.name; // create members models.ProjectMember.bulkCreate([{ id: 1, @@ -336,5 +345,51 @@ describe('Project Phases', () => { } }); }); + + describe('Bus api', () => { + let createEventSpy; + const sandbox = sinon.sandbox.create(); + + before((done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(() => { + createEventSpy = sandbox.spy(busApi, 'createEvent'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when phase added', (done) => { + request(server) + .post(`/v4/projects/${projectId}/phases/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ param: body }) + .expect('Content-Type', /json/) + .expect(201) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledOnce.should.be.true; + createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + projectId, + projectName, + projectUrl: `https://local.topcoder-dev.com/projects/${projectId}`, + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/phases/delete.spec.js b/src/routes/phases/delete.spec.js index 1b3ace93..78453f39 100644 --- a/src/routes/phases/delete.spec.js +++ b/src/routes/phases/delete.spec.js @@ -1,10 +1,16 @@ /* eslint-disable no-unused-expressions */ import _ from 'lodash'; import request from 'supertest'; +import sinon from 'sinon'; import chai from 'chai'; import server from '../../app'; import models from '../../models'; import testUtil from '../../tests/util'; +import busApi from '../../services/busApi'; + +import { + BUS_API_EVENT, +} from '../../constants'; const expectAfterDelete = (projectId, id, err, next) => { if (err) throw err; @@ -48,6 +54,7 @@ const body = { describe('Project Phases', () => { let projectId; + let projectName; let phaseId; const memberUser = { handle: testUtil.getDecodedToken(testUtil.jwts.member).handle, @@ -63,7 +70,7 @@ describe('Project Phases', () => { lastName: 'lName', email: 'some@abc.com', }; - before((done) => { + beforeEach((done) => { // mocks testUtil.clearDb() .then(() => { @@ -76,8 +83,11 @@ describe('Project Phases', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { projectId = p.id; + projectName = p.name; // create members models.ProjectMember.bulkCreate([{ id: 1, @@ -106,7 +116,7 @@ describe('Project Phases', () => { }); }); - after((done) => { + afterEach((done) => { testUtil.clearDb(done); }); @@ -159,5 +169,49 @@ describe('Project Phases', () => { }) .end(err => expectAfterDelete(projectId, phaseId, err, done)); }); + + describe('Bus api', () => { + let createEventSpy; + const sandbox = sinon.sandbox.create(); + + before((done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(() => { + createEventSpy = sandbox.spy(busApi, 'createEvent'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when phase removed', (done) => { + request(server) + .delete(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(204) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledOnce.should.be.true; + createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + projectId, + projectName, + projectUrl: `https://local.topcoder-dev.com/projects/${projectId}`, + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/phases/get.spec.js b/src/routes/phases/get.spec.js index 1dab6542..f6cde663 100644 --- a/src/routes/phases/get.spec.js +++ b/src/routes/phases/get.spec.js @@ -52,6 +52,8 @@ describe('Project Phases', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { projectId = p.id; // create members diff --git a/src/routes/phases/list.spec.js b/src/routes/phases/list.spec.js index 86b6d7e9..43a1d14f 100644 --- a/src/routes/phases/list.spec.js +++ b/src/routes/phases/list.spec.js @@ -58,6 +58,8 @@ describe('Project Phases', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { projectId = p.id; project = p.toJSON(); @@ -83,6 +85,8 @@ describe('Project Phases', () => { return models.ProjectPhase.create(body); }).then((phase) => { // Index to ES + // Overwrite lastActivityAt as otherwise ES fill not be able to parse it + project.lastActivityAt = 1; project.phases = [phase]; return server.services.es.index({ index: ES_PROJECT_INDEX, diff --git a/src/routes/phases/update.spec.js b/src/routes/phases/update.spec.js index ce9ddf16..ae9bd30c 100644 --- a/src/routes/phases/update.spec.js +++ b/src/routes/phases/update.spec.js @@ -1,10 +1,16 @@ /* eslint-disable no-unused-expressions */ import _ from 'lodash'; +import sinon from 'sinon'; import chai from 'chai'; import request from 'supertest'; import server from '../../app'; import models from '../../models'; import testUtil from '../../tests/util'; +import busApi from '../../services/busApi'; + +import { + BUS_API_EVENT, +} from '../../constants'; const should = chai.should(); @@ -45,6 +51,7 @@ const validatePhase = (resJson, expectedPhase) => { describe('Project Phases', () => { let projectId; + let projectName; let phaseId; let phaseId2; const memberUser = { @@ -74,8 +81,11 @@ describe('Project Phases', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { projectId = p.id; + projectName = p.name; // create members models.ProjectMember.bulkCreate([{ id: 1, @@ -261,5 +271,305 @@ describe('Project Phases', () => { } }); }); + + describe('Bus api', () => { + let createEventSpy; + const sandbox = sinon.sandbox.create(); + + before((done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(() => { + createEventSpy = sandbox.spy(busApi, 'createEvent'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when spentBudget updated', (done) => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + spentBudget: 123, + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledTwice.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + projectId, + projectName, + projectUrl: `https://local.topcoder-dev.com/projects/${projectId}`, + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + + createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATE_PAYMENT); + done(); + }); + } + }); + }); + + it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when progress updated', (done) => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + progress: 50, + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.callCount.should.be.eql(3); + createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + projectId, + projectName, + projectUrl: `https://local.topcoder-dev.com/projects/${projectId}`, + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATE_PROGRESS); + createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_PROGRESS_MODIFIED); + done(); + }); + } + }); + }); + + it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when details updated', (done) => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + details: { + text: 'something', + }, + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledTwice.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + projectId, + projectName, + projectUrl: `https://local.topcoder-dev.com/projects/${projectId}`, + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATE_SCOPE); + done(); + }); + } + }); + }); + + it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when status updated', (done) => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + status: 'completed', + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledTwice.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + projectId, + projectName, + projectUrl: `https://local.topcoder-dev.com/projects/${projectId}`, + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_PHASE_TRANSITION_COMPLETED); + done(); + }); + } + }); + }); + + it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when budget updated', (done) => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + budget: 123, + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledOnce.should.be.true; + createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + projectId, + projectName, + projectUrl: `https://local.topcoder-dev.com/projects/${projectId}`, + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + done(); + }); + } + }); + }); + + it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when startDate updated', (done) => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + startDate: 123, + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledOnce.should.be.true; + createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + projectId, + projectName, + projectUrl: `https://local.topcoder-dev.com/projects/${projectId}`, + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + done(); + }); + } + }); + }); + + it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when duration updated', (done) => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + duration: 100, + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledOnce.should.be.true; + createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + projectId, + projectName, + projectUrl: `https://local.topcoder-dev.com/projects/${projectId}`, + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + done(); + }); + } + }); + }); + + it('should not send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when order updated', (done) => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + order: 100, + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.notCalled.should.be.true; + done(); + }); + } + }); + }); + + it('should not send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when endDate updated', (done) => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + endDate: new Date(), + }, + }) + .expect('Content-Type', /json/) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.notCalled.should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/projectMembers/create.spec.js b/src/routes/projectMembers/create.spec.js index 4d0c9f2f..429b50de 100644 --- a/src/routes/projectMembers/create.spec.js +++ b/src/routes/projectMembers/create.spec.js @@ -8,7 +8,8 @@ import models from '../../models'; import util from '../../util'; import server from '../../app'; import testUtil from '../../tests/util'; -import { USER_ROLE } from '../../constants'; +import busApi from '../../services/busApi'; +import { USER_ROLE, PROJECT_MEMBER_ROLE, BUS_API_EVENT } from '../../constants'; const should = chai.should(); @@ -28,6 +29,8 @@ describe('Project Members create', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { project1 = p; // create members @@ -49,6 +52,8 @@ describe('Project Members create', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p2) => { project2 = p2; done(); @@ -474,5 +479,146 @@ describe('Project Members create', () => { } }); }); + + describe('Bus api', () => { + let createEventSpy; + + before((done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(() => { + createEventSpy = sandbox.spy(busApi, 'createEvent'); + }); + + it('sends single BUS_API_EVENT.PROJECT_TEAM_UPDATED message when manager added', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + get: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: [{ + roleName: USER_ROLE.MANAGER, + }], + }, + }, + }), + post: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: {}, + }, + }, + }), + }); + sandbox.stub(util, 'getHttpClient', () => mockHttpClient); + request(server) + .post(`/v4/projects/${project1.id}/members/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ + param: { + userId: 3, + role: PROJECT_MEMBER_ROLE.MANAGER, + }, + }) + .expect(201) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledTwice.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.MEMBER_JOINED_MANAGER); + createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_TEAM_UPDATED, sinon.match({ + projectId: project1.id, + projectName: project1.name, + projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`, + userId: 40051334, + initiatorUserId: 40051334, + })).should.be.true; + done(); + }); + } + }); + }); + + it('sends single BUS_API_EVENT.PROJECT_TEAM_UPDATED message when copilot added', (done) => { + request(server) + .post(`/v4/projects/${project1.id}/members/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + userId: 3, + role: PROJECT_MEMBER_ROLE.COPILOT, + }, + }) + .expect(201) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledTwice.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.MEMBER_JOINED_COPILOT); + createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_TEAM_UPDATED, sinon.match({ + projectId: project1.id, + projectName: project1.name, + projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`, + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + done(); + }); + } + }); + }); + + it('sends single BUS_API_EVENT.PROJECT_TEAM_UPDATED message when customer added', (done) => { + request(server) + .post(`/v4/projects/${project1.id}/members/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ + param: { + userId: 3, + role: PROJECT_MEMBER_ROLE.CUSTOMER, + }, + }) + .expect(201) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledTwice.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.MEMBER_JOINED); + createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_TEAM_UPDATED, sinon.match({ + projectId: project1.id, + projectName: project1.name, + projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`, + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/projectMembers/delete.spec.js b/src/routes/projectMembers/delete.spec.js index 3667bcff..9f539ef2 100644 --- a/src/routes/projectMembers/delete.spec.js +++ b/src/routes/projectMembers/delete.spec.js @@ -8,6 +8,8 @@ import models from '../../models'; import util from '../../util'; import server from '../../app'; import testUtil from '../../tests/util'; +import busApi from '../../services/busApi'; +import { BUS_API_EVENT } from '../../constants'; const should = chai.should(); @@ -46,6 +48,8 @@ describe('Project members delete', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { project1 = p; // create members @@ -315,5 +319,87 @@ describe('Project members delete', () => { }) .expect(403, done); }); + + describe('Bus api', () => { + let createEventSpy; + + before((done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(() => { + createEventSpy = sandbox.spy(busApi, 'createEvent'); + }); + + it('sends single BUS_API_EVENT.PROJECT_TEAM_UPDATED message when manager removed', (done) => { + const mockHttpClient = _.merge(testUtil.mockHttpClient, { + post: () => Promise.resolve({ + status: 200, + data: { + id: 'requesterId', + version: 'v3', + result: { + success: true, + status: 200, + content: {}, + }, + }, + }), + }); + 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) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledTwice.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.MEMBER_LEFT); + createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_TEAM_UPDATED, sinon.match({ + projectId: project1.id, + projectName: project1.name, + projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`, + userId: 40051334, + initiatorUserId: 40051334, + })).should.be.true; + done(); + }); + } + }); + }); + + it('sends single BUS_API_EVENT.PROJECT_TEAM_UPDATED message when copilot removed', (done) => { + request(server) + .delete(`/v4/projects/${project1.id}/members/${member1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(204) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledTwice.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.MEMBER_REMOVED); + createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_TEAM_UPDATED, sinon.match({ + projectId: project1.id, + projectName: project1.name, + projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`, + userId: 40051334, + initiatorUserId: 40051334, + })).should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/projectMembers/update.spec.js b/src/routes/projectMembers/update.spec.js index 9497c416..4db550cd 100644 --- a/src/routes/projectMembers/update.spec.js +++ b/src/routes/projectMembers/update.spec.js @@ -7,6 +7,8 @@ import models from '../../models'; import server from '../../app'; import util from '../../util'; import testUtil from '../../tests/util'; +import busApi from '../../services/busApi'; +import { BUS_API_EVENT } from '../../constants'; const should = chai.should(); @@ -28,6 +30,8 @@ describe('Project members update', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { project1 = p; // create members @@ -449,5 +453,50 @@ describe('Project members update', () => { } }); }); + + describe('Bus api', () => { + let createEventSpy; + + before((done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(() => { + createEventSpy = sandbox.spy(busApi, 'createEvent'); + }); + + it('sends single BUS_API_EVENT.PROJECT_TEAM_UPDATED message when user role updated', (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) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledOnce.should.be.true; + createEventSpy.calledWith(BUS_API_EVENT.PROJECT_TEAM_UPDATED, sinon.match({ + projectId: project1.id, + projectName: project1.name, + projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`, + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/projectUpgrade/create.spec.js b/src/routes/projectUpgrade/create.spec.js index 853f508a..41ee7e0b 100644 --- a/src/routes/projectUpgrade/create.spec.js +++ b/src/routes/projectUpgrade/create.spec.js @@ -40,6 +40,8 @@ describe('Project upgrade', () => { }, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', version: 'v2', directProjectId: 123, estimatedPrice: 15000, diff --git a/src/routes/projects/create.js b/src/routes/projects/create.js index b83fb4ea..2600203b 100644 --- a/src/routes/projects/create.js +++ b/src/routes/projects/create.js @@ -218,6 +218,8 @@ module.exports = [ status: PROJECT_STATUS.DRAFT, createdBy: req.authUser.userId, updatedBy: req.authUser.userId, + lastActivityAt: new Date(), + lastActivityUserId: req.authUser.userId.toString(10), members: [{ isPrimary: true, role: userRole, diff --git a/src/routes/projects/create.spec.js b/src/routes/projects/create.spec.js index f490d638..5810401e 100644 --- a/src/routes/projects/create.spec.js +++ b/src/routes/projects/create.spec.js @@ -335,6 +335,9 @@ describe('Project create', () => { resJson.bookmarks.should.have.lengthOf(1); resJson.bookmarks[0].title.should.be.eql('title1'); resJson.bookmarks[0].address.should.be.eql('http://www.address.com'); + // Check that activity fields are set + resJson.lastActivityUserId.should.be.eql('40051331'); + resJson.lastActivityAt.should.be.not.null; server.services.pubsub.publish.calledWith('project.draft-created').should.be.true; done(); } diff --git a/src/routes/projects/delete.spec.js b/src/routes/projects/delete.spec.js index 852d8fb4..0ade73b7 100644 --- a/src/routes/projects/delete.spec.js +++ b/src/routes/projects/delete.spec.js @@ -48,6 +48,8 @@ describe('Project delete test', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { project1 = p; // create members diff --git a/src/routes/projects/get.spec.js b/src/routes/projects/get.spec.js index 9fc165ee..08fd3bbf 100644 --- a/src/routes/projects/get.spec.js +++ b/src/routes/projects/get.spec.js @@ -25,6 +25,8 @@ describe('GET Project', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { project1 = p; // create members @@ -56,6 +58,8 @@ describe('GET Project', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { project2 = p; }); diff --git a/src/routes/projects/list-db.js b/src/routes/projects/list-db.js index a2ce93fa..68b07761 100644 --- a/src/routes/projects/list-db.js +++ b/src/routes/projects/list-db.js @@ -102,6 +102,7 @@ module.exports = [ const sortableProps = [ 'createdAt', 'createdAt asc', 'createdAt desc', 'updatedAt', 'updatedAt asc', 'updatedAt desc', + 'lastActivityAt', 'lastActivityAt asc', 'lastActivityAt desc', 'id', 'id asc', 'id desc', 'status', 'status asc', 'status desc', 'name', 'name asc', 'name desc', diff --git a/src/routes/projects/list-db.spec.js b/src/routes/projects/list-db.spec.js index 2c9560fb..08f4d14c 100644 --- a/src/routes/projects/list-db.spec.js +++ b/src/routes/projects/list-db.spec.js @@ -1,4 +1,5 @@ /* eslint-disable no-unused-expressions */ +/* eslint-disable max-len */ import chai from 'chai'; import request from 'supertest'; @@ -61,6 +62,8 @@ describe('LIST Project db', () => { }, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { project1 = p; // create members @@ -101,6 +104,8 @@ describe('LIST Project db', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 2, + lastActivityUserId: '1', }).then((p) => { project2 = p; return models.ProjectMember.create({ @@ -115,12 +120,14 @@ describe('LIST Project db', () => { const p3 = models.Project.create({ type: 'visual_design', billingAccountId: 1, - name: 'test2', + name: 'test3', description: 'test project3', status: 'reviewed', details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 3, + lastActivityUserId: '1', }); return Promise.all([p1, p2, p3]) .then(() => done()); @@ -403,6 +410,72 @@ describe('LIST Project db', () => { } }); }); + + it('should return list of projects ordered ascending by lastActivityAt when sort column is "lastActivityAt"', (done) => { + request(server) + .get('/v4/projects/db/?sort=lastActivityAt') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(3); + resJson[0].name.should.equal('test1'); + resJson[1].name.should.equal('test2'); + resJson[2].name.should.equal('test3'); + done(); + } + }); + }); + + it('should return list of projects ordered descending by lastActivityAt when sort column is "lastActivityAt desc"', (done) => { + request(server) + .get('/v4/projects/db/?sort=lastActivityAt desc') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(3); + resJson[0].name.should.equal('test3'); + resJson[1].name.should.equal('test2'); + resJson[2].name.should.equal('test1'); + done(); + } + }); + }); + + it('should return list of projects ordered ascending by lastActivityAt when sort column is "lastActivityAt asc"', (done) => { + request(server) + .get('/v4/projects/db/?sort=lastActivityAt asc') + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(3); + resJson[0].name.should.equal('test1'); + resJson[1].name.should.equal('test2'); + resJson[2].name.should.equal('test3'); + done(); + } + }); + }); }); }); }); diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index 483cff4d..c890619d 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -285,6 +285,7 @@ module.exports = [ 'best match', 'createdAt', 'createdAt asc', 'createdAt desc', 'updatedAt', 'updatedAt asc', 'updatedAt desc', + 'lastActivityAt', 'lastActivityAt asc', 'lastActivityAt desc', 'id', 'id asc', 'id desc', 'status', 'status asc', 'status desc', 'name', 'name asc', 'name desc', diff --git a/src/routes/projects/list.spec.js b/src/routes/projects/list.spec.js index b69fcf6f..ef1bb7c6 100644 --- a/src/routes/projects/list.spec.js +++ b/src/routes/projects/list.spec.js @@ -1,4 +1,5 @@ /* eslint-disable no-unused-expressions */ +/* eslint-disable max-len */ import chai from 'chai'; import request from 'supertest'; import sleep from 'sleep'; @@ -28,6 +29,8 @@ const data = [ }, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', members: [ { id: 1, @@ -72,6 +75,8 @@ const data = [ details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 2, + lastActivityUserId: '1', members: [ { id: 1, @@ -88,12 +93,14 @@ const data = [ id: 3, type: 'visual_design', billingAccountId: 1, - name: 'test2', + name: 'test3', description: 'test project3', status: 'reviewed', details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 3, + lastActivityUserId: '1', }, ]; @@ -118,6 +125,8 @@ describe('LIST Project', () => { }, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }).then((p) => { project1 = p; // create members @@ -158,6 +167,8 @@ describe('LIST Project', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 2, + lastActivityUserId: '1', }).then((p) => { project2 = p; return models.ProjectMember.create({ @@ -172,12 +183,14 @@ describe('LIST Project', () => { const p3 = models.Project.create({ type: 'visual_design', billingAccountId: 1, - name: 'test2', + name: 'test3', description: 'test project3', status: 'reviewed', details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 3, + lastActivityUserId: '1', }).then((p) => { project3 = p; return Promise.resolve(); @@ -474,6 +487,72 @@ describe('LIST Project', () => { }); }); + it('should return list of projects ordered ascending by lastActivityAt when sort column is "lastActivityAt"', (done) => { + request(server) + .get('/v4/projects/?sort=lastActivityAt') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(3); + resJson[0].name.should.equal('test1'); + resJson[1].name.should.equal('test2'); + resJson[2].name.should.equal('test3'); + done(); + } + }); + }); + + it('should return list of projects ordered descending by lastActivityAt when sort column is "lastActivityAt desc"', (done) => { + request(server) + .get('/v4/projects/?sort=lastActivityAt desc') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(3); + resJson[0].name.should.equal('test3'); + resJson[1].name.should.equal('test2'); + resJson[2].name.should.equal('test1'); + done(); + } + }); + }); + + it('should return list of projects ordered ascending by lastActivityAt when sort column is "lastActivityAt asc"', (done) => { + request(server) + .get('/v4/projects/?sort=lastActivityAt asc') + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(200) + .end((err, res) => { + if (err) { + done(err); + } else { + const resJson = res.body.result.content; + should.exist(resJson); + resJson.should.have.lengthOf(3); + resJson[0].name.should.equal('test1'); + resJson[1].name.should.equal('test2'); + resJson[2].name.should.equal('test3'); + done(); + } + }); + }); + describe('GET All /projects/ for Connect Admin, ', () => { it('should return the project ', (done) => { request(server) diff --git a/src/routes/projects/update.spec.js b/src/routes/projects/update.spec.js index 42abf4f0..f1cb51ef 100644 --- a/src/routes/projects/update.spec.js +++ b/src/routes/projects/update.spec.js @@ -8,8 +8,12 @@ import models from '../../models'; import server from '../../app'; import testUtil from '../../tests/util'; import util from '../../util'; + +import busApi from '../../services/busApi'; + import { PROJECT_STATUS, + BUS_API_EVENT, } from '../../constants'; const should = chai.should(); @@ -62,6 +66,8 @@ describe('Project', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', createdAt: '2016-06-30 00:33:07+00', updatedAt: '2016-06-30 00:33:07+00', }, { @@ -73,6 +79,8 @@ describe('Project', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', createdAt: '2016-06-30 00:33:07+00', updatedAt: '2016-06-30 00:33:07+00', }, { @@ -83,6 +91,8 @@ describe('Project', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', createdAt: '2016-06-30 00:33:07+00', updatedAt: '2016-06-30 00:33:07+00', }]) @@ -809,5 +819,254 @@ describe('Project', () => { }); }); }); + + describe('Bus api', () => { + let createEventSpy; + + before((done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(() => { + createEventSpy = sandbox.spy(busApi, 'createEvent'); + }); + + it('sends single BUS_API_EVENT.PROJECT_UPDATED message on project status update', (done) => { + request(server) + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ + param: { + status: PROJECT_STATUS.COMPLETED, + }, + }) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledTwice.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_COMPLETED); + createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED, sinon.match({ + projectId: project1.id, + projectName: project1.name, + projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`, + userId: 40051333, + initiatorUserId: 40051333, + })).should.be.true; + done(); + }); + } + }); + }); + + it('sends single BUS_API_EVENT.PROJECT_UPDATED message on project details update', (done) => { + request(server) + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ + param: { + details: { + info: 'something', + }, + }, + }) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledTwice.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_SPECIFICATION_MODIFIED); + createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED, sinon.match({ + projectId: project1.id, + projectName: project1.name, + projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`, + userId: 40051333, + initiatorUserId: 40051333, + })).should.be.true; + done(); + }); + } + }); + }); + + it('sends single BUS_API_EVENT.PROJECT_UPDATED message on project name update', (done) => { + request(server) + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ + param: { + name: 'New project name', + }, + }) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledTwice.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_SPECIFICATION_MODIFIED); + createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED, sinon.match({ + projectId: project1.id, + projectName: 'New project name', + projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`, + userId: 40051333, + initiatorUserId: 40051333, + })).should.be.true; + done(); + }); + } + }); + }); + + it('sends single BUS_API_EVENT.PROJECT_UPDATED message on project description update', (done) => { + request(server) + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ + param: { + description: 'Updated description', + }, + }) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledTwice.should.be.true; + createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED, sinon.match({ + projectId: project1.id, + projectName: project1.name, + projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`, + userId: 40051333, + initiatorUserId: 40051333, + })).should.be.true; + done(); + }); + } + }); + }); + + it('sends single BUS_API_EVENT.PROJECT_UPDATED message on project bookmarks update', (done) => { + request(server) + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ + param: { + bookmarks: [{ + title: 'title1', + address: 'http://someurl.com', + }], + }, + }) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledTwice.should.be.true; + createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_SPECIFICATION_MODIFIED); + createEventSpy.secondCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED, sinon.match({ + projectId: project1.id, + projectName: project1.name, + projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`, + userId: 40051333, + initiatorUserId: 40051333, + })).should.be.true; + done(); + }); + } + }); + }); + + it('should not send BUS_API_EVENT.PROJECT_UPDATED message when project estimatedPrice is updated', (done) => { + request(server) + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ + param: { + estimatedPrice: 123, + }, + }) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.notCalled.should.be.true; + done(); + }); + } + }); + }); + + it('should not send BUS_API_EVENT.PROJECT_UPDATED message when project actualPrice is updated', (done) => { + request(server) + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ + param: { + actualPrice: 123, + }, + }) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.notCalled.should.be.true; + done(); + }); + } + }); + }); + + it('should not send BUS_API_EVENT.PROJECT_UPDATED message when project terms are updated', (done) => { + request(server) + .patch(`/v4/projects/${project1.id}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ + param: { + terms: [1, 2, 3], + }, + }) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.notCalled.should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/routes/timelines/create.spec.js b/src/routes/timelines/create.spec.js index 7ce3e9c3..41590c37 100644 --- a/src/routes/timelines/create.spec.js +++ b/src/routes/timelines/create.spec.js @@ -22,6 +22,8 @@ const testProjects = [ details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }, { type: 'generic', @@ -33,6 +35,8 @@ const testProjects = [ createdBy: 2, updatedBy: 2, deletedAt: '2018-05-15T00:00:00Z', + lastActivityAt: 1, + lastActivityUserId: '1', }, ]; diff --git a/src/routes/timelines/delete.spec.js b/src/routes/timelines/delete.spec.js index 44ad6f07..68c505b2 100644 --- a/src/routes/timelines/delete.spec.js +++ b/src/routes/timelines/delete.spec.js @@ -51,6 +51,8 @@ describe('DELETE timeline', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }, { type: 'generic', @@ -61,6 +63,8 @@ describe('DELETE timeline', () => { details: {}, createdBy: 2, updatedBy: 2, + lastActivityAt: 1, + lastActivityUserId: '1', deletedAt: '2018-05-15T00:00:00Z', }, ]) diff --git a/src/routes/timelines/get.spec.js b/src/routes/timelines/get.spec.js index 253013da..2331295c 100644 --- a/src/routes/timelines/get.spec.js +++ b/src/routes/timelines/get.spec.js @@ -72,6 +72,8 @@ describe('GET timeline', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }, { type: 'generic', @@ -82,6 +84,8 @@ describe('GET timeline', () => { details: {}, createdBy: 2, updatedBy: 2, + lastActivityAt: 1, + lastActivityUserId: '1', deletedAt: '2018-05-15T00:00:00Z', }, ]) diff --git a/src/routes/timelines/list.spec.js b/src/routes/timelines/list.spec.js index 877141de..2c1395c8 100644 --- a/src/routes/timelines/list.spec.js +++ b/src/routes/timelines/list.spec.js @@ -112,6 +112,8 @@ describe('LIST timelines', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }, { type: 'generic', @@ -122,6 +124,8 @@ describe('LIST timelines', () => { details: {}, createdBy: 2, updatedBy: 2, + lastActivityAt: 1, + lastActivityUserId: '1', deletedAt: '2018-05-15T00:00:00Z', }, ]) diff --git a/src/routes/timelines/update.js b/src/routes/timelines/update.js index e91783e4..bf403148 100644 --- a/src/routes/timelines/update.js +++ b/src/routes/timelines/update.js @@ -104,6 +104,8 @@ module.exports = [ { original, updated }, { correlationId: req.id }, ); + req.app.emit(EVENT.ROUTING_KEY.TIMELINE_UPDATED, + { req, original, updated }); // Write to response res.json(util.wrapResponse(req.id, updated)); diff --git a/src/routes/timelines/update.spec.js b/src/routes/timelines/update.spec.js index cf00398c..4cb811a9 100644 --- a/src/routes/timelines/update.spec.js +++ b/src/routes/timelines/update.spec.js @@ -1,17 +1,19 @@ +/* eslint-disable no-unused-expressions */ /** * Tests for get.js */ import chai from 'chai'; +import sinon from 'sinon'; import request from 'supertest'; import _ from 'lodash'; import models from '../../models'; import server from '../../app'; import testUtil from '../../tests/util'; -import { EVENT } from '../../constants'; +import { EVENT, BUS_API_EVENT } from '../../constants'; +import busApi from '../../services/busApi'; const should = chai.should(); - const milestones = [ { id: 1, @@ -74,6 +76,8 @@ describe('UPDATE timeline', () => { details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', }, { type: 'generic', @@ -84,6 +88,8 @@ describe('UPDATE timeline', () => { details: {}, createdBy: 2, updatedBy: 2, + lastActivityAt: 1, + lastActivityUserId: '1', deletedAt: '2018-05-15T00:00:00Z', }, ], { returning: true }) @@ -623,5 +629,52 @@ describe('UPDATE timeline', () => { .expect(200) .end(done); }); + + describe('Bus api', () => { + let createEventSpy; + const sandbox = sinon.sandbox.create(); + + before((done) => { + // Wait for 500ms in order to wait for createEvent calls from previous tests to complete + testUtil.wait(done); + }); + + beforeEach(() => { + createEventSpy = sandbox.spy(busApi, 'createEvent'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + // not testing fields separately as startDate is required parameter, + // thus PROJECT_PLAN_UPDATED will be always sent + it('should send message BUS_API_EVENT.PROJECT_PLAN_UPDATED when timeline updated', (done) => { + request(server) + .patch('/v4/timelines/1') + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send(body) + .expect(200) + .end((err) => { + if (err) { + done(err); + } else { + testUtil.wait(() => { + createEventSpy.calledOnce.should.be.true; + createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PLAN_UPDATED, sinon.match({ + projectId: 1, + projectName: 'test1', + projectUrl: 'https://local.topcoder-dev.com/projects/1', + userId: 40051332, + initiatorUserId: 40051332, + })).should.be.true; + done(); + }); + } + }); + }); + }); }); }); diff --git a/src/services/index.js b/src/services/index.js index a1d84c12..017a6ec2 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -2,6 +2,8 @@ import config from 'config'; import RabbitMQService from './rabbitmq'; +import startKafkaConsumer from './kafkaConsumer'; +import { kafkaHandlers } from '../events'; /** * Responsible for establishing connections to all external services @@ -31,6 +33,10 @@ module.exports = (fapp, logger) => { .then(() => { logger.info('RabbitMQ service initialized'); }) + .then(() => startKafkaConsumer(kafkaHandlers, app, logger)) + .then(() => { + logger.info('Kafka consumer service initialized'); + }) .catch((err) => { logger.error('Error initializing services', err); // gracefulShutdown() diff --git a/src/services/kafkaConsumer.js b/src/services/kafkaConsumer.js new file mode 100644 index 00000000..639deced --- /dev/null +++ b/src/services/kafkaConsumer.js @@ -0,0 +1,61 @@ +import Kafka from 'no-kafka'; +import config from 'config'; +/** + * Initializes Kafka consumer and subscribes for the topics + * @param {Object} handlers Object that holds kafka handlers. Where property name is kafka topic and value is handler + * @param {Object} app Application object + * @param {Object} logger Logger object + * @return {Promise} Promise that got resolved on successful consumer creation + */ +export default async function startKafkaConsumer(handlers, app, logger) { + // Read config and prepare Kafka options object + const kafkaConfig = config.get('kafkaConfig'); + + const options = {}; + if (kafkaConfig.has('hosts')) { + options.connectionString = kafkaConfig.get('hosts'); + } + if (kafkaConfig.has('clientCert') && kafkaConfig.has('clientCertKey')) { + options.ssl = { + cert: kafkaConfig.get('clientCert'), + key: kafkaConfig.has('clientCertKey'), + }; + } + + const consumer = new Kafka.SimpleConsumer(options); + await consumer.init(); + + /** + * Function is invoked each time new messages are written to specified topic. + * Calls handler for each message in messageSet. Outputs errors from handler into logger without + * interrupting the application. + * @param {Array} messageSet list of received messages + * @param {String} topic topic where messages are written + * @param {Number} partition partition where messages are written + * @return {Promise} Promise + */ + const onConsume = async (messageSet, topic, partition) => { + messageSet.forEach(async (kafkaMessage) => { + logger.debug(`Consume topic '${topic}' with message: '${kafkaMessage.message.value.toString('utf8')}'.`); + try { + const handler = handlers[topic]; + if (!handler) { + logger.info(`No handler configured for topic: ${topic}`); + return; + } + + const busMessage = JSON.parse(kafkaMessage.message.value.toString('utf8')); + const payload = busMessage.payload; + await handler(app, topic, payload); + await consumer.commitOffset({ topic, partition, offset: kafkaMessage.offset }); + logger.info(`Message for topic '${topic}' was successfully processed`); + } catch (error) { + logger.error(`Message processing failed: ${error}`); + } + }); + }; + + // Subscribe for all topics defined in handlers + const promises = Object.keys(handlers).map(topic => consumer.subscribe(topic, onConsume)); + await Promise.all(promises); +} diff --git a/src/services/kafkaConsumer.spec.js b/src/services/kafkaConsumer.spec.js new file mode 100644 index 00000000..887a6b16 --- /dev/null +++ b/src/services/kafkaConsumer.spec.js @@ -0,0 +1,124 @@ +/* eslint-disable no-unused-expressions */ +import sinon from 'sinon'; +import * as Kafka from 'no-kafka'; +import chai from 'chai'; + +import startKafkaConsumer from './kafkaConsumer'; + +chai.should(); + +describe('Kafka service', () => { + const sandbox = sinon.sandbox.create(); + let mockedSubscribe; + + const mockedLogger = { + debug: sinon.stub(), + info: sinon.stub(), + error: sinon.stub(), + }; + + const mockedApp = { + services: { + pubsub: { + publish: sinon.stub(), + }, + }, + }; + + // Mock Kafka consumer class + before(() => { + mockedSubscribe = sinon.stub(Kafka.SimpleConsumer.prototype, 'subscribe'); + sinon.stub(Kafka.SimpleConsumer.prototype, 'init').returns(Promise.resolve()); + sinon.stub(Kafka.SimpleConsumer.prototype, 'commitOffset').returns(Promise.resolve()); + }); + + afterEach(() => { + sandbox.reset(); + }); + + after(() => { + sandbox.restore(); + }); + + it('subscribes to every topic defined in handlers', async () => { + const handlers = { + topic1: () => {}, + topic2: () => {}, + }; + + await startKafkaConsumer(handlers, mockedApp, mockedLogger); + + mockedSubscribe.calledTwice.should.be.true; + mockedSubscribe.firstCall.calledWith('topic1').should.be.true; + mockedSubscribe.secondCall.calledWith('topic2').should.be.true; + }); + + describe('consumer', () => { + let consumerFunction; + + let handlers; + + beforeEach(async () => { + mockedLogger.error.reset(); + mockedLogger.info.reset(); + + handlers = { + topic1: sinon.stub(), + topic2: sinon.stub(), + }; + await startKafkaConsumer(handlers, mockedApp, mockedLogger); + // Get consumer function + consumerFunction = mockedSubscribe.lastCall.args[1]; + }); + + it('calls handler for specific topic only', async () => { + await consumerFunction([{ + message: { + value: `{ + "payload": { + "prop": "message" + } + }`, + }, + }], 'topic1', {}); + + handlers.topic1.calledOnce.should.be.true; + handlers.topic2.notCalled.should.be.true; + }); + + it('logs error and continues when handler fails', async () => { + handlers.topic2 = sinon.stub().returns(Promise.reject('failure')); + + await consumerFunction([{ + message: { + value: `{ + "payload": { + "prop": "message" + } + }`, + }, + }], 'topic2', {}); + + handlers.topic2.calledOnce.should.be.true; + mockedLogger.error.calledOnce.should.be.true; + mockedLogger.error.calledWith('Message processing failed: failure'); + }); + + it('drops message when handler not found', async () => { + await consumerFunction([{ + message: { + value: `{ + "payload": { + "prop": "message" + } + }`, + }, + }], 'unknown-topic', {}); + + handlers.topic1.notCalled.should.be.true; + handlers.topic2.notCalled.should.be.true; + mockedLogger.info.calledOnce.should.be.true; + mockedLogger.info.calledWith('No handler configured for topic: unknown-topic').should.be.true; + }); + }); +}); diff --git a/src/services/rabbitmq.js b/src/services/rabbitmq.js index f2f9917f..bddcfab7 100644 --- a/src/services/rabbitmq.js +++ b/src/services/rabbitmq.js @@ -1,7 +1,7 @@ /* globals Promise */ import _ from 'lodash'; import amqplib from 'amqplib'; -import handlers from '../events'; +import { rabbitHandlers as handlers } from '../events'; module.exports = class RabbitMQService { diff --git a/src/tests/seed.js b/src/tests/seed.js index 6b693e5c..5c4f553d 100644 --- a/src/tests/seed.js +++ b/src/tests/seed.js @@ -13,6 +13,8 @@ models.sequelize.sync({ force: true }) details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: new Date(), + lastActivityUserId: '1', }, { type: 'visual_design', directProjectId: 1, @@ -23,6 +25,8 @@ models.sequelize.sync({ force: true }) details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: new Date(), + lastActivityUserId: '1', }, { type: 'visual_design', billingAccountId: 3, @@ -32,6 +36,8 @@ models.sequelize.sync({ force: true }) details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: new Date(), + lastActivityUserId: '1', }, { type: 'generic', billingAccountId: 4, @@ -41,6 +47,8 @@ models.sequelize.sync({ force: true }) details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: new Date(), + lastActivityUserId: '1', }, { type: 'generic', billingAccountId: 5, @@ -50,6 +58,8 @@ models.sequelize.sync({ force: true }) details: {}, createdBy: 1, updatedBy: 1, + lastActivityAt: new Date(), + lastActivityUserId: '1', }, { type: 'generic', billingAccountId: 5, @@ -69,6 +79,8 @@ models.sequelize.sync({ force: true }) }, createdBy: 1, updatedBy: 1, + lastActivityAt: new Date(), + lastActivityUserId: '1', version: 'v2', directProjectId: 123, estimatedPrice: 15000, @@ -92,6 +104,8 @@ models.sequelize.sync({ force: true }) }, createdBy: 1, updatedBy: 1, + lastActivityAt: new Date(), + lastActivityUserId: '1', version: 'v2', directProjectId: 123, estimatedPrice: 15000, diff --git a/src/tests/util.js b/src/tests/util.js index f3dff595..232c553d 100644 --- a/src/tests/util.js +++ b/src/tests/util.js @@ -28,4 +28,7 @@ export default { connectAdmin: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJDb25uZWN0IEFkbWluIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJjb25uZWN0X2FkbWluMSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzYiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoiY29ubmVjdF9hZG1pbjFAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.nSGfXMl02NZ90ZKLiEKPg75iAjU92mfteaY6xgqkM30', }, getDecodedToken: token => jwt.decode(token), + + // Waits for 500ms and executes cb function + wait: cb => setTimeout(cb, 500), };