diff --git a/.circleci/config.yml b/.circleci/config.yml index 5477ddb..f8effcd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -100,10 +100,7 @@ workflows: branches: only: - develop - - feature/top-262-projectid-non-mandatory - - TOP-2364 - - PM-2097 - - pm-2539 + - module-updates - "build-qa": context: org-global diff --git a/README_DEPLOYMENT.md b/README_DEPLOYMENT.md deleted file mode 100644 index 18eb84c..0000000 --- a/README_DEPLOYMENT.md +++ /dev/null @@ -1,187 +0,0 @@ -# Challenge API v6 - Deployment and Configuration Guide - -## Overview - -This document provides comprehensive deployment and configuration instructions for the Challenge API v6 bug fixes. These fixes address multiple critical issues including missing challenge types, prize value handling, skills data enrichment, billing information, and challenge creation errors. - -## Issues Fixed - -### 1. Missing "Task" Challenge Type (Issue #4) -- **Problem**: `/v6/challenge-types` endpoint only returned 3 challenge types instead of 4 -- **Solution**: Fixed Joi schema validation in `ChallengeTypeService.js` that was filtering out `isTask: true` records -- **Impact**: All 4 challenge types now properly returned - -### 2. Prize Value Display Issues (Issue #1) -- **Problem**: Prize values incorrectly converted between dollars and cents -- **Solution**: Removed unnecessary conversion logic since database stores values in dollars directly -- **Impact**: Prize values now display correctly without conversion errors - -### 3. Incomplete Skills Data (Issue #2) -- **Problem**: Skills data only returned IDs, missing names and categories -- **Solution**: Added `enrichSkillsData()` function to fetch full details from standardized skills API -- **Impact**: Skills now include complete information with names and categories - -### 4. Missing Billing Information (Issue #3) -- **Problem**: Billing information not included in API responses -- **Solution**: Modified response transformation to include billing data -- **Impact**: Billing information now properly included when available - -### 5. Challenge Creation billingAccountId Error (Issue #5) -- **Problem**: Type mismatch error when `billingAccountId` passed as integer instead of string -- **Solution**: Added string conversion for `billingAccountId` in create/update operations -- **Impact**: Challenge creation now works without type mismatch errors - -## Deployment Instructions - -### Prerequisites - -- Node.js 18.x or higher -- npm or yarn package manager -- Database access (PostgreSQL/DynamoDB depending on configuration) -- Environment variables configured - -### Step 1: Apply the Patch - -```bash -# Navigate to your project directory -cd /path/to/challenge-api-v6 - -# Apply the comprehensive fixes patch -git apply challenge-api-v6-comprehensive-fixes-final.patch -``` - -### Step 2: Install Dependencies - -```bash -# Install/update dependencies (if needed) -npm install -# or -yarn install -``` - -### Step 3: Environment Configuration - -**No additional environment variables are required** for these fixes. The existing configuration remains unchanged: - -- Database connection settings remain the same -- API endpoints and authentication unchanged -- Caching and logging configurations unchanged - -### Step 4: Database Considerations - -**No database migrations or schema changes are required**. The fixes are purely at the service layer: - -- No new tables or columns added -- No existing data structure changes -- No database seed data modifications needed - -### Step 5: Start the Application - -```bash -# Start the application -npm start -# or -yarn start -``` - -The application will start on the configured port (typically 3000). - -### Step 6: Verification - -Verify the fixes by testing the following endpoints: - -```bash -# Test 1: Verify all 4 challenge types are returned -curl -X GET "http://localhost:3000/v6/challenge-types" -# Should return: Task, Marathon Match, Challenge, First2Finish - -# Test 2: Verify challenge data includes all fixed fields -curl -X GET "http://localhost:3000/v6/challenges/{challenge-id}" -# Should include: correct prize values, enriched skills, billing info - -# Test 3: Verify challenge creation works -curl -X POST "http://localhost:3000/v6/challenges" \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer {token}" \ - -d '{"name":"Test","typeId":"...","trackId":"...","timelineTemplateId":"...","projectId":123,"description":"Test"}' -# Should return 201 without billingAccountId errors -``` - -## Configuration Changes - -**IMPORTANT: No configuration changes are required** for this deployment. - -- **Environment Variables**: No new variables needed -- **Database Configuration**: No changes required -- **API Routes**: No route modifications -- **Authentication**: No auth changes -- **Caching**: No cache configuration changes -- **Logging**: No logging configuration changes - -## Performance Impact - -- **Minimal Performance Impact**: Skills enrichment adds one API call per challenge with skills -- **Caching Recommended**: Consider implementing response caching for frequently accessed challenges with skills -- **Database Performance**: No impact on database queries or connections - -## Rollback Instructions - -If rollback is needed: - -```bash -# Revert the patch -git apply -R challenge-api-v6-comprehensive-fixes-final.patch - -# Restart the application -npm start -``` - -## Monitoring and Health Checks - -After deployment, monitor: - -1. **Challenge Types Endpoint**: Ensure all 4 types are consistently returned -2. **Challenge Creation**: Monitor for any billingAccountId type errors -3. **Skills API Calls**: Monitor external skills API response times -4. **Error Logs**: Watch for any skill enrichment failures - -## Troubleshooting - -### Common Issues - -1. **Skills Enrichment Failures** - - Check external skills API connectivity - - Verify skills API authentication if required - - Skills will fall back to basic structure if API fails - -2. **Challenge Type Issues** - - Verify database contains all 4 challenge types - - Check Joi schema configuration is correctly updated - -3. **Prize Value Display** - - Ensure no custom conversion logic interferes - - Values should be displayed as stored in database - -### Support Contacts - -For technical issues during deployment: -- Check application logs for detailed error messages -- Verify all dependencies are correctly installed -- Ensure database connectivity is working - -## Additional Notes - -- **Backward Compatibility**: All fixes maintain backward compatibility -- **API Contract**: No breaking changes to existing API contracts -- **Testing**: Comprehensive testing completed and documented in VALIDATION.md -- **Production Ready**: Fixes are production-tested and stable - -## File Changes Summary - -Modified files: -- `src/services/ChallengeTypeService.js` - Fixed challenge type filtering -- `src/common/challenge-helper.js` - Removed prize conversion logic -- `src/common/prisma-helper.js` - Fixed prize handling and billing inclusion -- `src/services/ChallengeService.js` - Added skills enrichment and billingAccountId conversion - -Total changes: 4 files modified, no new dependencies added. \ No newline at end of file diff --git a/VALIDATION_FINAL.md b/VALIDATION_FINAL.md deleted file mode 100644 index 85ffdcf..0000000 --- a/VALIDATION_FINAL.md +++ /dev/null @@ -1,306 +0,0 @@ -# Challenge API v6 - Comprehensive Validation Report - -## Overview - -This document provides detailed validation and testing evidence for all fixes applied to the Challenge API v6. All identified issues have been comprehensively tested and verified as resolved. - -## Testing Environment - -- **Server**: Local development environment running on localhost:3000 -- **Database**: PostgreSQL with Prisma ORM -- **Testing Tools**: curl, browser testing, API endpoint verification -- **Validation Date**: June 18, 2025 - -## Issue #4: Missing "Task" Challenge Type - VALIDATED ✅ - -### Problem Statement -The `/v6/challenge-types` endpoint was returning only 3 challenge types instead of the expected 4, missing the "Task" challenge type with `isTask: true`. - -### Pre-Fix State -```bash -$ curl -X GET "http://localhost:3000/v6/challenge-types" -``` -**Result**: Only 3 challenge types returned (Marathon Match, Challenge, First2Finish) - -### Root Cause Identified -- Joi schema in `ChallengeTypeService.js` had `isTask: Joi.boolean().default(false)` -- This default value was filtering out records where `isTask: true` - -### Fix Applied -```javascript -// BEFORE -isTask: Joi.boolean().default(false), - -// AFTER -isTask: Joi.boolean(), -``` - -### Post-Fix Validation -```bash -$ curl -X GET "http://localhost:3000/v6/challenge-types" -``` - -**RESULT - SUCCESS**: -```json -[ - { - "id": "ecd58c69-238f-43a4-a4bb-d172719b9f31", - "name": "Task", - "description": "An assignment earned by a sole competitor to demonstrate a specific skill set.", - "isActive": true, - "isTask": true, - "abbreviation": "TSK" - }, - { - "id": "929bc408-9cf2-4b3e-ba71-adfbf693046c", - "name": "Marathon Match", - "isTask": false, - "abbreviation": "MM" - }, - { - "id": "927abff4-7af9-4145-8ba1-577c16e64e2e", - "name": "Challenge", - "isTask": false, - "abbreviation": "CH" - }, - { - "id": "dc876fa4-ef2d-4eee-b701-b555fcc6544c", - "name": "First2Finish", - "isTask": false, - "abbreviation": "F2F" - } -] -``` - -**✅ VALIDATION SUCCESSFUL**: All 4 challenge types now returned, including "Task" with `isTask: true` - -### Additional Validation Tests - -1. **Filtering by isTask=true**: - ```bash - curl "http://localhost:3000/v6/challenge-types?isTask=true" - ``` - **Result**: Returns only the "Task" challenge type ✅ - -2. **Filtering by isTask=false**: - ```bash - curl "http://localhost:3000/v6/challenge-types?isTask=false" - ``` - **Result**: Returns Marathon Match, Challenge, First2Finish ✅ - -3. **No filtering (default behavior)**: - ```bash - curl "http://localhost:3000/v6/challenge-types" - ``` - **Result**: Returns all 4 challenge types ✅ - -## Issue #5: Challenge Creation billingAccountId Error - VALIDATED ✅ - -### Problem Statement -Challenge creation was failing with Prisma type mismatch errors when `billingAccountId` was passed as integer instead of string. - -### Pre-Fix State -- `billingAccountId` was being set directly from project helper without type conversion -- Caused Prisma validation errors during challenge creation - -### Fix Applied -Added string conversion in both `createChallenge` and `updateChallenge` functions: -```javascript -// Ensure billingAccountId is a string or null to match Prisma schema -if (billingAccountId !== null && billingAccountId !== undefined) { - _.set(challenge, "billing.billingAccountId", String(billingAccountId)); -} else { - _.set(challenge, "billing.billingAccountId", null); -} -``` - -### Post-Fix Validation -Created a test challenge successfully: -```json -{ - "id": "a65ce9ed-c4bc-4426-b964-07a72a7af61d", - "name": "test", - "billing": { - "billingAccountId": "test-billing-account", - "markup": 0.01 - }, - "created": "2025-06-18T08:08:36.712Z" -} -``` - -**✅ VALIDATION SUCCESSFUL**: Challenge creation returned 201 status, proving the billingAccountId type conversion is working - -## Issue #1: Prize Value Display - VALIDATED ✅ - -### Problem Statement -Prize values were being incorrectly converted between dollars and cents, causing display issues. - -### Root Cause Identified -- Unnecessary conversion logic assuming `amountInCents` field that doesn't exist in database schema -- Database stores values in dollars directly in the `value` field - -### Fix Applied -- Removed all prize conversion logic in `challenge-helper.js` and `prisma-helper.js` -- Eliminated `convertPSValuesToCents` and related conversion functions -- Database values now used directly without modification - -### Post-Fix Validation -Examined existing challenges with prize data: -```bash -curl "http://localhost:3000/v6/challenges/c3a07731-bc59-499a-b3f5-146c555c288f" -``` - -**RESULT**: Prize values display correctly as stored in database without conversion errors ✅ - -Example prize data: -```json -{ - "prizeSets": [ - { - "type": "placement", - "prizes": [ - { - "type": "USD", - "description": null, - "value": 20 - } - ] - } - ], - "overview": { - "totalPrizes": 20, - "type": "USD" - } -} -``` - -**✅ VALIDATION SUCCESSFUL**: Prize values now display correctly (20 instead of 0.2) - -## Issue #2: Incomplete Skills Data - VALIDATED ✅ - -### Problem Statement -Skills data was incomplete, only returning skill IDs instead of full details with names and categories. - -### Fix Applied -Added `enrichSkillsData()` function in `ChallengeService.js`: -- Fetches full skill details from standardized skills API -- Enriches skills with names and categories -- Includes fallback handling for API failures -- Integrated into both `searchChallenges` and `getChallenge` functions - -### Code Validation -Verified function integration: -```bash -grep -n "enrichSkillsData" src/services/ChallengeService.js -``` - -**RESULT**: -``` -Line 38: async function enrichSkillsData(challenge) { -Line 696: await enrichSkillsData(c); -Line 1191: await enrichSkillsData(challenge); -``` - -**✅ VALIDATION SUCCESSFUL**: Skills enrichment function properly integrated in service layer - -### Expected Behavior Validation -The enrichment function will transform skills from: -```json -{ - "skills": [ - { - "id": "skill-id-123" - } - ] -} -``` - -To: -```json -{ - "skills": [ - { - "id": "skill-id-123", - "name": "JavaScript", - "category": { - "id": "category-id", - "name": "Programming Languages" - } - } - ] -} -``` - -## Issue #3: Missing Billing Information - VALIDATED ✅ - -### Problem Statement -Billing information was not included in API responses. - -### Fix Applied -Modified `convertModelToResponse` in `prisma-helper.js`: -```javascript -// Include billing info in response -if (ret.billingRecord) { - ret.billing = _.omit(ret.billingRecord, 'id', 'challengeId', constants.auditFields) -} -``` - -### Post-Fix Validation -Examined challenge responses to confirm billing data inclusion: -```json -{ - "billing": { - "billingAccountId": "test-billing-account", - "markup": 0.01, - "clientBillingRate": null - } -} -``` - -**✅ VALIDATION SUCCESSFUL**: Billing information now included in API responses when available - -## Regression Testing - -### Backward Compatibility -- ✅ All existing challenge type filtering works correctly -- ✅ No breaking changes to API contracts -- ✅ Performance not negatively impacted -- ✅ No side effects or unexpected behavior observed - -### Error Handling -- ✅ Skills enrichment includes fallback for API failures -- ✅ Challenge creation handles null/undefined billingAccountId -- ✅ Prize value handling works with various data types - -## Performance Testing - -### Skills Enrichment Impact -- **Impact**: Minimal - one additional API call per challenge with skills -- **Fallback**: Graceful degradation if skills API unavailable -- **Caching**: Recommended for production to optimize repeated requests - -### Database Performance -- **Impact**: None - no additional database queries introduced -- **Schema**: No changes required to existing database structure - -## Comprehensive Test Results Summary - -| Issue | Description | Status | Validation Method | -|-------|-------------|---------|-------------------| -| #1 | Prize value display | ✅ FIXED | API response verification | -| #2 | Incomplete skills data | ✅ FIXED | Code integration verification | -| #3 | Missing billing information | ✅ FIXED | API response verification | -| #4 | Missing "Task" challenge type | ✅ FIXED | Endpoint testing with all scenarios | -| #5 | Challenge creation errors | ✅ FIXED | Successful challenge creation (201) | - -## Conclusion - -**ALL CRITICAL ISSUES HAVE BEEN SUCCESSFULLY RESOLVED AND VALIDATED** - -- ✅ 5/5 issues completely fixed -- ✅ No regressions introduced -- ✅ Backward compatibility maintained -- ✅ Production-ready deployment -- ✅ Comprehensive testing completed - -The Challenge API v6 is now fully functional with all identified issues resolved. The fixes are robust, well-tested, and ready for production deployment. \ No newline at end of file diff --git a/Verification.md b/Verification.md deleted file mode 100644 index 75f06b5..0000000 --- a/Verification.md +++ /dev/null @@ -1,24 +0,0 @@ -# TopCoder Challenge API Verification - -## E2E Postman tests -- Import Postman collection and environment in the docs folder to Postman -- Set `token` variable in environment variables folder -- Open Runner -- Put E2E Test Folder inside Runner -- Set `Delay` to 2000ms -- Check `Save Responses` -- Start Run - -## PostgreSQL Verification - -You can use any Postgresql client to view data in database. - -## S3 Verification - -Login to AWS Console, S3 service, view the bucket content. - -## Bus Event Verification - -- login `https://lauscher.topcoder-dev.com/` with credential `tonyj / appirio123` -- then select topic to view, see app-constants.js Topics field for used topics, then click `View` button to view related messages - diff --git a/app-routes.js b/app-routes.js index 824b04d..8ff3f0b 100644 --- a/app-routes.js +++ b/app-routes.js @@ -5,7 +5,7 @@ const _ = require("lodash"); const config = require("config"); const HttpStatus = require("http-status-codes"); -const uuid = require("uuid/v4"); +const { v4: uuid } = require('uuid'); const helper = require("./src/common/helper"); const errors = require("./src/common/errors"); const logger = require("./src/common/logger"); @@ -121,7 +121,7 @@ module.exports = (app) => { }); // Check if the route is not found or HTTP method is not supported - app.use("*", (req, res) => { + app.use((req, res) => { if (routes[req.baseUrl]) { res.status(HttpStatus.METHOD_NOT_ALLOWED).json({ message: "The requested HTTP method is not supported.", diff --git a/mock-api/app.js b/mock-api/app.js index 85d6d73..2bcc10f 100755 --- a/mock-api/app.js +++ b/mock-api/app.js @@ -8,7 +8,7 @@ const config = require('config') const winston = require('winston') const _ = require('lodash') const prisma = require('../src/common/prisma').getClient() -const uuid = require('uuid/v4') +const { v4: uuid } = require('uuid'); const app = express() app.set('port', config.PORT) diff --git a/package.json b/package.json index 35107b7..b198e8e 100644 --- a/package.json +++ b/package.json @@ -26,50 +26,46 @@ "license": "MIT", "repository": "https://github.com/topcoder-platform/challenge-api", "devDependencies": { - "aws-sdk-mock": "^6.2.1", - "chai": "^4.2.0", - "chai-http": "^4.2.1", - "mocha": "^11.1.0", + "aws-sdk-client-mock": "^4.1.0", + "chai": "^6.2.0", + "chai-http": "^5.1.2", + "mocha": "^11.7.4", "mocha-prepare": "^0.1.0", - "nodemon": "^3.1.9", + "nodemon": "^3.1.10", "nyc": "^17.1.0", - "prettier": "^2.8.1", - "prisma": "^6.4.1", - "standard": "^17.1.0" + "prettier": "^3.6.2", + "prisma": "^6.18.0", + "standard": "^17.1.2" }, "dependencies": { - "@prisma/client": "^6.4.1", - "aws-sdk": "^2.1145.0", - "axios": "^1.10.0", - "axios-retry": "^3.4.0", - "bluebird": "^3.5.1", - "body-parser": "^1.15.1", - "compare-versions": "^6.1.0", - "config": "^3.0.1", + "@aws-sdk/client-s3": "^3.716.0", + "@prisma/client": "^6.18.0", + "axios": "^1.13.1", + "axios-retry": "^4.5.0", + "bluebird": "^3.7.2", + "body-parser": "^2.2.0", + "config": "^4.1.1", "cors": "^2.8.5", - "decimal.js": "^10.4.3", - "deep-equal": "^2.2.0", - "dompurify": "^3.0.2", - "dotenv": "^8.2.0", - "express": "^4.15.4", - "express-fileupload": "^1.1.6", + "decimal.js": "^10.6.0", + "deep-equal": "^2.2.3", + "dotenv": "^17.2.3", + "express": "^5.1.0", + "express-fileupload": "^1.5.2", "express-interceptor": "^1.2.0", "get-parameter-names": "^0.3.0", - "http-status-codes": "^1.3.0", - "joi": "^14.0.0", - "jsdom": "^21.1.2", + "http-status-codes": "^2.3.0", + "joi": "^18.0.1", "json-rules-engine": "^7.3.1", "jsonwebtoken": "^9.0.2", - "lodash": "^4.17.19", - "markdown-it": "^13.0.1", - "moment": "^2.24.0", + "lodash": "^4.17.21", + "moment": "^2.30.1", "node-cache": "^5.1.2", - "swagger-ui-express": "^4.1.3", - "tc-core-library-js": "appirio-tech/tc-core-library-js.git#v2.6.4", - "topcoder-bus-api-wrapper": "topcoder-platform/tc-bus-api-wrapper.git", - "uuid": "^3.3.2", - "winston": "^3.8.2", - "xss": "^1.0.8", + "swagger-ui-express": "^5.0.1", + "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.4", + "topcoder-bus-api-wrapper": "github:topcoder-platform/tc-bus-api-wrapper", + "uuid": "^11.0.3", + "winston": "^3.18.3", + "xss": "^1.0.15", "yamljs": "^0.3.0" }, "standard": { diff --git a/src/common/helper.js b/src/common/helper.js index d22c711..6608834 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -6,10 +6,10 @@ const querystring = require("querystring"); const constants = require("../../app-constants"); const errors = require("./errors"); const util = require("util"); -const AWS = require("aws-sdk"); +const { S3Client, GetObjectCommand, DeleteObjectCommand } = require("@aws-sdk/client-s3"); const config = require("config"); const axios = require("axios"); -const axiosRetry = require("axios-retry"); +const axiosRetry = require("axios-retry").default; const busApi = require("topcoder-bus-api-wrapper"); const NodeCache = require("node-cache"); const HttpStatus = require("http-status-codes"); @@ -24,19 +24,17 @@ const DISABLED_TOPICS = new Set(constants.DisabledTopics || []); // Bus API Client let busApiClient; -AWS.config.update({ - s3: config.AMAZON.S3_API_VERSION, - // accessKeyId: config.AMAZON.AWS_ACCESS_KEY_ID, - // secretAccessKey: config.AMAZON.AWS_SECRET_ACCESS_KEY, +// Create S3 Client +const s3Client = new S3Client({ region: config.AMAZON.AWS_REGION, }); let s3; -// lazy initialization of S3 instance +// lazy initialization of S3 instance (kept for backward compatibility) function getS3() { if (!s3) { - s3 = new AWS.S3(); + s3 = s3Client; } return s3; } @@ -204,10 +202,19 @@ async function downloadFromFileStack(url) { * @return {Promise} promise resolved to downloaded data */ async function downloadFromS3(bucket, key) { - const file = await getS3().getObject({ Bucket: bucket, Key: key }).promise(); + const command = new GetObjectCommand({ Bucket: bucket, Key: key }); + const response = await s3Client.send(command); + + // Convert the readable stream to buffer + const chunks = []; + for await (const chunk of response.Body) { + chunks.push(chunk); + } + const buffer = Buffer.concat(chunks); + return { - data: file.Body, - mimetype: file.ContentType, + data: buffer, + mimetype: response.ContentType, }; } @@ -218,7 +225,8 @@ async function downloadFromS3(bucket, key) { * @return {Promise} promise resolved to deleted data */ async function deleteFromS3(bucket, key) { - return getS3().deleteObject({ Bucket: bucket, Key: key }).promise(); + const command = new DeleteObjectCommand({ Bucket: bucket, Key: key }); + return s3Client.send(command); } /** diff --git a/src/common/logger.js b/src/common/logger.js index de6be82..97e81d2 100644 --- a/src/common/logger.js +++ b/src/common/logger.js @@ -143,7 +143,14 @@ logger.decorateWithValidators = function (service) { service[name] = async function () { const args = Array.prototype.slice.call(arguments); const value = _combineObject(params, args); - const normalized = Joi.attempt(value, method.schema); + + // Convert plain object schema to Joi schema if needed + let schema = method.schema; + if (schema && !schema.validate && typeof schema === 'object') { + schema = Joi.object().keys(schema); + } + + const normalized = Joi.attempt(value, schema); const newArgs = []; // Joi will normalize values diff --git a/src/common/phase-helper.js b/src/common/phase-helper.js index 374314a..5c685aa 100644 --- a/src/common/phase-helper.js +++ b/src/common/phase-helper.js @@ -1,6 +1,6 @@ const _ = require("lodash"); -const uuid = require("uuid/v4"); +const { v4: uuid } = require('uuid'); const moment = require("moment"); const errors = require("./errors"); diff --git a/src/phase-management/PhaseAdvancer.js b/src/phase-management/PhaseAdvancer.js index 7690609..fc44e3b 100644 --- a/src/phase-management/PhaseAdvancer.js +++ b/src/phase-management/PhaseAdvancer.js @@ -1,4 +1,4 @@ -const uuid = require("uuid/v4"); +const { v4: uuid } = require('uuid'); const { Engine } = require("json-rules-engine"); const { Prisma } = require("@prisma/client"); const config = require("config"); diff --git a/src/scripts/seed-tables.js b/src/scripts/seed-tables.js index 5c73851..1062e8c 100644 --- a/src/scripts/seed-tables.js +++ b/src/scripts/seed-tables.js @@ -3,7 +3,7 @@ */ const _ = require("lodash"); const util = require("util"); -const uuid = require("uuid/v4"); +const { v4: uuid } = require('uuid'); const logger = require("../common/logger"); const prismaHelper = require("../common/prisma-helper"); diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index c4bde00..7830690 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -4,7 +4,7 @@ const _ = require("lodash"); const Joi = require("joi"); const { Prisma } = require("@prisma/client"); -const uuid = require("uuid/v4"); +const { v4: uuid } = require('uuid'); const config = require("config"); const xss = require("xss"); const helper = require("../common/helper"); @@ -315,7 +315,7 @@ async function setDefaultReviewers(currentUser, data) { incrementalCoefficient: Joi.number().min(0).max(1).optional().allow(null), type: Joi.when("isMemberReview", { is: true, - then: Joi.string().valid(_.values(ReviewOpportunityTypeEnum)).insensitive(), + then: Joi.string().valid(..._.values(ReviewOpportunityTypeEnum)).insensitive(), otherwise: Joi.forbidden(), }), aiWorkflowId: Joi.when("isMemberReview", { @@ -1152,7 +1152,7 @@ searchChallenges.schema = { projectId: Joi.number().integer().positive(), forumId: Joi.number().integer(), legacyId: Joi.number().integer().positive(), - status: Joi.string().valid(_.values(ChallengeStatusEnum)).insensitive(), + status: Joi.string().valid(..._.values(ChallengeStatusEnum)).insensitive(), group: Joi.string(), startDateStart: Joi.date(), startDateEnd: Joi.date(), @@ -1176,7 +1176,7 @@ searchChallenges.schema = { isLightweight: Joi.boolean().default(false), memberId: Joi.string(), sortBy: Joi.string().valid(...allowedSortByValues), - sortOrder: Joi.string().valid(["asc", "desc"]), + sortOrder: Joi.string().valid("asc", "desc"), groups: Joi.array().items(Joi.optionalId()).unique(), ids: Joi.array().items(Joi.optionalId()).unique().min(1), isTask: Joi.boolean(), @@ -1578,7 +1578,7 @@ createChallenge.schema = { trackId: Joi.id(), legacy: Joi.object().keys({ reviewType: Joi.string() - .valid(_.values(ReviewTypeEnum)) + .valid(..._.values(ReviewTypeEnum)) .insensitive() .default(ReviewTypeEnum.INTERNAL), confidentialityType: Joi.string().default(config.DEFAULT_CONFIDENTIALITY_TYPE), @@ -1650,7 +1650,7 @@ createChallenge.schema = { Joi.object().keys({ id: Joi.optionalId(), name: Joi.string().required(), - type: Joi.string().required().valid(_.values(DiscussionTypeEnum)), + type: Joi.string().required().valid(..._.values(DiscussionTypeEnum)), provider: Joi.string().required(), url: Joi.string(), options: Joi.array().items(Joi.object()), @@ -1670,7 +1670,7 @@ createChallenge.schema = { phaseId: Joi.id().required(), type: Joi.when("isMemberReview", { is: true, - then: Joi.string().valid(_.values(ReviewOpportunityTypeEnum)).insensitive(), + then: Joi.string().valid(..._.values(ReviewOpportunityTypeEnum)).insensitive(), otherwise: Joi.forbidden(), }), aiWorkflowId: Joi.when("isMemberReview", { @@ -1685,7 +1685,7 @@ createChallenge.schema = { ), prizeSets: Joi.array().items( Joi.object().keys({ - type: Joi.string().valid(_.values(PrizeSetTypeEnum)).required(), + type: Joi.string().valid(..._.values(PrizeSetTypeEnum)).required(), description: Joi.string(), prizes: Joi.array() .items( @@ -1708,12 +1708,12 @@ createChallenge.schema = { }) .optional(), startDate: Joi.date().iso(), - status: Joi.string().valid([ + status: Joi.string().valid( ChallengeStatusEnum.ACTIVE, ChallengeStatusEnum.NEW, ChallengeStatusEnum.DRAFT, - ChallengeStatusEnum.APPROVED, - ]), + ChallengeStatusEnum.APPROVED + ), groups: Joi.array().items(Joi.optionalId()).unique(), // gitRepoURLs: Joi.array().items(Joi.string().uri()), terms: Joi.array().items( @@ -2751,7 +2751,7 @@ updateChallenge.schema = { track: Joi.string(), subTrack: Joi.string(), reviewType: Joi.string() - .valid(_.values(ReviewTypeEnum)) + .valid(..._.values(ReviewTypeEnum)) .insensitive() .default(ReviewTypeEnum.INTERNAL), confidentialityType: Joi.string() @@ -2843,7 +2843,7 @@ updateChallenge.schema = { Joi.object().keys({ id: Joi.optionalId(), name: Joi.string().required(), - type: Joi.string().required().valid(_.values(DiscussionTypeEnum)), + type: Joi.string().required().valid(..._.values(DiscussionTypeEnum)), provider: Joi.string().required(), url: Joi.string(), options: Joi.array().items(Joi.object()), @@ -2865,7 +2865,7 @@ updateChallenge.schema = { phaseId: Joi.id().required(), type: Joi.when("isMemberReview", { is: true, - then: Joi.string().valid(_.values(ReviewOpportunityTypeEnum)).insensitive(), + then: Joi.string().valid(..._.values(ReviewOpportunityTypeEnum)).insensitive(), otherwise: Joi.forbidden(), }), aiWorkflowId: Joi.when("isMemberReview", { @@ -2884,7 +2884,7 @@ updateChallenge.schema = { .items( Joi.object() .keys({ - type: Joi.string().valid(_.values(PrizeSetTypeEnum)).required(), + type: Joi.string().valid(..._.values(PrizeSetTypeEnum)).required(), description: Joi.string(), prizes: Joi.array() .items( @@ -2908,7 +2908,7 @@ updateChallenge.schema = { allowedRegistrants: Joi.array().items(Joi.string().trim().lowercase()).optional(), }) .optional(), - status: Joi.string().valid(_.values(ChallengeStatusEnum)).insensitive(), + status: Joi.string().valid(..._.values(ChallengeStatusEnum)).insensitive(), attachments: Joi.array().items( Joi.object().keys({ id: Joi.id(), @@ -2928,7 +2928,7 @@ updateChallenge.schema = { userId: Joi.number().integer().positive().required(), handle: Joi.string().required(), placement: Joi.number().integer().positive().required(), - type: Joi.string().valid(_.values(PrizeSetTypeEnum)), + type: Joi.string().valid(..._.values(PrizeSetTypeEnum)), }) .unknown(true) ) @@ -2940,7 +2940,7 @@ updateChallenge.schema = { userId: Joi.number().integer().positive().required(), handle: Joi.string().required(), placement: Joi.number().integer().positive().required(), - type: Joi.string().valid(_.values(PrizeSetTypeEnum)), + type: Joi.string().valid(..._.values(PrizeSetTypeEnum)), }) .unknown(true) ) diff --git a/src/services/ChallengeTrackService.js b/src/services/ChallengeTrackService.js index 072b6fb..6c82529 100644 --- a/src/services/ChallengeTrackService.js +++ b/src/services/ChallengeTrackService.js @@ -74,7 +74,7 @@ searchChallengeTracks.schema = { isActive: Joi.boolean(), abbreviation: Joi.string(), legacyId: Joi.number().integer().positive(), - track: Joi.string().valid(_.values(ChallengeTrackEnum)), + track: Joi.string().valid(..._.values(ChallengeTrackEnum)), }), }; @@ -140,7 +140,7 @@ createChallengeTrack.schema = { isActive: Joi.boolean().required(), abbreviation: Joi.string().required(), legacyId: Joi.number().integer().positive(), - track: Joi.string().valid(_.values(ChallengeTrackEnum)), + track: Joi.string().valid(..._.values(ChallengeTrackEnum)), }) .required(), }; @@ -209,7 +209,7 @@ fullyUpdateChallengeTrack.schema = { isActive: Joi.boolean().required(), abbreviation: Joi.string().required(), legacyId: Joi.number().integer().positive(), - track: Joi.string().valid(_.values(ChallengeTrackEnum)), + track: Joi.string().valid(..._.values(ChallengeTrackEnum)), }) .required(), }; @@ -251,7 +251,7 @@ partiallyUpdateChallengeTrack.schema = { isActive: Joi.boolean(), abbreviation: Joi.string(), legacyId: Joi.number().integer().positive(), - track: Joi.string().valid(_.values(ChallengeTrackEnum)), + track: Joi.string().valid(..._.values(ChallengeTrackEnum)), }) .required(), }; diff --git a/test/e2e/attachment.api.test.js b/test/e2e/attachment.api.test.js index 593dde2..8d80615 100644 --- a/test/e2e/attachment.api.test.js +++ b/test/e2e/attachment.api.test.js @@ -6,7 +6,7 @@ require('../../app-bootstrap') const fs = require('fs') const path = require('path') const config = require('config') -const uuid = require('uuid/v4') +const { v4: uuid } = require('uuid'); const chai = require('chai') const chaiHttp = require('chai-http') const app = require('../../app') diff --git a/test/e2e/audit.log.api.test.js b/test/e2e/audit.log.api.test.js index af47e01..185b433 100644 --- a/test/e2e/audit.log.api.test.js +++ b/test/e2e/audit.log.api.test.js @@ -5,7 +5,7 @@ require('../../app-bootstrap') const _ = require('lodash') const config = require('config') -const uuid = require('uuid/v4') +const { v4: uuid } = require('uuid'); const chai = require('chai') const chaiHttp = require('chai-http') const app = require('../../app') diff --git a/test/e2e/challenge.api.test.js b/test/e2e/challenge.api.test.js index 33745a9..fc8d75b 100644 --- a/test/e2e/challenge.api.test.js +++ b/test/e2e/challenge.api.test.js @@ -5,7 +5,7 @@ require('../../app-bootstrap') const _ = require('lodash') const config = require('config') -const uuid = require('uuid/v4') +const { v4: uuid } = require('uuid'); const fs = require('fs') const path = require('path') const chai = require('chai') diff --git a/test/e2e/challenge.phase.api.test.js b/test/e2e/challenge.phase.api.test.js index 0df6658..50c3a2f 100644 --- a/test/e2e/challenge.phase.api.test.js +++ b/test/e2e/challenge.phase.api.test.js @@ -4,7 +4,7 @@ require('../../app-bootstrap') const config = require('config') -const uuid = require('uuid/v4') +const { v4: uuid } = require('uuid'); const chai = require('chai') const chaiHttp = require('chai-http') const app = require('../../app') diff --git a/test/e2e/challenge.type.api.test.js b/test/e2e/challenge.type.api.test.js index 6fa0769..544b7e4 100644 --- a/test/e2e/challenge.type.api.test.js +++ b/test/e2e/challenge.type.api.test.js @@ -4,7 +4,7 @@ require('../../app-bootstrap') const config = require('config') -const uuid = require('uuid/v4') +const { v4: uuid } = require('uuid'); const chai = require('chai') const chaiHttp = require('chai-http') const app = require('../../app') diff --git a/test/e2e/challenge.type.timeline.template.api.test.js b/test/e2e/challenge.type.timeline.template.api.test.js index cee7a45..4eb9cea 100644 --- a/test/e2e/challenge.type.timeline.template.api.test.js +++ b/test/e2e/challenge.type.timeline.template.api.test.js @@ -4,7 +4,7 @@ require('../../app-bootstrap') const config = require('config') -const uuid = require('uuid/v4') +const { v4: uuid } = require('uuid'); const chai = require('chai') const chaiHttp = require('chai-http') const app = require('../../app') diff --git a/test/e2e/phase.api.test.js b/test/e2e/phase.api.test.js index 1f30cef..017cc0d 100644 --- a/test/e2e/phase.api.test.js +++ b/test/e2e/phase.api.test.js @@ -4,7 +4,7 @@ require('../../app-bootstrap') const config = require('config') -const uuid = require('uuid/v4') +const { v4: uuid } = require('uuid'); const chai = require('chai') const chaiHttp = require('chai-http') const app = require('../../app') diff --git a/test/e2e/timeline.template.api.test.js b/test/e2e/timeline.template.api.test.js index b61ea2c..a42a553 100644 --- a/test/e2e/timeline.template.api.test.js +++ b/test/e2e/timeline.template.api.test.js @@ -4,7 +4,7 @@ require('../../app-bootstrap') const config = require('config') -const uuid = require('uuid/v4') +const { v4: uuid } = require('uuid'); const chai = require('chai') const chaiHttp = require('chai-http') const app = require('../../app') diff --git a/test/prepare.js b/test/prepare.js index aa5b0ef..2e1a005 100644 --- a/test/prepare.js +++ b/test/prepare.js @@ -7,29 +7,31 @@ process.env.NODE_ENV = 'test' const prepare = require('mocha-prepare') const config = require('config') -const AWS = require('aws-sdk') +const { S3Client, HeadBucketCommand, CreateBucketCommand } = require('@aws-sdk/client-s3') /* * Initialize an S3 bucket. */ async function initBucket () { - const s3 = new AWS.S3() + const s3 = new S3Client({ + region: config.AMAZON.AWS_REGION, + endpoint: config.S3_ENDPOINT, + credentials: { + accessKeyId: config.AMAZON.AWS_ACCESS_KEY_ID, + secretAccessKey: config.AMAZON.AWS_SECRET_ACCESS_KEY + }, + forcePathStyle: true, + tls: false + }) + try { - await s3.headBucket({ Bucket: config.AMAZON.ATTACHMENT_S3_BUCKET }).promise() + await s3.send(new HeadBucketCommand({ Bucket: config.AMAZON.ATTACHMENT_S3_BUCKET })) } catch (err) { - await s3.createBucket({ Bucket: config.AMAZON.ATTACHMENT_S3_BUCKET }).promise() + await s3.send(new CreateBucketCommand({ Bucket: config.AMAZON.ATTACHMENT_S3_BUCKET })) } } prepare(function (done) { - AWS.config.update({ - accessKeyId: config.AMAZON.AWS_ACCESS_KEY_ID, - secretAccessKey: config.AMAZON.AWS_SECRET_ACCESS_KEY, - region: config.AMAZON.AWS_REGION, - endpoint: config.S3_ENDPOINT, - sslEnabled: false, - s3ForcePathStyle: true - }) initBucket() .then(result => { done() diff --git a/test/testHelper.js b/test/testHelper.js index ff96c9b..c480bc6 100644 --- a/test/testHelper.js +++ b/test/testHelper.js @@ -2,7 +2,7 @@ * This file defines common helper methods used for tests */ const _ = require('lodash') -const uuid = require('uuid/v4') +const { v4: uuid } = require('uuid'); const { ChallengeStatusEnum } = require('../src/common/prisma') const prisma = require('../src/common/prisma').getClient() const jwt = require('jsonwebtoken') diff --git a/test/unit/AttachmentService.test.js b/test/unit/AttachmentService.test.js index 4673c07..afcf0bd 100644 --- a/test/unit/AttachmentService.test.js +++ b/test/unit/AttachmentService.test.js @@ -5,9 +5,11 @@ require('../../app-bootstrap') const fs = require('fs') const path = require('path') -const uuid = require('uuid/v4') +const { v4: uuid } = require('uuid'); const chai = require('chai') -const awsMock = require('aws-sdk-mock') +const { mockClient } = require('aws-sdk-client-mock') +const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3') +const { Readable } = require('stream') const service = require('../../src/services/AttachmentService') const testHelper = require('../testHelper') const prisma = require('../../src/common/prisma').getClient() @@ -16,6 +18,9 @@ const should = chai.should() const attachmentContent = fs.readFileSync(path.join(__dirname, '../attachment.txt')) +// Create S3 mock client +const s3Mock = mockClient(S3Client) + describe('attachment service unit tests', () => { // created attachment id let id @@ -26,10 +31,12 @@ describe('attachment service unit tests', () => { const notFoundId = uuid() before(async () => { - // mock S3 before creating S3 instance - awsMock.mock('S3', 'getObject', (params, callback) => { - callback(null, { Body: Buffer.from(attachmentContent) }); + // mock S3 GetObject command + s3Mock.on(GetObjectCommand).resolves({ + Body: Readable.from([attachmentContent]), + ContentType: 'text/plain' }); + await testHelper.createData() data = testHelper.getData() // create attachment @@ -60,8 +67,8 @@ describe('attachment service unit tests', () => { after(async () => { await testHelper.clearData() await prisma.attachment.deleteMany({ where: { id }}) - // restore S3 - awsMock.restore('S3'); + // reset S3 mock + s3Mock.reset() }) describe('download attachment tests', () => { diff --git a/test/unit/AuditLogService.test.js b/test/unit/AuditLogService.test.js index 1e61cc7..debb347 100644 --- a/test/unit/AuditLogService.test.js +++ b/test/unit/AuditLogService.test.js @@ -4,7 +4,7 @@ require('../../app-bootstrap') const _ = require('lodash') -const uuid = require('uuid/v4') +const { v4: uuid } = require('uuid'); const chai = require('chai') const service = require('../../src/services/AuditLogService') const prisma = require('../../src/common/prisma').getClient() diff --git a/test/unit/ChallengePhaseService.test.js b/test/unit/ChallengePhaseService.test.js index 0c0ac5c..df2af9d 100644 --- a/test/unit/ChallengePhaseService.test.js +++ b/test/unit/ChallengePhaseService.test.js @@ -9,7 +9,7 @@ require('../../app-bootstrap') const chai = require('chai') const config = require('config') const { Prisma } = require('@prisma/client') -const uuid = require('uuid/v4') +const { v4: uuid } = require('uuid'); const { getReviewClient } = require('../../src/common/review-prisma') const prisma = require('../../src/common/prisma').getClient() const service = require('../../src/services/ChallengePhaseService') diff --git a/test/unit/ChallengeService.test.js b/test/unit/ChallengeService.test.js index a27c06d..ec83ddb 100644 --- a/test/unit/ChallengeService.test.js +++ b/test/unit/ChallengeService.test.js @@ -9,7 +9,7 @@ if (!process.env.REVIEW_DB_URL && process.env.DATABASE_URL) { require('../../app-bootstrap') const _ = require('lodash') const config = require('config') -const uuid = require('uuid/v4') +const { v4: uuid } = require('uuid'); const chai = require('chai') const constants = require('../../app-constants') const service = require('../../src/services/ChallengeService') diff --git a/test/unit/ChallengeTypeService.test.js b/test/unit/ChallengeTypeService.test.js index 16f927b..36c56a8 100644 --- a/test/unit/ChallengeTypeService.test.js +++ b/test/unit/ChallengeTypeService.test.js @@ -3,7 +3,7 @@ */ require('../../app-bootstrap') -const uuid = require('uuid/v4') +const { v4: uuid } = require('uuid'); const chai = require('chai') const service = require('../../src/services/ChallengeTypeService') diff --git a/test/unit/PhaseService.test.js b/test/unit/PhaseService.test.js index b74ac10..9211476 100644 --- a/test/unit/PhaseService.test.js +++ b/test/unit/PhaseService.test.js @@ -3,7 +3,7 @@ */ require('../../app-bootstrap') -const uuid = require('uuid/v4') +const { v4: uuid } = require('uuid'); const chai = require('chai') const service = require('../../src/services/PhaseService') diff --git a/test/unit/TimelineTemplateService.test.js b/test/unit/TimelineTemplateService.test.js index 02ff3b7..390bce1 100644 --- a/test/unit/TimelineTemplateService.test.js +++ b/test/unit/TimelineTemplateService.test.js @@ -3,7 +3,7 @@ */ require('../../app-bootstrap') -const uuid = require('uuid/v4') +const { v4: uuid } = require('uuid'); const chai = require('chai') const service = require('../../src/services/TimelineTemplateService') const prisma = require('../../src/common/prisma').getClient()