diff --git a/README.md b/README.md index 23aa6953..3cf874bf 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ - nodejs https://nodejs.org/en/ (v10) - DynamoDB - AWS S3 +- Elasticsearch v6 - Docker, Docker Compose ## Configuration @@ -52,9 +53,20 @@ Set the following environment variables so that the app can get TC M2M token (us Also properly configure AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, ATTACHMENT_S3_BUCKET, IS_LOCAL_DB config parameters. -## DynamoDB Setup -We can use DynamoDB setup on Docker for testing purpose. Just run `docker-compose up` in `local` folder. -You can also use your own AWS DynamoDB service for testing purpose. +Test configuration is at `config/test.js`. You don't need to change them. +The following test parameters can be set in config file or in env variables: + +- ADMIN_TOKEN: admin token +- COPILOT_TOKEN: copilot token +- USER_TOKEN: user token +- EXPIRED_TOKEN: expired token +- INVALID_TOKEN: invalid token +- M2M_FULL_ACCESS_TOKEN: M2M full access token +- M2M_READ_ACCESS_TOKEN: M2M read access token +- M2M_UPDATE_ACCESS_TOKEN: M2M update (including 'delete') access token + +## Local Elasticsearch and DynamoDB setup +In the `local` folder, run `docker-compose up` ## AWS S3 Setup Go to https://console.aws.amazon.com/ and login. Choose S3 from Service folder and click `Create bucket`. Following the instruction to create S3 bucket. @@ -74,6 +86,7 @@ Go to `mock-api` folder and run command `npm run start` to start the mock-api li 3. Seed/Insert data to tables: `npm run seed-tables` 4. Initialize/Clear database in default environment: `npm run init-db` 5. View table data in default environment: `npm run view-data `, ModelName can be `Challenge`, `ChallengeType`, `ChallengeSetting`, `AuditLog`, `Phase`, `TimelineTemplate`or `Attachment` +6. Create Elasticsearch index: `npm run init-db`, or to re-create index: `npm run init-db force` ### Notes - The seed data are located in `src/scripts/seed` @@ -83,12 +96,48 @@ Go to `mock-api` folder and run command `npm run start` to start the mock-api li - Install dependencies `npm install` - Run lint `npm run lint` - Run lint fix `npm run lint:fix` +- initialize Elasticsearch, create configured Elasticsearch index if not present: `npm run init-es`, + or re-create the index: `npm run init-es force` - Create tables `npm run create-tables` - Clear and init db `npm run init-db` - Start app `npm start` - App is running at `http://localhost:3000` - Start mock-api, go to `mock-api` folder and `npm start`, mock api is running at `http://localhost:4000` +## Running tests + +Before running tests, DynamoDB tables should be created, ES index should be initialized, mock API should be started and +various config parameters are properly set. +Seeding db data is not needed. + +### Running unit tests + +To run unit tests alone + +```bash +npm run test +``` + +To run unit tests with coverage report + +```bash +npm run test:cov +``` + +### Running integration tests + +To run integration tests alone + +```bash +npm run e2e +``` + +To run integration tests with coverage report + +```bash +npm run e2e:cov +``` + ## Verification Refer to the verification document `Verification.md` diff --git a/app.js b/app.js index 3ea8eaee..2201a65d 100644 --- a/app.js +++ b/app.js @@ -87,3 +87,5 @@ app.use((err, req, res, next) => { app.listen(app.get('port'), () => { logger.info(`Express server listening on port ${app.get('port')}`) }) + +module.exports = app diff --git a/config/default.js b/config/default.js index 68e92d10..13f7ccce 100644 --- a/config/default.js +++ b/config/default.js @@ -33,7 +33,7 @@ module.exports = { ES: { // above AWS_REGION is used if we use AWS ES HOST: process.env.ES_HOST || 'localhost:9200', - API_VERSION: process.env.ES_API_VERSION || '6.3', + API_VERSION: process.env.ES_API_VERSION || '6.7', ES_INDEX: process.env.ES_INDEX || 'challenge', ES_TYPE: process.env.ES_TYPE || '_doc' // ES 6.x accepts only 1 Type per index and it's mandatory to define it }, diff --git a/config/test.js b/config/test.js new file mode 100644 index 00000000..d6b57e43 --- /dev/null +++ b/config/test.js @@ -0,0 +1,14 @@ +/** + * Configuration file to be used while running tests + */ + +module.exports = { + ADMIN_TOKEN: process.env.ADMIN_TOKEN || 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBTdXBwb3J0IiwiYWRtaW5pc3RyYXRvciIsInRlc3RSb2xlIiwiYWFhIiwidG9ueV90ZXN0XzEiLCJDb25uZWN0IE1hbmFnZXIiLCJDb25uZWN0IEFkbWluIiwiY29waWxvdCIsIkNvbm5lY3QgQ29waWxvdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJUb255SiIsImV4cCI6MTU4MTc5MjIxMSwidXNlcklkIjoiODU0Nzg5OSIsImlhdCI6MTU0OTc5MTYxMSwiZW1haWwiOiJ0amVmdHMrZml4QHRvcGNvZGVyLmNvbSIsImp0aSI6ImY5NGQxZTI2LTNkMGUtNDZjYS04MTE1LTg3NTQ1NDRhMDhmMSJ9.3nxk6c9P1GBWQ__XPsouddjXHAA3s_7t4E83tbFSFCA', + COPILOT_TOKEN: process.env.COPILOT_TOKEN || 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJjb3BpbG90Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJob2hvc2t5IiwiZXhwIjoxNTgxNzkyMzcwLCJ1c2VySWQiOiIxNjA5NjgyMyIsImlhdCI6MTU0OTc5MTc3MCwiZW1haWwiOiJlbWFpbEBkb21haW4uY29tLnoiLCJqdGkiOiJmMWU2MTNiZS1kNWI5LTQyMzEtYmFhZS1lZTlmMmQyMjcyMzQifQ.C58nUJ2rb0E5fI3xL34W_qrw7zldrMw4YMjcNA5CuZc', + USER_TOKEN: process.env.USER_TOKEN || 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJkZW5pcyIsImV4cCI6MTU4MjgwMDE2OSwidXNlcklkIjoiMjUxMjgwIiwiaWF0IjoxNTQ5Nzk5NTY5LCJlbWFpbCI6ImVtYWlsQGRvbWFpbi5jb20ueiIsImp0aSI6IjljNDUxMWM1LWMxNjUtNGExYi04OTllLWI2NWFkMGUwMmI1NSJ9.rYOYAZaM9P8c4jwbn7tC4YTSvs0MfO_5ZCMseJ-aqsM', + EXPIRED_TOKEN: process.env.EXPIRED_TOKEN || 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJjb3BpbG90IiwiQ29ubmVjdCBTdXBwb3J0Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJHaG9zdGFyIiwiZXhwIjoxNTQ5ODAwMDc3LCJ1c2VySWQiOiIxNTE3NDMiLCJpYXQiOjE1NDk3OTk0NzcsImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiMTJjMWMxMGItOTNlZi00NTMxLTgzMDUtYmE2NjVmYzRlMWI0In0.2n8k9pb16sE7LOLF_7mjAvEVKgggzS-wS3_8n2-R4RU', + INVALID_TOKEN: process.env.INVALID_TOKEN || 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJBZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6IlRvbnlKIiwiZXhwIjo1NTUzMDE5OTI1OSwidXNlcklkIjoiNDA0MzMyODgiLCJpYXQiOjE1MzAxOTg2NTksImVtYWlsIjoiYWRtaW5AdG9wY29kZXIuY29tIiwianRpIjoiYzNhYzYwOGEtNTZiZS00NWQwLThmNmEtMzFmZTk0Yjk1NjFjIn0.ePREgnJrBixP4URf1dd8FHISN2_6eRM5gjCReS0ZMK4', + M2M_FULL_ACCESS_TOKEN: process.env.M2M_FULL_ACCESS_TOKEN || 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjE1ODA5OTI3ODgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJhbGw6Y2hhbGxlbmdlcyBhbGw6Y2hhbGxlbmdlX3R5cGVzIGFsbDpjaGFsbGVuZ2Vfc2V0dGluZ3MgcmVhZDpjaGFsbGVuZ2VfYXVkaXRfbG9ncyBhbGw6Y2hhbGxlbmdlX3BoYXNlcyBhbGw6dGltZWxpbmVfdGVtcGxhdGVzIGFsbDpjaGFsbGVuZ2VfYXR0YWNobWVudHMiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.e8gjSvr6hYEkf5zGWXRzZUNOspKtRWWocK6YIxFtatg', + M2M_READ_ACCESS_TOKEN: process.env.M2M_READ_ACCESS_TOKEN || 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjE1ODA5OTI3ODgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJyZWFkOmNoYWxsZW5nZXMgcmVhZDpjaGFsbGVuZ2VfdHlwZXMgcmVhZDpjaGFsbGVuZ2Vfc2V0dGluZ3MgcmVhZDpjaGFsbGVuZ2VfYXVkaXRfbG9ncyByZWFkOmNoYWxsZW5nZV9waGFzZXMgcmVhZDp0aW1lbGluZV90ZW1wbGF0ZXMgcmVhZDpjaGFsbGVuZ2VfYXR0YWNobWVudHMiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.nBBSvMriaqOqaCVziGzllImDOd-os2SvvOSgH1SHby4', + M2M_UPDATE_ACCESS_TOKEN: process.env.M2M_UPDATE_ACCESS_TOKEN || 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUwOTA2Mzg4LCJleHAiOjE1ODA5OTI3ODgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJjcmVhdGU6Y2hhbGxlbmdlcyB1cGRhdGU6Y2hhbGxlbmdlcyBjcmVhdGU6Y2hhbGxlbmdlX3R5cGVzIHVwZGF0ZTpjaGFsbGVuZ2VfdHlwZXMgY3JlYXRlOmNoYWxsZW5nZV9zZXR0aW5ncyB1cGRhdGU6Y2hhbGxlbmdlX3NldHRpbmdzIGNyZWF0ZTpjaGFsbGVuZ2VfcGhhc2VzIHVwZGF0ZTpjaGFsbGVuZ2VfcGhhc2VzIGRlbGV0ZTpjaGFsbGVuZ2VfcGhhc2VzIGNyZWF0ZTp0aW1lbGluZV90ZW1wbGF0ZXMgdXBkYXRlOnRpbWVsaW5lX3RlbXBsYXRlcyBkZWxldGU6dGltZWxpbmVfdGVtcGxhdGVzIGNyZWF0ZTpjaGFsbGVuZ2VfYXR0YWNobWVudHMiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.o9NhSZQKh_lnVdfBih_26BxIz-Xz9XzYK_jo4gYV7lY' +} diff --git a/docs/topcoder-challenge-api.postman_environment.json b/docs/topcoder-challenge-api.postman_environment.json index 7ab5960e..ecef0136 100644 --- a/docs/topcoder-challenge-api.postman_environment.json +++ b/docs/topcoder-challenge-api.postman_environment.json @@ -9,22 +9,22 @@ }, { "key": "user_token", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJkZW5pcyIsImV4cCI6MTU2MjgwMDE2OSwidXNlcklkIjoiMjUxMjgwIiwiaWF0IjoxNTQ5Nzk5NTY5LCJlbWFpbCI6ImVtYWlsQGRvbWFpbi5jb20ueiIsImp0aSI6IjljNDUxMWM1LWMxNjUtNGExYi04OTllLWI2NWFkMGUwMmI1NSJ9.a5-oBMwFtwGkSw2161y0lEu1XvKsKElCmRu6e8Q6PPk", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJkZW5pcyIsImV4cCI6MTU4MjgwMDE2OSwidXNlcklkIjoiMjUxMjgwIiwiaWF0IjoxNTQ5Nzk5NTY5LCJlbWFpbCI6ImVtYWlsQGRvbWFpbi5jb20ueiIsImp0aSI6IjljNDUxMWM1LWMxNjUtNGExYi04OTllLWI2NWFkMGUwMmI1NSJ9.rYOYAZaM9P8c4jwbn7tC4YTSvs0MfO_5ZCMseJ-aqsM", "enabled": true }, { "key": "copilot1_token", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJjb3BpbG90IiwiQ29ubmVjdCBTdXBwb3J0Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJHaG9zdGFyIiwiZXhwIjoxNTYyODAwMDc3LCJ1c2VySWQiOiIxNTE3NDMiLCJpYXQiOjE1NDk3OTk0NzcsImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiMTJjMWMxMGItOTNlZi00NTMxLTgzMDUtYmE2NjVmYzRlMWI0In0.TgxNCChFrM6QhKYFyXkd6FWNg_XRC-0aWCW0nM3Z9mE", + "value": "eeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJjb3BpbG90IiwiQ29ubmVjdCBTdXBwb3J0Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJHaG9zdGFyIiwiZXhwIjoxNTgyODAwMDc3LCJ1c2VySWQiOiIxNTE3NDMiLCJpYXQiOjE1NDk3OTk0NzcsImVtYWlsIjoiZW1haWxAZG9tYWluLmNvbS56IiwianRpIjoiMTJjMWMxMGItOTNlZi00NTMxLTgzMDUtYmE2NjVmYzRlMWI0In0.Y3SsmT-C21ahWrQQgd2SALDBgC_4qKyrWXesc2cB1Ys", "enabled": true }, { "key": "copilot2_token", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJjb3BpbG90Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJob2hvc2t5IiwiZXhwIjoxNTYxNzkyMzcwLCJ1c2VySWQiOiIxNjA5NjgyMyIsImlhdCI6MTU0OTc5MTc3MCwiZW1haWwiOiJlbWFpbEBkb21haW4uY29tLnoiLCJqdGkiOiJmMWU2MTNiZS1kNWI5LTQyMzEtYmFhZS1lZTlmMmQyMjcyMzQifQ._ehIlaqxU5AdEdt2IFsYrulT40msSSV5j8gNuQaWwgQ", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJjb3BpbG90Il0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJob2hvc2t5IiwiZXhwIjoxNTgxNzkyMzcwLCJ1c2VySWQiOiIxNjA5NjgyMyIsImlhdCI6MTU0OTc5MTc3MCwiZW1haWwiOiJlbWFpbEBkb21haW4uY29tLnoiLCJqdGkiOiJmMWU2MTNiZS1kNWI5LTQyMzEtYmFhZS1lZTlmMmQyMjcyMzQifQ.C58nUJ2rb0E5fI3xL34W_qrw7zldrMw4YMjcNA5CuZc", "enabled": true }, { "key": "admin_token", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBTdXBwb3J0IiwiYWRtaW5pc3RyYXRvciIsInRlc3RSb2xlIiwiYWFhIiwidG9ueV90ZXN0XzEiLCJDb25uZWN0IE1hbmFnZXIiLCJDb25uZWN0IEFkbWluIiwiY29waWxvdCIsIkNvbm5lY3QgQ29waWxvdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJUb255SiIsImV4cCI6MTU2MTc5MjIxMSwidXNlcklkIjoiODU0Nzg5OSIsImlhdCI6MTU0OTc5MTYxMSwiZW1haWwiOiJ0amVmdHMrZml4QHRvcGNvZGVyLmNvbSIsImp0aSI6ImY5NGQxZTI2LTNkMGUtNDZjYS04MTE1LTg3NTQ1NDRhMDhmMSJ9.o8VQsaYepIZmgBNuVuU7K7HWnqcPWJSnd8p88SqDgQU", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiQ29ubmVjdCBTdXBwb3J0IiwiYWRtaW5pc3RyYXRvciIsInRlc3RSb2xlIiwiYWFhIiwidG9ueV90ZXN0XzEiLCJDb25uZWN0IE1hbmFnZXIiLCJDb25uZWN0IEFkbWluIiwiY29waWxvdCIsIkNvbm5lY3QgQ29waWxvdCBNYW5hZ2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJUb255SiIsImV4cCI6MTU4MTc5MjIxMSwidXNlcklkIjoiODU0Nzg5OSIsImlhdCI6MTU0OTc5MTYxMSwiZW1haWwiOiJ0amVmdHMrZml4QHRvcGNvZGVyLmNvbSIsImp0aSI6ImY5NGQxZTI2LTNkMGUtNDZjYS04MTE1LTg3NTQ1NDRhMDhmMSJ9.3nxk6c9P1GBWQ__XPsouddjXHAA3s_7t4E83tbFSFCA", "enabled": true }, { @@ -54,7 +54,7 @@ }, { "key": "m2m_token", - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUyMTE5NDQ4LCJleHAiOjE1NjIyMDU4NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJ1cGRhdGU6dXNlcl9wcm9maWxlcyBhbGw6d3JpdGU6dXNlcl9wcm9maWxlcyB3cml0ZTp1c2VyX3Byb2ZpbGVzIHJlYWQ6Y2hhbGxlbmdlcyByZWFkOmdyb3VwcyB1cGRhdGU6c3VibWlzc2lvbiByZWFkOnN1Ym1pc3Npb24gY3JlYXRlOnN1Ym1pc3Npb24gcmVhZDpyZXZpZXdfdHlwZSB1cGRhdGU6cmV2aWV3X3N1bW1hdGlvbiByZWFkOnJldmlld19zdW1tYXRpb24gZGVsZXRlOnJldmlld19zdW1tYXRpb24gY3JlYXRlOnJldmlld19zdW1tYXRpb24gYWxsOnJldmlld19zdW1tYXRpb24gdXBkYXRlOnJldmlldyByZWFkOnJldmlldyBkZWxldGU6cmV2aWV3IGNyZWF0ZTpyZXZpZXcgYWxsOnJldmlldyB3cml0ZTpidXNfYXBpIHJlYWQ6dXNlcl9wcm9maWxlcyByZWFkOnJvbGVzIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.FBnnL5MKwDQXyliCwKsVVAootakpcO6VHwMTOl44nF0", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3RvcGNvZGVyLWRldi5hdXRoMC5jb20vIiwic3ViIjoiZW5qdzE4MTBlRHozWFR3U08yUm4yWTljUVRyc3BuM0JAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vbTJtLnRvcGNvZGVyLWRldi5jb20vIiwiaWF0IjoxNTUyMTE5NDQ4LCJleHAiOjE1ODIyMDU4NDgsImF6cCI6ImVuancxODEwZUR6M1hUd1NPMlJuMlk5Y1FUcnNwbjNCIiwic2NvcGUiOiJ1cGRhdGU6dXNlcl9wcm9maWxlcyBhbGw6d3JpdGU6dXNlcl9wcm9maWxlcyB3cml0ZTp1c2VyX3Byb2ZpbGVzIHJlYWQ6Y2hhbGxlbmdlcyByZWFkOmdyb3VwcyB1cGRhdGU6c3VibWlzc2lvbiByZWFkOnN1Ym1pc3Npb24gY3JlYXRlOnN1Ym1pc3Npb24gcmVhZDpyZXZpZXdfdHlwZSB1cGRhdGU6cmV2aWV3X3N1bW1hdGlvbiByZWFkOnJldmlld19zdW1tYXRpb24gZGVsZXRlOnJldmlld19zdW1tYXRpb24gY3JlYXRlOnJldmlld19zdW1tYXRpb24gYWxsOnJldmlld19zdW1tYXRpb24gdXBkYXRlOnJldmlldyByZWFkOnJldmlldyBkZWxldGU6cmV2aWV3IGNyZWF0ZTpyZXZpZXcgYWxsOnJldmlldyB3cml0ZTpidXNfYXBpIHJlYWQ6dXNlcl9wcm9maWxlcyByZWFkOnJvbGVzIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.DA9HQvYY-bye_4KfAfJTQVAusbGFNXd7ab2p55KZeh8", "enabled": true }, { diff --git a/local/docker-compose.yml b/local/docker-compose.yml index 501f1973..879acb2d 100644 --- a/local/docker-compose.yml +++ b/local/docker-compose.yml @@ -5,3 +5,7 @@ services: ports: - "7777:7777" command: "-inMemory -port 7777" + esearch: + image: "docker.elastic.co/elasticsearch/elasticsearch:6.3.1" + ports: + - "9200:9200" diff --git a/package-lock.json b/package-lock.json index e37e73f4..fd227374 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,11 +4,188 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/generator": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz", + "integrity": "sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==", + "dev": true, + "requires": { + "@babel/types": "^7.4.4", + "jsesc": "^2.5.1", + "lodash": "^4.17.11", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + } + }, + "@babel/helper-function-name": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", + "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", + "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", + "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", + "dev": true, + "requires": { + "@babel/types": "^7.4.4" + } + }, + "@babel/highlight": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.5.tgz", + "integrity": "sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew==", + "dev": true + }, + "@babel/template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", + "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, + "@babel/traverse": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.5.tgz", + "integrity": "sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.4.4", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/parser": "^7.4.5", + "@babel/types": "^7.4.4", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.11" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz", + "integrity": "sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.11", + "to-fast-properties": "^2.0.0" + } + }, + "@types/chai": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", + "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", + "dev": true + }, + "@types/cookiejar": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz", + "integrity": "sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw==", + "dev": true + }, "@types/node": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.0.tgz", "integrity": "sha512-D5Rt+HXgEywr3RQJcGlZUCTCx1qVbCZpVk3/tOOA6spLNZdGm8BU+zRgdRYDoF1pO3RuXLxADzMrF903JlQXqg==" }, + "@types/superagent": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.8.7.tgz", + "integrity": "sha512-9KhCkyXv268A2nZ1Wvu7rQWM+BmdYUVkycFeNnYrUL5Zwu7o8wPQ3wBfW59dDP+wuoxw0ww8YKgTNv8j/cgscA==", + "dev": true, + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -55,6 +232,12 @@ "integrity": "sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw==", "dev": true }, + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "dev": true + }, "ansi-escapes": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", @@ -71,6 +254,21 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" }, + "append-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "dev": true, + "requires": { + "default-require-extensions": "^2.0.0" + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -108,6 +306,12 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", @@ -122,23 +326,23 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "auth0-js": { - "version": "9.10.4", - "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.10.4.tgz", - "integrity": "sha512-FKwwA0E9NDrn/MxCzeftA5wx6YCos9wINhuvXeFHDdx+Lis7ykR46kBXJF4+dYjDdC5QhQ7W0cAp6bBS5gS75Q==", + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.11.0.tgz", + "integrity": "sha512-a2aIys52TGIktSXUaRmajogCuqww1hthLGG9/1KpuLN27CLimwEjcHt3paTZdJBLdyqIxYzBPcxFqqUe/YgOWQ==", "requires": { "base64-js": "^1.2.0", - "idtoken-verifier": "^1.2.0", + "idtoken-verifier": "^1.4.0", "js-cookie": "^2.2.0", "qs": "^6.4.0", - "superagent": "^3.8.2", + "superagent": "^3.8.3", "url-join": "^4.0.0", "winchan": "^0.2.1" } }, "aws-sdk": { - "version": "2.476.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.476.0.tgz", - "integrity": "sha512-GBvp/VjKu5YAiwzLVyfUajq43BXBvxhNUz6D27mG/ttceeFdQxMkZbAHSk36vH7rtECGIgRTw5ok8BoHVCTGuw==", + "version": "2.483.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.483.0.tgz", + "integrity": "sha512-tMyteyFYgyDHecXpGod2DVXlrE+91w+PGfVVF8hmL3eS3ICia8SJooLySW2dXIRpgk6Nfx5/YCGOR/tGIFb2mA==", "requires": { "buffer": "4.9.1", "events": "1.1.1", @@ -179,6 +383,14 @@ "chalk": "^1.1.3", "esutils": "^2.0.2", "js-tokens": "^3.0.2" + }, + "dependencies": { + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + } } }, "babel-runtime": { @@ -256,6 +468,12 @@ "concat-map": "0.0.1" } }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, "buffer": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", @@ -296,6 +514,18 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, + "caching-transform": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-3.0.2.tgz", + "integrity": "sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w==", + "dev": true, + "requires": { + "hasha": "^3.0.0", + "make-dir": "^2.0.0", + "package-hash": "^3.0.0", + "write-file-atomic": "^2.4.2" + } + }, "caller-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", @@ -311,11 +541,46 @@ "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", "dev": true }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chai-http": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/chai-http/-/chai-http-4.3.0.tgz", + "integrity": "sha512-zFTxlN7HLMv+7+SPXZdkd5wUlK+KxH6Q7bIEMiEx0FK3zuuMqL7cwICAQ0V1+yYRozBburYuxN1qZstgHpFZQg==", + "dev": true, + "requires": { + "@types/chai": "4", + "@types/superagent": "^3.8.3", + "cookiejar": "^2.1.1", + "is-ip": "^2.0.0", + "methods": "^1.1.2", + "qs": "^6.5.1", + "superagent": "^3.7.0" + } + }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", @@ -334,6 +599,12 @@ "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", "dev": true }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, "circular-json": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", @@ -355,6 +626,40 @@ "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", "dev": true }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, "codependency": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/codependency/-/codependency-0.1.4.tgz", @@ -428,6 +733,19 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "dev": true, + "optional": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -465,6 +783,15 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, "cookie": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", @@ -499,6 +826,19 @@ "vary": "^1" } }, + "cp-file": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-6.2.0.tgz", + "integrity": "sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "make-dir": "^2.0.0", + "nested-error-stacks": "^2.0.0", + "pify": "^4.0.1", + "safe-buffer": "^5.0.1" + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -539,6 +879,21 @@ "integrity": "sha1-IwdjLUwEOCuN+KMvcLiVBG1SdF8=", "dev": true }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, "deep-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", @@ -550,6 +905,15 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "default-require-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "dev": true, + "requires": { + "strip-bom": "^3.0.0" + } + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -629,6 +993,12 @@ "streamsearch": "0.1.2" } }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, "doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -648,9 +1018,9 @@ } }, "dynamoose": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/dynamoose/-/dynamoose-1.8.0.tgz", - "integrity": "sha512-UHR4Mf3AY+u7XHvfYWtx8ClfKMCm4ZSJ6BAx1sVtKv2uDdnfTHqxIgkIuyYtwRuykzeHIeSSAUdP8PGM+RdcXw==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/dynamoose/-/dynamoose-1.8.5.tgz", + "integrity": "sha512-XBsecOXvZEtLWB02GaU3UAZp9xs2IJ/sehLhin0269RfzyPhq7Z9SWZaqcP/EV+Mqfgv4p3QO6527O259pK/JQ==", "requires": { "@types/node": "11.11.0", "aws-sdk": "2.395.0", @@ -715,15 +1085,21 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "elasticsearch": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/elasticsearch/-/elasticsearch-15.5.0.tgz", - "integrity": "sha512-ZGKKaDkOFAap61ObBNkAxhYXCcAbRfkI4NVoSeLGnTD6/cItvY2j9LII/VV8/zclGe1x5m6DsVp47E4ze4aAeQ==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/elasticsearch/-/elasticsearch-16.1.1.tgz", + "integrity": "sha512-OF2fIjcTPfq/4Tj6k4/SZr2IIlfWlBBQoy/em225mfevYFW1abN3nyXKWldXGV+eWI6LWNqB8lb3hAP4d6Rh/Q==", "requires": { "agentkeepalive": "^3.4.1", "chalk": "^1.0.0", "lodash": "^4.17.10" } }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, "enabled": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", @@ -737,6 +1113,15 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, "env-variable": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz", @@ -784,6 +1169,12 @@ "is-symbol": "^1.0.2" } }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, "es6-promise-polyfill": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/es6-promise-polyfill/-/es6-promise-polyfill-1.2.0.tgz", @@ -956,6 +1347,58 @@ "requires": { "ms": "2.0.0" } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } } } }, @@ -1005,24 +1448,124 @@ "esutils": "^2.0.2", "isarray": "^1.0.0" } - } - } - }, - "eslint-plugin-node": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-7.0.1.tgz", - "integrity": "sha512-lfVw3TEqThwq0j2Ba/Ckn2ABdwmL5dkOgAux1rvOk6CO7A6yGyPI2+zIxN6FyNkp1X1X/BSvKOceD6mBWSj4Yw==", - "dev": true, - "requires": { - "eslint-plugin-es": "^1.3.1", - "eslint-utils": "^1.3.1", - "ignore": "^4.0.2", - "minimatch": "^3.0.4", - "resolve": "^1.8.1", - "semver": "^5.5.0" - } - }, - "eslint-plugin-promise": { + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + } + } + }, + "eslint-plugin-node": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-7.0.1.tgz", + "integrity": "sha512-lfVw3TEqThwq0j2Ba/Ckn2ABdwmL5dkOgAux1rvOk6CO7A6yGyPI2+zIxN6FyNkp1X1X/BSvKOceD6mBWSj4Yw==", + "dev": true, + "requires": { + "eslint-plugin-es": "^1.3.1", + "eslint-utils": "^1.3.1", + "ignore": "^4.0.2", + "minimatch": "^3.0.4", + "resolve": "^1.8.1", + "semver": "^5.5.0" + } + }, + "eslint-plugin-promise": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.0.1.tgz", "integrity": "sha512-Si16O0+Hqz1gDHsys6RtFRrW7cCTB6P7p3OJmKp3Y3dxpQE2qwOA7d3xnV+0mBmrPoi0RBnxlCKvqu70te6wjg==", @@ -1126,6 +1669,21 @@ "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, "express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", @@ -1289,6 +1847,17 @@ } } }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, "find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -1296,12 +1865,21 @@ "dev": true }, "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", "dev": true, "requires": { - "locate-path": "^2.0.0" + "is-buffer": "~2.0.3" } }, "flat-cache": { @@ -1349,15 +1927,37 @@ "debug": "=3.1.0" } }, + "foreground-child": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", + "integrity": "sha1-T9ca0t/elnibmApcCilZN8svXOk=", + "dev": true, + "requires": { + "cross-spawn": "^4", + "signal-exit": "^3.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + } + } + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.4.0.tgz", + "integrity": "sha512-4FinE8RfqYnNim20xDwZZE0V2kOs/AuElIjFUbPuegQSaoZM+vUT5FnwSl10KPugH4voTg1bEQlcbCG9ka75TA==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -1397,6 +1997,18 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, "get-parameter-names": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/get-parameter-names/-/get-parameter-names-0.3.0.tgz", @@ -1408,6 +2020,15 @@ "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", "dev": true }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -1436,11 +2057,37 @@ "dev": true }, "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz", + "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, + "handlebars": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", + "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "dev": true, + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -1484,6 +2131,21 @@ "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", "dev": true }, + "hasha": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz", + "integrity": "sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk=", + "dev": true, + "requires": { + "is-stream": "^1.0.1" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, "hoek": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz", @@ -1549,9 +2211,9 @@ } }, "idtoken-verifier": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/idtoken-verifier/-/idtoken-verifier-1.3.1.tgz", - "integrity": "sha512-o0aplv9JqTuHz9jywi3fXXAHUWZ0nEWSjS1qBawLU74C+iqScORwBFXoac2zVoggE1hTaImikE8vALkZQu9I3Q==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/idtoken-verifier/-/idtoken-verifier-1.4.0.tgz", + "integrity": "sha512-644kKg9712hhEkyrRPncBGWKHd9pEh6b3r9DtkQ5zlqgoaX6YtsZGghNLXFVF+541MnwTLjFQqaDDmdQvT4qCQ==", "requires": { "base64-js": "^1.2.0", "crypto-js": "^3.1.9-1", @@ -1666,6 +2328,18 @@ } } }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, "ipaddr.js": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", @@ -1699,6 +2373,15 @@ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, + "is-ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-2.0.0.tgz", + "integrity": "sha1-aO6gfooKCpTC0IDdZ0xzGrKkYas=", + "dev": true, + "requires": { + "ip-regex": "^2.0.0" + } + }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", @@ -1779,6 +2462,134 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", + "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", + "dev": true, + "requires": { + "append-transform": "^1.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "requires": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.1.2.tgz", + "integrity": "sha512-z4PqiCpomGtWj8633oeAdXm1Kn1W++3T8epkZYnwiVgIYIJ0QHszhInYSJTYxebByQH7KVCEAn8R9duzZW2PhQ==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", + "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", + "dev": true, + "requires": { + "handlebars": "^4.1.2" + } + }, "jmespath": { "version": "0.15.0", "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", @@ -1800,9 +2611,9 @@ "integrity": "sha1-Gywnmm7s44ChIWi5JIUmWzWx7/s=" }, "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "js-yaml": { @@ -1820,6 +2631,12 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -1891,12 +2708,13 @@ } }, "jsx-ast-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.1.0.tgz", - "integrity": "sha512-yDGDG2DS4JcqhA6blsuYbtsT09xL8AoLuUR2Gb5exrw7UEM19sBcOTq+YBBhrNbl0PUC4R4LnFu+dHg2HKeVvA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.0.tgz", + "integrity": "sha512-yAmhGSzR7TsD0OQpu1AGLz8Bx84cxMqtgoJrufomY6BlveEDlREhvu1rea21936xbe5tlUh7IPda82m5ae0H8Q==", "dev": true, "requires": { - "array-includes": "^3.0.3" + "array-includes": "^3.0.3", + "object.assign": "^4.1.0" } }, "jwa": { @@ -1961,6 +2779,15 @@ "colornames": "^1.1.1" } }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, "le_node": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/le_node/-/le_node-1.8.0.tgz", @@ -1997,24 +2824,32 @@ "integrity": "sha512-XCpr5bElgDI65vVgstP8TWjv6/QKWm9GU5UG0Pr5sLQ3QLo8NVKsioe+Jed5/3vFOe3IQuqE7DKwTvKQkjTHvg==" }, "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", "dev": true, "requires": { "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", + "parse-json": "^4.0.0", + "pify": "^3.0.0", "strip-bom": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } } }, "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dev": true, "requires": { - "p-locate": "^2.0.0", + "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, @@ -2028,6 +2863,12 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, "lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2063,16 +2904,56 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, - "logform": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", - "integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==", + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, "requires": { - "colors": "^1.2.1", - "fast-safe-stringify": "^2.0.4", - "fecha": "^2.3.3", - "ms": "^2.1.1", - "triple-beam": "^1.3.0" + "chalk": "^2.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "logform": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", + "integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==", + "requires": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^2.3.3", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" }, "dependencies": { "ms": { @@ -2111,16 +2992,63 @@ "very-fast-args": "^1.1.0" } }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + } + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, + "merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -2150,9 +3078,9 @@ } }, "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, "minimatch": { @@ -2183,6 +3111,77 @@ } } }, + "mocha": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz", + "integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==", + "dev": true, + "requires": { + "ansi-colors": "3.2.3", + "browser-stdout": "1.3.1", + "debug": "3.2.6", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "find-up": "3.0.0", + "glob": "7.1.3", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "2.2.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "ms": "2.1.1", + "node-environment-flags": "1.0.5", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.2.2", + "yargs-parser": "13.0.0", + "yargs-unparser": "1.5.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "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==", + "dev": true + }, + "supports-color": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "moment": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", @@ -2234,12 +3233,34 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "nested-error-stacks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz", + "integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==", + "dev": true + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node-environment-flags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", + "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", + "dev": true, + "requires": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + } + }, "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", @@ -2257,6 +3278,79 @@ "validate-npm-package-license": "^3.0.1" } }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "nyc": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-14.1.1.tgz", + "integrity": "sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "caching-transform": "^3.0.2", + "convert-source-map": "^1.6.0", + "cp-file": "^6.2.0", + "find-cache-dir": "^2.1.0", + "find-up": "^3.0.0", + "foreground-child": "^1.5.6", + "glob": "^7.1.3", + "istanbul-lib-coverage": "^2.0.5", + "istanbul-lib-hook": "^2.0.7", + "istanbul-lib-instrument": "^3.3.0", + "istanbul-lib-report": "^2.0.8", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^2.2.4", + "js-yaml": "^3.13.1", + "make-dir": "^2.1.0", + "merge-source-map": "^1.1.0", + "resolve-from": "^4.0.0", + "rimraf": "^2.6.3", + "signal-exit": "^3.0.2", + "spawn-wrap": "^1.4.2", + "test-exclude": "^5.2.3", + "uuid": "^3.3.2", + "yargs": "^13.2.2", + "yargs-parser": "^13.0.0" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -2278,6 +3372,28 @@ "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.4.tgz", "integrity": "sha1-NwrnUvvzfePqcKhhwju6iRVpGUk=" }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -2306,6 +3422,32 @@ "dev": true, "requires": { "mimic-fn": "^1.0.0" + }, + "dependencies": { + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + } + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + } } }, "optionator": { @@ -2320,6 +3462,31 @@ "prelude-ls": "~1.1.2", "type-check": "~0.3.2", "wordwrap": "~1.0.0" + }, + "dependencies": { + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" } }, "os-tmpdir": { @@ -2328,37 +3495,68 @@ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true + }, "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", "dev": true, "requires": { - "p-try": "^1.0.0" + "p-try": "^2.0.0" } }, "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "dev": true, "requires": { - "p-limit": "^1.1.0" + "p-limit": "^2.0.0" } }, "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "package-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-3.0.0.tgz", + "integrity": "sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^3.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", "dev": true, "requires": { - "error-ex": "^1.2.0" + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" } }, "parseurl": { @@ -2401,23 +3599,37 @@ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", "dev": true, "requires": { - "pify": "^2.0.0" + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } } }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true }, "pkg-conf": { @@ -2430,32 +3642,47 @@ "load-json-file": "^4.0.0" }, "dependencies": { - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" + "locate-path": "^2.0.0" } }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", "dev": true, "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" } }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true } } @@ -2472,12 +3699,12 @@ } }, "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", "dev": true, "requires": { - "find-up": "^2.1.0" + "find-up": "^3.0.0" } }, "pluralize": { @@ -2498,9 +3725,9 @@ "dev": true }, "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "progress": { "version": "2.0.3", @@ -2534,9 +3761,19 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, "psl": { - "version": "1.1.32", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz", - "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g==" + "version": "1.1.33", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.33.tgz", + "integrity": "sha512-LTDP2uSrsc7XCb5lO7A8BI1qYxRe/8EqlRvMeEl6rsnYAqDOl8xHR+8lSAIVfrNaSAlTPTNOCgNjWcoUL3AZsw==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } }, "punycode": { "version": "1.3.2", @@ -2581,24 +3818,24 @@ "dev": true }, "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", "dev": true, "requires": { - "load-json-file": "^2.0.0", + "load-json-file": "^4.0.0", "normalize-package-data": "^2.3.2", - "path-type": "^2.0.0" + "path-type": "^3.0.0" } }, "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", "dev": true, "requires": { - "find-up": "^2.0.0", - "read-pkg": "^2.0.0" + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" } }, "readable-stream": { @@ -2633,6 +3870,15 @@ "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", "dev": true }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, "request": { "version": "2.88.0", "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", @@ -2660,6 +3906,16 @@ "uuid": "^3.3.2" }, "dependencies": { + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", @@ -2667,6 +3923,18 @@ } } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, "require-uncached": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", @@ -2675,21 +3943,29 @@ "requires": { "caller-path": "^0.1.0", "resolve-from": "^1.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + } } }, "resolve": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz", - "integrity": "sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", + "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", "dev": true, "requires": { "path-parse": "^1.0.6" } }, "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, "restore-cursor": { @@ -2814,6 +4090,12 @@ "send": "0.17.1" } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, "setprototypeof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", @@ -2857,6 +4139,51 @@ "is-fullwidth-code-point": "^2.0.0" } }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "spawn-wrap": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.2.tgz", + "integrity": "sha512-vMwR3OmmDhnxCVxM8M+xO/FtIp6Ju/mNaDfCMMW7FDcLRTPFWUswec4LXJHTJE2hwTI9O0YBfygu4DalFl7Ylg==", + "dev": true, + "requires": { + "foreground-child": "^1.5.6", + "mkdirp": "^0.5.0", + "os-homedir": "^1.0.1", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.2", + "which": "^1.3.0" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, "spdx-correct": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", @@ -3006,6 +4333,12 @@ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", "dev": true }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -3109,14 +4442,94 @@ } } }, - "tc-bus-api-wrapper": { - "version": "github:topcoder-platform/tc-bus-api-wrapper#a52f69ef72a80d27aeeab790fb18eb419a904862", + "tc-core-library-js": { + "version": "github:appirio-tech/tc-core-library-js#0d8b4dfc6a1cb0aa10a7ea1b90ed58ba5f0e11b0", + "from": "github:appirio-tech/tc-core-library-js#v2.6.2", + "requires": { + "auth0-js": "^9.4.2", + "axios": "^0.19.0", + "bunyan": "^1.8.12", + "jsonwebtoken": "^8.3.0", + "jwks-rsa": "^1.3.0", + "le_node": "^1.3.1", + "lodash": "^4.17.10", + "millisecond": "^0.1.2", + "request": "^2.88.0" + } + }, + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "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==" + }, + "topcoder-bus-api-wrapper": { + "version": "github:topcoder-platform/tc-bus-api-wrapper#520f1b541de3bffdd25a2eb597b1d4ad5ec78415", "from": "github:topcoder-platform/tc-bus-api-wrapper", "requires": { "joi": "^13.4.0", "lodash": "^4.17.10", "superagent": "^3.8.3", - "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6" + "tc-core-library-js": "github:appirio-tech/tc-core-library-js#d16413db30b1eed21c0cf426e185bedb2329ddab" }, "dependencies": { "axios": { @@ -3176,89 +4589,6 @@ } } }, - "tc-core-library-js": { - "version": "github:appirio-tech/tc-core-library-js#0d8b4dfc6a1cb0aa10a7ea1b90ed58ba5f0e11b0", - "from": "github:appirio-tech/tc-core-library-js#v2.6.2", - "requires": { - "auth0-js": "^9.4.2", - "axios": "^0.19.0", - "bunyan": "^1.8.12", - "jsonwebtoken": "^8.3.0", - "jwks-rsa": "^1.3.0", - "le_node": "^1.3.1", - "lodash": "^4.17.10", - "millisecond": "^0.1.2", - "request": "^2.88.0" - }, - "dependencies": { - "axios": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", - "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", - "requires": { - "follow-redirects": "1.5.10", - "is-buffer": "^2.0.2" - } - }, - "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" - } - }, - "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "requires": { - "debug": "=3.1.0" - } - }, - "is-buffer": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", - "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "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.3", "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz", @@ -3283,6 +4613,12 @@ } } }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, "triple-beam": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", @@ -3310,6 +4646,12 @@ "prelude-ls": "~1.1.2" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -3319,6 +4661,26 @@ "mime-types": "~2.1.24" } }, + "uglify-js": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", + "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "dev": true, + "optional": true, + "requires": { + "commander": "~2.20.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + } + } + }, "unfetch": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.1.0.tgz", @@ -3418,10 +4780,25 @@ "isexe": "^2.0.0" } }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, "winchan": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/winchan/-/winchan-0.2.1.tgz", - "integrity": "sha512-QrG9q+ObfmZBxScv0HSCqFm/owcgyR5Sgpiy1NlCZPpFXhbsmNHhTiLWoogItdBUi0fnU7Io/5ABEqRta5/6Dw==" + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/winchan/-/winchan-0.2.2.tgz", + "integrity": "sha512-pvN+IFAbRP74n/6mc6phNyCH8oVkzXsto4KCHPJ2AScniAnA1AmeLI03I2BzjePpaClGSI4GUMowzsD3qz5PRQ==" }, "winston": { "version": "3.2.1", @@ -3493,11 +4870,43 @@ } }, "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", "dev": true }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3512,6 +4921,17 @@ "mkdirp": "^0.5.1" } }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, "xml2js": { "version": "0.4.19", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", @@ -3532,10 +4952,128 @@ "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", "dev": true }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "yargs": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", + "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", + "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yargs-unparser": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", + "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", + "dev": true, + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.11", + "yargs": "^12.0.5" + }, + "dependencies": { + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } } } } diff --git a/package.json b/package.json index 2b35c1f0..945a4fae 100644 --- a/package.json +++ b/package.json @@ -7,15 +7,24 @@ "start": "node app.js", "lint": "standard", "lint:fix": "standard --fix", + "init-es": "node src/init-es.js", "init-db": "node src/init-db.js", "drop-tables": "node src/scripts/drop-tables.js", "create-tables": "node src/scripts/create-tables.js", "seed-tables": "node src/scripts/seed-tables.js", - "view-data": "node src/scripts/view-data.js" + "view-data": "node src/scripts/view-data.js", + "test": "mocha -t 20000 test/unit/*.test.js --exit", + "e2e": "mocha -t 20000 test/e2e/*.test.js --exit", + "test:cov": "nyc --reporter=html --reporter=text npm test", + "e2e:cov": "nyc --reporter=html --reporter=text npm run e2e" }, "author": "TCSCODER", "license": "none", "devDependencies": { + "chai": "^4.2.0", + "chai-http": "^4.2.1", + "mocha": "^6.1.4", + "nyc": "^14.0.0", "standard": "^12.0.1" }, "dependencies": { @@ -25,9 +34,9 @@ "body-parser": "^1.15.1", "config": "^3.0.1", "cors": "^2.7.1", - "dynamoose": "^1.6.4", - "elasticsearch": "^15.1.1", - "express": "^4.14.0", + "dynamoose": "^1.8.0", + "elasticsearch": "^16.1.1", + "express": "^4.15.4", "express-fileupload": "^1.1.4", "express-interceptor": "^1.2.0", "get-parameter-names": "^0.3.0", @@ -37,13 +46,16 @@ "jsonwebtoken": "^8.3.0", "lodash": "^4.17.11", "tc-core-library-js": "appirio-tech/tc-core-library-js.git#v2.6.2", - "tc-bus-api-wrapper": "topcoder-platform/tc-bus-api-wrapper.git", + "topcoder-bus-api-wrapper": "topcoder-platform/tc-bus-api-wrapper.git", "uuid": "^3.3.2", "winston": "^3.1.0" }, "standard": { "ignore": [ "mock-api" + ], + "env": [ + "mocha" ] }, "engines": { diff --git a/src/common/helper.js b/src/common/helper.js index 69cf39ea..68f0de50 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -12,7 +12,7 @@ const config = require('config') const m2mAuth = require('tc-core-library-js').auth.m2m const m2m = m2mAuth(_.pick(config, ['AUTH0_URL', 'AUTH0_AUDIENCE', 'TOKEN_CACHE_TIME'])) const axios = require('axios') -const busApi = require('tc-bus-api-wrapper') +const busApi = require('topcoder-bus-api-wrapper') const elasticsearch = require('elasticsearch') // Bus API Client @@ -314,8 +314,7 @@ async function validatePhases (phases) { _.each(records, r => { map.set(r.id, r) }) - const invalidPhases = _.filter(phases, p => !map.has(p.id) || !p.isActive || - !_.isEqual(_.pick(map.get(p.id), 'id', 'name', 'description', 'predecessor', 'isActive'), _.omit(p, 'duration'))) + const invalidPhases = _.filter(phases, p => !map.has(p.id) || !p.isActive) if (invalidPhases.length > 0) { throw new errors.BadRequestError(`The following phases are invalid or inactive: ${toString(invalidPhases)}`) } diff --git a/src/init-es.js b/src/init-es.js new file mode 100644 index 00000000..f3c8474b --- /dev/null +++ b/src/init-es.js @@ -0,0 +1,40 @@ +/** + * Initialize elastic search. + * It will create configured index in elastic search if it is not present. + * It can delete and re-create index if providing an extra 'force' argument. + * Usage: + * node src/init-es + * node src/init-es force + */ +const config = require('config') +const logger = require('./common/logger') +const helper = require('./common/helper') + +const client = helper.getESClient() + +const initES = async () => { + if (process.argv.length === 3 && process.argv[2] === 'force') { + logger.info(`Delete index ${config.ES.ES_INDEX} if any.`) + try { + await client.indices.delete({ index: config.ES.ES_INDEX }) + } catch (err) { + // ignore + } + } + + const exists = await client.indices.exists({ index: config.ES.ES_INDEX }) + if (exists) { + logger.info(`The index ${config.ES.ES_INDEX} exists.`) + } else { + logger.info(`The index ${config.ES.ES_INDEX} will be created.`) + await client.indices.create({ index: config.ES.ES_INDEX }) + } +} + +initES().then(() => { + logger.info('Done!') + process.exit() +}).catch((e) => { + logger.logFullError(e) + process.exit() +}) diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 494ad642..0a73e476 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -106,7 +106,8 @@ async function searchChallenges (currentUser, criteria) { const docs = await esClient.search(esQuery) // Extract data from hits const total = docs.hits.total - const result = _.map(docs.hits.hits, item => item._source) + let result = _.map(docs.hits.hits, item => item._source) + result = await filterChallengesByGroupsAccess(currentUser, result) const typeList = await helper.scan('ChallengeType') const typeMap = new Map() diff --git a/test/attachment.txt b/test/attachment.txt new file mode 100644 index 00000000..30d74d25 --- /dev/null +++ b/test/attachment.txt @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/test/e2e/attachment.api.test.js b/test/e2e/attachment.api.test.js new file mode 100644 index 00000000..400ac622 --- /dev/null +++ b/test/e2e/attachment.api.test.js @@ -0,0 +1,158 @@ +/* + * E2E tests of attachment API + */ + +// During the test the env variable is set to test +process.env.NODE_ENV = 'test' + +require('../../app-bootstrap') +const fs = require('fs') +const path = require('path') +const config = require('config') +const uuid = require('uuid/v4') +const chai = require('chai') +const chaiHttp = require('chai-http') +const app = require('../../app') +const testHelper = require('../testHelper') + +const should = chai.should() +chai.use(chaiHttp) + +const attachmentContent = fs.readFileSync(path.join(__dirname, '../attachment.txt')) + +describe('attachment API E2E tests', () => { + // created attachment id + let id + // generated data + let data + const notFoundId = uuid() + + before(async () => { + await testHelper.createData() + data = testHelper.getData() + }) + + after(async () => { + await testHelper.clearData() + }) + + describe('upload attachment API tests', () => { + it('upload attachment successfully', async () => { + const response = await chai.request(app) + .post(`/challenges/${data.challenge.id}/attachments`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .attach('attachment', attachmentContent, 'attachment.txt') + should.equal(response.status, 200) + const result = response.body + should.exist(result.id) + id = result.id + should.equal(result.fileSize, attachmentContent.length) + should.equal(result.fileName, 'attachment.txt') + should.equal(result.challengeId, data.challenge.id) + }) + + it('upload attachment - missing token', async () => { + const response = await chai.request(app) + .post(`/challenges/${data.challenge.id}/attachments`) + .attach('attachment', attachmentContent, 'attachment.txt') + should.equal(response.status, 401) + should.equal(response.body.message, 'No token provided.') + }) + + it('upload attachment - invalid bearer format', async () => { + const response = await chai.request(app) + .post(`/challenges/${data.challenge.id}/attachments`) + .set('Authorization', 'invalid format') + .attach('attachment', attachmentContent, 'attachment.txt') + should.equal(response.status, 401) + should.equal(response.body.message, 'No token provided.') + }) + + it('upload attachment - invalid token', async () => { + const response = await chai.request(app) + .post(`/challenges/${data.challenge.id}/attachments`) + .set('Authorization', `Bearer ${config.INVALID_TOKEN}`) + .attach('attachment', attachmentContent, 'attachment.txt') + should.equal(response.status, 401) + should.equal(response.body.message, 'Failed to authenticate token.') + }) + + it('upload attachment - expired token', async () => { + const response = await chai.request(app) + .post(`/challenges/${data.challenge.id}/attachments`) + .set('Authorization', `Bearer ${config.EXPIRED_TOKEN}`) + .attach('attachment', attachmentContent, 'attachment.txt') + should.equal(response.status, 401) + should.equal(response.body.message, 'Failed to authenticate token.') + }) + + it('upload attachment - invalid attachment', async () => { + const response = await chai.request(app) + .post(`/challenges/${data.challenge.id}/attachments`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .attach('invalid', attachmentContent, 'attachment.txt') + should.equal(response.status, 400) + should.equal(response.body.message, '"attachment" is required') + }) + + it('upload attachment - challenge not found', async () => { + const response = await chai.request(app) + .post(`/challenges/${notFoundId}/attachments`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .attach('attachment', attachmentContent, 'attachment.txt') + should.equal(response.status, 404) + should.equal(response.body.message, `Challenge with id: ${notFoundId} doesn't exist`) + }) + + it('upload attachment - forbidden', async () => { + const response = await chai.request(app) + .post(`/challenges/${data.challenge.id}/attachments`) + .set('Authorization', `Bearer ${config.USER_TOKEN}`) + .attach('attachment', attachmentContent, 'attachment.txt') + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + }) + + describe('download attachment API tests', () => { + it('download attachment successfully', async () => { + const response = await chai.request(app) + .get(`/challenges/${data.challenge.id}/attachments/${id}`) + .set('Authorization', `Bearer ${config.M2M_FULL_ACCESS_TOKEN}`) + should.equal(response.status, 200) + should.equal(response.text, 'test') // attachment content is 'test' + }) + + it('download attachment - forbidden', async () => { + const response = await chai.request(app) + .get(`/challenges/${data.challenge.id}/attachments/${id}`) + .set('Authorization', `Bearer ${config.COPILOT_TOKEN}`) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to download attachment of the challenge.') + }) + + it('download attachment - attachment not found', async () => { + const response = await chai.request(app) + .get(`/challenges/${data.challenge.id}/attachments/${notFoundId}`) + .set('Authorization', `Bearer ${config.M2M_FULL_ACCESS_TOKEN}`) + should.equal(response.status, 404) + should.equal(response.body.message, `Attachment with id: ${notFoundId} doesn't exist`) + }) + + it('download attachment - challenge id mismatched', async () => { + const response = await chai.request(app) + .get(`/challenges/${notFoundId}/attachments/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + should.equal(response.status, 400) + should.equal(response.body.message, 'The attachment challengeId does not match the path challengeId.') + }) + + it('download attachment - invalid id', async () => { + const response = await chai.request(app) + .get(`/challenges/${notFoundId}/attachments/invalid`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + should.equal(response.status, 400) + should.equal(response.body.message, '"attachmentId" must be a valid GUID') + }) + }) +}) diff --git a/test/e2e/audit.log.api.test.js b/test/e2e/audit.log.api.test.js new file mode 100644 index 00000000..c19f09af --- /dev/null +++ b/test/e2e/audit.log.api.test.js @@ -0,0 +1,142 @@ +/* + * E2E tests of audit log API + */ + +// During the test the env variable is set to test +process.env.NODE_ENV = 'test' + +require('../../app-bootstrap') +const _ = require('lodash') +const config = require('config') +const uuid = require('uuid/v4') +const chai = require('chai') +const chaiHttp = require('chai-http') +const app = require('../../app') +const testHelper = require('../testHelper') + +const should = chai.should() +chai.use(chaiHttp) + +const basePath = '/challengeAuditLogs' + +describe('audit log API E2E tests', () => { + // generated data + let data + const notFoundId = uuid() + + before(async () => { + await testHelper.createData() + data = testHelper.getData() + }) + + after(async () => { + await testHelper.clearData() + }) + + describe('search audit logs API tests', () => { + it('search audit logs successfully 1', async () => { + // update challenge so that there are some audit logs + const updateChallengeResponse = await chai.request(app) + .patch(`/challenges/${data.challenge.id}`) + .set('Authorization', `Bearer ${config.M2M_FULL_ACCESS_TOKEN}`) + .send({ + track: 'track-abc', + description: 'desc-abc' + }) + should.equal(updateChallengeResponse.status, 200) + + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.M2M_READ_ACCESS_TOKEN}`) + .query({ + page: 1, + perPage: 10, + challengeId: data.challenge.id, + createdDateStart: new Date(new Date().getTime() - 1000 * 60 * 60 * 30), + createdDateEnd: '2022-01-02' + }) + should.equal(response.status, 200) + should.equal(response.headers['x-page'], '1') + should.equal(response.headers['x-per-page'], '10') + should.equal(response.headers['x-total'], '2') + should.equal(response.headers['x-total-pages'], '1') + should.exist(response.headers['link']) + + should.equal(response.body.length, 2) + let log = _.find(response.body, (item) => item.fieldName === 'track') + should.exist(log) + should.equal(log.oldValue, '"track"') + should.equal(log.newValue, '"track-abc"') + should.equal(log.challengeId, data.challenge.id) + should.exist(log.createdBy) + should.exist(log.created) + should.exist(log.id) + log = _.find(response.body, (item) => item.fieldName === 'description') + should.exist(log) + should.equal(log.oldValue, '"desc"') + should.equal(log.newValue, '"desc-abc"') + should.equal(log.challengeId, data.challenge.id) + should.exist(log.createdBy) + should.exist(log.created) + should.exist(log.id) + }) + + it('search audit logs successfully 2', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ challengeId: notFoundId }) + should.equal(response.status, 200) + should.equal(response.headers['x-page'], '1') + should.equal(response.headers['x-per-page'], '20') + should.equal(response.headers['x-total'], '0') + should.equal(response.headers['x-total-pages'], '0') + should.equal(response.body.length, 0) + }) + + it('search audit logs - invalid page', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ page: -1 }) + should.equal(response.status, 400) + should.equal(response.body.message, '"page" must be larger than or equal to 1') + }) + + it('search audit logs - invalid perPage', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ perPage: 'abc' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"perPage" must be a number') + }) + + it('search audit logs - invalid start date', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ createdDateStart: 'abc' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"createdDateStart" must be a number of milliseconds or valid date string') + }) + + it('search audit logs - invalid end date', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ createdDateEnd: 'abc' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"createdDateEnd" must be a number of milliseconds or valid date string') + }) + + it('search audit logs - unexpected field', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ other: 123 }) + should.equal(response.status, 400) + should.equal(response.body.message, '"other" is not allowed') + }) + }) +}) diff --git a/test/e2e/challenge.api.test.js b/test/e2e/challenge.api.test.js new file mode 100644 index 00000000..3c26d774 --- /dev/null +++ b/test/e2e/challenge.api.test.js @@ -0,0 +1,824 @@ +/* + * E2E tests of challenge API + */ + +// During the test the env variable is set to test +process.env.NODE_ENV = 'test' + +require('../../app-bootstrap') +const _ = require('lodash') +const config = require('config') +const uuid = require('uuid/v4') +const fs = require('fs') +const path = require('path') +const chai = require('chai') +const chaiHttp = require('chai-http') +const app = require('../../app') +const AttachmentService = require('../../src/services/AttachmentService') +const testHelper = require('../testHelper') + +const should = chai.should() +chai.use(chaiHttp) + +const basePath = '/challenges' + +const attachmentContent = fs.readFileSync(path.join(__dirname, '../attachment.txt')) + +describe('challenge API E2E tests', () => { + // created entity id + let id + let attachment + // generated data + let data + const notFoundId = uuid() + + before(async () => { + await testHelper.createData() + data = testHelper.getData() + // create an attachment for test + attachment = await AttachmentService.uploadAttachment({ + isMachine: true + }, data.challenge.id, { + attachment: { + data: attachmentContent, + mimetype: 'plain/text', + name: 'attachment.txt', + size: attachmentContent.length + } + }) + }) + + after(async () => { + await testHelper.clearData() + }) + + describe('create challenge API tests', () => { + it('create challenge successfully 1', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_FULL_ACCESS_TOKEN}`) + .send(_.omit(data.challenge, ['id', 'created', 'createdBy'])) + should.equal(response.status, 201) + const result = response.body + should.exist(result.id) + id = result.id + should.equal(result.typeId, data.challenge.typeId) + should.equal(result.track, data.challenge.track) + should.equal(result.name, data.challenge.name) + should.equal(result.description, data.challenge.description) + should.equal(result.challengeSettings.length, 1) + should.equal(result.challengeSettings[0].type, data.challenge.challengeSettings[0].type) + should.equal(result.challengeSettings[0].value, data.challenge.challengeSettings[0].value) + should.equal(result.timelineTemplateId, data.challenge.timelineTemplateId) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, data.challenge.phases[0].id) + should.equal(result.phases[0].name, data.challenge.phases[0].name) + should.equal(result.phases[0].description, data.challenge.phases[0].description) + should.equal(result.phases[0].isActive, data.challenge.phases[0].isActive) + should.equal(result.phases[0].duration, data.challenge.phases[0].duration) + should.equal(result.prizeSets.length, 1) + should.equal(result.prizeSets[0].type, data.challenge.prizeSets[0].type) + should.equal(result.prizeSets[0].description, data.challenge.prizeSets[0].description) + should.equal(result.prizeSets[0].prizes.length, 1) + should.equal(result.prizeSets[0].prizes[0].description, data.challenge.prizeSets[0].prizes[0].description) + should.equal(result.prizeSets[0].prizes[0].type, data.challenge.prizeSets[0].prizes[0].type) + should.equal(result.prizeSets[0].prizes[0].value, data.challenge.prizeSets[0].prizes[0].value) + should.equal(result.reviewType, data.challenge.reviewType) + should.equal(result.tags.length, 1) + should.equal(result.tags[0], data.challenge.tags[0]) + should.equal(result.projectId, data.challenge.projectId) + should.equal(result.legacyId, data.challenge.legacyId) + should.equal(result.forumId, data.challenge.forumId) + should.equal(result.status, data.challenge.status) + should.equal(result.groups.length, 1) + should.equal(result.groups[0], data.challenge.groups[0]) + should.exist(result.startDate) + should.exist(result.created) + should.exist(result.createdBy) + }) + + it('create challenge - missing token', async () => { + const response = await chai.request(app) + .post(basePath) + .send(_.omit(data.challenge, ['id', 'created', 'createdBy'])) + should.equal(response.status, 401) + should.equal(response.body.message, 'No token provided.') + }) + + it('create challenge - invalid bearer format', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', 'invalid format') + .send(_.omit(data.challenge, ['id', 'created', 'createdBy'])) + should.equal(response.status, 401) + should.equal(response.body.message, 'No token provided.') + }) + + it('create challenge - invalid token', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.INVALID_TOKEN}`) + .send(_.omit(data.challenge, ['id', 'created', 'createdBy'])) + should.equal(response.status, 401) + should.equal(response.body.message, 'Failed to authenticate token.') + }) + + it('create challenge - expired token', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.EXPIRED_TOKEN}`) + .send(_.omit(data.challenge, ['id', 'created', 'createdBy'])) + should.equal(response.status, 401) + should.equal(response.body.message, 'Failed to authenticate token.') + }) + + it('create challenge - forbidden', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_READ_ACCESS_TOKEN}`) + .send(_.omit(data.challenge, ['id', 'created', 'createdBy'])) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + + it('create challenge - type not found', async () => { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.typeId = notFoundId + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send(challengeData) + should.equal(response.status, 400) + should.equal(response.body.message, `No challenge type found with id: ${notFoundId}.`) + }) + + it('create challenge - invalid track', async () => { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.track = [1, 2] + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send(challengeData) + should.equal(response.status, 400) + should.equal(response.body.message, '"track" must be a string') + }) + + it('create challenge - invalid description', async () => { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.description = [1, 2] + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send(challengeData) + should.equal(response.status, 400) + should.equal(response.body.message, '"description" must be a string') + }) + + it('create challenge - invalid projectId', async () => { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.projectId = -1 + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send(challengeData) + should.equal(response.status, 400) + should.equal(response.body.message, '"projectId" must be a positive number') + }) + + it('create challenge - invalid forumId', async () => { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.forumId = -1 + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send(challengeData) + should.equal(response.status, 400) + should.equal(response.body.message, '"forumId" must be a positive number') + }) + + it('create challenge - invalid legacyId', async () => { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.legacyId = -1 + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send(challengeData) + should.equal(response.status, 400) + should.equal(response.body.message, '"legacyId" must be a positive number') + }) + + it('create challenge - missing name', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send(_.omit(data.challenge, ['id', 'name', 'created', 'createdBy'])) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" is required') + }) + + it('create challenge - invalid start date', async () => { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.startDate = 'abc' + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send(challengeData) + should.equal(response.status, 400) + should.equal(response.body.message, '"startDate" must be a number of milliseconds or valid date string') + }) + + it('create challenge - invalid status', async () => { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.status = 'invalid' + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send(challengeData) + should.equal(response.status, 400) + should.equal(response.body.message, '"status" must be one of [Draft, Canceled, Active, Completed]') + }) + + it('create challenge - unexpected field', async () => { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.other = 'invalid' + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send(challengeData) + should.equal(response.status, 400) + should.equal(response.body.message, '"other" is not allowed') + }) + }) + + describe('get challenge API tests', () => { + it('get challenge successfully', async () => { + const response = await chai.request(app) + .get(`${basePath}/${data.challenge.id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + should.equal(response.status, 200) + const result = response.body + should.equal(result.id, data.challenge.id) + should.equal(result.typeId, data.challenge.typeId) + should.equal(result.track, data.challenge.track) + should.equal(result.name, data.challenge.name) + should.equal(result.description, data.challenge.description) + should.equal(result.challengeSettings.length, 1) + should.equal(result.challengeSettings[0].type, data.challengeSetting.name) + should.equal(result.challengeSettings[0].value, data.challenge.challengeSettings[0].value) + should.equal(result.timelineTemplateId, data.challenge.timelineTemplateId) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, data.challenge.phases[0].id) + should.equal(result.phases[0].name, data.challenge.phases[0].name) + should.equal(result.phases[0].description, data.challenge.phases[0].description) + should.equal(result.phases[0].isActive, data.challenge.phases[0].isActive) + should.equal(result.phases[0].duration, data.challenge.phases[0].duration) + should.equal(result.prizeSets.length, 1) + should.equal(result.prizeSets[0].type, data.challenge.prizeSets[0].type) + should.equal(result.prizeSets[0].description, data.challenge.prizeSets[0].description) + should.equal(result.prizeSets[0].prizes.length, 1) + should.equal(result.prizeSets[0].prizes[0].description, data.challenge.prizeSets[0].prizes[0].description) + should.equal(result.prizeSets[0].prizes[0].type, data.challenge.prizeSets[0].prizes[0].type) + should.equal(result.prizeSets[0].prizes[0].value, data.challenge.prizeSets[0].prizes[0].value) + should.equal(result.reviewType, data.challenge.reviewType) + should.equal(result.tags.length, 1) + should.equal(result.tags[0], data.challenge.tags[0]) + should.equal(result.projectId, data.challenge.projectId) + should.equal(result.legacyId, data.challenge.legacyId) + should.equal(result.forumId, data.challenge.forumId) + should.equal(result.status, data.challenge.status) + should.equal(result.groups.length, 1) + should.equal(result.groups[0], data.challenge.groups[0]) + should.exist(result.startDate) + should.exist(result.created) + should.exist(result.createdBy) + }) + + it('get challenge - forbidden', async () => { + const response = await chai.request(app) + .get(`${basePath}/${data.challenge.id}`) + .set('Authorization', `Bearer ${config.USER_TOKEN}`) + should.equal(response.status, 403) + should.equal(response.body.message, 'You don\'t have access to this group!') + }) + + it('get challenge - not found', async () => { + const response = await chai.request(app) + .get(`${basePath}/${notFoundId}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + should.equal(response.status, 404) + should.equal(response.body.message, `Challenge of id ${notFoundId} is not found.`) + }) + + it('get challenge - invalid id', async () => { + const response = await chai.request(app) + .get(`${basePath}/invalid`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + should.equal(response.status, 400) + should.equal(response.body.message, '"id" must be a valid GUID') + }) + }) + + describe('search challenges API tests', () => { + it('search challenges successfully 1', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.M2M_READ_ACCESS_TOKEN}`) + .query({ + page: 1, + perPage: 10, + id: data.challenge.id, + typeId: data.challenge.typeId, + track: data.challenge.track, + name: data.challenge.name.substring(2).trim().toUpperCase(), + description: data.challenge.description, + timelineTemplateId: data.challenge.timelineTemplateId, + reviewType: data.challenge.reviewType, + tag: data.challenge.tags[0], + projectId: data.challenge.projectId, + forumId: data.challenge.forumId, + legacyId: data.challenge.legacyId, + status: data.challenge.status, + group: data.challenge.groups[0], + createdDateStart: '1992-01-02', + createdDateEnd: '2022-01-02', + createdBy: data.challenge.createdBy + }) + should.equal(response.status, 200) + should.equal(response.headers['x-page'], '1') + should.equal(response.headers['x-per-page'], '10') + should.equal(response.headers['x-total'], '1') + should.equal(response.headers['x-total-pages'], '1') + should.exist(response.headers['link']) + + should.equal(response.body.length, 1) + const result = response.body[0] + should.equal(result.id, data.challenge.id) + should.equal(result.type, data.challengeType.name) + should.equal(result.track, data.challenge.track) + should.equal(result.name, data.challenge.name) + should.equal(result.description, data.challenge.description) + should.equal(result.challengeSettings.length, 1) + should.equal(result.challengeSettings[0].type, data.challengeSetting.name) + should.equal(result.challengeSettings[0].value, data.challenge.challengeSettings[0].value) + should.equal(result.timelineTemplateId, data.challenge.timelineTemplateId) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, data.challenge.phases[0].id) + should.equal(result.phases[0].name, data.challenge.phases[0].name) + should.equal(result.phases[0].description, data.challenge.phases[0].description) + should.equal(result.phases[0].isActive, data.challenge.phases[0].isActive) + should.equal(result.phases[0].duration, data.challenge.phases[0].duration) + should.equal(result.prizeSets.length, 1) + should.equal(result.prizeSets[0].type, data.challenge.prizeSets[0].type) + should.equal(result.prizeSets[0].description, data.challenge.prizeSets[0].description) + should.equal(result.prizeSets[0].prizes.length, 1) + should.equal(result.prizeSets[0].prizes[0].description, data.challenge.prizeSets[0].prizes[0].description) + should.equal(result.prizeSets[0].prizes[0].type, data.challenge.prizeSets[0].prizes[0].type) + should.equal(result.prizeSets[0].prizes[0].value, data.challenge.prizeSets[0].prizes[0].value) + should.equal(result.reviewType, data.challenge.reviewType) + should.equal(result.tags.length, 1) + should.equal(result.tags[0], data.challenge.tags[0]) + should.equal(result.projectId, data.challenge.projectId) + should.equal(result.legacyId, data.challenge.legacyId) + should.equal(result.forumId, data.challenge.forumId) + should.equal(result.status, data.challenge.status) + should.equal(result.groups.length, 1) + should.equal(result.groups[0], data.challenge.groups[0]) + should.exist(result.startDate) + should.exist(result.created) + should.exist(result.createdBy) + }) + + it('search challenges successfully 2', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.M2M_FULL_ACCESS_TOKEN}`) + .query({ name: 'xxjklsdjfihx' }) + should.equal(response.status, 200) + should.equal(response.headers['x-page'], '1') + should.equal(response.headers['x-per-page'], '20') + should.equal(response.headers['x-total'], '0') + should.equal(response.headers['x-total-pages'], '0') + should.equal(response.body.length, 0) + }) + + it('search challenges - invalid page', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ page: -1 }) + should.equal(response.status, 400) + should.equal(response.body.message, '"page" must be larger than or equal to 1') + }) + + it('search challenges - invalid perPage', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ perPage: 'abc' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"perPage" must be a number') + }) + + it('search challenges - invalid id', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ id: 'abc' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"id" must be a valid GUID') + }) + + it('search challenges - invalid typeId', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ typeId: 'abc' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"typeId" must be a valid GUID') + }) + + it('search challenges - invalid timelineTemplateId', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ timelineTemplateId: 'abc' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"timelineTemplateId" must be a valid GUID') + }) + + it('search challenges - invalid projectId', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ projectId: -1 }) + should.equal(response.status, 400) + should.equal(response.body.message, '"projectId" must be a positive number') + }) + + it('search challenges - invalid forumId', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ forumId: -1 }) + should.equal(response.status, 400) + should.equal(response.body.message, '"forumId" must be a positive number') + }) + + it('search challenges - invalid legacyId', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ legacyId: -1 }) + should.equal(response.status, 400) + should.equal(response.body.message, '"legacyId" must be a positive number') + }) + + it('search challenges - invalid status', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ status: 'abc' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"status" must be one of [Draft, Canceled, Active, Completed]') + }) + + it('search challenges - invalid createdDateStart', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ createdDateStart: 'abc' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"createdDateStart" must be a number of milliseconds or valid date string') + }) + + it('search challenges - invalid createdDateEnd', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ createdDateEnd: 'abc' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"createdDateEnd" must be a number of milliseconds or valid date string') + }) + + it('search challenges - invalid updatedDateStart', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ updatedDateStart: 'abc' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"updatedDateStart" must be a number of milliseconds or valid date string') + }) + + it('search challenges - invalid updatedDateEnd', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ updatedDateEnd: 'abc' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"updatedDateEnd" must be a number of milliseconds or valid date string') + }) + + it('search challenges - unexpected field', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ other: 123 }) + should.equal(response.status, 400) + should.equal(response.body.message, '"other" is not allowed') + }) + }) + + describe('fully update challenge API tests', () => { + it('fully update challenge successfully', async () => { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.track = 'updated-track' + challengeData.projectId = 112233 + challengeData.legacyId = 445566 + challengeData.attachmentIds = [attachment.id] + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send(challengeData) + should.equal(response.status, 200) + const result = response.body + should.equal(result.id, id) + should.equal(result.typeId, data.challenge.typeId) + should.equal(result.track, 'updated-track') + should.equal(result.name, data.challenge.name) + should.equal(result.description, data.challenge.description) + should.equal(result.challengeSettings.length, 1) + should.equal(result.challengeSettings[0].type, data.challenge.challengeSettings[0].type) + should.equal(result.challengeSettings[0].value, data.challenge.challengeSettings[0].value) + should.equal(result.timelineTemplateId, data.challenge.timelineTemplateId) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, data.challenge.phases[0].id) + should.equal(result.phases[0].name, data.challenge.phases[0].name) + should.equal(result.phases[0].description, data.challenge.phases[0].description) + should.equal(result.phases[0].isActive, data.challenge.phases[0].isActive) + should.equal(result.phases[0].duration, data.challenge.phases[0].duration) + should.equal(result.prizeSets.length, 1) + should.equal(result.prizeSets[0].type, data.challenge.prizeSets[0].type) + should.equal(result.prizeSets[0].description, data.challenge.prizeSets[0].description) + should.equal(result.prizeSets[0].prizes.length, 1) + should.equal(result.prizeSets[0].prizes[0].description, data.challenge.prizeSets[0].prizes[0].description) + should.equal(result.prizeSets[0].prizes[0].type, data.challenge.prizeSets[0].prizes[0].type) + should.equal(result.prizeSets[0].prizes[0].value, data.challenge.prizeSets[0].prizes[0].value) + should.equal(result.reviewType, data.challenge.reviewType) + should.equal(result.tags.length, 1) + should.equal(result.tags[0], data.challenge.tags[0]) + should.equal(result.projectId, 112233) + should.equal(result.legacyId, 445566) + should.equal(result.forumId, data.challenge.forumId) + should.equal(result.status, data.challenge.status) + should.equal(result.groups.length, 1) + should.equal(result.groups[0], data.challenge.groups[0]) + should.equal(result.attachments.length, 1) + should.equal(result.attachments[0].id, attachment.id) + should.equal(result.attachments[0].fileSize, attachment.fileSize) + should.equal(result.attachments[0].fileName, attachment.fileName) + should.equal(result.attachments[0].challengeId, attachment.challengeId) + should.exist(result.startDate) + should.exist(result.createdBy) + should.exist(result.updatedBy) + should.exist(result.created) + should.exist(result.updated) + }) + + it('fully update challenge - forbidden', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.USER_TOKEN}`) + .send(_.omit(data.challenge, ['id', 'created', 'createdBy'])) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + + it('fully update challenge - not found', async () => { + const response = await chai.request(app) + .put(`${basePath}/${notFoundId}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send(_.omit(data.challenge, ['id', 'created', 'createdBy'])) + should.equal(response.status, 404) + should.equal(response.body.message, `Challenge with id: ${notFoundId} doesn't exist`) + }) + + it('fully update challenge - invalid id', async () => { + const response = await chai.request(app) + .put(`${basePath}/invalid`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send(_.omit(data.challenge, ['id', 'created', 'createdBy'])) + should.equal(response.status, 400) + should.equal(response.body.message, '"challengeId" must be a valid GUID') + }) + + it('fully update challenge - null name', async () => { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.name = null + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send(challengeData) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('fully update challenge - invalid track', async () => { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.track = ['updated-track'] + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send(challengeData) + should.equal(response.status, 400) + should.equal(response.body.message, '"track" must be a string') + }) + + it('fully update challenge - invalid settings', async () => { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.challengeSettings = [{ value: 'value' }] + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send(challengeData) + should.equal(response.status, 400) + should.equal(response.body.message, '"type" is required') + }) + + it('fully update challenge - invalid timelineTemplateId', async () => { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.timelineTemplateId = 'abc' + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send(challengeData) + should.equal(response.status, 400) + should.equal(response.body.message, '"timelineTemplateId" must be a valid GUID') + }) + + it('fully update challenge - invalid phases', async () => { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.phases = [{ name: 'review' }] + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send(challengeData) + should.equal(response.status, 400) + should.equal(response.body.message, '"id" is required') + }) + + it('fully update challenge - empty reviewType', async () => { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.reviewType = '' + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send(challengeData) + should.equal(response.status, 400) + should.equal(response.body.message, '"reviewType" is not allowed to be empty') + }) + + it('fully update challenge - invalid prize', async () => { + const challengeData = _.cloneDeep(_.omit(data.challenge, ['id', 'created', 'createdBy'])) + challengeData.prizeSets[0].prizes[0].value = -1 + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send(challengeData) + should.equal(response.status, 400) + should.equal(response.body.message, '"value" must be a positive number') + }) + }) + + describe('partially update challenge API tests', () => { + it('partially update challenge successfully', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.M2M_FULL_ACCESS_TOKEN}`) + .send({ + track: 'track 333', + description: 'updated desc', + attachmentIds: [] // this will delete attachments + }) + should.equal(response.status, 200) + const result = response.body + should.equal(result.id, id) + should.equal(result.typeId, data.challenge.typeId) + should.equal(result.track, 'track 333') + should.equal(result.name, data.challenge.name) + should.equal(result.description, 'updated desc') + should.equal(result.challengeSettings.length, 1) + should.equal(result.challengeSettings[0].type, data.challenge.challengeSettings[0].type) + should.equal(result.challengeSettings[0].value, data.challenge.challengeSettings[0].value) + should.equal(result.timelineTemplateId, data.challenge.timelineTemplateId) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, data.challenge.phases[0].id) + should.equal(result.phases[0].name, data.challenge.phases[0].name) + should.equal(result.phases[0].description, data.challenge.phases[0].description) + should.equal(result.phases[0].isActive, data.challenge.phases[0].isActive) + should.equal(result.phases[0].duration, data.challenge.phases[0].duration) + should.equal(result.prizeSets.length, 1) + should.equal(result.prizeSets[0].type, data.challenge.prizeSets[0].type) + should.equal(result.prizeSets[0].description, data.challenge.prizeSets[0].description) + should.equal(result.prizeSets[0].prizes.length, 1) + should.equal(result.prizeSets[0].prizes[0].description, data.challenge.prizeSets[0].prizes[0].description) + should.equal(result.prizeSets[0].prizes[0].type, data.challenge.prizeSets[0].prizes[0].type) + should.equal(result.prizeSets[0].prizes[0].value, data.challenge.prizeSets[0].prizes[0].value) + should.equal(result.reviewType, data.challenge.reviewType) + should.equal(result.tags.length, 1) + should.equal(result.tags[0], data.challenge.tags[0]) + should.equal(result.projectId, 112233) + should.equal(result.legacyId, 445566) + should.equal(result.forumId, data.challenge.forumId) + should.equal(result.status, data.challenge.status) + should.equal(result.groups.length, 1) + should.equal(result.groups[0], data.challenge.groups[0]) + should.equal(!result.attachments || result.attachments.length === 0, true) + should.exist(result.startDate) + should.exist(result.createdBy) + should.exist(result.updatedBy) + should.exist(result.created) + should.exist(result.updated) + }) + + it('partially update challenge - forbidden', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.M2M_READ_ACCESS_TOKEN}`) + .send({ name: 'testing2' }) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + + it('partially update challenge - not found', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${notFoundId}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: 'testing2' }) + should.equal(response.status, 404) + should.equal(response.body.message, `Challenge with id: ${notFoundId} doesn't exist`) + }) + + it('partially update challenge - invalid id', async () => { + const response = await chai.request(app) + .patch(`${basePath}/invalid`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: 'testing2' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"challengeId" must be a valid GUID') + }) + + it('partially update challenge - null name', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: null }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('partially update challenge - invalid name', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: { invalid: 123 } }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('partially update challenge - empty name', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: '' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" is not allowed to be empty') + }) + + it('partially update challenge - invalid tags', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ tags: [] }) + should.equal(response.status, 400) + should.equal(response.body.message, '"tags" does not contain 1 required value(s)') + }) + + it('partially update challenge - invalid groups', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ groups: 'group1 group2' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"groups" must be an array') + }) + + it('partially update challenge - invalid attachmentIds', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ attachmentIds: ['abc'] }) + should.equal(response.status, 400) + should.equal(response.body.message, '"0" must be a valid GUID') + }) + }) +}) diff --git a/test/e2e/challenge.setting.api.test.js b/test/e2e/challenge.setting.api.test.js new file mode 100644 index 00000000..d6362c60 --- /dev/null +++ b/test/e2e/challenge.setting.api.test.js @@ -0,0 +1,344 @@ +/* + * E2E tests of challenge setting API + */ + +// During the test the env variable is set to test +process.env.NODE_ENV = 'test' + +require('../../app-bootstrap') +const config = require('config') +const uuid = require('uuid/v4') +const chai = require('chai') +const chaiHttp = require('chai-http') +const app = require('../../app') + +const should = chai.should() +chai.use(chaiHttp) + +const basePath = '/challengeSettings' + +describe('challenge setting API E2E tests', () => { + // created entity ids + let id + let id2 + // random names + const name = `test1${new Date().getTime()}` + const name2 = `test2${new Date().getTime()}` + const notFoundId = uuid() + + describe('create challenge setting API tests', () => { + it('create challenge setting successfully 1', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_FULL_ACCESS_TOKEN}`) + .send({ + name + }) + should.equal(response.status, 201) + const result = response.body + should.equal(result.name, name) + should.exist(result.id) + id = result.id + }) + + it('create challenge setting successfully 2', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name: name2 + }) + should.equal(response.status, 201) + const result = response.body + should.equal(result.name, name2) + should.exist(result.id) + id2 = result.id + }) + + it('create challenge setting - missing token', async () => { + const response = await chai.request(app) + .post(basePath) + .send({ + name: 'ueueue7878' + }) + should.equal(response.status, 401) + should.equal(response.body.message, 'No token provided.') + }) + + it('create challenge setting - invalid bearer format', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', 'invalid format') + .send({ + name: 'ueueue7878' + }) + should.equal(response.status, 401) + should.equal(response.body.message, 'No token provided.') + }) + + it('create challenge setting - invalid token', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.INVALID_TOKEN}`) + .send({ + name: 'ueueue7878' + }) + should.equal(response.status, 401) + should.equal(response.body.message, 'Failed to authenticate token.') + }) + + it('create challenge setting - expired token', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.EXPIRED_TOKEN}`) + .send({ + name: 'ueueue7878' + }) + should.equal(response.status, 401) + should.equal(response.body.message, 'Failed to authenticate token.') + }) + + it('create challenge setting - name already used', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name + }) + should.equal(response.status, 409) + should.equal(response.body.message, `ChallengeSetting with name: ${name} already exist`) + }) + + it('create challenge setting - forbidden', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_READ_ACCESS_TOKEN}`) + .send({ + name: 'flskjdf' + }) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + + it('create challenge setting - missing name', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" is required') + }) + + it('create challenge setting - invalid name', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name: ['xx'] + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('create challenge setting - unexpected field', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name: 'flskjdf', + other: 'def' + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"other" is not allowed') + }) + }) + + describe('get challenge setting API tests', () => { + it('get challenge setting successfully', async () => { + const response = await chai.request(app) + .get(`${basePath}/${id2}`) + .set('Authorization', `Bearer ${config.COPILOT_TOKEN}`) + should.equal(response.status, 200) + const result = response.body + should.equal(result.id, id2) + should.equal(result.name, name2) + }) + + it('get challenge setting - forbidden', async () => { + const response = await chai.request(app) + .get(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + + it('get challenge setting - not found', async () => { + const response = await chai.request(app) + .get(`${basePath}/${notFoundId}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + should.equal(response.status, 404) + should.equal(response.body.message, `ChallengeSetting with id: ${notFoundId} doesn't exist`) + }) + + it('get challenge setting - invalid id', async () => { + const response = await chai.request(app) + .get(`${basePath}/invalid`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + should.equal(response.status, 400) + should.equal(response.body.message, '"id" must be a valid GUID') + }) + }) + + describe('search challenge settings API tests', () => { + it('search challenge settings successfully 1', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.COPILOT_TOKEN}`) + .query({ page: 1, perPage: 10, name: name2.substring(1).toUpperCase() }) + should.equal(response.status, 200) + should.equal(response.headers['x-page'], '1') + should.equal(response.headers['x-per-page'], '10') + should.equal(response.headers['x-total'], '1') + should.equal(response.headers['x-total-pages'], '1') + should.exist(response.headers['link']) + + const result = response.body + should.equal(result.length, 1) + should.equal(result[0].id, id2) + should.equal(result[0].name, name2) + }) + + it('search challenge settings successfully 2', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.COPILOT_TOKEN}`) + .query({ name: 'xxjklsdjfihx' }) + should.equal(response.status, 200) + should.equal(response.headers['x-page'], '1') + should.equal(response.headers['x-per-page'], '20') + should.equal(response.headers['x-total'], '0') + should.equal(response.headers['x-total-pages'], '0') + should.equal(response.body.length, 0) + }) + + it('search challenge settings - invalid page', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ page: -1 }) + should.equal(response.status, 400) + should.equal(response.body.message, '"page" must be larger than or equal to 1') + }) + + it('search challenge settings - invalid perPage', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ perPage: 'abc' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"perPage" must be a number') + }) + + it('search challenge settings - unexpected field', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ other: 123 }) + should.equal(response.status, 400) + should.equal(response.body.message, '"other" is not allowed') + }) + }) + + describe('fully update challenge setting API tests', () => { + it('fully update challenge setting successfully', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id2}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: `${name2}-updated` + }) + should.equal(response.status, 200) + const result = response.body + should.equal(result.id, id2) + should.equal(result.name, `${name2}-updated`) + }) + + it('fully update challenge setting - name already used', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id2}`) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name + }) + should.equal(response.status, 409) + should.equal(response.body.message, `ChallengeSetting with name: ${name} already exist`) + }) + + it('fully update challenge setting - forbidden', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.USER_TOKEN}`) + .send({ + name: 'xlkjfeug' + }) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + + it('fully update challenge setting - not found', async () => { + const response = await chai.request(app) + .put(`${basePath}/${notFoundId}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: 'fjgjgjg' + }) + should.equal(response.status, 404) + should.equal(response.body.message, `ChallengeSetting with id: ${notFoundId} doesn't exist`) + }) + + it('fully update challenge setting - invalid id', async () => { + const response = await chai.request(app) + .put(`${basePath}/invalid`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: 'fjgjgjg' + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"id" must be a valid GUID') + }) + + it('fully update challenge setting - null name', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: null + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('fully update challenge setting - invalid name', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: { invalid: 123 } + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('fully update challenge setting - empty name', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: '' + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" is not allowed to be empty') + }) + }) +}) diff --git a/test/e2e/challenge.type.api.test.js b/test/e2e/challenge.type.api.test.js new file mode 100644 index 00000000..dfa57f29 --- /dev/null +++ b/test/e2e/challenge.type.api.test.js @@ -0,0 +1,506 @@ +/* + * E2E tests of challenge type API + */ + +// During the test the env variable is set to test +process.env.NODE_ENV = 'test' + +require('../../app-bootstrap') +const config = require('config') +const uuid = require('uuid/v4') +const chai = require('chai') +const chaiHttp = require('chai-http') +const app = require('../../app') + +const should = chai.should() +chai.use(chaiHttp) + +const basePath = '/challengeTypes' + +describe('challenge type API E2E tests', () => { + // created entity ids + let id + let id2 + // random names + const name = `test1${new Date().getTime()}` + const name2 = `test2${new Date().getTime()}` + const notFoundId = uuid() + + describe('create challenge type API tests', () => { + it('create challenge type successfully 1', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_FULL_ACCESS_TOKEN}`) + .send({ + name, + description: 'desc', + isActive: true + }) + should.equal(response.status, 201) + const result = response.body + should.equal(result.name, name) + should.equal(result.description, 'desc') + should.equal(result.isActive, true) + should.exist(result.id) + id = result.id + }) + + it('create challenge type successfully 2', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name: name2, + description: 'desc2', + isActive: false + }) + should.equal(response.status, 201) + const result = response.body + should.equal(result.name, name2) + should.equal(result.description, 'desc2') + should.equal(result.isActive, false) + should.exist(result.id) + id2 = result.id + }) + + it('create challenge type - missing token', async () => { + const response = await chai.request(app) + .post(basePath) + .send({ + name: 'ueueue7878', + description: 'desc', + isActive: true + }) + should.equal(response.status, 401) + should.equal(response.body.message, 'No token provided.') + }) + + it('create challenge type - invalid bearer format', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', 'invalid format') + .send({ + name: 'ueueue7878', + description: 'desc', + isActive: true + }) + should.equal(response.status, 401) + should.equal(response.body.message, 'No token provided.') + }) + + it('create challenge type - invalid token', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.INVALID_TOKEN}`) + .send({ + name: 'ueueue7878', + description: 'desc', + isActive: true + }) + should.equal(response.status, 401) + should.equal(response.body.message, 'Failed to authenticate token.') + }) + + it('create challenge type - expired token', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.EXPIRED_TOKEN}`) + .send({ + name: 'ueueue7878', + description: 'desc', + isActive: true + }) + should.equal(response.status, 401) + should.equal(response.body.message, 'Failed to authenticate token.') + }) + + it('create challenge type - name already used', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name, + description: 'desc', + isActive: false + }) + should.equal(response.status, 409) + should.equal(response.body.message, `ChallengeType with name: ${name} already exist`) + }) + + it('create challenge type - forbidden', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_READ_ACCESS_TOKEN}`) + .send({ + name: 'flskjdf', + description: 'desc', + isActive: false + }) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + + it('create challenge type - missing name', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + description: 'desc', + isActive: false + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" is required') + }) + + it('create challenge type - invalid name', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name: ['xx'], + description: 'desc', + isActive: false + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('create challenge type - invalid isActive', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name: 'fjsioijdf', + description: 'desc', + isActive: 'abc' + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"isActive" must be a boolean') + }) + + it('create challenge type - unexpected field', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name: 'flskjdf', + description: 'desc', + isActive: false, + other: 'def' + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"other" is not allowed') + }) + }) + + describe('get challenge type API tests', () => { + it('get challenge type successfully', async () => { + const response = await chai.request(app) + .get(`${basePath}/${id2}`) + .set('Authorization', `Bearer ${config.COPILOT_TOKEN}`) + should.equal(response.status, 200) + const result = response.body + should.equal(result.id, id2) + should.equal(result.name, name2) + should.equal(result.description, 'desc2') + should.equal(result.isActive, false) + }) + + it('get challenge type - not found', async () => { + const response = await chai.request(app) + .get(`${basePath}/${notFoundId}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + should.equal(response.status, 404) + should.equal(response.body.message, `ChallengeType with id: ${notFoundId} doesn't exist`) + }) + + it('get challenge type - invalid id', async () => { + const response = await chai.request(app) + .get(`${basePath}/invalid`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + should.equal(response.status, 400) + should.equal(response.body.message, '"id" must be a valid GUID') + }) + }) + + describe('search challenge types API tests', () => { + it('search challenge types successfully 1', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.COPILOT_TOKEN}`) + .query({ + page: 1, + perPage: 10, + name: name2.substring(1).toUpperCase(), + description: 'desc', + isActive: false + }) + should.equal(response.status, 200) + should.equal(response.headers['x-page'], '1') + should.equal(response.headers['x-per-page'], '10') + should.equal(response.headers['x-total'], '1') + should.equal(response.headers['x-total-pages'], '1') + should.exist(response.headers['link']) + + const result = response.body + should.equal(result.length, 1) + should.equal(result[0].id, id2) + should.equal(result[0].name, name2) + should.equal(result[0].description, 'desc2') + should.equal(result[0].isActive, false) + }) + + it('search challenge types successfully 2', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.COPILOT_TOKEN}`) + .query({ name: 'xxjklsdjfihx' }) + should.equal(response.status, 200) + should.equal(response.headers['x-page'], '1') + should.equal(response.headers['x-per-page'], '20') + should.equal(response.headers['x-total'], '0') + should.equal(response.headers['x-total-pages'], '0') + should.equal(response.body.length, 0) + }) + + it('search challenge types - invalid page', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ page: -1 }) + should.equal(response.status, 400) + should.equal(response.body.message, '"page" must be larger than or equal to 1') + }) + + it('search challenge types - invalid perPage', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ perPage: 'abc' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"perPage" must be a number') + }) + + it('search challenge types - unexpected field', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ other: 123 }) + should.equal(response.status, 400) + should.equal(response.body.message, '"other" is not allowed') + }) + }) + + describe('fully update challenge type API tests', () => { + it('fully update challenge type successfully', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id2}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: `${name2}-updated`, + description: 'desc222', + isActive: true + }) + should.equal(response.status, 200) + const result = response.body + should.equal(result.id, id2) + should.equal(result.name, `${name2}-updated`) + should.equal(result.description, 'desc222') + should.equal(result.isActive, true) + }) + + it('fully update challenge type - name already used', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id2}`) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name, + description: 'desc', + isActive: false + }) + should.equal(response.status, 409) + should.equal(response.body.message, `ChallengeType with name: ${name} already exist`) + }) + + it('fully update challenge type - forbidden', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.USER_TOKEN}`) + .send({ + name: 'xlkjfeug', + description: 'desc', + isActive: false + }) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + + it('fully update challenge type - not found', async () => { + const response = await chai.request(app) + .put(`${basePath}/${notFoundId}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: 'fjgjgjg', + description: 'desc', + isActive: false + }) + should.equal(response.status, 404) + should.equal(response.body.message, `ChallengeType with id: ${notFoundId} doesn't exist`) + }) + + it('fully update challenge type - invalid id', async () => { + const response = await chai.request(app) + .put(`${basePath}/invalid`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: 'fjgjgjg', + description: 'desc', + isActive: false + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"id" must be a valid GUID') + }) + + it('fully update challenge type - null name', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: null, + description: 'desc', + isActive: false + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('fully update challenge type - invalid name', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: { invalid: 123 }, + description: 'desc', + isActive: false + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('fully update challenge type - empty name', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: '', + description: 'desc', + isActive: false + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" is not allowed to be empty') + }) + + it('fully update challenge type - invalid isActive', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: 'slkdjfhhghg', + description: 'desc', + isActive: [] + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"isActive" must be a boolean') + }) + }) + + describe('partially update challenge type API tests', () => { + it('partially update challenge type successfully', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id2}`) + .set('Authorization', `Bearer ${config.M2M_FULL_ACCESS_TOKEN}`) + .send({ + name: `${name2}-33`, + description: 'desc33' + }) + should.equal(response.status, 200) + const result = response.body + should.equal(result.id, id2) + should.equal(result.name, `${name2}-33`) + should.equal(result.description, 'desc33') + should.equal(result.isActive, true) + }) + + it('partially update challenge type - name already used', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id2}`) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ name }) + should.equal(response.status, 409) + should.equal(response.body.message, `ChallengeType with name: ${name} already exist`) + }) + + it('partially update challenge type - forbidden', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.M2M_READ_ACCESS_TOKEN}`) + .send({ name: 'testing2' }) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + + it('partially update challenge type - not found', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${notFoundId}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: 'testing2' }) + should.equal(response.status, 404) + should.equal(response.body.message, `ChallengeType with id: ${notFoundId} doesn't exist`) + }) + + it('partially update challenge type - invalid id', async () => { + const response = await chai.request(app) + .patch(`${basePath}/invalid`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: 'testing2' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"id" must be a valid GUID') + }) + + it('partially update challenge type - null name', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: null }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('partially update challenge type - invalid name', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: { invalid: 123 } }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('partially update challenge type - empty name', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: '' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" is not allowed to be empty') + }) + + it('partially update challenge type - invalid description', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ description: { invalid: 123 } }) + should.equal(response.status, 400) + should.equal(response.body.message, '"description" must be a string') + }) + }) +}) diff --git a/test/e2e/health.api.test.js b/test/e2e/health.api.test.js new file mode 100644 index 00000000..075804fc --- /dev/null +++ b/test/e2e/health.api.test.js @@ -0,0 +1,23 @@ +/* + * E2E tests of health API + */ + +// During the test the env variable is set to test +process.env.NODE_ENV = 'test' + +require('../../app-bootstrap') +const chai = require('chai') +const chaiHttp = require('chai-http') +const app = require('../../app') + +const should = chai.should() +chai.use(chaiHttp) + +describe('health API tests', () => { + it('Check health successfully', async () => { + const response = await chai.request(app) + .get('/health') + should.equal(response.status, 200) + should.equal(response.body.checksRun >= 1, true) + }) +}) diff --git a/test/e2e/phase.api.test.js b/test/e2e/phase.api.test.js new file mode 100644 index 00000000..c13ff288 --- /dev/null +++ b/test/e2e/phase.api.test.js @@ -0,0 +1,623 @@ +/* + * E2E tests of phase API + */ + +// During the test the env variable is set to test +process.env.NODE_ENV = 'test' + +require('../../app-bootstrap') +const config = require('config') +const uuid = require('uuid/v4') +const chai = require('chai') +const chaiHttp = require('chai-http') +const app = require('../../app') + +const should = chai.should() +chai.use(chaiHttp) + +const basePath = '/challengePhases' + +describe('phase API E2E tests', () => { + // created entity ids + let id + let id2 + // random names + const name = `test1${new Date().getTime()}` + const name2 = `test2${new Date().getTime()}` + const notFoundId = uuid() + + describe('create phase API tests', () => { + it('create phase successfully 1', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_FULL_ACCESS_TOKEN}`) + .send({ + name, + description: 'desc', + isActive: true, + duration: 123 + }) + should.equal(response.status, 201) + const result = response.body + should.equal(result.name, name) + should.equal(result.description, 'desc') + should.equal(result.isActive, true) + should.equal(result.duration, 123) + should.exist(result.id) + id = result.id + }) + + it('create phase successfully 2', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name: name2, + description: 'desc2', + predecessor: id, + isActive: false, + duration: 456 + }) + should.equal(response.status, 201) + const result = response.body + should.equal(result.name, name2) + should.equal(result.description, 'desc2') + should.equal(result.predecessor, id) + should.equal(result.isActive, false) + should.equal(result.duration, 456) + should.exist(result.id) + id2 = result.id + }) + + it('create phase - missing token', async () => { + const response = await chai.request(app) + .post(basePath) + .send({ + name: 'ueueue7878', + description: 'desc', + isActive: true, + duration: 123 + }) + should.equal(response.status, 401) + should.equal(response.body.message, 'No token provided.') + }) + + it('create phase - invalid bearer format', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', 'invalid format') + .send({ + name: 'ueueue7878', + description: 'desc', + isActive: true, + duration: 123 + }) + should.equal(response.status, 401) + should.equal(response.body.message, 'No token provided.') + }) + + it('create phase - invalid token', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.INVALID_TOKEN}`) + .send({ + name: 'ueueue7878', + description: 'desc', + isActive: true, + duration: 123 + }) + should.equal(response.status, 401) + should.equal(response.body.message, 'Failed to authenticate token.') + }) + + it('create phase - expired token', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.EXPIRED_TOKEN}`) + .send({ + name: 'ueueue7878', + description: 'desc', + isActive: true, + duration: 123 + }) + should.equal(response.status, 401) + should.equal(response.body.message, 'Failed to authenticate token.') + }) + + it('create phase - predecessor not found', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name: 'igj58687', + description: 'desc2', + predecessor: notFoundId, + isActive: false, + duration: 456 + }) + should.equal(response.status, 404) + should.equal(response.body.message, `Phase with id: ${notFoundId} doesn't exist`) + }) + + it('create phase - name already used', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name, + description: 'desc', + isActive: false, + duration: 456 + }) + should.equal(response.status, 409) + should.equal(response.body.message, `Phase with name: ${name} already exist`) + }) + + it('create phase - forbidden', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_READ_ACCESS_TOKEN}`) + .send({ + name: 'flskjdf', + description: 'desc', + isActive: false, + duration: 456 + }) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + + it('create phase - missing name', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + description: 'desc', + isActive: false, + duration: 456 + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" is required') + }) + + it('create phase - invalid name', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name: ['xx'], + description: 'desc', + isActive: false, + duration: 456 + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('create phase - null description', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name: 'fijgijfgfg', + description: null, + isActive: false, + duration: 456 + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"description" must be a string') + }) + + it('create phase - invalid predecessor', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name: 'fjsioijdf', + description: 'desc', + predecessor: 'abc', + isActive: false, + duration: 456 + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"predecessor" must be a valid GUID') + }) + + it('create phase - unexpected field', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name: 'flskjdf', + description: 'desc', + isActive: false, + duration: 456, + other: 'def' + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"other" is not allowed') + }) + }) + + describe('get phase API tests', () => { + it('get phase successfully', async () => { + const response = await chai.request(app) + .get(`${basePath}/${id2}`) + .set('Authorization', `Bearer ${config.COPILOT_TOKEN}`) + should.equal(response.status, 200) + const result = response.body + should.equal(result.id, id2) + should.equal(result.name, name2) + should.equal(result.description, 'desc2') + should.equal(result.predecessor, id) + should.equal(result.isActive, false) + should.equal(result.duration, 456) + }) + + it('get phase - forbidden', async () => { + const response = await chai.request(app) + .get(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + + it('get phase - not found', async () => { + const response = await chai.request(app) + .get(`${basePath}/${notFoundId}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + should.equal(response.status, 404) + should.equal(response.body.message, `Phase with id: ${notFoundId} doesn't exist`) + }) + + it('get phase - invalid id', async () => { + const response = await chai.request(app) + .get(`${basePath}/invalid`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + should.equal(response.status, 400) + should.equal(response.body.message, '"phaseId" must be a valid GUID') + }) + }) + + describe('search phases API tests', () => { + it('search phases successfully 1', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.COPILOT_TOKEN}`) + .query({ page: 1, perPage: 10, name: name2.substring(1).toUpperCase() }) + should.equal(response.status, 200) + should.equal(response.headers['x-page'], '1') + should.equal(response.headers['x-per-page'], '10') + should.equal(response.headers['x-total'], '1') + should.equal(response.headers['x-total-pages'], '1') + should.exist(response.headers['link']) + + const result = response.body + should.equal(result.length, 1) + should.equal(result[0].id, id2) + should.equal(result[0].name, name2) + should.equal(result[0].description, 'desc2') + should.equal(result[0].predecessor, id) + should.equal(result[0].isActive, false) + should.equal(result[0].duration, 456) + }) + + it('search phases successfully 2', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.COPILOT_TOKEN}`) + .query({ name: 'xxjklsdjfihx' }) + should.equal(response.status, 200) + should.equal(response.headers['x-page'], '1') + should.equal(response.headers['x-per-page'], '20') + should.equal(response.headers['x-total'], '0') + should.equal(response.headers['x-total-pages'], '0') + should.equal(response.body.length, 0) + }) + + it('search phases - invalid page', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ page: -1 }) + should.equal(response.status, 400) + should.equal(response.body.message, '"page" must be larger than or equal to 1') + }) + + it('search phases - invalid perPage', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ perPage: 'abc' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"perPage" must be a number') + }) + + it('search phases - unexpected field', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ other: 123 }) + should.equal(response.status, 400) + should.equal(response.body.message, '"other" is not allowed') + }) + }) + + describe('fully update phase API tests', () => { + it('fully update phase successfully', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id2}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: `${name2}-updated`, + description: 'desc222', + predecessor: id, + isActive: true, + duration: 789 + }) + should.equal(response.status, 200) + const result = response.body + should.equal(result.id, id2) + should.equal(result.name, `${name2}-updated`) + should.equal(result.description, 'desc222') + should.equal(result.predecessor, id) + should.equal(result.isActive, true) + should.equal(result.duration, 789) + }) + + it('fully update phase - name already used', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id2}`) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name, + description: 'desc', + isActive: false, + duration: 456 + }) + should.equal(response.status, 409) + should.equal(response.body.message, `Phase with name: ${name} already exist`) + }) + + it('fully update phase - forbidden', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.USER_TOKEN}`) + .send({ + name: 'xlkjfeug', + description: 'desc', + isActive: false, + duration: 456 + }) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + + it('fully update phase - not found', async () => { + const response = await chai.request(app) + .put(`${basePath}/${notFoundId}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: 'fjgjgjg', + description: 'desc', + isActive: false, + duration: 456 + }) + should.equal(response.status, 404) + should.equal(response.body.message, `Phase with id: ${notFoundId} doesn't exist`) + }) + + it('fully update phase - invalid id', async () => { + const response = await chai.request(app) + .put(`${basePath}/invalid`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: 'fjgjgjg', + description: 'desc', + isActive: false, + duration: 456 + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"phaseId" must be a valid GUID') + }) + + it('fully update phase - null name', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: null, + description: 'desc', + isActive: false, + duration: 456 + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('fully update phase - invalid name', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: { invalid: 123 }, + description: 'desc', + isActive: false, + duration: 456 + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('fully update phase - empty name', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: '', + description: 'desc', + isActive: false, + duration: 456 + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" is not allowed to be empty') + }) + + it('fully update phase - invalid isActive', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: 'slkdjfhhghg', + description: 'desc', + isActive: [], + duration: 456 + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"isActive" must be a boolean') + }) + }) + + describe('partially update phase API tests', () => { + it('partially update phase successfully', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id2}`) + .set('Authorization', `Bearer ${config.M2M_FULL_ACCESS_TOKEN}`) + .send({ + name: `${name2}-33`, + description: 'desc33', + duration: 111 + }) + should.equal(response.status, 200) + const result = response.body + should.equal(result.id, id2) + should.equal(result.name, `${name2}-33`) + should.equal(result.description, 'desc33') + should.equal(result.predecessor, id) + should.equal(result.isActive, true) + should.equal(result.duration, 111) + }) + + it('partially update phase - name already used', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id2}`) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ name }) + should.equal(response.status, 409) + should.equal(response.body.message, `Phase with name: ${name} already exist`) + }) + + it('partially update phase - forbidden', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.M2M_READ_ACCESS_TOKEN}`) + .send({ name: 'testing2' }) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + + it('partially update phase - not found', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${notFoundId}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: 'testing2' }) + should.equal(response.status, 404) + should.equal(response.body.message, `Phase with id: ${notFoundId} doesn't exist`) + }) + + it('partially update phase - invalid id', async () => { + const response = await chai.request(app) + .patch(`${basePath}/invalid`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: 'testing2' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"phaseId" must be a valid GUID') + }) + + it('partially update phase - null name', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: null }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('partially update phase - invalid name', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: { invalid: 123 } }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('partially update phase - empty name', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: '' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" is not allowed to be empty') + }) + + it('partially update phase - invalid duration', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ duration: 0 }) + should.equal(response.status, 400) + should.equal(response.body.message, '"duration" must be a positive number') + }) + }) + + describe('remove phase API tests', () => { + it('remove phase - forbidden', async () => { + const response = await chai.request(app) + .delete(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.USER_TOKEN}`) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + + it('remove phase - there is dependent phase', async () => { + const response = await chai.request(app) + .delete(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.M2M_FULL_ACCESS_TOKEN}`) + should.equal(response.status, 400) + should.equal(response.body.message, `Can't delete phase ${id} because it is preceding phase of other phases.`) + }) + + it('remove phase successfully 1', async () => { + const response = await chai.request(app) + .delete(`${basePath}/${id2}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + should.equal(response.status, 200) + should.equal(response.body.id, id2) + }) + + it('remove phase successfully 2', async () => { + const response = await chai.request(app) + .delete(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.M2M_FULL_ACCESS_TOKEN}`) + should.equal(response.status, 200) + should.equal(response.body.id, id) + }) + + it('remove phase - not found', async () => { + const response = await chai.request(app) + .delete(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + should.equal(response.status, 404) + should.equal(response.body.message, `Phase with id: ${id} doesn't exist`) + }) + + it('remove phase - invalid id', async () => { + const response = await chai.request(app) + .delete(`${basePath}/invalid`) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + should.equal(response.status, 400) + should.equal(response.body.message, '"phaseId" must be a valid GUID') + }) + }) +}) diff --git a/test/e2e/timeline.template.api.test.js b/test/e2e/timeline.template.api.test.js new file mode 100644 index 00000000..a1720664 --- /dev/null +++ b/test/e2e/timeline.template.api.test.js @@ -0,0 +1,638 @@ +/* + * E2E tests of timeline template API + */ + +// During the test the env variable is set to test +process.env.NODE_ENV = 'test' + +require('../../app-bootstrap') +const config = require('config') +const uuid = require('uuid/v4') +const chai = require('chai') +const chaiHttp = require('chai-http') +const app = require('../../app') +const helper = require('../../src/common/helper') + +const should = chai.should() +chai.use(chaiHttp) + +const basePath = '/timelineTemplates' + +describe('timeline template API E2E tests', () => { + // created entity ids + let id + let id2 + // random names + const name = `test1${new Date().getTime()}` + const name2 = `test2${new Date().getTime()}` + const notFoundId = uuid() + // reference data + let phase + + before(async () => { + phase = await helper.create('Phase', { + id: uuid(), + name: `phase${new Date().getTime()}`, // random name + description: 'desc', + isActive: true, + duration: 123 + }) + }) + + after(async () => { + await phase.delete() + }) + + describe('create timeline template API tests', () => { + it('create timeline template successfully 1', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_FULL_ACCESS_TOKEN}`) + .send({ + name, + description: 'desc', + isActive: true, + phases: [phase] + }) + should.equal(response.status, 201) + const result = response.body + should.exist(result.id) + id = result.id + should.equal(result.name, name) + should.equal(result.description, 'desc') + should.equal(result.isActive, true) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, phase.id) + should.equal(result.phases[0].name, phase.name) + should.equal(result.phases[0].description, phase.description) + should.equal(result.phases[0].isActive, phase.isActive) + should.equal(result.phases[0].duration, phase.duration) + }) + + it('create timeline template successfully 2', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name: name2, + description: 'desc', + isActive: false, + phases: [phase] + }) + should.equal(response.status, 201) + const result = response.body + should.exist(result.id) + id2 = result.id + should.equal(result.name, name2) + should.equal(result.description, 'desc') + should.equal(result.isActive, false) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, phase.id) + should.equal(result.phases[0].name, phase.name) + should.equal(result.phases[0].description, phase.description) + should.equal(result.phases[0].isActive, phase.isActive) + should.equal(result.phases[0].duration, phase.duration) + }) + + it('create timeline template - missing token', async () => { + const response = await chai.request(app) + .post(basePath) + .send({ + name, + description: 'desc', + isActive: true, + phases: [phase] + }) + should.equal(response.status, 401) + should.equal(response.body.message, 'No token provided.') + }) + + it('create timeline template - invalid bearer format', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', 'invalid format') + .send({ + name, + description: 'desc', + isActive: true, + phases: [phase] + }) + should.equal(response.status, 401) + should.equal(response.body.message, 'No token provided.') + }) + + it('create timeline template - invalid token', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.INVALID_TOKEN}`) + .send({ + name, + description: 'desc', + isActive: true, + phases: [phase] + }) + should.equal(response.status, 401) + should.equal(response.body.message, 'Failed to authenticate token.') + }) + + it('create timeline template - expired token', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.EXPIRED_TOKEN}`) + .send({ + name, + description: 'desc', + isActive: true, + phases: [phase] + }) + should.equal(response.status, 401) + should.equal(response.body.message, 'Failed to authenticate token.') + }) + + it('create timeline template - forbidden', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_READ_ACCESS_TOKEN}`) + .send({ + name, + description: 'desc', + isActive: true, + phases: [phase] + }) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + + it('create timeline template - name already used', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name, + description: 'desc', + isActive: true, + phases: [phase] + }) + should.equal(response.status, 409) + should.equal(response.body.message, `TimelineTemplate with name: ${name} already exist`) + }) + + it('create timeline template - missing name', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + description: 'desc', + isActive: true, + phases: [phase] + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" is required') + }) + + it('create timeline template - invalid name', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name: ['xx'], + description: 'desc', + isActive: true, + phases: [phase] + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('create timeline template - invalid description', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name: 'ghghguue', + description: ['desc'], + isActive: true, + phases: [phase] + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"description" must be a string') + }) + + it('create timeline template - invalid phases', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name, + description: 'desc', + isActive: true, + phases: [{ name: 'invalid' }] + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"id" is required') + }) + + it('create timeline template - unexpected field', async () => { + const response = await chai.request(app) + .post(basePath) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name: 'flskjdf', + description: 'desc', + isActive: true, + phases: [phase], + other: 'def' + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"other" is not allowed') + }) + }) + + describe('get timeline template API tests', () => { + it('get timeline template successfully', async () => { + const response = await chai.request(app) + .get(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.COPILOT_TOKEN}`) + should.equal(response.status, 200) + const result = response.body + should.equal(result.id, id) + should.equal(result.name, name) + should.equal(result.description, 'desc') + should.equal(result.isActive, true) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, phase.id) + should.equal(result.phases[0].name, phase.name) + should.equal(result.phases[0].description, phase.description) + should.equal(result.phases[0].isActive, phase.isActive) + should.equal(result.phases[0].duration, phase.duration) + }) + + it('get timeline template - forbidden', async () => { + const response = await chai.request(app) + .get(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + + it('get timeline template - not found', async () => { + const response = await chai.request(app) + .get(`${basePath}/${notFoundId}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + should.equal(response.status, 404) + should.equal(response.body.message, `TimelineTemplate with id: ${notFoundId} doesn't exist`) + }) + + it('get timeline template - invalid id', async () => { + const response = await chai.request(app) + .get(`${basePath}/invalid`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + should.equal(response.status, 400) + should.equal(response.body.message, '"timelineTemplateId" must be a valid GUID') + }) + }) + + describe('search timeline templates API tests', () => { + it('search timeline templates successfully 1', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.COPILOT_TOKEN}`) + .query({ page: 1, perPage: 10, name: name.substring(1).toUpperCase() }) + should.equal(response.status, 200) + should.equal(response.headers['x-page'], '1') + should.equal(response.headers['x-per-page'], '10') + should.equal(response.headers['x-total'], '1') + should.equal(response.headers['x-total-pages'], '1') + should.exist(response.headers['link']) + + const result = response.body + should.equal(result.length, 1) + should.equal(result[0].id, id) + should.equal(result[0].name, name) + should.equal(result[0].description, 'desc') + should.equal(result[0].isActive, true) + should.equal(result[0].phases.length, 1) + should.equal(result[0].phases[0].id, phase.id) + should.equal(result[0].phases[0].name, phase.name) + should.equal(result[0].phases[0].description, phase.description) + should.equal(result[0].phases[0].isActive, phase.isActive) + should.equal(result[0].phases[0].duration, phase.duration) + }) + + it('search timeline templates successfully 2', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.COPILOT_TOKEN}`) + .query({ name: 'xxjklsdjfihx' }) + should.equal(response.status, 200) + should.equal(response.headers['x-page'], '1') + should.equal(response.headers['x-per-page'], '20') + should.equal(response.headers['x-total'], '0') + should.equal(response.headers['x-total-pages'], '0') + should.equal(response.body.length, 0) + }) + + it('search timeline templates - invalid page', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ page: -1 }) + should.equal(response.status, 400) + should.equal(response.body.message, '"page" must be larger than or equal to 1') + }) + + it('search timeline templates - invalid perPage', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ perPage: 'abc' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"perPage" must be a number') + }) + + it('search timeline templates - unexpected field', async () => { + const response = await chai.request(app) + .get(basePath) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .query({ other: 123 }) + should.equal(response.status, 400) + should.equal(response.body.message, '"other" is not allowed') + }) + }) + + describe('fully update timeline template API tests', () => { + it('fully update timeline template successfully', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: `${name}-updated`, + description: 'desc222', + isActive: false, + phases: [phase] + }) + should.equal(response.status, 200) + const result = response.body + should.equal(result.id, id) + should.equal(result.name, `${name}-updated`) + should.equal(result.description, 'desc222') + should.equal(result.isActive, false) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, phase.id) + should.equal(result.phases[0].name, phase.name) + should.equal(result.phases[0].description, phase.description) + should.equal(result.phases[0].isActive, phase.isActive) + should.equal(result.phases[0].duration, phase.duration) + }) + + it('fully update timeline template - name already used', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ + name: name2, + description: 'desc', + isActive: false, + phases: [phase] + }) + should.equal(response.status, 409) + should.equal(response.body.message, `TimelineTemplate with name: ${name2} already exist`) + }) + + it('fully update timeline template - forbidden', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.USER_TOKEN}`) + .send({ + name: 'xlkjfeug', + description: 'desc', + isActive: false, + phases: [phase] + }) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + + it('fully update timeline template - not found', async () => { + const response = await chai.request(app) + .put(`${basePath}/${notFoundId}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: 'fjgjgjg', + description: 'desc', + isActive: false, + phases: [phase] + }) + should.equal(response.status, 404) + should.equal(response.body.message, `TimelineTemplate with id: ${notFoundId} doesn't exist`) + }) + + it('fully update timeline template - invalid id', async () => { + const response = await chai.request(app) + .put(`${basePath}/invalid`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: 'fjgjgjg', + description: 'desc', + isActive: false, + phases: [phase] + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"timelineTemplateId" must be a valid GUID') + }) + + it('fully update timeline template - null name', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: null, + description: 'desc', + isActive: false, + phases: [phase] + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('fully update timeline template - invalid name', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: { invalid: 123 }, + description: 'desc', + isActive: false, + phases: [phase] + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('fully update timeline template - empty name', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: '', + description: 'desc', + isActive: false, + phases: [phase] + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" is not allowed to be empty') + }) + + it('fully update timeline template - invalid isActive', async () => { + const response = await chai.request(app) + .put(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ + name: 'slkdjfhhghg', + description: 'desc', + isActive: [], + phases: [phase] + }) + should.equal(response.status, 400) + should.equal(response.body.message, '"isActive" must be a boolean') + }) + }) + + describe('partially update timeline template API tests', () => { + it('partially update timeline template successfully', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.M2M_FULL_ACCESS_TOKEN}`) + .send({ + name: `${name}-33`, + description: 'desc33' + }) + should.equal(response.status, 200) + const result = response.body + should.equal(result.id, id) + should.equal(result.name, `${name}-33`) + should.equal(result.description, 'desc33') + should.equal(result.isActive, false) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, phase.id) + should.equal(result.phases[0].name, phase.name) + should.equal(result.phases[0].description, phase.description) + should.equal(result.phases[0].isActive, phase.isActive) + should.equal(result.phases[0].duration, phase.duration) + }) + + it('partially update timeline template - name already used', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + .send({ name: name2 }) + should.equal(response.status, 409) + should.equal(response.body.message, `TimelineTemplate with name: ${name2} already exist`) + }) + + it('partially update timeline template - forbidden', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.M2M_READ_ACCESS_TOKEN}`) + .send({ name: 'testing2' }) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + + it('partially update timeline template - not found', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${notFoundId}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: 'testing2' }) + should.equal(response.status, 404) + should.equal(response.body.message, `TimelineTemplate with id: ${notFoundId} doesn't exist`) + }) + + it('partially update timeline template - invalid id', async () => { + const response = await chai.request(app) + .patch(`${basePath}/invalid`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: 'testing2' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"timelineTemplateId" must be a valid GUID') + }) + + it('partially update timeline template - null name', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: null }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('partially update timeline template - invalid name', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: { invalid: 123 } }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" must be a string') + }) + + it('partially update timeline template - empty name', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ name: '' }) + should.equal(response.status, 400) + should.equal(response.body.message, '"name" is not allowed to be empty') + }) + + it('partially update timeline template - invalid phases', async () => { + const response = await chai.request(app) + .patch(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + .send({ phases: ['abc'] }) + should.equal(response.status, 400) + should.equal(response.body.message, '"0" must be an object') + }) + }) + + describe('remove timeline template API tests', () => { + it('remove timeline template - forbidden', async () => { + const response = await chai.request(app) + .delete(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.USER_TOKEN}`) + should.equal(response.status, 403) + should.equal(response.body.message, 'You are not allowed to perform this action!') + }) + + it('remove timeline template successfully 1', async () => { + const response = await chai.request(app) + .delete(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) + should.equal(response.status, 200) + should.equal(response.body.id, id) + }) + + it('remove timeline template successfully 2', async () => { + const response = await chai.request(app) + .delete(`${basePath}/${id2}`) + .set('Authorization', `Bearer ${config.M2M_FULL_ACCESS_TOKEN}`) + should.equal(response.status, 200) + should.equal(response.body.id, id2) + }) + + it('remove timeline template - not found', async () => { + const response = await chai.request(app) + .delete(`${basePath}/${id}`) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + should.equal(response.status, 404) + should.equal(response.body.message, `TimelineTemplate with id: ${id} doesn't exist`) + }) + + it('remove timeline template - invalid id', async () => { + const response = await chai.request(app) + .delete(`${basePath}/invalid`) + .set('Authorization', `Bearer ${config.M2M_UPDATE_ACCESS_TOKEN}`) + should.equal(response.status, 400) + should.equal(response.body.message, '"timelineTemplateId" must be a valid GUID') + }) + }) +}) diff --git a/test/testHelper.js b/test/testHelper.js new file mode 100644 index 00000000..4502c342 --- /dev/null +++ b/test/testHelper.js @@ -0,0 +1,120 @@ +/** + * This file defines common helper methods used for tests + */ +const uuid = require('uuid/v4') +const config = require('config') +const helper = require('../src/common/helper') +const constants = require('../app-constants') + +const esClient = helper.getESClient() + +let challengeType +let challengeSetting +let phase +let timelineTemplate +let challenge + +/** + * Create test data + */ +async function createData () { + challengeType = await helper.create('ChallengeType', { + id: uuid(), + name: `type-${new Date().getTime()}`, + description: 'desc', + isActive: true + }) + challengeSetting = await helper.create('ChallengeSetting', { + id: uuid(), + name: `setting-${new Date().getTime()}` + }) + phase = await helper.create('Phase', { + id: uuid(), + name: `phase-${new Date().getTime()}`, + description: 'desc', + isActive: true, + duration: 123 + }) + timelineTemplate = await helper.create('TimelineTemplate', { + id: uuid(), + name: `tt-${new Date().getTime()}`, + description: 'desc', + isActive: true, + phases: [phase] + }) + challenge = await helper.create('Challenge', { + id: uuid(), + typeId: challengeType.id, + track: 'track', + name: `a B c challenge${new Date().getTime()}`, + description: 'desc', + challengeSettings: [{ type: challengeSetting.id, value: 'value' }], + timelineTemplateId: timelineTemplate.id, + phases: [phase], + prizeSets: [{ + type: constants.prizeSetTypes.ChallengePrizes, + description: 'ddd', + prizes: [{ + description: 'some prize', + type: 'type', + value: 800 + }] + }], + reviewType: 'review type', + tags: ['tag1'], + projectId: 111, + legacyId: 222, + forumId: 333, + startDate: new Date(), + status: constants.challengeStatuses.Active, + groups: ['group1'], + created: new Date(), + createdBy: 'admin' + }) + // create challenge in Elasticsearch + await esClient.create({ + index: config.ES.ES_INDEX, + type: config.ES.ES_TYPE, + id: challenge.id, + body: challenge, + refresh: 'true' // refresh ES so that it is visible for read operations instantly + }) +} + +/** + * Clear test data + */ +async function clearData () { + // remove challenge in Elasticsearch + await esClient.delete({ + index: config.ES.ES_INDEX, + type: config.ES.ES_TYPE, + id: challenge.id, + refresh: 'true' // refresh ES so that it is effective for read operations instantly + }) + + await challenge.delete() + await timelineTemplate.delete() + await phase.delete() + await challengeSetting.delete() + await challengeType.delete() +} + +/** + * Get created test data. + */ +function getData () { + return { + challengeType: challengeType.originalItem(), + challengeSetting: challengeSetting.originalItem(), + phase: phase.originalItem(), + timelineTemplate: timelineTemplate.originalItem(), + challenge: challenge.originalItem() + } +} + +module.exports = { + createData, + clearData, + getData +} diff --git a/test/unit/AttachmentService.test.js b/test/unit/AttachmentService.test.js new file mode 100644 index 00000000..a1959743 --- /dev/null +++ b/test/unit/AttachmentService.test.js @@ -0,0 +1,171 @@ +/* + * Unit tests of attachment service + */ + +// During the test the env variable is set to test +process.env.NODE_ENV = 'test' + +require('../../app-bootstrap') +const fs = require('fs') +const path = require('path') +const uuid = require('uuid/v4') +const chai = require('chai') +const service = require('../../src/services/AttachmentService') +const testHelper = require('../testHelper') + +const should = chai.should() + +const attachmentContent = fs.readFileSync(path.join(__dirname, '../attachment.txt')) + +describe('attachment service unit tests', () => { + // created attachment id + let id + // generated data + let data + const notFoundId = uuid() + + before(async () => { + await testHelper.createData() + data = testHelper.getData() + }) + + after(async () => { + await testHelper.clearData() + }) + + describe('upload attachment tests', () => { + it('upload attachment successfully', async () => { + const result = await service.uploadAttachment({ + isMachine: true + }, data.challenge.id, { + attachment: { + data: attachmentContent, + mimetype: 'text/plain', + name: 'attachment.txt', + size: attachmentContent.length + } + }) + should.exist(result.id) + id = result.id + should.equal(result.fileSize, attachmentContent.length) + should.equal(result.fileName, 'attachment.txt') + should.equal(result.challengeId, data.challenge.id) + }) + + it('upload attachment - forbidden', async () => { + try { + await service.uploadAttachment({ + roles: ['user'] + }, data.challenge.id, { + attachment: { + data: attachmentContent, + mimetype: 'text/plain', + name: 'attachment.txt', + size: attachmentContent.length + } + }) + } catch (e) { + should.equal(e.message, 'You are not allowed to upload attachment of the challenge.') + return + } + throw new Error('should not reach here') + }) + + it('upload attachment - file too large', async () => { + try { + await service.uploadAttachment({ + isMachine: true + }, data.challenge.id, { + attachment: { + truncated: true, + data: attachmentContent, + mimetype: 'text/plain', + name: 'attachment.txt', + size: attachmentContent.length + } + }) + } catch (e) { + should.equal(e.message.indexOf('attachment is too large') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('upload attachment - challenge not found', async () => { + try { + await service.uploadAttachment({ + isMachine: true + }, notFoundId, { + attachment: { + data: attachmentContent, + mimetype: 'text/plain', + name: 'attachment.txt', + size: attachmentContent.length + } + }) + } catch (e) { + should.equal(e.message, `Challenge with id: ${notFoundId} doesn't exist`) + return + } + throw new Error('should not reach here') + }) + }) + + describe('download attachment tests', () => { + it('download attachment successfully', async () => { + const result = await service.downloadAttachment({ isMachine: true }, data.challenge.id, id) + should.equal(result.fileName, 'attachment.txt') + should.equal(attachmentContent.compare(result.data), 0) + }) + + it('download attachment - forbidden', async () => { + try { + await service.downloadAttachment({ roles: ['user'], userId: 678678 }, data.challenge.id, id) + } catch (e) { + should.equal(e.message, 'You are not allowed to download attachment of the challenge.') + return + } + throw new Error('should not reach here') + }) + + it('download attachment - attachment not found', async () => { + try { + await service.downloadAttachment({ isMachine: true }, data.challenge.id, notFoundId) + } catch (e) { + should.equal(e.message, `Attachment with id: ${notFoundId} doesn't exist`) + return + } + throw new Error('should not reach here') + }) + + it('download attachment - challenge id mismatched', async () => { + try { + await service.downloadAttachment({ isMachine: true }, notFoundId, id) + } catch (e) { + should.equal(e.message, 'The attachment challengeId does not match the path challengeId.') + return + } + throw new Error('should not reach here') + }) + + it('download attachment - invalid id', async () => { + try { + await service.downloadAttachment({ isMachine: true }, data.challenge.id, 'invalid') + } catch (e) { + should.equal(e.message.indexOf('"attachmentId" must be a valid GUID') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('download attachment - invalid challenge id', async () => { + try { + await service.downloadAttachment({ isMachine: true }, 'invalid', id) + } catch (e) { + should.equal(e.message.indexOf('"challengeId" must be a valid GUID') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) +}) diff --git a/test/unit/AuditLogService.test.js b/test/unit/AuditLogService.test.js new file mode 100644 index 00000000..2098b9aa --- /dev/null +++ b/test/unit/AuditLogService.test.js @@ -0,0 +1,149 @@ +/* + * Unit tests of audit log service + */ + +// During the test the env variable is set to test +process.env.NODE_ENV = 'test' + +require('../../app-bootstrap') +const _ = require('lodash') +const uuid = require('uuid/v4') +const chai = require('chai') +const service = require('../../src/services/AuditLogService') +const ChallengeService = require('../../src/services/ChallengeService') +const testHelper = require('../testHelper') + +const should = chai.should() + +describe('audit log service unit tests', () => { + // generated data + let data + const notFoundId = uuid() + + before(async () => { + await testHelper.createData() + data = testHelper.getData() + }) + + after(async () => { + await testHelper.clearData() + }) + + describe('search audit logs tests', () => { + it('search audit logs successfully 1', async () => { + // update challenge so that there are some audit logs + await ChallengeService.partiallyUpdateChallenge({ isMachine: true, sub: 'sub' }, data.challenge.id, { + track: 'track-abc', + description: 'desc-abc' + }) + + const res = await service.searchAuditLogs({ + page: 1, + perPage: 10, + challengeId: data.challenge.id, + createdDateStart: new Date(new Date().getTime() - 1000 * 60 * 60 * 30), + createdDateEnd: '2022-01-02', + createdBy: 'sub' + }) + should.equal(res.total, 2) + should.equal(res.page, 1) + should.equal(res.perPage, 10) + should.equal(res.result.length, 2) + let log = _.find(res.result, (item) => item.fieldName === 'track') + should.exist(log) + should.equal(log.oldValue, '"track"') + should.equal(log.newValue, '"track-abc"') + should.equal(log.challengeId, data.challenge.id) + should.equal(log.createdBy, 'sub') + should.exist(log.created) + should.exist(log.id) + log = _.find(res.result, (item) => item.fieldName === 'description') + should.exist(log) + should.equal(log.oldValue, '"desc"') + should.equal(log.newValue, '"desc-abc"') + should.equal(log.challengeId, data.challenge.id) + should.equal(log.createdBy, 'sub') + should.exist(log.created) + should.exist(log.id) + }) + + it('search audit logs successfully 2', async () => { + const result = await service.searchAuditLogs({ challengeId: notFoundId }) + should.equal(result.total, 0) + should.equal(result.page, 1) + should.equal(result.perPage, 20) + should.equal(result.result.length, 0) + }) + + it('search audit logs - invalid fieldName', async () => { + try { + await service.searchAuditLogs({ fieldName: ['invalid'] }) + } catch (e) { + should.equal(e.message.indexOf('"fieldName" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search audit logs - invalid challengeId', async () => { + try { + await service.searchAuditLogs({ challengeId: ['invalid'] }) + } catch (e) { + should.equal(e.message.indexOf('"challengeId" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search audit logs - invalid createdBy', async () => { + try { + await service.searchAuditLogs({ createdBy: ['invalid'] }) + } catch (e) { + should.equal(e.message.indexOf('"createdBy" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search audit logs - invalid end date', async () => { + try { + await service.searchAuditLogs({ createdDateEnd: 'abc' }) + } catch (e) { + should.equal(e.message.indexOf( + '"createdDateEnd" must be a number of milliseconds or valid date string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search audit logs - invalid page', async () => { + try { + await service.searchAuditLogs({ page: -1 }) + } catch (e) { + should.equal(e.message.indexOf('"page" must be larger than or equal to 1') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search audit logs - invalid perPage', async () => { + try { + await service.searchAuditLogs({ perPage: -1 }) + } catch (e) { + should.equal(e.message.indexOf('"perPage" must be larger than or equal to 1') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search audit logs - unexpected field', async () => { + try { + await service.searchAuditLogs({ other: 123 }) + } catch (e) { + should.equal(e.message.indexOf('"other" is not allowed') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) +}) diff --git a/test/unit/ChallengeService.test.js b/test/unit/ChallengeService.test.js new file mode 100644 index 00000000..6a9b2138 --- /dev/null +++ b/test/unit/ChallengeService.test.js @@ -0,0 +1,692 @@ +/* + * Unit tests of challenge service + */ + +// During the test the env variable is set to test +process.env.NODE_ENV = 'test' + +require('../../app-bootstrap') +const _ = require('lodash') +const uuid = require('uuid/v4') +const chai = require('chai') +const fs = require('fs') +const path = require('path') +const service = require('../../src/services/ChallengeService') +const AttachmentService = require('../../src/services/AttachmentService') +const testHelper = require('../testHelper') + +const should = chai.should() + +const attachmentContent = fs.readFileSync(path.join(__dirname, '../attachment.txt')) + +describe('challenge service unit tests', () => { + // created entity id + let id + let attachment + // generated data + let data + const notFoundId = uuid() + + before(async () => { + await testHelper.createData() + data = testHelper.getData() + // create an attachment for test + attachment = await AttachmentService.uploadAttachment({ + isMachine: true + }, data.challenge.id, { + attachment: { + data: attachmentContent, + mimetype: 'plain/text', + name: 'attachment.txt', + size: attachmentContent.length + } + }) + }) + + after(async () => { + await testHelper.clearData() + }) + + describe('create challenge tests', () => { + it('create challenge successfully', async () => { + const result = await service.createChallenge({ isMachine: true, sub: 'sub' }, + _.omit(data.challenge, ['id', 'created', 'createdBy'])) + should.exist(result.id) + id = result.id + should.equal(result.typeId, data.challenge.typeId) + should.equal(result.track, data.challenge.track) + should.equal(result.name, data.challenge.name) + should.equal(result.description, data.challenge.description) + should.equal(result.challengeSettings.length, 1) + should.equal(result.challengeSettings[0].type, data.challenge.challengeSettings[0].type) + should.equal(result.challengeSettings[0].value, data.challenge.challengeSettings[0].value) + should.equal(result.timelineTemplateId, data.challenge.timelineTemplateId) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, data.challenge.phases[0].id) + should.equal(result.phases[0].name, data.challenge.phases[0].name) + should.equal(result.phases[0].description, data.challenge.phases[0].description) + should.equal(result.phases[0].isActive, data.challenge.phases[0].isActive) + should.equal(result.phases[0].duration, data.challenge.phases[0].duration) + should.equal(result.prizeSets.length, 1) + should.equal(result.prizeSets[0].type, data.challenge.prizeSets[0].type) + should.equal(result.prizeSets[0].description, data.challenge.prizeSets[0].description) + should.equal(result.prizeSets[0].prizes.length, 1) + should.equal(result.prizeSets[0].prizes[0].description, data.challenge.prizeSets[0].prizes[0].description) + should.equal(result.prizeSets[0].prizes[0].type, data.challenge.prizeSets[0].prizes[0].type) + should.equal(result.prizeSets[0].prizes[0].value, data.challenge.prizeSets[0].prizes[0].value) + should.equal(result.reviewType, data.challenge.reviewType) + should.equal(result.tags.length, 1) + should.equal(result.tags[0], data.challenge.tags[0]) + should.equal(result.projectId, data.challenge.projectId) + should.equal(result.legacyId, data.challenge.legacyId) + should.equal(result.forumId, data.challenge.forumId) + should.equal(result.status, data.challenge.status) + should.equal(result.groups.length, 1) + should.equal(result.groups[0], data.challenge.groups[0]) + should.equal(result.createdBy, 'sub') + should.exist(result.startDate) + should.exist(result.created) + }) + + it('create challenge - type not found', async () => { + try { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.typeId = notFoundId + await service.createChallenge({ isMachine: true, sub: 'sub' }, challengeData) + } catch (e) { + should.equal(e.message, `No challenge type found with id: ${notFoundId}.`) + return + } + throw new Error('should not reach here') + }) + + it('create challenge - invalid projectId', async () => { + try { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.projectId = -1 + await service.createChallenge({ isMachine: true, sub: 'sub' }, challengeData) + } catch (e) { + should.equal(e.message.indexOf('"projectId" must be a positive number') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('create challenge - missing name', async () => { + try { + const challengeData = _.omit(data.challenge, ['id', 'name', 'created', 'createdBy']) + await service.createChallenge({ isMachine: true, sub: 'sub' }, challengeData) + } catch (e) { + should.equal(e.message.indexOf('"name" is required') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('create challenge - invalid date', async () => { + try { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.startDate = 'abc' + await service.createChallenge({ isMachine: true, sub: 'sub' }, challengeData) + } catch (e) { + should.equal(e.message.indexOf('"startDate" must be a number of milliseconds or valid date string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('create challenge - invalid status', async () => { + try { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.status = ['Active'] + await service.createChallenge({ isMachine: true, sub: 'sub' }, challengeData) + } catch (e) { + should.equal(e.message.indexOf('"status" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('create challenge - unexpected field', async () => { + try { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.other = 123 + await service.createChallenge({ isMachine: true, sub: 'sub' }, challengeData) + } catch (e) { + should.equal(e.message.indexOf('"other" is not allowed') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('get challenge tests', () => { + it('get challenge successfully', async () => { + const result = await service.getChallenge({ isMachine: true }, data.challenge.id) + should.equal(result.id, data.challenge.id) + should.equal(result.typeId, data.challenge.typeId) + should.equal(result.track, data.challenge.track) + should.equal(result.name, data.challenge.name) + should.equal(result.description, data.challenge.description) + should.equal(result.challengeSettings.length, 1) + should.equal(result.challengeSettings[0].type, data.challengeSetting.name) + should.equal(result.challengeSettings[0].value, data.challenge.challengeSettings[0].value) + should.equal(result.timelineTemplateId, data.challenge.timelineTemplateId) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, data.challenge.phases[0].id) + should.equal(result.phases[0].name, data.challenge.phases[0].name) + should.equal(result.phases[0].description, data.challenge.phases[0].description) + should.equal(result.phases[0].isActive, data.challenge.phases[0].isActive) + should.equal(result.phases[0].duration, data.challenge.phases[0].duration) + should.equal(result.prizeSets.length, 1) + should.equal(result.prizeSets[0].type, data.challenge.prizeSets[0].type) + should.equal(result.prizeSets[0].description, data.challenge.prizeSets[0].description) + should.equal(result.prizeSets[0].prizes.length, 1) + should.equal(result.prizeSets[0].prizes[0].description, data.challenge.prizeSets[0].prizes[0].description) + should.equal(result.prizeSets[0].prizes[0].type, data.challenge.prizeSets[0].prizes[0].type) + should.equal(result.prizeSets[0].prizes[0].value, data.challenge.prizeSets[0].prizes[0].value) + should.equal(result.reviewType, data.challenge.reviewType) + should.equal(result.tags.length, 1) + should.equal(result.tags[0], data.challenge.tags[0]) + should.equal(result.projectId, data.challenge.projectId) + should.equal(result.legacyId, data.challenge.legacyId) + should.equal(result.forumId, data.challenge.forumId) + should.equal(result.status, data.challenge.status) + should.equal(result.groups.length, 1) + should.equal(result.groups[0], data.challenge.groups[0]) + should.equal(result.createdBy, 'admin') + should.exist(result.startDate) + should.exist(result.created) + }) + + it('get challenge - not found', async () => { + try { + await service.getChallenge({ isMachine: true }, notFoundId) + } catch (e) { + should.equal(e.message, `Challenge of id ${notFoundId} is not found.`) + return + } + throw new Error('should not reach here') + }) + + it('get challenge - invalid id', async () => { + try { + await service.getChallenge({ isMachine: true }, 'invalid') + } catch (e) { + should.equal(e.message.indexOf('"id" must be a valid GUID') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('search challenges tests', () => { + it('search challenges successfully 1', async () => { + const res = await service.searchChallenges({ isMachine: true }, { + page: 1, + perPage: 10, + id: data.challenge.id, + typeId: data.challenge.typeId, + track: data.challenge.track, + name: data.challenge.name.substring(2).trim().toUpperCase(), + description: data.challenge.description, + timelineTemplateId: data.challenge.timelineTemplateId, + reviewType: data.challenge.reviewType, + tag: data.challenge.tags[0], + projectId: data.challenge.projectId, + forumId: data.challenge.forumId, + legacyId: data.challenge.legacyId, + status: data.challenge.status, + group: data.challenge.groups[0], + createdDateStart: '1992-01-02', + createdDateEnd: '2022-01-02', + createdBy: data.challenge.createdBy + }) + should.equal(res.total, 1) + should.equal(res.page, 1) + should.equal(res.perPage, 10) + should.equal(res.result.length, 1) + const result = res.result[0] + should.equal(result.id, data.challenge.id) + should.equal(result.type, data.challengeType.name) + should.equal(result.track, data.challenge.track) + should.equal(result.name, data.challenge.name) + should.equal(result.description, data.challenge.description) + should.equal(result.challengeSettings.length, 1) + should.equal(result.challengeSettings[0].type, data.challengeSetting.name) + should.equal(result.challengeSettings[0].value, data.challenge.challengeSettings[0].value) + should.equal(result.timelineTemplateId, data.challenge.timelineTemplateId) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, data.challenge.phases[0].id) + should.equal(result.phases[0].name, data.challenge.phases[0].name) + should.equal(result.phases[0].description, data.challenge.phases[0].description) + should.equal(result.phases[0].isActive, data.challenge.phases[0].isActive) + should.equal(result.phases[0].duration, data.challenge.phases[0].duration) + should.equal(result.prizeSets.length, 1) + should.equal(result.prizeSets[0].type, data.challenge.prizeSets[0].type) + should.equal(result.prizeSets[0].description, data.challenge.prizeSets[0].description) + should.equal(result.prizeSets[0].prizes.length, 1) + should.equal(result.prizeSets[0].prizes[0].description, data.challenge.prizeSets[0].prizes[0].description) + should.equal(result.prizeSets[0].prizes[0].type, data.challenge.prizeSets[0].prizes[0].type) + should.equal(result.prizeSets[0].prizes[0].value, data.challenge.prizeSets[0].prizes[0].value) + should.equal(result.reviewType, data.challenge.reviewType) + should.equal(result.tags.length, 1) + should.equal(result.tags[0], data.challenge.tags[0]) + should.equal(result.projectId, data.challenge.projectId) + should.equal(result.legacyId, data.challenge.legacyId) + should.equal(result.forumId, data.challenge.forumId) + should.equal(result.status, data.challenge.status) + should.equal(result.groups.length, 1) + should.equal(result.groups[0], data.challenge.groups[0]) + should.equal(result.createdBy, 'admin') + should.exist(result.startDate) + should.exist(result.created) + }) + + it('search challenges successfully 2', async () => { + const result = await service.searchChallenges({ isMachine: true }, { name: 'aaa bbb ccc' }) + should.equal(result.total, 0) + should.equal(result.page, 1) + should.equal(result.perPage, 20) + should.equal(result.result.length, 0) + }) + + it('search challenges - invalid name', async () => { + try { + await service.searchChallenges({ isMachine: true }, { name: ['invalid'] }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenges - invalid forumId', async () => { + try { + await service.searchChallenges({ isMachine: true }, { forumId: -1 }) + } catch (e) { + should.equal(e.message.indexOf('"forumId" must be a positive number') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenges - invalid page', async () => { + try { + await service.searchChallenges({ isMachine: true }, { page: -1 }) + } catch (e) { + should.equal(e.message.indexOf('"page" must be larger than or equal to 1') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenges - invalid perPage', async () => { + try { + await service.searchChallenges({ isMachine: true }, { perPage: -1 }) + } catch (e) { + should.equal(e.message.indexOf('"perPage" must be larger than or equal to 1') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenges - invalid name', async () => { + try { + await service.searchChallenges({ isMachine: true }, { name: ['abc'] }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenges - invalid track', async () => { + try { + await service.searchChallenges({ isMachine: true }, { track: ['abc'] }) + } catch (e) { + should.equal(e.message.indexOf('"track" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenges - invalid description', async () => { + try { + await service.searchChallenges({ isMachine: true }, { description: ['abc'] }) + } catch (e) { + should.equal(e.message.indexOf('"description" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenges - invalid reviewType', async () => { + try { + await service.searchChallenges({ isMachine: true }, { reviewType: ['abc'] }) + } catch (e) { + should.equal(e.message.indexOf('"reviewType" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenges - invalid tag', async () => { + try { + await service.searchChallenges({ isMachine: true }, { tag: ['abc'] }) + } catch (e) { + should.equal(e.message.indexOf('"tag" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenges - invalid group', async () => { + try { + await service.searchChallenges({ isMachine: true }, { group: ['abc'] }) + } catch (e) { + should.equal(e.message.indexOf('"group" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenges - invalid createdBy', async () => { + try { + await service.searchChallenges({ isMachine: true }, { createdBy: ['abc'] }) + } catch (e) { + should.equal(e.message.indexOf('"createdBy" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenges - invalid updatedBy', async () => { + try { + await service.searchChallenges({ isMachine: true }, { updatedBy: ['abc'] }) + } catch (e) { + should.equal(e.message.indexOf('"updatedBy" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenges - unexpected field', async () => { + try { + await service.searchChallenges({ isMachine: true }, { other: 123 }) + } catch (e) { + should.equal(e.message.indexOf('"other" is not allowed') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('fully update challenge tests', () => { + it('fully update challenge successfully', async () => { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.track = 'updated-track' + challengeData.projectId = 112233 + challengeData.legacyId = 445566 + challengeData.attachmentIds = [attachment.id] + const result = await service.fullyUpdateChallenge({ isMachine: true, sub: 'sub2' }, id, challengeData) + should.equal(result.id, id) + should.equal(result.typeId, data.challenge.typeId) + should.equal(result.track, 'updated-track') + should.equal(result.name, data.challenge.name) + should.equal(result.description, data.challenge.description) + should.equal(result.challengeSettings.length, 1) + should.equal(result.challengeSettings[0].type, data.challenge.challengeSettings[0].type) + should.equal(result.challengeSettings[0].value, data.challenge.challengeSettings[0].value) + should.equal(result.timelineTemplateId, data.challenge.timelineTemplateId) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, data.challenge.phases[0].id) + should.equal(result.phases[0].name, data.challenge.phases[0].name) + should.equal(result.phases[0].description, data.challenge.phases[0].description) + should.equal(result.phases[0].isActive, data.challenge.phases[0].isActive) + should.equal(result.phases[0].duration, data.challenge.phases[0].duration) + should.equal(result.prizeSets.length, 1) + should.equal(result.prizeSets[0].type, data.challenge.prizeSets[0].type) + should.equal(result.prizeSets[0].description, data.challenge.prizeSets[0].description) + should.equal(result.prizeSets[0].prizes.length, 1) + should.equal(result.prizeSets[0].prizes[0].description, data.challenge.prizeSets[0].prizes[0].description) + should.equal(result.prizeSets[0].prizes[0].type, data.challenge.prizeSets[0].prizes[0].type) + should.equal(result.prizeSets[0].prizes[0].value, data.challenge.prizeSets[0].prizes[0].value) + should.equal(result.reviewType, data.challenge.reviewType) + should.equal(result.tags.length, 1) + should.equal(result.tags[0], data.challenge.tags[0]) + should.equal(result.projectId, 112233) + should.equal(result.legacyId, 445566) + should.equal(result.forumId, data.challenge.forumId) + should.equal(result.status, data.challenge.status) + should.equal(result.groups.length, 1) + should.equal(result.groups[0], data.challenge.groups[0]) + should.equal(result.attachments.length, 1) + should.equal(result.attachments[0].id, attachment.id) + should.equal(result.attachments[0].fileSize, attachment.fileSize) + should.equal(result.attachments[0].fileName, attachment.fileName) + should.equal(result.attachments[0].challengeId, attachment.challengeId) + should.equal(result.createdBy, 'sub') + should.equal(result.updatedBy, 'sub2') + should.exist(result.startDate) + should.exist(result.created) + should.exist(result.updated) + }) + + it('fully update challenge - not found', async () => { + try { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.track = 'updated-track' + await service.fullyUpdateChallenge({ isMachine: true, sub: 'sub2' }, notFoundId, challengeData) + } catch (e) { + should.equal(e.message, `Challenge with id: ${notFoundId} doesn't exist`) + return + } + throw new Error('should not reach here') + }) + + it('fully update challenge - invalid id', async () => { + try { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.track = 'updated-track' + challengeData.attachmentIds = [attachment.id] + await service.fullyUpdateChallenge({ isMachine: true, sub: 'sub2' }, 'invalid', challengeData) + } catch (e) { + should.equal(e.message.indexOf('"challengeId" must be a valid GUID') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update challenge - null name', async () => { + try { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.name = null + await service.fullyUpdateChallenge({ isMachine: true, sub: 'sub2' }, id, challengeData) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update challenge - invalid name', async () => { + try { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.name = ['abc'] + await service.fullyUpdateChallenge({ isMachine: true, sub: 'sub2' }, id, challengeData) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update challenge - empty track', async () => { + try { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.track = '' + await service.fullyUpdateChallenge({ isMachine: true, sub: 'sub2' }, id, challengeData) + } catch (e) { + should.equal(e.message.indexOf('"track" is not allowed to be empty') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update challenge - invalid status', async () => { + try { + const challengeData = _.omit(data.challenge, ['id', 'created', 'createdBy']) + challengeData.status = 'invalid' + await service.fullyUpdateChallenge({ isMachine: true, sub: 'sub2' }, id, challengeData) + } catch (e) { + should.equal(e.message.indexOf('"status" must be one of [Draft, Canceled, Active, Completed]') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('partially update challenge tests', () => { + it('partially update challenge successfully 1', async () => { + const result = await service.partiallyUpdateChallenge({ isMachine: true, sub: 'sub3' }, id, { + track: 'track 333', + description: 'updated desc', + attachmentIds: [] // this will delete attachments + }) + should.equal(result.id, id) + should.equal(result.typeId, data.challenge.typeId) + should.equal(result.track, 'track 333') + should.equal(result.name, data.challenge.name) + should.equal(result.description, 'updated desc') + should.equal(result.challengeSettings.length, 1) + should.equal(result.challengeSettings[0].type, data.challenge.challengeSettings[0].type) + should.equal(result.challengeSettings[0].value, data.challenge.challengeSettings[0].value) + should.equal(result.timelineTemplateId, data.challenge.timelineTemplateId) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, data.challenge.phases[0].id) + should.equal(result.phases[0].name, data.challenge.phases[0].name) + should.equal(result.phases[0].description, data.challenge.phases[0].description) + should.equal(result.phases[0].isActive, data.challenge.phases[0].isActive) + should.equal(result.phases[0].duration, data.challenge.phases[0].duration) + should.equal(result.prizeSets.length, 1) + should.equal(result.prizeSets[0].type, data.challenge.prizeSets[0].type) + should.equal(result.prizeSets[0].description, data.challenge.prizeSets[0].description) + should.equal(result.prizeSets[0].prizes.length, 1) + should.equal(result.prizeSets[0].prizes[0].description, data.challenge.prizeSets[0].prizes[0].description) + should.equal(result.prizeSets[0].prizes[0].type, data.challenge.prizeSets[0].prizes[0].type) + should.equal(result.prizeSets[0].prizes[0].value, data.challenge.prizeSets[0].prizes[0].value) + should.equal(result.reviewType, data.challenge.reviewType) + should.equal(result.tags.length, 1) + should.equal(result.tags[0], data.challenge.tags[0]) + should.equal(result.projectId, 112233) + should.equal(result.legacyId, 445566) + should.equal(result.forumId, data.challenge.forumId) + should.equal(result.status, data.challenge.status) + should.equal(result.groups.length, 1) + should.equal(result.groups[0], data.challenge.groups[0]) + should.equal(!result.attachments || result.attachments.length === 0, true) + should.equal(result.createdBy, 'sub') + should.equal(result.updatedBy, 'sub3') + should.exist(result.startDate) + should.exist(result.created) + should.exist(result.updated) + }) + + it('partially update challenge - timeline template not found', async () => { + try { + await service.partiallyUpdateChallenge({ isMachine: true, sub: 'sub3' }, id, { + timelineTemplateId: notFoundId + }) + } catch (e) { + should.equal(e.message, `TimelineTemplate with id: ${notFoundId} doesn't exist`) + return + } + throw new Error('should not reach here') + }) + + it('partially update challenge - challenge not found', async () => { + try { + await service.partiallyUpdateChallenge({ isMachine: true, sub: 'sub3' }, notFoundId, { + track: 'track 333' + }) + } catch (e) { + should.equal(e.message, `Challenge with id: ${notFoundId} doesn't exist`) + return + } + throw new Error('should not reach here') + }) + + it('partially update challenge - invalid type id', async () => { + try { + await service.partiallyUpdateChallenge({ isMachine: true, sub: 'sub3' }, id, { + typeId: 'invalid' + }) + } catch (e) { + should.equal(e.message.indexOf('"typeId" must be a valid GUID') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('partially update challenge - empty tags', async () => { + try { + await service.partiallyUpdateChallenge({ isMachine: true, sub: 'sub3' }, id, { + tags: [] + }) + } catch (e) { + should.equal(e.message.indexOf('"tags" does not contain 1 required value(s)') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('partially update challenge - invalid start date', async () => { + try { + await service.partiallyUpdateChallenge({ isMachine: true, sub: 'sub3' }, id, { + startDate: 'abc' + }) + } catch (e) { + should.equal(e.message.indexOf('"startDate" must be a number of milliseconds or valid date string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('partially update challenge - invalid forumId', async () => { + try { + await service.partiallyUpdateChallenge({ isMachine: true, sub: 'sub3' }, id, { + forumId: 'abc' + }) + } catch (e) { + should.equal(e.message.indexOf('"forumId" must be a number') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('partially update challenge - empty name', async () => { + try { + await service.partiallyUpdateChallenge({ isMachine: true, sub: 'sub3' }, id, { + name: '' + }) + } catch (e) { + should.equal(e.message.indexOf('"name" is not allowed to be empty') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('partially update challenge - unexpected field', async () => { + try { + await service.partiallyUpdateChallenge({ isMachine: true, sub: 'sub3' }, id, { + other: '123' + }) + } catch (e) { + should.equal(e.message.indexOf('"other" is not allowed') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) +}) diff --git a/test/unit/ChallengeSettingService.test.js b/test/unit/ChallengeSettingService.test.js new file mode 100644 index 00000000..3cf52a31 --- /dev/null +++ b/test/unit/ChallengeSettingService.test.js @@ -0,0 +1,261 @@ +/* + * Unit tests of challenge setting service + */ + +// During the test the env variable is set to test +process.env.NODE_ENV = 'test' + +require('../../app-bootstrap') +const uuid = require('uuid/v4') +const chai = require('chai') +const service = require('../../src/services/ChallengeSettingService') + +const should = chai.should() + +describe('challenge setting service unit tests', () => { + // created entity ids + let id + let id2 + // random names + const name = `test1${new Date().getTime()}` + const name2 = `test2${new Date().getTime()}` + const notFoundId = uuid() + + describe('create challenge setting tests', () => { + it('create challenge setting successfully 1', async () => { + const result = await service.createChallengeSetting({ + name + }) + should.equal(result.name, name) + should.exist(result.id) + id = result.id + }) + + it('create challenge setting successfully 2', async () => { + const result = await service.createChallengeSetting({ + name: name2 + }) + should.equal(result.name, name2) + should.exist(result.id) + id2 = result.id + }) + + it('create challenge setting - name already used', async () => { + try { + await service.createChallengeSetting({ + name + }) + } catch (e) { + should.equal(e.message, `ChallengeSetting with name: ${name} already exist`) + return + } + throw new Error('should not reach here') + }) + + it('create challenge setting - missing name', async () => { + try { + await service.createChallengeSetting({ + }) + } catch (e) { + should.equal(e.message.indexOf('"name" is required') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('create challenge setting - invalid name', async () => { + try { + await service.createChallengeSetting({ + name: ['xx'] + }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('create challenge setting - unexpected field', async () => { + try { + await service.createChallengeSetting({ + name: 'some name', + other: 123 + }) + } catch (e) { + should.equal(e.message.indexOf('"other" is not allowed') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('get challenge setting tests', () => { + it('get challenge setting successfully', async () => { + const result = await service.getChallengeSetting(id2) + should.equal(result.id, id2) + should.equal(result.name, name2) + }) + + it('get challenge setting - not found', async () => { + try { + await service.getChallengeSetting(notFoundId) + } catch (e) { + should.equal(e.message, `ChallengeSetting with id: ${notFoundId} doesn't exist`) + return + } + throw new Error('should not reach here') + }) + + it('get challenge setting - invalid id', async () => { + try { + await service.getChallengeSetting('invalid') + } catch (e) { + should.equal(e.message.indexOf('"id" must be a valid GUID') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('search challenge settings tests', () => { + it('search challenge settings successfully 1', async () => { + const result = await service.searchChallengeSettings({ page: 1, perPage: 10, name: name2.substring(1).toUpperCase() }) + should.equal(result.total, 1) + should.equal(result.page, 1) + should.equal(result.perPage, 10) + should.equal(result.result.length, 1) + should.equal(result.result[0].id, id2) + should.equal(result.result[0].name, name2) + }) + + it('search challenge settings successfully 2', async () => { + const result = await service.searchChallengeSettings({ name: 'xyzxyz123' }) + should.equal(result.total, 0) + should.equal(result.page, 1) + should.equal(result.perPage, 20) + should.equal(result.result.length, 0) + }) + + it('search challenge settings - invalid name', async () => { + try { + await service.searchChallengeSettings({ name: ['invalid'] }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenge settings - invalid page', async () => { + try { + await service.searchChallengeSettings({ page: -1 }) + } catch (e) { + should.equal(e.message.indexOf('"page" must be larger than or equal to 1') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenge settings - invalid perPage', async () => { + try { + await service.searchChallengeSettings({ perPage: -1 }) + } catch (e) { + should.equal(e.message.indexOf('"perPage" must be larger than or equal to 1') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenge settings - unexpected field', async () => { + try { + await service.searchChallengeSettings({ other: 123 }) + } catch (e) { + should.equal(e.message.indexOf('"other" is not allowed') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('fully update challenge setting tests', () => { + it('fully update challenge setting successfully', async () => { + const result = await service.updateChallengeSetting(id2, { + name: `${name2}-updated` + }) + should.equal(result.id, id2) + should.equal(result.name, `${name2}-updated`) + }) + + it('fully update challenge setting - name already used', async () => { + try { + await service.updateChallengeSetting(id2, { + name + }) + } catch (e) { + should.equal(e.message, `ChallengeSetting with name: ${name} already exist`) + return + } + throw new Error('should not reach here') + }) + + it('fully update challenge setting - not found', async () => { + try { + await service.updateChallengeSetting(notFoundId, { + name: 'slkdjflskjdf' + }) + } catch (e) { + should.equal(e.message, `ChallengeSetting with id: ${notFoundId} doesn't exist`) + return + } + throw new Error('should not reach here') + }) + + it('fully update challenge setting - invalid id', async () => { + try { + await service.updateChallengeSetting('invalid', { + name: 'slkdjflskjdf' + }) + } catch (e) { + should.equal(e.message.indexOf('"id" must be a valid GUID') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update challenge setting - null name', async () => { + try { + await service.updateChallengeSetting(id, { + name: null + }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update challenge setting - invalid name', async () => { + try { + await service.updateChallengeSetting(id, { + name: { invalid: 'x' } + }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update challenge setting - empty name', async () => { + try { + await service.updateChallengeSetting(id, { + name: '' + }) + } catch (e) { + should.equal(e.message.indexOf('"name" is not allowed to be empty') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) +}) diff --git a/test/unit/ChallengeTypeService.test.js b/test/unit/ChallengeTypeService.test.js new file mode 100644 index 00000000..2eff6b51 --- /dev/null +++ b/test/unit/ChallengeTypeService.test.js @@ -0,0 +1,452 @@ +/* + * Unit tests of challenge type service + */ + +// During the test the env variable is set to test +process.env.NODE_ENV = 'test' + +require('../../app-bootstrap') +const uuid = require('uuid/v4') +const chai = require('chai') +const service = require('../../src/services/ChallengeTypeService') + +const should = chai.should() + +describe('challenge type service unit tests', () => { + // created entity ids + let id + let id2 + // random names + const name = `test1${new Date().getTime()}` + const name2 = `test2${new Date().getTime()}` + const notFoundId = uuid() + + describe('create challenge type tests', () => { + it('create challenge type successfully 1', async () => { + const result = await service.createChallengeType({ + name, + description: 'desc', + isActive: true + }) + should.equal(result.name, name) + should.equal(result.description, 'desc') + should.equal(result.isActive, true) + should.exist(result.id) + id = result.id + }) + + it('create challenge type successfully 2', async () => { + const result = await service.createChallengeType({ + name: name2, + description: 'desc2', + isActive: false + }) + should.equal(result.name, name2) + should.equal(result.description, 'desc2') + should.equal(result.isActive, false) + should.exist(result.id) + id2 = result.id + }) + + it('create challenge type - name already used', async () => { + try { + await service.createChallengeType({ + name, + description: 'desc', + isActive: false + }) + } catch (e) { + should.equal(e.message, `ChallengeType with name: ${name} already exist`) + return + } + throw new Error('should not reach here') + }) + + it('create challenge type - missing name', async () => { + try { + await service.createChallengeType({ + description: 'desc', + isActive: false + }) + } catch (e) { + should.equal(e.message.indexOf('"name" is required') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('create challenge type - invalid name', async () => { + try { + await service.createChallengeType({ + name: ['xx'], + description: 'desc', + isActive: false + }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('create challenge type - invalid isActive', async () => { + try { + await service.createChallengeType({ + name: 'some name', + description: 'desc', + isActive: 'abc' + }) + } catch (e) { + should.equal(e.message.indexOf('"isActive" must be a boolean') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('create challenge type - unexpected field', async () => { + try { + await service.createChallengeType({ + name: 'some name', + description: 'desc', + isActive: false, + other: 123 + }) + } catch (e) { + should.equal(e.message.indexOf('"other" is not allowed') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('get challenge type tests', () => { + it('get challenge type successfully', async () => { + const result = await service.getChallengeType(id2) + should.equal(result.id, id2) + should.equal(result.name, name2) + should.equal(result.description, 'desc2') + should.equal(result.isActive, false) + }) + + it('get challenge type - not found', async () => { + try { + await service.getChallengeType(notFoundId) + } catch (e) { + should.equal(e.message, `ChallengeType with id: ${notFoundId} doesn't exist`) + return + } + throw new Error('should not reach here') + }) + + it('get challenge type - invalid id', async () => { + try { + await service.getChallengeType('invalid') + } catch (e) { + should.equal(e.message.indexOf('"id" must be a valid GUID') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('search challenge types tests', () => { + it('search challenge types successfully 1', async () => { + const result = await service.searchChallengeTypes({ + page: 1, + perPage: 10, + name: name2.substring(1).toUpperCase(), + description: 'desc', + isActive: false + }) + should.equal(result.total, 1) + should.equal(result.page, 1) + should.equal(result.perPage, 10) + should.equal(result.result.length, 1) + should.equal(result.result[0].id, id2) + should.equal(result.result[0].name, name2) + should.equal(result.result[0].description, 'desc2') + should.equal(result.result[0].isActive, false) + }) + + it('search challenge types successfully 2', async () => { + const result = await service.searchChallengeTypes({ name: 'xyzxyz123' }) + should.equal(result.total, 0) + should.equal(result.page, 1) + should.equal(result.perPage, 20) + should.equal(result.result.length, 0) + }) + + it('search challenge types - invalid name', async () => { + try { + await service.searchChallengeTypes({ name: ['invalid'] }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenge types - invalid description', async () => { + try { + await service.searchChallengeTypes({ description: ['invalid'] }) + } catch (e) { + should.equal(e.message.indexOf('"description" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenge types - invalid isActive', async () => { + try { + await service.searchChallengeTypes({ isActive: 'abc' }) + } catch (e) { + should.equal(e.message.indexOf('"isActive" must be a boolean') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenge types - invalid page', async () => { + try { + await service.searchChallengeTypes({ page: -1 }) + } catch (e) { + should.equal(e.message.indexOf('"page" must be larger than or equal to 1') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenge types - invalid perPage', async () => { + try { + await service.searchChallengeTypes({ perPage: -1 }) + } catch (e) { + should.equal(e.message.indexOf('"perPage" must be larger than or equal to 1') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search challenge types - unexpected field', async () => { + try { + await service.searchChallengeTypes({ other: 123 }) + } catch (e) { + should.equal(e.message.indexOf('"other" is not allowed') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('fully update challenge type tests', () => { + it('fully update challenge type successfully', async () => { + const result = await service.fullyUpdateChallengeType(id2, { + name: `${name2}-updated`, + description: 'desc222', + isActive: true + }) + should.equal(result.id, id2) + should.equal(result.name, `${name2}-updated`) + should.equal(result.description, 'desc222') + should.equal(result.isActive, true) + }) + + it('fully update challenge type - name already used', async () => { + try { + await service.fullyUpdateChallengeType(id2, { + name, + description: 'desc', + isActive: false + }) + } catch (e) { + should.equal(e.message, `ChallengeType with name: ${name} already exist`) + return + } + throw new Error('should not reach here') + }) + + it('fully update challenge type - not found', async () => { + try { + await service.fullyUpdateChallengeType(notFoundId, { + name: 'slkdjflskjdf', + description: 'desc', + isActive: false + }) + } catch (e) { + should.equal(e.message, `ChallengeType with id: ${notFoundId} doesn't exist`) + return + } + throw new Error('should not reach here') + }) + + it('fully update challenge type - invalid id', async () => { + try { + await service.fullyUpdateChallengeType('invalid', { + name: 'slkdjflskjdf', + description: 'desc', + isActive: false + }) + } catch (e) { + should.equal(e.message.indexOf('"id" must be a valid GUID') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update challenge type - null name', async () => { + try { + await service.fullyUpdateChallengeType(id, { + name: null, + description: 'desc', + isActive: false + }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update challenge type - invalid name', async () => { + try { + await service.fullyUpdateChallengeType(id, { + name: { invalid: 'x' }, + description: 'desc', + isActive: false + }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update challenge type - invalid description', async () => { + try { + await service.fullyUpdateChallengeType(id, { + name: 'some name', + description: ['desc'], + isActive: false + }) + } catch (e) { + should.equal(e.message.indexOf('"description" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update challenge type - empty name', async () => { + try { + await service.fullyUpdateChallengeType(id, { + name: '', + description: 'desc', + isActive: false + }) + } catch (e) { + should.equal(e.message.indexOf('"name" is not allowed to be empty') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update challenge type - invalid isActive', async () => { + try { + await service.fullyUpdateChallengeType(id, { + name: 'asdfsadfsdf', + description: 'desc', + isActive: 'invalid' + }) + } catch (e) { + should.equal(e.message.indexOf('"isActive" must be a boolean') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('partially update challenge type tests', () => { + it('partially update challenge type successfully 1', async () => { + const result = await service.partiallyUpdateChallengeType(id2, { + name: `${name2}-33`, + description: 'desc33' + }) + should.equal(result.id, id2) + should.equal(result.name, `${name2}-33`) + should.equal(result.description, 'desc33') + should.equal(result.isActive, true) + }) + + it('partially update challenge type - name already used', async () => { + try { + await service.partiallyUpdateChallengeType(id2, { + name + }) + } catch (e) { + should.equal(e.message, `ChallengeType with name: ${name} already exist`) + return + } + throw new Error('should not reach here') + }) + + it('partially update challenge type - not found', async () => { + try { + await service.partiallyUpdateChallengeType(notFoundId, { + name: 'slkdjflskjdf' + }) + } catch (e) { + should.equal(e.message, `ChallengeType with id: ${notFoundId} doesn't exist`) + return + } + throw new Error('should not reach here') + }) + + it('partially update challenge type - invalid id', async () => { + try { + await service.partiallyUpdateChallengeType('invalid', { name: 'hufdufhdfx' }) + } catch (e) { + should.equal(e.message.indexOf('"id" must be a valid GUID') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('partially update challenge type - null name', async () => { + try { + await service.partiallyUpdateChallengeType(id, { name: null }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('partially update challenge type - invalid description', async () => { + try { + await service.partiallyUpdateChallengeType(id, { description: { invalid: 'x' } }) + } catch (e) { + should.equal(e.message.indexOf('"description" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('partially update challenge type - empty name', async () => { + try { + await service.partiallyUpdateChallengeType(id, { name: '' }) + } catch (e) { + should.equal(e.message.indexOf('"name" is not allowed to be empty') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('partially update challenge type - unexpected field', async () => { + try { + await service.partiallyUpdateChallengeType(id, { name: 'xx', other: 'xx' }) + } catch (e) { + should.equal(e.message.indexOf('"other" is not allowed') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) +}) diff --git a/test/unit/PhaseService.test.js b/test/unit/PhaseService.test.js new file mode 100644 index 00000000..ca447e4a --- /dev/null +++ b/test/unit/PhaseService.test.js @@ -0,0 +1,507 @@ +/* + * Unit tests of phase service + */ + +// During the test the env variable is set to test +process.env.NODE_ENV = 'test' + +require('../../app-bootstrap') +const uuid = require('uuid/v4') +const chai = require('chai') +const service = require('../../src/services/PhaseService') + +const should = chai.should() + +describe('phase service unit tests', () => { + // created entity ids + let id + let id2 + // random names + const name = `test1${new Date().getTime()}` + const name2 = `test2${new Date().getTime()}` + const notFoundId = uuid() + + describe('create phase tests', () => { + it('create phase successfully 1', async () => { + const result = await service.createPhase({ + name, + description: 'desc', + isActive: true, + duration: 123 + }) + should.equal(result.name, name) + should.equal(result.description, 'desc') + should.equal(result.isActive, true) + should.equal(result.duration, 123) + should.exist(result.id) + id = result.id + }) + + it('create phase successfully 2', async () => { + const result = await service.createPhase({ + name: name2, + description: 'desc2', + predecessor: id, + isActive: false, + duration: 456 + }) + should.equal(result.name, name2) + should.equal(result.description, 'desc2') + should.equal(result.predecessor, id) + should.equal(result.isActive, false) + should.equal(result.duration, 456) + should.exist(result.id) + id2 = result.id + }) + + it('create phase - predecessor not found', async () => { + try { + await service.createPhase({ + name: 'fjifgutuyob234', + description: 'desc2', + predecessor: notFoundId, + isActive: false, + duration: 456 + }) + } catch (e) { + should.equal(e.message, `Phase with id: ${notFoundId} doesn't exist`) + return + } + throw new Error('should not reach here') + }) + + it('create phase - name already used', async () => { + try { + await service.createPhase({ + name, + description: 'desc', + isActive: false, + duration: 456 + }) + } catch (e) { + should.equal(e.message, `Phase with name: ${name} already exist`) + return + } + throw new Error('should not reach here') + }) + + it('create phase - missing name', async () => { + try { + await service.createPhase({ + description: 'desc', + isActive: false, + duration: 456 + }) + } catch (e) { + should.equal(e.message.indexOf('"name" is required') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('create phase - invalid name', async () => { + try { + await service.createPhase({ + name: ['xx'], + description: 'desc', + isActive: false, + duration: 456 + }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('create phase - invalid duration', async () => { + try { + await service.createPhase({ + name: 'some name', + description: 'desc', + isActive: false, + duration: -456 + }) + } catch (e) { + should.equal(e.message.indexOf('"duration" must be a positive number') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('create phase - unexpected field', async () => { + try { + await service.createPhase({ + name: 'some name', + description: 'desc', + isActive: false, + duration: 456, + other: 123 + }) + } catch (e) { + should.equal(e.message.indexOf('"other" is not allowed') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('get phase tests', () => { + it('get phase successfully', async () => { + const result = await service.getPhase(id2) + should.equal(result.id, id2) + should.equal(result.name, name2) + should.equal(result.description, 'desc2') + should.equal(result.predecessor, id) + should.equal(result.isActive, false) + should.equal(result.duration, 456) + }) + + it('get phase - not found', async () => { + try { + await service.getPhase(notFoundId) + } catch (e) { + should.equal(e.message, `Phase with id: ${notFoundId} doesn't exist`) + return + } + throw new Error('should not reach here') + }) + + it('get phase - invalid id', async () => { + try { + await service.getPhase('invalid') + } catch (e) { + should.equal(e.message.indexOf('"phaseId" must be a valid GUID') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('search phases tests', () => { + it('search phases successfully 1', async () => { + const result = await service.searchPhases({ page: 1, perPage: 10, name: name2.substring(1).toUpperCase() }) + should.equal(result.total, 1) + should.equal(result.page, 1) + should.equal(result.perPage, 10) + should.equal(result.result.length, 1) + should.equal(result.result[0].id, id2) + should.equal(result.result[0].name, name2) + should.equal(result.result[0].description, 'desc2') + should.equal(result.result[0].predecessor, id) + should.equal(result.result[0].isActive, false) + should.equal(result.result[0].duration, 456) + }) + + it('search phases successfully 2', async () => { + const result = await service.searchPhases({ name: 'xyzxyz123' }) + should.equal(result.total, 0) + should.equal(result.page, 1) + should.equal(result.perPage, 20) + should.equal(result.result.length, 0) + }) + + it('search phases - invalid name', async () => { + try { + await service.searchPhases({ name: ['invalid'] }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search phases - invalid page', async () => { + try { + await service.searchPhases({ page: -1 }) + } catch (e) { + should.equal(e.message.indexOf('"page" must be larger than or equal to 1') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search phases - invalid perPage', async () => { + try { + await service.searchPhases({ perPage: -1 }) + } catch (e) { + should.equal(e.message.indexOf('"perPage" must be larger than or equal to 1') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search phases - unexpected field', async () => { + try { + await service.searchPhases({ other: 123 }) + } catch (e) { + should.equal(e.message.indexOf('"other" is not allowed') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('fully update phase tests', () => { + it('fully update phase successfully', async () => { + const result = await service.fullyUpdatePhase(id2, { + name: `${name2}-updated`, + description: 'desc222', + predecessor: id, + isActive: true, + duration: 789 + }) + should.equal(result.id, id2) + should.equal(result.name, `${name2}-updated`) + should.equal(result.description, 'desc222') + should.equal(result.predecessor, id) + should.equal(result.isActive, true) + should.equal(result.duration, 789) + }) + + it('fully update phase - name already used', async () => { + try { + await service.fullyUpdatePhase(id2, { + name, + description: 'desc', + isActive: false, + duration: 456 + }) + } catch (e) { + should.equal(e.message, `Phase with name: ${name} already exist`) + return + } + throw new Error('should not reach here') + }) + + it('fully update phase - not found', async () => { + try { + await service.fullyUpdatePhase(notFoundId, { + name: 'slkdjflskjdf', + description: 'desc', + isActive: false, + duration: 456 + }) + } catch (e) { + should.equal(e.message, `Phase with id: ${notFoundId} doesn't exist`) + return + } + throw new Error('should not reach here') + }) + + it('fully update phase - invalid id', async () => { + try { + await service.fullyUpdatePhase('invalid', { + name: 'slkdjflskjdf', + description: 'desc', + isActive: false, + duration: 456 + }) + } catch (e) { + should.equal(e.message.indexOf('"phaseId" must be a valid GUID') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update phase - null name', async () => { + try { + await service.fullyUpdatePhase(id, { + name: null, + description: 'desc', + isActive: false, + duration: 456 + }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update phase - invalid name', async () => { + try { + await service.fullyUpdatePhase(id, { + name: { invalid: 'x' }, + description: 'desc', + isActive: false, + duration: 456 + }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update phase - empty name', async () => { + try { + await service.fullyUpdatePhase(id, { + name: '', + description: 'desc', + isActive: false, + duration: 456 + }) + } catch (e) { + should.equal(e.message.indexOf('"name" is not allowed to be empty') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update phase - invalid isActive', async () => { + try { + await service.fullyUpdatePhase(id, { + name: 'asdfsadfsdf', + description: 'desc', + isActive: 'invalid', + duration: 456 + }) + } catch (e) { + should.equal(e.message.indexOf('"isActive" must be a boolean') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('partially update phase tests', () => { + it('partially update phase successfully 1', async () => { + const result = await service.partiallyUpdatePhase(id2, { + name: `${name2}-33`, + description: 'desc33', + duration: 111 + }) + should.equal(result.id, id2) + should.equal(result.name, `${name2}-33`) + should.equal(result.description, 'desc33') + should.equal(result.predecessor, id) + should.equal(result.isActive, true) + should.equal(result.duration, 111) + }) + + it('partially update phase - name already used', async () => { + try { + await service.partiallyUpdatePhase(id2, { + name + }) + } catch (e) { + should.equal(e.message, `Phase with name: ${name} already exist`) + return + } + throw new Error('should not reach here') + }) + + it('partially update phase - not found', async () => { + try { + await service.partiallyUpdatePhase(notFoundId, { + name: 'slkdjflskjdf' + }) + } catch (e) { + should.equal(e.message, `Phase with id: ${notFoundId} doesn't exist`) + return + } + throw new Error('should not reach here') + }) + + it('partially update phase - invalid id', async () => { + try { + await service.partiallyUpdatePhase('invalid', { name: 'hufdufhdfx' }) + } catch (e) { + should.equal(e.message.indexOf('"phaseId" must be a valid GUID') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('partially update phase - null name', async () => { + try { + await service.partiallyUpdatePhase(id, { name: null }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('partially update phase - invalid description', async () => { + try { + await service.partiallyUpdatePhase(id, { description: { invalid: 'x' } }) + } catch (e) { + should.equal(e.message.indexOf('"description" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('partially update phase - invalid predecessor', async () => { + try { + await service.partiallyUpdatePhase(id, { predecessor: 'abc' }) + } catch (e) { + should.equal(e.message.indexOf('"predecessor" must be a valid GUID') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('partially update phase - empty name', async () => { + try { + await service.partiallyUpdatePhase(id, { name: '' }) + } catch (e) { + should.equal(e.message.indexOf('"name" is not allowed to be empty') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('partially update phase - unexpected field', async () => { + try { + await service.partiallyUpdatePhase(id, { name: 'xx', other: 'xx' }) + } catch (e) { + should.equal(e.message.indexOf('"other" is not allowed') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('remove phase tests', () => { + it('remove phase - there is dependent phase', async () => { + try { + await service.deletePhase(id) + } catch (e) { + should.equal(e.message, `Can't delete phase ${id} because it is preceding phase of other phases.`) + return + } + throw new Error('should not reach here') + }) + + it('remove phase successfully 1', async () => { + await service.deletePhase(id2) + }) + + it('remove phase successfully 2', async () => { + await service.deletePhase(id) + }) + + it('remove phase - not found', async () => { + try { + await service.deletePhase(notFoundId) + } catch (e) { + should.equal(e.message, `Phase with id: ${notFoundId} doesn't exist`) + return + } + throw new Error('should not reach here') + }) + + it('remove phase - invalid id', async () => { + try { + await service.deletePhase('invalid') + } catch (e) { + should.equal(e.message.indexOf('"phaseId" must be a valid GUID') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) +}) diff --git a/test/unit/TimelineTemplateService.test.js b/test/unit/TimelineTemplateService.test.js new file mode 100644 index 00000000..663be03e --- /dev/null +++ b/test/unit/TimelineTemplateService.test.js @@ -0,0 +1,549 @@ +/* + * Unit tests of timeline template service + */ + +// During the test the env variable is set to test +process.env.NODE_ENV = 'test' + +require('../../app-bootstrap') +const uuid = require('uuid/v4') +const chai = require('chai') +const service = require('../../src/services/TimelineTemplateService') +const helper = require('../../src/common/helper') + +const should = chai.should() + +describe('timeline template service unit tests', () => { + // created entity ids + let id + let id2 + // random names + const name = `test1${new Date().getTime()}` + const name2 = `test2${new Date().getTime()}` + const notFoundId = uuid() + // reference data + let phase + + before(async () => { + phase = await helper.create('Phase', { + id: uuid(), + name: `phase${new Date().getTime()}`, // random name + description: 'desc', + isActive: true, + duration: 123 + }) + }) + + after(async () => { + await phase.delete() + }) + + describe('create timeline template tests', () => { + it('create timeline template successfully 1', async () => { + const result = await service.createTimelineTemplate({ + name, + description: 'desc', + isActive: true, + phases: [phase] + }) + should.exist(result.id) + id = result.id + should.equal(result.name, name) + should.equal(result.description, 'desc') + should.equal(result.isActive, true) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, phase.id) + should.equal(result.phases[0].name, phase.name) + should.equal(result.phases[0].description, phase.description) + should.equal(result.phases[0].isActive, phase.isActive) + should.equal(result.phases[0].duration, phase.duration) + }) + + it('create timeline template successfully 2', async () => { + const result = await service.createTimelineTemplate({ + name: name2, + description: 'desc', + isActive: false, + phases: [phase] + }) + should.exist(result.id) + id2 = result.id + should.equal(result.name, name2) + should.equal(result.description, 'desc') + should.equal(result.isActive, false) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, phase.id) + should.equal(result.phases[0].name, phase.name) + should.equal(result.phases[0].description, phase.description) + should.equal(result.phases[0].isActive, phase.isActive) + should.equal(result.phases[0].duration, phase.duration) + }) + + it('create timeline template - name already used', async () => { + try { + await service.createTimelineTemplate({ + name, + description: 'desc', + isActive: false, + phases: [phase] + }) + } catch (e) { + should.equal(e.message, `TimelineTemplate with name: ${name} already exist`) + return + } + throw new Error('should not reach here') + }) + + it('create timeline template - missing name', async () => { + try { + await service.createTimelineTemplate({ + description: 'desc', + isActive: false, + phases: [phase] + }) + } catch (e) { + should.equal(e.message.indexOf('"name" is required') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('create timeline template - invalid name', async () => { + try { + await service.createTimelineTemplate({ + name: ['xx'], + description: 'desc', + isActive: false, + phases: [phase] + }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('create timeline template - invalid phases', async () => { + try { + await service.createTimelineTemplate({ + name: 'jjghurturt34', + description: 'desc', + isActive: false, + phases: [{ id: phase.id, name: phase.name, isActive: false, duration: 345 }] + }) + } catch (e) { + should.equal(e.message.indexOf('phases are invalid or inactive') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('create timeline template - missing phases', async () => { + try { + await service.createTimelineTemplate({ + name: 'jjghurturt34', + description: 'desc', + isActive: false, + phases: [] + }) + } catch (e) { + should.equal(e.message.indexOf('"phases" must contain at least 1 items') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('create timeline template - unexpected field', async () => { + try { + await service.createTimelineTemplate({ + name: 'some name 1232323', + description: 'desc', + isActive: false, + phases: [phase], + other: 123 + }) + } catch (e) { + should.equal(e.message.indexOf('"other" is not allowed') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('get timeline template tests', () => { + it('get timeline template successfully', async () => { + const result = await service.getTimelineTemplate(id) + should.equal(result.id, id) + should.equal(result.name, name) + should.equal(result.description, 'desc') + should.equal(result.isActive, true) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, phase.id) + should.equal(result.phases[0].name, phase.name) + should.equal(result.phases[0].description, phase.description) + should.equal(result.phases[0].isActive, phase.isActive) + should.equal(result.phases[0].duration, phase.duration) + }) + + it('get timeline template - not found', async () => { + try { + await service.getTimelineTemplate(notFoundId) + } catch (e) { + should.equal(e.message, `TimelineTemplate with id: ${notFoundId} doesn't exist`) + return + } + throw new Error('should not reach here') + }) + + it('get timeline template - invalid id', async () => { + try { + await service.getTimelineTemplate('invalid') + } catch (e) { + should.equal(e.message.indexOf('"timelineTemplateId" must be a valid GUID') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('search timeline templates tests', () => { + it('search timeline templates successfully 1', async () => { + const result = await service.searchTimelineTemplates({ page: 1, perPage: 10, name: name.substring(1).toUpperCase() }) + should.equal(result.total, 1) + should.equal(result.page, 1) + should.equal(result.perPage, 10) + should.equal(result.result.length, 1) + should.equal(result.result[0].id, id) + should.equal(result.result[0].name, name) + should.equal(result.result[0].description, 'desc') + should.equal(result.result[0].isActive, true) + should.equal(result.result[0].phases.length, 1) + should.equal(result.result[0].phases[0].id, phase.id) + should.equal(result.result[0].phases[0].name, phase.name) + should.equal(result.result[0].phases[0].description, phase.description) + should.equal(result.result[0].phases[0].isActive, phase.isActive) + should.equal(result.result[0].phases[0].duration, phase.duration) + }) + + it('search timeline templates successfully 2', async () => { + const result = await service.searchTimelineTemplates({ name: 'xyzxyz123' }) + should.equal(result.total, 0) + should.equal(result.page, 1) + should.equal(result.perPage, 20) + should.equal(result.result.length, 0) + }) + + it('search timeline templates - invalid name', async () => { + try { + await service.searchTimelineTemplates({ name: ['invalid'] }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search timeline templates - invalid page', async () => { + try { + await service.searchTimelineTemplates({ page: -1 }) + } catch (e) { + should.equal(e.message.indexOf('"page" must be larger than or equal to 1') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search timeline templates - invalid perPage', async () => { + try { + await service.searchTimelineTemplates({ perPage: -1 }) + } catch (e) { + should.equal(e.message.indexOf('"perPage" must be larger than or equal to 1') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('search timeline templates - unexpected field', async () => { + try { + await service.searchTimelineTemplates({ other: 123 }) + } catch (e) { + should.equal(e.message.indexOf('"other" is not allowed') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('fully update timeline template tests', () => { + it('fully update timeline template successfully', async () => { + const result = await service.fullyUpdateTimelineTemplate(id, { + name: `${name}-updated`, + description: 'desc222', + isActive: false, + phases: [phase] + }) + should.equal(result.id, id) + should.equal(result.name, `${name}-updated`) + should.equal(result.description, 'desc222') + should.equal(result.isActive, false) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, phase.id) + should.equal(result.phases[0].name, phase.name) + should.equal(result.phases[0].description, phase.description) + should.equal(result.phases[0].isActive, phase.isActive) + should.equal(result.phases[0].duration, phase.duration) + }) + + it('fully update timeline template - name already used', async () => { + try { + await service.fullyUpdateTimelineTemplate(id, { + name: name2, + description: 'desc', + isActive: false, + phases: [phase] + }) + } catch (e) { + should.equal(e.message, `TimelineTemplate with name: ${name2} already exist`) + return + } + throw new Error('should not reach here') + }) + + it('fully update timeline template - not found', async () => { + try { + await service.fullyUpdateTimelineTemplate(notFoundId, { + name: 'slkdjflskjdf', + description: 'desc', + isActive: false, + phases: [phase] + }) + } catch (e) { + should.equal(e.message, `TimelineTemplate with id: ${notFoundId} doesn't exist`) + return + } + throw new Error('should not reach here') + }) + + it('fully update timeline template - invalid id', async () => { + try { + await service.fullyUpdateTimelineTemplate('invalid', { + name: 'slkdjflskjdf', + description: 'desc', + isActive: false, + phases: [phase] + }) + } catch (e) { + should.equal(e.message.indexOf('"timelineTemplateId" must be a valid GUID') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update timeline template - null name', async () => { + try { + await service.fullyUpdateTimelineTemplate(id, { + name: null, + description: 'desc', + isActive: false, + phases: [phase] + }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update timeline template - invalid name', async () => { + try { + await service.fullyUpdateTimelineTemplate(id, { + name: { invalid: 'x' }, + description: 'desc', + isActive: false, + phases: [phase] + }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update timeline template - empty name', async () => { + try { + await service.fullyUpdateTimelineTemplate(id, { + name: '', + description: 'desc', + isActive: false, + phases: [phase] + }) + } catch (e) { + should.equal(e.message.indexOf('"name" is not allowed to be empty') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update timeline template - invalid isActive', async () => { + try { + await service.fullyUpdateTimelineTemplate(id, { + name: 'asdfsadfsdf', + description: 'desc', + isActive: 'invalid', + phases: [phase] + }) + } catch (e) { + should.equal(e.message.indexOf('"isActive" must be a boolean') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('fully update timeline template - missing phases', async () => { + try { + await service.fullyUpdateTimelineTemplate(id, { + name: 'asdfsadfsdf', + description: 'desc', + isActive: true + }) + } catch (e) { + should.equal(e.message.indexOf('"phases" is required') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('partially update timeline template tests', () => { + it('partially update timeline template successfully 1', async () => { + const result = await service.partiallyUpdateTimelineTemplate(id, { + name: `${name}-33`, + description: 'desc33' + }) + should.equal(result.id, id) + should.equal(result.name, `${name}-33`) + should.equal(result.description, 'desc33') + should.equal(result.isActive, false) + should.equal(result.phases.length, 1) + should.equal(result.phases[0].id, phase.id) + should.equal(result.phases[0].name, phase.name) + should.equal(result.phases[0].description, phase.description) + should.equal(result.phases[0].isActive, phase.isActive) + should.equal(result.phases[0].duration, phase.duration) + }) + + it('partially update timeline template - name already used', async () => { + try { + await service.partiallyUpdateTimelineTemplate(id, { + name: name2 + }) + } catch (e) { + should.equal(e.message, `TimelineTemplate with name: ${name2} already exist`) + return + } + throw new Error('should not reach here') + }) + + it('partially update timeline template - not found', async () => { + try { + await service.partiallyUpdateTimelineTemplate(notFoundId, { + name: 'slkdjflskjdf' + }) + } catch (e) { + should.equal(e.message, `TimelineTemplate with id: ${notFoundId} doesn't exist`) + return + } + throw new Error('should not reach here') + }) + + it('partially update timeline template - invalid id', async () => { + try { + await service.partiallyUpdateTimelineTemplate('invalid', { name: 'hufdufhdfx' }) + } catch (e) { + should.equal(e.message.indexOf('"timelineTemplateId" must be a valid GUID') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('partially update timeline template - null name', async () => { + try { + await service.partiallyUpdateTimelineTemplate(id, { name: null }) + } catch (e) { + should.equal(e.message.indexOf('"name" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('partially update timeline template - invalid description', async () => { + try { + await service.partiallyUpdateTimelineTemplate(id, { description: { invalid: 'x' } }) + } catch (e) { + should.equal(e.message.indexOf('"description" must be a string') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('partially update timeline template - invalid isActive', async () => { + try { + await service.partiallyUpdateTimelineTemplate(id, { isActive: 'abc' }) + } catch (e) { + should.equal(e.message.indexOf('"isActive" must be a boolean') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('partially update timeline template - empty name', async () => { + try { + await service.partiallyUpdateTimelineTemplate(id, { name: '' }) + } catch (e) { + should.equal(e.message.indexOf('"name" is not allowed to be empty') >= 0, true) + return + } + throw new Error('should not reach here') + }) + + it('partially update timeline template - unexpected field', async () => { + try { + await service.partiallyUpdateTimelineTemplate(id, { name: 'ww', other: 'ww' }) + } catch (e) { + should.equal(e.message.indexOf('"other" is not allowed') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) + + describe('remove timeline template tests', () => { + it('remove timeline template successfully 1', async () => { + await service.deleteTimelineTemplate(id) + }) + + it('remove timeline template successfully 2', async () => { + await service.deleteTimelineTemplate(id2) + }) + + it('remove timeline template - not found', async () => { + try { + await service.deleteTimelineTemplate(notFoundId) + } catch (e) { + should.equal(e.message, `TimelineTemplate with id: ${notFoundId} doesn't exist`) + return + } + throw new Error('should not reach here') + }) + + it('remove timeline template - invalid id', async () => { + try { + await service.deleteTimelineTemplate('invalid') + } catch (e) { + should.equal(e.message.indexOf('"timelineTemplateId" must be a valid GUID') >= 0, true) + return + } + throw new Error('should not reach here') + }) + }) +})