diff --git a/.env.sample b/.env.sample index fa6a0fabaa4..a3b839830fb 100644 --- a/.env.sample +++ b/.env.sample @@ -24,6 +24,7 @@ REFRESH_TOKEN_SECRET= # This environment variable is used to provide connection string of the mongoDB # database for talawa-api to connect to. +# Sample url for docker setup: mongodb://localhost:port/?replicaSet=rs0&directConnection=true MONGO_DB_URL= @@ -104,4 +105,7 @@ MINIO_DATA_DIR= # this environment variable is for setting the environment variable for Image Upload size -IMAGE_SIZE_LIMIT_KB=3000 \ No newline at end of file +IMAGE_SIZE_LIMIT_KB=3000 + +# This environment variable is for setting the working directory when Docker is used +PWD=. \ No newline at end of file diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index bae6336a0f9..baa00c3f2d9 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -313,6 +313,130 @@ jobs: with: path: './coverage/lcov.info' min_coverage: 95.0 + + Docker-Check: + name: Docker Check + needs: Test-Application + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.x' + + - name: Cache Node.js dependencies + uses: actions/cache@v4 + with: + path: | + ~/.npm + node_modules + key: ${{ runner.os }}-docker-check-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-docker-check- + + - name: Install Docker Compose + run: | + sudo apt-get update + sudo apt-get install -y docker-compose + + - name: Check if Talawa API starts in Docker + run: | + # Ensure no containers are running + docker-compose -f docker-compose.dev.yaml down -v || true + + # Verify docker-compose file exists + chmod +x .github/workflows/scripts/docker_file_check.sh + ./.github/workflows/scripts/docker_file_check.sh + + # Start containers + if ! timeout 300 docker-compose -f docker-compose.dev.yaml up -d --build; then + echo "Failed to start containers" + docker-compose -f docker-compose.dev.yaml logs + exit 1 + fi + + # Wait for MongoDB and Redis to be ready + echo "Waiting for MongoDB..." + timeout=30 + until docker-compose -f docker-compose.dev.yaml exec -T mongo mongosh --eval "db.runCommand({ ping: 1 }).ok">/dev/null 2>&1 || [ $timeout -eq 0 ]; do + echo "Waiting for MongoDB to be ready..." + sleep 1 + ((timeout--)) + done + if [ $timeout -eq 0 ]; then + echo "Error: MongoDB failed to start within timeout" + echo "Fetching MongoDB logs..." + docker-compose -f docker-compose.dev.yaml logs mongodb + echo "Shutting down MongoDB..." + docker-compose -f docker-compose.dev.yaml down -v + exit 1 + else + echo "MongoDB is ready!" + fi + + echo "Waiting for Redis..." + timeout=30 + until docker-compose -f docker-compose.dev.yaml exec -T redis-stack-server redis-cli ping >/dev/null 2>&1 || [ $timeout -eq 0 ]; do + sleep 1 + ((timeout--)) + done + if [ $timeout -eq 0 ]; then + echo "Error: Redis failed to start within timeout" + echo "Fetching Redis logs..." + docker-compose -f docker-compose.dev.yaml logs redis-stack-server + echo "Shutting down Redis..." + docker-compose -f docker-compose.dev.yaml down -v + exit 1 + else + echo "Redis is ready!" + fi + + # Wait for TALAWA API to be healthy + timeout=60 + until docker-compose -f docker-compose.dev.yaml exec -T talawa-api-dev curl -v -X OPTIONS "http://talawa-api-dev:4000/graphql" 2>&1 || [ $timeout -eq 0 ]; do + echo "Waiting for API to start... ($timeout seconds remaining)" + sleep 1 + ((timeout--)) + done + + if [ $timeout -eq 0 ]; then + echo "Error: API failed to start within timeout" + docker-compose -f docker-compose.dev.yaml logs + docker-compose -f docker-compose.dev.yaml down -v + exit 1 + fi + + echo "API started successfully" + + # Ensure cleanup runs even if the script fails + cleanup() { + local exit_code=$? + echo "Cleaning up containers..." + if ! docker-compose -f docker-compose.dev.yaml down -v; then + echo "Warning: Failed to cleanup containers" + fi + exit $exit_code + } + trap cleanup EXIT + env: + HEALTH_CHECK_URL: http://localhost:4000 + COMPOSE_PROJECT_NAME: pr-${{ github.event.pull_request.number }} + MONGO_DB_URL: mongodb://mongo:27017?replicaSet=rs0 + REDIS_HOST: redis-stack-server + REDIS_PORT: 6379 + ACCESS_TOKEN_SECRET: ${{ secrets.GITHUB_TOKEN }}_${{ github.run_id }}_${{ github.run_number }} + REFRESH_TOKEN_SECRET: ${{ secrets.GITHUB_TOKEN }}_${{ github.run_id }}_${{ github.run_attempt }} + LAST_RESORT_SUPERADMIN_EMAIL: "abc@gmail.com" + COLORIZE_LOGS: "true" + LOG_LEVEL: "info" + RECAPTCHA_SITE_KEY: ${{secrets.RECAPTCHA_SITE_KEY}} + RECAPTCHA_SECRET_KEY: ${{secrets.RECAPTCHA_SECRET_KEY}} + MAIL_USERNAME: ${{secrets.MAIL_USERNAME}} + MAIL_PASSWORD: ${{secrets.MAIL_PASSWORD}} Test-Builds: name: Test Development and Production Builds @@ -347,7 +471,7 @@ jobs: LAST_RESORT_SUPERADMIN_EMAIL: "abc@gmail.com" COLORIZE_LOGS: "true" LOG_LEVEL: "info" - + steps: - name: Checkout repository uses: actions/checkout@v4 diff --git a/.github/workflows/scripts/docker_file_check.sh b/.github/workflows/scripts/docker_file_check.sh new file mode 100755 index 00000000000..75cfc01e779 --- /dev/null +++ b/.github/workflows/scripts/docker_file_check.sh @@ -0,0 +1,13 @@ +#!/bin/bash +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +if ! command -v docker-compose &> /dev/null; then + echo "Error: docker-compose is not installed" + exit 1 +fi + +docker-compose -f "${REPO_ROOT}/docker-compose.dev.yaml" down -v || { + echo "Warning: Failed to stop containers. Continuing anyway..." +} + +echo "docker-compose.dev.yaml file found. Ready to proceed." \ No newline at end of file diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index 12907723b7f..6f7dd712004 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -1,14 +1,25 @@ -version: "3.8" - services: - mongodb: + mongo: image: mongo:latest + container_name: mongo + restart: unless-stopped + healthcheck: + test: mongosh --eval "try { rs.status().ok } catch (e) { 0 }" + interval: 30s + timeout: 5s + retries: 30 + deploy: + resources: + limits: + memory: 1G ports: - - 27017:27017 + - "127.0.0.1:27017:27017" volumes: - - mongodb-data:/data/db + - mongo_data:/data/db + - ./init-mongo.sh:/init-mongo.sh networks: - talawa-network + entrypoint: ["/bin/bash", "/init-mongo.sh"] redis-stack-server: image: redis/redis-stack-server:latest @@ -24,7 +35,7 @@ services: ports: - "9000:9000" - "9001:9001" - environment: + environment: - MINIO_ROOT_USER=${MINIO_ROOT_USER} - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} command: server /data --console-address ":9001" @@ -37,18 +48,25 @@ services: build: context: . dockerfile: Dockerfile.dev + container_name: talawa-api-dev + ports: + - "4000:4000" volumes: - .:/usr/src/app - /usr/src/app/node_modules depends_on: - - mongodb - - redis-stack-server - - minio + mongo: + condition: service_healthy + redis-stack-server: + condition: service_started + minio: + condition: service_started environment: - - MONGO_DB_URL=mongodb://mongodb:27017 + - MONGO_DB_URL=mongodb://mongo:27017/talawa-api?replicaSet=rs0&directConnection=true - REDIS_HOST=redis-stack-server - REDIS_PORT=6379 - + networks: + - talawa-network caddy: image: caddy/caddy:2.2.1-alpine container_name: caddy-service @@ -61,13 +79,14 @@ services: - $PWD/site:/srv - caddy_data:/data - caddy_config:/config + volumes: - mongodb-data: redis-data: + minio-data: + mongo_data: caddy_data: caddy_config: - minio-data: networks: talawa-network: diff --git a/init-mongo.sh b/init-mongo.sh new file mode 100755 index 00000000000..5b0bd22c7db --- /dev/null +++ b/init-mongo.sh @@ -0,0 +1,53 @@ +#!/bin/bash +set -e + +# Handle cleanup on script exit +cleanup() { + echo "Shutting down MongoDB..." + mongod --shutdown + exit 0 +} +trap cleanup SIGTERM SIGINT + +mongod --replSet rs0 --bind_ip_all --dbpath /data/db & +MONGOD_PID=$! + +# Wait for MongoDB to be ready +MAX_TRIES=30 +COUNTER=0 +echo "Waiting for MongoDB to start..." +until mongosh --eval "db.adminCommand('ping')" >/dev/null 2>&1; do + if [ $COUNTER -gt $MAX_TRIES ]; then + echo "Error: MongoDB failed to start" + exit 1 + fi + let COUNTER=COUNTER+1 + sleep 1 +done + +# Initialize the replica set +mongosh --eval ' + config = { + "_id" : "rs0", + "members" : [ + { + "_id" : 0, + "host" : "mongo:27017", + "priority": 1 + } + ] + }; + + while (true) { + try { + rs.initiate(config); + break; + } catch (err) { + print("Failed to initiate replica set, retrying in 5 seconds..."); + sleep(5000); + } + } +' + +# Keep container running +wait $MONGOD_PID \ No newline at end of file diff --git a/sample_data/users.json b/sample_data/users.json index 92f21c20ae1..bcaee45ea1a 100644 --- a/sample_data/users.json +++ b/sample_data/users.json @@ -21,7 +21,7 @@ "state": "State" }, "email": "testsuperadmin@example.com", - "password": "$2a$12$bSYpay6TRMpTOaAmYPFXku4avwmqfFBtmgg39TabxmtFEiz4plFtW", + "password": "$2a$12$gawMkattI6b.HAAwW7khxOJNzGZ2/N6YFe387/2yG8Z0G6tKVtv2y", "image": null, "createdAt": "2023-04-13T04:53:17.742Z", "__v": 0 diff --git a/setup.ts b/setup.ts index bad4b347bd3..760de987aa7 100644 --- a/setup.ts +++ b/setup.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-restricted-imports */ import * as cryptolib from "crypto"; import dotenv from "dotenv"; import fs from "fs"; @@ -7,31 +6,31 @@ import path from "path"; import type { ExecException } from "child_process"; import { exec, spawn, execSync } from "child_process"; import { MongoClient } from "mongodb"; -import { MAXIMUM_IMAGE_SIZE_LIMIT_KB } from "./src/constants"; +import { MAXIMUM_IMAGE_SIZE_LIMIT_KB } from "@constants"; import { askForMongoDBUrl, checkConnection, checkExistingMongoDB, -} from "./src/setup/MongoDB"; -import { askToKeepValues } from "./src/setup/askToKeepValues"; -import { getNodeEnvironment } from "./src/setup/getNodeEnvironment"; -import { isValidEmail } from "./src/setup/isValidEmail"; -import { validateRecaptcha } from "./src/setup/reCaptcha"; +} from "@setup/MongoDB"; +import { askToKeepValues } from "@setup/askToKeepValues"; +import { getNodeEnvironment } from "@setup/getNodeEnvironment"; +import { isValidEmail } from "@setup/isValidEmail"; +import { validateRecaptcha } from "@setup/reCaptcha"; import { askForRedisUrl, checkExistingRedis, checkRedisConnection, -} from "./src/setup/redisConfiguration"; +} from "@setup/redisConfiguration"; import { setImageUploadSize, validateImageFileSize, -} from "./src/setup/setImageUploadSize"; -import { askForSuperAdminEmail } from "./src/setup/superAdmin"; -import { updateEnvVariable } from "./src/setup/updateEnvVariable"; -import { verifySmtpConnection } from "./src/setup/verifySmtpConnection"; -import { loadDefaultOrganiation } from "./src/utilities/loadDefaultOrg"; -import { isMinioInstalled } from "./src/setup/isMinioInstalled"; -import { installMinio } from "./src/setup/installMinio"; +} from "@setup/setImageUploadSize"; +import { askForSuperAdminEmail } from "@setup/superAdmin"; +import { updateEnvVariable } from "@setup/updateEnvVariable"; +import { verifySmtpConnection } from "@setup/verifySmtpConnection"; +import { loadDefaultOrganiation } from "@utilities/loadDefaultOrg"; +import { isMinioInstalled } from "@setup/isMinioInstalled"; +import { installMinio } from "@setup/installMinio"; dotenv.config(); @@ -1015,6 +1014,14 @@ async function main(): Promise { const REDIS_PASSWORD = ""; const MINIO_ENDPOINT = "http://minio:9000"; + const { pwdVariable } = await inquirer.prompt({ + type: "input", + name: "pwdVariable", + message: + "Please enter the value for PWD (working directory for Docker setup):", + default: ".", + }); + const config = dotenv.parse(fs.readFileSync(".env")); config.MONGO_DB_URL = DB_URL; @@ -1022,18 +1029,21 @@ async function main(): Promise { config.REDIS_PORT = REDIS_PORT; config.REDIS_PASSWORD = REDIS_PASSWORD; config.MINIO_ENDPOINT = MINIO_ENDPOINT; + config.PWD = pwdVariable; process.env.MONGO_DB_URL = DB_URL; process.env.REDIS_HOST = REDIS_HOST; process.env.REDIS_PORT = REDIS_PORT; process.env.REDIS_PASSWORD = REDIS_PASSWORD; process.env.MINIO_ENDPOINT = MINIO_ENDPOINT; + process.env.PWD = pwdVariable; updateEnvVariable(config); console.log(`Your MongoDB URL is:\n${process.env.MONGO_DB_URL}`); console.log(`Your Redis host is:\n${process.env.REDIS_HOST}`); console.log(`Your Redis port is:\n${process.env.REDIS_PORT}`); console.log(`Your MinIO endpoint is:\n${process.env.MINIO_ENDPOINT}`); + console.log(`Your PWD value is:\n${process.env.PWD}`); } if (!isDockerInstallation) { diff --git a/src/utilities/loadSampleData.ts b/src/utilities/loadSampleData.ts index 14cc4a93cc7..176a3c0eba7 100644 --- a/src/utilities/loadSampleData.ts +++ b/src/utilities/loadSampleData.ts @@ -74,6 +74,7 @@ async function formatDatabase(): Promise { Post.deleteMany({}), AppUserProfile.deleteMany({}), RecurrenceRule.deleteMany({}), + Venue.deleteMany({}), ]); console.log("Cleared all collections\n"); } @@ -85,7 +86,7 @@ async function formatDatabase(): Promise { async function insertCollections(collections: string[]): Promise { try { // Connect to MongoDB database - await connect(); + await connect("talawa-api"); const { format } = yargs(hideBin(process.argv)) .options({ @@ -170,7 +171,7 @@ async function insertCollections(collections: string[]): Promise { async function checkCountAfterImport(): Promise { try { // Connect to MongoDB database - await connect(); + await connect("talawa-api"); const collections = [ { name: "users", model: User }, diff --git a/tests/helpers/volunteers.ts b/tests/helpers/volunteers.ts index 1b17d5d4d0c..ada916a6a3e 100644 --- a/tests/helpers/volunteers.ts +++ b/tests/helpers/volunteers.ts @@ -138,8 +138,6 @@ export const createVolunteerAndActions = async (): Promise< }); const today = new Date(); - const yesterday = new Date(today); - yesterday.setDate(today.getDate() - 1); const twoWeeksAgo = new Date(today); twoWeeksAgo.setDate(today.getDate() - 14); const twoMonthsAgo = new Date(today); diff --git a/tests/resolvers/Query/getVolunteerRanks.spec.ts b/tests/resolvers/Query/getVolunteerRanks.spec.ts index 67060e0bcc4..d742e10b741 100644 --- a/tests/resolvers/Query/getVolunteerRanks.spec.ts +++ b/tests/resolvers/Query/getVolunteerRanks.spec.ts @@ -77,7 +77,7 @@ describe("resolvers -> Query -> getVolunteerRanks", () => { {}, )) as unknown as VolunteerRank[]; - expect(volunteerRanks[0].hoursVolunteered).toEqual(6); + expect(volunteerRanks[0].hoursVolunteered).toEqual(2); expect(volunteerRanks[0].user._id).toEqual(testUser1?._id); expect(volunteerRanks[0].rank).toEqual(1); }); @@ -94,7 +94,7 @@ describe("resolvers -> Query -> getVolunteerRanks", () => { }, {}, )) as unknown as VolunteerRank[]; - expect(volunteerRanks[0].hoursVolunteered).toEqual(8); + expect(volunteerRanks[0].hoursVolunteered).toEqual(2); expect(volunteerRanks[0].user._id).toEqual(testUser1?._id); expect(volunteerRanks[0].rank).toEqual(1); }); diff --git a/tests/setup/checkExistingMongoDB.spec.ts b/tests/setup/checkExistingMongoDB.spec.ts index 02d934eca06..9d4bff9117c 100644 --- a/tests/setup/checkExistingMongoDB.spec.ts +++ b/tests/setup/checkExistingMongoDB.spec.ts @@ -9,6 +9,8 @@ describe("Setup -> checkExistingMongoDB", () => { }); it("should return the first valid URL when a connection is found", async () => { const result = await checkExistingMongoDB(); + console.log(result); + console.log(process.env.MONGO_DB_URL); expect(result).toBe(process.env.MONGO_DB_URL); }); @@ -60,7 +62,8 @@ describe("Setup -> checkExistingMongoDB", () => { expect(result).toBe(false); }); it("should return the first valid URL when a connection is found", async () => { - process.env.MONGO_DB_URL = "mongodb://localhost:27017/talawa-api"; + process.env.MONGO_DB_URL = + "mongodb://localhost:27017/test?replicaSet=rs0&directConnection=true"; const result = await checkExistingMongoDB(); expect(result).toBe(process.env.MONGO_DB_URL); }); diff --git a/tests/setup/mongoDB.spec.ts b/tests/setup/mongoDB.spec.ts index c6969b5af30..9cf28acac6e 100644 --- a/tests/setup/mongoDB.spec.ts +++ b/tests/setup/mongoDB.spec.ts @@ -117,7 +117,8 @@ describe("Setup -> mongoDB", () => { }); it("should return true for a successfull connection with MongoDB", async () => { - const url = "mongodb://localhost:27017"; + const url = + "mongodb://localhost:27017/test?replicaSet=rs0&directConnection=true"; const result = await module.checkConnection(url); expect(result).toEqual(true); }); diff --git a/tests/utilities/loadSampleData.spec.ts b/tests/utilities/loadSampleData.spec.ts index c5ab00832b2..9140d0fb03d 100644 --- a/tests/utilities/loadSampleData.spec.ts +++ b/tests/utilities/loadSampleData.spec.ts @@ -15,7 +15,7 @@ import { execSync } from "child_process"; describe("Sample Data Import Tests", () => { beforeAll(async () => { - await connect(); + await connect("talawa-api"); }); afterAll(async () => { diff --git a/tsconfig.json b/tsconfig.json index fc155159d50..a60626d6013 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,11 @@ "skipLibCheck": true /* Skip type checking all .d.ts files. */, "resolveJsonModule": true /* Allow to import JSON files */, "moduleResolution": "node", - "noUncheckedSideEffectImports": true /* Checks if module's path exists. */ + "noUncheckedSideEffectImports": true /* Checks if module's path exists. */, + "paths": { + "@constants": ["./src/constants"], + "@setup/*": ["./src/setup/*"], + "@utilities/*": ["./src/utilities/*"] + } } } diff --git a/vite.config.mts b/vite.config.mts index 12749c1ce54..87eae5c65a5 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -1,6 +1,15 @@ import { defineConfig } from "vitest/config"; +import path from 'node:path'; export default defineConfig({ + resolve: { + alias: { + '@constants': path.resolve(__dirname, 'src/constants'), + '@setup': path.resolve(__dirname, 'src/setup'), + '@utilities': path.resolve(__dirname, 'src/utilities'), + }, + }, + test: { setupFiles: ["./config/vitestSetup.ts"], @@ -36,6 +45,7 @@ export default defineConfig({ "src/utilities/sample_data/*", "src/utilities/loadSampleData.ts", ], + // This is used to tell vitest which coverage provider to use. c8 is the newer and // recommended coverage provider for node.js applications. You can swap it with