diff --git a/.circleci/config.yml b/.circleci/config.yml index ac9ca05..83bfe3b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,12 +1,13 @@ version: 2.1 - +orbs: + aws-cli: circleci/aws-cli@4.1.2 + aws-ecr: circleci/aws-ecr@9.0.1 + aws-ecs: circleci/aws-ecs@4.0.0 jobs: build_and_test: docker: - image: circleci/node:14 - working_directory: ~/app - steps: - checkout - run: @@ -26,9 +27,41 @@ jobs: name: Test Frontend command: | npm test + run_task: + docker: + - image: cimg/python:3.10 + steps: + - aws-cli/setup: + role_arn: arn:aws:iam::797929460436:role/ecs + role_session_name: example-session + - aws-ecs/run_task: + awsvpc: false + cluster: arn:aws:ecs:eu-north-1:797929460436:cluster/mern-ludo-cluster + launch_type: EC2 + task_definition: mern-ludo-docker-task workflows: - version: 2 - build: + build_and_test_and_deploy: jobs: - build_and_test + - aws-ecr/build_and_push_image: + auth: + - aws-cli/setup: + role_arn: arn:aws:iam::797929460436:role/openid + role_session_name: example-session + repo: mern-ludo + public_registry: true + tag: latest + requires: + - build_and_test + filters: + branches: + only: + - main + - run_task: + requires: + - aws-ecr/build_and_push_image + filters: + branches: + only: + - main \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..71a7578 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,35 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/engine/reference/builder/#dockerignore-file + +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.next +**/.cache +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +**/build +**/dist +LICENSE +README.md +node_modules diff --git a/.gitignore b/.gitignore index d2d2990..2e01d74 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ backend/node_modules /coverage # production -/build +build # misc .DS_Store @@ -21,3 +21,4 @@ backend/node_modules npm-debug.log* yarn-debug.log* yarn-error.log* +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8c665bc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM node:14 as frontend +WORKDIR /app +COPY . /app +RUN npm install --production +RUN npm run build + +FROM node:14 as backend +WORKDIR /app +COPY /backend /app +RUN npm install + +FROM node:14 +WORKDIR /app +COPY --from=backend /app /app/ +COPY --from=frontend /app/build /app/build + +EXPOSE 8080 + +CMD ["npm", "run", "start"] diff --git a/README.md b/README.md index eeaa3ea..71272d7 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,14 @@ -#
Online Multiplayer Ludo Game
- -## About +

Online Multiplayer Ludo Game

Ludo Online is a multiplayer web-based implementation of the classic board game Ludo, built using the MERN stack and integrated with SocketIO for real-time communication. -\>\> Play Online here << -\>\> [Watch YouTube Video here](https://youtu.be/mGMnH9Nvsyw) << +

+>> Play online here << +

+ +

+>> Watch YouTube Video here << +

## Architecture @@ -14,34 +17,49 @@ Ludo Online is a multiplayer web-based implementation of the classic board game ## Tech Stack Frontend: + ![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E) ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) ![React Router](https://img.shields.io/badge/React_Router-CA4245?style=for-the-badge&logo=react-router&logoColor=white) ![CSS3](https://img.shields.io/badge/css3-%231572B6.svg?style=for-the-badge&logo=css3&logoColor=white) ![HTML5](https://img.shields.io/badge/html5-%23E34F26.svg?style=for-the-badge&logo=html5&logoColor=white) ![MUI](https://img.shields.io/badge/MUI-%230081CB.svg?style=for-the-badge&logo=mui&logoColor=white) + Backend: + ![MongoDB](https://img.shields.io/badge/MongoDB-%234ea94b.svg?style=for-the-badge&logo=mongodb&logoColor=white) ![Express.js](https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) ![Socket.io](https://img.shields.io/badge/Socket.io-black?style=for-the-badge&logo=socket.io&badgeColor=010101) ![NodeJS](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white) + Tests: + ![cypress](https://img.shields.io/badge/-cypress-%23E5E5E5?style=for-the-badge&logo=cypress&logoColor=058a5e) ![Mocha](https://img.shields.io/badge/-mocha-%238D6748?style=for-the-badge&logo=mocha&logoColor=white) ![Jest](https://img.shields.io/badge/-jest-%23C21325?style=for-the-badge&logo=jest&logoColor=white) -Tools: + +Other: + ![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) ![AWS](https://img.shields.io/badge/AWS-%23FF9900.svg?style=for-the-badge&logo=amazon-aws&logoColor=white) ![CircleCI](https://img.shields.io/badge/circle%20ci-%23161616.svg?style=for-the-badge&logo=circleci&logoColor=white) ![Git](https://img.shields.io/badge/git-%23F05033.svg?style=for-the-badge&logo=git&logoColor=white) ![Jira](https://img.shields.io/badge/jira-%230A0FFF.svg?style=for-the-badge&logo=jira&logoColor=white) ## Key Features and Challenges - Maintained session consistency with **Express Session** and **MongoDB**. + - Enabled real-time communication via **WebSocket** and **SocketIO**. + - Maintained code reliability by implementing unit and integration tests using **Mocha**, **Chai**, and **Jest**. -- Hosted in a **Docker** container on **AWS EC2**. -- Established CI/CD using **CircleCI**. + - Implemented E2E tests utilizing **Cypress**, addressing challenges related to [testing collaboration](https://docs.cypress.io/guides/references/trade-offs#Multiple-browsers-open-at-the-same-time) and canvas functionality in the application. +- Established a CI/CD pipeline using **CircleCI**, with pushing **Docker** container to **AWS ECR** and deploying to **AWS ECS** + + ## Installation 1. Download this repository + 2. Generate your own [mongoDB atlas](https://www.mongodb.com) credential URL. It should looks like this: ``` + mongodb+srv://madmin:@clustername.mongodb.net/?retryWrites=true&w=majority + ``` -3. Add this URL to the /backend/credentials.js file +3. Add this URL to the /backend/.env file (refer to .env.example) + 4. Perform these commands in the main directory: ``` diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..52a81f1 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,3 @@ +PORT=8080 +CONNECTION_URI=your_mongodb_connection_uri +NODE_ENV="development" \ No newline at end of file diff --git a/backend/config/database.js b/backend/config/database.js index 5873253..f347966 100644 --- a/backend/config/database.js +++ b/backend/config/database.js @@ -1,9 +1,7 @@ -const CONNECTION_URI = require('../credentials.js'); - module.exports = function (mongoose) { mongoose.set('useFindAndModify', false); mongoose - .connect(CONNECTION_URI, { + .connect(process.env.CONNECTION_URI, { useNewUrlParser: true, useUnifiedTopology: true, dbName: 'test', diff --git a/backend/config/session.js b/backend/config/session.js index 56ead1c..c66fb40 100644 --- a/backend/config/session.js +++ b/backend/config/session.js @@ -1,8 +1,8 @@ const session = require('express-session'); -const CONNECTION_URI = require('../credentials.js'); const MongoDBStore = require('connect-mongodb-session')(session); + const store = new MongoDBStore({ - uri: CONNECTION_URI, + uri: process.env.CONNECTION_URI, collection: 'sessions', }); const sessionMiddleware = session({ diff --git a/backend/credentials.js b/backend/credentials.js deleted file mode 100644 index 4be1469..0000000 --- a/backend/credentials.js +++ /dev/null @@ -1,2 +0,0 @@ -// Write your own mongoDBatlas credentials here -module.exports = ''; diff --git a/backend/handlers/playerHandler.js b/backend/handlers/playerHandler.js index 93869e1..4280673 100644 --- a/backend/handlers/playerHandler.js +++ b/backend/handlers/playerHandler.js @@ -1,5 +1,5 @@ const { getRoom, updateRoom } = require('../services/roomService'); -const { colors } = require('../utils/constants'); +const { COLORS } = require('../utils/constants'); module.exports = socket => { const req = socket.request; @@ -44,7 +44,7 @@ module.exports = socket => { if (err) return socket.disconnect(); req.session.roomId = room._id.toString(); req.session.playerId = room.players[room.players.length - 1]._id.toString(); - req.session.color = colors[room.players.length - 1]; + req.session.color = COLORS[room.players.length - 1]; req.session.save(); socket.join(room._id.toString()); socket.emit('player:data', JSON.stringify(req.session)); diff --git a/backend/models/room.js b/backend/models/room.js index 47fd2f7..b427494 100644 --- a/backend/models/room.js +++ b/backend/models/room.js @@ -1,6 +1,7 @@ const mongoose = require('mongoose'); -const { colors } = require('../utils/constants'); +const { COLORS, MOVE_TIME } = require('../utils/constants'); const { makeRandomMove } = require('../handlers/handlersFunctions'); +const timeoutManager = require('./timeoutManager.js'); const PawnSchema = require('./pawn'); const PlayerSchema = require('./player'); @@ -12,7 +13,6 @@ const RoomSchema = new mongoose.Schema({ started: { type: Boolean, default: false }, full: { type: Boolean, default: false }, nextMoveTime: Number, - timeoutID: Number, rolledNumber: Number, players: [PlayerSchema], winner: { type: String, default: null }, @@ -24,10 +24,10 @@ const RoomSchema = new mongoose.Schema({ let pawn = {}; pawn.basePos = i; pawn.position = i; - if (i < 4) pawn.color = colors[0]; - else if (i < 8) pawn.color = colors[1]; - else if (i < 12) pawn.color = colors[2]; - else if (i < 16) pawn.color = colors[3]; + if (i < 4) pawn.color = COLORS[0]; + else if (i < 8) pawn.color = COLORS[1]; + else if (i < 12) pawn.color = COLORS[2]; + else if (i < 16) pawn.color = COLORS[3]; startPositions.push(pawn); } return startPositions; @@ -54,10 +54,10 @@ RoomSchema.methods.changeMovingPlayer = function () { } else { this.players[playerIndex + 1].nowMoving = true; } - this.nextMoveTime = Date.now() + 15000; + this.nextMoveTime = Date.now() + MOVE_TIME; this.rolledNumber = null; - if (this.timeoutID) clearTimeout(this.timeoutID); - this.timeoutID = setTimeout(makeRandomMove, 15000, this._id.toString()); + timeoutManager.clear(this._id.toString()); + timeoutManager.set(makeRandomMove, MOVE_TIME, this._id.toString()); }; RoomSchema.methods.movePawn = function (pawn) { @@ -83,14 +83,14 @@ RoomSchema.methods.canStartGame = function () { RoomSchema.methods.startGame = function () { this.started = true; - this.nextMoveTime = Date.now() + 15000; + this.nextMoveTime = Date.now() + MOVE_TIME; this.players.forEach(player => (player.ready = true)); this.players[0].nowMoving = true; - this.timeoutID = setTimeout(makeRandomMove, 15000, this._id.toString()); + timeoutManager.set(makeRandomMove, MOVE_TIME, this._id.toString()); }; RoomSchema.methods.endGame = function (winner) { - this.timeoutID = null; + timeoutManager.clear(this._id.toString()); this.rolledNumber = null; this.nextMoveTime = null; this.players.map(player => (player.nowMoving = false)); @@ -131,7 +131,7 @@ RoomSchema.methods.addPlayer = function (name, id) { sessionID: id, name: name, ready: false, - color: colors[this.players.length], + color: COLORS[this.players.length], }); }; diff --git a/backend/models/timeoutManager.js b/backend/models/timeoutManager.js new file mode 100644 index 0000000..a7f04d9 --- /dev/null +++ b/backend/models/timeoutManager.js @@ -0,0 +1,19 @@ +const timeoutManager = { + timeouts: new Map(), + add: function (roomId, timeoutId) { + this.timeouts.set(roomId, timeoutId); + }, + get: function (roomId) { + return this.timeouts.get(roomId); + }, + clear: function (roomId) { + clearTimeout(this.timeouts.get(roomId)); + this.timeouts.delete(roomId); + }, + set: function (timeoutFunction, time, roomId) { + const timeoutId = setTimeout(timeoutFunction, time, roomId); + this.add(roomId, timeoutId); + }, +}; + +module.exports = timeoutManager; diff --git a/backend/package-lock.json b/backend/package-lock.json index 122fc4f..4e797bb 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,9 +10,11 @@ "connect-mongodb-session": "^3.1.1", "cookie-parser": "^1.4.5", "cors": "^2.8.5", + "dotenv": "^16.3.1", "express": "^4.17.1", "express-session": "^1.17.1", "mongoose": "^5.12.0", + "sinon": "^17.0.1", "socket.io": "^4.5.1" }, "devDependencies": { @@ -21,6 +23,45 @@ "socket.io-client": "^4.7.2" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==" + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -668,6 +709,17 @@ "node": ">=0.3.1" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1081,7 +1133,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -1257,6 +1308,11 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==" + }, "node_modules/kareem": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz", @@ -1293,6 +1349,11 @@ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, "node_modules/lodash.set": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", @@ -1676,6 +1737,55 @@ "node": ">= 0.6" } }, + "node_modules/nise": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz", + "integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/nise/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dependencies": { + "isarray": "0.0.1" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2039,6 +2149,42 @@ "resolved": "https://registry.npmjs.org/sift/-/sift-13.5.2.tgz", "integrity": "sha512-+gxdEOMA2J+AI+fVsCqeNn7Tgx3M9ZN9jdi95939l1IJ8cZsqS8sqpJyOkic2SJk+1+98Uwryt/gL6XDaV+UZA==" }, + "node_modules/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/sliced": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", @@ -2306,7 +2452,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, "engines": { "node": ">=4" } @@ -2506,6 +2651,47 @@ } }, "dependencies": { + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "requires": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==" + }, "@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -2983,6 +3169,11 @@ "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "dev": true }, + "dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==" + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3291,8 +3482,7 @@ "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "he": { "version": "1.2.0", @@ -3412,6 +3602,11 @@ "argparse": "^2.0.1" } }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==" + }, "kareem": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz", @@ -3439,6 +3634,11 @@ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, "lodash.set": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", @@ -3711,6 +3911,59 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "nise": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz", + "integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==", + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "requires": { + "@sinonjs/commons": "^3.0.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "requires": { + "isarray": "0.0.1" + } + } + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3989,6 +4242,34 @@ "resolved": "https://registry.npmjs.org/sift/-/sift-13.5.2.tgz", "integrity": "sha512-+gxdEOMA2J+AI+fVsCqeNn7Tgx3M9ZN9jdi95939l1IJ8cZsqS8sqpJyOkic2SJk+1+98Uwryt/gL6XDaV+UZA==" }, + "sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "requires": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "dependencies": { + "diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "sliced": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", @@ -4187,8 +4468,7 @@ "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" }, "type-is": { "version": "1.6.18", diff --git a/backend/package.json b/backend/package.json index 751caf9..3f5149b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,9 +5,11 @@ "connect-mongodb-session": "^3.1.1", "cookie-parser": "^1.4.5", "cors": "^2.8.5", + "dotenv": "^16.3.1", "express": "^4.17.1", "express-session": "^1.17.1", "mongoose": "^5.12.0", + "sinon": "^17.0.1", "socket.io": "^4.5.1" }, "devDependencies": { diff --git a/backend/server.js b/backend/server.js index f018038..b4a1cd0 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,10 +1,12 @@ const express = require('express'); const cors = require('cors'); +const path = require('path'); const cookieParser = require('cookie-parser'); const mongoose = require('mongoose'); +require('dotenv').config(); const { sessionMiddleware } = require('./config/session'); -const PORT = 8080; +const PORT = process.env.PORT; const app = express(); @@ -30,9 +32,10 @@ require('./config/database')(mongoose); require('./config/socket')(server); if (process.env.NODE_ENV === 'production') { - app.use(express.static('/app/build')); - app.get('/', (req, res) => { - res.sendFile('/app/build/index.html'); + app.use(express.static('./build')); + app.get('*', (req, res) => { + const indexPath = path.join(__dirname, './build/index.html'); + res.sendFile(indexPath); }); } diff --git a/backend/tests/models/timeoutManager.test.js b/backend/tests/models/timeoutManager.test.js new file mode 100644 index 0000000..692ef5e --- /dev/null +++ b/backend/tests/models/timeoutManager.test.js @@ -0,0 +1,50 @@ +const chai = require('chai'); +const sinon = require('sinon'); +const timeoutManager = require('../../models/timeoutManager'); + +const { expect } = chai; + +describe('timeoutManager', () => { + beforeEach(() => { + timeoutManager.timeouts.clear(); + sinon.useFakeTimers({ shouldClearNativeTimers: true }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should add a timeout to the map', () => { + timeoutManager.add('room1', 1); + expect(timeoutManager.timeouts.size).to.equal(1); + }); + + it('should get a timeout from the map', () => { + timeoutManager.add('room1', 1); + const timeoutId = timeoutManager.get('room1'); + expect(timeoutId).to.equal(1); + }); + + it('should clear a timeout from the map', () => { + timeoutManager.add('room1', 1); + timeoutManager.clear('room1'); + expect(timeoutManager.timeouts.size).to.equal(0); + }); + + it('should set a new timeout', () => { + const timeoutFunction = sinon.spy(); + timeoutManager.set(timeoutFunction, 100, 'room1'); + expect(timeoutManager.timeouts.size).to.equal(1); + sinon.clock.tick(101); + sinon.assert.calledOnce(timeoutFunction); + }); + + it('should not call the timeout function if cleared', () => { + const timeoutFunction = sinon.spy(); + timeoutManager.set(timeoutFunction, 100, 'room1'); + timeoutManager.clear('room1'); + sinon.clock.tick(101); + sinon.assert.notCalled(timeoutFunction); + expect(timeoutManager.timeouts.size).to.equal(0); + }); +}); diff --git a/backend/utils/constants.js b/backend/utils/constants.js index a65048d..15978cb 100644 --- a/backend/utils/constants.js +++ b/backend/utils/constants.js @@ -1,2 +1,3 @@ -const colors = ["red", "blue", "green", "yellow"]; -module.exports = { colors }; +const COLORS = ['red', 'blue', 'green', 'yellow']; +const MOVE_TIME = 15000; +module.exports = { COLORS, MOVE_TIME }; diff --git a/package.json b/package.json index 5f62271..e7ffcc6 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,7 @@ "scripts": { "start": "react-scripts start", "build": "react-scripts build", - "heroku-postbuild": "cd backend && npm install && cd .. && npm install && npm run build", - "test": "react-scripts test", - "eject": "react-scripts eject" + "test": "react-scripts test" }, "eslintConfig": { "extends": [ @@ -48,11 +46,10 @@ "last 1 safari version" ] }, - "proxy": "http://localhost:5000", "devDependencies": { "@testing-library/jest-dom": "^6.1.5", "@testing-library/react": "^14.1.2", "cypress": "^13.6.1", - "@babel/plugin-proposal-private-property-in-object": "^7.16.7" + "@babel/plugin-transform-private-property-in-object": "^7.16.7" } } diff --git a/src/App.js b/src/App.js index 920df19..61034eb 100644 --- a/src/App.js +++ b/src/App.js @@ -13,7 +13,7 @@ function App() { const [playerSocket, setPlayerSocket] = useState(); const [redirect, setRedirect] = useState(); useEffect(() => { - const socket = io('http://localhost:8080', { withCredentials: true }); + const socket = io(`http://${window.location.hostname}:8080`, { withCredentials: true }); socket.on('player:data', data => { data = JSON.parse(data); setPlayerData(data); diff --git a/src/components/LoginPage/JoinServer/JoinServer.module.css b/src/components/LoginPage/JoinServer/JoinServer.module.css index bd89d03..c2361bb 100644 --- a/src/components/LoginPage/JoinServer/JoinServer.module.css +++ b/src/components/LoginPage/JoinServer/JoinServer.module.css @@ -1,7 +1,7 @@ .serversTableContainer { display: flex; - height: 500px; overflow: scroll; + height: 500px; width: 100%; } .refresh { diff --git a/src/components/LoginPage/JoinServer/ServersTable/ServersTable.module.css b/src/components/LoginPage/JoinServer/ServersTable/ServersTable.module.css index 25636ad..a176bf8 100644 --- a/src/components/LoginPage/JoinServer/ServersTable/ServersTable.module.css +++ b/src/components/LoginPage/JoinServer/ServersTable/ServersTable.module.css @@ -1,6 +1,8 @@ .roomName { max-width: 150px; overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; text-align: left !important; } .rooms > thead > tr :nth-child(2) { @@ -30,6 +32,7 @@ .rooms { border-collapse: collapse; width: 100%; + height: fit-content; } .lastColumn {