├── README.md ├── assertion-analyser.js ├── package-lock.json ├── package.json ├── public ├── Collectible.mjs ├── Player.mjs ├── game.mjs └── style.css ├── replit.nix ├── routes └── fcctesting.js ├── sample.env ├── server.js ├── test-runner.js ├── tests ├── 1_unit-tests.js └── 2_functional-tests.js └── views └── index.html /README.md: -------------------------------------------------------------------------------- 1 | # [Secure Real Time Multiplayer Game](https://www.freecodecamp.org/learn/information-security/information-security-projects/secure-real-time-multiplayer-game) 2 | 3 | ## Live: https://secure-real-time-multiplayer-game-freecodecamp.hawshemi.repl.co 4 | 5 | 6 | For running locally create a file named `.env` with the code below: 7 | ``` 8 | PORT:3000 9 | NODE_ENV=test 10 | ``` 11 | -------------------------------------------------------------------------------- /assertion-analyser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * 4 | * 5 | * 6 | * 7 | * 8 | * 9 | * 10 | * 11 | * 12 | * 13 | * DO NOT EDIT THIS FILE 14 | * For FCC testing purposes! 15 | * 16 | * 17 | * 18 | * 19 | * 20 | * 21 | * 22 | * 23 | * 24 | * 25 | * 26 | */ 27 | 28 | function objParser(str, init) { 29 | // finds objects, arrays, strings, and function arguments 30 | // between parens, because they may contain ',' 31 | var openSym = ['[', '{', '"', "'", '(']; 32 | var closeSym = [']', '}', '"', "'", ')']; 33 | var type; 34 | for(var i = (init || 0); i < str.length; i++ ) { 35 | type = openSym.indexOf(str[i]); 36 | if( type !== -1) break; 37 | } 38 | if (type === -1) return null; 39 | var open = openSym[type]; 40 | var close = closeSym[type]; 41 | var count = 1; 42 | for(var k = i+1; k < str.length; k++) { 43 | if(open === '"' || open === "'") { 44 | if(str[k] === close) count--; 45 | if(str[k] === '\\') k++; 46 | } else { 47 | if(str[k] === open) count++; 48 | if(str[k] === close) count--; 49 | } 50 | if(count === 0) break; 51 | } 52 | if(count !== 0) return null; 53 | var obj = str.slice(i, k+1); 54 | return { 55 | start : i, 56 | end: k, 57 | obj: obj 58 | }; 59 | } 60 | 61 | function replacer(str) { 62 | // replace objects with a symbol ( __#n) 63 | var obj; 64 | var cnt = 0; 65 | var data = []; 66 | while(obj = objParser(str)) { 67 | data[cnt] = obj.obj; 68 | str = str.substring(0, obj.start) + '__#' + cnt++ + str.substring(obj.end+1) 69 | } 70 | return { 71 | str : str, 72 | dictionary : data 73 | } 74 | } 75 | 76 | function splitter(str) { 77 | // split on commas, then restore the objects 78 | var strObj = replacer(str); 79 | var args = strObj.str.split(','); 80 | args = args.map(function(a){ 81 | var m = a.match(/__#(\d+)/); 82 | while (m) { 83 | a = a.replace(/__#(\d+)/, strObj.dictionary[m[1]]); 84 | m = a.match(/__#(\d+)/); 85 | } 86 | return a.trim(); 87 | }) 88 | return args; 89 | } 90 | 91 | function assertionAnalyser(body) { 92 | 93 | // already filtered in the test runner 94 | // // remove comments 95 | // body = body.replace(/\/\/.*\n|\/\*.*\*\//g, ''); 96 | // // get test function body 97 | // body = body.match(/\{\s*([\s\S]*)\}\s*$/)[1]; 98 | 99 | if(!body) return "invalid assertion"; 100 | // replace assertions bodies, so that they cannot 101 | // contain the word 'assertion' 102 | 103 | var body = body.match(/(?:browser\s*\.\s*)?assert\s*\.\s*\w*\([\s\S]*\)/)[0]; 104 | var s = replacer(body); 105 | // split on 'assertion' 106 | var splittedAssertions = s.str.split('assert'); 107 | var assertions = splittedAssertions.slice(1); 108 | // match the METHODS 109 | 110 | var assertionBodies = []; 111 | var methods = assertions.map(function(a, i){ 112 | var m = a.match(/^\s*\.\s*(\w+)__#(\d+)/); 113 | assertionBodies.push(parseInt(m[2])); 114 | var pre = splittedAssertions[i].match(/browser\s*\.\s*/) ? 'browser.' : ''; 115 | return pre + m[1]; 116 | }); 117 | if(methods.some(function(m){ return !m })) return "invalid assertion"; 118 | // remove parens from the assertions bodies 119 | var bodies = assertionBodies.map(function(b){ 120 | return s.dictionary[b].slice(1,-1).trim(); 121 | }); 122 | assertions = methods.map(function(m, i) { 123 | return { 124 | method: m, 125 | args: splitter(bodies[i]) //replace objects, split on ',' ,then restore objects 126 | } 127 | }) 128 | return assertions; 129 | } 130 | 131 | module.exports = assertionAnalyser; 132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secure-real-time-multiplayer-game", 3 | "version": "1.0.0", 4 | "description": "Information Security 5: Secure Real-Time Multiplayer Game", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "nodemon server.js", 8 | "test": "PORT=3005 mocha --require @babel/register --recursive --exit --ui tdd tests/" 9 | }, 10 | "dependencies": { 11 | "@babel/core": "^7.7.5", 12 | "@babel/preset-env": "^7.7.6", 13 | "@babel/register": "^7.7.4", 14 | "body-parser": "^1.19.0", 15 | "chai": "^4.2.0", 16 | "chai-http": "^4.3.0", 17 | "cors": "^2.8.5", 18 | "dotenv": "^8.2.0", 19 | "express": "^4.17.1", 20 | "helmet": "^3.23.3", 21 | "jsdom": "^16.2.0", 22 | "mocha": "^7.1.0", 23 | "nodemon": "^2.0.22", 24 | "socket.io": "^2.3.0", 25 | "socket.io-client": "^2.3.0" 26 | }, 27 | "license": "MIT" 28 | } 29 | -------------------------------------------------------------------------------- /public/Collectible.mjs: -------------------------------------------------------------------------------- 1 | class Collectible { 2 | constructor({ x, y, value, id }) { 3 | this.x = x 4 | this.y = y 5 | this.value = value 6 | this.id = id 7 | } 8 | } 9 | 10 | /* 11 | Note: Export using CommonJS module system 12 | */ 13 | module.exports = Collectible; 14 | -------------------------------------------------------------------------------- /public/Player.mjs: -------------------------------------------------------------------------------- 1 | class Player { 2 | constructor({ x, y, score, id }) { 3 | this.x = x 4 | this.y = y 5 | this.score = score 6 | this.id = id 7 | } 8 | 9 | movePlayer(dir, speed) { 10 | switch (dir) { 11 | case "left": 12 | this.x -= speed; 13 | break; 14 | case "right": 15 | this.x += speed; 16 | break; 17 | case "up": 18 | this.y -= speed; 19 | break; 20 | case "down": 21 | this.y += speed; 22 | break; 23 | } 24 | } 25 | 26 | collision(item) { 27 | let baitRadius = item.value * 2 + 10 28 | if (Math.abs(this.x - item.x) < baitRadius + 15 29 | && Math.abs(this.y - item.y) < baitRadius + 15) { 30 | return true 31 | } else { 32 | return false 33 | } 34 | } 35 | 36 | calculateRank(arr) { 37 | let currentRank = 1 38 | for (let p of arr) { 39 | if (p.score > this.score) { 40 | currentRank += 1 41 | } 42 | } 43 | return `Rank: ${currentRank}/${arr.length}` 44 | } 45 | 46 | } 47 | 48 | module.exports = Player; -------------------------------------------------------------------------------- /public/game.mjs: -------------------------------------------------------------------------------- 1 | //import Player from './Player.mjs'; 2 | //import Collectible from './Collectible.mjs'; 3 | const Player = require('./Player.mjs'); 4 | const Collectible = require('./Collectible.mjs'); 5 | 6 | const socket = io(); 7 | const canvas = document.getElementById('game-window'); 8 | 9 | const CANVAS_WIDTH = 800 10 | const CANVAS_HEIGHT = 500 11 | 12 | canvas.width = CANVAS_WIDTH 13 | canvas.height = CANVAS_HEIGHT 14 | 15 | const context = canvas.getContext('2d'); 16 | let initialColor = getRandomColor() 17 | context.fillStyle = initialColor 18 | 19 | let allPlayers = [] 20 | let cPlayer 21 | let cBait 22 | 23 | socket.on('connect', function () { 24 | 25 | let playerId = socket.io.engine.id 26 | // create a new player 27 | cPlayer = new Player({ 28 | x: Math.floor(Math.random() * CANVAS_WIDTH - 30), 29 | y: Math.floor(Math.random() * CANVAS_HEIGHT - 30), 30 | score: 0, 31 | id: playerId, 32 | }) 33 | 34 | socket.emit("start", cPlayer) 35 | 36 | socket.on("player_updates", (players) => { 37 | allPlayers = players 38 | drawPlayers(allPlayers) 39 | }) 40 | 41 | socket.on("bait", (bait) => { 42 | cBait = bait 43 | drawBait(bait.x, bait.y, bait.value) 44 | }) 45 | 46 | window.addEventListener("keydown", (e) => { 47 | let cur_key = e.key.toLowerCase() 48 | let direction = cur_key === "d" ? "right" : 49 | cur_key === "a" ? "left" : 50 | cur_key === "w" ? "up" : 51 | cur_key === "s" ? "down" : null 52 | 53 | if (direction) { 54 | context.clearRect(...getCoord(cPlayer)) 55 | cPlayer.movePlayer(direction, 10) 56 | checkBoundary(cPlayer) 57 | context.fillRect(...getCoord(cPlayer)) 58 | allPlayers = allPlayers.map(p => { 59 | if (p.id === cPlayer.id) { 60 | return cPlayer 61 | } else { 62 | return p 63 | } 64 | }) 65 | socket.emit("player_updates", allPlayers) 66 | } 67 | 68 | if (cPlayer.collision(cBait)) { 69 | context.clearRect(...getBaitCoord(cBait)) 70 | cBait = { value: 0 } 71 | context.fillRect(...getCoord(cPlayer)) 72 | socket.emit("collision", cPlayer) 73 | let rank = cPlayer.calculateRank(allPlayers) 74 | document.getElementById("rank").innerText = rank 75 | } 76 | }) 77 | 78 | }); 79 | 80 | // format player(square box) coordinate x, y, width, height 81 | function getCoord(player) { 82 | return [player.x, player.y, 20, 20] 83 | } 84 | 85 | // get random colors for player 86 | function getRandomColor() { 87 | let r, g, b 88 | r = Math.floor(Math.random() * 256) 89 | g = Math.floor(Math.random() * 256) 90 | b = Math.floor(Math.random() * 256) 91 | return `rgb(${r}, ${g}, ${b})` 92 | } 93 | 94 | // check boundary collision 95 | function checkBoundary(player) { 96 | if (player.x < 5) { 97 | player.x = 5 98 | } 99 | if (player.x > CANVAS_WIDTH - 25) { 100 | player.x = CANVAS_WIDTH - 25 101 | } 102 | if (player.y < 5) { 103 | player.y = 5 104 | } 105 | if (player.y > CANVAS_HEIGHT - 25) { 106 | player.y = CANVAS_HEIGHT - 25 107 | } 108 | } 109 | 110 | // draw bait or collectible for player to catch 111 | function drawBait(x, y, value) { 112 | // value 1-5 113 | // bait of different colors and size acc. to value 114 | let colors = ["#f542cb", "#f55742", "#f5f242", "#428df5", "#42f56c"] 115 | context.beginPath() 116 | context.arc(x, y, value * 2 + 10, 0, 2 * Math.PI, false) 117 | context.fillStyle = colors[value - 1] 118 | context.fill() 119 | } 120 | 121 | // format bait coordinate 122 | // to clear it from screen 123 | function getBaitCoord(bait) { 124 | let radFactor = bait.value * 2 + 10 125 | return [bait.x - radFactor, bait.y - radFactor, bait.x + radFactor, bait.y + radFactor] 126 | } 127 | 128 | // draw all players 129 | function drawPlayers(players) { 130 | for (let p of players) { 131 | context.fillRect(...getCoord(p)) 132 | } 133 | } 134 | 135 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 20px; 3 | } 4 | 5 | code { 6 | background: #d0d0d5; 7 | } 8 | 9 | canvas { 10 | margin: auto; 11 | border: 10px solid #1e1e1e; 12 | } 13 | 14 | .container { 15 | display: flex; 16 | flex-direction: column; 17 | } 18 | 19 | @media (min-width: 800px) { 20 | .container { 21 | flex-direction: row; 22 | } 23 | } 24 | 25 | .control-center { 26 | display: flex; 27 | justify-content: space-around; 28 | } -------------------------------------------------------------------------------- /replit.nix: -------------------------------------------------------------------------------- 1 | { pkgs }: { 2 | deps = [ 3 | pkgs.nodejs-18_x 4 | pkgs.nodePackages.typescript-language-server 5 | pkgs.yarn 6 | pkgs.replitPackages.jest 7 | ]; 8 | } -------------------------------------------------------------------------------- /routes/fcctesting.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * 4 | * 5 | * 6 | * 7 | * 8 | * 9 | * 10 | * 11 | * 12 | * 13 | * DO NOT EDIT THIS FILE 14 | * For FCC testing purposes! 15 | * 16 | * 17 | * 18 | * 19 | * 20 | * 21 | * 22 | * 23 | * 24 | * 25 | * 26 | */ 27 | 28 | 'use strict'; 29 | 30 | const cors = require('cors'); 31 | const fs = require('fs'); 32 | const runner = require('../test-runner'); 33 | 34 | module.exports = function(app) { 35 | 36 | app.route('/_api/server.js') 37 | .get(function(req, res, next) { 38 | console.log('requested'); 39 | fs.readFile(__dirname + '/server.js', function(err, data) { 40 | if (err) return next(err); 41 | res.send(data.toString()); 42 | }); 43 | }); 44 | app.route('/_api/routes/api.js') 45 | .get(function(req, res, next) { 46 | console.log('requested'); 47 | fs.readFile(__dirname + '/routes/api.js', function(err, data) { 48 | if (err) return next(err); 49 | res.type('txt').send(data.toString()); 50 | }); 51 | }); 52 | app.route('/_api/controllers/convertHandler.js') 53 | .get(function(req, res, next) { 54 | console.log('requested'); 55 | fs.readFile(__dirname + '/controllers/convertHandler.js', function(err, data) { 56 | if (err) return next(err); 57 | res.type('txt').send(data.toString()); 58 | }); 59 | }); 60 | 61 | var error; 62 | app.get('/_api/get-tests', cors(), function(req, res, next) { 63 | console.log(error); 64 | if (!error && process.env.NODE_ENV === 'test') return next(); 65 | res.json({ status: 'unavailable' }); 66 | }, 67 | function(req, res, next) { 68 | if (!runner.report) return next(); 69 | res.json(testFilter(runner.report, req.query.type, req.query.n)); 70 | }, 71 | function(req, res) { 72 | runner.on('done', function(report) { 73 | process.nextTick(() => res.json(testFilter(runner.report, req.query.type, req.query.n))); 74 | }); 75 | }); 76 | app.get('/_api/app-info', function(req, res) { 77 | var hs = Object.keys(res.getHeaders()) 78 | .filter(h => !h.match(/^access-control-\w+/)); 79 | var hObj = {}; 80 | hs.forEach(h => { hObj[h] = res.getHeaders()[h] }); 81 | delete res.getHeaders()['strict-transport-security']; 82 | res.json({ headers: hObj }); 83 | }); 84 | 85 | }; 86 | 87 | function testFilter(tests, type, n) { 88 | var out; 89 | switch (type) { 90 | case 'unit': 91 | out = tests.filter(t => t.context.match('Unit Tests')); 92 | break; 93 | case 'functional': 94 | out = tests.filter(t => t.context.match('Functional Tests') && !t.title.match('#example')); 95 | break; 96 | default: 97 | out = tests; 98 | } 99 | if (n !== undefined) { 100 | return out[n] || out; 101 | } 102 | return out; 103 | } -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | # Environment Config 2 | 3 | # store your secrets and config variables in here 4 | # only invited collaborators will be able to see your .env values 5 | 6 | # note: .env is a shell file so there can't be spaces around '=' 7 | 8 | PORT=3000 9 | # NODE_ENV=test -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const helmet = require("helmet") 4 | const bodyParser = require('body-parser'); 5 | const expect = require('chai'); 6 | const socket = require('socket.io'); 7 | const cors = require('cors'); 8 | 9 | const fccTestingRoutes = require('./routes/fcctesting.js'); 10 | const runner = require('./test-runner.js'); 11 | 12 | const app = express(); 13 | 14 | app.use('/public', express.static(process.cwd() + '/public')); 15 | app.use('/assets', express.static(process.cwd() + '/assets')); 16 | 17 | app.use(bodyParser.json()); 18 | app.use(bodyParser.urlencoded({ extended: true })); 19 | 20 | app.disable("x-powered-by") 21 | 22 | //For FCC testing purposes and enables user to connect from outside the hosting platform 23 | app.use(cors({ origin: '*' })); 24 | 25 | 26 | // helmet 27 | app.use(helmet.xssFilter()) 28 | app.use(helmet.noSniff()) 29 | app.use(helmet.noCache()) 30 | 31 | // custom header 32 | function customHeader(req, res, next) { 33 | res.setHeader("X-Powered-By", "PHP 7.4.3") 34 | next() 35 | } 36 | app.use(customHeader) 37 | 38 | // Index page (static HTML) 39 | app.route('/') 40 | .get(function(req, res) { 41 | res.sendFile(process.cwd() + '/views/index.html'); 42 | }); 43 | 44 | //For FCC testing purposes 45 | fccTestingRoutes(app); 46 | 47 | // 404 Not Found Middleware 48 | app.use(function(req, res, next) { 49 | res.status(404) 50 | .type('text') 51 | .send('Not Found'); 52 | }); 53 | 54 | const portNum = process.env.PORT || 3000; 55 | 56 | // Set up server and tests 57 | const server = app.listen(portNum, () => { 58 | console.log(`Listening on port ${portNum}`); 59 | if (process.env.NODE_ENV === 'test') { 60 | console.log('Running Tests...'); 61 | setTimeout(function() { 62 | try { 63 | runner.run(); 64 | } catch (error) { 65 | console.log('Tests are not valid:'); 66 | console.error(error); 67 | } 68 | }, 1500); 69 | } 70 | }); 71 | 72 | 73 | // socket connection 74 | const io = socket.listen(server) 75 | const Collectible = require("./public/Collectible.mjs") 76 | const CANVAS_WIDTH = 800 77 | const CANVAS_HEIGHT = 500 78 | 79 | let players = [] 80 | let baitNum = 0 81 | let bait 82 | 83 | io.on("connection", (socket) => { 84 | 85 | // on player connect 86 | socket.on("start", (player) => { 87 | console.log("player has joined", player) 88 | players.push(player) 89 | 90 | // send player info 91 | socket.emit("player_updates", players) 92 | 93 | // create a bait 94 | bait = createBait(baitNum) 95 | socket.emit("bait", bait) 96 | }) 97 | 98 | // on player collision 99 | socket.on("collision", (player) => { 100 | for (let p of players) { 101 | if (p.id === player.id) { 102 | p.score += bait.value 103 | } 104 | } 105 | // update bait 106 | bait = createBait(baitNum) 107 | socket.emit("bait", bait) 108 | }) 109 | }) 110 | 111 | // create a new collectible 112 | function createBait(id) { 113 | let random_x, random_y 114 | random_x = Math.floor(Math.random() * (CANVAS_WIDTH - 20)) + 20 115 | random_y = Math.floor(Math.random() * (CANVAS_HEIGHT - 20)) + 20 116 | random_value = Math.floor(Math.random() * 5) + 1 117 | baitNum += 1 118 | return new Collectible({ 119 | x: random_x, 120 | y: random_y, 121 | value: random_value, 122 | id: id 123 | }) 124 | } 125 | 126 | 127 | module.exports = app; // For testing 128 | -------------------------------------------------------------------------------- /test-runner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * 4 | * 5 | * 6 | * 7 | * 8 | * 9 | * 10 | * 11 | * 12 | * 13 | * DO NOT EDIT THIS FILE 14 | * For FCC testing purposes! 15 | * 16 | * 17 | * 18 | * 19 | * 20 | * 21 | * 22 | * 23 | * 24 | * 25 | * 26 | */ 27 | 28 | var analyser = require('./assertion-analyser'); 29 | var EventEmitter = require('events').EventEmitter; 30 | 31 | var Mocha = require('mocha'), 32 | fs = require('fs'), 33 | path = require('path'); 34 | require("@babel/register"); 35 | 36 | var mocha = new Mocha(); 37 | var testDir = './tests' 38 | 39 | 40 | // Add each .js file to the mocha instance 41 | fs.readdirSync(testDir).filter(function(file){ 42 | // Only keep the .js files 43 | return file.substr(-3) === '.js'; 44 | 45 | }).forEach(function(file){ 46 | mocha.addFile( 47 | path.join(testDir, file) 48 | ); 49 | }); 50 | 51 | var emitter = new EventEmitter(); 52 | emitter.run = function() { 53 | 54 | var tests = []; 55 | var context = ""; 56 | var separator = ' -> '; 57 | // Run the tests. 58 | try { 59 | var runner = mocha.ui('tdd').run() 60 | .on('test end', function(test) { 61 | // remove comments 62 | var body = test.body.replace(/\/\/.*\n|\/\*.*\*\//g, ''); 63 | // collapse spaces 64 | body = body.replace(/\s+/g,' '); 65 | var obj = { 66 | title: test.title, 67 | context: context.slice(0, -separator.length), 68 | state: test.state, 69 | // body: body, 70 | assertions: analyser(body) 71 | }; 72 | tests.push(obj); 73 | }) 74 | .on('end', function() { 75 | emitter.report = tests; 76 | emitter.emit('done', tests) 77 | }) 78 | .on('suite', function(s) { 79 | context += (s.title + separator); 80 | 81 | }) 82 | .on('suite end', function(s) { 83 | context = context.slice(0, -(s.title.length + separator.length)) 84 | }) 85 | } catch(e) { 86 | throw(e); 87 | } 88 | }; 89 | 90 | module.exports = emitter; 91 | 92 | /* 93 | * Mocha.runner Events: 94 | * can be used to build a better custom report 95 | * 96 | * - `start` execution started 97 | * - `end` execution complete 98 | * - `suite` (suite) test suite execution started 99 | * - `suite end` (suite) all tests (and sub-suites) have finished 100 | * - `test` (test) test execution started 101 | * - `test end` (test) test completed 102 | * - `hook` (hook) hook execution started 103 | * - `hook end` (hook) hook complete 104 | * - `pass` (test) test passed 105 | * - `fail` (test, err) test failed 106 | * - `pending` (test) test pending 107 | */ -------------------------------------------------------------------------------- /tests/1_unit-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * To run the tests on Repl.it, set `NODE_ENV` to `test` 4 | * without quotes in the `.env` file. 5 | * To run the tests in the console, open the terminal 6 | * with [Ctrl + `] (backtick) and run the command `npm run test`. 7 | * 8 | */ 9 | 10 | // import Player from '../public/Player.mjs'; 11 | const Player = require('../public/Player.mjs'); 12 | 13 | // import Collectible from '../public/Collectible.mjs'; 14 | const Collectible = require('../public/Collectible.mjs'); 15 | 16 | const chai = require('chai'); 17 | const assert = chai.assert; 18 | const { JSDOM } = require('jsdom'); 19 | 20 | suite('Unit Tests', () => { 21 | suiteSetup(() => { 22 | // Mock the DOM for testing and load Solver 23 | return JSDOM.fromFile('./views/index.html') 24 | .then((dom) => { 25 | 26 | global.window = dom.window; 27 | global.document = dom.window.document; 28 | }); 29 | }); 30 | 31 | suite('Collectible class', () => { 32 | test('Collectible class generates a collectible item object.', done => { 33 | const testItem = new Collectible({ x: 100, y: 100, id: Date.now() }); 34 | 35 | assert.isObject(testItem); 36 | done(); 37 | }); 38 | 39 | test('Collectible item object contains x and y coordinates and a unique id.', done => { 40 | const testItem = new Collectible({ x: 100, y: 100, id: Date.now() }); 41 | 42 | assert.typeOf(testItem.x, 'Number'); 43 | assert.typeOf(testItem.y, 'Number'); 44 | assert.exists(testItem.id); 45 | done(); 46 | }); 47 | }); 48 | 49 | suite('Player class', () => { 50 | test('Player class generates a player object.', done => { 51 | const testPlayer = new Player({ x: 100, y: 100, score: 0, id: Date.now() }); 52 | 53 | assert.isObject(testPlayer); 54 | done(); 55 | }); 56 | 57 | test('Player object contains a score, x and y coordinates, and a unique id.', done => { 58 | const testPlayer = new Player({ x: 100, y: 100, score: 0, id: Date.now() }); 59 | 60 | assert.typeOf(testPlayer.x, 'Number'); 61 | assert.typeOf(testPlayer.y, 'Number'); 62 | assert.typeOf(testPlayer.score, 'Number'); 63 | assert.exists(testPlayer.id); 64 | done(); 65 | }); 66 | 67 | test("movePlayer(str, num) adjusts a player's position.", done => { 68 | // Note: Only testing movement along the x axis in case 69 | // the game is a 2D platformer 70 | const testPlayer = new Player({ x: 100, y: 100, score: 0, id: Date.now() }); 71 | testPlayer.movePlayer('right', 5); 72 | const testPos1 = { x: testPlayer.x, y: testPlayer.y } 73 | const expectedPos1 = { x: 105, y: 100 } 74 | 75 | testPlayer.movePlayer('left', 10); 76 | const testPos2 = { x: testPlayer.x, y: testPlayer.y } 77 | const expectedPos2 = { x: 95, y: 100 } 78 | 79 | assert.deepEqual(testPos1, expectedPos1); 80 | assert.deepEqual(testPos2, expectedPos2); 81 | done(); 82 | }); 83 | 84 | test("collision(obj) returns true when a player's avatar collides with a collectible item object.", done => { 85 | const testPlayer = new Player({ x: 100, y: 100, id: Date.now() }); 86 | const testItem = new Collectible({ x: 100, y: 100, value: 1, id: Date.now() }); 87 | 88 | assert.isTrue(testPlayer.collision(testItem)); 89 | done(); 90 | }); 91 | 92 | test("calculateRank(arr) returns the player's rank string.", done => { 93 | const testPlayer1 = new Player({ x: 100, y: 100, id: 1 }); 94 | const testPlayer2 = new Player({ x: 150, y: 150, id: 2 }); 95 | testPlayer1.score = 5; 96 | testPlayer2.score = 3; 97 | const testArr = [testPlayer1, testPlayer2]; 98 | 99 | // Account for possible space 100 | assert.match(testPlayer1.calculateRank(testArr), /Rank\: 1\s?\/\s?2/); 101 | assert.match(testPlayer2.calculateRank(testArr), /Rank\: 2\s?\/\s?2/); 102 | done(); 103 | }); 104 | }); 105 | 106 | }); 107 | -------------------------------------------------------------------------------- /tests/2_functional-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * To run the tests on Repl.it, set `NODE_ENV` to `test` 4 | * without quotes in the `.env` file. 5 | * To run the tests in the console, open the terminal 6 | * with [Ctrl + `] (backtick) and run the command `npm run test`. 7 | * 8 | */ 9 | 10 | const chai = require('chai'); 11 | const assert = chai.assert; 12 | const chaiHttp = require('chai-http'); 13 | const server = require('../server'); 14 | 15 | chai.use(chaiHttp); 16 | 17 | suite('Functional Tests', () => { 18 | 19 | suite('Headers test', () => { 20 | test("Prevent the client from trying to guess / sniff the MIME type.", done => { 21 | chai.request(server) 22 | .get('/') 23 | .end((err, res) => { 24 | assert.deepStrictEqual(res.header['x-content-type-options'], 'nosniff'); 25 | done(); 26 | }); 27 | }); 28 | 29 | test("Prevent cross-site scripting (XSS) attacks.", done => { 30 | chai.request(server) 31 | .get('/') 32 | .end((err, res) => { 33 | assert.deepStrictEqual(res.header['x-xss-protection'], '1; mode=block'); 34 | done(); 35 | }); 36 | }); 37 | 38 | test("Nothing from the website is cached in the client.", done => { 39 | chai.request(server) 40 | .get('/') 41 | .end((err, res) => { 42 | assert.deepStrictEqual(res.header['surrogate-control'], 'no-store'); 43 | assert.deepStrictEqual(res.header['cache-control'], 'no-store, no-cache, must-revalidate, proxy-revalidate'); 44 | assert.deepStrictEqual(res.header['pragma'], 'no-cache'); 45 | assert.deepStrictEqual(res.header['expires'], '0'); 46 | done(); 47 | }); 48 | }); 49 | 50 | test("The headers say that the site is powered by 'PHP 7.4.3'.", done => { 51 | chai.request(server) 52 | .get('/') 53 | .end((err, res) => { 54 | assert.deepStrictEqual(res.header['x-powered-by'], 'PHP 7.4.3'); 55 | done(); 56 | }); 57 | }); 58 | }); 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Secure Real-Time Multiplayer Game 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

Secure Real Time Multiplayer Game

18 |
19 |
20 | 21 |
22 |

Controls: wasd

23 |

Rank: 1/1

24 |
25 | 26 |
27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | --------------------------------------------------------------------------------