diff --git a/.circleci/config.yml b/.circleci/config.yml index 1440d2b0..f2f9c6cb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,92 +1,401 @@ -# version: 2.17 -# jobs: -# build: -# docker: -# - image: circleci/node:10.0.0 -# working_directory: ~/project-service/ -# steps: -# - checkout -# - restore_cache: -# key: dependency-cache-{{ checksum "package.json" }} -# - run: -# name: update-npm -# command: 'sudo npm install -g npm@latest' -# - run: -# name: install-npm -# command: npm install -# - save_cache: -# key: dependency-cache-{{checksum "package.json"}} -# paths: -# - ./node_modules -# - run: -# name: Executing unit test cases -# command: npm install nyc && npm run coverage - -# - run: -# name: Install sonar scanner -# command: 'sudo npm install -g sonarqube-scanner' - -# - run: -# name: Sonar scanner -# command: | -# sonar-scanner -# workflows: -# version: 2 -# build_and_test: -# jobs: -# - build -version: 2.1 # CircleCI version -orbs: - sonarcloud: sonarsource/sonarcloud@1.1.1 +version: 2.1 + +# ───────────────────────────────────────────────────────────────────────────── +# Reusable commands +# ───────────────────────────────────────────────────────────────────────────── +commands: + wait_for_port: + description: 'Poll a TCP port until it accepts connections (max 90 s)' + parameters: + port: + type: integer + label: + type: string + steps: + - run: + name: 'Wait for << parameters.label >> on port << parameters.port >>' + command: | + echo "Waiting for << parameters.label >> (port << parameters.port >>)..." + for i in $(seq 1 30); do + nc -z localhost << parameters.port >> 2>/dev/null && echo "<< parameters.label >> is up." && exit 0 + echo " attempt $i/30 – not ready, retrying in 3 s..." + sleep 3 + done + echo "ERROR: << parameters.label >> did not start within 90 s." >&2 + exit 1 + + wait_for_http: + description: 'Poll an HTTP endpoint until it returns HTTP 2xx (max 90 s)' + parameters: + url: + type: string + label: + type: string + steps: + - run: + name: 'Wait for << parameters.label >> at << parameters.url >>' + command: | + echo "Waiting for << parameters.label >> at << parameters.url >>..." + for i in $(seq 1 30); do + curl -sf --max-time 3 "<< parameters.url >>" 2>/dev/null && echo "<< parameters.label >> is up." && exit 0 + echo " attempt $i/30 – not ready, retrying in 3 s..." + sleep 3 + done + echo "ERROR: << parameters.label >> did not become healthy within 90 s." >&2 + exit 1 + +# ───────────────────────────────────────────────────────────────────────────── +# Jobs +# ───────────────────────────────────────────────────────────────────────────── jobs: - build: - machine: #Linux machine instead of docker environment - image: ubuntu-2004:202111-01 + # ─────────────────────────────────────────────────────────────────────────── + # Full integration test + # + # Dependency graph (all on localhost): + # + # [Zookeeper :2181] + # │ + # [Kafka :9092] [MongoDB :27017] [Redis :6379] [PostgreSQL :5432] + # │ │ │ + # ┌─────┴────────────────┴──────────────────┤ + # │ user-service :7001 │ + # │ entity-management :5002 │ + # └─────────────────────────────────────────┘ + # │ + # [interface-service :3567] ← routes all inter-service HTTP traffic + # │ + # [project-service :5003] ← service under test + # + # ─────────────────────────────────────────────────────────────────────────── + integration-test: + machine: + image: ubuntu-2204:current docker_layer_caching: true - working_directory: ~/project-service # Default working directory + working_directory: ~/project-service + steps: - checkout: path: ~/project-service + + # ── Use Node 20 for all services ───────────────────────────────────── + - run: + name: Switch to Node 20 + command: | + source ~/.nvm/nvm.sh + nvm install 20 + nvm use 20 + nvm alias default 20 + node --version + echo "export NVM_DIR=\"$HOME/.nvm\"" >> $BASH_ENV + echo "source \"\$NVM_DIR/nvm.sh\"" >> $BASH_ENV + echo "nvm use 20 --silent" >> $BASH_ENV + + # ── Install project-service dependencies ──────────────────────────── - restore_cache: - key: project-service-dependency-cache-{{ checksum "package.json" }} + keys: + - project-deps-v1-{{ checksum "package.json" }} + - project-deps-v1- + - run: - name: Install dependencies + name: Install project-service dependencies command: npm install + - save_cache: - key: project-service-dependency-cache-{{checksum "package.json"}} + key: project-deps-v1-{{ checksum "package.json" }} paths: - - ./node_modules + - node_modules + + # ── Clone dependent microservices (always from develop) ────────────── - run: - name: Executing unit test cases - command: npm test -- --collectCoverage --collectCoverageFrom="modules/helper/*" - - store_artifacts: - path: coverage/ - destination: /coverage/ - - sonarcloud/scan + name: Clone user-service + command: | + git clone --depth 1 --branch develop \ + https://github.com/ELEVATE-Project/user.git \ + ~/services/user + + - run: + name: Clone entity-management service + command: | + git clone --depth 1 --branch develop \ + https://github.com/ELEVATE-Project/entity-management.git \ + ~/services/entity-management + + - run: + name: Clone interface-service + command: | + git clone --depth 1 --branch develop \ + https://github.com/ELEVATE-Project/interface-service.git \ + ~/services/interface-service + + # ── Install dependencies for each dependent service ────────────────── + - restore_cache: + keys: + - user-deps-v1-{{ checksum "/root/services/user/src/package.json" }} + - user-deps-v1- + - run: + name: Install user-service dependencies + command: cd ~/services/user/src && npm install + - save_cache: + key: user-deps-v1-{{ checksum "/root/services/user/src/package.json" }} + paths: + - /root/services/user/src/node_modules + + - restore_cache: + keys: + - entity-deps-v1-{{ checksum "/root/services/entity-management/src/package.json" }} + - entity-deps-v1- + - run: + name: Install entity-management dependencies + command: cd ~/services/entity-management/src && npm install + - save_cache: + key: entity-deps-v1-{{ checksum "/root/services/entity-management/src/package.json" }} + paths: + - /root/services/entity-management/src/node_modules + + - restore_cache: + keys: + - interface-deps-v1-{{ checksum "/root/services/interface-service/src/package.json" }} + - interface-deps-v1- + - run: + name: Install interface-service dependencies + command: cd ~/services/interface-service/src && npm install + - save_cache: + key: interface-deps-v1-{{ checksum "/root/services/interface-service/src/package.json" }} + paths: + - /root/services/interface-service/src/node_modules + + # ── Infrastructure: all services via docker-compose ────────────────── - run: - name: Checking prerequisites - command: |- - docker-compose --version + name: Start infrastructure (docker-compose) + command: | + docker-compose -f ~/project-service/dev-ops/docker-compose.yml up -d + + - wait_for_port: + label: Zookeeper + port: 2181 + + - wait_for_port: + label: Kafka + port: 9092 + - run: - name: Starting the docker containers - command: |- - cd dev-ops/ && docker-compose up -d + name: Wait for MongoDB + command: | + echo "Waiting for MongoDB..." + for i in $(seq 1 30); do + docker exec mongo mongo --eval "db.adminCommand({ping:1})" --quiet 2>/dev/null \ + && echo "MongoDB is up." && exit 0 + echo " attempt $i/30 – not ready, retrying in 3 s..." + sleep 3 + done + echo "ERROR: MongoDB did not start in time." >&2; exit 1 + - run: - name: Running test cases - command: |- + name: Wait for Redis + command: | + for i in $(seq 1 15); do + docker exec redis redis-cli ping 2>/dev/null | grep -q PONG \ + && echo "Redis is up." && exit 0 + sleep 2 + done + echo "ERROR: Redis did not start in time." >&2; exit 1 + + - run: + name: Wait for PostgreSQL + command: | + echo "Waiting for PostgreSQL..." + for i in $(seq 1 20); do + docker exec postgres pg_isready -U postgres 2>/dev/null \ + && echo "PostgreSQL is up." && exit 0 + echo " attempt $i/20 – not ready, retrying in 2 s..." + sleep 2 + done + echo "ERROR: PostgreSQL did not start in time." >&2; exit 1 + + - wait_for_port: + label: Gotenberg + port: 3003 + + # ── Create log directory before any background service starts ─────────── + - run: + name: Create log directory + command: mkdir -p /tmp/logs + + # ── Configure environments and run DB migrations ────────────────────── + # Cloud storage credentials (CLOUD_STORAGE_PROVIDER, CLOUD_STORAGE_SECRET, + # CLOUD_STORAGE_ACCOUNTNAME, CLOUD_STORAGE_BUCKETNAME, PUBLIC_ASSET_BUCKETNAME) + # must be set as CircleCI environment variables (Project Settings → Env Vars + # or a Context). They are injected into process.env at runtime and take + # priority over the .env file values automatically via dotenv first-wins. + - run: + name: Configure user-service environment + command: cp ~/project-service/dev-ops/user_integration_test.env ~/services/user/src/.env + + - run: + name: Inject cloud credentials into user-service environment + command: | + # Prepend secrets to .env so they take priority (dotenv first-wins). + # CLOUD_STORAGE_SECRET is a multi-line PEM — CircleCI injects it as a + # shell env var automatically; dotenv will not override a pre-set var. + { + printf 'AWS_ACCESS_KEY_ID=%s\n' "${AWS_ACCESS_KEY_ID}" + printf 'AWS_BUCKET_ENDPOINT=%s\n' "${AWS_BUCKET_ENDPOINT}" + printf 'AWS_BUCKET_REGION=%s\n' "${AWS_BUCKET_REGION}" + printf 'AWS_SECRET_ACCESS_KEY=%s\n' "${AWS_SECRET_ACCESS_KEY}" + printf 'AZURE_ACCOUNT_KEY=%s\n' "${AZURE_ACCOUNT_KEY}" + printf 'AZURE_ACCOUNT_NAME=%s\n' "${AZURE_ACCOUNT_NAME}" + printf 'CLOUD_STORAGE=%s\n' "${CLOUD_STORAGE}" + printf 'CLOUD_STORAGE_ACCOUNTNAME=%s\n' "${CLOUD_STORAGE_ACCOUNTNAME}" + printf 'CLOUD_STORAGE_BUCKETNAME=%s\n' "${CLOUD_STORAGE_BUCKETNAME}" + printf 'CLOUD_STORAGE_BUCKET_TYPE=%s\n' "${CLOUD_STORAGE_BUCKET_TYPE}" + printf 'CLOUD_STORAGE_PROJECT=%s\n' "${CLOUD_STORAGE_PROJECT}" + printf 'CLOUD_STORAGE_PROVIDER=%s\n' "${CLOUD_STORAGE_PROVIDER}" + } | cat - ~/services/user/src/.env > /tmp/user_env_tmp \ + && mv /tmp/user_env_tmp ~/services/user/src/.env + + - run: + name: Run user-service database migrations + command: | + cd ~/services/user/src + npm run db:init || true + npm run db:migrate + npm run db:seed:all || true + + - run: + name: Configure entity-management environment + command: cp ~/project-service/dev-ops/entity_integration_test.env ~/services/entity-management/src/.env + + - run: + name: Configure project-service environment + command: cp ~/project-service/dev-ops/project_integration_test.env ~/project-service/.env + + - run: + name: Inject cloud credentials into project-service environment + command: | + # Prepend secrets to .env so they take priority (dotenv first-wins). + # CLOUD_STORAGE_SECRET is a multi-line PEM — CircleCI injects it as a + # shell env var automatically; dotenv will not override a pre-set var. + { + printf 'CLOUD_STORAGE=%s\n' "${CLOUD_STORAGE}" + printf 'CLOUD_STORAGE_ACCOUNTNAME=%s\n' "${CLOUD_STORAGE_ACCOUNTNAME}" + printf 'CLOUD_STORAGE_BUCKETNAME=%s\n' "${CLOUD_STORAGE_BUCKETNAME}" + printf 'CLOUD_STORAGE_BUCKET_TYPE=%s\n' "${CLOUD_STORAGE_BUCKET_TYPE}" + printf 'CLOUD_STORAGE_PROJECT=%s\n' "${CLOUD_STORAGE_PROJECT}" + printf 'CLOUD_STORAGE_PROVIDER=%s\n' "${CLOUD_STORAGE_PROVIDER}" + } | cat - ~/project-service/.env > /tmp/project_env_tmp \ + && mv /tmp/project_env_tmp ~/project-service/.env + + # ── Start user, entity, project in parallel ─────────────────────────── + # interface-service fetches API docs from these at startup, so they must + # be healthy before interface-service is launched. + - run: + name: Start user-service + command: | + cd ~/services/user/src + npx nodemon app.js >> /tmp/logs/user-service.log 2>&1 + background: true + environment: + NODE_ENV: development + + - run: + name: Start entity-management service + command: | + cd ~/services/entity-management/src + npx nodemon app.js >> /tmp/logs/entity-service.log 2>&1 + background: true + environment: + NODE_ENV: development + + - run: + name: Start project-service + command: | + cd ~/project-service + npx nodemon app.js >> /tmp/logs/project-service.log 2>&1 + background: true + environment: + NODE_ENV: development + + - wait_for_http: + label: user-service + url: http://localhost:7001/health + + - wait_for_http: + label: entity-management + url: http://localhost:5002/health + + - wait_for_http: + label: project-service + url: http://localhost:5003/health + + # ── Start interface-service after dependencies are healthy ───────────── + - run: + name: Configure interface-service environment + command: cp ~/project-service/dev-ops/interface_integration_test.env ~/services/interface-service/src/.env + + - run: + name: Start interface-service + command: | + cd ~/services/interface-service/src + node app.js >> /tmp/logs/interface-service.log 2>&1 + background: true + environment: + NODE_ENV: development + + - wait_for_port: + label: interface-service + port: 3567 + + # ── Run integration tests ───────────────────────────────────────────── + - run: + name: Run project-service integration tests + command: | + mkdir -p dev-ops/report npm run test:integration + environment: + MOCHA_FILE: dev-ops/report/integration-results.xml + INTERFACE_SERVICE_URL: http://localhost:3567 + PROJECT_SERVICE_URL: http://localhost:5003 + USER_SERVICE_URL: http://localhost:7001 + ENTITY_SERVICE_URL: http://localhost:5002 + USER_INTERNAL_ACCESS_TOKEN: internal-access-token + ADMIN_SECRET_CODE: W5bF7gesuS0xsNWmpsKy + no_output_timeout: 5m + + # ── Collect logs on failure ─────────────────────────────────────────── + - run: + name: Collect service logs + when: always + command: | + mkdir -p /tmp/logs + echo "=== user-service ===" >> /tmp/logs/all-services.log + cat /tmp/logs/user-service.log >> /tmp/logs/all-services.log 2>/dev/null || true + echo "=== entity-management ===" >> /tmp/logs/all-services.log + cat /tmp/logs/entity-service.log >> /tmp/logs/all-services.log 2>/dev/null || true + echo "=== interface-service ===" >> /tmp/logs/all-services.log + cat /tmp/logs/interface-service.log >> /tmp/logs/all-services.log 2>/dev/null || true + echo "=== project-service ===" >> /tmp/logs/all-services.log + cat /tmp/logs/project-service.log >> /tmp/logs/all-services.log 2>/dev/null || true + + - store_artifacts: + path: /tmp/logs + destination: service-logs + + # Requires JUnit XML output (see test:integration script note above) - store_test_results: - path: ./dev-ops/report + path: dev-ops/report + + - store_artifacts: + path: dev-ops/report + destination: test-report +# ───────────────────────────────────────────────────────────────────────────── +# Workflows +# ───────────────────────────────────────────────────────────────────────────── workflows: - build-and-test: # This is the name of the workflow, - # Inside the workflow, you define the jobs you want to run. + ci: jobs: - - build: - context: - - SonarCloud + - integration-test: filters: - tags: + branches: only: - develop -#test commit \ No newline at end of file diff --git a/dev-ops/docker-compose.yml b/dev-ops/docker-compose.yml index 15e44ee6..42490738 100644 --- a/dev-ops/docker-compose.yml +++ b/dev-ops/docker-compose.yml @@ -1,71 +1,77 @@ -version: '3' +version: '3.8' + +# Integration-test infrastructure for project-service. +# All services bind only the ports needed by the Node.js processes running on +# the host. Port mappings use non-default host ports where a long-running +# instance already occupies the default port on the developer's machine. +# +# Usage (local): +# docker-compose -f project-service/dev-ops/docker-compose.yml up -d +# docker-compose -f project-service/dev-ops/docker-compose.yml down +# +# Usage (CI): docker-compose up -d (run from the dev-ops directory) + +networks: + elevate_net: + name: elevate_net + driver: bridge + services: + zookeeper: + image: confluentinc/cp-zookeeper:7.3.0 + container_name: zookeeper + networks: [elevate_net] + ports: + - '2181:2181' + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + kafka: - image: 'confluentinc/cp-kafka:7.3.0' + image: confluentinc/cp-kafka:7.3.0 container_name: kafka + networks: [elevate_net] ports: - '9092:9092' environment: KAFKA_BROKER_ID: 1 KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://kafka:9093 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true' + KAFKA_LOG_RETENTION_HOURS: 1 depends_on: - zookeeper - networks: - - elevate_net - logging: - driver: none + mongo: - image: 'mongo:4.1.4' + image: mongo:4.4 container_name: mongo - restart: 'always' - command: - - '--logpath' - - '/var/log/mongodb/mongod.log' + networks: [elevate_net] ports: - '27017:27017' - networks: - - elevate_net - volumes: - - mongo-data:/data/db - - logs:/var/log/mongodb + redis: - image: 'redis:7.0.0' + image: redis:7.0.0 container_name: redis - restart: 'always' + networks: [elevate_net] ports: - '6379:6379' - networks: - - elevate_net - logging: - driver: none - project: - build: '.' - # image: elevate-unnati - # image: shikshalokamqa/elevate-samiksha-service:0.0.6 - container_name: project + + postgres: + image: postgres:14-alpine + container_name: postgres + networks: [elevate_net] ports: - - '5003:5003' - volumes: - - .:/var/src - command: ['nodemon', 'app.js'] + - '5432:5432' environment: - - MONGODB_URL=mongodb://mongo:27017/elevate-project - env_file: - - integration_test.env - depends_on: - - kafka - - mongo - - redis - networks: - - elevate_net - -networks: - elevate_net: - external: false -volumes: - mongo-data: - logs: + POSTGRES_USER: postgres + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_DB: user + + gotenberg: + image: gotenberg/gotenberg:7 + container_name: gotenberg + networks: [elevate_net] + ports: + - '3000:3000' diff --git a/dev-ops/entity_integration_test.env b/dev-ops/entity_integration_test.env new file mode 100644 index 00000000..c2cb9080 --- /dev/null +++ b/dev-ops/entity_integration_test.env @@ -0,0 +1,21 @@ +ACCESS_TOKEN_EXPIRY=1440h +ACCESS_TOKEN_SECRET=hsghasghjab1273JHajnbabsjdj1273981273jhajksdh8y3123yhjkah812398yhjqwe7617237yuhdhhdqwu271 +ADMIN_ACCESS_TOKEN=N0DM5NAwwCN5KNXKJwlwu6c0nQQt6Rcl +ADMIN_TOKEN_HEADER_NAME=admin-auth-token +API_DOC_URL=/entity-management/api-doc +APPLICATION_BASE_URL=/entity-management/ +APPLICATION_ENV=development +APPLICATION_HOST=localhost +APPLICATION_PORT=5002 +AUTH_METHOD=native +INTERFACE_SERVICE_URL=http://localhost:3567 +INTERNAL_ACCESS_TOKEN=Fqn0m0HQ0gXydRtBCg5l +IS_AUTH_TOKEN_BEARER=false +KAFKA_COMMUNICATIONS_ON_OFF=ON +KAFKA_GROUP_ID=dev.entity +KAFKA_HEALTH_CHECK_TOPIC=entity-health-check-topic-check +KAFKA_URL=localhost:9092 +MONGODB_URL=mongodb://localhost:27017/entity +SERVICE_NAME=EntityManagementService +USER_SERVICE_BASE_URL=/user +USER_SERVICE_URL=http://localhost:3567 \ No newline at end of file diff --git a/dev-ops/integration_test.env b/dev-ops/integration_test.env deleted file mode 100644 index 6b43156f..00000000 --- a/dev-ops/integration_test.env +++ /dev/null @@ -1,5 +0,0 @@ -APPLICATION_PORT = 5003 -APPLICATION_ENV = development -INTERNAL_ACCESS_TOKEN = 8c3a94f0931e01a4940a -APPLICATION_BASE_URL=/project/ -MONGODB_URL = mongodb://localhost:27017/elevate-project diff --git a/dev-ops/interface_integration_test.env b/dev-ops/interface_integration_test.env new file mode 100644 index 00000000..9b114068 --- /dev/null +++ b/dev-ops/interface_integration_test.env @@ -0,0 +1,26 @@ +API_DOC_URL=/interface/api-doc +APPLICATION_ENV=development +APPLICATION_PORT=3567 +ELEVATE_NOTIFICATION_KAFKA_BROKERS=localhost:9092 +ELEVATE_NOTIFICATION_KAFKA_GROUP_ID=dev.mentoring +ELEVATE_NOTIFICATION_KAFKA_TOPIC=dev.notification +ENTITY_SERVICE_BASE_URL=http://localhost:5002 +INSTALLED_PACKAGES=elevate-mentoring elevate-survey-observation@1.0.1 elevate-self-creation-portal +MENTORING_SERVICE_BASE_URL=http://localhost:7101 +NOTIFICATION_SERVICE_BASE_URL=http://localhost:7201 +PROJECT_SERVICE_BASE_URL=http://localhost:5003 +RATE_LIMITER_ENABLED=true +RATE_LIMITER_GENERAL_LIMIT=500 +RATE_LIMITER_NUMBER_OF_PROXIES=3 +RATE_LIMITER_PUBLIC_LOW_LIMIT=5 +REQUIRED_BASE_PACKAGES=elevate-mentoring elevate-project elevate-survey-observation self-creation-portal +REQUIRED_PACKAGES=elevate-mentoring@1.2.95 elevate-survey-observation@3.4.2 elevate-project@3.4.1 shiksha-notification@1.1.3 elevate-self-creation-portal@1.0.70 +ROUTE_CONFIG_JSON_URLS_PATHS=https://raw.githubusercontent.com/ELEVATE-Project/utils/refs/heads/develop/interface-routes/elevate-routes.json +SAAS_NOTIFICATION_BASE_URL=interface/v1/notification/send-raw +SAAS_NOTIFICATION_SEND_EMAIL_ROUTE=interface/v1/notification/send-raw +SAMIKSHA_SERVICE_BASE_URL=http://localhost:5007 +SCHEDULER_SERVICE_BASE_URL=http://localhost:7401 +SELF-CREATION-PORTAL_SERVICE_BASE_URL=http://localhost:6001 +SUPPORTED_HTTP_TYPES=GET POST PUT PATCH DELETE +SURVEY_SERVICE_BASE_URL=http://localhost:5007 +USER_SERVICE_BASE_URL=http://localhost:7001 \ No newline at end of file diff --git a/dev-ops/project_integration_test.env b/dev-ops/project_integration_test.env new file mode 100644 index 00000000..4319c6fd --- /dev/null +++ b/dev-ops/project_integration_test.env @@ -0,0 +1,62 @@ +ACCESS_TOKEN_EXPIRY=1440h +ACCESS_TOKEN_SECRET=hsghasghjab1273JHajnbabsjdj1273981273jhajksdh8y3123yhjkah812398yhjqwe7617237yuhdhhdqwu271 +ADMIN_ACCESS_TOKEN=N0DM5NAwwCN5KNXKJwlwu6c0nQQt6Rcl +ADMIN_AUTH_TOKEN=N0DM5NAwwCN5KNXKJwlwu6c0nQQt6Rcl +ADMIN_TOKEN_HEADER_NAME=admin-auth-token +API_DOC_URL=/project/api-doc +APPLICATION_BASE_URL=/project/ +APPLICATION_ENV=development +APPLICATION_HOST=localhost +APPLICATION_PORT=5003 +APP_PORTAL_BASE_URL=https://dev.elevate-ml.shikshalokam.org +AUTH_CONFIG_FILE_PATH=config.json +AUTH_METHOD=native + +CLOUD_STORAGE= +CLOUD_STORAGE_ACCOUNTNAME= +CLOUD_STORAGE_BUCKETNAME= +CLOUD_STORAGE_BUCKET_TYPE= +CLOUD_STORAGE_PROJECT= +CLOUD_STORAGE_PROVIDER= +CLOUD_STORAGE_SECRET= + +DEFAULT_ORGANISATION_CODE=default_code +DOWNLOADABLE_URL_EXPIRY_IN_SECONDS=300 +ELEVATE_PROJECT_SERVICE_URL=https://dev.elevate-apis.shikshalokam.org +ENABLE_REFLECTION=false +ENTITY_BASE_URL=http://localhost:3567 +ENTITY_MANAGEMENT_SERVICE_BASE_URL=/entity-management +ENTITY_MONGODB_URL=mongodb://localhost:27017/entity +GOTENBERG_URL=http://localhost:3000 +INTERFACE_SERVICE_URL=http://localhost:3567 +INTERNAL_ACCESS_TOKEN=Fqn0m0HQ0gXydRtBCg5l +IS_AUTH_TOKEN_BEARER=false +KAFKA_COMMUNICATIONS_ON_OFF=ON +KAFKA_GROUP_ID=dev.projects +KAFKA_HEALTH_CHECK_TOPIC=project-health-check-topic-check +KAFKA_URL=localhost:9092 +MONGODB_URL=mongodb://localhost:27017/project +ORGANIZATION_EXTENSION_TOPIC=elevate_project_org_extension_event_listener +ORG_ID_HEADER_NAME=Org-id +ORG_UPDATES_TOPIC=dev.organizationEvent +PRESIGNED_URL_EXPIRY_IN_SECONDS=300 +PROGRAM_USER_MAPPING_TOPIC=dev.program +PROJECT_SUBMISSION_TOPIC=elevate.improvement.project.submission.dev +SERVICE_NAME=project +SESSION_VERIFICATION_METHOD=user_service_authenticated +SOURCE_MONGODB_URL=mongodb://localhost:27017/project +SUBMISSION_LEVEL=USER +SUBMISSION_TOPIC=elevate_project_task_submissions_dev +SURVEY_MONGODB_URL=mongodb://localhost:27017/survey +SURVEY_SERVICE_URL=http://localhost:3567/survey +TENANT_CACHE_TTL=86400 +TIMEZONE_DIFFRENECE_BETWEEN_LOCAL_TIME_AND_UTC=+05:30 +USER_ACCOUNT_EVENT_TOPIC=dev.userCreate +USER_ACTIVITY_TOPIC=dev-user-activities +USER_COURSES_SUBMISSION_TOPIC=elevate_user_courses_dev +USER_COURSES_TOPIC=elevate_user_courses_raw +USER_DELETE_ON_OFF=ON +USER_DELETE_TOPIC=dev.userCreate +USER_SERVICE_BASE_URL=/user +USER_SERVICE_INTERNAL_ACCESS_TOKEN_HEADER_KEY=internal_access_token +VALIDATE_ENTITIES=ON \ No newline at end of file diff --git a/dev-ops/user_integration_test.env b/dev-ops/user_integration_test.env new file mode 100644 index 00000000..a5299587 --- /dev/null +++ b/dev-ops/user_integration_test.env @@ -0,0 +1,115 @@ +ACCESS_TOKEN_EXPIRY=1440h +ACCESS_TOKEN_SECRET=hsghasghjab1273JHajnbabsjdj1273981273jhajksdh8y3123yhjkah812398yhjqwe7617237yuhdhhdqwu271 +ADMIN_INVITEE_UPLOAD_EMAIL_TEMPLATE_CODE=invitee_upload_status +ADMIN_SECRET_CODE=W5bF7gesuS0xsNWmpsKy +ALLOWED_HOST=*,localhost +ALLOWED_IDLE_TIME=86400 +API_DOC_URL=/user/api-doc +APPLICATION_BASE_URL=/user +APPLICATION_ENV=development +APPLICATION_HOST=localhost +APPLICATION_PORT=7001 +APP_NAME=MentorED + + +AWS_ACCESS_KEY_ID= +AWS_BUCKET_ENDPOINT= +AWS_BUCKET_REGION= +AWS_SECRET_ACCESS_KEY= +AZURE_ACCOUNT_KEY= +AZURE_ACCOUNT_NAME= + +CAPTCHA_ENABLE=false +CAPTCHA_SERVICE=googleRecaptcha +CHANGE_PASSWORD_TEMPLATE_CODE=change_password +CLEAR_INTERNAL_CACHE=userinternal + +CLOUD_STORAGE= +CLOUD_STORAGE_ACCOUNTNAME= +CLOUD_STORAGE_BUCKETNAME= +CLOUD_STORAGE_BUCKET_TYPE= +CLOUD_STORAGE_PROJECT= +CLOUD_STORAGE_PROVIDER= +CLOUD_STORAGE_SECRET= + +DB_POOL_EVICT_MS=10000 +DB_POOL_MAX=20 +DB_POOL_MIN=4 +DEFAULT_AWS_BUCKET_NAME=mentoring-dev-storage +DEFAULT_AZURE_CONTAINER_NAME=mentoring-images +DEFAULT_GCP_BUCKET_NAME=mentoring-dev-storage-private +DEFAULT_ORGANISATION_CODE=default_code +DEFAULT_ORG_ID=1 +DEFAULT_QUEUE=defaultUser-queue +DEFAULT_ROLE=mentee +DEFAULT_TENANT_ORG_CODE=default_code +DEFAULT_TENANT_ORG_NAME=Default Organization +DEV_DATABASE_URL=postgres://postgres@localhost:5432/user +DISABLE_LOG=false +EMAIL_ID_ENCRYPTION_ALGORITHM=aes-256-cbc +EMAIL_ID_ENCRYPTION_IV=c9c7bd480494409071847264652f5c95 +EMAIL_ID_ENCRYPTION_KEY=eef7e009626c18724be86afa41a2620e0718561a508c61f92d7ee0377177ef7b +ENABLE_EMAIL_OTP_VERIFICATION=false +ENABLE_LOG=true +ENTITY_MANAGEMENT_SERVICE_BASE_URL=http://localhost:3567/entity-management +ERROR_LOG_LEVEL=silly +EVENT_ENABLE_ORG_EVENTS=false +EVENT_ENABLE_ORG_KAFKA_EVENTS=true +EVENT_ENABLE_TENANT_KAFKA_EVENTS=true +EVENT_ENABLE_USER_EVENTS=false +EVENT_ENABLE_USER_KAFKA_EVENTS=true +EVENT_ORGANIZATION_KAFKA_TOPIC=dev.organizationEvent +EVENT_ORG_LISTENER_URLS=http://localhost:3567/mentoring/v1/organization/eventListener +EVENT_TENANT_KAFKA_TOPIC=dev.tenantEvent +EVENT_USER_KAFKA_TOPIC=dev.userCreate +EVENT_USER_LISTENER_API=http://localhost:3567/mentoring/v1/users/add +GCP_PATH=gcp.json +GCP_PROJECT_ID=sl-dev-project +GENERIC_INVITATION_EMAIL_TEMPLATE_CODE=generic_invite +GOOGLE_RECAPTCHA_HOST=https://www.google.com +GOOGLE_RECAPTCHA_URL=/recaptcha/api/siteverify +INTERFACE_SERVICE_HOST=http://localhost:3567 +INTERNAL_ACCESS_TOKEN=Fqn0m0HQ0gXydRtBCg5l +INTERNAL_CACHE_EXP_TIME=86400 +INVITEE_EMAIL_TEMPLATE_CODE=invite_user +IS_AUTH_TOKEN_BEARER=false +IV=LHYOA5YnTonqcgrm15k3/Q== +JWT_SECRET=xG7V87nfj2394fJD394jfgh08dfDfh98DKJlndfj9d +KAFKA_GROUP_ID=dev.users +KAFKA_HEALTH_CHECK_TOPIC=user-health-check-topic-check +KAFKA_TOPIC=dev.topic +KAFKA_URL=localhost:9092 +KEY=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz +MENTEE_INVITATION_EMAIL_TEMPLATE_CODE=invite_mentee +MENTORING_SERVICE_URL=http://localhost:3567 +MENTOR_INVITATION_EMAIL_TEMPLATE_CODE=invite_mentor +MENTOR_REQUEST_ACCEPTED_EMAIL_TEMPLATE_CODE=mentor_request_accepted +MENTOR_REQUEST_REJECTED_EMAIL_TEMPLATE_CODE=mentor_request_rejected +MENTOR_SECRET_CODE=4567 +MONGODB_URL=mongodb://localhost:27017/user +NOTIFICATION_API_URL=https://saas-qa-interface.tekdinext.com/interface/v1/notification/send-raw +NOTIFICATION_KAFKA_TOPIC=dev.notification +NOTIFICATION_MODE=API +OLD_DEV_DATABASE_URL=postgres://shikshalokam:slpassword123@10.148.0.36:9700/saas_dev_user +ORG_ADMIN_INVITATION_EMAIL_TEMPLATE_CODE=invite_org_admin +OTP_EMAIL_TEMPLATE_CODE=emailotp +OTP_EXP_TIME=86400 +PORTAL_URL=https://dev.elevate-mentoring.shikshalokam.org/auth/login +PUBLIC_ASSET_BUCKETNAME=mentoring-dev-storage-public +RATING_KAFKA_TOPIC=dev.mentor_rating +RBAC_JWT_SECRET=3609eeeab5d80e873ddb1ffde5f181f540c4973bdbd8ef611f5e7271cdf2e03d +RECAPTCHA_SECRET_KEY=6LfWEKYpAAAAAJS0eukS2Su_5vwlXRs5vPbC34W0 +REDIS_HOST=redis://localhost:6379 +REFRESH_TOKEN_EXPIRY=7 +REFRESH_TOKEN_SECRET=371hkjkjady2y3ihdkajshdkiq23iuekw71yekhaskdvkvegavy23t78veqwexqvxveit6ttxyeeytt62tx236vv +REFRESH_VIEW_INTERVAL=30000 +REGISTRATION_EMAIL_TEMPLATE_CODE=registration +REGISTRATION_OTP_EMAIL_TEMPLATE_CODE=registrationotp +SALT_ROUNDS=10 +SAMPLE_CSV_FILE_PATH=sample/bulk_user_creation.csv +SCHEDULER_SERVICE_BASE_URL=/scheduler/ +SCHEDULER_SERVICE_ERROR_REPORTING_EMAIL_ID=rakesh.k@pacewisdom.com +SCHEDULER_SERVICE_HOST=http://localhost:3567 +SCHEDULER_SERVICE_URL=http://localhost:3567/jobs/scheduleJob +SERVICE_NAME=UserService +SIGNED_URL_EXPIRY_IN_SECONDS=900 \ No newline at end of file diff --git a/integration-tests/helpers/commonTests.js b/integration-tests/helpers/commonTests.js new file mode 100644 index 00000000..74198aa9 --- /dev/null +++ b/integration-tests/helpers/commonTests.js @@ -0,0 +1,75 @@ +'use strict' + +const axios = require('axios') + +const USER_SERVICE_URL = process.env.USER_SERVICE_URL || 'http://localhost:3001' +// Must match INTERNAL_ACCESS_TOKEN in user_integration_test.env +const USER_INTERNAL_ACCESS_TOKEN = process.env.USER_INTERNAL_ACCESS_TOKEN || 'internal-access-token' +// Must match ADMIN_SECRET_CODE in user_integration_test.env +const ADMIN_SECRET_CODE = process.env.ADMIN_SECRET_CODE || 'W5bF7gesuS0xsNWmpsKy' + +const TEST_ADMIN = { + name: 'Test Admin', + email: 'testadmin@default.org', + password: 'Test@1234', +} + +async function createAdminUser() { + const response = await axios.post( + `${USER_SERVICE_URL}/user/v1/admin/create`, + { + name: TEST_ADMIN.name, + email: TEST_ADMIN.email, + password: TEST_ADMIN.password, + secret_code: ADMIN_SECRET_CODE, + }, + { + headers: { + internal_access_token: USER_INTERNAL_ACCESS_TOKEN, + 'Content-Type': 'application/json', + }, + validateStatus: () => true, // don't throw on 4xx/5xx + } + ) + return response.data +} + +async function loginAdminUser() { + const response = await axios.post( + `${USER_SERVICE_URL}/user/v1/admin/login`, + { + email: TEST_ADMIN.email, + password: TEST_ADMIN.password, + }, + { + headers: { 'Content-Type': 'application/json' }, + } + ) + return response.data +} + +// Creates the admin user and logs in, returning the access token. +// Safe to call multiple times: treats ADMIN_USER_ALREADY_EXISTS as non-fatal. +// Call this in a before() hook in your test suite. +async function setupAdminUser() { + const createData = await createAdminUser() + const alreadyExists = + createData.message === 'ADMIN_USER_ALREADY_EXISTS' || (createData.result && createData.result.user) + + if (!alreadyExists && createData.responseCode === 'CLIENT_ERROR') { + throw new Error(`Admin user creation failed: ${createData.message}`) + } + + const loginData = await loginAdminUser() + return loginData.result.access_token +} + +module.exports = { + TEST_ADMIN, + USER_SERVICE_URL, + USER_INTERNAL_ACCESS_TOKEN, + ADMIN_SECRET_CODE, + createAdminUser, + loginAdminUser, + setupAdminUser, +} diff --git a/integration-tests/user.test.js b/integration-tests/user.test.js new file mode 100644 index 00000000..b789c1c0 --- /dev/null +++ b/integration-tests/user.test.js @@ -0,0 +1,97 @@ +'use strict' + +const axios = require('axios') +const { expect } = require('chai') +const { + TEST_ADMIN, + USER_SERVICE_URL, + USER_INTERNAL_ACCESS_TOKEN, + ADMIN_SECRET_CODE, + createAdminUser, + loginAdminUser, + setupAdminUser, +} = require('./helpers/commonTests') + +const TEST_USER = { + name: 'Test User', + email: 'testuser@localhost', + password: 'Test@1234', +} + +describe('User Creation Flow', function () { + let adminToken + + before(async function () { + adminToken = await setupAdminUser() + }) + + // ── Admin user ─────────────────────────────────────────────────────────── + + describe('Admin user', function () { + it('should create admin user or report already exists', async function () { + const data = await createAdminUser() + const isSuccess = data.result && data.result.user + const isAlreadyExists = data.message === 'ADMIN_USER_ALREADY_EXISTS' + expect(isSuccess || isAlreadyExists).to.equal(true) + }) + + it('should login admin user and return access token', async function () { + const data = await loginAdminUser() + expect(data.result).to.exist + expect(data.result.access_token).to.be.a('string').and.not.empty + }) + + it('before() should have resolved an admin token', function () { + expect(adminToken).to.be.a('string').and.not.empty + }) + }) + + // ── Regular user ───────────────────────────────────────────────────────── + // Origin header must be 'http://localhost' so the domain 'localhost' resolves + // to the default tenant in the tenant_domains table (seeded by migration). + + describe('Regular user', function () { + it('should create a regular user via user-service', async function () { + const response = await axios.post( + `${USER_SERVICE_URL}/user/v1/account/create`, + { + name: TEST_USER.name, + email: TEST_USER.email, + password: TEST_USER.password, + }, + { + headers: { + 'Content-Type': 'application/json', + Origin: 'http://localhost', + }, + validateStatus: () => true, + } + ) + + const data = response.data + const isSuccess = response.status === 201 || response.status === 200 + const isAlreadyExists = data.message === 'USER_ALREADY_EXISTS' || data.message === 'EMAIL_ID_EXISTS' + + expect(isSuccess || isAlreadyExists).to.equal(true, `Unexpected response: ${JSON.stringify(data)}`) + }) + + it('should login a regular user and return access token', async function () { + const response = await axios.post( + `${USER_SERVICE_URL}/user/v1/account/login`, + { + email: TEST_USER.email, + password: TEST_USER.password, + }, + { + headers: { + 'Content-Type': 'application/json', + Origin: 'http://localhost', + }, + } + ) + + expect(response.data.result).to.exist + expect(response.data.result.access_token).to.be.a('string').and.not.empty + }) + }) +}) diff --git a/package.json b/package.json index ebc8d3a0..c2360238 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "integration": "node app.js", "dev": "nodemon app.js", "prepare": "husky install", - "coverage": " nyc --reporter=lcov mocha --timeout 10000 test --exit" + "coverage": " nyc --reporter=lcov mocha --timeout 10000 test --exit", + "test:integration": "mocha --timeout 30000 integration-tests --recursive --exit --reporter mocha-junit-reporter" }, "repository": { "type": "git", @@ -95,6 +96,7 @@ "gulp-apidoc": "0.2.8", "husky": "8.0.3", "lint-staged": "12.5.0", + "mocha-junit-reporter": "2.2.1", "nodemon": "2.0.22", "prettier": "2.8.8" },