diff --git a/README.md b/README.md index af41df9..a4946d8 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## Dependencies - Nodejs (v10) -- Mongodb (v4) +- AWS DynamoDB ## Configuration @@ -12,16 +12,26 @@ The following parameters can be set in config files or in env variables: - LOG_LEVEL: the log level - PORT: the server port -- MONGODB_URL: Mongo DB URL - CHALLENGE_API_URL: the Topcoder challenge API URL - MEMBER_API_URL: the Topcoder member API URL -- GROUP_IDS: the valid group ids - AUTH0_URL: Auth0 URL, used to get TC M2M token - AUTH0_AUDIENCE: Auth0 audience, used to get TC M2M token - TOKEN_CACHE_TIME: Auth0 token cache time, used to get TC M2M token - AUTH0_CLIENT_ID: Auth0 client id, used to get TC M2M token - AUTH0_CLIENT_SECRET: Auth0 client secret, used to get TC M2M token - AUTH0_PROXY_SERVER_URL: Proxy Auth0 URL, used to get TC M2M token +- AMAZON.AWS_ACCESS_KEY_ID: The Amazon certificate key to use when connecting. For local dynamodb you can set fake value. +- AMAZON.AWS_SECRET_ACCESS_KEY: The Amazon certificate access key to use when connecting. For local dynamodb you can set fake value. +- AMAZON.AWS_REGION: The Amazon region to use when connecting. For local dynamodb you can set fake value. +- AMAZON.DYNAMODB_READ_CAPACITY_UNITS: the AWS DynamoDB read capacity units +- AMAZON.DYNAMODB_WRITE_CAPACITY_UNITS: the AWS DynamoDB write capacity units +- AMAZON.IS_LOCAL_DB: Use local or AWS Amazon DynamoDB +- AMAZON.DYNAMODB_URL: The local url, if using local Amazon DynamoDB +- HEALTH_CHECK_TIMEOUT: health check timeout in milliseconds + +## Local DynamoDB +Change to the ./docker-dynamodb directory and run `docker-compose up`. +An instance of DynamoDB listening on port `8000` will be initialized inside docker. ## Local deployment @@ -39,6 +49,12 @@ npm run lint npm run lint:fix # To fix possible lint errors ``` +- Make sure DynamoDB instance is up and create tables + +```bash +npm run create-tables +``` + - Clear and Insert data into database ```bash @@ -57,16 +73,8 @@ npm start For verification purpose, we need a mock app for Topcoder Challenge API and Topcoder Member API. You can run command `npm run mock-api` to start the mock app. -## Heroku Deployment - -- git init -- git add . -- git commit -m init -- heroku create -- heroku config:set MONGODB_URL=... -- git push heroku master - ## Verification +First of all, ensure you have started local DynamoDB and created tables. ### Tests @@ -90,7 +98,7 @@ npm run e2e npm run mock-api ``` -- Ensure you have start MongoDB and properly configure `MONGODB_URL`. Run the following commands to clear and insert test data, step up environment variables and start the app. +- Run the following commands clear and insert test data, step up environment variables and start the app. ```bash npm run init-db diff --git a/config/default.js b/config/default.js index 6b0a677..13d2a74 100644 --- a/config/default.js +++ b/config/default.js @@ -9,18 +9,27 @@ module.exports = { VALID_ISSUERS: process.env.VALID_ISSUERS ? process.env.VALID_ISSUERS.replace(/\\"/g, '') : '["https://topcoder-dev.auth0.com/", "https://api.topcoder.com/"]', - MONGODB_URL: process.env.MONGODB_URL || 'mongodb://localhost:27017/leaderboardDB', CHALLENGE_API_URL: process.env.CHALLENGE_API_URL || 'https://api.topcoder-dev.com/v5/challenges', MEMBER_API_URL: process.env.MEMBER_API_URL || 'https://api.topcoder-dev.com/v5/members', - GROUP_IDS: process.env.GROUP_IDS || '202343,20000000', // Comma separated string of Group IDs, - SCORE_RESET_TIME: process.env.SCORE_RESET_TIME || 10000, // 10 seconds default AUTH0_URL: process.env.AUTH0_URL || 'https://topcoder-dev.auth0.com/oauth/token', AUTH0_AUDIENCE: process.env.AUTH0_AUDIENCE || 'https://m2m.topcoder-dev.com/', TOKEN_CACHE_TIME: process.env.TOKEN_CACHE_TIME || 90, - AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID || '8QovDh27SrDu1XSs68m21A1NBP8isvOt', - AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET || '3QVxxu20QnagdH-McWhVz0WfsQzA1F8taDdGDI4XphgpEYZPcMTF4lX3aeOIeCzh', - AUTH0_PROXY_SERVER_URL: process.env.AUTH0_PROXY_SERVER_URL + AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID, + AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET, + AUTH0_PROXY_SERVER_URL: process.env.AUTH0_PROXY_SERVER_URL, + + AMAZON: { + AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY, + AWS_REGION: process.env.AWS_REGION || 'us-east-1', + DYNAMODB_READ_CAPACITY_UNITS: process.env.DYNAMODB_READ_CAPACITY_UNITS || 10, + DYNAMODB_WRITE_CAPACITY_UNITS: process.env.DYNAMODB_WRITE_CAPACITY_UNITS || 5, + IS_LOCAL_DB: process.env.IS_LOCAL_DB ? process.env.IS_LOCAL_DB === 'true' : false, + // Below configuration is required if IS_LOCAL_DB is true + DYNAMODB_URL: process.env.DYNAMODB_URL || 'http://localhost:8000/' + }, + HEALTH_CHECK_TIMEOUT: process.env.HEALTH_CHECK_TIMEOUT || 3000 } diff --git a/config/test.js b/config/test.js index abe771f..7554749 100644 --- a/config/test.js +++ b/config/test.js @@ -3,8 +3,7 @@ */ module.exports = { - CHALLENGE_API_URL: 'https://api.topcoder-dev.com/v4/challenges', - MEMBER_API_URL: 'https://api.topcoder-dev.com/v3/users', - MOCK_API_PORT: 3001, - MONGODB_URL: process.env.TEST_MONGODB_URL || 'mongodb://localhost:27017/leaderboardDB_test' + CHALLENGE_API_URL: 'https://api.topcoder-dev.com/v5/challenges', + MEMBER_API_URL: 'https://api.topcoder-dev.com/v5/members', + MOCK_API_PORT: 3001 } diff --git a/docker-dynamodb/docker-compose.yaml b/docker-dynamodb/docker-compose.yaml new file mode 100644 index 0000000..3698d7a --- /dev/null +++ b/docker-dynamodb/docker-compose.yaml @@ -0,0 +1,6 @@ +version: '3' +services: + dynamodb: + image: amazon/dynamodb-local + ports: + - "8000:8000" diff --git a/docs/Leaderboard API.postman_collection.json b/docs/Leaderboard API.postman_collection.json index a53b0d2..ae4dc42 100755 --- a/docs/Leaderboard API.postman_collection.json +++ b/docs/Leaderboard API.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "adfe8d84-410e-4628-9a1c-f74e16e89c83", + "_postman_id": "5595d7da-5a7f-4bff-932c-ab639907c9eb", "name": "Leaderboard API", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -173,9 +173,13 @@ "header": [ { "key": "Content-Type", - "name": "Content-Type", "value": "application/json", "type": "text" + }, + { + "key": "Authorization", + "value": "Bearer {{M2M_TOKEN}}", + "type": "text" } ], "body": { @@ -210,9 +214,13 @@ "header": [ { "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" + }, + { + "key": "Authorization", + "value": " Bearer {{M2M_TOKEN}}", + "type": "text" } ], "body": { @@ -247,9 +255,13 @@ "header": [ { "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" + }, + { + "key": "Authorization", + "value": " Bearer {{M2M_TOKEN}}", + "type": "text" } ], "body": { @@ -292,6 +304,11 @@ "key": "Content-Type", "value": "application/json", "type": "text" + }, + { + "key": "Authorization", + "value": " Bearer {{M2M_TOKEN}}", + "type": "text" } ], "body": { @@ -321,8 +338,13 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" + }, + { + "key": "Authorization", + "value": " Bearer {{M2M_TOKEN}}", + "type": "text" } ], "body": { @@ -352,8 +374,13 @@ "header": [ { "key": "Content-Type", - "type": "text", - "value": "application/json" + "value": "application/json", + "type": "text" + }, + { + "key": "Authorization", + "value": " Bearer {{M2M_TOKEN}}", + "type": "text" } ], "body": { @@ -386,7 +413,18 @@ "name": "delete leaderboard", "request": { "method": "DELETE", - "header": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Authorization", + "value": " Bearer {{M2M_TOKEN}}", + "type": "text" + } + ], "url": { "raw": "{{URL}}/leaderboard/review/661d3655-9c80-4f90-8051-e209e8c21704", "host": [ @@ -405,7 +443,18 @@ "name": "delete leaderboard 404", "request": { "method": "DELETE", - "header": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Authorization", + "value": " Bearer {{M2M_TOKEN}}", + "type": "text" + } + ], "url": { "raw": "{{URL}}/leaderboard/review/231d3655-9c80-4f90-8051-e209e8c21704", "host": [ @@ -422,6 +471,186 @@ } ], "protocolProfileBehavior": {} + }, + { + "name": "search groups", + "item": [ + { + "name": "search groups success", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{URL}}/leaderboard/groups", + "host": [ + "{{URL}}" + ], + "path": [ + "leaderboard", + "groups" + ] + } + }, + "response": [] + } + ], + "protocolProfileBehavior": {} + }, + { + "name": "create group", + "item": [ + { + "name": "create group success", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Authorization", + "value": " Bearer {{M2M_TOKEN}}", + "type": "text" + } + ], + "url": { + "raw": "{{URL}}/leaderboard/groups/:groupId", + "host": [ + "{{URL}}" + ], + "path": [ + "leaderboard", + "groups", + ":groupId" + ], + "variable": [ + { + "key": "groupId", + "value": "300" + } + ] + } + }, + "response": [] + }, + { + "name": "create group with duplicated groupId 409", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + }, + { + "key": "Authorization", + "type": "text", + "value": " Bearer {{M2M_TOKEN}}" + } + ], + "url": { + "raw": "{{URL}}/leaderboard/groups/:groupId", + "host": [ + "{{URL}}" + ], + "path": [ + "leaderboard", + "groups", + ":groupId" + ], + "variable": [ + { + "key": "groupId", + "value": "100" + } + ] + } + }, + "response": [] + } + ], + "protocolProfileBehavior": {} + }, + { + "name": "delete group", + "item": [ + { + "name": "delete group success", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + }, + { + "key": "Authorization", + "type": "text", + "value": " Bearer {{M2M_TOKEN}}" + } + ], + "url": { + "raw": "{{URL}}/leaderboard/groups/:groupId", + "host": [ + "{{URL}}" + ], + "path": [ + "leaderboard", + "groups", + ":groupId" + ], + "variable": [ + { + "key": "groupId", + "value": "100" + } + ] + } + }, + "response": [] + }, + { + "name": "delete group non-exist 404", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/json" + }, + { + "key": "Authorization", + "type": "text", + "value": " Bearer {{M2M_TOKEN}}" + } + ], + "url": { + "raw": "{{URL}}/leaderboard/groups/:groupId", + "host": [ + "{{URL}}" + ], + "path": [ + "leaderboard", + "groups", + ":groupId" + ], + "variable": [ + { + "key": "groupId", + "value": "1000" + } + ] + } + }, + "response": [] + } + ], + "protocolProfileBehavior": {} } ], "protocolProfileBehavior": {} diff --git a/docs/leaderboard-api.postman_environment.json b/docs/leaderboard-api.postman_environment.json index 51ebc68..f0d6330 100755 --- a/docs/leaderboard-api.postman_environment.json +++ b/docs/leaderboard-api.postman_environment.json @@ -1,14 +1,19 @@ { - "id": "e8b02e84-a9a1-4e68-8860-28d81acaf159", + "id": "58e3bdac-4b4f-41c2-b3f8-a3e196ce0c94", "name": "leaderboard-api", "values": [ { "key": "URL", "value": "http://localhost:3000/v5", "enabled": true + }, + { + "key": "M2M_TOKEN", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiOFFvdkRoMjdTckR1MVhTczY4bTIxQTFOQlA4aXN2T3RAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNjAzMjE4NjM1LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6IjhRb3ZEaDI3U3JEdTFYU3M2OG0yMUExTkJQOGlzdk90Iiwic2NvcGUiOiJyZWFkOmNoYWxsZW5nZXMgcmVhZDpncm91cHMgcmVhZDpzdWJtaXNzaW9uIHJlYWQ6cmV2aWV3X3R5cGUgdXBkYXRlOnJldmlld19zdW1tYXRpb24gcmVhZDpyZXZpZXdfc3VtbWF0aW9uIGNyZWF0ZTpyZXZpZXdfc3VtbWF0aW9uIHVwZGF0ZTpyZXZpZXcgcmVhZDpyZXZpZXcgZGVsZXRlOnJldmlldyBjcmVhdGU6cmV2aWV3IGFsbDpyZXZpZXcgcmVhZDpwcm9qZWN0IHdyaXRlOmJ1c19hcGkgcmVhZDp1c2VyX3Byb2ZpbGVzIHJlYWQ6cm9sZXMgY3JlYXRlOmxlYWRlcmJvYXJkIHVwZGF0ZTpsZWFkZXJib2FyZCBkZWxldGU6bGVhZGVyYm9hcmQgYWxsOmxlYWRlcmJvYXJkIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.wAv4YZhNXbPai9zKUfVFBhQ3vq6Itir8265WgybeBVk", + "enabled": true } ], "_postman_variable_scope": "environment", - "_postman_exported_at": "2019-10-22T15:48:23.697Z", - "_postman_exported_using": "Postman/7.9.0" + "_postman_exported_at": "2020-10-22T05:34:45.382Z", + "_postman_exported_using": "Postman/7.29.0" } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b38aefb..4012241 100644 --- a/package-lock.json +++ b/package-lock.json @@ -258,6 +258,21 @@ "@types/node": "*" } }, + "@types/source-map-support": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@types/source-map-support/-/source-map-support-0.5.3.tgz", + "integrity": "sha512-fvjMjVH8Rmokw2dWh1dkj90iX5R8FPjeZzjNH+6eFXReh0QnHFf1YBl3B0CF0RohIAA3SDRJsGeeUWKl6d7HqA==", + "requires": { + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, "@types/superagent": { "version": "3.8.7", "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.8.7.tgz", @@ -541,6 +556,29 @@ } } }, + "aws-sdk": { + "version": "2.775.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.775.0.tgz", + "integrity": "sha512-rlej1sgHmfhl+PJqpQ2qOOsbHEEnLBIKBmanMTUNGiEAfuS0MpFjXECXTpJIOrbUzl3OZuAYrGuBkg2qrBwRbQ==", + "requires": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + }, + "dependencies": { + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -634,20 +672,79 @@ "integrity": "sha512-dhHTWMI7kMx5whMQntl7Vr9C6BvV10lFXDAasnqnrMYhXVCzzk6IO9Fo2L75jXHT07WrOngL1WDXOp+yYS91Yg==" }, "body-parser": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", - "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", "requires": { - "bytes": "3.0.0", + "bytes": "3.1.0", "content-type": "~1.0.4", "debug": "2.6.9", "depd": "~1.1.2", - "http-errors": "~1.6.3", - "iconv-lite": "0.4.23", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", "on-finished": "~2.3.0", - "qs": "6.5.2", - "raw-body": "2.3.3", - "type-is": "~1.6.16" + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + } } }, "brace-expansion": { @@ -665,16 +762,26 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, - "bson": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.1.tgz", - "integrity": "sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg==" + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } }, "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", "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, "builtin-modules": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", @@ -693,9 +800,9 @@ } }, "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, "caching-transform": { "version": "3.0.2", @@ -1251,6 +1358,30 @@ "nan": "^2.14.0" } }, + "dynamoose": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/dynamoose/-/dynamoose-2.3.0.tgz", + "integrity": "sha512-XfaPu3eXctD1NiRNzSvjoJvPAuAAHW8jY5wwYXnkVuizTolS4D8R9mAwuKYenK6+eZwHCAgoJEPYE2caFlJWhA==", + "requires": { + "@types/node": "^13.13.4", + "@types/source-map-support": "^0.5.1", + "aws-sdk": "^2.668.0", + "source-map-support": "^0.5.19", + "uuid": "^8.0.0" + }, + "dependencies": { + "@types/node": { + "version": "13.13.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.27.tgz", + "integrity": "sha512-IeZlpkPnUqO45iBxJocIQzwV+K6phdSVaCxRwlvHHQ0YL+Gb1fvuv9GmIMYllZcjyzqoRKDNJeNo6p8dNWSPSQ==" + }, + "uuid": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==" + } + } + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -1625,6 +1756,11 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, "express": { "version": "4.16.4", "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", @@ -1660,6 +1796,41 @@ "type-is": "~1.6.16", "utils-merge": "1.0.1", "vary": "~1.1.2" + }, + "dependencies": { + "body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + } + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + } + } } }, "extend": { @@ -2227,6 +2398,11 @@ "url-join": "^4.0.1" } }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -2534,6 +2710,11 @@ "handlebars": "^4.1.2" } }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, "joi": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/joi/-/joi-14.0.0.tgz", @@ -2706,11 +2887,6 @@ "safe-buffer": "^5.0.1" } }, - "kareem": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.1.tgz", - "integrity": "sha512-l3hLhffs9zqoDe8zjmb/mAN4B8VT3L56EUvKNqLFVs9YlFA+zx7ke1DO8STAdDyYNkeSo1nKmjuvQeI12So8Xw==" - }, "kuler": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", @@ -2918,12 +3094,6 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, - "memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "optional": true - }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -3136,79 +3306,6 @@ "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", "optional": true }, - "mongodb": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.3.3.tgz", - "integrity": "sha512-MdRnoOjstmnrKJsK8PY0PjP6fyF/SBS4R8coxmhsfEU7tQ46/J6j+aSHF2n4c2/H8B+Hc/Klbfp8vggZfI0mmA==", - "requires": { - "bson": "^1.1.1", - "require_optional": "^1.0.1", - "safe-buffer": "^5.1.2", - "saslprep": "^1.0.0" - } - }, - "mongoose": { - "version": "5.7.7", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.7.7.tgz", - "integrity": "sha512-FU59waB4LKBa9KOnqBUcCcMIVRc09TFo1F8nMxrzSiIWATaJpjxxSSH5FBVUDxQfNdJLfg9uFHxaTxhhwjsZOQ==", - "requires": { - "bson": "~1.1.1", - "kareem": "2.3.1", - "mongodb": "3.3.3", - "mongoose-legacy-pluralize": "1.0.2", - "mpath": "0.6.0", - "mquery": "3.2.2", - "ms": "2.1.2", - "regexp-clone": "1.0.0", - "safe-buffer": "5.1.2", - "sift": "7.0.1", - "sliced": "1.0.1" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "mongoose-legacy-pluralize": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", - "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" - }, - "mpath": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.6.0.tgz", - "integrity": "sha512-i75qh79MJ5Xo/sbhxrDrPSEG0H/mr1kcZXJ8dH6URU5jD/knFxCVqVC/gVSW7GIXL/9hHWlT9haLbCXWOll3qw==" - }, - "mquery": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.2.tgz", - "integrity": "sha512-XB52992COp0KP230I3qloVUbkLUxJIu328HBP2t2EsxSFtf4W1HPSOBWOXf1bqxK4Xbb66lfMJ+Bpfd9/yZE1Q==", - "requires": { - "bluebird": "3.5.1", - "debug": "3.1.0", - "regexp-clone": "^1.0.0", - "safe-buffer": "5.1.2", - "sliced": "1.0.1" - }, - "dependencies": { - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -3469,6 +3566,12 @@ "requires": { "glob": "^7.1.3" } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true } } }, @@ -3849,20 +3952,57 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, "range-parser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" }, "raw-body": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", - "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.3", - "iconv-lite": "0.4.23", + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", "unpipe": "1.0.0" + }, + "dependencies": { + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + } } }, "read-pkg": { @@ -3919,11 +4059,6 @@ "backoff": "~2.5.0" } }, - "regexp-clone": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz", - "integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw==" - }, "regexpp": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", @@ -3975,6 +4110,11 @@ "combined-stream": "^1.0.6", "mime-types": "^2.1.12" } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" } } }, @@ -4008,15 +4148,6 @@ } } }, - "require_optional": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", - "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", - "requires": { - "resolve-from": "^2.0.0", - "semver": "^5.1.0" - } - }, "resolve": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", @@ -4026,11 +4157,6 @@ "path-parse": "^1.0.5" } }, - "resolve-from": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", - "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" - }, "restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -4090,14 +4216,10 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "saslprep": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", - "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", - "optional": true, - "requires": { - "sparse-bitfield": "^3.0.3" - } + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" }, "semver": { "version": "5.6.0", @@ -4161,11 +4283,6 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, - "sift": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/sift/-/sift-7.0.1.tgz", - "integrity": "sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g==" - }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -4189,24 +4306,26 @@ "is-fullwidth-code-point": "^2.0.0" } }, - "sliced": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", - "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, - "sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", - "optional": true, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", "requires": { - "memory-pager": "^1.0.2" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } } }, "spawn-wrap": { @@ -4622,6 +4741,11 @@ "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, "topo": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.0.tgz", @@ -4725,6 +4849,22 @@ "punycode": "^2.1.0" } }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, "url-join": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", @@ -4741,9 +4881,9 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==" }, "validate-npm-package-license": { "version": "3.0.4", @@ -4903,6 +5043,20 @@ "signal-exit": "^3.0.2" } }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", diff --git a/package.json b/package.json index fe52cf6..758df2d 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,9 @@ "main": "src/app.js", "scripts": { "start": "node src/app.js", - "init-db": "node src/init-db.js", - "test-data": "node src/test-data.js", + "create-tables": "node scripts/create-tables.js", + "init-db": "node scripts/init-db.js", + "test-data": "node scripts/test-data.js", "mock-api": "NODE_ENV=test node test/common/mock.js", "lint": "standard", "lint:fix": "standard --fix", @@ -26,8 +27,10 @@ }, "dependencies": { "bluebird": "^3.5.1", + "body-parser": "^1.19.0", "config": "^1.21.0", "cors": "^2.7.1", + "dynamoose": "^2.3.0", "express": "^4.14.0", "get-parameter-names": "^0.3.0", "http-status-codes": "^1.3.0", @@ -36,6 +39,7 @@ "mongoose": "^5.7.5", "superagent": "^5.1.0", "tc-core-library-js": "appirio-tech/tc-core-library-js.git#v2.6.3", + "uuid": "^8.3.1", "winston": "^3.1.0" }, "engines": { diff --git a/scripts/create-tables.js b/scripts/create-tables.js new file mode 100644 index 0000000..4cfb842 --- /dev/null +++ b/scripts/create-tables.js @@ -0,0 +1,32 @@ +/** + * Create table schemes in database + */ + +require('../src/bootstrap') +const models = require('../src/models') +const logger = require('../src/common/logger') +const dynamoose = require('dynamoose') + +logger.info('Requesting to create tables') + +const createTables = async () => { + const ddb = dynamoose.aws.ddb() + for (const model of Object.values(models)) { + const modelTableParams = await model.table.create.request() + await ddb.createTable(modelTableParams).promise() + } +} + +if (!module.parent) { + createTables().then(() => { + logger.info('Done! The index creation might still take some time. Please check the Dynamodb interface to verify that all processes have indeed completed and you are good to proceed with the next step') + process.exit() + }).catch((e) => { + logger.logFullError(e) + process.exit(1) + }) +} + +module.exports = { + createTables +} diff --git a/src/init-db.js b/scripts/init-db.js similarity index 54% rename from src/init-db.js rename to scripts/init-db.js index 3b573b1..01d8e5f 100644 --- a/src/init-db.js +++ b/scripts/init-db.js @@ -1,15 +1,20 @@ /** * Initialize database tables. All data will be cleared. */ -require('./bootstrap') -const { Leaderboard } = require('./models') -const logger = require('./common/logger') +require('../src/bootstrap') +const models = require('../src/models') +const logger = require('../src/common/logger') logger.info('Initialize database tables.') const initDB = async () => { // clear data - await Leaderboard.deleteMany({}) + for (const model of Object.values(models)) { + const entities = await model.scan().all().exec() + for (const item of entities) { + await model.delete(item) + } + } } if (!module.parent) { diff --git a/src/test-data.js b/scripts/test-data.js similarity index 90% rename from src/test-data.js rename to scripts/test-data.js index 5044617..0797109 100644 --- a/src/test-data.js +++ b/scripts/test-data.js @@ -1,9 +1,9 @@ /** * Insert test data. */ -require('./bootstrap') -const { Leaderboard } = require('./models') -const logger = require('./common/logger') +require('../src/bootstrap') +const { Leaderboard, Group } = require('../src/models') +const logger = require('../src/common/logger') const insertData = async () => { await Leaderboard.create({ @@ -83,6 +83,11 @@ const insertData = async () => { totalTestCases: 6, groupIds: ['100'] }) + for (const groupId of ['202343', '20000000', '100', '200']) { + await Group.create({ + groupId + }) + } } if (!module.parent) { diff --git a/src/common/errors.js b/src/common/errors.js index 34ca9be..961e53e 100644 --- a/src/common/errors.js +++ b/src/common/errors.js @@ -35,5 +35,6 @@ module.exports = { NotFoundError: createError('NotFoundError', 404), ForbiddenError: createError('ForbiddenError', 403), UnauthorizedError: createError('UnauthorizedError', 401), - ConflictError: createError('ConflictError', 409) + ConflictError: createError('ConflictError', 409), + ServiceUnavailableError: createError('ServiceUnavailableError', 503) } diff --git a/src/common/helper.js b/src/common/helper.js index ceb999e..97e7f3a 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -5,6 +5,7 @@ const _ = require('lodash') const config = require('config') const request = require('superagent') +const models = require('../models') const m2mAuth = require('tc-core-library-js').auth.m2m const m2m = m2mAuth(_.pick(config, ['AUTH0_URL', 'AUTH0_AUDIENCE', 'TOKEN_CACHE_TIME', 'AUTH0_PROXY_SERVER_URL'])) @@ -14,10 +15,10 @@ const m2m = m2mAuth(_.pick(config, ['AUTH0_URL', 'AUTH0_AUDIENCE', 'TOKEN_CACHE_ * @param {String []} groupIds Array of group ID * @returns {Boolean} True if any one of the Group ID is present in config */ -const isGroupIdValid = (groupIds) => { +const isGroupIdValid = async (groupIds) => { // Get the Group IDs from config - const confGroupIds = config.GROUP_IDS.split(',') - if (_.intersectionBy(confGroupIds, groupIds).length !== 0) { + const confGroupIds = _.map(await models.Group.scan().all().exec(), (group) => group.groupId) + if (_.intersection(confGroupIds, groupIds).length !== 0) { return true } return false diff --git a/src/controllers/GroupController.js b/src/controllers/GroupController.js new file mode 100644 index 0000000..11528a1 --- /dev/null +++ b/src/controllers/GroupController.js @@ -0,0 +1,40 @@ +/** + * Group Controller + */ + +const GroupService = require('../services/GroupService') + +/** + * Search groups + * @param req the request + * @param res the response + */ +async function searchGroups (req, res) { + res.json(await GroupService.searchGroups()) +} + +/** + * Create group + * @param req the request + * @param res the response + */ +async function createGroup (req, res) { + const result = await GroupService.createGroup(req.params.groupId) + res.json(result) +} + +/** + * Delete group by id + * @param req the request + * @param res the response + */ +async function deleteGroup (req, res) { + await GroupService.deleteGroup(req.params.groupId) + res.status(204).end() +} + +module.exports = { + searchGroups, + createGroup, + deleteGroup +} diff --git a/src/controllers/HealthCheckController.js b/src/controllers/HealthCheckController.js index 8433479..ad25d20 100644 --- a/src/controllers/HealthCheckController.js +++ b/src/controllers/HealthCheckController.js @@ -1,27 +1,34 @@ /** - * Health Check Controller + * Controller for health check endpoint */ - const config = require('config') -const db = require('../datasource').getDb(config.MONGODB_URL) +const models = require('../models') +const uuid = require('uuid') +const errors = require('../common/errors') + +// the topcoder-healthcheck-dropin library returns checksRun count, +// here it follows that to return such count +let checksRun = 0 +const randomReviewId = uuid.v4() /** - * Check for health of the app - * @param req the request - * @param res the response + * Check health of the app + * @param {Object} req the request + * @param {Object} res the response */ async function checkHealth (req, res) { - if (db.readyState === 1) { - res.status(200).json({ - checksRun: 1 + // perform a quick database access operation, if there is no error and is quick, then consider it healthy + checksRun += 1 + await models.Leaderboard.query({ reviewId: randomReviewId }).limit(1).exec() + .timeout(config.HEALTH_CHECK_TIMEOUT) + .catch((e) => { + if (e.name === 'TimeoutError') { + throw new errors.ServiceUnavailableError('Database operation is slow.') + } + throw new errors.ServiceUnavailableError(`There is database operation error, ${e.message}`) }) - - return - } - - res.status(503).json({ - checksRun: 1 - }) + // there is no error, and it is quick, then return checks run count + res.send({ checksRun }) } module.exports = { diff --git a/src/datasource.js b/src/datasource.js deleted file mode 100644 index 8284432..0000000 --- a/src/datasource.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Functions related to MongoDB instances - */ - -// The mongoose instance. -const mongoose = require('mongoose') - -// use bluebird promise library instead of mongoose default promise library -mongoose.Promise = global.Promise - -// The database mapping. -const dbs = { } - -/** - * Gets a db connection for a URL. - * @param {String} url MongoDB URL - * @return {object} connection for the given URL - */ -const getDb = (url) => { - if (!dbs[url]) { - const db = mongoose.createConnection(url, { - useNewUrlParser: true - }) - dbs[url] = db - } - return dbs[url] -} - -// exports the functions -module.exports = { - getDb -} diff --git a/src/models/Group.js b/src/models/Group.js new file mode 100644 index 0000000..3956de2 --- /dev/null +++ b/src/models/Group.js @@ -0,0 +1,22 @@ +/** + * Group Schema + */ + +const config = require('config') +const dynamoose = require('dynamoose') + +const Schema = dynamoose.Schema + +const GroupSchema = new Schema({ + groupId: { + type: String, + hashKey: true + } +}, { + throughput: { + read: Number(config.AMAZON.DYNAMODB_READ_CAPACITY_UNITS), + write: Number(config.AMAZON.DYNAMODB_WRITE_CAPACITY_UNITS) + } +}) + +module.exports = GroupSchema diff --git a/src/models/Leaderboard.js b/src/models/Leaderboard.js index 03d7c07..968c66a 100644 --- a/src/models/Leaderboard.js +++ b/src/models/Leaderboard.js @@ -2,22 +2,42 @@ * Leaderboard Schema */ -const Schema = require('mongoose').Schema +const config = require('config') +const dynamoose = require('dynamoose') + +const Schema = dynamoose.Schema const LeaderboardSchema = new Schema({ - reviewId: { type: String }, + reviewId: { + type: String, + index: { + name: 'reviewId-index', + global: true + } + }, submissionId: { type: String }, - challengeId: { type: String }, - memberId: { type: String }, + challengeId: { + type: String, + hashKey: true + }, + memberId: { type: String, rangeKey: true }, handle: { type: String }, aggregateScore: { type: Number }, testsPassed: { type: Number }, scoreLevel: { type: String }, scoreResetTime: { type: Number }, totalTestCases: { type: Number }, - groupIds: { type: [String] }, + groupIds: { + type: Array, + schema: [String] + }, status: { type: String }, finalDetails: { type: Object } +}, { + throughput: { + read: Number(config.AMAZON.DYNAMODB_READ_CAPACITY_UNITS), + write: Number(config.AMAZON.DYNAMODB_WRITE_CAPACITY_UNITS) + } }) module.exports = LeaderboardSchema diff --git a/src/models/index.js b/src/models/index.js index 7ee76ab..23f9168 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -1,28 +1,38 @@ /** - * Initialize MongoDB models + * Initialize DynamoDB models */ +const config = require('config') +const dynamoose = require('dynamoose') const fs = require('fs') const path = require('path') -const config = require('config') -const db = require('../datasource').getDb(config.MONGODB_URL) -const models = {} -// Bootstrap models +const awsConfig = { + region: config.AMAZON.AWS_REGION +} +if (config.AMAZON.AWS_ACCESS_KEY_ID && config.AMAZON.AWS_SECRET_ACCESS_KEY) { + awsConfig.accessKeyId = config.AMAZON.AWS_ACCESS_KEY_ID + awsConfig.secretAccessKey = config.AMAZON.AWS_SECRET_ACCESS_KEY +} +dynamoose.aws.sdk.config.update(awsConfig) + +if (config.AMAZON.IS_LOCAL_DB) { + dynamoose.aws.ddb.local(config.AMAZON.DYNAMODB_URL) +} + +dynamoose.model.defaults.set({ + create: false, + update: false, + waitForActive: false +}) + +const models = {} fs.readdirSync(__dirname).forEach((file) => { if (file !== 'index.js') { const filename = file.split('.')[0] const schema = require(path.join(__dirname, filename)) - const model = db.model(filename, schema) + const model = dynamoose.model(filename, schema) models[filename] = model - - model.schema.options.toJSON = { - transform: (doc, ret) => { - delete ret._id - delete ret.__v - return ret - } - } } }) diff --git a/src/routes.js b/src/routes.js index a46b923..4278efc 100644 --- a/src/routes.js +++ b/src/routes.js @@ -33,6 +33,26 @@ module.exports = { scopes: [constants.Scopes.LeaderboardDelete, constants.Scopes.LeaderboardAll] } }, + '/leaderboard/groups': { + get: { + controller: 'GroupController', + method: 'searchGroups' + } + }, + '/leaderboard/groups/:groupId': { + post: { + controller: 'GroupController', + method: 'createGroup', + auth: 'jwt', + scopes: [constants.Scopes.LeaderboardCreate, constants.Scopes.LeaderboardAll] + }, + delete: { + controller: 'GroupController', + method: 'deleteGroup', + auth: 'jwt', + scopes: [constants.Scopes.LeaderboardDelete, constants.Scopes.LeaderboardAll] + } + }, '/health': { get: { controller: 'HealthCheckController', diff --git a/src/services/GroupService.js b/src/services/GroupService.js new file mode 100644 index 0000000..c93fbcf --- /dev/null +++ b/src/services/GroupService.js @@ -0,0 +1,57 @@ +/** + * Group service + */ +const _ = require('lodash') +const joi = require('joi') +const logger = require('../common/logger') +const errors = require('../common/errors') +const { Group } = require('../models') + +/** + * Search groups + * @returns {Array} the groups + */ +async function searchGroups () { + return _.map(await Group.scan().all().exec(), (group) => group.groupId) +} + +/** + * Create group + * @param {Number} groupId the group Id + * @returns {Object} the created group + */ +async function createGroup (groupId) { + const entity = await Group.get(groupId) + if (entity) { + throw new errors.ConflictError(`groupId # ${groupId} already exists.`) + } + return Group.create({ groupId }) +} + +createGroup.schema = { + groupId: joi.string().required() +} + +/** + * Delete group by id + * @param {Number} groupId the group Id + */ +async function deleteGroup (groupId) { + const entity = await Group.get(groupId) + if (!entity) { + throw new errors.NotFoundError(`groupId # ${groupId} doesn't exist`) + } + await Group.delete(entity) +} + +deleteGroup.schema = { + groupId: joi.string().required() +} + +module.exports = { + searchGroups, + createGroup, + deleteGroup +} + +logger.buildService(module.exports) diff --git a/src/services/LeaderboardService.js b/src/services/LeaderboardService.js index 3afcab3..915fb68 100644 --- a/src/services/LeaderboardService.js +++ b/src/services/LeaderboardService.js @@ -20,7 +20,7 @@ const timers = {} * @returns {Object} the leaderboard detail */ async function getLeaderboard (challengeId, memberId) { - return Leaderboard.find({ $and: [{ challengeId }, { memberId }] }) + return Leaderboard.query({ challengeId, memberId }).exec() } /** @@ -83,8 +83,8 @@ async function createLeaderboard (challengeId, memberId, review) { } const groupIds = challenge.groups - if (!helper.isGroupIdValid(groupIds)) { - logger.debug(`Group ID (${JSON.stringify(groupIds)}) of Challenge # ${challengeId} is not in the configured set of Ids (${config.GROUP_IDS}) configured for processing!`) + if (!(await helper.isGroupIdValid(groupIds))) { + logger.debug(`Group ID (${JSON.stringify(groupIds)}) of Challenge # ${challengeId} is not in the approved list. Ignoring request`) // Ignore the message return } @@ -99,7 +99,7 @@ async function createLeaderboard (challengeId, memberId, review) { scoreLevel = 'queued' } - // Record to be written into MongoDB + // Record to be written into DynamoDB const record = { reviewId: review.id, submissionId: review.submissionId, @@ -127,17 +127,17 @@ createLeaderboard.schema = { }).unknown(true).required() } +/* istanbul ignore next */ /** * If the app gets restarted, this function will handle incomplete resets */ async function resetIncompleteScoreLevels () { // find records where scoreLevel doesn't equal ('' and 'queued') and scoreResetTime doesn't equal null - const existRecords = await Leaderboard.find({ - $and: [ - { $and: [{ scoreLevel: { $ne: '' } }, { scoreLevel: { $ne: 'queued' } }] }, - { scoreResetTime: { $ne: null } } - ] - }) + const existRecords = await Leaderboard.scan() + .where('scoreLevel').not().eq('') + .and().where('scoreLevel').not().eq('queued') + .and().filter('scoreResetTime').exists() + .all().exec() _.forEach(existRecords, (record) => { const currentTime = Date.now() // if reset time already passed, reset immediately @@ -153,14 +153,14 @@ async function resetIncompleteScoreLevels () { /** * Resets the scoreLevel back to an empty string - * Resets the scoreResetTime to null + * Delete the scoreResetTime(by setting its value to undefined) * - * @param {Object} record Mongoose record + * @param {Object} record Dynamoose record */ -async function resetScoreLevel (record) { +function resetScoreLevel (record) { _.assignIn(record, { scoreLevel: '', - scoreResetTime: null + scoreResetTime: undefined }) record.save() const timerKey = `${record.challengeId}:${record.memberId}` @@ -169,7 +169,7 @@ async function resetScoreLevel (record) { /** * Resets the scoreLevel back to an empty string - * Resets the scoreResetTime to null + * Delete the scoreResetTime(by setting its value to undefined) * * @param {String} challengeId the challenge id * @param {String} memberId the member id @@ -197,7 +197,7 @@ async function updateLeaderboard (challengeId, memberId, review) { let scoreLevel = '' // when the score should be reset - incomplete timers will be reloaded on restart - let scoreResetTime = null + let scoreResetTime const { testsPassed, totalTestCases } = calculateResult(review) @@ -208,7 +208,7 @@ async function updateLeaderboard (challengeId, memberId, review) { finalDetails: { aggregateScore: review.aggregateScore, testsPassed, - totalTestCases, + totalTestCases } }) } else { @@ -220,7 +220,7 @@ async function updateLeaderboard (challengeId, memberId, review) { scoreLevel = 'up' scoreLevelChanged = true } - + if (scoreLevelChanged) { const timerKey = `${challengeId}:${memberId}` // if we got a new review for the same challengeId:memberId, reset the timer @@ -232,7 +232,7 @@ async function updateLeaderboard (challengeId, memberId, review) { } else { scoreLevel = 'queued' } - + _.assignIn(existRecords[0], { aggregateScore: review.score, reviewId: review.id, @@ -243,7 +243,7 @@ async function updateLeaderboard (challengeId, memberId, review) { status: review.status }) } - + return existRecords[0].save() } @@ -271,12 +271,13 @@ async function searchLeaderboards (filter) { return getLeaderboard(filter.challengeId, filter.memberId) } if (filter.challengeId) { - return Leaderboard.find({ challengeId: filter.challengeId }) - .sort({ aggregateScore: -1 }) - .skip((filter.page - 1) * filter.perPage) - .limit(filter.perPage) + const result = await Leaderboard.query({ challengeId: filter.challengeId }).all().exec() + return _.orderBy(result, ['aggregateScore'], ['desc']).slice( + (filter.page - 1) * filter.perPage, + filter.perPage * filter.page + ) } else if (filter.groupId) { - const leaderboards = await Leaderboard.find({ groupIds: filter.groupId }) + const leaderboards = await Leaderboard.scan({ groupIds: { contains: filter.groupId } }).all().exec() const map = new Map() _.each(leaderboards, e => { if (!map.has(e.memberId)) { @@ -306,7 +307,7 @@ async function searchLeaderboards (filter) { searchLeaderboards.schema = { filter: joi.object().keys({ - challengeId: joi.alternatives().try(joi.id(), joi.string().uuid()), + challengeId: joi.alternatives().try(joi.string(), joi.string().uuid()), memberId: joi.string(), groupId: joi.string(), page: joi.page(), @@ -320,11 +321,11 @@ searchLeaderboards.schema = { * @param {String} reviewId the review id */ async function deleteLeaderboard (reviewId) { - const entity = await Leaderboard.findOne({ reviewId }) + const [entity] = await Leaderboard.query({ reviewId }).exec() if (!entity) { throw new errors.NotFoundError(`Leaderboard record with review id: ${reviewId} doesn't exist`) } - await entity.remove() + await Leaderboard.delete(entity) } deleteLeaderboard.schema = { diff --git a/test/common/prepare.js b/test/common/prepare.js index c528b4c..b81a2f5 100644 --- a/test/common/prepare.js +++ b/test/common/prepare.js @@ -4,26 +4,26 @@ const nock = require('nock') const prepare = require('mocha-prepare') -const { challengeAPIResponse, memberAPIResponse } = require('./testData') +const { challengeAPIResponse, memberAPIResponse, M2M_FULL_TOKEN } = require('./testData') prepare(function (done) { nock(/\.com/) .persist() .post('/oauth/token') - .reply(200, { access_token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBTdXBwb3J0IiwiYWRtaW5pc3RyYXRvciIsInRlc3RSb2xlIiwiYWFhIiwidG9ueV90ZXN0XzEiLCJDb25uZWN0IE1hbmFnZXIiLCJDb25uZWN0IEFkbWluIiwiY29waWxvdCIsIkNvbm5lY3QgQ29waWxvdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJUb255SiIsImV4cCI6MTU2NTY4MTkyMCwidXNlcklkIjoiODU0Nzg5OSIsImlhdCI6MTU1NTY4MTMyMCwiZW1haWwiOiJhamVmdHNAdG9wY29kZXIuY29tIiwianRpIjoiMTlhMDkzNzAtMjk4OC00N2I4LTkxODktMGRhODVjNjM0ZWQyIn0.V8nsQpbzQ_4iEd0dAbuYsfeydnhSAEQ95AKKwl8RONw' }) - .get('/v4/challenges?filter=id=30000001') + .reply(200, { access_token: M2M_FULL_TOKEN }) + .get('/v5/challenges?legacyId=30000001') .reply(200, challengeAPIResponse[0]) - .get('/v4/challenges?filter=id=30051825') + .get('/v5/challenges?legacyId=30051825') .reply(200, challengeAPIResponse[1]) - .get('/v4/challenges?filter=id=30051826') + .get('/v5/challenges?legacyId=30051826') .reply(200, challengeAPIResponse[2]) - .get('/v4/challenges?filter=id=31000000') + .get('/v5/challenges?legacyId=31000000') .reply(200, challengeAPIResponse[3]) - .get('/v3/users?filter=id=10000') + .get('/v5/members?userId=10000&fields=userId,handle') .reply(200, memberAPIResponse[0]) - .get('/v3/users?filter=id=8547899') + .get('/v5/members?userId=8547899&fields=userId,handle') .reply(200, memberAPIResponse[1]) - .get('/v3/users?filter=id=22688726') + .get('/v5/members?userId=22688726&fields=userId,handle') .reply(200, memberAPIResponse[2]) done() diff --git a/test/common/testData.js b/test/common/testData.js index cc947cc..0a2b308 100644 --- a/test/common/testData.js +++ b/test/common/testData.js @@ -36,164 +36,46 @@ const submissionAPIResponse = [ ] const challengeAPIResponse = [ - { - 'id': '2587d0de:16de737ff0d:3225', - 'result': { - 'success': true, - 'status': 200, - 'metadata': null, - 'content': [] - }, - 'version': 'v3' - }, - { - 'id': '-3254eca5:16658dbed10:2c2a', - 'result': { - 'success': true, - 'status': 200, - 'metadata': { - 'fields': null, - 'totalCount': 1 - }, - 'content': [ - { - 'groupIds': [ - 20000000 - ] - } - ] - } - }, - { - 'id': '-3254eca5:16658dbed10:2c2a', - 'result': { - 'success': true, - 'status': 200, - 'metadata': { - 'fields': null, - 'totalCount': 1 - }, - 'content': [ - { - 'groupIds': [ - 202343, - 20000000 - ] - } - ] - } - }, - { - 'id': '-3254eca5:16658dbed10:2c2a', - 'result': { - 'success': true, - 'status': 200, - 'metadata': { - 'fields': null, - 'totalCount': 1 - }, - 'content': [ - { - 'groupIds': [ - 30000 - ] - } - ] - } - } + [], + [{ + 'groups': [ + '20000000' + ] + }], + [{ + 'groups': [ + '202343', + '20000000' + ] + }], + [{ + 'groups': [ + '30000' + ] + }] ] const memberAPIResponse = [ - { - 'id': '2587d0de:16de737ff0d:3225', - 'result': { - 'success': true, - 'status': 200, - 'metadata': null, - 'content': [] - }, - 'version': 'v3' - }, - { - 'id': '311d312:166718c336c:1936', - 'result': { - 'success': true, - 'status': 200, - 'metadata': null, - 'content': [ - { - 'id': '8547899', - 'modifiedBy': null, - 'modifiedAt': '2018-10-14T02:53:38.000Z', - 'createdBy': null, - 'createdAt': '2004-03-21T21:05:32.000Z', - 'handle': 'TonyJ', - 'email': 'tjefts+dev@topcoder.com', - 'firstName': 'Tony', - 'lastName': 'L_NAME', - 'credential': { - 'activationCode': 'UASI7X0JS', - 'resetToken': null, - 'hasPassword': true - }, - 'status': 'A', - 'country': null, - 'regSource': null, - 'utmSource': null, - 'utmMedium': null, - 'utmCampaign': null, - 'roles': null, - 'ssoLogin': false, - 'active': true, - 'profile': null, - 'emailActive': true - } - ] - }, - 'version': 'v3' - }, - { - 'id': '2587d0de:16de737ff0d:3203', - 'result': { - 'success': true, - 'status': 200, - 'metadata': null, - 'content': [ - { - 'id': '22688726', - 'modifiedBy': null, - 'modifiedAt': '2019-10-16T03:39:37.000Z', - 'createdBy': null, - 'createdAt': '2007-07-05T05:25:12.000Z', - 'handle': 'vasyl', - 'email': 'email@domain.com.z', - 'firstName': 'F_NAME', - 'lastName': 'L_NAME', - 'credential': { - 'activationCode': 'MC8XPOC3Z7', - 'resetToken': null, - 'hasPassword': true - }, - 'status': 'A', - 'country': null, - 'regSource': null, - 'utmSource': null, - 'utmMedium': null, - 'utmCampaign': null, - 'roles': null, - 'ssoLogin': false, - 'profile': null, - 'active': true, - 'emailActive': true - } - ] - }, - 'version': 'v3' - } + [], + [ + { + 'userId': 8547899, + 'handle': 'TonyJ' + } + ], + [ + { + 'userId': 22688726, + 'handle': 'vasyl' + } + ] ] +const M2M_FULL_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiOFFvdkRoMjdTckR1MVhTczY4bTIxQTFOQlA4aXN2T3RAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNjAzMjE4NjM1LCJleHAiOjIxNDc0ODM2NDgsImF6cCI6IjhRb3ZEaDI3U3JEdTFYU3M2OG0yMUExTkJQOGlzdk90Iiwic2NvcGUiOiJyZWFkOmNoYWxsZW5nZXMgcmVhZDpncm91cHMgcmVhZDpzdWJtaXNzaW9uIHJlYWQ6cmV2aWV3X3R5cGUgdXBkYXRlOnJldmlld19zdW1tYXRpb24gcmVhZDpyZXZpZXdfc3VtbWF0aW9uIGNyZWF0ZTpyZXZpZXdfc3VtbWF0aW9uIHVwZGF0ZTpyZXZpZXcgcmVhZDpyZXZpZXcgZGVsZXRlOnJldmlldyBjcmVhdGU6cmV2aWV3IGFsbDpyZXZpZXcgcmVhZDpwcm9qZWN0IHdyaXRlOmJ1c19hcGkgcmVhZDp1c2VyX3Byb2ZpbGVzIHJlYWQ6cm9sZXMgY3JlYXRlOmxlYWRlcmJvYXJkIHVwZGF0ZTpsZWFkZXJib2FyZCBkZWxldGU6bGVhZGVyYm9hcmQgYWxsOmxlYWRlcmJvYXJkIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.wAv4YZhNXbPai9zKUfVFBhQ3vq6Itir8265WgybeBVk' + module.exports = { submissionAPIResponse, challengeAPIResponse, - memberAPIResponse + memberAPIResponse, + M2M_FULL_TOKEN } diff --git a/test/e2e/test.js b/test/e2e/test.js index 9b6a466..4af7d93 100644 --- a/test/e2e/test.js +++ b/test/e2e/test.js @@ -5,13 +5,15 @@ process.env.NODE_ENV = 'test' require('../../src/bootstrap') const chai = require('chai') +const _ = require('lodash') const expect = require('chai').expect const chaiHttp = require('chai-http') const logger = require('../../src/common/logger') -const { initDB } = require('../../src/init-db') -const { insertData } = require('../../src/test-data') +const { initDB } = require('../../scripts/init-db') +const { insertData } = require('../../scripts/test-data') const { Leaderboard } = require('../../src/models') const { expressApp } = require('../../src/app') +const { M2M_FULL_TOKEN } = require('../common/testData') chai.use(chaiHttp) @@ -106,25 +108,25 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { expect(res.status).to.equal(200) const result = res.body expect(result.length).to.equal(3) - expect(result[0]).to.deep.equal({ - numberOfChallenges: 2, - finalAggregationScore: 140, - totalTests: 16, - totalTestsPassed: 13, - memberId: '123458', - memberHandle: 'user3' - }) - expect(result[1]).to.deep.equal({ + expect(result[0]).to.contain({ numberOfChallenges: 3, - finalAggregationScore: 110, + finalAggregationScore: 330, totalTests: 36, totalTestsPassed: 22, memberId: '123456', memberHandle: 'user1' }) - expect(result[2]).to.deep.equal({ + expect(result[1]).to.contain({ numberOfChallenges: 2, - finalAggregationScore: 70, + finalAggregationScore: 280, + totalTests: 16, + totalTestsPassed: 13, + memberId: '123458', + memberHandle: 'user3' + }) + expect(result[2]).to.contain({ + numberOfChallenges: 2, + finalAggregationScore: 140, totalTests: 30, totalTestsPassed: 19, memberId: '123457', @@ -139,13 +141,13 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { expect(res.status).to.equal(200) const result = res.body expect(result.length).to.equal(1) - expect(result[0]).to.deep.equal({ - numberOfChallenges: 1, - finalAggregationScore: 80, - totalTests: 10, - totalTestsPassed: 8, - memberId: '123458', - memberHandle: 'user3' + expect(result[0]).to.contain({ + numberOfChallenges: 2, + finalAggregationScore: 140, + totalTests: 30, + totalTestsPassed: 18, + memberId: '123456', + memberHandle: 'user1' }) }) @@ -170,6 +172,7 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { it('create leaderboard success 1', async () => { const res = await chai.request(expressApp) .post('/v5/leaderboard/challenge/30051825/member/8547899') + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) .send({ id: '161d3655-9c80-4f90-8051-e209e8c21701', submissionId: '261d3655-9c80-4f90-8051-e209e8c21701', @@ -183,6 +186,7 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { submissionId: '261d3655-9c80-4f90-8051-e209e8c21701', memberId: '8547899', challengeId: '30051825', + scoreLevel: '', handle: 'TonyJ', aggregateScore: 0, testsPassed: 0, @@ -193,6 +197,7 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { it('create leaderboard success 2', async () => { const res = await chai.request(expressApp) .post('/v5/leaderboard/challenge/30051826/member/8547899') + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) .send({ id: '161d3655-9c80-4f90-8051-e209e8c21702', submissionId: '261d3655-9c80-4f90-8051-e209e8c21702', @@ -213,6 +218,7 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { submissionId: '261d3655-9c80-4f90-8051-e209e8c21702', memberId: '8547899', challengeId: '30051826', + scoreLevel: '', handle: 'TonyJ', aggregateScore: 90, testsPassed: 9, @@ -223,6 +229,7 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { it('create leaderboard with invalid metadata 1', async () => { const res = await chai.request(expressApp) .post('/v5/leaderboard/challenge/30051825/member/22688726') + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) .send({ id: '161d3655-9c80-4f90-8051-e209e8c21703', submissionId: '261d3655-9c80-4f90-8051-e209e8c21703', @@ -242,6 +249,7 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { submissionId: '261d3655-9c80-4f90-8051-e209e8c21703', memberId: '22688726', challengeId: '30051825', + scoreLevel: '', handle: 'vasyl', aggregateScore: 0, testsPassed: 0, @@ -252,6 +260,7 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { it('create leaderboard with invalid metadata 2', async () => { const res = await chai.request(expressApp) .post('/v5/leaderboard/challenge/30051826/member/22688726') + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) .send({ id: '161d3655-9c80-4f90-8051-e209e8c21704', submissionId: '261d3655-9c80-4f90-8051-e209e8c21704', @@ -266,6 +275,7 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { submissionId: '261d3655-9c80-4f90-8051-e209e8c21704', memberId: '22688726', challengeId: '30051826', + scoreLevel: '', handle: 'vasyl', aggregateScore: 0, testsPassed: 0, @@ -276,6 +286,7 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { it('failure - create leaderboard with incorrect challenge', async () => { const res = await chai.request(expressApp) .post('/v5/leaderboard/challenge/30000001/member/8547899') + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) .send({ id: '161d3655-9c80-4f90-8051-e209e8c21705', submissionId: '261d3655-9c80-4f90-8051-e209e8c21705', @@ -289,6 +300,7 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { it('failure - create leaderboard with incorrect member', async () => { const res = await chai.request(expressApp) .post('/v5/leaderboard/challenge/30051826/member/10000') + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) .send({ id: '161d3655-9c80-4f90-8051-e209e8c21706', submissionId: '261d3655-9c80-4f90-8051-e209e8c21706', @@ -302,6 +314,7 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { it('failure - create leaderboard already exists', async () => { const res = await chai.request(expressApp) .post('/v5/leaderboard/challenge/30051825/member/8547899') + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) .send({ id: '161d3655-9c80-4f90-8051-e209e8c21701', submissionId: '261d3655-9c80-4f90-8051-e209e8c21701', @@ -315,6 +328,7 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { it('failure - create leaderboard with invalid parameter 1', async () => { const res = await chai.request(expressApp) .post('/v5/leaderboard/challenge/30051825/member/8547899') + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) .send({ submissionId: '261d3655-9c80-4f90-8051-e209e8c21701', aggregateScore: 0 @@ -327,6 +341,7 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { it('failure - create leaderboard with invalid parameter 2', async () => { const res = await chai.request(expressApp) .post('/v5/leaderboard/challenge/30051825/member/8547899') + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) .send({ id: '161d3655-9c80-4f90-8051-e209e8c21707', aggregateScore: 0 @@ -339,6 +354,7 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { it('failure - create leaderboard with invalid parameter 3', async () => { const res = await chai.request(expressApp) .post('/v5/leaderboard/challenge/30051825/member/8547899') + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) .send({ id: '161d3655-9c80-4f90-8051-e209e8c21707', submissionId: '261d3655-9c80-4f90-8051-e209e8c21701' @@ -351,13 +367,14 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { it('ignore - create leaderboard with ignored challenge', async () => { const res = await chai.request(expressApp) .post('/v5/leaderboard/challenge/31000000/member/22688726') + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) .send({ id: '161d3655-9c80-4f90-8051-e209e8c21707', submissionId: '261d3655-9c80-4f90-8051-e209e8c21707', score: 50 }) expect(res.status).to.equal(204) - expect(debugLogs[3]).to.equal('Group ID ([30000]) of Challenge # 31000000 is not in the configured set of Ids (202343,20000000) configured for processing!') + expect(debugLogs[3]).to.equal('Group ID (["30000"]) of Challenge # 31000000 is not in the approved list. Ignoring request') }) }) @@ -365,6 +382,7 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { it('update leaderboard success', async () => { const res = await chai.request(expressApp) .patch('/v5/leaderboard/challenge/30051825/member/8547899') + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) .send({ id: '361d3655-9c80-4f90-8051-e209e8c21701', metadata: { @@ -378,8 +396,8 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { }) expect(res.status).to.equal(200) - expect(res.body).to.deep.equal({ - groupIds: [ '20000000' ], + expect(res.body).to.deep.contain({ + groupIds: ['20000000'], reviewId: '361d3655-9c80-4f90-8051-e209e8c21701', submissionId: '261d3655-9c80-4f90-8051-e209e8c21701', memberId: '8547899', @@ -390,11 +408,13 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { totalTestCases: 5, scoreLevel: 'up' }) + expect(_.isNumber(res.body.scoreResetTime)).to.equal(true) }) it('failure - update leaderboard not found', async () => { const res = await chai.request(expressApp) .patch('/v5/leaderboard/challenge/30051825/member/5547899') + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) .send({ id: '361d3655-9c80-4f90-8051-e209e8c21701', score: 80 @@ -407,6 +427,7 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { it('failure - update leaderboard with invalid parameter 1', async () => { const res = await chai.request(expressApp) .patch('/v5/leaderboard/challenge/30051825/member/8547899') + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) .send({ aggregateScore: 0 }) @@ -418,6 +439,7 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { it('failure - update leaderboard with invalid parameter 2', async () => { const res = await chai.request(expressApp) .patch('/v5/leaderboard/challenge/30051825/member/8547899') + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) .send({ id: '161d3655-9c80-4f90-8051-e209e8c21707' }) @@ -433,18 +455,66 @@ describe('Topcoder - Leaderboard API E2E Tests', () => { it('delete leaderboard success', async () => { const res = await chai.request(expressApp) .delete(`/v5/leaderboard/review/${id}`) + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) expect(res.status).to.equal(204) - const result = await Leaderboard.find({ reviewId: id }) + const result = await Leaderboard.query({ reviewId: id }).exec() expect(result.length).to.equal(0) }) it('failure - delete leaderboard not found', async () => { const res = await chai.request(expressApp) .delete(`/v5/leaderboard/review/${id}`) + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) expect(res.status).to.equal(404) expect(res.body.message).to.equal(`Leaderboard record with review id: ${id} doesn't exist`) }) }) + + describe('search groups test', async () => { + it('search groups success', async () => { + const res = await chai.request(expressApp) + .get(`/v5/leaderboard/groups`) + expect(res.status).to.equal(200) + expect(res.body).to.eql(['202343', '100', '20000000', '200']) + }) + }) + + describe('create group test', async () => { + it('create group success', async () => { + const groupId = '300' + const res = await chai.request(expressApp) + .post(`/v5/leaderboard/groups/${groupId}`) + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) + expect(res.status).to.equal(200) + expect(res.body).to.eql({ groupId }) + }) + it('failure - group wth specified groupId already exist', async () => { + const groupId = '300' + const res = await chai.request(expressApp) + .post(`/v5/leaderboard/groups/${groupId}`) + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) + expect(res.status).to.equal(409) + expect(res.body).to.eql({ message: `groupId # ${groupId} already exists.` }) + }) + }) + + describe('delete group test', async () => { + it('delete group success', async () => { + const groupId = '300' + const res = await chai.request(expressApp) + .delete(`/v5/leaderboard/groups/${groupId}`) + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) + expect(res.status).to.equal(204) + }) + it('failure - group with specified groupId does not exist', async () => { + const groupId = '300' + const res = await chai.request(expressApp) + .delete(`/v5/leaderboard/groups/${groupId}`) + .set('Authorization', `Bearer ${M2M_FULL_TOKEN}`) + expect(res.status).to.equal(404) + expect(res.body).to.eql({ message: `groupId # ${groupId} doesn't exist` }) + }) + }) }) diff --git a/test/unit/test.js b/test/unit/test.js index 1b9f767..aac57f2 100644 --- a/test/unit/test.js +++ b/test/unit/test.js @@ -7,9 +7,10 @@ require('../../src/bootstrap') const _ = require('lodash') const expect = require('chai').expect const service = require('../../src/services/LeaderboardService') +const GroupService = require('../../src/services/GroupService') const logger = require('../../src/common/logger') -const { initDB } = require('../../src/init-db') -const { insertData } = require('../../src/test-data') +const { initDB } = require('../../scripts/init-db') +const { insertData } = require('../../scripts/test-data') const { Leaderboard } = require('../../src/models') describe('Topcoder - Leaderboard API Unit Tests', () => { @@ -58,7 +59,7 @@ describe('Topcoder - Leaderboard API Unit Tests', () => { it('search leaderboard with challengeId success', async () => { const result = await service.searchLeaderboards({ challengeId: '30104644', page: 2, perPage: 2 }) expect(result.length).to.equal(1) - expect(_.omit(result[0]._doc, ['__v', '_id'])).to.deep.equal({ + expect(result[0].toJSON()).to.deep.equal({ reviewId: '661d3655-9c80-4f90-8051-e209e8c21706', submissionId: '2b5e54b9-f03c-418b-92f3-5f072b0f3bf6', challengeId: '30104644', @@ -74,25 +75,25 @@ describe('Topcoder - Leaderboard API Unit Tests', () => { it('search leaderboard with groupId 100 success', async () => { const result = await service.searchLeaderboards({ groupId: '100' }) expect(result.length).to.equal(3) - expect(result[0]).to.deep.equal({ - numberOfChallenges: 2, - finalAggregationScore: 140, - totalTests: 16, - totalTestsPassed: 13, - memberId: '123458', - memberHandle: 'user3' - }) - expect(result[1]).to.deep.equal({ + expect(result[0]).to.contain({ numberOfChallenges: 3, - finalAggregationScore: 110, + finalAggregationScore: 330, totalTests: 36, totalTestsPassed: 22, memberId: '123456', memberHandle: 'user1' }) - expect(result[2]).to.deep.equal({ + expect(result[1]).to.contain({ + numberOfChallenges: 2, + finalAggregationScore: 280, + totalTests: 16, + totalTestsPassed: 13, + memberId: '123458', + memberHandle: 'user3' + }) + expect(result[2]).to.contain({ numberOfChallenges: 2, - finalAggregationScore: 70, + finalAggregationScore: 140, totalTests: 30, totalTestsPassed: 19, memberId: '123457', @@ -103,13 +104,13 @@ describe('Topcoder - Leaderboard API Unit Tests', () => { it('search leaderboard with groupId 200 success', async () => { const result = await service.searchLeaderboards({ groupId: '200', perPage: 1 }) expect(result.length).to.equal(1) - expect(result[0]).to.deep.equal({ - numberOfChallenges: 1, - finalAggregationScore: 80, - totalTests: 10, - totalTestsPassed: 8, - memberId: '123458', - memberHandle: 'user3' + expect(result[0]).to.contain({ + numberOfChallenges: 2, + finalAggregationScore: 140, + totalTests: 30, + totalTestsPassed: 18, + memberId: '123456', + memberHandle: 'user1' }) }) @@ -127,7 +128,7 @@ describe('Topcoder - Leaderboard API Unit Tests', () => { await service.searchLeaderboards({ challengeId: '1', groupId: '1' }) throw new Error('should not throw error here') } catch (err) { - expect(err.message).to.equal(`You can't filter the result using both challengeId and groupId filter.`) + expect(err.message).to.equal('You can\'t filter the result using both challengeId and groupId filter.') } }) }) @@ -139,12 +140,13 @@ describe('Topcoder - Leaderboard API Unit Tests', () => { submissionId: '261d3655-9c80-4f90-8051-e209e8c21701', score: 0 }) - expect(_.omit(result._doc, ['__v', '_id'])).to.deep.equal({ - groupIds: [ '20000000' ], + expect(result.toJSON()).to.deep.equal({ + groupIds: ['20000000'], reviewId: '161d3655-9c80-4f90-8051-e209e8c21701', submissionId: '261d3655-9c80-4f90-8051-e209e8c21701', memberId: '8547899', challengeId: '30051825', + scoreLevel: '', handle: 'TonyJ', aggregateScore: 0, testsPassed: 0, @@ -166,12 +168,13 @@ describe('Topcoder - Leaderboard API Unit Tests', () => { score: 90 }) - expect(_.omit(result._doc, ['__v', '_id'])).to.deep.equal({ - groupIds: [ '202343', '20000000' ], + expect(result.toJSON()).to.deep.equal({ + groupIds: ['202343', '20000000'], reviewId: '161d3655-9c80-4f90-8051-e209e8c21702', submissionId: '261d3655-9c80-4f90-8051-e209e8c21702', memberId: '8547899', challengeId: '30051826', + scoreLevel: '', handle: 'TonyJ', aggregateScore: 90, testsPassed: 9, @@ -192,12 +195,13 @@ describe('Topcoder - Leaderboard API Unit Tests', () => { score: 0 }) - expect(_.omit(result._doc, ['__v', '_id'])).to.deep.equal({ - groupIds: [ '20000000' ], + expect(result.toJSON()).to.deep.equal({ + groupIds: ['20000000'], reviewId: '161d3655-9c80-4f90-8051-e209e8c21703', submissionId: '261d3655-9c80-4f90-8051-e209e8c21703', memberId: '22688726', challengeId: '30051825', + scoreLevel: '', handle: 'vasyl', aggregateScore: 0, testsPassed: 0, @@ -213,12 +217,13 @@ describe('Topcoder - Leaderboard API Unit Tests', () => { score: 0 }) - expect(_.omit(result._doc, ['__v', '_id'])).to.deep.equal({ - groupIds: [ '202343', '20000000' ], + expect(result.toJSON()).to.deep.equal({ + groupIds: ['202343', '20000000'], reviewId: '161d3655-9c80-4f90-8051-e209e8c21704', submissionId: '261d3655-9c80-4f90-8051-e209e8c21704', memberId: '22688726', challengeId: '30051826', + scoreLevel: '', handle: 'vasyl', aggregateScore: 0, testsPassed: 0, @@ -235,7 +240,7 @@ describe('Topcoder - Leaderboard API Unit Tests', () => { }) throw new Error('should not throw error here') } catch (err) { - expect(err.message).to.equal(`Challenge # 30000001 doesn't exist`) + expect(err.message).to.equal('Challenge # 30000001 doesn\'t exist') } }) @@ -248,7 +253,7 @@ describe('Topcoder - Leaderboard API Unit Tests', () => { }) throw new Error('should not throw error here') } catch (err) { - expect(err.message).to.equal(`Member # 10000 doesn't exist`) + expect(err.message).to.equal('Member # 10000 doesn\'t exist') } }) @@ -261,7 +266,7 @@ describe('Topcoder - Leaderboard API Unit Tests', () => { }) throw new Error('should not throw error here') } catch (err) { - expect(err.message).to.equal(`Leaderboard record with challenge # 30051825 and member # 8547899 already exists.`) + expect(err.message).to.equal('Leaderboard record with challenge # 30051825 and member # 8547899 already exists.') } }) @@ -307,7 +312,7 @@ describe('Topcoder - Leaderboard API Unit Tests', () => { submissionId: '261d3655-9c80-4f90-8051-e209e8c21707', score: 50 }) - expect(debugLogs[3]).to.equal('Group ID ([30000]) of Challenge # 31000000 is not in the configured set of Ids (202343,20000000) configured for processing!') + expect(debugLogs[3]).to.equal('Group ID (["30000"]) of Challenge # 31000000 is not in the approved list. Ignoring request') }) }) @@ -324,8 +329,9 @@ describe('Topcoder - Leaderboard API Unit Tests', () => { }, score: 80 }) - expect(_.omit(result._doc, ['__v', '_id'])).to.deep.equal({ - groupIds: [ '20000000' ], + const resultAsJSON = result.toJSON() + expect(resultAsJSON).to.deep.contain({ + groupIds: ['20000000'], reviewId: '361d3655-9c80-4f90-8051-e209e8c21701', submissionId: '261d3655-9c80-4f90-8051-e209e8c21701', memberId: '8547899', @@ -336,6 +342,7 @@ describe('Topcoder - Leaderboard API Unit Tests', () => { totalTestCases: 5, scoreLevel: 'up' }) + expect(_.isNumber(resultAsJSON.scoreResetTime)).to.equal(true) }) it('failure - update leaderboard not found', async () => { @@ -346,7 +353,7 @@ describe('Topcoder - Leaderboard API Unit Tests', () => { }) throw new Error('should not throw error here') } catch (err) { - expect(err.message).to.equal(`Leaderboard record with challenge # 30051825 and member # 5547899 doesn't exist`) + expect(err.message).to.equal('Leaderboard record with challenge # 30051825 and member # 5547899 doesn\'t exist') } }) @@ -378,7 +385,7 @@ describe('Topcoder - Leaderboard API Unit Tests', () => { it('delete leaderboard success', async () => { await service.deleteLeaderboard(id) - const result = await Leaderboard.find({ reviewId: id }) + const result = await Leaderboard.query({ reviewId: id }).exec() expect(result.length).to.equal(0) }) @@ -400,4 +407,48 @@ describe('Topcoder - Leaderboard API Unit Tests', () => { } }) }) + + describe('search groups test', async () => { + it('search groups success', async () => { + const result = await GroupService.searchGroups() + expect(result).to.eql(['202343', '100', '20000000', '200']) + }) + }) + + describe('create group test', async () => { + it('create group success', async () => { + const groupId = '300' + const result = await GroupService.createGroup(groupId) + expect(result.groupId).to.equal(groupId) + const groups = await GroupService.searchGroups() + expect(groups).to.contain(groupId) + }) + it('failure - group wth specified groupId already exist', async () => { + const groupId = '300' + try { + await GroupService.createGroup(groupId) + throw new Error('should not throw error here') + } catch (err) { + expect(err.message).to.equal(`groupId # ${groupId} already exists.`) + } + }) + }) + + describe('delete group test', async () => { + it('delete group success', async () => { + const groupId = '300' + await GroupService.deleteGroup(groupId) + const groups = await GroupService.searchGroups() + expect(groups).to.not.contain(groupId) + }) + it('failure - group with specified groupId does not exist', async () => { + const groupId = '300' + try { + await GroupService.deleteGroup(groupId) + throw new Error('should not throw error here') + } catch (err) { + expect(err.message).to.equal(`groupId # ${groupId} doesn't exist`) + } + }) + }) })