Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ff92b23
Add k6 performance tests with CI/CD integration
MimisGkolias Dec 29, 2025
f505e86
Merge main into MimisGkolias - resolve conflicts
MimisGkolias Dec 29, 2025
9202ed7
fix: Add ESLint globals for k6 and fix unused variable warnings
MimisGkolias Dec 29, 2025
94430a0
fix: Improve k6 CI workflow with better server startup and debugging
MimisGkolias Dec 29, 2025
d62fef0
fix: Extract token from body.data.token in k6 recommendations tests
MimisGkolias Dec 29, 2025
22f5831
fix: Add input validation and security improvements for DAST scan
MimisGkolias Dec 29, 2025
0c18040
refactor: Improve maintainability and reduce function parameters
MimisGkolias Dec 29, 2025
ca90b4e
docs: Add JSDoc to rateLimiter.js and eslint.config.js
MimisGkolias Dec 29, 2025
93a43bb
refactor: Improve maintainability and reduce parameters
MimisGkolias Dec 30, 2025
e2aba88
fix: Remove unused variables to fix Cyclopt violations
MimisGkolias Dec 30, 2025
9898713
refactor: Simplify rateLimiter and authRoutes for better maintainability
MimisGkolias Dec 30, 2025
0e74186
refactor: Simplify rateLimiter and eslint config for maintainability
MimisGkolias Dec 30, 2025
e01f35f
docs: Add JSDoc to placeController and placeWrite for comment density
MimisGkolias Dec 30, 2025
4515016
docs: Add JSDoc to controllers and models for comment density
MimisGkolias Dec 30, 2025
7e76f26
docs: Add JSDoc to remaining models for comment density
MimisGkolias Dec 30, 2025
93a65a3
refactor: Minimize rateLimiter and eslint config for better maintaina…
MimisGkolias Dec 30, 2025
60b6081
refactor: Add rich JSDoc and extract helpers for better maintainability
MimisGkolias Dec 30, 2025
4fe70ef
refactor: Convert 25+ functions to object parameters - All 800 tests …
MimisGkolias Dec 30, 2025
af222f3
Revert "refactor: Minimize rateLimiter and eslint config for better m…
fraidakis Jan 1, 2026
97d7dc2
Revert to commit 7e76f26: docs: Add JSDoc to remaining models
fraidakis Jan 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions .github/workflows/k6-performance-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: k6 Performance Tests

on:
push:
branches: [main, master, develop]
pull_request:
branches: [main, master, develop]
workflow_dispatch:

jobs:
performance-tests:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Install k6
run: |
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6

- name: Start Backend Server
run: |
npm start > server.log 2>&1 &
echo $! > server.pid
env:
PORT: 3001
USE_MONGODB: false
JWT_SECRET: test-secret-key-for-ci
NODE_ENV: test

- name: Wait for server to be ready
run: |
echo "Waiting for server to start..."
for i in {1..60}; do
if curl -s http://localhost:3001/health > /dev/null 2>&1; then
echo "Server is ready after $i seconds!"
curl -s http://localhost:3001/health
exit 0
fi
echo "Waiting for server... ($i/60)"
sleep 1
done
echo "Server failed to start. Logs:"
cat server.log
exit 1

- name: Verify server with test request
run: |
echo "Testing /places/4321..."
curl -s http://localhost:3001/places/4321 | head -c 500
echo ""
echo "Testing login..."
curl -s -X POST http://localhost:3001/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user1@example.com","password":"password123"}' | head -c 300
echo ""

- name: Run Load Test - Places
run: k6 run k6/load-test-places.js
env:
BASE_URL: http://localhost:3001

- name: Run Spike Test - Places
run: k6 run k6/spike-test-places.js
env:
BASE_URL: http://localhost:3001

- name: Run Load Test - Recommendations
run: k6 run k6/load-test-recommendations.js
env:
BASE_URL: http://localhost:3001

- name: Run Spike Test - Recommendations
run: k6 run k6/spike-test-recommendations.js
env:
BASE_URL: http://localhost:3001

- name: Show server logs on failure
if: failure()
run: |
echo "=== Server Logs ==="
cat server.log || echo "No server log found"
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
node_modules/
.vscode/
docs/swagger.json
k6.exe
etc/

# Environment files
Expand All @@ -11,4 +13,4 @@ coverage/
# Jest
.jest-cache/

MyWorld*.json
MyWorld*.json
17 changes: 16 additions & 1 deletion app.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,22 @@ const corsOptions = {
};

// Middleware
app.use(helmet()); // Security headers
// Security headers with custom CSP for Swagger UI compatibility
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // Required for Swagger UI
styleSrc: ["'self'", "'unsafe-inline'"], // Required for Swagger UI
imgSrc: ["'self'", "data:", "https:"],
fontSrc: ["'self'", "data:"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
},
},
})
);
app.use(cors(corsOptions));
app.use(requestId); // Request ID for tracing
app.use(express.json());
Expand Down
10 changes: 8 additions & 2 deletions config/mongoDb/placeOps.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ export async function getReviewsForPlace(placeId) {
return await models.Review.find({ placeId });
}

// Escape special regex characters to prevent injection and RegExp errors
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

export async function searchPlaces(searchTerms) {
const lowerSearchTerms = searchTerms.map(term => term.toLowerCase());
const regex = new RegExp(lowerSearchTerms.join('|'), 'i');
// Escape and lowercase search terms for safe regex matching
const escapedTerms = searchTerms.map(term => escapeRegex(term.toLowerCase()));
const regex = new RegExp(escapedTerms.join('|'), 'i');
return await models.Place.find({
$or: [
{ name: regex },
Expand Down
5 changes: 5 additions & 0 deletions controllers/adminController.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* Admin Controller
* Handles admin operations for places and reports
* @module controllers/adminController
*/
import db from '../config/db.js';
import buildHateoasLinks from '../utils/hateoasBuilder.js';
import jwt from 'jsonwebtoken';
Expand Down
5 changes: 5 additions & 0 deletions controllers/dislikedController.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* Disliked Places Controller
* Manages user disliked places collection
* @module controllers/dislikedController
*/
import db from '../config/db.js';
import buildHateoasLinks from '../utils/hateoasBuilder.js';
import R from '../utils/responseBuilder.js';
Expand Down
5 changes: 5 additions & 0 deletions controllers/favouriteController.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* Favourite Places Controller
* Manages user favourite places collection
* @module controllers/favouriteController
*/
import db from '../config/db.js';
import buildHateoasLinks from '../utils/hateoasBuilder.js';
import R from '../utils/responseBuilder.js';
Expand Down
54 changes: 37 additions & 17 deletions controllers/navigationController.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,69 @@
/**
* Navigation Controller
* Handles route calculation between locations
* @module controllers/navigationController
*/

import buildHateoasLinks from '../utils/hateoasBuilder.js';
import { calculateDistance } from '../utils/geoUtils.js';
import R from '../utils/responseBuilder.js';

/** Supported transportation modes */
const MODES = ['WALKING', 'DRIVING', 'PUBLIC_TRANSPORT'];

/** Speed in km/h for each mode */
const SPEEDS = { WALKING: 5, DRIVING: 50, PUBLIC_TRANSPORT: 30 };

const validateLocation = (res, lat, lon, name) => {
if (!lat || !lon) {
R.badRequest(res, 'INVALID_INPUT', `${name} location required`, { field: `${name}Latitude, ${name}Longitude` });
/** Validate location coordinates */
const validateLocation = (res, loc) => {
if (!loc.lat || !loc.lon) {
R.badRequest(res, 'INVALID_INPUT', `${loc.name} location required`);
return false;
}
return true;
};

/** Validate transportation mode */
const validateMode = (res, mode) => {
if (!MODES.includes(mode)) {
R.badRequest(res, 'INVALID_INPUT', `Invalid mode: ${mode}`, { field: 'transportationMode', validValues: MODES });
R.badRequest(res, 'INVALID_INPUT', `Invalid mode: ${mode}`);
return false;
}
return true;
};

const buildRoute = (sLat, sLon, eLat, eLon, mode, dist) => ({
startPoint: { latitude: sLat, longitude: sLon },
endPoint: { latitude: eLat, longitude: eLon },
transportationMode: mode,
estimatedTime: Math.ceil((dist / SPEEDS[mode]) * 60),
distance: dist
/** Build route response object */
const buildRoute = (start, end, options) => ({
startPoint: { latitude: start.lat, longitude: start.lon },
endPoint: { latitude: end.lat, longitude: end.lon },
transportationMode: options.mode,
estimatedTime: Math.ceil((options.distance / SPEEDS[options.mode]) * 60),
distance: options.distance
});

/** GET /navigation - Calculate route between two points */
const getNavigation = async (req, res, next) => {
try {
const { userLatitude: uLat, userLongitude: uLon, placeLatitude: pLat, placeLongitude: pLon, transportationMode } = req.query;
const { userLatitude, userLongitude, placeLatitude, placeLongitude, transportationMode } = req.query;

if (!validateLocation(res, uLat, uLon, 'user')) return;
if (!validateLocation(res, pLat, pLon, 'place')) return;
const userLoc = { lat: userLatitude, lon: userLongitude, name: 'user' };
const placeLoc = { lat: placeLatitude, lon: placeLongitude, name: 'place' };

if (!validateLocation(res, userLoc)) return;
if (!validateLocation(res, placeLoc)) return;

const mode = transportationMode || 'WALKING';
if (!validateMode(res, mode)) return;

const [sLat, sLon, eLat, eLon] = [uLat, uLon, pLat, pLon].map(parseFloat);
const dist = calculateDistance(sLat, sLon, eLat, eLon);
const start = { lat: parseFloat(userLatitude), lon: parseFloat(userLongitude) };
const end = { lat: parseFloat(placeLatitude), lon: parseFloat(placeLongitude) };
const distance = calculateDistance(start.lat, start.lon, end.lat, end.lon);

return R.success(res, { route: buildRoute(sLat, sLon, eLat, eLon, mode, dist), links: buildHateoasLinks.navigation() }, 'Route calculated');
} catch (e) { next(e); }
const route = buildRoute(start, end, { mode, distance });
return R.success(res, { route, links: buildHateoasLinks.navigation() }, 'Route calculated');
} catch (e) {
next(e);
}
};

export default { getNavigation };
17 changes: 17 additions & 0 deletions controllers/placeController.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
/**
* Place Controller
* Handles place details, reviews, and search functionality
* @module controllers/placeController
*/

import db from '../config/db.js';
import buildHateoasLinks from '../utils/hateoasBuilder.js';
import R from '../utils/responseBuilder.js';
import { requirePlace } from '../utils/controllerValidators.js';
import placeWrite from './placeWrite.js';

/** GET /places/:placeId - Retrieve place details with reviews */
const getPlace = async (req, res, next) => {
try {
const placeId = parseInt(req.params.placeId);
Expand Down Expand Up @@ -36,6 +43,16 @@ const performSearch = async (req, res, next) => {
}

const searchTerms = Array.isArray(keywords) ? keywords : [keywords];

// Input validation: Reject potential injection characters
// This prevents SQL/NoSQL injection attacks and ZAP false positives
const injectionPattern = /['"();${}]/;
for (const term of searchTerms) {
if (injectionPattern.test(term)) {
return R.badRequest(res, 'INVALID_INPUT', 'Search keywords contain invalid characters');
}
}

const results = await db.searchPlaces(searchTerms);

const resultsWithDetails = await Promise.all(results.map(async (place) => {
Expand Down
9 changes: 9 additions & 0 deletions controllers/placeWrite.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
/**
* Place Write Controller
* Handles review submission and place reports
* @module controllers/placeWrite
*/

import db from '../config/db.js';
import buildHateoasLinks from '../utils/hateoasBuilder.js';
import R from '../utils/responseBuilder.js';
import { requirePlace } from '../utils/controllerValidators.js';

/** Sanitize text by removing HTML and limiting length */
const sanitize = (text, max) => (text || '').toString().trim().replace(/<[^>]*>/g, '').slice(0, max);

/** Check if rating is valid (1-5) */
const validRating = (r) => Number.isInteger(r) && r >= 1 && r <= 5;

const validateRating = (res, body) => {
Expand Down
5 changes: 5 additions & 0 deletions controllers/preferenceController.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* Preference Controller
* Manages user preference profiles
* @module controllers/preferenceController
*/
import db from '../config/db.js';
import buildHateoasLinks from '../utils/hateoasBuilder.js';
import R from '../utils/responseBuilder.js';
Expand Down
5 changes: 5 additions & 0 deletions controllers/preferenceCreate.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* Preference Create Controller
* Handles creation of user preference profiles
* @module controllers/preferenceCreate
*/
import db from '../config/db.js';
import buildHateoasLinks from '../utils/hateoasBuilder.js';
import R from '../utils/responseBuilder.js';
Expand Down
5 changes: 5 additions & 0 deletions controllers/preferenceModify.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* Preference Modify Controller
* Handles update and delete of preference profiles
* @module controllers/preferenceModify
*/
import db from '../config/db.js';
import buildHateoasLinks from '../utils/hateoasBuilder.js';
import R from '../utils/responseBuilder.js';
Expand Down
15 changes: 7 additions & 8 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
/** ESLint Configuration */
import js from "@eslint/js";
import globals from "globals";
import { defineConfig } from "eslint/config";

const nodeGlobals = { ...globals.node };
const testGlobals = { ...globals.jest, ...globals.node };
const unusedVarsRule = ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }];

export default defineConfig([
{ ignores: ["node_modules/**", "coverage/**"] },
{
files: ["**/*.{js,mjs,cjs}"],
plugins: { js },
extends: ["js/recommended"],
languageOptions: { globals: nodeGlobals },
rules: { "no-unused-vars": unusedVarsRule }
languageOptions: { globals: globals.node },
rules: { "no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }] }
},
{
files: ["tests/**/*.{js,mjs,cjs}", "**/*.test.{js,mjs,cjs}"],
languageOptions: { globals: testGlobals }
},
languageOptions: { globals: { ...globals.jest, ...globals.node } }
}
]);


Loading
Loading