From c0bb93b7cd6159e0f70d511ea3139cedb5234a6e Mon Sep 17 00:00:00 2001 From: Sandeep Date: Mon, 1 Feb 2021 17:10:33 +0530 Subject: [PATCH 01/12] migration to v5 --- config/default.js | 2 +- config/test.js | 2 +- src/common/helper.js | 186 ++++++++++++++++++++---------- src/services/SubmissionService.js | 20 +--- test/unit/prepare.js | 2 +- 5 files changed, 133 insertions(+), 79 deletions(-) diff --git a/config/default.js b/config/default.js index aaf4412b..5ba2d291 100755 --- a/config/default.js +++ b/config/default.js @@ -21,8 +21,8 @@ module.exports = { BUSAPI_URL: process.env.BUSAPI_URL || 'https://api.topcoder-dev.com/v5', KAFKA_ERROR_TOPIC: process.env.KAFKA_ERROR_TOPIC || 'error.notification', KAFKA_AGGREGATE_TOPIC: process.env.KAFKA_AGGREGATE_TOPIC || 'submission.notification.aggregate', - CHALLENGEAPI_URL: process.env.CHALLENGEAPI_URL || 'https://api.topcoder-dev.com/v4/challenges', CHALLENGEAPI_V5_URL: process.env.CHALLENGEAPI_V5_URL || 'https://api.topcoder-dev.com/v5/challenges', + RESOURCEAPI_V5_BASE_URL: process.env.RESOURCEAPI_V5_BASE_URL || 'https://api.topcoder-dev.com/v5', AUTH0_URL: process.env.AUTH0_URL, // Auth0 credentials for Submission Service AUTH0_AUDIENCE: process.env.AUTH0_AUDIENCE || 'https://www.topcoder.com', TOKEN_CACHE_TIME: process.env.TOKEN_CACHE_TIME, diff --git a/config/test.js b/config/test.js index c702b1f7..fde3087d 100644 --- a/config/test.js +++ b/config/test.js @@ -16,7 +16,7 @@ module.exports = { S3_BUCKET: process.env.S3_BUCKET_TEST || 'tc-testing-submissions' // S3 Bucket to which submissions need to be uploaded }, BUSAPI_EVENTS_URL: 'https://api.topcoder-dev.com/v5/bus/events', - CHALLENGEAPI_URL: 'https://api.topcoder-dev.com/v4/challenges', + CHALLENGEAPI_V5_URL: 'https://api.topcoder-dev.com/v5/challenges', esConfig: { ES_INDEX: process.env.ES_INDEX_TEST || 'submission-test', ES_TYPE: process.env.ES_TYPE_TEST || '_doc' // ES 6.x accepts only 1 Type per index and it's mandatory to define it diff --git a/src/common/helper.js b/src/common/helper.js index 0cd32dae..5b7059ee 100755 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -299,30 +299,6 @@ function * getM2Mtoken () { return yield m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) } -/** - * Get legacy challenge id if the challenge id is uuid form - * @param {String} challengeId Challenge ID - * @returns {String} Legacy Challenge ID of the given challengeId - */ -function * getLegacyChallengeId (challengeId) { - if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(challengeId)) { - logger.debug(`${challengeId} detected as uuid. Fetching legacy challenge id`) - const token = yield getM2Mtoken() - try { - const response = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${challengeId}`) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') - const legacyId = parseInt(response.body.legacyId, 10) - logger.debug(`Legacy challenge id is ${legacyId} for v5 challenge id ${challengeId}`) - return legacyId - } catch (err) { - logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${challengeId}`) - throw err - } - } - return challengeId -} - /* * Get submission phase ID of a challenge from Challenge API * @param challengeId Challenge ID @@ -335,20 +311,20 @@ function * getSubmissionPhaseId (challengeId) { try { logger.info(`Calling to challenge API to find submission phase Id for ${challengeId}`) const token = yield getM2Mtoken() - response = yield request.get(`${config.CHALLENGEAPI_URL}/${challengeId}/phases`) + response = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${challengeId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') logger.info(`returned from finding submission phase Id for ${challengeId}`) } catch (ex) { - logger.error(`Error while accessing ${config.CHALLENGEAPI_URL}/${challengeId}/phases`) + logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${challengeId}`) logger.debug('Setting submissionPhaseId to Null') response = null } if (response) { - const phases = _.get(response.body, 'result.content', []) - const checkPoint = _.filter(phases, { phaseType: 'Checkpoint Submission', phaseStatus: 'Open' }) - const submissionPh = _.filter(phases, { phaseType: 'Submission', phaseStatus: 'Open' }) - const finalFixPh = _.filter(phases, { phaseType: 'Final Fix', phaseStatus: 'Open' }) + const phases = _.get(response.body, 'phases', []) + const checkPoint = _.filter(phases, { name: 'Checkpoint Submission', isOpen: true }) + const submissionPh = _.filter(phases, { name: 'Submission', isOpen: true }) + const finalFixPh = _.filter(phases, { name: 'Final Fix', isOpen: true }) if (checkPoint.length !== 0) { phaseId = checkPoint[0].id } else if (submissionPh.length !== 0) { @@ -379,31 +355,45 @@ function * checkCreateAccess (authUser, subEntity) { try { logger.info(`Calling to challenge API for fetch phases and winners for ${subEntity.challengeId}`) - challengeDetails = yield request.get(`${config.CHALLENGEAPI_URL}?filter=id=${subEntity.challengeId}`) + challengeDetails = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${subEntity.challengeId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') logger.info(`returned for ${subEntity.challengeId} with ${JSON.stringify(challengeDetails)}`) } catch (ex) { - logger.error(`Error while accessing ${config.CHALLENGEAPI_URL}?filter=id=${subEntity.challengeId}`) + logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${subEntity.challengeId}`) logger.error(ex) throw new errors.HttpStatusError(503, `Could not fetch details of challenge with id ${subEntity.challengeId}`) } try { - resources = yield request.get(`${config.CHALLENGEAPI_URL}/${subEntity.challengeId}/resources`) + resources = yield request.get(`${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${subEntity.challengeId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') } catch (ex) { - logger.error(`Error while accessing ${config.CHALLENGEAPI_URL}/${subEntity.challengeId}/resources`) + logger.error(`Error while accessing ${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${subEntity.challengeId}`) logger.error(ex) throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${subEntity.challengeId}`) } + // Get map of role id to role name + const resourceRolesMap = yield getRoleIdToRoleNameMap() + + // Check if role id to role name mapping is available. If not user's role cannot be determined. + if (resourceRolesMap == null || _.size(resourceRolesMap) === 0) { + throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${subEntity.challengeId}`) + } + if (resources && challengeDetails) { - const currUserRoles = _.filter(resources.body.result.content, { properties: { Handle: authUser.handle } }) + const currUserRoles = _.filter(resources.body, { memberHandle: authUser.handle }) + + // Populate the role names for the current user role ids + _.forEach(currUserRoles, currentUserRole => { + currentUserRole.role = resourceRolesMap[currentUserRole.roleId] + }) + // Get phases and winner detail from challengeDetails - const phases = challengeDetails.body.result.content[0].allPhases - const winner = challengeDetails.body.result.content[0].winners + const phases = challengeDetails.body.phases + const winner = challengeDetails.body.winners // Check if the User is registered for the contest const submitters = _.filter(currUserRoles, { role: 'Submitter' }) @@ -419,7 +409,7 @@ function * checkCreateAccess (authUser, subEntity) { const currPhase = _.filter(phases, { id: submissionPhaseId }) - if (currPhase[0].phaseType === 'Final Fix') { + if (currPhase[0].name === 'Final Fix') { if (!authUser.handle.equals(winner[0].handle)) { throw new errors.HttpStatusError(403, 'Only winner is allowed to submit during Final Fix phase') } @@ -448,30 +438,43 @@ function * checkGetAccess (authUser, submission) { const token = yield getM2Mtoken() try { - resources = yield request.get(`${config.CHALLENGEAPI_URL}/${submission.challengeId}/resources`) + resources = yield request.get(`${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${submission.challengeId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') } catch (ex) { - logger.error(`Error while accessing ${config.CHALLENGEAPI_URL}/${submission.challengeId}/resources`) + logger.error(`Error while accessing ${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${submission.challengeId}`) logger.error(ex) throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${submission.challengeId}`) } try { - challengeDetails = yield request.get(`${config.CHALLENGEAPI_URL}?filter=id=${submission.challengeId}`) + challengeDetails = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${submission.challengeId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') } catch (ex) { - logger.error(`Error while accessing ${config.CHALLENGEAPI_URL}?filter=id=${submission.challengeId}`) + logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${submission.challengeId}`) logger.error(ex) throw new errors.HttpStatusError(503, `Could not fetch details of challenge with id ${submission.challengeId}`) } + // Get map of role id to role name + const resourceRolesMap = yield getRoleIdToRoleNameMap() + + // Check if role id to role name mapping is available. If not user's role cannot be determined. + if (resourceRolesMap == null || _.size(resourceRolesMap) === 0) { + throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${submission.challengeId}`) + } + if (resources && challengeDetails) { // Fetch all roles of the User pertaining to the current challenge - const currUserRoles = _.filter(resources.body.result.content, { properties: { Handle: authUser.handle } }) - const subTrack = challengeDetails.body.result.content[0].subTrack - const phases = challengeDetails.body.result.content[0].allPhases + const currUserRoles = _.filter(resources.body, { memberHandle: authUser.handle }) + + // Populate the role names for the current user role ids + _.forEach(currUserRoles, currentUserRole => { + currentUserRole.role = resourceRolesMap[currentUserRole.roleId] + }) + + const subTrack = challengeDetails.body.legacy.subTrack // Check if the User is a Copilot const copilot = _.filter(currUserRoles, { role: 'Copilot' }) @@ -494,18 +497,18 @@ function * checkGetAccess (authUser, submission) { // User is either a Reviewer or Screener if (screener.length !== 0 || reviewer.length !== 0) { - const screeningPhase = _.filter(phases, { phaseType: 'Screening', phaseStatus: 'Scheduled' }) - const reviewPhase = _.filter(phases, { phaseType: 'Review', phaseStatus: 'Scheduled' }) + const screeningPhaseStatus = getPhaseStatus('Screening', challengeDetails.body) + const reviewPhaseStatus = getPhaseStatus('Review', challengeDetails.body) // Neither Screening Nor Review is Opened / Closed - if (screeningPhase.length !== 0 && reviewPhase.length !== 0) { + if (screeningPhaseStatus === 'Scheduled' && reviewPhaseStatus === 'Scheduled') { throw new errors.HttpStatusError(403, 'You can access the submission only when Screening / Review is open') } } else { - const appealsResponse = _.filter(phases, { phaseType: 'Appeals Response', phaseStatus: 'Closed' }) + const appealsResponseStatus = getPhaseStatus('Appeals Response', challengeDetails.body) // Appeals Response is not closed yet - if (appealsResponse.length === 0) { + if (appealsResponseStatus !== 'Closed') { throw new errors.HttpStatusError(403, 'You cannot access other submissions before the end of Appeals Response phase') } else { const userSubmission = yield fetchFromES({ @@ -545,28 +548,27 @@ function * checkReviewGetAccess (authUser, submission) { const token = yield getM2Mtoken() try { - challengeDetails = yield request.get(`${config.CHALLENGEAPI_URL}?filter=id=${submission.challengeId}`) + challengeDetails = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${submission.challengeId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') } catch (ex) { - logger.error(`Error while accessing ${config.CHALLENGEAPI_URL}?filter=id=${submission.challengeId}`) + logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${submission.challengeId}`) logger.error(ex) return false } if (challengeDetails) { - const subTrack = challengeDetails.body.result.content[0].subTrack - const phases = challengeDetails.body.result.content[0].allPhases + const subTrack = challengeDetails.body.legacy.subTrack // For Marathon Match, everyone can access review result if (subTrack === 'DEVELOP_MARATHON_MATCH') { logger.info('No access check for Marathon match') return true } else { - const appealsResponse = _.filter(phases, { phaseType: 'Appeals Response', phaseStatus: 'Closed' }) + const appealsResponseStatus = getPhaseStatus('Appeals Response', challengeDetails.body) // Appeals Response is not closed yet - if (appealsResponse.length === 0) { + if (appealsResponseStatus !== 'Closed') { throw new errors.HttpStatusError(403, 'You cannot access the review before the end of the Appeals Response phase') } @@ -642,6 +644,74 @@ function cleanseReviews (reviews, authUser) { return reviews } +/** + * Function to get role id to role name map + * @returns {Object|null} map + */ +function * getRoleIdToRoleNameMap () { + let resourceRoles + let resourceRolesMap = null + const token = yield getM2Mtoken() + try { + resourceRoles = yield request.get(`${config.RESOURCEAPI_V5_BASE_URL}/resource-roles`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + } catch (ex) { + logger.error(`Error while accessing ${config.RESOURCEAPI_V5_BASE_URL}/resource-roles`) + logger.error(ex) + resourceRoles = null + } + if (resourceRoles) { + resourceRolesMap = {} + _.forEach(resourceRoles.body, resourceRole => { + resourceRolesMap[resourceRole.id] = resourceRole.name + }) + } + return resourceRolesMap +} + +/** + * Function to get phase status of phases used in an active challenge + * @param {String} phaseName the phase name for retrieving status + * @param {Object} challengeDetails the challenge details + * @returns {('Scheduled' | 'Open' | 'Closed' | 'Invalid')} status of the phase + */ +function * getPhaseStatus (phaseName, challengeDetails) { + const phases = challengeDetails.phases + if (challengeDetails.status === 'Completed') { + return 'Closed' + } else if (challengeDetails.status === 'Active') { + const queriedPhaseIndex = _.findIndex(phases, phase => { + return phase.name === phaseName + }) + // Requested phase name could not be found in phases hence 'Invalid' + if (queriedPhaseIndex === -1) { + return 'Invalid' + } + // If requested phase name is open return 'Open' + if (phases[queriedPhaseIndex].isOpen) { + return 'Open' + } + + // Search for phase where isOpen == true from list of phases + // Phases are already in sorted order as per challenge-api repository + const currentOpenPhaseIndex = _.findLastIndex(phases, phase => { + return phase.isOpen === true + }) + + // if queried phase occurs before current open phase it is 'Closed' + // else it is 'Scheduled' + if (currentOpenPhaseIndex !== -1) { + return currentOpenPhaseIndex > queriedPhaseIndex ? 'Closed' : 'Scheduled' + } else { + // if no phase is open but the challenge is Active return Scheduled + return 'Scheduled' + } + } else { // if challenge is not in Active or Completed state return Scheduled + return 'Scheduled' + } +} + module.exports = { wrapExpress, autoWrapExpress, @@ -649,12 +719,12 @@ module.exports = { fetchFromES, camelize, setPaginationHeaders, - getLegacyChallengeId, getSubmissionPhaseId, checkCreateAccess, checkGetAccess, checkReviewGetAccess, downloadFile, postToBusApi, - cleanseReviews + cleanseReviews, + getRoleIdToRoleNameMap } diff --git a/src/services/SubmissionService.js b/src/services/SubmissionService.js index efa4a4e9..66291545 100755 --- a/src/services/SubmissionService.js +++ b/src/services/SubmissionService.js @@ -181,12 +181,6 @@ function * downloadSubmission (authUser, submissionId) { * @return {Object} Data fetched from ES */ function * listSubmissions (authUser, query) { - if (query.challengeId) { - // Submission api only works with legacy challenge id - // If it is a v5 challenge id, get the associated legacy challenge id - query.challengeId = yield helper.getLegacyChallengeId(query.challengeId) - } - const data = yield helper.fetchFromES(query, helper.camelize(table)) logger.info(`listSubmissions: returning ${data.length} submissions for query: ${JSON.stringify(query)}`) @@ -268,10 +262,6 @@ function * createSubmission (authUser, files, entity) { throw new errors.HttpStatusError(400, 'The file should be uploaded under the "submission" attribute') } - // Submission api only works with legacy challenge id - // If it is a v5 challenge id, get the associated legacy challenge id - const challengeId = yield helper.getLegacyChallengeId(entity.challengeId) - const currDate = (new Date()).toISOString() const item = { @@ -279,7 +269,7 @@ function * createSubmission (authUser, files, entity) { type: entity.type, url: url, memberId: entity.memberId, - challengeId: challengeId, + challengeId: entity.challengeId, created: currDate, updated: currDate, createdBy: authUser.handle || authUser.sub, @@ -297,7 +287,7 @@ function * createSubmission (authUser, files, entity) { if (entity.submissionPhaseId) { item.submissionPhaseId = entity.submissionPhaseId } else { - item.submissionPhaseId = yield helper.getSubmissionPhaseId(challengeId) + item.submissionPhaseId = yield helper.getSubmissionPhaseId(entity.challengeId) } if (entity.fileType) { @@ -391,12 +381,6 @@ function * _updateSubmission (authUser, submissionId, entity) { throw new errors.HttpStatusError(404, `Submission with ID = ${submissionId} is not found`) } - if (entity.challengeId) { - // Submission api only works with legacy challenge id - // If it is a v5 challenge id, get the associated legacy challenge id - entity.challengeId = yield helper.getLegacyChallengeId(entity.challengeId) - } - const currDate = (new Date()).toISOString() // Record used for updating in Database const record = { diff --git a/test/unit/prepare.js b/test/unit/prepare.js index 824420db..300ace33 100644 --- a/test/unit/prepare.js +++ b/test/unit/prepare.js @@ -70,7 +70,7 @@ prepare(function (done) { // Mock Posting to Bus API and ES interactions const authUrl = URL.parse(config.AUTH0_URL) const busUrl = URL.parse(config.BUSAPI_EVENTS_URL) - const challengeApiUrl = URL.parse(`${config.CHALLENGEAPI_URL}/30049360/phases`) + const challengeApiUrl = URL.parse(`${config.CHALLENGEAPI_V5_URL}/30049360/phases`) nock(/.com/) .persist() From 05a464aae64e464e3c2edb0e6568a5a255d89e8c Mon Sep 17 00:00:00 2001 From: Sachin Maheshwari Date: Wed, 3 Feb 2021 11:10:09 +0530 Subject: [PATCH 02/12] just verfiying the branch on dev env --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d71b5cf1..47bba1e3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -67,7 +67,7 @@ workflows: context : org-global filters: branches: - only: ['develop'] + only: ['develop', 'develop-temp'] - "build-prod": context : org-global filters: From a0a788bc67849587d1704866357341721e46031d Mon Sep 17 00:00:00 2001 From: mark-nakachon Date: Wed, 3 Feb 2021 23:16:42 +0530 Subject: [PATCH 03/12] migration to v5 API --- config/default.js | 2 +- config/test.js | 8 +- src/common/helper.js | 180 ++++++++----- src/services/SubmissionService.js | 28 +- test/common/testData.js | 318 ++++++++++++++++------- test/unit/ReviewService.test.js | 5 +- test/unit/ReviewSummationService.test.js | 2 +- test/unit/ReviewTypeService.test.js | 5 +- test/unit/SubmissionService.test.js | 56 ++-- test/unit/prepare.js | 20 +- 10 files changed, 408 insertions(+), 216 deletions(-) diff --git a/config/default.js b/config/default.js index aaf4412b..5ba2d291 100755 --- a/config/default.js +++ b/config/default.js @@ -21,8 +21,8 @@ module.exports = { BUSAPI_URL: process.env.BUSAPI_URL || 'https://api.topcoder-dev.com/v5', KAFKA_ERROR_TOPIC: process.env.KAFKA_ERROR_TOPIC || 'error.notification', KAFKA_AGGREGATE_TOPIC: process.env.KAFKA_AGGREGATE_TOPIC || 'submission.notification.aggregate', - CHALLENGEAPI_URL: process.env.CHALLENGEAPI_URL || 'https://api.topcoder-dev.com/v4/challenges', CHALLENGEAPI_V5_URL: process.env.CHALLENGEAPI_V5_URL || 'https://api.topcoder-dev.com/v5/challenges', + RESOURCEAPI_V5_BASE_URL: process.env.RESOURCEAPI_V5_BASE_URL || 'https://api.topcoder-dev.com/v5', AUTH0_URL: process.env.AUTH0_URL, // Auth0 credentials for Submission Service AUTH0_AUDIENCE: process.env.AUTH0_AUDIENCE || 'https://www.topcoder.com', TOKEN_CACHE_TIME: process.env.TOKEN_CACHE_TIME, diff --git a/config/test.js b/config/test.js index c702b1f7..4b9a3680 100644 --- a/config/test.js +++ b/config/test.js @@ -7,7 +7,7 @@ module.exports = { LOG_LEVEL: 'info', WEB_SERVER_PORT: 3010, AUTH_SECRET: 'mysecret', - VALID_ISSUERS: '["https://api.topcoder.com"]', + VALID_ISSUERS: process.env.VALID_ISSUERS ? process.env.VALID_ISSUERS.replace(/\\"/g, '') : '["https://api.topcoder.com","https://topcoder-dev.auth0.com/"]', API_VERSION: process.env.API_VERSION || '/api/v5', aws: { AWS_REGION: process.env.AWS_REGION || 'us-east-1', // AWS Region to be used by the application @@ -16,14 +16,14 @@ module.exports = { S3_BUCKET: process.env.S3_BUCKET_TEST || 'tc-testing-submissions' // S3 Bucket to which submissions need to be uploaded }, BUSAPI_EVENTS_URL: 'https://api.topcoder-dev.com/v5/bus/events', - CHALLENGEAPI_URL: 'https://api.topcoder-dev.com/v4/challenges', + BUSAPI_URL: 'https://api.topcoder-dev.com/v5', + CHALLENGEAPI_V5_URL: 'https://api.topcoder-dev.com/v5/challenges', esConfig: { ES_INDEX: process.env.ES_INDEX_TEST || 'submission-test', ES_TYPE: process.env.ES_TYPE_TEST || '_doc' // ES 6.x accepts only 1 Type per index and it's mandatory to define it }, AUTH0_URL: process.env.AUTH0_URL, // Auth0 credentials for Submission Service - AUTH0_AUDIENCE: process.env.AUTH0_AUDIENCE || 'https://www.topcoder.com', - TOKEN_CACHE_TIME: process.env.TOKEN_CACHE_TIME, + AUTH0_AUDIENCE: process.env.AUTH0_AUDIENCE, AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET, USER_TOKEN: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLmNvbSIsImhhbmRsZSI6IlNoYXJhdGhrdW1hcjkyIiwiZXhwIjo1NTUzMDE5OTI1OSwidXNlcklkIjoiNDA0OTMwNTAiLCJpYXQiOjE1MzAxOTg2NTksImVtYWlsIjoiU2hhcmF0aGt1bWFyOTJAdG9wY29kZXIuY29tIiwianRpIjoiYzNhYzYwOGEtNTZiZS00NWQwLThmNmEtMzFmZTk0Yjk1NjFjIn0.2gtNJwhcv7MYc-muX3Nv-B0RdWbhMRl7-xrwFUsLazM', diff --git a/src/common/helper.js b/src/common/helper.js index 0cd32dae..5289ea67 100755 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -53,7 +53,7 @@ function autoWrapExpress (obj) { return obj } _.each(obj, (value, key) => { - obj[key] = autoWrapExpress(value); //eslint-disable-line + obj[key] = autoWrapExpress(value); //eslint-disable-line }) return obj } @@ -299,30 +299,6 @@ function * getM2Mtoken () { return yield m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) } -/** - * Get legacy challenge id if the challenge id is uuid form - * @param {String} challengeId Challenge ID - * @returns {String} Legacy Challenge ID of the given challengeId - */ -function * getLegacyChallengeId (challengeId) { - if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(challengeId)) { - logger.debug(`${challengeId} detected as uuid. Fetching legacy challenge id`) - const token = yield getM2Mtoken() - try { - const response = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${challengeId}`) - .set('Authorization', `Bearer ${token}`) - .set('Content-Type', 'application/json') - const legacyId = parseInt(response.body.legacyId, 10) - logger.debug(`Legacy challenge id is ${legacyId} for v5 challenge id ${challengeId}`) - return legacyId - } catch (err) { - logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${challengeId}`) - throw err - } - } - return challengeId -} - /* * Get submission phase ID of a challenge from Challenge API * @param challengeId Challenge ID @@ -335,20 +311,20 @@ function * getSubmissionPhaseId (challengeId) { try { logger.info(`Calling to challenge API to find submission phase Id for ${challengeId}`) const token = yield getM2Mtoken() - response = yield request.get(`${config.CHALLENGEAPI_URL}/${challengeId}/phases`) + response = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${challengeId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') logger.info(`returned from finding submission phase Id for ${challengeId}`) } catch (ex) { - logger.error(`Error while accessing ${config.CHALLENGEAPI_URL}/${challengeId}/phases`) + logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${challengeId}`) logger.debug('Setting submissionPhaseId to Null') response = null } if (response) { - const phases = _.get(response.body, 'result.content', []) - const checkPoint = _.filter(phases, { phaseType: 'Checkpoint Submission', phaseStatus: 'Open' }) - const submissionPh = _.filter(phases, { phaseType: 'Submission', phaseStatus: 'Open' }) - const finalFixPh = _.filter(phases, { phaseType: 'Final Fix', phaseStatus: 'Open' }) + const phases = _.get(response.body, 'phases', []) + const checkPoint = _.filter(phases, { name: 'Checkpoint Submission', isOpen: true }) + const submissionPh = _.filter(phases, { name: 'Submission', isOpen: true }) + const finalFixPh = _.filter(phases, { name: 'Final Fix', isOpen: true }) if (checkPoint.length !== 0) { phaseId = checkPoint[0].id } else if (submissionPh.length !== 0) { @@ -379,31 +355,45 @@ function * checkCreateAccess (authUser, subEntity) { try { logger.info(`Calling to challenge API for fetch phases and winners for ${subEntity.challengeId}`) - challengeDetails = yield request.get(`${config.CHALLENGEAPI_URL}?filter=id=${subEntity.challengeId}`) + challengeDetails = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${subEntity.challengeId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') logger.info(`returned for ${subEntity.challengeId} with ${JSON.stringify(challengeDetails)}`) } catch (ex) { - logger.error(`Error while accessing ${config.CHALLENGEAPI_URL}?filter=id=${subEntity.challengeId}`) + logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${subEntity.challengeId}`) logger.error(ex) throw new errors.HttpStatusError(503, `Could not fetch details of challenge with id ${subEntity.challengeId}`) } try { - resources = yield request.get(`${config.CHALLENGEAPI_URL}/${subEntity.challengeId}/resources`) + resources = yield request.get(`${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${subEntity.challengeId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') } catch (ex) { - logger.error(`Error while accessing ${config.CHALLENGEAPI_URL}/${subEntity.challengeId}/resources`) + logger.error(`Error while accessing ${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${subEntity.challengeId}`) logger.error(ex) throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${subEntity.challengeId}`) } + // Get map of role id to role name + const resourceRolesMap = yield getRoleIdToRoleNameMap() + + // Check if role id to role name mapping is available. If not user's role cannot be determined. + if (resourceRolesMap == null || _.size(resourceRolesMap) === 0) { + throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${subEntity.challengeId}`) + } + if (resources && challengeDetails) { - const currUserRoles = _.filter(resources.body.result.content, { properties: { Handle: authUser.handle } }) + const currUserRoles = _.filter(resources.body, { memberHandle: authUser.handle }) + + // Populate the role names for the current user role ids + _.forEach(currUserRoles, currentUserRole => { + currentUserRole.role = resourceRolesMap[currentUserRole.roleId] + }) + // Get phases and winner detail from challengeDetails - const phases = challengeDetails.body.result.content[0].allPhases - const winner = challengeDetails.body.result.content[0].winners + const phases = challengeDetails.body.phases + const winner = challengeDetails.body.winners // Check if the User is registered for the contest const submitters = _.filter(currUserRoles, { role: 'Submitter' }) @@ -419,7 +409,7 @@ function * checkCreateAccess (authUser, subEntity) { const currPhase = _.filter(phases, { id: submissionPhaseId }) - if (currPhase[0].phaseType === 'Final Fix') { + if (currPhase[0].name === 'Final Fix') { if (!authUser.handle.equals(winner[0].handle)) { throw new errors.HttpStatusError(403, 'Only winner is allowed to submit during Final Fix phase') } @@ -448,30 +438,43 @@ function * checkGetAccess (authUser, submission) { const token = yield getM2Mtoken() try { - resources = yield request.get(`${config.CHALLENGEAPI_URL}/${submission.challengeId}/resources`) + resources = yield request.get(`${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${submission.challengeId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') } catch (ex) { - logger.error(`Error while accessing ${config.CHALLENGEAPI_URL}/${submission.challengeId}/resources`) + logger.error(`Error while accessing ${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${submission.challengeId}`) logger.error(ex) throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${submission.challengeId}`) } try { - challengeDetails = yield request.get(`${config.CHALLENGEAPI_URL}?filter=id=${submission.challengeId}`) + challengeDetails = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${submission.challengeId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') } catch (ex) { - logger.error(`Error while accessing ${config.CHALLENGEAPI_URL}?filter=id=${submission.challengeId}`) + logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${submission.challengeId}`) logger.error(ex) throw new errors.HttpStatusError(503, `Could not fetch details of challenge with id ${submission.challengeId}`) } + // Get map of role id to role name + const resourceRolesMap = yield getRoleIdToRoleNameMap() + + // Check if role id to role name mapping is available. If not user's role cannot be determined. + if (resourceRolesMap == null || _.size(resourceRolesMap) === 0) { + throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${submission.challengeId}`) + } + if (resources && challengeDetails) { // Fetch all roles of the User pertaining to the current challenge - const currUserRoles = _.filter(resources.body.result.content, { properties: { Handle: authUser.handle } }) - const subTrack = challengeDetails.body.result.content[0].subTrack - const phases = challengeDetails.body.result.content[0].allPhases + const currUserRoles = _.filter(resources.body, { memberHandle: authUser.handle }) + + // Populate the role names for the current user role ids + _.forEach(currUserRoles, currentUserRole => { + currentUserRole.role = resourceRolesMap[currentUserRole.roleId] + }) + + const subTrack = challengeDetails.body.legacy.subTrack // Check if the User is a Copilot const copilot = _.filter(currUserRoles, { role: 'Copilot' }) @@ -494,18 +497,18 @@ function * checkGetAccess (authUser, submission) { // User is either a Reviewer or Screener if (screener.length !== 0 || reviewer.length !== 0) { - const screeningPhase = _.filter(phases, { phaseType: 'Screening', phaseStatus: 'Scheduled' }) - const reviewPhase = _.filter(phases, { phaseType: 'Review', phaseStatus: 'Scheduled' }) + const screeningPhaseStatus = getPhaseStatus('Screening', challengeDetails.body) + const reviewPhaseStatus = getPhaseStatus('Review', challengeDetails.body) // Neither Screening Nor Review is Opened / Closed - if (screeningPhase.length !== 0 && reviewPhase.length !== 0) { + if (screeningPhaseStatus === 'Scheduled' && reviewPhaseStatus === 'Scheduled') { throw new errors.HttpStatusError(403, 'You can access the submission only when Screening / Review is open') } } else { - const appealsResponse = _.filter(phases, { phaseType: 'Appeals Response', phaseStatus: 'Closed' }) + const appealsResponseStatus = getPhaseStatus('Appeals Response', challengeDetails.body) // Appeals Response is not closed yet - if (appealsResponse.length === 0) { + if (appealsResponseStatus !== 'Closed') { throw new errors.HttpStatusError(403, 'You cannot access other submissions before the end of Appeals Response phase') } else { const userSubmission = yield fetchFromES({ @@ -545,28 +548,27 @@ function * checkReviewGetAccess (authUser, submission) { const token = yield getM2Mtoken() try { - challengeDetails = yield request.get(`${config.CHALLENGEAPI_URL}?filter=id=${submission.challengeId}`) + challengeDetails = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${submission.challengeId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') } catch (ex) { - logger.error(`Error while accessing ${config.CHALLENGEAPI_URL}?filter=id=${submission.challengeId}`) + logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${submission.challengeId}`) logger.error(ex) return false } if (challengeDetails) { - const subTrack = challengeDetails.body.result.content[0].subTrack - const phases = challengeDetails.body.result.content[0].allPhases + const subTrack = challengeDetails.body.legacy.subTrack // For Marathon Match, everyone can access review result if (subTrack === 'DEVELOP_MARATHON_MATCH') { logger.info('No access check for Marathon match') return true } else { - const appealsResponse = _.filter(phases, { phaseType: 'Appeals Response', phaseStatus: 'Closed' }) + const appealsResponseStatus = getPhaseStatus('Appeals Response', challengeDetails.body) // Appeals Response is not closed yet - if (appealsResponse.length === 0) { + if (appealsResponseStatus !== 'Closed') { throw new errors.HttpStatusError(403, 'You cannot access the review before the end of the Appeals Response phase') } @@ -642,6 +644,66 @@ function cleanseReviews (reviews, authUser) { return reviews } +/** + * Function to get role id to role name map + * @returns {Object|null} map + */ +function * getRoleIdToRoleNameMap () { + let resourceRoles + let resourceRolesMap = null + const token = yield getM2Mtoken() + try { + resourceRoles = yield request.get(`${config.RESOURCEAPI_V5_BASE_URL}/resource-roles`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + } catch (ex) { + logger.error(`Error while accessing ${config.RESOURCEAPI_V5_BASE_URL}/resource-roles`) + logger.error(ex) + resourceRoles = null + } + if (resourceRoles) { + resourceRolesMap = {} + _.forEach(resourceRoles.body, resourceRole => { + resourceRolesMap[resourceRole.id] = resourceRole.name + }) + } + return resourceRolesMap +} + +/** + * Function to get phase status of phases used in an active challenge + * @param {String} phaseName the phase name for retrieving status + * @param {Object} challengeDetails the challenge details + * @returns {('Scheduled' | 'Open' | 'Closed' | 'Invalid')} status of the phase + */ +function getPhaseStatus (phaseName, challengeDetails) { + const { phases } = challengeDetails + const queriedPhaseIndex = _.findIndex(phases, phase => { + return phase.name === phaseName + }) + // Requested phase name could not be found in phases hence 'Invalid' + if (queriedPhaseIndex === -1) { + return 'Invalid' + } + // If requested phase name is open return 'Open' + if (phases[queriedPhaseIndex].isOpen) { + return 'Open' + } else { + const { actualEndDate } = phases[queriedPhaseIndex] + if (!_.isEmpty(actualEndDate)) { + const present = new Date().getTime() + const actualDate = new Date(actualEndDate).getTime() + if (present > actualDate) { + return 'Closed' + } else { + return 'Scheduled' + } + } else { + return 'Scheduled' + } + } +} + module.exports = { wrapExpress, autoWrapExpress, @@ -649,12 +711,12 @@ module.exports = { fetchFromES, camelize, setPaginationHeaders, - getLegacyChallengeId, getSubmissionPhaseId, checkCreateAccess, checkGetAccess, checkReviewGetAccess, downloadFile, postToBusApi, - cleanseReviews + cleanseReviews, + getRoleIdToRoleNameMap } diff --git a/src/services/SubmissionService.js b/src/services/SubmissionService.js index efa4a4e9..20c323dc 100755 --- a/src/services/SubmissionService.js +++ b/src/services/SubmissionService.js @@ -181,12 +181,6 @@ function * downloadSubmission (authUser, submissionId) { * @return {Object} Data fetched from ES */ function * listSubmissions (authUser, query) { - if (query.challengeId) { - // Submission api only works with legacy challenge id - // If it is a v5 challenge id, get the associated legacy challenge id - query.challengeId = yield helper.getLegacyChallengeId(query.challengeId) - } - const data = yield helper.fetchFromES(query, helper.camelize(table)) logger.info(`listSubmissions: returning ${data.length} submissions for query: ${JSON.stringify(query)}`) @@ -203,7 +197,7 @@ const listSubmissionsQuerySchema = { type: joi.string(), url: joi.string().uri().trim(), memberId: joi.alternatives().try(joi.id(), joi.string().uuid()), - challengeId: joi.alternatives().try(joi.id(), joi.string().uuid()), + challengeId: joi.string().uuid(), legacySubmissionId: joi.alternatives().try(joi.id(), joi.string().uuid()), legacyUploadId: joi.alternatives().try(joi.id(), joi.string().uuid()), submissionPhaseId: joi.id(), @@ -268,10 +262,6 @@ function * createSubmission (authUser, files, entity) { throw new errors.HttpStatusError(400, 'The file should be uploaded under the "submission" attribute') } - // Submission api only works with legacy challenge id - // If it is a v5 challenge id, get the associated legacy challenge id - const challengeId = yield helper.getLegacyChallengeId(entity.challengeId) - const currDate = (new Date()).toISOString() const item = { @@ -279,7 +269,7 @@ function * createSubmission (authUser, files, entity) { type: entity.type, url: url, memberId: entity.memberId, - challengeId: challengeId, + challengeId: entity.challengeId, created: currDate, updated: currDate, createdBy: authUser.handle || authUser.sub, @@ -297,7 +287,7 @@ function * createSubmission (authUser, files, entity) { if (entity.submissionPhaseId) { item.submissionPhaseId = entity.submissionPhaseId } else { - item.submissionPhaseId = yield helper.getSubmissionPhaseId(challengeId) + item.submissionPhaseId = yield helper.getSubmissionPhaseId(entity.challengeId) } if (entity.fileType) { @@ -366,7 +356,7 @@ createSubmission.schema = { fileType: joi.string(), url: joi.string().uri().trim(), memberId: joi.alternatives().try(joi.id(), joi.string().uuid()).required(), - challengeId: joi.alternatives().try(joi.id(), joi.string().uuid()).required(), + challengeId: joi.string().uuid().required(), legacySubmissionId: joi.alternatives().try(joi.id(), joi.string().uuid()), legacyUploadId: joi.alternatives().try(joi.id(), joi.string().uuid()), submissionPhaseId: joi.id(), @@ -391,12 +381,6 @@ function * _updateSubmission (authUser, submissionId, entity) { throw new errors.HttpStatusError(404, `Submission with ID = ${submissionId} is not found`) } - if (entity.challengeId) { - // Submission api only works with legacy challenge id - // If it is a v5 challenge id, get the associated legacy challenge id - entity.challengeId = yield helper.getLegacyChallengeId(entity.challengeId) - } - const currDate = (new Date()).toISOString() // Record used for updating in Database const record = { @@ -496,7 +480,7 @@ updateSubmission.schema = { type: joi.string(), url: joi.string().uri().trim().required(), memberId: joi.alternatives().try(joi.id(), joi.string().uuid()).required(), - challengeId: joi.alternatives().try(joi.id(), joi.string().uuid()).required(), + challengeId: joi.string().uuid().required(), legacySubmissionId: joi.alternatives().try(joi.id(), joi.string().uuid()), legacyUploadId: joi.alternatives().try(joi.id(), joi.string().uuid()), submissionPhaseId: joi.id(), @@ -522,7 +506,7 @@ patchSubmission.schema = { type: joi.string(), url: joi.string().uri().trim(), memberId: joi.alternatives().try(joi.id(), joi.string().uuid()), - challengeId: joi.alternatives().try(joi.id(), joi.string().uuid()), + challengeId: joi.string().uuid(), legacySubmissionId: joi.alternatives().try(joi.id(), joi.string().uuid()), legacyUploadId: joi.alternatives().try(joi.id(), joi.string().uuid()), submissionPhaseId: joi.id(), diff --git a/test/common/testData.js b/test/common/testData.js index 2a2bb39f..fdeddb2a 100644 --- a/test/common/testData.js +++ b/test/common/testData.js @@ -121,19 +121,63 @@ const testReviewTypesES = { const nonExSubmissionId = 'b3564180-65aa-42ec-a945-5fd21dec0502' +const testChallengeResources = [ + { + 'id': '9a06daeb-1b8e-4d91-9bd4-c5fda7c93db2', + 'challengeId': '9131c5da-6ed9-4186-9a1b-4de31df5ba17', + 'memberId': '88774396', + 'memberHandle': 'Sharathkumar92', + 'roleId': 'cfe12b3f-2a24-4639-9d8b-ec86726f76bd', + 'created': '2021-02-02T22:51:59.000Z', + 'createdBy': 'jmgasper' + }, + { + 'id': '9a06daeb-1b8e-4d91-9bd4-c5fda7c93db2', + 'challengeId': '9131c5da-6ed9-4186-9a1b-4de31df5ba17', + 'memberId': '88774396', + 'memberHandle': 'Sharathkumar92', + 'roleId': 'cfe12b3f-2a24-4639-9d8b-ec86726f76bb', + 'created': '2021-02-02T22:51:59.000Z', + 'createdBy': 'jmgasper' + } +] + +const testResourceRoles = [ + { + 'id': 'cfe12b3f-2a24-4639-9d8b-ec86726f76bd', + 'name': 'Copilot', + 'legacyId': 14, + 'fullReadAccess': true, + 'fullWriteAccess': true, + 'isActive': true, + 'selfObtainable': false + }, + { + 'id': 'cfe12b3f-2a24-4639-9d8b-ec86726f76bb', + 'name': 'Submitter', + 'legacyId': 14, + 'fullReadAccess': true, + 'fullWriteAccess': true, + 'isActive': true, + 'selfObtainable': false + } +] + const testSubmission = { Item: { challengeId: 'c3564180-65aa-42ec-a945-5fd21dec0502', id: 'a12a4180-65aa-42ec-a945-5fd21dec0501', type: 'ContestSubmission', url: 'https://software.topcoder.com/review/actions/DownloadContestSubmission?uid=123456', - memberId: 'b24d4180-65aa-42ec-a945-5fd21dec0501', + memberId: 40493050, legacySubmissionId: 'b24d4180-65aa-42ec-a945-5fd21dec0501', submissionPhaseId: 764567, created: '2018-05-20T07:00:30.123Z', createdBy: 'topcoder user', updated: '2018-06-01T07:36:28.178Z', - updatedBy: 'topcoder user' + updatedBy: 'topcoder user', + review: [], + reviewSummation: [] } } @@ -385,6 +429,7 @@ const testReview = { id: 'd24d4180-65aa-42ec-a945-5fd21dec0502', score: 92, reviewerId: 'c23a4180-65aa-42ec-a945-5fd21dec0503', + reviewedDate: '2021-02-02T11:39:38.685Z', submissionId: 'a12a4180-65aa-42ec-a945-5fd21dec0501', scoreCardId: 123456789, status: 'queued', @@ -401,6 +446,7 @@ const testReviewPatch = { id: 'd24d4180-65aa-42ec-a945-5fd21dec0502', score: 90, reviewerId: 'c23a4180-65aa-42ec-a945-5fd21dec0503', + reviewedDate: '2021-02-02T11:39:38.685Z', submissionId: 'a12a4180-65aa-42ec-a945-5fd21dec0501', scoreCardId: 123456789, status: 'queued', @@ -535,6 +581,7 @@ const testReviewSummation = { aggregateScore: 99, isPassing: true, submissionId: 'a12a4180-65aa-42ec-a945-5fd21dec0501', + reviewedDate: '2021-02-02T11:39:38.685Z', scoreCardId: 123456789, created: '2018-05-20T07:00:30.123Z', updated: '2018-06-01T07:36:28.178Z', @@ -549,6 +596,7 @@ const testReviewSummationPatch = { aggregateScore: 78.5, isPassing: false, submissionId: 'a12a4180-65aa-42ec-a945-5fd21dec0501', + reviewedDate: '2021-02-02T11:39:38.685Z', scoreCardId: 123456789, created: '2018-05-20T07:00:30.123Z', updated: '2018-06-01T07:36:28.178Z', @@ -664,98 +712,180 @@ const testReviewSummationsES = { } const testChallengeAPIResponse = { - id: '24a97f2f:1655fef5034:-7568', - result: { - success: true, - status: 200, - metadata: { - fields: null, - totalCount: 5 + 'id': '77eb9522-ea41-4334-974d-7604097d23e7', + 'created': '2020-11-02T21:34:19Z', + 'createdBy': 'tcwebservice', + 'updated': '2020-12-28T06:44:27Z', + 'updatedBy': 'AutoPilot', + 'status': 'Active', + 'projectId': 16661, + 'name': 'TCO Leaderboard Test 3', + 'typeId': '927abff4-7af9-4145-8ba1-577c16e64e2e', + 'trackId': '9b6fc876-f4d9-4ccb-9dfd-419247628825', + 'startDate': '2020-12-21T18:24:09Z', + 'legacy': { + 'reviewType': 'COMMUNITY', + 'isTask': false, + 'subTrack': 'CODE', + 'directProjectId': 23741, + 'track': 'DEVELOP', + 'reviewScorecardId': 30001610, + 'forumId': 0 + }, + 'descriptionFormat': 'HTML', + 'timelineTemplateId': '7ebf1c69-f62f-4d3a-bdfb-fe9ddb56861c', + 'terms': [ + { + 'roleId': '732339e7-8e30-49d7-9198-cccf9451e221', + 'id': 'b11da5cd-713f-478d-90f4-f679ef53ee95' + }, + { + 'roleId': '3eedd4a4-3c68-4f68-8de4-a1ca5c2055e5', + 'id': '82a35602-57c2-4b48-a9b9-b4e133b22035' + }, + { + 'roleId': '318b9c07-079a-42d9-a81f-b96be1dc1099', + 'id': '82a35602-57c2-4b48-a9b9-b4e133b22035' }, - content: [ - { - challengeId: 30049360, - id: 733195, - phaseType: 'Registration', - phaseStatus: 'Open', - scheduledStartTime: '1438002000000', - scheduledEndTime: '2019-12-02T09:00:00Z', - actualStartTime: '1438002000000', - actualEndTime: null, - fixedStartTime: '1438002000000', - duration: 137293200000, - updatedAt: '2018-07-30T08:38Z', - createdAt: '2015-07-27T09:19Z', - createdBy: '11823846', - updatedBy: '8547899' - }, - { - challengeId: 30049360, - id: 733196, - phaseType: 'Submission', - phaseStatus: 'Open', - scheduledStartTime: '1438002300000', - scheduledEndTime: '2019-12-02T09:00:00Z', - actualStartTime: null, - actualEndTime: null, - fixedStartTime: null, - duration: 137292900000, - updatedAt: '2018-07-30T08:38Z', - createdAt: '2015-07-27T09:19Z', - createdBy: '11823846', - updatedBy: '8547899' - }, - { - challengeId: 30049360, - id: 733197, - phaseType: 'Review', - phaseStatus: 'Scheduled', - scheduledStartTime: '1575295200000', - scheduledEndTime: '2019-12-04T09:00:00Z', - actualStartTime: null, - actualEndTime: null, - fixedStartTime: null, - duration: 172800000, - updatedAt: '2018-07-30T08:38Z', - createdAt: '2015-07-27T09:19Z', - createdBy: '11823846', - updatedBy: '8547899' - }, - { - challengeId: 30049360, - id: 733198, - phaseType: 'Appeals', - phaseStatus: 'Scheduled', - scheduledStartTime: '1575468000000', - scheduledEndTime: '2019-12-05T09:00:00Z', - actualStartTime: null, - actualEndTime: null, - fixedStartTime: null, - duration: 86400000, - updatedAt: '2018-07-30T08:38Z', - createdAt: '2015-07-27T09:19Z', - createdBy: '11823846', - updatedBy: '8547899' - }, - { - challengeId: 30049360, - id: 733199, - phaseType: 'Appeals Response', - phaseStatus: 'Scheduled', - scheduledStartTime: '1575554400000', - scheduledEndTime: '2019-12-05T09:00:00Z', - actualStartTime: null, - actualEndTime: null, - fixedStartTime: null, - duration: 43200000, - updatedAt: '2018-07-30T08:38Z', - createdAt: '2015-07-27T09:19Z', - createdBy: '11823846', - updatedBy: '8547899' - } - ] + { + 'roleId': 'ff556573-5da6-4392-b38c-08c1d7599c4a', + 'id': '82a35602-57c2-4b48-a9b9-b4e133b22035' + }, + { + 'roleId': 'e0544b94-6420-4afc-8f63-238eddc751b9', + 'id': '82a35602-57c2-4b48-a9b9-b4e133b22035' + }, + { + 'roleId': '0e9c6879-39e4-4eb6-b8df-92407890faf1', + 'id': '75d2f6bb-aadc-475e-9728-32c1dbd13655' + }, + { + 'roleId': 'cfe12b3f-2a24-4639-9d8b-ec86726f76bd', + 'id': 'e0993b1a-abf7-45e6-8ed9-8cd0546be90b' + }, + { + 'roleId': 'd663fc84-5c37-43d1-a537-793feffb7667', + 'id': '82a35602-57c2-4b48-a9b9-b4e133b22035' + } + ], + 'phases': [ + { + 'duration': 561600, + 'scheduledEndDate': '2020-12-28T06:44:27Z', + 'actualEndDate': '2020-12-28T06:44:27Z', + 'isOpen': false, + 'name': 'Registration', + 'phaseId': 'a93544bc-c165-4af4-b55e-18f3593b457a', + 'actualStartDate': '2020-12-21T18:24:09Z', + 'id': 'f6166029-cdef-4b72-b7a4-f2d3074bafac', + 'scheduledStartDate': '2020-12-21T18:24:09Z' + }, + { + 'duration': 561300, + 'scheduledEndDate': '2020-12-28T06:44:28Z', + 'actualEndDate': '2020-12-28T06:44:28Z', + 'isOpen': true, + 'name': 'Submission', + 'phaseId': '6950164f-3c5e-4bdc-abc8-22aaf5a1bd49', + 'actualStartDate': '2020-12-21T18:44:58Z', + 'id': '90ddb27a-cc49-454c-8367-354011eeba73', + 'scheduledStartDate': '2020-12-21T18:44:58Z' + }, + { + 'duration': 172800, + 'scheduledEndDate': '2020-12-30T06:44:00Z', + 'actualEndDate': '2020-12-28T06:51:27Z', + 'isOpen': false, + 'name': 'Review', + 'phaseId': 'aa5a3f78-79e0-4bf7-93ff-b11e8f5b398b', + 'actualStartDate': '2020-12-28T06:51:27Z', + 'id': '35a75a3f-c9ba-46ad-8003-f605c9bb4791', + 'scheduledStartDate': '2020-12-28T06:44:28Z' + }, + { + 'duration': 86400, + 'scheduledEndDate': '2020-12-31T06:44:00Z', + 'actualEndDate': '2020-12-28T06:51:27Z', + 'isOpen': false, + 'name': 'Appeals', + 'phaseId': '1c24cfb3-5b0a-4dbd-b6bd-4b0dff5349c6', + 'actualStartDate': '2020-12-28T06:51:27Z', + 'id': '3d16078a-2362-41fe-af82-01112b8f27c8', + 'scheduledStartDate': '2020-12-30T06:44:00Z' + }, + { + 'duration': 43200, + 'scheduledEndDate': '2020-12-31T18:44:00Z', + 'actualEndDate': '2020-12-28T06:51:27Z', + 'isOpen': false, + 'name': 'Appeals Response', + 'phaseId': '797a6af7-cd3f-4436-9fca-9679f773bee9', + 'actualStartDate': '2020-12-28T06:51:27Z', + 'id': '2359d4fd-aa1a-4403-98c5-a1f841b8062e', + 'scheduledStartDate': '2020-12-31T06:44:00Z' + }, + { + 'duration': 86400, + 'scheduledEndDate': '2020-12-29T06:48:00Z', + 'actualEndDate': '2020-12-28T06:51:27Z', + 'isOpen': true, + 'name': 'Post-Mortem', + 'phaseId': 'f308bdb4-d3da-43d8-942b-134dfbaf5c45', + 'actualStartDate': '2020-12-28T06:48:44Z', + 'id': '3a579100-e334-4b8f-ac89-7c8c696d42f0', + 'scheduledStartDate': '2020-12-28T06:48:44Z' + } + ], + 'discussions': [ + { + 'provider': 'vanilla', + 'name': 'TCO Leaderboard Test 3 Discussion', + 'id': 'cfbb21e8-a67a-4a23-997c-04022894d958', + 'type': 'challenge', + 'url': 'https://vanilla.topcoder-dev.com/categories/77eb9522-ea41-4334-974d-7604097d23e7' + } + ], + 'description': 'test', + 'groups': [], + 'endDate': '2020-12-29T06:48:00Z', + 'numOfSubmissions': 0, + 'numOfRegistrants': 0, + 'currentPhaseNames': [ + 'Post-Mortem' + ], + 'registrationStartDate': '2020-12-21T18:24:09Z', + 'registrationEndDate': '2020-12-28T06:44:27Z', + 'submissionStartDate': '2020-12-21T18:44:58Z', + 'submissionEndDate': '2020-12-28T06:44:28Z', + 'track': 'Development', + 'type': 'Challenge', + 'attachments': [], + 'prizeSets': [ + { + 'prizes': [ + { + 'type': 'USD', + 'value': 1 + } + ], + 'description': 'Challenge Prizes', + 'type': 'placement' + } + ], + 'tags': [ + 'Automated Testing' + ], + 'legacyId': 30057477, + 'metadata': [], + 'events': [], + 'task': { + 'isAssigned': false, + 'isTask': false, + 'memberId': null }, - version: 'v4' + 'overview': { + 'totalPrizes': 1 + } } module.exports = { @@ -780,5 +910,7 @@ module.exports = { testReviewSummationPatch, testReviewSummationES, testReviewSummationsES, - testChallengeAPIResponse + testChallengeAPIResponse, + testResourceRoles, + testChallengeResources } diff --git a/test/unit/ReviewService.test.js b/test/unit/ReviewService.test.js index 62181110..b7e3d71a 100644 --- a/test/unit/ReviewService.test.js +++ b/test/unit/ReviewService.test.js @@ -75,8 +75,7 @@ describe('Review Service tests', () => { .get(`${config.API_VERSION}/reviews/${testReview.Item.id}`) .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) .end((err, res) => { - res.should.have.status(200) - res.body.should.have.all.keys(Object.keys(testReview.Item)) + res.body.should.have.all.keys(Object.keys(_.omit(testReview.Item, ['reviewedDate']))) res.body.id.should.be.eql(testReview.Item.id) res.body.score.should.be.eql(testReview.Item.score) res.body.reviewerId.should.be.eql(testReview.Item.reviewerId) @@ -99,7 +98,7 @@ describe('Review Service tests', () => { .send({}) .end((err, res) => { res.should.have.status(400) - res.body.message.should.be.eql('"score" is required') + res.body.message.should.be.eql('"typeId" is required') done() }) }) diff --git a/test/unit/ReviewSummationService.test.js b/test/unit/ReviewSummationService.test.js index af8f0213..ee9a7311 100644 --- a/test/unit/ReviewSummationService.test.js +++ b/test/unit/ReviewSummationService.test.js @@ -76,7 +76,7 @@ describe('Review Summation Service tests', () => { .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) .end((err, res) => { res.should.have.status(200) - res.body.should.have.all.keys(Object.keys(testReviewSummation.Item)) + res.body.should.have.all.keys(Object.keys(_.omit(testReviewSummation.Item, ['reviewedDate']))) res.body.id.should.be.eql(testReviewSummation.Item.id) res.body.aggregateScore.should.be.eql(testReviewSummation.Item.aggregateScore) res.body.submissionId.should.be.eql(testReviewSummation.Item.submissionId) diff --git a/test/unit/ReviewTypeService.test.js b/test/unit/ReviewTypeService.test.js index 8777aa65..922f519c 100644 --- a/test/unit/ReviewTypeService.test.js +++ b/test/unit/ReviewTypeService.test.js @@ -429,13 +429,12 @@ describe('ReviewType Service tests', () => { }) }) - it('Getting review types with user token should throw 403', (done) => { + it('Getting review types with user token should return 200', (done) => { chai.request(app) .get(`${config.API_VERSION}/reviewTypes`) .set('Authorization', `Bearer ${config.USER_TOKEN}`) .end((err, res) => { - res.should.have.status(403) - res.body.message.should.be.eql('You are not allowed to perform this action!') + res.should.have.status(200) done() }) }) diff --git a/test/unit/SubmissionService.test.js b/test/unit/SubmissionService.test.js index 9e92daaf..652e0cba 100644 --- a/test/unit/SubmissionService.test.js +++ b/test/unit/SubmissionService.test.js @@ -14,7 +14,7 @@ const chaiHttp = require('chai-http') const should = chai.should() // eslint-disable-line const app = require('../../app') const { - nonExSubmissionId, testSubmission, testSubmissionWoLegacy, + nonExSubmissionId, testSubmission, testSubmissionPatch } = require('../common/testData') @@ -65,7 +65,7 @@ describe('Submission Service tests', () => { .set('Authorization', `Bearer ${config.USER_TOKEN}`) .end((err, res) => { res.should.have.status(200) - res.body.should.have.keys(Object.keys(testSubmission.Item)) + res.body.should.have.keys(Object.keys(_.omit(testSubmission.Item, ['submittedDate']))) res.body.id.should.be.eql(testSubmission.Item.id) res.body.challengeId.should.be.eql(testSubmission.Item.challengeId) res.body.type.should.be.eql(testSubmission.Item.type) @@ -80,7 +80,7 @@ describe('Submission Service tests', () => { .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) .end((err, res) => { res.should.have.status(200) - res.body.should.have.all.keys(Object.keys(testSubmission.Item)) + res.body.should.have.all.keys(Object.keys(_.omit(testSubmission.Item, ['submittedDate']))) res.body.id.should.be.eql(testSubmission.Item.id) res.body.challengeId.should.be.eql(testSubmission.Item.challengeId) res.body.type.should.be.eql(testSubmission.Item.type) @@ -135,7 +135,7 @@ describe('Submission Service tests', () => { chai.request(app) .post(`${config.API_VERSION}/submissions`) .set('Authorization', `Bearer ${config.USER_TOKEN}`) - .send(_.omit(testSubmission.Item, ['id', 'url', 'created', 'updated', 'createdBy', 'updatedBy'])) + .send(_.omit(testSubmission.Item, ['id', 'url', 'created', 'updated', 'createdBy', 'updatedBy', 'review', 'reviewSummation'])) .end((err, res) => { res.should.have.status(400) res.body.message.should.be.eql('Either file to be uploaded or URL should be present') @@ -163,9 +163,9 @@ describe('Submission Service tests', () => { chai.request(app) .post(`${config.API_VERSION}/submissions`) .set('Authorization', `Bearer ${config.USER_TOKEN}`) - .field('challengeId', testSubmissionWoLegacy.Item.challengeId) - .field('type', testSubmissionWoLegacy.Item.type) - .field('memberId', testSubmissionWoLegacy.Item.memberId) + .field('challengeId', testSubmission.Item.challengeId) + .field('type', testSubmission.Item.type) + .field('memberId', testSubmission.Item.memberId) .field('fileType', 'pdf') .attach('submission', './test/common/fileToUpload.zip', 'fileToUpload.zip') .end((err, res) => { @@ -179,21 +179,21 @@ describe('Submission Service tests', () => { chai.request(app) .post(`${config.API_VERSION}/submissions`) .set('Authorization', `Bearer ${config.USER_TOKEN}`) - .field('challengeId', testSubmissionWoLegacy.Item.challengeId) - .field('type', testSubmissionWoLegacy.Item.type) - .field('memberId', testSubmissionWoLegacy.Item.memberId) + .field('challengeId', testSubmission.Item.challengeId) + .field('type', testSubmission.Item.type) + .field('memberId', testSubmission.Item.memberId) .field('fileType', 'zip') .attach('submission', './test/common/fileToUpload.zip', 'fileToUpload.zip') .end((err, res) => { res.should.have.status(200) - res.body.should.have.keys(Object.keys(_.extend({ fileType: 'zip', submissionPhaseId: 733196 }, testSubmissionWoLegacy.Item))) + res.body.should.have.keys(Object.keys(_.extend({ fileType: 'zip', submissionPhaseId: 733196, submittedDate: '2018-05-20T07:00:30.123Z' }, _.omit(testSubmission.Item, ['legacySubmissionId', 'review', 'reviewSummation'])))) res.body.id.should.not.be.eql(null) - res.body.challengeId.should.be.eql(testSubmissionWoLegacy.Item.challengeId) - res.body.type.should.be.eql(testSubmissionWoLegacy.Item.type) + res.body.challengeId.should.be.eql(testSubmission.Item.challengeId) + res.body.type.should.be.eql(testSubmission.Item.type) res.body.url.should.not.be.eql(null) - res.body.memberId.should.be.eql(testSubmissionWoLegacy.Item.memberId) + res.body.memberId.should.be.eql(testSubmission.Item.memberId) res.body.fileType.should.be.eql('zip') - res.body.submissionPhaseId.should.be.eql(733196) + res.body.submissionPhaseId.should.be.eql('90ddb27a-cc49-454c-8367-354011eeba73') done() }) }) @@ -216,10 +216,10 @@ describe('Submission Service tests', () => { chai.request(app) .post(`${config.API_VERSION}/submissions`) .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) - .send(_.omit(testSubmission.Item, ['id', 'created', 'updated', 'createdBy', 'updatedBy'])) + .send(_.omit(testSubmission.Item, ['id', 'created', 'updated', 'createdBy', 'updatedBy', 'review', 'reviewSummation'])) .end((err, res) => { res.should.have.status(200) - res.body.should.have.keys(Object.keys(_.extend({ fileType: 'zip' }, testSubmission.Item))) + res.body.should.have.keys(Object.keys(_.extend({ fileType: 'zip', submissionPhaseId: 733196, submittedDate: '2018-05-20T07:00:30.123Z' }, _.omit(testSubmission.Item, ['review', 'reviewSummation'])))) res.body.id.should.not.be.eql(null) res.body.challengeId.should.be.eql(testSubmission.Item.challengeId) res.body.type.should.be.eql(testSubmission.Item.type) @@ -233,10 +233,10 @@ describe('Submission Service tests', () => { chai.request(app) .post(`${config.API_VERSION}/submissions`) .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) - .send(_.omit(testSubmission.Item, ['id', 'created', 'updated', 'createdBy', 'updatedBy', 'submissionPhaseId'])) + .send(_.omit(testSubmission.Item, ['id', 'created', 'updated', 'createdBy', 'updatedBy', 'submissionPhaseId', 'review', 'reviewSummation'])) .end((err, res) => { res.should.have.status(200) - res.body.should.have.keys(Object.keys(_.extend({ fileType: 'zip' }, testSubmission.Item))) + res.body.should.have.keys(Object.keys(_.extend({ fileType: 'zip', submissionPhaseId: 733196, submittedDate: '2018-05-20T07:00:30.123Z' }, _.omit(testSubmission.Item, ['review', 'reviewSummation'])))) res.body.id.should.not.be.eql(null) res.body.challengeId.should.be.eql(testSubmission.Item.challengeId) res.body.type.should.be.eql(testSubmission.Item.type) @@ -316,7 +316,7 @@ describe('Submission Service tests', () => { chai.request(app) .put(`${config.API_VERSION}/submissions/${nonExSubmissionId}`) .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) - .send(_.omit(testSubmission.Item, ['id', 'created', 'updated', 'createdBy', 'updatedBy'])) + .send(_.omit(testSubmission.Item, ['id', 'created', 'updated', 'createdBy', 'updatedBy', 'review', 'reviewSummation'])) .end((err, res) => { res.should.have.status(404) res.body.message.should.be.eql(`Submission with ID = ${nonExSubmissionId} is not found`) @@ -328,10 +328,10 @@ describe('Submission Service tests', () => { chai.request(app) .put(`${config.API_VERSION}/submissions/${testSubmission.Item.id}`) .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) - .send(_.omit(testSubmission.Item, ['id', 'created', 'updated', 'createdBy', 'updatedBy'])) + .send(_.omit(testSubmission.Item, ['id', 'created', 'updated', 'createdBy', 'updatedBy', 'review', 'reviewSummation'])) .end((err, res) => { res.should.have.status(200) - res.body.should.have.keys(Object.keys(testSubmission.Item)) + res.body.should.have.keys(Object.keys(_.extend({ submissionPhaseId: 733196 }, _.omit(testSubmission.Item, ['review', 'reviewSummation', 'submittedDate'])))) res.body.id.should.not.be.eql(null) res.body.challengeId.should.be.eql(testSubmission.Item.challengeId) res.body.type.should.be.eql(testSubmission.Item.type) @@ -342,16 +342,16 @@ describe('Submission Service tests', () => { it('Updating submission without legacy fields with Admin token should get succeeded', (done) => { chai.request(app) - .put(`${config.API_VERSION}/submissions/${testSubmissionWoLegacy.Item.id}`) + .put(`${config.API_VERSION}/submissions/${testSubmission.Item.id}`) .set('Authorization', `Bearer ${config.ADMIN_TOKEN}`) - .send(_.omit(testSubmissionWoLegacy.Item, ['id', 'created', 'updated', 'createdBy', 'updatedBy'])) + .send(_.omit(testSubmission.Item, ['id', 'created', 'updated', 'createdBy', 'updatedBy', 'review', 'reviewSummation'])) .end((err, res) => { res.should.have.status(200) - res.body.should.have.keys(Object.keys(testSubmissionWoLegacy.Item)) + res.body.should.have.keys(Object.keys(_.extend({ submissionPhaseId: 733196 }, _.omit(testSubmission.Item, ['review', 'reviewSummation', 'submittedDate'])))) res.body.id.should.not.be.eql(null) - res.body.challengeId.should.be.eql(testSubmissionWoLegacy.Item.challengeId) - res.body.type.should.be.eql(testSubmissionWoLegacy.Item.type) - res.body.url.should.be.eql(testSubmissionWoLegacy.Item.url) + res.body.challengeId.should.be.eql(testSubmission.Item.challengeId) + res.body.type.should.be.eql(testSubmission.Item.type) + res.body.url.should.be.eql(testSubmission.Item.url) done() }) }).timeout(10000) diff --git a/test/unit/prepare.js b/test/unit/prepare.js index 824420db..9a476250 100644 --- a/test/unit/prepare.js +++ b/test/unit/prepare.js @@ -34,6 +34,16 @@ prepare(function (done) { } }) + AWS.mock('DynamoDB.DocumentClient', 'query', (params, callback) => { + if (params.ExpressionAttributeValues[':p_submissionId'] === testData.nonExSubmissionId) { + callback(null, { + Count: 0 + }) + } else if (params.ExpressionAttributeValues[':p_submissionId'] === testData.testSubmission.Item.id) { + callback(null, []) + } + }) + AWS.mock('DynamoDB.DocumentClient', 'put', (params, callback) => { callback(null, {}) }) @@ -70,7 +80,9 @@ prepare(function (done) { // Mock Posting to Bus API and ES interactions const authUrl = URL.parse(config.AUTH0_URL) const busUrl = URL.parse(config.BUSAPI_EVENTS_URL) - const challengeApiUrl = URL.parse(`${config.CHALLENGEAPI_URL}/30049360/phases`) + const challengeApiUrl = URL.parse(`${config.CHALLENGEAPI_V5_URL}/c3564180-65aa-42ec-a945-5fd21dec0502`) + const resourcesApi = URL.parse(`${config.BUSAPI_URL}/resources?challengeId=c3564180-65aa-42ec-a945-5fd21dec0502`) + const resourceRolesApi = URL.parse(`${config.BUSAPI_URL}/resource-roles`) nock(/.com/) .persist() @@ -91,11 +103,15 @@ prepare(function (done) { return body }) .post(authUrl.path) - .reply(200, { access_token: 'test' }) + .reply(200, { access_token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci5jb20nIiwiaWF0IjoxNjEyMjg0NDIyLCJleHAiOjE2MTIyODgyODEsInVzZXJJZCI6IjQwNDMzMjg4IiwiZW1haWwiOiJhZG1pbkB0b3Bjb2Rlci5jb20iLCJqdGkiOiJjM2FjNjA4YS01NmJlLTQ1ZDAtOGY2YS0zMWZlOTRiOTU2MWMiLCJyb2xlcyI6IltcIkFkbWluaXN0cmF0b3JcIl0iLCJoYW5kbGUiOiJUb255SiJ9.7MLAeTtAxS-RvQWA2fEoS2va7mOLd_n-COnDWzLVQ_s' }) .post(busUrl.path) .reply(204) .get(challengeApiUrl.path) .reply(200, testData.testChallengeAPIResponse) + .get(resourcesApi.path) + .reply(200, testData.testChallengeResources) + .get(resourceRolesApi.path) + .reply(200, testData.testResourceRoles) .post(`/${config.esConfig.ES_INDEX}/${config.esConfig.ES_TYPE}/_search`, 'reviewType') .query(true) .reply(200, testData.testReviewTypesES) From 294df4547b9ff130d425c6d1132082e796c698de Mon Sep 17 00:00:00 2001 From: Sachin Maheshwari Date: Fri, 5 Feb 2021 22:33:52 +0530 Subject: [PATCH 04/12] reverted to fetch legacy-id code --- src/common/helper.js | 24 ++++++++++++++++++++++++ src/services/SubmissionService.js | 6 ++++++ 2 files changed, 30 insertions(+) diff --git a/src/common/helper.js b/src/common/helper.js index 5289ea67..4ae7ba77 100755 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -299,6 +299,30 @@ function * getM2Mtoken () { return yield m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) } +/** + * Get legacy challenge id if the challenge id is uuid form + * @param {String} challengeId Challenge ID + * @returns {String} Legacy Challenge ID of the given challengeId + */ +function * getLegacyChallengeId (challengeId) { + if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(challengeId)) { + logger.debug(`${challengeId} detected as uuid. Fetching legacy challenge id`) + const token = yield getM2Mtoken() + try { + const response = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${challengeId}`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + const legacyId = parseInt(response.body.legacyId, 10) + logger.debug(`Legacy challenge id is ${legacyId} for v5 challenge id ${challengeId}`) + return legacyId + } catch (err) { + logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${challengeId}`) + throw err + } + } + return challengeId +} + /* * Get submission phase ID of a challenge from Challenge API * @param challengeId Challenge ID diff --git a/src/services/SubmissionService.js b/src/services/SubmissionService.js index 20c323dc..3a3c9602 100755 --- a/src/services/SubmissionService.js +++ b/src/services/SubmissionService.js @@ -181,6 +181,12 @@ function * downloadSubmission (authUser, submissionId) { * @return {Object} Data fetched from ES */ function * listSubmissions (authUser, query) { + if (query.challengeId) { + // Submission api only works with legacy challenge id + // If it is a v5 challenge id, get the associated legacy challenge id + query.challengeId = yield helper.getLegacyChallengeId(query.challengeId) + } + const data = yield helper.fetchFromES(query, helper.camelize(table)) logger.info(`listSubmissions: returning ${data.length} submissions for query: ${JSON.stringify(query)}`) From a6ee3c0dec4f2be990b0fc13fb66b37f56f5e7cd Mon Sep 17 00:00:00 2001 From: Sachin Maheshwari Date: Fri, 5 Feb 2021 22:36:11 +0530 Subject: [PATCH 05/12] deploying feature/shapeup-pure-v5-task branch for testing --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 47bba1e3..e8eb4a18 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -67,7 +67,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'develop-temp'] + only: ['develop', 'feature/shapeup-pure-v5-task'] - "build-prod": context : org-global filters: From 03c56a6f0afaaa1628c0e26828d1d6f72062822a Mon Sep 17 00:00:00 2001 From: Sachin Maheshwari Date: Fri, 5 Feb 2021 22:42:33 +0530 Subject: [PATCH 06/12] typo --- src/common/helper.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/helper.js b/src/common/helper.js index 4ae7ba77..0c013a45 100755 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -735,6 +735,7 @@ module.exports = { fetchFromES, camelize, setPaginationHeaders, + getLegacyChallengeId, getSubmissionPhaseId, checkCreateAccess, checkGetAccess, From 5c83c09e7db4a475c633ddd3c8b817e9637edcb1 Mon Sep 17 00:00:00 2001 From: Sachin Maheshwari Date: Fri, 5 Feb 2021 23:22:03 +0530 Subject: [PATCH 07/12] reverting schema validation too --- src/services/SubmissionService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/SubmissionService.js b/src/services/SubmissionService.js index 3a3c9602..0de1118b 100755 --- a/src/services/SubmissionService.js +++ b/src/services/SubmissionService.js @@ -203,7 +203,7 @@ const listSubmissionsQuerySchema = { type: joi.string(), url: joi.string().uri().trim(), memberId: joi.alternatives().try(joi.id(), joi.string().uuid()), - challengeId: joi.string().uuid(), + challengeId: joi.alternatives().try(joi.id(), joi.string().uuid()), legacySubmissionId: joi.alternatives().try(joi.id(), joi.string().uuid()), legacyUploadId: joi.alternatives().try(joi.id(), joi.string().uuid()), submissionPhaseId: joi.id(), From fc9184dc711cf7d6e0ed883f93569cfac6d2d6c0 Mon Sep 17 00:00:00 2001 From: Sachin Maheshwari Date: Sun, 7 Feb 2021 21:45:36 +0530 Subject: [PATCH 08/12] getting v5 challenge id from legeacy id --- src/common/helper.js | 103 +++++++++++++++++++----------- src/services/SubmissionService.js | 5 +- 2 files changed, 70 insertions(+), 38 deletions(-) diff --git a/src/common/helper.js b/src/common/helper.js index 0c013a45..4531fe63 100755 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -31,8 +31,8 @@ let busApiClient * @param {Function} fn the generator function * @returns {Function} the wrapped function */ -function wrapExpress (fn) { - return function wrap (req, res, next) { +function wrapExpress(fn) { + return function wrap(req, res, next) { co(fn(req, res, next)).catch(next) } } @@ -42,7 +42,7 @@ function wrapExpress (fn) { * @param obj the object (controller exports) * @returns {Object|Array} the wrapped object */ -function autoWrapExpress (obj) { +function autoWrapExpress(obj) { if (_.isArray(obj)) { return obj.map(autoWrapExpress) } @@ -62,7 +62,7 @@ function autoWrapExpress (obj) { * Get Bus API Client * @return {Object} Bus API Client Instance */ -function getBusApiClient () { +function getBusApiClient() { // If there is no Client instance, Create a new instance if (!busApiClient) { logger.debug(`Creating Bus API client for ${config.BUSAPI_URL} `) @@ -80,7 +80,7 @@ function getBusApiClient () { * Get ES Client * @return {Object} Elastic Host Client Instance */ -function getEsClient () { +function getEsClient() { const esHost = config.get('esConfig.HOST') if (!esClients.client) { // AWS ES configuration is different from other providers @@ -109,7 +109,7 @@ function getEsClient () { * @param str Input string * @returns string String converted into camelCase */ -function camelize (str) { +function camelize(str) { return str.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function (match, index) { if (+match === 0) return '' // or if (/\s+/.test(match)) for white spaces return index === 0 ? match.toLowerCase() : match.toUpperCase() @@ -122,7 +122,7 @@ function camelize (str) { * @param {String} actResource Resource name in ES * @return {Object} search request body that can be passed to ES */ -function prepESFilter (query, actResource) { +function prepESFilter(query, actResource) { const pageSize = query.perPage || config.get('PAGE_SIZE') const page = query.page || 1 const { sortBy, orderBy } = query @@ -220,7 +220,7 @@ function prepESFilter (query, actResource) { * @param {String} resource Resource name in ES * @return {Object} Data fetched from ES based on the filters */ -function * fetchFromES (query, resource) { +function* fetchFromES(query, resource) { const esClient = getEsClient() // Construct ES filter const filter = prepESFilter(query, resource) @@ -244,7 +244,7 @@ function * fetchFromES (query, resource) { * @param res HTTP response * @param {Object} data Data for which pagination need to be applied */ -function setPaginationHeaders (req, res, data) { +function setPaginationHeaders(req, res, data) { const totalPages = Math.ceil(data.total / data.pageSize) let fullUrl = req.protocol + '://' + req.get('host') + req.url.replace(`&page=${data.page}`, '') // URL formatting to add pagination parameters accordingly @@ -295,7 +295,7 @@ function setPaginationHeaders (req, res, data) { /* Function to get M2M token * @returns {Promise} */ -function * getM2Mtoken () { +function* getM2Mtoken() { return yield m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) } @@ -304,7 +304,7 @@ function * getM2Mtoken () { * @param {String} challengeId Challenge ID * @returns {String} Legacy Challenge ID of the given challengeId */ -function * getLegacyChallengeId (challengeId) { +function* getLegacyChallengeId(challengeId) { if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(challengeId)) { logger.debug(`${challengeId} detected as uuid. Fetching legacy challenge id`) const token = yield getM2Mtoken() @@ -323,14 +323,39 @@ function * getLegacyChallengeId (challengeId) { return challengeId } +/** + * Get v5 challenge id (uuid) if legacy challenge id + * @param {Integer} challengeId Challenge ID + * @returns {String} v5 uuid Challenge ID of the given challengeId + */ +function* getV5ChallengeId(challengeId) { + if (!(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(challengeId))) { + logger.debug(`${challengeId} detected as legacy challenge id. Fetching legacy challenge id`) + const token = yield getM2Mtoken() + try { + const response = yield request.get(`${config.CHALLENGEAPI_V5_URL}?legacyId=${challengeId}`) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + const v5Uuid = _.get(response, 'body.id') + logger.debug(`V5 challenge id is ${v5Uuid} for legacy challenge id ${challengeId}`) + return v5Uuid + } catch (err) { + logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}?legacyId=${challengeId}`) + throw err + } + } + return challengeId +} + /* * Get submission phase ID of a challenge from Challenge API * @param challengeId Challenge ID * @returns {Integer} Submission phase ID of the given challengeId */ -function * getSubmissionPhaseId (challengeId) { +function* getSubmissionPhaseId(challengeId) { let phaseId = null let response + challengeId = getV5ChallengeId(challengeId) try { logger.info(`Calling to challenge API to find submission phase Id for ${challengeId}`) @@ -366,10 +391,12 @@ function * getSubmissionPhaseId (challengeId) { * @param subEntity Submission Entity * @returns {Promise} */ -function * checkCreateAccess (authUser, subEntity) { +function* checkCreateAccess(authUser, subEntity) { let challengeDetails let resources + const challengeId = getV5ChallengeId(subEntity.challengeId) + // User can only create submission for themselves if (authUser.userId !== subEntity.memberId) { throw new errors.HttpStatusError(403, 'You are not allowed to submit on behalf of others') @@ -378,25 +405,25 @@ function * checkCreateAccess (authUser, subEntity) { const token = yield getM2Mtoken() try { - logger.info(`Calling to challenge API for fetch phases and winners for ${subEntity.challengeId}`) - challengeDetails = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${subEntity.challengeId}`) + logger.info(`Calling to challenge API for fetch phases and winners for ${challengeId}`) + challengeDetails = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${challengeId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - logger.info(`returned for ${subEntity.challengeId} with ${JSON.stringify(challengeDetails)}`) + logger.info(`returned for ${challengeId} with ${JSON.stringify(challengeDetails)}`) } catch (ex) { - logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${subEntity.challengeId}`) + logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${challengeId}`) logger.error(ex) - throw new errors.HttpStatusError(503, `Could not fetch details of challenge with id ${subEntity.challengeId}`) + throw new errors.HttpStatusError(503, `Could not fetch details of challenge with id ${challengeId}`) } try { - resources = yield request.get(`${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${subEntity.challengeId}`) + resources = yield request.get(`${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${challengeId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') } catch (ex) { - logger.error(`Error while accessing ${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${subEntity.challengeId}`) + logger.error(`Error while accessing ${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${challengeId}`) logger.error(ex) - throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${subEntity.challengeId}`) + throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${challengeId}`) } // Get map of role id to role name @@ -451,7 +478,7 @@ function * checkCreateAccess (authUser, subEntity) { * @param submission Submission Entity * @returns {Promise} */ -function * checkGetAccess (authUser, submission) { +function* checkGetAccess(authUser, submission) { let resources let challengeDetails // Allow downloading Own submission @@ -460,25 +487,26 @@ function * checkGetAccess (authUser, submission) { } const token = yield getM2Mtoken() + const challengeId = getV5ChallengeId(submission.challengeId) try { - resources = yield request.get(`${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${submission.challengeId}`) + resources = yield request.get(`${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${challengeId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') } catch (ex) { - logger.error(`Error while accessing ${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${submission.challengeId}`) + logger.error(`Error while accessing ${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${challengeId}`) logger.error(ex) - throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${submission.challengeId}`) + throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${challengeId}`) } try { - challengeDetails = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${submission.challengeId}`) + challengeDetails = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${challengeId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') } catch (ex) { - logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${submission.challengeId}`) + logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${challengeId}`) logger.error(ex) - throw new errors.HttpStatusError(503, `Could not fetch details of challenge with id ${submission.challengeId}`) + throw new errors.HttpStatusError(503, `Could not fetch details of challenge with id ${challengeId}`) } // Get map of role id to role name @@ -486,7 +514,7 @@ function * checkGetAccess (authUser, submission) { // Check if role id to role name mapping is available. If not user's role cannot be determined. if (resourceRolesMap == null || _.size(resourceRolesMap) === 0) { - throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${submission.challengeId}`) + throw new errors.HttpStatusError(503, `Could not determine the user's role in the challenge with id ${challengeId}`) } if (resources && challengeDetails) { @@ -567,16 +595,17 @@ function * checkGetAccess (authUser, submission) { * @param submission Submission Entity * @returns {Promise} */ -function * checkReviewGetAccess (authUser, submission) { +function* checkReviewGetAccess(authUser, submission) { let challengeDetails const token = yield getM2Mtoken() + const challengeId = getV5ChallengeId(submission.challengeId) try { - challengeDetails = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${submission.challengeId}`) + challengeDetails = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${challengeId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') } catch (ex) { - logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${submission.challengeId}`) + logger.error(`Error while accessing ${config.CHALLENGEAPI_V5_URL}/${challengeId}`) logger.error(ex) return false } @@ -606,7 +635,7 @@ function * checkReviewGetAccess (authUser, submission) { * @param{String} fileURL S3 URL of the file to be downloaded * @returns {Buffer} Buffer of downloaded file */ -function * downloadFile (fileURL) { +function* downloadFile(fileURL) { const { bucket, key } = AmazonS3URI(fileURL) logger.info(`downloadFile(): file is on S3 ${bucket} / ${key}`) const downloadedFile = yield s3.getObject({ Bucket: bucket, Key: key }).promise() @@ -619,7 +648,7 @@ function * downloadFile (fileURL) { * Also stores the original topic in the payload * @param {Object} payload Data that needs to be posted to the bus api */ -function * postToBusApi (payload) { +function* postToBusApi(payload) { const busApiClient = getBusApiClient() const originalTopic = payload.topic @@ -639,7 +668,7 @@ function * postToBusApi (payload) { * @param {Array} reviews The reviews to remove metadata from * @param {Object} authUser The authenticated user details */ -function cleanseReviews (reviews, authUser) { +function cleanseReviews(reviews, authUser) { // Not a machine user if (!authUser.scopes) { const admin = _.filter(authUser.roles, role => role.toLowerCase() === 'Administrator'.toLowerCase()) @@ -672,7 +701,7 @@ function cleanseReviews (reviews, authUser) { * Function to get role id to role name map * @returns {Object|null} map */ -function * getRoleIdToRoleNameMap () { +function* getRoleIdToRoleNameMap() { let resourceRoles let resourceRolesMap = null const token = yield getM2Mtoken() @@ -700,7 +729,7 @@ function * getRoleIdToRoleNameMap () { * @param {Object} challengeDetails the challenge details * @returns {('Scheduled' | 'Open' | 'Closed' | 'Invalid')} status of the phase */ -function getPhaseStatus (phaseName, challengeDetails) { +function getPhaseStatus(phaseName, challengeDetails) { const { phases } = challengeDetails const queriedPhaseIndex = _.findIndex(phases, phase => { return phase.name === phaseName diff --git a/src/services/SubmissionService.js b/src/services/SubmissionService.js index 0de1118b..c96c493f 100755 --- a/src/services/SubmissionService.js +++ b/src/services/SubmissionService.js @@ -268,6 +268,9 @@ function * createSubmission (authUser, files, entity) { throw new errors.HttpStatusError(400, 'The file should be uploaded under the "submission" attribute') } + // Submission api only works with legacy challenge id + // If it is a v5 challenge id, get the associated legacy challenge id + const challengeId = yield helper.getLegacyChallengeId(entity.challengeId) const currDate = (new Date()).toISOString() const item = { @@ -275,7 +278,7 @@ function * createSubmission (authUser, files, entity) { type: entity.type, url: url, memberId: entity.memberId, - challengeId: entity.challengeId, + challengeId: challengeId, created: currDate, updated: currDate, createdBy: authUser.handle || authUser.sub, From 8a1d6e4d160a08b1ef9be77e2344683f7504bf80 Mon Sep 17 00:00:00 2001 From: Mithun Kamath Date: Wed, 10 Feb 2021 12:15:30 +0530 Subject: [PATCH 09/12] #209 - npm script to store v5 challenge id for submissions --- README.md | 18 ++++++++ config/default.js | 2 + package.json | 1 + scripts/updateToV5ChallengeId.js | 78 ++++++++++++++++++++++++++++++++ src/common/helper.js | 47 +++++++++---------- 5 files changed, 123 insertions(+), 23 deletions(-) create mode 100644 scripts/updateToV5ChallengeId.js diff --git a/README.md b/README.md index e2dcdfcd..75723530 100755 --- a/README.md +++ b/README.md @@ -171,6 +171,24 @@ To migrate the existing data from DynamoDB to ES, run the following script npm run db-to-es ``` +#### Store v5 challenge id for current records + +Submission API started off using the legacy challenge ids. With the v5 upgrade to the challenge api, we now need to make use of the v5 challenge ids. We have thus created a script to update existing `challengeId` attribute on submissions to v5 and store the older challenge ids in the `legacyChallengeId` attribute. + +To update the existing challengeId data on submissions in DynamoDB to v5 challengeId, set the following env variables: + +```bash +SUBMISSION_TABLE_NAME // Table name of the submission records. Defaults to 'Submission' +UPDATE_V5_CHALLENGE_BATCH_SIZE // Number of records that are updated simultaneously. Defaults to 250 +``` + + +and then run the following script + +``` +npm run update-to-v5-challengeId +``` + #### Swagger UI Swagger UI will be served at `http://localhost:3000/docs` diff --git a/config/default.js b/config/default.js index 5ba2d291..cfef076f 100755 --- a/config/default.js +++ b/config/default.js @@ -37,5 +37,7 @@ module.exports = { PAGE_SIZE: process.env.PAGE_SIZE || 20, MAX_PAGE_SIZE: parseInt(process.env.MAX_PAGE_SIZE) || 100, ES_BATCH_SIZE: process.env.ES_BATCH_SIZE || 250, + UPDATE_V5_CHALLENGE_BATCH_SIZE: process.env.UPDATE_V5_CHALLENGE_BATCH_SIZE || 250, + SUBMISSION_TABLE_NAME: process.env.SUBMISSION_TABLE_NAME || 'Submission', AUTH0_PROXY_SERVER_URL: process.env.AUTH0_PROXY_SERVER_URL } diff --git a/package.json b/package.json index b09e8038..14ec3b52 100755 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "delete-index": "node scripts/deleteIndex.js", "init-es": "node scripts/loadES.js", "db-to-es": "node scripts/migrateFromDBToES.js", + "update-to-v5-challengeId": "node scripts/updateToV5ChallengeId.js", "test": "mocha test/unit/*.test.js --require test/unit/prepare.js --exit", "e2e": "mocha test/e2e/*.test.js --require test/e2e/prepare.js --exit", "cov": "nyc --reporter=html --reporter=text mocha test/unit/*.test.js --require test/unit/prepare.js --exit", diff --git a/scripts/updateToV5ChallengeId.js b/scripts/updateToV5ChallengeId.js new file mode 100644 index 00000000..b12ace12 --- /dev/null +++ b/scripts/updateToV5ChallengeId.js @@ -0,0 +1,78 @@ +/** + * Store v5 challenge id for current records + */ + +const _ = require('lodash') +const co = require('co') +const config = require('config') +const logger = require('../src/common/logger') +const dbhelper = require('../src/common/dbhelper') +const helper = require('../src/common/helper') + +/** + * Update Submission's challenge id to v5 + * @param {Object} submission The submission record + * @returns {Promise} + */ +function * updateRecord (submission) { + const v5challengeId = yield helper.getV5ChallengeId(submission.challengeId) + const record = { + TableName: 'Submission', + Key: { + id: submission.id + }, + UpdateExpression: `set challengeId = :c, legacyChallengeId = :l`, + ExpressionAttributeValues: { + ':c': v5challengeId, + ':l': submission.challengeId + } + } + if (!v5challengeId) { + logger.warn(`the challengeId: ${submission.challengeId} is not having a v5 challengeId`) + + return + } else if (v5challengeId === submission.challengeId) { + logger.info(`the challengeId: ${submission.challengeId} is already a v5 challengeId`) + } + + yield dbhelper.updateRecord(record) +} + +/* + * Update all submission's challenge id to v5 + * @returns {Promise} + */ +function * updateRecords () { + const tableName = config.SUBMISSION_TABLE_NAME + let promises = [] + const params = { + TableName: tableName + } + // Process until all the records from DB is fetched + while (true) { + const records = yield dbhelper.scanRecords(params) + const totalRecords = records.Items.length + logger.debug(`Number of ${tableName}s fetched from DB - ${totalRecords}. More fetch iterations may follow (pagination in progress)`) + for (let i = 0; i < totalRecords; i++) { + const record = records.Items[i] + promises.push(updateRecord(record)) + } + // Continue fetching the remaining records from Database + if (typeof records.LastEvaluatedKey !== 'undefined') { + params.ExclusiveStartKey = records.LastEvaluatedKey + } else { + break // If there are no more records to process, exit the loop + } + } + logger.debug(`All records fetched. Proceeding to update them in batches of ${config.UPDATE_V5_CHALLENGE_BATCH_SIZE}`) + const paraRecords = _.chunk(promises, config.UPDATE_V5_CHALLENGE_BATCH_SIZE) + for (const rs of paraRecords) { + yield rs + } +} + +co(function * () { + yield updateRecords() +}).catch((err) => { + logger.logFullError(err) +}) diff --git a/src/common/helper.js b/src/common/helper.js index 4531fe63..9e769bfc 100755 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -31,8 +31,8 @@ let busApiClient * @param {Function} fn the generator function * @returns {Function} the wrapped function */ -function wrapExpress(fn) { - return function wrap(req, res, next) { +function wrapExpress (fn) { + return function wrap (req, res, next) { co(fn(req, res, next)).catch(next) } } @@ -42,7 +42,7 @@ function wrapExpress(fn) { * @param obj the object (controller exports) * @returns {Object|Array} the wrapped object */ -function autoWrapExpress(obj) { +function autoWrapExpress (obj) { if (_.isArray(obj)) { return obj.map(autoWrapExpress) } @@ -62,7 +62,7 @@ function autoWrapExpress(obj) { * Get Bus API Client * @return {Object} Bus API Client Instance */ -function getBusApiClient() { +function getBusApiClient () { // If there is no Client instance, Create a new instance if (!busApiClient) { logger.debug(`Creating Bus API client for ${config.BUSAPI_URL} `) @@ -80,7 +80,7 @@ function getBusApiClient() { * Get ES Client * @return {Object} Elastic Host Client Instance */ -function getEsClient() { +function getEsClient () { const esHost = config.get('esConfig.HOST') if (!esClients.client) { // AWS ES configuration is different from other providers @@ -109,7 +109,7 @@ function getEsClient() { * @param str Input string * @returns string String converted into camelCase */ -function camelize(str) { +function camelize (str) { return str.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function (match, index) { if (+match === 0) return '' // or if (/\s+/.test(match)) for white spaces return index === 0 ? match.toLowerCase() : match.toUpperCase() @@ -122,7 +122,7 @@ function camelize(str) { * @param {String} actResource Resource name in ES * @return {Object} search request body that can be passed to ES */ -function prepESFilter(query, actResource) { +function prepESFilter (query, actResource) { const pageSize = query.perPage || config.get('PAGE_SIZE') const page = query.page || 1 const { sortBy, orderBy } = query @@ -220,7 +220,7 @@ function prepESFilter(query, actResource) { * @param {String} resource Resource name in ES * @return {Object} Data fetched from ES based on the filters */ -function* fetchFromES(query, resource) { +function * fetchFromES (query, resource) { const esClient = getEsClient() // Construct ES filter const filter = prepESFilter(query, resource) @@ -244,7 +244,7 @@ function* fetchFromES(query, resource) { * @param res HTTP response * @param {Object} data Data for which pagination need to be applied */ -function setPaginationHeaders(req, res, data) { +function setPaginationHeaders (req, res, data) { const totalPages = Math.ceil(data.total / data.pageSize) let fullUrl = req.protocol + '://' + req.get('host') + req.url.replace(`&page=${data.page}`, '') // URL formatting to add pagination parameters accordingly @@ -295,7 +295,7 @@ function setPaginationHeaders(req, res, data) { /* Function to get M2M token * @returns {Promise} */ -function* getM2Mtoken() { +function * getM2Mtoken () { return yield m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET) } @@ -304,7 +304,7 @@ function* getM2Mtoken() { * @param {String} challengeId Challenge ID * @returns {String} Legacy Challenge ID of the given challengeId */ -function* getLegacyChallengeId(challengeId) { +function * getLegacyChallengeId (challengeId) { if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(challengeId)) { logger.debug(`${challengeId} detected as uuid. Fetching legacy challenge id`) const token = yield getM2Mtoken() @@ -328,7 +328,7 @@ function* getLegacyChallengeId(challengeId) { * @param {Integer} challengeId Challenge ID * @returns {String} v5 uuid Challenge ID of the given challengeId */ -function* getV5ChallengeId(challengeId) { +function * getV5ChallengeId (challengeId) { if (!(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(challengeId))) { logger.debug(`${challengeId} detected as legacy challenge id. Fetching legacy challenge id`) const token = yield getM2Mtoken() @@ -336,7 +336,7 @@ function* getV5ChallengeId(challengeId) { const response = yield request.get(`${config.CHALLENGEAPI_V5_URL}?legacyId=${challengeId}`) .set('Authorization', `Bearer ${token}`) .set('Content-Type', 'application/json') - const v5Uuid = _.get(response, 'body.id') + const v5Uuid = _.get(response, 'body[0].id') logger.debug(`V5 challenge id is ${v5Uuid} for legacy challenge id ${challengeId}`) return v5Uuid } catch (err) { @@ -352,7 +352,7 @@ function* getV5ChallengeId(challengeId) { * @param challengeId Challenge ID * @returns {Integer} Submission phase ID of the given challengeId */ -function* getSubmissionPhaseId(challengeId) { +function * getSubmissionPhaseId (challengeId) { let phaseId = null let response challengeId = getV5ChallengeId(challengeId) @@ -391,7 +391,7 @@ function* getSubmissionPhaseId(challengeId) { * @param subEntity Submission Entity * @returns {Promise} */ -function* checkCreateAccess(authUser, subEntity) { +function * checkCreateAccess (authUser, subEntity) { let challengeDetails let resources @@ -478,7 +478,7 @@ function* checkCreateAccess(authUser, subEntity) { * @param submission Submission Entity * @returns {Promise} */ -function* checkGetAccess(authUser, submission) { +function * checkGetAccess (authUser, submission) { let resources let challengeDetails // Allow downloading Own submission @@ -595,7 +595,7 @@ function* checkGetAccess(authUser, submission) { * @param submission Submission Entity * @returns {Promise} */ -function* checkReviewGetAccess(authUser, submission) { +function * checkReviewGetAccess (authUser, submission) { let challengeDetails const token = yield getM2Mtoken() const challengeId = getV5ChallengeId(submission.challengeId) @@ -635,7 +635,7 @@ function* checkReviewGetAccess(authUser, submission) { * @param{String} fileURL S3 URL of the file to be downloaded * @returns {Buffer} Buffer of downloaded file */ -function* downloadFile(fileURL) { +function * downloadFile (fileURL) { const { bucket, key } = AmazonS3URI(fileURL) logger.info(`downloadFile(): file is on S3 ${bucket} / ${key}`) const downloadedFile = yield s3.getObject({ Bucket: bucket, Key: key }).promise() @@ -648,7 +648,7 @@ function* downloadFile(fileURL) { * Also stores the original topic in the payload * @param {Object} payload Data that needs to be posted to the bus api */ -function* postToBusApi(payload) { +function * postToBusApi (payload) { const busApiClient = getBusApiClient() const originalTopic = payload.topic @@ -668,7 +668,7 @@ function* postToBusApi(payload) { * @param {Array} reviews The reviews to remove metadata from * @param {Object} authUser The authenticated user details */ -function cleanseReviews(reviews, authUser) { +function cleanseReviews (reviews, authUser) { // Not a machine user if (!authUser.scopes) { const admin = _.filter(authUser.roles, role => role.toLowerCase() === 'Administrator'.toLowerCase()) @@ -701,7 +701,7 @@ function cleanseReviews(reviews, authUser) { * Function to get role id to role name map * @returns {Object|null} map */ -function* getRoleIdToRoleNameMap() { +function * getRoleIdToRoleNameMap () { let resourceRoles let resourceRolesMap = null const token = yield getM2Mtoken() @@ -729,7 +729,7 @@ function* getRoleIdToRoleNameMap() { * @param {Object} challengeDetails the challenge details * @returns {('Scheduled' | 'Open' | 'Closed' | 'Invalid')} status of the phase */ -function getPhaseStatus(phaseName, challengeDetails) { +function getPhaseStatus (phaseName, challengeDetails) { const { phases } = challengeDetails const queriedPhaseIndex = _.findIndex(phases, phase => { return phase.name === phaseName @@ -772,5 +772,6 @@ module.exports = { downloadFile, postToBusApi, cleanseReviews, - getRoleIdToRoleNameMap + getRoleIdToRoleNameMap, + getV5ChallengeId } From 219acd25db373355bb07942bcfb2259e54c38c8f Mon Sep 17 00:00:00 2001 From: Mithun Kamath Date: Wed, 10 Feb 2021 19:11:09 +0530 Subject: [PATCH 10/12] #210 - Swap challenge id details when responding to api request or posting to kafka --- src/common/helper.js | 22 +++++++++++++++----- src/services/SubmissionService.js | 34 +++++++++++++++++++++++-------- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/common/helper.js b/src/common/helper.js index 9e769bfc..bce61c86 100755 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -355,7 +355,7 @@ function * getV5ChallengeId (challengeId) { function * getSubmissionPhaseId (challengeId) { let phaseId = null let response - challengeId = getV5ChallengeId(challengeId) + challengeId = yield getV5ChallengeId(challengeId) try { logger.info(`Calling to challenge API to find submission phase Id for ${challengeId}`) @@ -395,7 +395,7 @@ function * checkCreateAccess (authUser, subEntity) { let challengeDetails let resources - const challengeId = getV5ChallengeId(subEntity.challengeId) + const challengeId = yield getV5ChallengeId(subEntity.challengeId) // User can only create submission for themselves if (authUser.userId !== subEntity.memberId) { @@ -487,7 +487,7 @@ function * checkGetAccess (authUser, submission) { } const token = yield getM2Mtoken() - const challengeId = getV5ChallengeId(submission.challengeId) + const challengeId = yield getV5ChallengeId(submission.challengeId) try { resources = yield request.get(`${config.RESOURCEAPI_V5_BASE_URL}/resources?challengeId=${challengeId}`) @@ -598,7 +598,7 @@ function * checkGetAccess (authUser, submission) { function * checkReviewGetAccess (authUser, submission) { let challengeDetails const token = yield getM2Mtoken() - const challengeId = getV5ChallengeId(submission.challengeId) + const challengeId = yield getV5ChallengeId(submission.challengeId) try { challengeDetails = yield request.get(`${config.CHALLENGEAPI_V5_URL}/${challengeId}`) @@ -757,6 +757,17 @@ function getPhaseStatus (phaseName, challengeDetails) { } } +/** + * Change challengeId to v5ChallengeId and legacyChallengeId to challengeId + * @param {Object} submission + */ +function adjustSubmissionChallengeId (submission) { + if (submission.challengeId && submission.legacyChallengeId) { + submission.v5ChallengeId = submission.challengeId + submission.challengeId = submission.legacyChallengeId + } +} + module.exports = { wrapExpress, autoWrapExpress, @@ -773,5 +784,6 @@ module.exports = { postToBusApi, cleanseReviews, getRoleIdToRoleNameMap, - getV5ChallengeId + getV5ChallengeId, + adjustSubmissionChallengeId } diff --git a/src/services/SubmissionService.js b/src/services/SubmissionService.js index c96c493f..d8b2ad93 100755 --- a/src/services/SubmissionService.js +++ b/src/services/SubmissionService.js @@ -154,6 +154,7 @@ function * getSubmission (authUser, submissionId) { // Return the retrieved submission logger.info(`getSubmission: returning data for submissionId: ${submissionId}`) + helper.adjustSubmissionChallengeId(submissionRecord) return submissionRecord } @@ -194,6 +195,7 @@ function * listSubmissions (authUser, query) { if (submission.review) { submission.review = helper.cleanseReviews(submission.review, authUser) } + helper.adjustSubmissionChallengeId(submission) return submission }) return data @@ -270,7 +272,8 @@ function * createSubmission (authUser, files, entity) { // Submission api only works with legacy challenge id // If it is a v5 challenge id, get the associated legacy challenge id - const challengeId = yield helper.getLegacyChallengeId(entity.challengeId) + const challengeId = yield helper.getV5ChallengeId(entity.challengeId) + const legacyChallengeId = yield helper.getLegacyChallengeId(entity.challengeId) const currDate = (new Date()).toISOString() const item = { @@ -278,7 +281,8 @@ function * createSubmission (authUser, files, entity) { type: entity.type, url: url, memberId: entity.memberId, - challengeId: challengeId, + challengeId, + legacyChallengeId, created: currDate, updated: currDate, createdBy: authUser.handle || authUser.sub, @@ -328,6 +332,9 @@ function * createSubmission (authUser, files, entity) { logger.info('Prepared submission item to insert into Dynamodb. Inserting...') yield dbhelper.insertRecord(record) + // After save to db, adjust challengeId to busApi and response + helper.adjustSubmissionChallengeId(item) + // Push Submission created event to Bus API // Request body for Posting to Bus API const reqBody = { @@ -365,7 +372,7 @@ createSubmission.schema = { fileType: joi.string(), url: joi.string().uri().trim(), memberId: joi.alternatives().try(joi.id(), joi.string().uuid()).required(), - challengeId: joi.string().uuid().required(), + challengeId: joi.alternatives().try(joi.id(), joi.string().uuid()).required(), legacySubmissionId: joi.alternatives().try(joi.id(), joi.string().uuid()), legacyUploadId: joi.alternatives().try(joi.id(), joi.string().uuid()), submissionPhaseId: joi.id(), @@ -391,6 +398,12 @@ function * _updateSubmission (authUser, submissionId, entity) { } const currDate = (new Date()).toISOString() + let challengeId = exist.challengeId + let legacyChallengeId = exist.legacyChallengeId + if (entity.challengeId) { + challengeId = yield helper.getV5ChallengeId(entity.challengeId) + legacyChallengeId = yield helper.getLegacyChallengeId(entity.challengeId) + } // Record used for updating in Database const record = { TableName: table, @@ -398,12 +411,13 @@ function * _updateSubmission (authUser, submissionId, entity) { id: submissionId }, UpdateExpression: `set #type = :t, #url = :u, memberId = :m, challengeId = :c, - updated = :ua, updatedBy = :ub, submittedDate = :sb`, + legacyChallengeId = :lc, updated = :ua, updatedBy = :ub, submittedDate = :sb`, ExpressionAttributeValues: { ':t': entity.type || exist.type, ':u': entity.url || exist.url, ':m': entity.memberId || exist.memberId, - ':c': entity.challengeId || exist.challengeId, + ':c': challengeId, + ':lc': legacyChallengeId, ':ua': currDate, ':ub': authUser.handle || authUser.sub, ':sb': entity.submittedDate || exist.submittedDate || exist.created @@ -437,6 +451,7 @@ function * _updateSubmission (authUser, submissionId, entity) { yield dbhelper.updateRecord(record) const updatedSub = yield _getSubmission(submissionId) + helper.adjustSubmissionChallengeId(updatedSub) // Push Submission updated event to Bus API // Request body for Posting to Bus API const reqBody = { @@ -448,6 +463,7 @@ function * _updateSubmission (authUser, submissionId, entity) { resource: helper.camelize(table), id: submissionId, challengeId: updatedSub.challengeId, + v5ChallengeId: updatedSub.v5ChallengeId, memberId: updatedSub.memberId, submissionPhaseId: updatedSub.submissionPhaseId, type: updatedSub.type, @@ -466,7 +482,9 @@ function * _updateSubmission (authUser, submissionId, entity) { { updated: currDate, updatedBy: authUser.handle || authUser.sub, - submittedDate: updatedSub.submittedDate + submittedDate: updatedSub.submittedDate, + challengeId: updatedSub.challengeId, + v5ChallengeId: updatedSub.v5ChallengeId } ) } @@ -489,7 +507,7 @@ updateSubmission.schema = { type: joi.string(), url: joi.string().uri().trim().required(), memberId: joi.alternatives().try(joi.id(), joi.string().uuid()).required(), - challengeId: joi.string().uuid().required(), + challengeId: joi.alternatives().try(joi.id(), joi.string().uuid()).required(), legacySubmissionId: joi.alternatives().try(joi.id(), joi.string().uuid()), legacyUploadId: joi.alternatives().try(joi.id(), joi.string().uuid()), submissionPhaseId: joi.id(), @@ -515,7 +533,7 @@ patchSubmission.schema = { type: joi.string(), url: joi.string().uri().trim(), memberId: joi.alternatives().try(joi.id(), joi.string().uuid()), - challengeId: joi.string().uuid(), + challengeId: joi.alternatives().try(joi.id(), joi.string().uuid()), legacySubmissionId: joi.alternatives().try(joi.id(), joi.string().uuid()), legacyUploadId: joi.alternatives().try(joi.id(), joi.string().uuid()), submissionPhaseId: joi.id(), From cc57dc75c82406a857cefebbe8b554a235bef1fa Mon Sep 17 00:00:00 2001 From: sachin-maheshwari Date: Wed, 10 Feb 2021 19:47:05 +0530 Subject: [PATCH 11/12] Update config.yml --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e8eb4a18..d71b5cf1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -67,7 +67,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'feature/shapeup-pure-v5-task'] + only: ['develop'] - "build-prod": context : org-global filters: From 7aa8f3da7e0023da732376575f90d62a72c280a1 Mon Sep 17 00:00:00 2001 From: Mithun Kamath Date: Thu, 11 Feb 2021 11:18:35 +0530 Subject: [PATCH 12/12] Set schema for challenge id --- scripts/createIndex.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/createIndex.js b/scripts/createIndex.js index 0780a041..eb39d293 100644 --- a/scripts/createIndex.js +++ b/scripts/createIndex.js @@ -18,6 +18,7 @@ co(function * createIndex () { properties: { resource: { type: 'keyword' }, challengeId: { type: 'keyword' }, + legacyChallengeId: { type: 'keyword' }, memberId: { type: 'keyword' }, type: { type: 'keyword' }, isFileSubmission: { type: 'boolean' },