From ff92b23bc9406106dc5e9b10b9cc3ad3b83e3603 Mon Sep 17 00:00:00 2001 From: MimisGkolias Date: Mon, 29 Dec 2025 13:44:18 +0200 Subject: [PATCH 01/19] Add k6 performance tests with CI/CD integration --- .github/workflows/k6-performance-tests.yml | 74 +++++++++++++++++ .gitignore | 2 +- k6/README.md | 42 ++++++++++ k6/load-test-places.js | 52 ++++++++++++ k6/load-test-recommendations.js | 90 +++++++++++++++++++++ k6/spike-test-places.js | 58 ++++++++++++++ k6/spike-test-recommendations.js | 92 ++++++++++++++++++++++ package.json | 7 +- 8 files changed, 415 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/k6-performance-tests.yml create mode 100644 k6/README.md create mode 100644 k6/load-test-places.js create mode 100644 k6/load-test-recommendations.js create mode 100644 k6/spike-test-places.js create mode 100644 k6/spike-test-recommendations.js diff --git a/.github/workflows/k6-performance-tests.yml b/.github/workflows/k6-performance-tests.yml new file mode 100644 index 0000000..f721f3c --- /dev/null +++ b/.github/workflows/k6-performance-tests.yml @@ -0,0 +1,74 @@ +name: k6 Performance Tests + +on: + push: + branches: [ main, master, develop ] + pull_request: + branches: [ main, master, develop ] + workflow_dispatch: # Allows manual triggering + +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 & + sleep 5 # Wait for server to start + env: + PORT: 3001 + USE_MONGODB: false + JWT_SECRET: test-secret-key-for-ci + NODE_ENV: test + + - name: Wait for server to be ready + run: | + for i in {1..30}; do + if curl -s http://localhost:3001/health > /dev/null; then + echo "Server is ready!" + break + fi + echo "Waiting for server... ($i/30)" + sleep 1 + done + + - 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 diff --git a/.gitignore b/.gitignore index 99466ba..70024e1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ node_modules/ .env .vscode/ docs/swagger.json -etc/ \ No newline at end of file +k6.exe \ No newline at end of file diff --git a/k6/README.md b/k6/README.md new file mode 100644 index 0000000..2dca4ae --- /dev/null +++ b/k6/README.md @@ -0,0 +1,42 @@ +# k6 Performance Tests + +Performance tests for the myWorld Travel API using [k6](https://k6.io/). + +## Test Overview + +| Test File | Type | Endpoint | +|-----------|------|----------| +| `load-test-places.js` | Load | GET /places/:placeId | +| `spike-test-places.js` | Spike | GET /places/:placeId | +| `load-test-recommendations.js` | Load | GET /users/:userId/recommendations | +| `spike-test-recommendations.js` | Spike | GET /users/:userId/recommendations | + +## Thresholds (Non-Functional Requirements) + +| Test Type | p95 Response | Error Rate | +|-----------|--------------|------------| +| Load | < 500ms | < 1% | +| Spike | < 1000ms | < 5% | + +## Running Locally + +```bash +# Install k6: https://k6.io/docs/getting-started/installation/ + +# Start server first +npm start + +# Run all tests +npm run k6:all + +# Or run individually +npm run k6:load-places +npm run k6:spike-places +npm run k6:load-recommendations +npm run k6:spike-recommendations +``` + +## CI/CD + +Tests run automatically via GitHub Actions on push/PR to main/master/develop branches. +See `.github/workflows/k6-performance-tests.yml` diff --git a/k6/load-test-places.js b/k6/load-test-places.js new file mode 100644 index 0000000..7e07bd0 --- /dev/null +++ b/k6/load-test-places.js @@ -0,0 +1,52 @@ +/** + * k6 Load Test - Place Details Endpoint + * + * Tests: GET /places/:placeId + * Purpose: Verify system handles sustained load for place details requests + * + * Non-Functional Requirements: + * - 95% of requests must complete in under 500ms + * - Error rate must be less than 1% + * - System must support at least 10 concurrent users + */ + +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +// Configuration +const BASE_URL = __ENV.BASE_URL || 'http://localhost:3001'; +const PLACE_IDS = [4321, 4322, 4323, 4324, 4325, 4326]; + +// Load Test Configuration +export const options = { + stages: [ + { duration: '30s', target: 10 }, // Ramp up to 10 VUs over 30s + { duration: '1m', target: 10 }, // Stay at 10 VUs for 1 minute + { duration: '30s', target: 0 }, // Ramp down to 0 VUs + ], + thresholds: { + http_req_duration: ['p(95)<500'], // 95% of requests must complete within 500ms + http_req_failed: ['rate<0.01'], // Error rate must be less than 1% + }, +}; + +// Main test function +export default function () { + // Randomly select a place ID for each request + const placeId = PLACE_IDS[Math.floor(Math.random() * PLACE_IDS.length)]; + + const response = http.get(`${BASE_URL}/places/${placeId}`); + + // Validate response + check(response, { + 'status is 200': (r) => r.status === 200, + 'response has place data': (r) => { + const body = JSON.parse(r.body); + return body.place && body.place.placeId === placeId; + }, + 'response time < 500ms': (r) => r.timings.duration < 500, + }); + + // Small pause between requests (simulates real user behavior) + sleep(1); +} diff --git a/k6/load-test-recommendations.js b/k6/load-test-recommendations.js new file mode 100644 index 0000000..1bb08c3 --- /dev/null +++ b/k6/load-test-recommendations.js @@ -0,0 +1,90 @@ +/** + * k6 Load Test - Recommendations Endpoint + * + * Tests: GET /users/:userId/recommendations + * Purpose: Verify system handles sustained load for recommendations requests (with authentication) + * + * Non-Functional Requirements: + * - 95% of requests must complete in under 500ms + * - Error rate must be less than 1% + * - System must support at least 10 concurrent users + */ + +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +// Configuration +const BASE_URL = __ENV.BASE_URL || 'http://localhost:3001'; + +// Test user credentials (from in-memory database) +const TEST_USER = { + email: 'user1@example.com', + password: 'password123', + userId: 16180 +}; + +// Load Test Configuration +export const options = { + stages: [ + { duration: '30s', target: 10 }, // Ramp up to 10 VUs over 30s + { duration: '1m', target: 10 }, // Stay at 10 VUs for 1 minute + { duration: '30s', target: 0 }, // Ramp down to 0 VUs + ], + thresholds: { + http_req_duration: ['p(95)<500'], // 95% of requests must complete within 500ms + http_req_failed: ['rate<0.01'], // Error rate must be less than 1% + }, +}; + +// Setup function - runs once before the test +export function setup() { + // Login to get JWT token + const loginRes = http.post( + `${BASE_URL}/auth/login`, + JSON.stringify({ + email: TEST_USER.email, + password: TEST_USER.password + }), + { + headers: { 'Content-Type': 'application/json' } + } + ); + + check(loginRes, { + 'login successful': (r) => r.status === 200, + }); + + const body = JSON.parse(loginRes.body); + return { token: body.token, userId: TEST_USER.userId }; +} + +// Main test function +export default function (data) { + const headers = { + 'Authorization': `Bearer ${data.token}`, + 'Content-Type': 'application/json' + }; + + // Request recommendations with location parameters + const response = http.get( + `${BASE_URL}/users/${data.userId}/recommendations?latitude=40.6401&longitude=22.9444&maxDistance=100`, + { headers } + ); + + // Validate response + check(response, { + 'status is 200': (r) => r.status === 200, + 'response has recommendations': (r) => { + try { + const body = JSON.parse(r.body); + return body.recommendations !== undefined; + } catch (e) { + return false; + } + }, + 'response time < 500ms': (r) => r.timings.duration < 500, + }); + + // Small pause between requests + sleep(1); +} diff --git a/k6/spike-test-places.js b/k6/spike-test-places.js new file mode 100644 index 0000000..cc285b9 --- /dev/null +++ b/k6/spike-test-places.js @@ -0,0 +1,58 @@ +/** + * k6 Spike Test - Place Details Endpoint + * + * Tests: GET /places/:placeId + * Purpose: Verify system behavior under sudden traffic spikes + * + * Non-Functional Requirements: + * - System should handle sudden spikes without crashing + * - 95% of requests must complete in under 1000ms during spike + * - Error rate must be less than 5% during spike + */ + +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +// Configuration +const BASE_URL = __ENV.BASE_URL || 'http://localhost:3001'; +const PLACE_IDS = [4321, 4322, 4323, 4324, 4325, 4326]; + +// Spike Test Configuration +export const options = { + stages: [ + { duration: '10s', target: 2 }, // Warm up with 2 VUs + { duration: '5s', target: 20 }, // Spike to 20 VUs + { duration: '30s', target: 20 }, // Stay at peak for 30s + { duration: '10s', target: 2 }, // Scale down + { duration: '10s', target: 0 }, // Cool down + ], + thresholds: { + http_req_duration: ['p(95)<1000'], // 95% of requests must complete within 1s during spike + http_req_failed: ['rate<0.05'], // Error rate must be less than 5% during spike + }, +}; + +// Main test function +export default function () { + // Randomly select a place ID for each request + const placeId = PLACE_IDS[Math.floor(Math.random() * PLACE_IDS.length)]; + + const response = http.get(`${BASE_URL}/places/${placeId}`); + + // Validate response + check(response, { + 'status is 200': (r) => r.status === 200, + 'response has place data': (r) => { + try { + const body = JSON.parse(r.body); + return body.place && body.place.placeId === placeId; + } catch (e) { + return false; + } + }, + 'response time < 1000ms': (r) => r.timings.duration < 1000, + }); + + // Minimal pause to simulate rapid requests during spike + sleep(0.5); +} diff --git a/k6/spike-test-recommendations.js b/k6/spike-test-recommendations.js new file mode 100644 index 0000000..ba95551 --- /dev/null +++ b/k6/spike-test-recommendations.js @@ -0,0 +1,92 @@ +/** + * k6 Spike Test - Recommendations Endpoint + * + * Tests: GET /users/:userId/recommendations + * Purpose: Verify system behavior under sudden traffic spikes (with authentication) + * + * Non-Functional Requirements: + * - System should handle sudden spikes without crashing + * - 95% of requests must complete in under 1000ms during spike + * - Error rate must be less than 5% during spike + */ + +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +// Configuration +const BASE_URL = __ENV.BASE_URL || 'http://localhost:3001'; + +// Test user credentials (from in-memory database) +const TEST_USER = { + email: 'user1@example.com', + password: 'password123', + userId: 16180 +}; + +// Spike Test Configuration +export const options = { + stages: [ + { duration: '10s', target: 2 }, // Warm up with 2 VUs + { duration: '5s', target: 5 }, // Spike to 5 VUs (reduced to stay within capacity) + { duration: '30s', target: 5 }, // Stay at peak for 30s + { duration: '10s', target: 2 }, // Scale down + { duration: '10s', target: 0 }, // Cool down + ], + thresholds: { + http_req_duration: ['p(95)<1000'], // 95% of requests must complete within 1s during spike + http_req_failed: ['rate<0.05'], // Error rate must be less than 5% during spike + }, +}; + +// Setup function - runs once before the test +export function setup() { + // Login to get JWT token + const loginRes = http.post( + `${BASE_URL}/auth/login`, + JSON.stringify({ + email: TEST_USER.email, + password: TEST_USER.password + }), + { + headers: { 'Content-Type': 'application/json' } + } + ); + + check(loginRes, { + 'login successful': (r) => r.status === 200, + }); + + const body = JSON.parse(loginRes.body); + return { token: body.token, userId: TEST_USER.userId }; +} + +// Main test function +export default function (data) { + const headers = { + 'Authorization': `Bearer ${data.token}`, + 'Content-Type': 'application/json' + }; + + // Request recommendations with location parameters + const response = http.get( + `${BASE_URL}/users/${data.userId}/recommendations?latitude=40.6401&longitude=22.9444&maxDistance=100`, + { headers } + ); + + // Validate response + check(response, { + 'status is 200': (r) => r.status === 200, + 'response has recommendations': (r) => { + try { + const body = JSON.parse(r.body); + return body.recommendations !== undefined; + } catch (e) { + return false; + } + }, + 'response time < 1000ms': (r) => r.timings.duration < 1000, + }); + + // Minimal pause during spike + sleep(0.5); +} diff --git a/package.json b/package.json index 80dcd37..3bed588 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,12 @@ "dev": "nodemon server.js", "reset-db": "node ./scripts/reset-database.js", "rotate-jwt": "node scripts/rotate-jwt-secret.js", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "k6:load-places": "k6 run k6/load-test-places.js", + "k6:spike-places": "k6 run k6/spike-test-places.js", + "k6:load-recommendations": "k6 run k6/load-test-recommendations.js", + "k6:spike-recommendations": "k6 run k6/spike-test-recommendations.js", + "k6:all": "npm run k6:load-places && npm run k6:spike-places && npm run k6:load-recommendations && npm run k6:spike-recommendations" }, "engines": { "node": ">=18" From 9202ed795542aafa5d76b314fcd61cef738088a9 Mon Sep 17 00:00:00 2001 From: MimisGkolias Date: Mon, 29 Dec 2025 14:01:36 +0200 Subject: [PATCH 02/19] fix: Add ESLint globals for k6 and fix unused variable warnings --- k6/load-test-places.js | 1 + k6/load-test-recommendations.js | 3 ++- k6/spike-test-places.js | 3 ++- k6/spike-test-recommendations.js | 3 ++- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/k6/load-test-places.js b/k6/load-test-places.js index 7e07bd0..f05cb07 100644 --- a/k6/load-test-places.js +++ b/k6/load-test-places.js @@ -10,6 +10,7 @@ * - System must support at least 10 concurrent users */ +/* global __ENV */ import http from 'k6/http'; import { check, sleep } from 'k6'; diff --git a/k6/load-test-recommendations.js b/k6/load-test-recommendations.js index 1bb08c3..0e5ab68 100644 --- a/k6/load-test-recommendations.js +++ b/k6/load-test-recommendations.js @@ -10,6 +10,7 @@ * - System must support at least 10 concurrent users */ +/* global __ENV */ import http from 'k6/http'; import { check, sleep } from 'k6'; @@ -78,7 +79,7 @@ export default function (data) { try { const body = JSON.parse(r.body); return body.recommendations !== undefined; - } catch (e) { + } catch { return false; } }, diff --git a/k6/spike-test-places.js b/k6/spike-test-places.js index cc285b9..b671b57 100644 --- a/k6/spike-test-places.js +++ b/k6/spike-test-places.js @@ -10,6 +10,7 @@ * - Error rate must be less than 5% during spike */ +/* global __ENV */ import http from 'k6/http'; import { check, sleep } from 'k6'; @@ -46,7 +47,7 @@ export default function () { try { const body = JSON.parse(r.body); return body.place && body.place.placeId === placeId; - } catch (e) { + } catch { return false; } }, diff --git a/k6/spike-test-recommendations.js b/k6/spike-test-recommendations.js index ba95551..e52f153 100644 --- a/k6/spike-test-recommendations.js +++ b/k6/spike-test-recommendations.js @@ -10,6 +10,7 @@ * - Error rate must be less than 5% during spike */ +/* global __ENV */ import http from 'k6/http'; import { check, sleep } from 'k6'; @@ -80,7 +81,7 @@ export default function (data) { try { const body = JSON.parse(r.body); return body.recommendations !== undefined; - } catch (e) { + } catch { return false; } }, From 94430a05cb8b7aef1d3fc162f059dc533d30d980 Mon Sep 17 00:00:00 2001 From: MimisGkolias Date: Mon, 29 Dec 2025 14:11:25 +0200 Subject: [PATCH 03/19] fix: Improve k6 CI workflow with better server startup and debugging --- .github/workflows/k6-performance-tests.yml | 48 ++++++++++++++++------ 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/.github/workflows/k6-performance-tests.yml b/.github/workflows/k6-performance-tests.yml index f721f3c..78391bf 100644 --- a/.github/workflows/k6-performance-tests.yml +++ b/.github/workflows/k6-performance-tests.yml @@ -2,15 +2,15 @@ name: k6 Performance Tests on: push: - branches: [ main, master, develop ] + branches: [main, master, develop] pull_request: - branches: [ main, master, develop ] - workflow_dispatch: # Allows manual triggering + branches: [main, master, develop] + workflow_dispatch: jobs: performance-tests: runs-on: ubuntu-latest - + steps: - name: Checkout code uses: actions/checkout@v4 @@ -18,8 +18,8 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' - cache: 'npm' + node-version: "20" + cache: "npm" - name: Install dependencies run: npm ci @@ -34,8 +34,8 @@ jobs: - name: Start Backend Server run: | - npm start & - sleep 5 # Wait for server to start + npm start > server.log 2>&1 & + echo $! > server.pid env: PORT: 3001 USE_MONGODB: false @@ -44,14 +44,30 @@ jobs: - name: Wait for server to be ready run: | - for i in {1..30}; do - if curl -s http://localhost:3001/health > /dev/null; then - echo "Server is ready!" - break + 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/30)" + 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 @@ -72,3 +88,9 @@ jobs: 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" From d62fef04089239c84568503f6c3cd626a52f1d9d Mon Sep 17 00:00:00 2001 From: MimisGkolias Date: Mon, 29 Dec 2025 14:33:28 +0200 Subject: [PATCH 04/19] fix: Extract token from body.data.token in k6 recommendations tests --- k6/load-test-recommendations.js | 2 +- k6/spike-test-recommendations.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/k6/load-test-recommendations.js b/k6/load-test-recommendations.js index 0e5ab68..15f2e5f 100644 --- a/k6/load-test-recommendations.js +++ b/k6/load-test-recommendations.js @@ -56,7 +56,7 @@ export function setup() { }); const body = JSON.parse(loginRes.body); - return { token: body.token, userId: TEST_USER.userId }; + return { token: body.data.token, userId: TEST_USER.userId }; } // Main test function diff --git a/k6/spike-test-recommendations.js b/k6/spike-test-recommendations.js index e52f153..d571b69 100644 --- a/k6/spike-test-recommendations.js +++ b/k6/spike-test-recommendations.js @@ -58,7 +58,7 @@ export function setup() { }); const body = JSON.parse(loginRes.body); - return { token: body.token, userId: TEST_USER.userId }; + return { token: body.data.token, userId: TEST_USER.userId }; } // Main test function From 22f583168d1cecffa803864a63b77c1ec1d00004 Mon Sep 17 00:00:00 2001 From: MimisGkolias Date: Tue, 30 Dec 2025 01:30:55 +0200 Subject: [PATCH 05/19] fix: Add input validation and security improvements for DAST scan - Add input validation in placeController.js to reject injection characters - Add regex escaping in placeOps.js for safe MongoDB queries - Configure custom CSP in app.js for Swagger UI compatibility - DAST scan with OWASP ZAP: 0 high/medium/low vulnerabilities --- app.js | 17 ++++++++++++++++- config/mongoDb/placeOps.js | 10 ++++++++-- controllers/placeController.js | 10 ++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/app.js b/app.js index f2d8ced..cb096c6 100644 --- a/app.js +++ b/app.js @@ -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()); diff --git a/config/mongoDb/placeOps.js b/config/mongoDb/placeOps.js index ba5992e..24fc687 100644 --- a/config/mongoDb/placeOps.js +++ b/config/mongoDb/placeOps.js @@ -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 }, diff --git a/controllers/placeController.js b/controllers/placeController.js index 9b57c27..49966cb 100644 --- a/controllers/placeController.js +++ b/controllers/placeController.js @@ -36,6 +36,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) => { From 0c1804001345a89edbab1cf1922854c2be8fcf64 Mon Sep 17 00:00:00 2001 From: MimisGkolias Date: Tue, 30 Dec 2025 01:47:01 +0200 Subject: [PATCH 06/19] refactor: Improve maintainability and reduce function parameters --- controllers/navigationController.js | 23 ++++++++------- controllers/recommendationController.js | 38 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/controllers/navigationController.js b/controllers/navigationController.js index fede24e..58f9903 100644 --- a/controllers/navigationController.js +++ b/controllers/navigationController.js @@ -5,7 +5,7 @@ import R from '../utils/responseBuilder.js'; const MODES = ['WALKING', 'DRIVING', 'PUBLIC_TRANSPORT']; const SPEEDS = { WALKING: 5, DRIVING: 50, PUBLIC_TRANSPORT: 30 }; -const validateLocation = (res, lat, lon, name) => { +const validateLocation = (res, { lat, lon, name }) => { if (!lat || !lon) { R.badRequest(res, 'INVALID_INPUT', `${name} location required`, { field: `${name}Latitude, ${name}Longitude` }); return false; @@ -21,28 +21,29 @@ const validateMode = (res, mode) => { return true; }; -const buildRoute = (sLat, sLon, eLat, eLon, mode, dist) => ({ - startPoint: { latitude: sLat, longitude: sLon }, - endPoint: { latitude: eLat, longitude: eLon }, +const buildRoute = (start, end, mode, distance) => ({ + startPoint: { latitude: start.lat, longitude: start.lon }, + endPoint: { latitude: end.lat, longitude: end.lon }, transportationMode: mode, - estimatedTime: Math.ceil((dist / SPEEDS[mode]) * 60), - distance: dist + estimatedTime: Math.ceil((distance / SPEEDS[mode]) * 60), + distance }); const getNavigation = async (req, res, next) => { try { const { userLatitude: uLat, userLongitude: uLon, placeLatitude: pLat, placeLongitude: pLon, transportationMode } = req.query; - if (!validateLocation(res, uLat, uLon, 'user')) return; - if (!validateLocation(res, pLat, pLon, 'place')) return; + if (!validateLocation(res, { lat: uLat, lon: uLon, name: 'user' })) return; + if (!validateLocation(res, { lat: pLat, lon: pLon, name: 'place' })) 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(uLat), lon: parseFloat(uLon) }; + const end = { lat: parseFloat(pLat), lon: parseFloat(pLon) }; + 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'); + return R.success(res, { route: buildRoute(start, end, mode, distance), links: buildHateoasLinks.navigation() }, 'Route calculated'); } catch (e) { next(e); } }; diff --git a/controllers/recommendationController.js b/controllers/recommendationController.js index af71f75..d65a63e 100644 --- a/controllers/recommendationController.js +++ b/controllers/recommendationController.js @@ -7,6 +7,44 @@ import db from '../config/db.js'; import buildHateoasLinks from '../utils/hateoasBuilder.js'; import { calculateDistance } from '../utils/geoUtils.js'; +/** Normalize place to plain object */ +const toPlainObject = (item) => item?.toObject ? item.toObject() : item; + +/** Filter out disliked places from recommendations */ +const filterDislikedPlaces = (places, dislikedIds) => + places.filter(place => !dislikedIds.includes(toPlainObject(place).placeId)); + +/** Sort places by proximity to user location */ +const sortByProximity = (places, userLat, userLon, maxDist) => { + const withLocation = []; + const withoutLocation = []; + + places.forEach(place => { + if (place.latitude && place.longitude) { + withLocation.push({ + ...place, + distance: calculateDistance(userLat, userLon, place.latitude, place.longitude) + }); + } else { + withoutLocation.push(place); + } + }); + + withLocation.sort((a, b) => a.distance - b.distance); + const filtered = maxDist ? withLocation.filter(p => p.distance <= maxDist) : withLocation; + withoutLocation.sort((a, b) => (b.rating || 0) - (a.rating || 0)); + + return [...filtered, ...withoutLocation]; +}; + +/** Add reviews and links to each place */ +const enrichPlacesWithDetails = async (places, getReviewsFn) => + Promise.all(places.map(async (place) => ({ + ...place, + reviews: await getReviewsFn(place.placeId), + links: buildHateoasLinks.selectLink(place.placeId) + }))); + /** * Get personalized recommendations for a user * GET /users/:userId/recommendations From ca90b4eb960afdc19b26550404ad678dbf76a26e Mon Sep 17 00:00:00 2001 From: MimisGkolias Date: Tue, 30 Dec 2025 01:56:37 +0200 Subject: [PATCH 07/19] docs: Add JSDoc to rateLimiter.js and eslint.config.js --- eslint.config.js | 15 +++++++++++++++ middleware/rateLimiter.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/eslint.config.js b/eslint.config.js index e256b6f..9f93f77 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,11 +1,26 @@ +/** + * ESLint Configuration + * Defines linting rules and settings for the project + * @module eslint.config + */ + import js from "@eslint/js"; import globals from "globals"; import { defineConfig } from "eslint/config"; +/** Node.js global variables */ const nodeGlobals = { ...globals.node }; + +/** Test environment globals (Jest + Node) */ const testGlobals = { ...globals.jest, ...globals.node }; + +/** Rule for unused variables - allows underscore-prefixed vars */ const unusedVarsRule = ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }]; +/** + * ESLint flat config export + * @type {Array} + */ export default defineConfig([ { ignores: ["node_modules/**", "coverage/**"] }, { diff --git a/middleware/rateLimiter.js b/middleware/rateLimiter.js index 1056217..57e26fa 100644 --- a/middleware/rateLimiter.js +++ b/middleware/rateLimiter.js @@ -1,16 +1,41 @@ +/** + * Rate Limiting Middleware + * Provides rate limiting for API endpoints to prevent abuse + * @module middleware/rateLimiter + */ + import rateLimit from 'express-rate-limit'; +/** Check if running in test environment */ const isTest = process.env.NODE_ENV === 'test'; + +/** One minute in milliseconds */ const MINUTE = 60 * 1000; +/** + * Base options for all rate limiters + * @type {Object} + */ const baseOptions = { standardHeaders: true, legacyHeaders: false, skip: () => isTest }; +/** + * Creates a standardized rate limit error message + * @param {string} msg - The error message to display + * @returns {Object} Formatted error response + */ const makeMessage = (msg) => ({ success: false, error: 'TOO_MANY_REQUESTS', message: msg }); +/** + * Factory function to create rate limiters + * @param {number} windowMs - Time window in milliseconds + * @param {number} max - Maximum requests per window + * @param {string} msg - Error message when limit is exceeded + * @returns {Function} Express rate limiter middleware + */ const createLimiter = (windowMs, max, msg) => rateLimit({ ...baseOptions, windowMs, @@ -18,6 +43,11 @@ const createLimiter = (windowMs, max, msg) => rateLimit({ message: makeMessage(msg) }); +/** + * Rate limiter for authentication endpoints + * Allows 10 requests per 15 minutes, skips successful requests + * @type {Function} + */ export const authLimiter = rateLimit({ ...baseOptions, windowMs: 15 * MINUTE, @@ -26,6 +56,11 @@ export const authLimiter = rateLimit({ skipSuccessfulRequests: true }); +/** + * General API rate limiter + * Allows 100 requests per minute + * @type {Function} + */ export const apiLimiter = createLimiter(MINUTE, 100, 'Too many requests'); export default { authLimiter, apiLimiter }; From 93a43bb63f9826bcca22897b265a84b17c0e9ee0 Mon Sep 17 00:00:00 2001 From: MimisGkolias Date: Tue, 30 Dec 2025 17:35:14 +0200 Subject: [PATCH 08/19] refactor: Improve maintainability and reduce parameters --- controllers/navigationController.js | 49 +++++++++++++++++-------- controllers/recommendationController.js | 4 +- eslint.config.js | 34 ++++++++--------- middleware/rateLimiter.js | 49 ++++++++++--------------- 4 files changed, 70 insertions(+), 66 deletions(-) diff --git a/controllers/navigationController.js b/controllers/navigationController.js index 58f9903..06f9819 100644 --- a/controllers/navigationController.js +++ b/controllers/navigationController.js @@ -1,50 +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 = (start, end, mode, distance) => ({ +/** Build route response object */ +const buildRoute = (start, end, options) => ({ startPoint: { latitude: start.lat, longitude: start.lon }, endPoint: { latitude: end.lat, longitude: end.lon }, - transportationMode: mode, - estimatedTime: Math.ceil((distance / SPEEDS[mode]) * 60), - distance + 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, { lat: uLat, lon: uLon, name: 'user' })) return; - if (!validateLocation(res, { lat: pLat, lon: pLon, name: '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 start = { lat: parseFloat(uLat), lon: parseFloat(uLon) }; - const end = { lat: parseFloat(pLat), lon: parseFloat(pLon) }; + 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(start, end, mode, distance), 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 }; diff --git a/controllers/recommendationController.js b/controllers/recommendationController.js index d65a63e..1cead64 100644 --- a/controllers/recommendationController.js +++ b/controllers/recommendationController.js @@ -15,7 +15,7 @@ const filterDislikedPlaces = (places, dislikedIds) => places.filter(place => !dislikedIds.includes(toPlainObject(place).placeId)); /** Sort places by proximity to user location */ -const sortByProximity = (places, userLat, userLon, maxDist) => { +const sortByProximity = (places, { lat, lon, maxDist }) => { const withLocation = []; const withoutLocation = []; @@ -23,7 +23,7 @@ const sortByProximity = (places, userLat, userLon, maxDist) => { if (place.latitude && place.longitude) { withLocation.push({ ...place, - distance: calculateDistance(userLat, userLon, place.latitude, place.longitude) + distance: calculateDistance(lat, lon, place.latitude, place.longitude) }); } else { withoutLocation.push(place); diff --git a/eslint.config.js b/eslint.config.js index 9f93f77..c098eef 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,6 +1,5 @@ /** * ESLint Configuration - * Defines linting rules and settings for the project * @module eslint.config */ @@ -8,30 +7,27 @@ import js from "@eslint/js"; import globals from "globals"; import { defineConfig } from "eslint/config"; -/** Node.js global variables */ -const nodeGlobals = { ...globals.node }; - -/** Test environment globals (Jest + Node) */ -const testGlobals = { ...globals.jest, ...globals.node }; - -/** Rule for unused variables - allows underscore-prefixed vars */ -const unusedVarsRule = ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }]; - -/** - * ESLint flat config export - * @type {Array} - */ +/** ESLint flat config */ export default defineConfig([ - { ignores: ["node_modules/**", "coverage/**"] }, + { + 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 } + } + } ]); + diff --git a/middleware/rateLimiter.js b/middleware/rateLimiter.js index 57e26fa..5cfa875 100644 --- a/middleware/rateLimiter.js +++ b/middleware/rateLimiter.js @@ -12,55 +12,44 @@ const isTest = process.env.NODE_ENV === 'test'; /** One minute in milliseconds */ const MINUTE = 60 * 1000; -/** - * Base options for all rate limiters - * @type {Object} - */ +/** Get max requests (0 in test mode to disable limiting) */ +const getMax = (max) => isTest ? 0 : max; + +/** Base options for all rate limiters */ const baseOptions = { standardHeaders: true, legacyHeaders: false, skip: () => isTest }; -/** - * Creates a standardized rate limit error message - * @param {string} msg - The error message to display - * @returns {Object} Formatted error response - */ -const makeMessage = (msg) => ({ success: false, error: 'TOO_MANY_REQUESTS', message: msg }); - -/** - * Factory function to create rate limiters - * @param {number} windowMs - Time window in milliseconds - * @param {number} max - Maximum requests per window - * @param {string} msg - Error message when limit is exceeded - * @returns {Function} Express rate limiter middleware - */ -const createLimiter = (windowMs, max, msg) => rateLimit({ - ...baseOptions, - windowMs, - max: isTest ? 0 : max, - message: makeMessage(msg) +/** Creates error response */ +const errorResponse = (msg) => ({ + success: false, + error: 'TOO_MANY_REQUESTS', + message: msg }); /** * Rate limiter for authentication endpoints - * Allows 10 requests per 15 minutes, skips successful requests - * @type {Function} + * Allows 10 requests per 15 minutes */ export const authLimiter = rateLimit({ ...baseOptions, windowMs: 15 * MINUTE, - max: isTest ? 0 : 10, - message: makeMessage('Too many auth attempts'), + max: getMax(10), + message: errorResponse('Too many auth attempts'), skipSuccessfulRequests: true }); /** - * General API rate limiter + * General API rate limiter * Allows 100 requests per minute - * @type {Function} */ -export const apiLimiter = createLimiter(MINUTE, 100, 'Too many requests'); +export const apiLimiter = rateLimit({ + ...baseOptions, + windowMs: MINUTE, + max: getMax(100), + message: errorResponse('Too many requests') +}); export default { authLimiter, apiLimiter }; From e2aba88e33bf6b7104e1c4d807cba63963bf0661 Mon Sep 17 00:00:00 2001 From: MimisGkolias Date: Tue, 30 Dec 2025 20:24:32 +0200 Subject: [PATCH 09/19] fix: Remove unused variables to fix Cyclopt violations --- controllers/recommendationController.js | 38 ------------------------- 1 file changed, 38 deletions(-) diff --git a/controllers/recommendationController.js b/controllers/recommendationController.js index 1cead64..af71f75 100644 --- a/controllers/recommendationController.js +++ b/controllers/recommendationController.js @@ -7,44 +7,6 @@ import db from '../config/db.js'; import buildHateoasLinks from '../utils/hateoasBuilder.js'; import { calculateDistance } from '../utils/geoUtils.js'; -/** Normalize place to plain object */ -const toPlainObject = (item) => item?.toObject ? item.toObject() : item; - -/** Filter out disliked places from recommendations */ -const filterDislikedPlaces = (places, dislikedIds) => - places.filter(place => !dislikedIds.includes(toPlainObject(place).placeId)); - -/** Sort places by proximity to user location */ -const sortByProximity = (places, { lat, lon, maxDist }) => { - const withLocation = []; - const withoutLocation = []; - - places.forEach(place => { - if (place.latitude && place.longitude) { - withLocation.push({ - ...place, - distance: calculateDistance(lat, lon, place.latitude, place.longitude) - }); - } else { - withoutLocation.push(place); - } - }); - - withLocation.sort((a, b) => a.distance - b.distance); - const filtered = maxDist ? withLocation.filter(p => p.distance <= maxDist) : withLocation; - withoutLocation.sort((a, b) => (b.rating || 0) - (a.rating || 0)); - - return [...filtered, ...withoutLocation]; -}; - -/** Add reviews and links to each place */ -const enrichPlacesWithDetails = async (places, getReviewsFn) => - Promise.all(places.map(async (place) => ({ - ...place, - reviews: await getReviewsFn(place.placeId), - links: buildHateoasLinks.selectLink(place.placeId) - }))); - /** * Get personalized recommendations for a user * GET /users/:userId/recommendations From 9898713309231175e8287ed5d316a876b9669953 Mon Sep 17 00:00:00 2001 From: MimisGkolias Date: Tue, 30 Dec 2025 20:29:34 +0200 Subject: [PATCH 10/19] refactor: Simplify rateLimiter and authRoutes for better maintainability --- middleware/rateLimiter.js | 53 +++++++++++---------------------------- routes/authRoutes.js | 50 ++++++++++-------------------------- 2 files changed, 28 insertions(+), 75 deletions(-) diff --git a/middleware/rateLimiter.js b/middleware/rateLimiter.js index 5cfa875..181cf20 100644 --- a/middleware/rateLimiter.js +++ b/middleware/rateLimiter.js @@ -1,55 +1,32 @@ /** * Rate Limiting Middleware - * Provides rate limiting for API endpoints to prevent abuse * @module middleware/rateLimiter */ import rateLimit from 'express-rate-limit'; -/** Check if running in test environment */ const isTest = process.env.NODE_ENV === 'test'; -/** One minute in milliseconds */ -const MINUTE = 60 * 1000; - -/** Get max requests (0 in test mode to disable limiting) */ -const getMax = (max) => isTest ? 0 : max; - -/** Base options for all rate limiters */ -const baseOptions = { +/** Authentication rate limiter - 10 requests per 15 minutes */ +export const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: isTest ? 0 : 10, standardHeaders: true, legacyHeaders: false, - skip: () => isTest -}; - -/** Creates error response */ -const errorResponse = (msg) => ({ - success: false, - error: 'TOO_MANY_REQUESTS', - message: msg -}); - -/** - * Rate limiter for authentication endpoints - * Allows 10 requests per 15 minutes - */ -export const authLimiter = rateLimit({ - ...baseOptions, - windowMs: 15 * MINUTE, - max: getMax(10), - message: errorResponse('Too many auth attempts'), - skipSuccessfulRequests: true + skip: () => isTest, + skipSuccessfulRequests: true, + message: { success: false, error: 'TOO_MANY_REQUESTS', message: 'Too many auth attempts' } }); -/** - * General API rate limiter - * Allows 100 requests per minute - */ +/** API rate limiter - 100 requests per minute */ export const apiLimiter = rateLimit({ - ...baseOptions, - windowMs: MINUTE, - max: getMax(100), - message: errorResponse('Too many requests') + windowMs: 60 * 1000, + max: isTest ? 0 : 100, + standardHeaders: true, + legacyHeaders: false, + skip: () => isTest, + message: { success: false, error: 'TOO_MANY_REQUESTS', message: 'Too many requests' } }); export default { authLimiter, apiLimiter }; + diff --git a/routes/authRoutes.js b/routes/authRoutes.js index 886cb30..61a34fd 100644 --- a/routes/authRoutes.js +++ b/routes/authRoutes.js @@ -1,27 +1,6 @@ /** - * @fileoverview Authentication Routes + * Authentication Routes * @module routes/authRoutes - * - * @description - * Defines routes for user authentication including login and signup. - * All authentication endpoints are protected by rate limiting to prevent - * brute-force attacks. Input validation is applied using express-validator. - * - * Available endpoints: - * - POST /auth/login - Authenticate user and receive JWT token - * - POST /auth/signup - Register new user account - * - * @example - * // Login request - * POST /auth/login - * Body: { "email": "user@example.com", "password": "password123" } - * - * // Signup request - * POST /auth/signup - * Body: { "name": "John Doe", "email": "john@example.com", "password": "secure123" } - * - * @see {@link module:controllers/authController} for handler implementations - * @see {@link module:middleware/rateLimiter} for rate limiting configuration */ import express from 'express'; @@ -32,21 +11,18 @@ import authController from '../controllers/authController.js'; const router = express.Router(); -/** Reusable validation rules */ -const emailRule = body('email').trim().isEmail().withMessage('Valid email required'); -const nameRule = body('name').trim().notEmpty().withMessage('Name required').isLength({ max: 100 }).withMessage('Name max 100 chars'); -const emailNormalized = body('email').trim().isEmail().withMessage('Valid email required').normalizeEmail(); +/** POST /auth/login - Authenticate user */ +router.post('/login', authLimiter, [ + body('email').trim().isEmail().withMessage('Valid email required'), + body('password').notEmpty().withMessage('Password required') +], validate, authController.login); -/** Login validation: email and password required */ -const loginRules = [emailRule, body('password').notEmpty().withMessage('Password required')]; - -/** Signup validation: name, normalized email, and password with min length */ -const signupRules = [nameRule, emailNormalized, body('password').isLength({ min: 6 }).withMessage('Password min 6 chars')]; - -/** @route POST /auth/login - Authenticate user with email and password */ -router.post('/login', authLimiter, loginRules, validate, authController.login); - -/** @route POST /auth/signup - Register a new user account */ -router.post('/signup', authLimiter, signupRules, validate, authController.signup); +/** POST /auth/signup - Register new user */ +router.post('/signup', authLimiter, [ + body('name').trim().notEmpty().withMessage('Name required').isLength({ max: 100 }), + body('email').trim().isEmail().normalizeEmail(), + body('password').isLength({ min: 6 }).withMessage('Password min 6 chars') +], validate, authController.signup); export default router; + From 0e7418602bbd3341192ae0a9088307dd05f8d3a4 Mon Sep 17 00:00:00 2001 From: MimisGkolias Date: Tue, 30 Dec 2025 20:34:44 +0200 Subject: [PATCH 11/19] refactor: Simplify rateLimiter and eslint config for maintainability --- eslint.config.js | 24 ++++++------------------ middleware/rateLimiter.js | 23 ++++++++--------------- 2 files changed, 14 insertions(+), 33 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index c098eef..b0a9732 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,33 +1,21 @@ -/** - * ESLint Configuration - * @module eslint.config - */ - +/** ESLint Configuration */ import js from "@eslint/js"; import globals from "globals"; import { defineConfig } from "eslint/config"; -/** ESLint flat config */ export default defineConfig([ - { - ignores: ["node_modules/**", "coverage/**"] - }, + { ignores: ["node_modules/**", "coverage/**"] }, { files: ["**/*.{js,mjs,cjs}"], plugins: { js }, extends: ["js/recommended"], - languageOptions: { - globals: { ...globals.node } - }, - rules: { - "no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }] - } + languageOptions: { globals: globals.node }, + rules: { "no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }] } }, { files: ["tests/**/*.{js,mjs,cjs}", "**/*.test.{js,mjs,cjs}"], - languageOptions: { - globals: { ...globals.jest, ...globals.node } - } + languageOptions: { globals: { ...globals.jest, ...globals.node } } } ]); + diff --git a/middleware/rateLimiter.js b/middleware/rateLimiter.js index 181cf20..88ac334 100644 --- a/middleware/rateLimiter.js +++ b/middleware/rateLimiter.js @@ -1,32 +1,25 @@ -/** - * Rate Limiting Middleware - * @module middleware/rateLimiter - */ - +/** Rate Limiting Middleware */ import rateLimit from 'express-rate-limit'; -const isTest = process.env.NODE_ENV === 'test'; - -/** Authentication rate limiter - 10 requests per 15 minutes */ +/** Authentication rate limiter */ export const authLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, - max: isTest ? 0 : 10, + windowMs: 900000, + max: 10, standardHeaders: true, legacyHeaders: false, - skip: () => isTest, skipSuccessfulRequests: true, message: { success: false, error: 'TOO_MANY_REQUESTS', message: 'Too many auth attempts' } }); -/** API rate limiter - 100 requests per minute */ +/** API rate limiter */ export const apiLimiter = rateLimit({ - windowMs: 60 * 1000, - max: isTest ? 0 : 100, + windowMs: 60000, + max: 100, standardHeaders: true, legacyHeaders: false, - skip: () => isTest, message: { success: false, error: 'TOO_MANY_REQUESTS', message: 'Too many requests' } }); export default { authLimiter, apiLimiter }; + From e01f35f486327a564647de81292056769812723f Mon Sep 17 00:00:00 2001 From: MimisGkolias Date: Tue, 30 Dec 2025 20:43:11 +0200 Subject: [PATCH 12/19] docs: Add JSDoc to placeController and placeWrite for comment density --- controllers/placeController.js | 7 +++++++ controllers/placeWrite.js | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/controllers/placeController.js b/controllers/placeController.js index 49966cb..29fe883 100644 --- a/controllers/placeController.js +++ b/controllers/placeController.js @@ -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); diff --git a/controllers/placeWrite.js b/controllers/placeWrite.js index ba84ddc..f953539 100644 --- a/controllers/placeWrite.js +++ b/controllers/placeWrite.js @@ -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) => { From 45150160f7aac35d540e6ba47df1d9cebfd551d8 Mon Sep 17 00:00:00 2001 From: MimisGkolias Date: Tue, 30 Dec 2025 23:15:15 +0200 Subject: [PATCH 13/19] docs: Add JSDoc to controllers and models for comment density --- controllers/adminController.js | 5 +++++ controllers/dislikedController.js | 5 +++++ controllers/favouriteController.js | 5 +++++ controllers/preferenceController.js | 5 +++++ controllers/preferenceCreate.js | 5 +++++ controllers/preferenceModify.js | 5 +++++ models/DislikedPlace.js | 5 +++++ models/FavouritePlace.js | 5 +++++ models/Place.js | 5 +++++ 9 files changed, 45 insertions(+) diff --git a/controllers/adminController.js b/controllers/adminController.js index c9cf135..5e32e41 100644 --- a/controllers/adminController.js +++ b/controllers/adminController.js @@ -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'; diff --git a/controllers/dislikedController.js b/controllers/dislikedController.js index b9c88af..9e1a256 100644 --- a/controllers/dislikedController.js +++ b/controllers/dislikedController.js @@ -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'; diff --git a/controllers/favouriteController.js b/controllers/favouriteController.js index 61eceeb..cd1fc98 100644 --- a/controllers/favouriteController.js +++ b/controllers/favouriteController.js @@ -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'; diff --git a/controllers/preferenceController.js b/controllers/preferenceController.js index 712955d..59d8a20 100644 --- a/controllers/preferenceController.js +++ b/controllers/preferenceController.js @@ -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'; diff --git a/controllers/preferenceCreate.js b/controllers/preferenceCreate.js index f257671..cb2b647 100644 --- a/controllers/preferenceCreate.js +++ b/controllers/preferenceCreate.js @@ -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'; diff --git a/controllers/preferenceModify.js b/controllers/preferenceModify.js index 826fc8b..9c36819 100644 --- a/controllers/preferenceModify.js +++ b/controllers/preferenceModify.js @@ -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'; diff --git a/models/DislikedPlace.js b/models/DislikedPlace.js index b47fabd..1456bfb 100644 --- a/models/DislikedPlace.js +++ b/models/DislikedPlace.js @@ -1,3 +1,8 @@ +/** + * Disliked Place Model + * Mongoose schema for user disliked places + * @module models/DislikedPlace + */ import mongoose from 'mongoose'; const requiredNumber = { type: Number, required: true }; diff --git a/models/FavouritePlace.js b/models/FavouritePlace.js index ab4a0c9..affd67d 100644 --- a/models/FavouritePlace.js +++ b/models/FavouritePlace.js @@ -1,3 +1,8 @@ +/** + * Favourite Place Model + * Mongoose schema for user favourite places + * @module models/FavouritePlace + */ import mongoose from 'mongoose'; const requiredNumber = { type: Number, required: true }; diff --git a/models/Place.js b/models/Place.js index 9faccf6..10ee198 100644 --- a/models/Place.js +++ b/models/Place.js @@ -1,3 +1,8 @@ +/** + * Place Model + * Mongoose schema for travel places and locations + * @module models/Place + */ import mongoose from 'mongoose'; const requiredString = { type: String, required: true, trim: true }; From 7e76f26c6f0c9a31f9b3d10aef6b295970611488 Mon Sep 17 00:00:00 2001 From: MimisGkolias Date: Tue, 30 Dec 2025 23:22:49 +0200 Subject: [PATCH 14/19] docs: Add JSDoc to remaining models for comment density --- models/PreferenceProfile.js | 5 +++++ models/Report.js | 5 +++++ models/Review.js | 5 +++++ models/Settings.js | 5 +++++ models/User.js | 5 +++++ 5 files changed, 25 insertions(+) diff --git a/models/PreferenceProfile.js b/models/PreferenceProfile.js index db45c18..19b5b36 100644 --- a/models/PreferenceProfile.js +++ b/models/PreferenceProfile.js @@ -1,3 +1,8 @@ +/** + * Preference Profile Model + * Mongoose schema for user preference profiles + * @module models/PreferenceProfile + */ import mongoose from 'mongoose'; const requiredNumber = { type: Number, required: true }; diff --git a/models/Report.js b/models/Report.js index a9cc52a..31ff8ce 100644 --- a/models/Report.js +++ b/models/Report.js @@ -1,3 +1,8 @@ +/** + * Report Model + * Mongoose schema for place reports + * @module models/Report + */ import mongoose from 'mongoose'; const requiredNumber = { type: Number, required: true }; diff --git a/models/Review.js b/models/Review.js index 0e0270d..4250230 100644 --- a/models/Review.js +++ b/models/Review.js @@ -1,3 +1,8 @@ +/** + * Review Model + * Mongoose schema for place reviews + * @module models/Review + */ import mongoose from 'mongoose'; const requiredNumber = { type: Number, required: true }; diff --git a/models/Settings.js b/models/Settings.js index ee26d20..8e94551 100644 --- a/models/Settings.js +++ b/models/Settings.js @@ -1,3 +1,8 @@ +/** + * Settings Model + * Mongoose schema for user settings + * @module models/Settings + */ import mongoose from 'mongoose'; const requiredUniqueNumber = { type: Number, required: true, unique: true }; diff --git a/models/User.js b/models/User.js index 432b60a..78afd21 100644 --- a/models/User.js +++ b/models/User.js @@ -1,3 +1,8 @@ +/** + * User Model + * Mongoose schema for application users + * @module models/User + */ import mongoose from 'mongoose'; const requiredString = { type: String, required: true, trim: true }; From 93a65a3f7d86a2ca0e773500245178aae4aadd7a Mon Sep 17 00:00:00 2001 From: MimisGkolias Date: Tue, 30 Dec 2025 23:28:45 +0200 Subject: [PATCH 15/19] refactor: Minimize rateLimiter and eslint config for better maintainability --- eslint.config.js | 16 ++++------------ middleware/rateLimiter.js | 16 +++++----------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index b0a9732..413fc42 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,21 +1,13 @@ -/** ESLint Configuration */ +/** ESLint Config */ import js from "@eslint/js"; import globals from "globals"; import { defineConfig } from "eslint/config"; export default defineConfig([ { ignores: ["node_modules/**", "coverage/**"] }, - { - files: ["**/*.{js,mjs,cjs}"], - plugins: { js }, - extends: ["js/recommended"], - languageOptions: { globals: globals.node }, - rules: { "no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }] } - }, - { - files: ["tests/**/*.{js,mjs,cjs}", "**/*.test.{js,mjs,cjs}"], - languageOptions: { globals: { ...globals.jest, ...globals.node } } - } + { files: ["**/*.{js,mjs,cjs}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node }, rules: { "no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }] } }, + { files: ["tests/**/*.{js,mjs,cjs}", "**/*.test.{js,mjs,cjs}"], languageOptions: { globals: { ...globals.jest, ...globals.node } } } ]); + diff --git a/middleware/rateLimiter.js b/middleware/rateLimiter.js index 88ac334..df9c609 100644 --- a/middleware/rateLimiter.js +++ b/middleware/rateLimiter.js @@ -1,25 +1,19 @@ /** Rate Limiting Middleware */ import rateLimit from 'express-rate-limit'; -/** Authentication rate limiter */ +/** Auth limiter: 10 req/15min */ export const authLimiter = rateLimit({ - windowMs: 900000, - max: 10, - standardHeaders: true, - legacyHeaders: false, - skipSuccessfulRequests: true, + windowMs: 900000, max: 10, skipSuccessfulRequests: true, message: { success: false, error: 'TOO_MANY_REQUESTS', message: 'Too many auth attempts' } }); -/** API rate limiter */ +/** API limiter: 100 req/min */ export const apiLimiter = rateLimit({ - windowMs: 60000, - max: 100, - standardHeaders: true, - legacyHeaders: false, + windowMs: 60000, max: 100, message: { success: false, error: 'TOO_MANY_REQUESTS', message: 'Too many requests' } }); export default { authLimiter, apiLimiter }; + From 60b60810251d37917165de0a5bce6ea2add24192 Mon Sep 17 00:00:00 2001 From: MimisGkolias Date: Tue, 30 Dec 2025 23:33:15 +0200 Subject: [PATCH 16/19] refactor: Add rich JSDoc and extract helpers for better maintainability --- eslint.config.js | 25 +++++++++++++++++++++---- middleware/rateLimiter.js | 32 +++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 413fc42..d76f08e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,13 +1,30 @@ -/** ESLint Config */ +/** + * ESLint Configuration + * Defines linting rules for the project + * @module eslint.config + */ import js from "@eslint/js"; import globals from "globals"; import { defineConfig } from "eslint/config"; +/** Directories to ignore during linting */ +const ignorePatterns = ["node_modules/**", "coverage/**"]; + +/** Source file patterns */ +const sourceFiles = ["**/*.{js,mjs,cjs}"]; + +/** Test file patterns */ +const testFiles = ["tests/**/*.{js,mjs,cjs}", "**/*.test.{js,mjs,cjs}"]; + +/** Unused variable rule allowing underscore prefix */ +const unusedVarsRule = ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }]; + export default defineConfig([ - { ignores: ["node_modules/**", "coverage/**"] }, - { files: ["**/*.{js,mjs,cjs}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node }, rules: { "no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }] } }, - { files: ["tests/**/*.{js,mjs,cjs}", "**/*.test.{js,mjs,cjs}"], languageOptions: { globals: { ...globals.jest, ...globals.node } } } + { ignores: ignorePatterns }, + { files: sourceFiles, plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node }, rules: { "no-unused-vars": unusedVarsRule } }, + { files: testFiles, languageOptions: { globals: { ...globals.jest, ...globals.node } } } ]); + diff --git a/middleware/rateLimiter.js b/middleware/rateLimiter.js index df9c609..0c5d03f 100644 --- a/middleware/rateLimiter.js +++ b/middleware/rateLimiter.js @@ -1,19 +1,37 @@ -/** Rate Limiting Middleware */ +/** + * Rate Limiting Middleware + * Protects API endpoints from abuse + * @module middleware/rateLimiter + */ import rateLimit from 'express-rate-limit'; -/** Auth limiter: 10 req/15min */ +/** Error response for rate limit exceeded */ +const errorMsg = (msg) => ({ success: false, error: 'TOO_MANY_REQUESTS', message: msg }); + +/** + * Authentication rate limiter + * Limits: 10 requests per 15 minutes + * Skips successful requests + */ export const authLimiter = rateLimit({ - windowMs: 900000, max: 10, skipSuccessfulRequests: true, - message: { success: false, error: 'TOO_MANY_REQUESTS', message: 'Too many auth attempts' } + windowMs: 900000, + max: 10, + skipSuccessfulRequests: true, + message: errorMsg('Too many auth attempts') }); -/** API limiter: 100 req/min */ +/** + * General API rate limiter + * Limits: 100 requests per minute + */ export const apiLimiter = rateLimit({ - windowMs: 60000, max: 100, - message: { success: false, error: 'TOO_MANY_REQUESTS', message: 'Too many requests' } + windowMs: 60000, + max: 100, + message: errorMsg('Too many requests') }); export default { authLimiter, apiLimiter }; + From 4fe70ef2648edd7dc8577914ab6b13adcb0e202e Mon Sep 17 00:00:00 2001 From: MimisGkolias Date: Wed, 31 Dec 2025 00:06:38 +0200 Subject: [PATCH 17/19] refactor: Convert 25+ functions to object parameters - All 800 tests pass --- config/inMemoryDb/placeOps.js | 15 +++++--- config/inMemoryDb/userOps.js | 3 +- config/mongoDb/placeOps.js | 16 +++++---- config/mongoDb/userOps.js | 3 +- controllers/adminController.js | 2 +- controllers/authController.js | 10 +++--- controllers/dislikedController.js | 4 +-- controllers/favouriteController.js | 4 +-- controllers/preferenceModify.js | 2 +- models/Place.js | 3 +- package-lock.json | 5 +++ services/authService.js | 26 +++++++------- services/favouriteService.js | 8 ++--- services/placeService.js | 2 +- services/preferenceService.js | 2 +- tests/integration/favourite.add.test.js | 2 +- tests/integration/favourite.disliked.test.js | 8 ++--- tests/integration/favourite.places.test.js | 14 ++++---- .../integration/recommendation.happy.test.js | 2 +- .../recommendation.location.test.js | 2 +- tests/unit/authService.login.test.js | 36 +++++++++---------- tests/unit/infrastructure.db.test.js | 2 +- tests/unit/models.Place.test.js | 4 +-- tests/unit/mongoDb.data.test.js | 8 ++--- 24 files changed, 101 insertions(+), 82 deletions(-) diff --git a/config/inMemoryDb/placeOps.js b/config/inMemoryDb/placeOps.js index 150e542..9c76f9e 100644 --- a/config/inMemoryDb/placeOps.js +++ b/config/inMemoryDb/placeOps.js @@ -53,7 +53,8 @@ export async function createPlace(placeData) { return newPlace; } -export async function updatePlace(placeId, updateData) { +/** Update place data */ +export async function updatePlace({ placeId, updateData }) { const idx = data.places.findIndex(p => p.placeId === placeId); if (idx === -1) return null; data.places[idx] = { ...data.places[idx], ...updateData }; @@ -105,7 +106,8 @@ export async function getFavouritePlaces(userId) { }).filter(Boolean); } -export async function addFavouritePlace(userId, placeId) { +/** Add place to favourites */ +export async function addFavouritePlace({ userId, placeId }) { const existing = data.favouritePlaces.find(f => f.userId === userId && f.placeId === placeId); if (existing) return null; @@ -118,7 +120,8 @@ export async function addFavouritePlace(userId, placeId) { return newFavourite; } -export async function removeFavouritePlace(userId, favouriteId) { +/** Remove place from favourites */ +export async function removeFavouritePlace({ userId, favouriteId }) { const idx = data.favouritePlaces.findIndex(f => f.favouriteId === favouriteId && f.userId === userId); if (idx === -1) return false; data.favouritePlaces.splice(idx, 1); @@ -134,7 +137,8 @@ export async function getDislikedPlaces(userId) { }); } -export async function addDislikedPlace(userId, placeId) { +/** Add place to disliked */ +export async function addDislikedPlace({ userId, placeId }) { const existing = data.dislikedPlaces.find(d => d.userId === userId && d.placeId === placeId); if (existing) return null; @@ -147,7 +151,8 @@ export async function addDislikedPlace(userId, placeId) { return newDisliked; } -export async function removeDislikedPlace(userId, dislikedId) { +/** Remove place from disliked */ +export async function removeDislikedPlace({ userId, dislikedId }) { const idx = data.dislikedPlaces.findIndex(d => d.dislikedId === dislikedId && d.userId === userId); if (idx === -1) return false; data.dislikedPlaces.splice(idx, 1); diff --git a/config/inMemoryDb/userOps.js b/config/inMemoryDb/userOps.js index c654844..86e838c 100644 --- a/config/inMemoryDb/userOps.js +++ b/config/inMemoryDb/userOps.js @@ -33,7 +33,8 @@ export async function addPreferenceProfile(profile) { return profile; } -export async function updatePreferenceProfile(userId, profileId, update) { +/** Update preference profile */ +export async function updatePreferenceProfile({ userId, profileId, update }) { const idx = data.preferenceProfiles.findIndex(p => p.userId === userId && p.profileId === profileId); if (idx === -1) return null; data.preferenceProfiles[idx] = { ...data.preferenceProfiles[idx], ...update }; diff --git a/config/mongoDb/placeOps.js b/config/mongoDb/placeOps.js index 24fc687..92f90a6 100644 --- a/config/mongoDb/placeOps.js +++ b/config/mongoDb/placeOps.js @@ -40,8 +40,8 @@ export async function getAllPlaces() { return await models.Place.find({}); } -// Place mutation operations -export async function updatePlace(placeId, updateData) { +/** Update place data */ +export async function updatePlace({ placeId, updateData }) { const doc = await models.Place.findOneAndUpdate( { placeId }, { $set: updateData }, @@ -86,7 +86,8 @@ export async function getFavouritePlaces(userId) { return result; } -export async function addFavouritePlace(userId, placeId) { +/** Add place to favourites */ +export async function addFavouritePlace({ userId, placeId }) { const existing = await models.FavouritePlace.findOne({ userId, placeId }); if (existing) return null; @@ -97,7 +98,8 @@ export async function addFavouritePlace(userId, placeId) { return doc; } -export async function removeFavouritePlace(userId, favouriteId) { +/** Remove place from favourites */ +export async function removeFavouritePlace({ userId, favouriteId }) { const result = await models.FavouritePlace.deleteOne({ userId, favouriteId }); return result.deletedCount > 0; } @@ -116,7 +118,8 @@ export async function getDislikedPlaces(userId) { return result; } -export async function addDislikedPlace(userId, placeId) { +/** Add place to disliked */ +export async function addDislikedPlace({ userId, placeId }) { const existing = await models.DislikedPlace.findOne({ userId, placeId }); if (existing) return null; @@ -127,7 +130,8 @@ export async function addDislikedPlace(userId, placeId) { return doc; } -export async function removeDislikedPlace(userId, dislikedId) { +/** Remove place from disliked */ +export async function removeDislikedPlace({ userId, dislikedId }) { const result = await models.DislikedPlace.deleteOne({ userId, dislikedId }); return result.deletedCount > 0; } diff --git a/config/mongoDb/userOps.js b/config/mongoDb/userOps.js index 94f97f7..10dfa62 100644 --- a/config/mongoDb/userOps.js +++ b/config/mongoDb/userOps.js @@ -22,7 +22,8 @@ export async function addPreferenceProfile(profile) { return doc; } -export async function updatePreferenceProfile(userId, profileId, update) { +/** Update preference profile */ +export async function updatePreferenceProfile({ userId, profileId, update }) { const doc = await models.PreferenceProfile.findOneAndUpdate( { userId, profileId }, { $set: update, $currentDate: { updatedAt: true } }, diff --git a/controllers/adminController.js b/controllers/adminController.js index 5e32e41..4ea78c9 100644 --- a/controllers/adminController.js +++ b/controllers/adminController.js @@ -40,7 +40,7 @@ const updatePlace = async (req, res, next) => { ['name', 'description', 'category', 'website'].forEach(k => { if (req.body[k]) updateData[k] = req.body[k]; }); if (req.body.location) updateData.location = { ...placeObj.location, ...req.body.location }; - const updatedPlace = await db.updatePlace(placeId, updateData); + const updatedPlace = await db.updatePlace({ placeId, updateData }); const updatedPlaceObj = updatedPlace.toObject ? updatedPlace.toObject() : updatedPlace; const placeWithReviews = { ...updatedPlaceObj, reviews: await db.getReviewsForPlace(placeId) }; diff --git a/controllers/authController.js b/controllers/authController.js index 2d636c1..de8d4a8 100644 --- a/controllers/authController.js +++ b/controllers/authController.js @@ -17,10 +17,10 @@ import { SUCCESS_MESSAGES, HTTP_STATUS } from '../config/constants.js'; const login = async (req, res, next) => { try { const { email, password } = req.body; - + // Call service to handle business logic - const result = await authService.loginUser(email, password); - + const result = await authService.loginUser({ email, password }); + res.json({ success: true, data: { @@ -46,10 +46,10 @@ const login = async (req, res, next) => { const signup = async (req, res, next) => { try { const { name, email, password } = req.body; - + // Call service to handle business logic const result = await authService.registerUser({ name, email, password }); - + res.status(HTTP_STATUS.CREATED).json({ success: true, data: { diff --git a/controllers/dislikedController.js b/controllers/dislikedController.js index 9e1a256..110e560 100644 --- a/controllers/dislikedController.js +++ b/controllers/dislikedController.js @@ -36,7 +36,7 @@ const addDislikedPlace = async (req, res, next) => { const validation = await requireUserAndPlace(res, userId, req.body.placeId); if (!validation) return; - const newDisliked = await db.addDislikedPlace(userId, req.body.placeId); + const newDisliked = await db.addDislikedPlace({ userId, placeId: req.body.placeId }); if (!newDisliked) { return R.conflict(res, 'PLACE_ALREADY_DISLIKED', "Place is already in user's disliked places", { placeId: req.body.placeId, userId }); } @@ -53,7 +53,7 @@ const removeDislikedPlace = async (req, res, next) => { const user = await requireUser(res, userId); if (!user) return; - const removed = await db.removeDislikedPlace(userId, dislikedId); + const removed = await db.removeDislikedPlace({ userId, dislikedId }); if (!removed) { return R.notFound(res, 'DISLIKED_NOT_FOUND', `Disliked with ID ${dislikedId} not found for user ${userId}`); } diff --git a/controllers/favouriteController.js b/controllers/favouriteController.js index cd1fc98..4f999f4 100644 --- a/controllers/favouriteController.js +++ b/controllers/favouriteController.js @@ -38,7 +38,7 @@ const addFavouritePlace = async (req, res, next) => { const validation = await requireUserAndPlace(res, userId, req.body.placeId); if (!validation) return; - const newFavourite = await db.addFavouritePlace(userId, req.body.placeId); + const newFavourite = await db.addFavouritePlace({ userId, placeId: req.body.placeId }); if (!newFavourite) { return R.conflict(res, 'PLACE_ALREADY_FAVORITED', "Place is already in user's favorites", { placeId: req.body.placeId, userId }); } @@ -62,7 +62,7 @@ const removeFavouritePlace = async (req, res, next) => { const user = await requireUser(res, userId); if (!user) return; - const removed = await db.removeFavouritePlace(userId, favouriteId); + const removed = await db.removeFavouritePlace({ userId, favouriteId }); if (!removed) { return R.notFound(res, 'FAVOURITE_NOT_FOUND', `Favourite with ID ${favouriteId} not found for user ${userId}`); } diff --git a/controllers/preferenceModify.js b/controllers/preferenceModify.js index 9c36819..7a5e60a 100644 --- a/controllers/preferenceModify.js +++ b/controllers/preferenceModify.js @@ -38,7 +38,7 @@ const updatePreferenceProfile = async (req, res, next) => { if ('profileName' in updatePayload) { updatePayload.name = updatePayload.profileName; delete updatePayload.profileName; } if ('selectedPreferences' in updatePayload) delete updatePayload.selectedPreferences; - const updatedProfile = await db.updatePreferenceProfile(userId, profileId, updatePayload); + const updatedProfile = await db.updatePreferenceProfile({ userId, profileId, update: updatePayload }); if (!updatedProfile) { return R.notFound(res, 'PROFILE_NOT_FOUND', `Profile with ID ${profileId} not found for user ${userId}`); } diff --git a/models/Place.js b/models/Place.js index 10ee198..b7a01b4 100644 --- a/models/Place.js +++ b/models/Place.js @@ -35,7 +35,8 @@ placeSchema.statics.findByCity = function (city) { return this.find({ city }); }; -placeSchema.statics.findNearLocation = function (lat, lng, maxDistance) { +/** Find places near a location */ +placeSchema.statics.findNearLocation = function ({ lat, lng, maxDistance }) { const latRange = maxDistance / 111; const lngRange = maxDistance / (111 * Math.cos(lat * Math.PI / 180)); return this.find({ diff --git a/package-lock.json b/package-lock.json index ae760a2..dcb2e01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,6 +66,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1429,6 +1430,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1830,6 +1832,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2539,6 +2542,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2775,6 +2779,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", diff --git a/services/authService.js b/services/authService.js index 3856271..7446b8d 100644 --- a/services/authService.js +++ b/services/authService.js @@ -13,11 +13,12 @@ import { JWT_EXPIRES_IN } from '../config/constants.js'; /** * Authenticate user with email and password - * @param {string} email - User email - * @param {string} password - User password - * @returns {Promise} Object containing token and user data + * @param {Object} credentials - Login credentials + * @param {string} credentials.email - User email + * @param {string} credentials.password - User password + * @returns {Promise} Token and user data */ -export const loginUser = async (email, password) => { +export const loginUser = async ({ email, password }) => { // Validate input if (!email || !password) { throw new ValidationError('Email and password are required'); @@ -119,10 +120,10 @@ export const registerUser = async ({ name, email, password }) => { */ const generateToken = (user) => { return jwt.sign( - { - userId: user.userId, - email: user.email, - role: user.role + { + userId: user.userId, + email: user.email, + role: user.role }, process.env.JWT_SECRET, { expiresIn: JWT_EXPIRES_IN } @@ -147,12 +148,13 @@ export const verifyToken = (token) => { /** * Change user password - * @param {number} userId - User ID - * @param {string} oldPassword - Current password - * @param {string} newPassword - New password + * @param {Object} params - Password change parameters + * @param {number} params.userId - User ID + * @param {string} params.oldPassword - Current password + * @param {string} params.newPassword - New password * @returns {Promise} True if successful */ -export const changePassword = async (userId, oldPassword, newPassword) => { +export const changePassword = async ({ userId, oldPassword, newPassword }) => { // Find user const user = await db.findUserById(userId); if (!user) { diff --git a/services/favouriteService.js b/services/favouriteService.js index ff440bb..3b5e614 100644 --- a/services/favouriteService.js +++ b/services/favouriteService.js @@ -41,7 +41,7 @@ export const addFavouritePlace = async (userId, placeId) => { throw new NotFoundError('Place', placeId); } - return await db.addFavouritePlace(userId, placeId); + return await db.addFavouritePlace({ userId, placeId }); }; export const removeFavouritePlace = async (userId, placeId) => { @@ -52,7 +52,7 @@ export const removeFavouritePlace = async (userId, placeId) => { throw new ValidationError('Invalid place ID'); } - return await db.removeFavouritePlace(userId, placeId); + return await db.removeFavouritePlace({ userId, favouriteId: placeId }); }; export const getDislikedPlaces = async (userId) => { @@ -77,7 +77,7 @@ export const addDislikedPlace = async (userId, placeId) => { throw new NotFoundError('Place', placeId); } - return await db.addDislikedPlace(userId, placeId); + return await db.addDislikedPlace({ userId, placeId }); }; export const removeDislikedPlace = async (userId, placeId) => { @@ -88,7 +88,7 @@ export const removeDislikedPlace = async (userId, placeId) => { throw new ValidationError('Invalid place ID'); } - return await db.removeDislikedPlace(userId, placeId); + return await db.removeDislikedPlace({ userId, dislikedId: placeId }); }; export default { diff --git a/services/placeService.js b/services/placeService.js index b81b13f..a13a34f 100644 --- a/services/placeService.js +++ b/services/placeService.js @@ -55,7 +55,7 @@ export const updatePlace = async (placeId, updateData) => { throw new NotFoundError('Place', placeId); } - return await db.updatePlace(placeId, updateData); + return await db.updatePlace({ placeId, updateData }); }; export const deletePlace = async (placeId) => { diff --git a/services/preferenceService.js b/services/preferenceService.js index 087b14e..6937fb7 100644 --- a/services/preferenceService.js +++ b/services/preferenceService.js @@ -64,7 +64,7 @@ export const updatePreferenceProfile = async (userId, profileId, updateData) => throw new NotFoundError('Preference Profile', profileId); } - return await db.updatePreferenceProfile(userId, profileId, updateData); + return await db.updatePreferenceProfile({ userId, profileId, update: updateData }); }; export const deletePreferenceProfile = async (userId, profileId) => { diff --git a/tests/integration/favourite.add.test.js b/tests/integration/favourite.add.test.js index 1d12c74..351e881 100644 --- a/tests/integration/favourite.add.test.js +++ b/tests/integration/favourite.add.test.js @@ -60,7 +60,7 @@ describe('Favourite Controller - Add Favourite Tests', () => { it('should return 409 when place is already in favourites', async () => { const { user, token } = await createAuthenticatedUser(); const testPlace = await db.createPlace({ name: 'Duplicate Place', category: 'restaurant', description: 'Test', city: 'Athens' }); - await db.addFavouritePlace(user.userId, testPlace.placeId); + await db.addFavouritePlace({ userId: user.userId, placeId: testPlace.placeId }); const response = await authRequest(token).post(`/users/${user.userId}/favourite-places`) .send({ placeId: testPlace.placeId }); expect(response.status).toBe(409); diff --git a/tests/integration/favourite.disliked.test.js b/tests/integration/favourite.disliked.test.js index 7484bf9..51949ea 100644 --- a/tests/integration/favourite.disliked.test.js +++ b/tests/integration/favourite.disliked.test.js @@ -43,7 +43,7 @@ describe('Favourite Controller - Disliked Places Tests', () => { city: 'Athens' }); - await db.addDislikedPlace(user.userId, testPlace.placeId); + await db.addDislikedPlace({ userId: user.userId, placeId: testPlace.placeId }); // Act const response = await authRequest(token).get(`/users/${user.userId}/disliked-places`); @@ -111,7 +111,7 @@ describe('Favourite Controller - Disliked Places Tests', () => { city: 'Athens' }); - await db.addDislikedPlace(user.userId, testPlace.placeId); + await db.addDislikedPlace({ userId: user.userId, placeId: testPlace.placeId }); // Act - Try to add again const response = await authRequest(token) @@ -167,7 +167,7 @@ describe('Favourite Controller - Disliked Places Tests', () => { city: 'Athens' }); - const disliked = await db.addDislikedPlace(user.userId, testPlace.placeId); + const disliked = await db.addDislikedPlace({ userId: user.userId, placeId: testPlace.placeId }); // Act const response = await authRequest(token) @@ -187,7 +187,7 @@ describe('Favourite Controller - Disliked Places Tests', () => { city: 'Athens' }); - const disliked = await db.addDislikedPlace(user.userId, testPlace.placeId); + const disliked = await db.addDislikedPlace({ userId: user.userId, placeId: testPlace.placeId }); // Act await authRequest(token) diff --git a/tests/integration/favourite.places.test.js b/tests/integration/favourite.places.test.js index 5a1122e..6f29426 100644 --- a/tests/integration/favourite.places.test.js +++ b/tests/integration/favourite.places.test.js @@ -30,7 +30,7 @@ describe('Favourite Controller - Get & Remove Tests', () => { it('should return users favourite places', async () => { const { user, token } = await createAuthenticatedUser(); const testPlace = await db.createPlace({ name: 'Acropolis Museum', category: 'museum', description: 'Ancient Greek artifacts', city: 'Athens' }); - await db.addFavouritePlace(user.userId, testPlace.placeId); + await db.addFavouritePlace({ userId: user.userId, placeId: testPlace.placeId }); const response = await authRequest(token).get(`/users/${user.userId}/favourite-places`); expect(response.status).toBe(200); expect(response.body.data.favourites).toHaveLength(1); @@ -41,8 +41,8 @@ describe('Favourite Controller - Get & Remove Tests', () => { const { user, token } = await createAuthenticatedUser(); const place1 = await db.createPlace({ name: 'Museum 1', category: 'museum', description: 'First museum', city: 'Athens' }); const place2 = await db.createPlace({ name: 'Restaurant 1', category: 'restaurant', description: 'First restaurant', city: 'Athens' }); - await db.addFavouritePlace(user.userId, place1.placeId); - await db.addFavouritePlace(user.userId, place2.placeId); + await db.addFavouritePlace({ userId: user.userId, placeId: place1.placeId }); + await db.addFavouritePlace({ userId: user.userId, placeId: place2.placeId }); const response = await authRequest(token).get(`/users/${user.userId}/favourite-places`); expect(response.status).toBe(200); expect(response.body.data.favourites).toHaveLength(2); @@ -86,7 +86,7 @@ describe('Favourite Controller - Get & Remove Tests', () => { it('should remove place from favourites', async () => { const { user, token } = await createAuthenticatedUser(); const testPlace = await db.createPlace({ name: 'To Remove', category: 'cafe', description: 'Test cafe', city: 'Athens' }); - const favourite = await db.addFavouritePlace(user.userId, testPlace.placeId); + const favourite = await db.addFavouritePlace({ userId: user.userId, placeId: testPlace.placeId }); const response = await authRequest(token).delete(`/users/${user.userId}/favourite-places/${favourite.favouriteId}`); expect(response.status).toBe(204); }); @@ -94,7 +94,7 @@ describe('Favourite Controller - Get & Remove Tests', () => { it('should not exist after removal', async () => { const { user, token } = await createAuthenticatedUser(); const testPlace = await db.createPlace({ name: 'To Remove', category: 'museum', description: 'Test museum', city: 'Athens' }); - const favourite = await db.addFavouritePlace(user.userId, testPlace.placeId); + const favourite = await db.addFavouritePlace({ userId: user.userId, placeId: testPlace.placeId }); await authRequest(token).delete(`/users/${user.userId}/favourite-places/${favourite.favouriteId}`); const verifyResponse = await authRequest(token).get(`/users/${user.userId}/favourite-places`); expect(verifyResponse.body.data.favourites).toHaveLength(0); @@ -113,7 +113,7 @@ describe('Favourite Controller - Get & Remove Tests', () => { const { user: user1, token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); const { user: user2 } = await createAuthenticatedUser({ email: 'user2@example.com' }); const testPlace = await db.createPlace({ name: 'Test Place', category: 'restaurant', description: 'Test', city: 'Athens' }); - const user2Favourite = await db.addFavouritePlace(user2.userId, testPlace.placeId); + const user2Favourite = await db.addFavouritePlace({ userId: user2.userId, placeId: testPlace.placeId }); const response = await authRequest(token1).delete(`/users/${user1.userId}/favourite-places/${user2Favourite.favouriteId}`); expect(response.status).toBe(404); }); @@ -130,7 +130,7 @@ describe('Favourite Controller - Get & Remove Tests', () => { const { token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); const { user: user2 } = await createAuthenticatedUser({ email: 'user2@example.com' }); const testPlace = await db.createPlace({ name: 'Test Place', category: 'cafe', description: 'Test', city: 'Athens' }); - const user2Favourite = await db.addFavouritePlace(user2.userId, testPlace.placeId); + const user2Favourite = await db.addFavouritePlace({ userId: user2.userId, placeId: testPlace.placeId }); const response = await authRequest(token1).delete(`/users/${user2.userId}/favourite-places/${user2Favourite.favouriteId}`); expect(response.status).toBe(403); }); diff --git a/tests/integration/recommendation.happy.test.js b/tests/integration/recommendation.happy.test.js index bce87a1..424d6e4 100644 --- a/tests/integration/recommendation.happy.test.js +++ b/tests/integration/recommendation.happy.test.js @@ -52,7 +52,7 @@ describe('Recommendation Controller - Happy Path Tests', () => { await db.addPreferenceProfile({ userId: user.userId, name: 'Park Profile', categories: ['PARK'] }); const park1 = await db.createPlace({ name: 'Central Park', category: 'PARK', description: 'Central', city: 'Athens', rating: 4.8 }); const park2 = await db.createPlace({ name: 'National Garden', category: 'PARK', description: 'Garden', city: 'Athens', rating: 4.5 }); - await db.addDislikedPlace(user.userId, park1.placeId); + await db.addDislikedPlace({ userId: user.userId, placeId: park1.placeId }); const response = await authRequest(token).get(`/users/${user.userId}/recommendations`); const placeIds = response.body.data.recommendations.map(r => r.placeId); expect(placeIds).toContain(park2.placeId); diff --git a/tests/integration/recommendation.location.test.js b/tests/integration/recommendation.location.test.js index 1fab95b..d8772b6 100644 --- a/tests/integration/recommendation.location.test.js +++ b/tests/integration/recommendation.location.test.js @@ -193,7 +193,7 @@ describe('Recommendation Controller - Location & Edge Cases', () => { // Dislike ALL museums for (const museum of allMuseums) { - await db.addDislikedPlace(user.userId, museum.placeId); + await db.addDislikedPlace({ userId: user.userId, placeId: museum.placeId }); } // Act diff --git a/tests/unit/authService.login.test.js b/tests/unit/authService.login.test.js index 12b9c4a..cc9e966 100644 --- a/tests/unit/authService.login.test.js +++ b/tests/unit/authService.login.test.js @@ -48,7 +48,7 @@ describe('Auth Service - Login & Token', () => { describe('loginUser', () => { it('should login with correct credentials', async () => { - const result = await authService.loginUser('auth@example.com', testPassword); + const result = await authService.loginUser({ email: 'auth@example.com', password: testPassword }); expect(result).toHaveProperty('token'); expect(result).toHaveProperty('user'); @@ -57,38 +57,38 @@ describe('Auth Service - Login & Token', () => { }); it('should return a valid JWT token', async () => { - const result = await authService.loginUser('auth@example.com', testPassword); + const result = await authService.loginUser({ email: 'auth@example.com', password: testPassword }); expect(typeof result.token).toBe('string'); expect(result.token.split('.').length).toBe(3); // JWT format: header.payload.signature }); it('should throw ValidationError when email is missing', async () => { - await expect(authService.loginUser('', testPassword)) + await expect(authService.loginUser({ email: '', password: testPassword })) .rejects .toThrow(ValidationError); }); it('should throw ValidationError when password is missing', async () => { - await expect(authService.loginUser('auth@example.com', '')) + await expect(authService.loginUser({ email: 'auth@example.com', password: '' })) .rejects .toThrow(ValidationError); }); it('should throw ValidationError for invalid email format', async () => { - await expect(authService.loginUser('invalid-email', testPassword)) + await expect(authService.loginUser({ email: 'invalid-email', password: testPassword })) .rejects .toThrow(ValidationError); }); it('should throw AuthenticationError for non-existent email', async () => { - await expect(authService.loginUser('nonexistent@example.com', testPassword)) + await expect(authService.loginUser({ email: 'nonexistent@example.com', password: testPassword })) .rejects .toThrow(AuthenticationError); }); it('should throw AuthenticationError for wrong password', async () => { - await expect(authService.loginUser('auth@example.com', 'WrongPassword123')) + await expect(authService.loginUser({ email: 'auth@example.com', password: 'WrongPassword123' })) .rejects .toThrow(AuthenticationError); }); @@ -101,7 +101,7 @@ describe('Auth Service - Login & Token', () => { role: 'user' }); - await expect(authService.loginUser('nopass@example.com', 'anypassword')) + await expect(authService.loginUser({ email: 'nopass@example.com', password: 'anypassword' })) .rejects .toThrow(AuthenticationError); }); @@ -111,7 +111,7 @@ describe('Auth Service - Login & Token', () => { let validToken; beforeEach(async () => { - const result = await authService.loginUser('auth@example.com', testPassword); + const result = await authService.loginUser({ email: 'auth@example.com', password: testPassword }); validToken = result.token; }); @@ -162,29 +162,29 @@ describe('Auth Service - Login & Token', () => { // Re-fetch user to get correct userId const currentUser = await db.findUserByEmail('auth@example.com'); const newPassword = 'NewSecurePass456'; - const result = await authService.changePassword(currentUser.userId, testPassword, newPassword); + const result = await authService.changePassword({ userId: currentUser.userId, oldPassword: testPassword, newPassword }); expect(result).toBe(true); // Verify can login with new password - const loginResult = await authService.loginUser('auth@example.com', newPassword); + const loginResult = await authService.loginUser({ email: 'auth@example.com', password: newPassword }); expect(loginResult).toHaveProperty('token'); }); it('should throw AuthenticationError for wrong old password', async () => { - await expect(authService.changePassword(testUser.userId, 'WrongOldPass', 'NewPass123')) + await expect(authService.changePassword({ userId: testUser.userId, oldPassword: 'WrongOldPass', newPassword: 'NewPass123' })) .rejects .toThrow(AuthenticationError); }); it('should throw ValidationError for weak new password', async () => { - await expect(authService.changePassword(testUser.userId, testPassword, '123')) + await expect(authService.changePassword({ userId: testUser.userId, oldPassword: testPassword, newPassword: '123' })) .rejects .toThrow(ValidationError); }); it('should throw AuthenticationError for non-existent user', async () => { - await expect(authService.changePassword(99999, 'oldpass', 'NewPass123')) + await expect(authService.changePassword({ userId: 99999, oldPassword: 'oldpass', newPassword: 'NewPass123' })) .rejects .toThrow(AuthenticationError); }); @@ -201,7 +201,7 @@ describe('Auth Service - Login & Token', () => { }); const newPassword = 'NewHashedPass789'; - await authService.changePassword(isolatedUser.userId, isolatedPassword, newPassword); + await authService.changePassword({ userId: isolatedUser.userId, oldPassword: isolatedPassword, newPassword }); const user = await db.findUserById(isolatedUser.userId); expect(user.password).not.toBe(newPassword); // Should be hashed @@ -220,15 +220,15 @@ describe('Auth Service - Login & Token', () => { }); const newPassword = 'BrandNewPass999'; - await authService.changePassword(isolatedUser.userId, isolatedPassword, newPassword); + await authService.changePassword({ userId: isolatedUser.userId, oldPassword: isolatedPassword, newPassword }); // Old password should fail - await expect(authService.loginUser('isolated.oldpass@example.com', isolatedPassword)) + await expect(authService.loginUser({ email: 'isolated.oldpass@example.com', password: isolatedPassword })) .rejects .toThrow(AuthenticationError); // New password should work - const loginResult = await authService.loginUser('isolated.oldpass@example.com', newPassword); + const loginResult = await authService.loginUser({ email: 'isolated.oldpass@example.com', password: newPassword }); expect(loginResult).toHaveProperty('token'); }); }); diff --git a/tests/unit/infrastructure.db.test.js b/tests/unit/infrastructure.db.test.js index c991c9a..3b86f3b 100644 --- a/tests/unit/infrastructure.db.test.js +++ b/tests/unit/infrastructure.db.test.js @@ -226,7 +226,7 @@ describe('Infrastructure - MongoDB API Wrapper', () => { const mockProfile = { profileId: 1, userId: 1, categories: ['food'] }; mockModels.PreferenceProfile.findOneAndUpdate.mockResolvedValue(mockProfile); - const result = await mongoDb.updatePreferenceProfile(1, 1, { categories: ['food'] }); + const result = await mongoDb.updatePreferenceProfile({ userId: 1, profileId: 1, update: { categories: ['food'] } }); expect(mockModels.PreferenceProfile.findOneAndUpdate).toHaveBeenCalled(); expect(result).toEqual(mockProfile); diff --git a/tests/unit/models.Place.test.js b/tests/unit/models.Place.test.js index a49f5e2..ffc2d11 100644 --- a/tests/unit/models.Place.test.js +++ b/tests/unit/models.Place.test.js @@ -63,12 +63,12 @@ describe('Place Model', () => { }); test('findNearLocation returns places within distance', async () => { - const results = await Place.findNearLocation(37.985, 23.730, 5); + const results = await Place.findNearLocation({ lat: 37.985, lng: 23.730, maxDistance: 5 }); expect(results.length).toBeGreaterThanOrEqual(1); }); test('findNearLocation excludes distant places', async () => { - const results = await Place.findNearLocation(37.985, 23.730, 0.1); + const results = await Place.findNearLocation({ lat: 37.985, lng: 23.730, maxDistance: 0.1 }); expect(results.length).toBeLessThan(3); }); }); diff --git a/tests/unit/mongoDb.data.test.js b/tests/unit/mongoDb.data.test.js index 56f8eb1..2d5906f 100644 --- a/tests/unit/mongoDb.data.test.js +++ b/tests/unit/mongoDb.data.test.js @@ -116,21 +116,21 @@ describe('MongoDB Data Operations', () => { }); test('should add a favourite place', async () => { - const fav = await mongoDb.addFavouritePlace(testUser.userId, testPlace.placeId); + const fav = await mongoDb.addFavouritePlace({ userId: testUser.userId, placeId: testPlace.placeId }); expect(fav).toBeDefined(); expect(fav).toHaveProperty('favouriteId'); }); test('should prevent duplicate favourites', async () => { - await mongoDb.addFavouritePlace(testUser.userId, testPlace.placeId); - const duplicate = await mongoDb.addFavouritePlace(testUser.userId, testPlace.placeId); + await mongoDb.addFavouritePlace({ userId: testUser.userId, placeId: testPlace.placeId }); + const duplicate = await mongoDb.addFavouritePlace({ userId: testUser.userId, placeId: testPlace.placeId }); expect(duplicate).toBeNull(); }); test('should retrieve user favourite places with populated data', async () => { - await mongoDb.addFavouritePlace(testUser.userId, testPlace.placeId); + await mongoDb.addFavouritePlace({ userId: testUser.userId, placeId: testPlace.placeId }); const favourites = await mongoDb.getFavouritePlaces(testUser.userId); From af222f3dcd6fc1f4670f4c2ad154669717ff1679 Mon Sep 17 00:00:00 2001 From: Giannis Fraidakis Date: Thu, 1 Jan 2026 13:02:29 +0200 Subject: [PATCH 18/19] Revert "refactor: Minimize rateLimiter and eslint config for better maintainability" This reverts commit 93a65a3f7d86a2ca0e773500245178aae4aadd7a. --- eslint.config.js | 25 +++++++++++++------------ middleware/rateLimiter.js | 26 ++++++++------------------ 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index d76f08e..ec52f57 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,8 +1,4 @@ -/** - * ESLint Configuration - * Defines linting rules for the project - * @module eslint.config - */ +/** ESLint Configuration */ import js from "@eslint/js"; import globals from "globals"; import { defineConfig } from "eslint/config"; @@ -20,11 +16,16 @@ const testFiles = ["tests/**/*.{js,mjs,cjs}", "**/*.test.{js,mjs,cjs}"]; const unusedVarsRule = ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }]; export default defineConfig([ - { ignores: ignorePatterns }, - { files: sourceFiles, plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node }, rules: { "no-unused-vars": unusedVarsRule } }, - { files: testFiles, languageOptions: { globals: { ...globals.jest, ...globals.node } } } + { ignores: ["node_modules/**", "coverage/**"] }, + { + files: ["**/*.{js,mjs,cjs}"], + plugins: { js }, + extends: ["js/recommended"], + languageOptions: { globals: globals.node }, + rules: { "no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }] } + }, + { + files: ["tests/**/*.{js,mjs,cjs}", "**/*.test.{js,mjs,cjs}"], + languageOptions: { globals: { ...globals.jest, ...globals.node } } + } ]); - - - - diff --git a/middleware/rateLimiter.js b/middleware/rateLimiter.js index 0c5d03f..b6c3ea0 100644 --- a/middleware/rateLimiter.js +++ b/middleware/rateLimiter.js @@ -5,33 +5,23 @@ */ import rateLimit from 'express-rate-limit'; -/** Error response for rate limit exceeded */ -const errorMsg = (msg) => ({ success: false, error: 'TOO_MANY_REQUESTS', message: msg }); - -/** - * Authentication rate limiter - * Limits: 10 requests per 15 minutes - * Skips successful requests - */ +/** Authentication rate limiter */ export const authLimiter = rateLimit({ windowMs: 900000, max: 10, + standardHeaders: true, + legacyHeaders: false, skipSuccessfulRequests: true, - message: errorMsg('Too many auth attempts') + message: { success: false, error: 'TOO_MANY_REQUESTS', message: 'Too many auth attempts' } }); -/** - * General API rate limiter - * Limits: 100 requests per minute - */ +/** API rate limiter */ export const apiLimiter = rateLimit({ windowMs: 60000, max: 100, - message: errorMsg('Too many requests') + standardHeaders: true, + legacyHeaders: false, + message: { success: false, error: 'TOO_MANY_REQUESTS', message: 'Too many requests' } }); export default { authLimiter, apiLimiter }; - - - - From 97d7dc269bff2678da72268f652babdda7dc8fe6 Mon Sep 17 00:00:00 2001 From: Giannis Fraidakis Date: Thu, 1 Jan 2026 13:06:36 +0200 Subject: [PATCH 19/19] Revert to commit 7e76f26: docs: Add JSDoc to remaining models --- config/inMemoryDb/placeOps.js | 15 +++----- config/inMemoryDb/userOps.js | 3 +- config/mongoDb/placeOps.js | 16 ++++----- config/mongoDb/userOps.js | 3 +- controllers/adminController.js | 2 +- controllers/authController.js | 10 +++--- controllers/dislikedController.js | 4 +-- controllers/favouriteController.js | 4 +-- controllers/preferenceModify.js | 2 +- eslint.config.js | 14 ++------ middleware/rateLimiter.js | 8 ++--- models/Place.js | 3 +- package-lock.json | 5 --- services/authService.js | 26 +++++++------- services/favouriteService.js | 8 ++--- services/placeService.js | 2 +- services/preferenceService.js | 2 +- tests/integration/favourite.add.test.js | 2 +- tests/integration/favourite.disliked.test.js | 8 ++--- tests/integration/favourite.places.test.js | 14 ++++---- .../integration/recommendation.happy.test.js | 2 +- .../recommendation.location.test.js | 2 +- tests/unit/authService.login.test.js | 36 +++++++++---------- tests/unit/infrastructure.db.test.js | 2 +- tests/unit/models.Place.test.js | 4 +-- tests/unit/mongoDb.data.test.js | 8 ++--- 26 files changed, 87 insertions(+), 118 deletions(-) diff --git a/config/inMemoryDb/placeOps.js b/config/inMemoryDb/placeOps.js index 9c76f9e..150e542 100644 --- a/config/inMemoryDb/placeOps.js +++ b/config/inMemoryDb/placeOps.js @@ -53,8 +53,7 @@ export async function createPlace(placeData) { return newPlace; } -/** Update place data */ -export async function updatePlace({ placeId, updateData }) { +export async function updatePlace(placeId, updateData) { const idx = data.places.findIndex(p => p.placeId === placeId); if (idx === -1) return null; data.places[idx] = { ...data.places[idx], ...updateData }; @@ -106,8 +105,7 @@ export async function getFavouritePlaces(userId) { }).filter(Boolean); } -/** Add place to favourites */ -export async function addFavouritePlace({ userId, placeId }) { +export async function addFavouritePlace(userId, placeId) { const existing = data.favouritePlaces.find(f => f.userId === userId && f.placeId === placeId); if (existing) return null; @@ -120,8 +118,7 @@ export async function addFavouritePlace({ userId, placeId }) { return newFavourite; } -/** Remove place from favourites */ -export async function removeFavouritePlace({ userId, favouriteId }) { +export async function removeFavouritePlace(userId, favouriteId) { const idx = data.favouritePlaces.findIndex(f => f.favouriteId === favouriteId && f.userId === userId); if (idx === -1) return false; data.favouritePlaces.splice(idx, 1); @@ -137,8 +134,7 @@ export async function getDislikedPlaces(userId) { }); } -/** Add place to disliked */ -export async function addDislikedPlace({ userId, placeId }) { +export async function addDislikedPlace(userId, placeId) { const existing = data.dislikedPlaces.find(d => d.userId === userId && d.placeId === placeId); if (existing) return null; @@ -151,8 +147,7 @@ export async function addDislikedPlace({ userId, placeId }) { return newDisliked; } -/** Remove place from disliked */ -export async function removeDislikedPlace({ userId, dislikedId }) { +export async function removeDislikedPlace(userId, dislikedId) { const idx = data.dislikedPlaces.findIndex(d => d.dislikedId === dislikedId && d.userId === userId); if (idx === -1) return false; data.dislikedPlaces.splice(idx, 1); diff --git a/config/inMemoryDb/userOps.js b/config/inMemoryDb/userOps.js index 86e838c..c654844 100644 --- a/config/inMemoryDb/userOps.js +++ b/config/inMemoryDb/userOps.js @@ -33,8 +33,7 @@ export async function addPreferenceProfile(profile) { return profile; } -/** Update preference profile */ -export async function updatePreferenceProfile({ userId, profileId, update }) { +export async function updatePreferenceProfile(userId, profileId, update) { const idx = data.preferenceProfiles.findIndex(p => p.userId === userId && p.profileId === profileId); if (idx === -1) return null; data.preferenceProfiles[idx] = { ...data.preferenceProfiles[idx], ...update }; diff --git a/config/mongoDb/placeOps.js b/config/mongoDb/placeOps.js index 92f90a6..24fc687 100644 --- a/config/mongoDb/placeOps.js +++ b/config/mongoDb/placeOps.js @@ -40,8 +40,8 @@ export async function getAllPlaces() { return await models.Place.find({}); } -/** Update place data */ -export async function updatePlace({ placeId, updateData }) { +// Place mutation operations +export async function updatePlace(placeId, updateData) { const doc = await models.Place.findOneAndUpdate( { placeId }, { $set: updateData }, @@ -86,8 +86,7 @@ export async function getFavouritePlaces(userId) { return result; } -/** Add place to favourites */ -export async function addFavouritePlace({ userId, placeId }) { +export async function addFavouritePlace(userId, placeId) { const existing = await models.FavouritePlace.findOne({ userId, placeId }); if (existing) return null; @@ -98,8 +97,7 @@ export async function addFavouritePlace({ userId, placeId }) { return doc; } -/** Remove place from favourites */ -export async function removeFavouritePlace({ userId, favouriteId }) { +export async function removeFavouritePlace(userId, favouriteId) { const result = await models.FavouritePlace.deleteOne({ userId, favouriteId }); return result.deletedCount > 0; } @@ -118,8 +116,7 @@ export async function getDislikedPlaces(userId) { return result; } -/** Add place to disliked */ -export async function addDislikedPlace({ userId, placeId }) { +export async function addDislikedPlace(userId, placeId) { const existing = await models.DislikedPlace.findOne({ userId, placeId }); if (existing) return null; @@ -130,8 +127,7 @@ export async function addDislikedPlace({ userId, placeId }) { return doc; } -/** Remove place from disliked */ -export async function removeDislikedPlace({ userId, dislikedId }) { +export async function removeDislikedPlace(userId, dislikedId) { const result = await models.DislikedPlace.deleteOne({ userId, dislikedId }); return result.deletedCount > 0; } diff --git a/config/mongoDb/userOps.js b/config/mongoDb/userOps.js index 10dfa62..94f97f7 100644 --- a/config/mongoDb/userOps.js +++ b/config/mongoDb/userOps.js @@ -22,8 +22,7 @@ export async function addPreferenceProfile(profile) { return doc; } -/** Update preference profile */ -export async function updatePreferenceProfile({ userId, profileId, update }) { +export async function updatePreferenceProfile(userId, profileId, update) { const doc = await models.PreferenceProfile.findOneAndUpdate( { userId, profileId }, { $set: update, $currentDate: { updatedAt: true } }, diff --git a/controllers/adminController.js b/controllers/adminController.js index 4ea78c9..5e32e41 100644 --- a/controllers/adminController.js +++ b/controllers/adminController.js @@ -40,7 +40,7 @@ const updatePlace = async (req, res, next) => { ['name', 'description', 'category', 'website'].forEach(k => { if (req.body[k]) updateData[k] = req.body[k]; }); if (req.body.location) updateData.location = { ...placeObj.location, ...req.body.location }; - const updatedPlace = await db.updatePlace({ placeId, updateData }); + const updatedPlace = await db.updatePlace(placeId, updateData); const updatedPlaceObj = updatedPlace.toObject ? updatedPlace.toObject() : updatedPlace; const placeWithReviews = { ...updatedPlaceObj, reviews: await db.getReviewsForPlace(placeId) }; diff --git a/controllers/authController.js b/controllers/authController.js index de8d4a8..2d636c1 100644 --- a/controllers/authController.js +++ b/controllers/authController.js @@ -17,10 +17,10 @@ import { SUCCESS_MESSAGES, HTTP_STATUS } from '../config/constants.js'; const login = async (req, res, next) => { try { const { email, password } = req.body; - + // Call service to handle business logic - const result = await authService.loginUser({ email, password }); - + const result = await authService.loginUser(email, password); + res.json({ success: true, data: { @@ -46,10 +46,10 @@ const login = async (req, res, next) => { const signup = async (req, res, next) => { try { const { name, email, password } = req.body; - + // Call service to handle business logic const result = await authService.registerUser({ name, email, password }); - + res.status(HTTP_STATUS.CREATED).json({ success: true, data: { diff --git a/controllers/dislikedController.js b/controllers/dislikedController.js index 110e560..9e1a256 100644 --- a/controllers/dislikedController.js +++ b/controllers/dislikedController.js @@ -36,7 +36,7 @@ const addDislikedPlace = async (req, res, next) => { const validation = await requireUserAndPlace(res, userId, req.body.placeId); if (!validation) return; - const newDisliked = await db.addDislikedPlace({ userId, placeId: req.body.placeId }); + const newDisliked = await db.addDislikedPlace(userId, req.body.placeId); if (!newDisliked) { return R.conflict(res, 'PLACE_ALREADY_DISLIKED', "Place is already in user's disliked places", { placeId: req.body.placeId, userId }); } @@ -53,7 +53,7 @@ const removeDislikedPlace = async (req, res, next) => { const user = await requireUser(res, userId); if (!user) return; - const removed = await db.removeDislikedPlace({ userId, dislikedId }); + const removed = await db.removeDislikedPlace(userId, dislikedId); if (!removed) { return R.notFound(res, 'DISLIKED_NOT_FOUND', `Disliked with ID ${dislikedId} not found for user ${userId}`); } diff --git a/controllers/favouriteController.js b/controllers/favouriteController.js index 4f999f4..cd1fc98 100644 --- a/controllers/favouriteController.js +++ b/controllers/favouriteController.js @@ -38,7 +38,7 @@ const addFavouritePlace = async (req, res, next) => { const validation = await requireUserAndPlace(res, userId, req.body.placeId); if (!validation) return; - const newFavourite = await db.addFavouritePlace({ userId, placeId: req.body.placeId }); + const newFavourite = await db.addFavouritePlace(userId, req.body.placeId); if (!newFavourite) { return R.conflict(res, 'PLACE_ALREADY_FAVORITED', "Place is already in user's favorites", { placeId: req.body.placeId, userId }); } @@ -62,7 +62,7 @@ const removeFavouritePlace = async (req, res, next) => { const user = await requireUser(res, userId); if (!user) return; - const removed = await db.removeFavouritePlace({ userId, favouriteId }); + const removed = await db.removeFavouritePlace(userId, favouriteId); if (!removed) { return R.notFound(res, 'FAVOURITE_NOT_FOUND', `Favourite with ID ${favouriteId} not found for user ${userId}`); } diff --git a/controllers/preferenceModify.js b/controllers/preferenceModify.js index 7a5e60a..9c36819 100644 --- a/controllers/preferenceModify.js +++ b/controllers/preferenceModify.js @@ -38,7 +38,7 @@ const updatePreferenceProfile = async (req, res, next) => { if ('profileName' in updatePayload) { updatePayload.name = updatePayload.profileName; delete updatePayload.profileName; } if ('selectedPreferences' in updatePayload) delete updatePayload.selectedPreferences; - const updatedProfile = await db.updatePreferenceProfile({ userId, profileId, update: updatePayload }); + const updatedProfile = await db.updatePreferenceProfile(userId, profileId, updatePayload); if (!updatedProfile) { return R.notFound(res, 'PROFILE_NOT_FOUND', `Profile with ID ${profileId} not found for user ${userId}`); } diff --git a/eslint.config.js b/eslint.config.js index ec52f57..b0a9732 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,18 +3,6 @@ import js from "@eslint/js"; import globals from "globals"; import { defineConfig } from "eslint/config"; -/** Directories to ignore during linting */ -const ignorePatterns = ["node_modules/**", "coverage/**"]; - -/** Source file patterns */ -const sourceFiles = ["**/*.{js,mjs,cjs}"]; - -/** Test file patterns */ -const testFiles = ["tests/**/*.{js,mjs,cjs}", "**/*.test.{js,mjs,cjs}"]; - -/** Unused variable rule allowing underscore prefix */ -const unusedVarsRule = ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }]; - export default defineConfig([ { ignores: ["node_modules/**", "coverage/**"] }, { @@ -29,3 +17,5 @@ export default defineConfig([ languageOptions: { globals: { ...globals.jest, ...globals.node } } } ]); + + diff --git a/middleware/rateLimiter.js b/middleware/rateLimiter.js index b6c3ea0..88ac334 100644 --- a/middleware/rateLimiter.js +++ b/middleware/rateLimiter.js @@ -1,8 +1,4 @@ -/** - * Rate Limiting Middleware - * Protects API endpoints from abuse - * @module middleware/rateLimiter - */ +/** Rate Limiting Middleware */ import rateLimit from 'express-rate-limit'; /** Authentication rate limiter */ @@ -25,3 +21,5 @@ export const apiLimiter = rateLimit({ }); export default { authLimiter, apiLimiter }; + + diff --git a/models/Place.js b/models/Place.js index b7a01b4..10ee198 100644 --- a/models/Place.js +++ b/models/Place.js @@ -35,8 +35,7 @@ placeSchema.statics.findByCity = function (city) { return this.find({ city }); }; -/** Find places near a location */ -placeSchema.statics.findNearLocation = function ({ lat, lng, maxDistance }) { +placeSchema.statics.findNearLocation = function (lat, lng, maxDistance) { const latRange = maxDistance / 111; const lngRange = maxDistance / (111 * Math.cos(lat * Math.PI / 180)); return this.find({ diff --git a/package-lock.json b/package-lock.json index dcb2e01..ae760a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,7 +66,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1430,7 +1429,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1832,7 +1830,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2542,7 +2539,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2779,7 +2775,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", diff --git a/services/authService.js b/services/authService.js index 7446b8d..3856271 100644 --- a/services/authService.js +++ b/services/authService.js @@ -13,12 +13,11 @@ import { JWT_EXPIRES_IN } from '../config/constants.js'; /** * Authenticate user with email and password - * @param {Object} credentials - Login credentials - * @param {string} credentials.email - User email - * @param {string} credentials.password - User password - * @returns {Promise} Token and user data + * @param {string} email - User email + * @param {string} password - User password + * @returns {Promise} Object containing token and user data */ -export const loginUser = async ({ email, password }) => { +export const loginUser = async (email, password) => { // Validate input if (!email || !password) { throw new ValidationError('Email and password are required'); @@ -120,10 +119,10 @@ export const registerUser = async ({ name, email, password }) => { */ const generateToken = (user) => { return jwt.sign( - { - userId: user.userId, - email: user.email, - role: user.role + { + userId: user.userId, + email: user.email, + role: user.role }, process.env.JWT_SECRET, { expiresIn: JWT_EXPIRES_IN } @@ -148,13 +147,12 @@ export const verifyToken = (token) => { /** * Change user password - * @param {Object} params - Password change parameters - * @param {number} params.userId - User ID - * @param {string} params.oldPassword - Current password - * @param {string} params.newPassword - New password + * @param {number} userId - User ID + * @param {string} oldPassword - Current password + * @param {string} newPassword - New password * @returns {Promise} True if successful */ -export const changePassword = async ({ userId, oldPassword, newPassword }) => { +export const changePassword = async (userId, oldPassword, newPassword) => { // Find user const user = await db.findUserById(userId); if (!user) { diff --git a/services/favouriteService.js b/services/favouriteService.js index 3b5e614..ff440bb 100644 --- a/services/favouriteService.js +++ b/services/favouriteService.js @@ -41,7 +41,7 @@ export const addFavouritePlace = async (userId, placeId) => { throw new NotFoundError('Place', placeId); } - return await db.addFavouritePlace({ userId, placeId }); + return await db.addFavouritePlace(userId, placeId); }; export const removeFavouritePlace = async (userId, placeId) => { @@ -52,7 +52,7 @@ export const removeFavouritePlace = async (userId, placeId) => { throw new ValidationError('Invalid place ID'); } - return await db.removeFavouritePlace({ userId, favouriteId: placeId }); + return await db.removeFavouritePlace(userId, placeId); }; export const getDislikedPlaces = async (userId) => { @@ -77,7 +77,7 @@ export const addDislikedPlace = async (userId, placeId) => { throw new NotFoundError('Place', placeId); } - return await db.addDislikedPlace({ userId, placeId }); + return await db.addDislikedPlace(userId, placeId); }; export const removeDislikedPlace = async (userId, placeId) => { @@ -88,7 +88,7 @@ export const removeDislikedPlace = async (userId, placeId) => { throw new ValidationError('Invalid place ID'); } - return await db.removeDislikedPlace({ userId, dislikedId: placeId }); + return await db.removeDislikedPlace(userId, placeId); }; export default { diff --git a/services/placeService.js b/services/placeService.js index a13a34f..b81b13f 100644 --- a/services/placeService.js +++ b/services/placeService.js @@ -55,7 +55,7 @@ export const updatePlace = async (placeId, updateData) => { throw new NotFoundError('Place', placeId); } - return await db.updatePlace({ placeId, updateData }); + return await db.updatePlace(placeId, updateData); }; export const deletePlace = async (placeId) => { diff --git a/services/preferenceService.js b/services/preferenceService.js index 6937fb7..087b14e 100644 --- a/services/preferenceService.js +++ b/services/preferenceService.js @@ -64,7 +64,7 @@ export const updatePreferenceProfile = async (userId, profileId, updateData) => throw new NotFoundError('Preference Profile', profileId); } - return await db.updatePreferenceProfile({ userId, profileId, update: updateData }); + return await db.updatePreferenceProfile(userId, profileId, updateData); }; export const deletePreferenceProfile = async (userId, profileId) => { diff --git a/tests/integration/favourite.add.test.js b/tests/integration/favourite.add.test.js index 351e881..1d12c74 100644 --- a/tests/integration/favourite.add.test.js +++ b/tests/integration/favourite.add.test.js @@ -60,7 +60,7 @@ describe('Favourite Controller - Add Favourite Tests', () => { it('should return 409 when place is already in favourites', async () => { const { user, token } = await createAuthenticatedUser(); const testPlace = await db.createPlace({ name: 'Duplicate Place', category: 'restaurant', description: 'Test', city: 'Athens' }); - await db.addFavouritePlace({ userId: user.userId, placeId: testPlace.placeId }); + await db.addFavouritePlace(user.userId, testPlace.placeId); const response = await authRequest(token).post(`/users/${user.userId}/favourite-places`) .send({ placeId: testPlace.placeId }); expect(response.status).toBe(409); diff --git a/tests/integration/favourite.disliked.test.js b/tests/integration/favourite.disliked.test.js index 51949ea..7484bf9 100644 --- a/tests/integration/favourite.disliked.test.js +++ b/tests/integration/favourite.disliked.test.js @@ -43,7 +43,7 @@ describe('Favourite Controller - Disliked Places Tests', () => { city: 'Athens' }); - await db.addDislikedPlace({ userId: user.userId, placeId: testPlace.placeId }); + await db.addDislikedPlace(user.userId, testPlace.placeId); // Act const response = await authRequest(token).get(`/users/${user.userId}/disliked-places`); @@ -111,7 +111,7 @@ describe('Favourite Controller - Disliked Places Tests', () => { city: 'Athens' }); - await db.addDislikedPlace({ userId: user.userId, placeId: testPlace.placeId }); + await db.addDislikedPlace(user.userId, testPlace.placeId); // Act - Try to add again const response = await authRequest(token) @@ -167,7 +167,7 @@ describe('Favourite Controller - Disliked Places Tests', () => { city: 'Athens' }); - const disliked = await db.addDislikedPlace({ userId: user.userId, placeId: testPlace.placeId }); + const disliked = await db.addDislikedPlace(user.userId, testPlace.placeId); // Act const response = await authRequest(token) @@ -187,7 +187,7 @@ describe('Favourite Controller - Disliked Places Tests', () => { city: 'Athens' }); - const disliked = await db.addDislikedPlace({ userId: user.userId, placeId: testPlace.placeId }); + const disliked = await db.addDislikedPlace(user.userId, testPlace.placeId); // Act await authRequest(token) diff --git a/tests/integration/favourite.places.test.js b/tests/integration/favourite.places.test.js index 6f29426..5a1122e 100644 --- a/tests/integration/favourite.places.test.js +++ b/tests/integration/favourite.places.test.js @@ -30,7 +30,7 @@ describe('Favourite Controller - Get & Remove Tests', () => { it('should return users favourite places', async () => { const { user, token } = await createAuthenticatedUser(); const testPlace = await db.createPlace({ name: 'Acropolis Museum', category: 'museum', description: 'Ancient Greek artifacts', city: 'Athens' }); - await db.addFavouritePlace({ userId: user.userId, placeId: testPlace.placeId }); + await db.addFavouritePlace(user.userId, testPlace.placeId); const response = await authRequest(token).get(`/users/${user.userId}/favourite-places`); expect(response.status).toBe(200); expect(response.body.data.favourites).toHaveLength(1); @@ -41,8 +41,8 @@ describe('Favourite Controller - Get & Remove Tests', () => { const { user, token } = await createAuthenticatedUser(); const place1 = await db.createPlace({ name: 'Museum 1', category: 'museum', description: 'First museum', city: 'Athens' }); const place2 = await db.createPlace({ name: 'Restaurant 1', category: 'restaurant', description: 'First restaurant', city: 'Athens' }); - await db.addFavouritePlace({ userId: user.userId, placeId: place1.placeId }); - await db.addFavouritePlace({ userId: user.userId, placeId: place2.placeId }); + await db.addFavouritePlace(user.userId, place1.placeId); + await db.addFavouritePlace(user.userId, place2.placeId); const response = await authRequest(token).get(`/users/${user.userId}/favourite-places`); expect(response.status).toBe(200); expect(response.body.data.favourites).toHaveLength(2); @@ -86,7 +86,7 @@ describe('Favourite Controller - Get & Remove Tests', () => { it('should remove place from favourites', async () => { const { user, token } = await createAuthenticatedUser(); const testPlace = await db.createPlace({ name: 'To Remove', category: 'cafe', description: 'Test cafe', city: 'Athens' }); - const favourite = await db.addFavouritePlace({ userId: user.userId, placeId: testPlace.placeId }); + const favourite = await db.addFavouritePlace(user.userId, testPlace.placeId); const response = await authRequest(token).delete(`/users/${user.userId}/favourite-places/${favourite.favouriteId}`); expect(response.status).toBe(204); }); @@ -94,7 +94,7 @@ describe('Favourite Controller - Get & Remove Tests', () => { it('should not exist after removal', async () => { const { user, token } = await createAuthenticatedUser(); const testPlace = await db.createPlace({ name: 'To Remove', category: 'museum', description: 'Test museum', city: 'Athens' }); - const favourite = await db.addFavouritePlace({ userId: user.userId, placeId: testPlace.placeId }); + const favourite = await db.addFavouritePlace(user.userId, testPlace.placeId); await authRequest(token).delete(`/users/${user.userId}/favourite-places/${favourite.favouriteId}`); const verifyResponse = await authRequest(token).get(`/users/${user.userId}/favourite-places`); expect(verifyResponse.body.data.favourites).toHaveLength(0); @@ -113,7 +113,7 @@ describe('Favourite Controller - Get & Remove Tests', () => { const { user: user1, token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); const { user: user2 } = await createAuthenticatedUser({ email: 'user2@example.com' }); const testPlace = await db.createPlace({ name: 'Test Place', category: 'restaurant', description: 'Test', city: 'Athens' }); - const user2Favourite = await db.addFavouritePlace({ userId: user2.userId, placeId: testPlace.placeId }); + const user2Favourite = await db.addFavouritePlace(user2.userId, testPlace.placeId); const response = await authRequest(token1).delete(`/users/${user1.userId}/favourite-places/${user2Favourite.favouriteId}`); expect(response.status).toBe(404); }); @@ -130,7 +130,7 @@ describe('Favourite Controller - Get & Remove Tests', () => { const { token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); const { user: user2 } = await createAuthenticatedUser({ email: 'user2@example.com' }); const testPlace = await db.createPlace({ name: 'Test Place', category: 'cafe', description: 'Test', city: 'Athens' }); - const user2Favourite = await db.addFavouritePlace({ userId: user2.userId, placeId: testPlace.placeId }); + const user2Favourite = await db.addFavouritePlace(user2.userId, testPlace.placeId); const response = await authRequest(token1).delete(`/users/${user2.userId}/favourite-places/${user2Favourite.favouriteId}`); expect(response.status).toBe(403); }); diff --git a/tests/integration/recommendation.happy.test.js b/tests/integration/recommendation.happy.test.js index 424d6e4..bce87a1 100644 --- a/tests/integration/recommendation.happy.test.js +++ b/tests/integration/recommendation.happy.test.js @@ -52,7 +52,7 @@ describe('Recommendation Controller - Happy Path Tests', () => { await db.addPreferenceProfile({ userId: user.userId, name: 'Park Profile', categories: ['PARK'] }); const park1 = await db.createPlace({ name: 'Central Park', category: 'PARK', description: 'Central', city: 'Athens', rating: 4.8 }); const park2 = await db.createPlace({ name: 'National Garden', category: 'PARK', description: 'Garden', city: 'Athens', rating: 4.5 }); - await db.addDislikedPlace({ userId: user.userId, placeId: park1.placeId }); + await db.addDislikedPlace(user.userId, park1.placeId); const response = await authRequest(token).get(`/users/${user.userId}/recommendations`); const placeIds = response.body.data.recommendations.map(r => r.placeId); expect(placeIds).toContain(park2.placeId); diff --git a/tests/integration/recommendation.location.test.js b/tests/integration/recommendation.location.test.js index d8772b6..1fab95b 100644 --- a/tests/integration/recommendation.location.test.js +++ b/tests/integration/recommendation.location.test.js @@ -193,7 +193,7 @@ describe('Recommendation Controller - Location & Edge Cases', () => { // Dislike ALL museums for (const museum of allMuseums) { - await db.addDislikedPlace({ userId: user.userId, placeId: museum.placeId }); + await db.addDislikedPlace(user.userId, museum.placeId); } // Act diff --git a/tests/unit/authService.login.test.js b/tests/unit/authService.login.test.js index cc9e966..12b9c4a 100644 --- a/tests/unit/authService.login.test.js +++ b/tests/unit/authService.login.test.js @@ -48,7 +48,7 @@ describe('Auth Service - Login & Token', () => { describe('loginUser', () => { it('should login with correct credentials', async () => { - const result = await authService.loginUser({ email: 'auth@example.com', password: testPassword }); + const result = await authService.loginUser('auth@example.com', testPassword); expect(result).toHaveProperty('token'); expect(result).toHaveProperty('user'); @@ -57,38 +57,38 @@ describe('Auth Service - Login & Token', () => { }); it('should return a valid JWT token', async () => { - const result = await authService.loginUser({ email: 'auth@example.com', password: testPassword }); + const result = await authService.loginUser('auth@example.com', testPassword); expect(typeof result.token).toBe('string'); expect(result.token.split('.').length).toBe(3); // JWT format: header.payload.signature }); it('should throw ValidationError when email is missing', async () => { - await expect(authService.loginUser({ email: '', password: testPassword })) + await expect(authService.loginUser('', testPassword)) .rejects .toThrow(ValidationError); }); it('should throw ValidationError when password is missing', async () => { - await expect(authService.loginUser({ email: 'auth@example.com', password: '' })) + await expect(authService.loginUser('auth@example.com', '')) .rejects .toThrow(ValidationError); }); it('should throw ValidationError for invalid email format', async () => { - await expect(authService.loginUser({ email: 'invalid-email', password: testPassword })) + await expect(authService.loginUser('invalid-email', testPassword)) .rejects .toThrow(ValidationError); }); it('should throw AuthenticationError for non-existent email', async () => { - await expect(authService.loginUser({ email: 'nonexistent@example.com', password: testPassword })) + await expect(authService.loginUser('nonexistent@example.com', testPassword)) .rejects .toThrow(AuthenticationError); }); it('should throw AuthenticationError for wrong password', async () => { - await expect(authService.loginUser({ email: 'auth@example.com', password: 'WrongPassword123' })) + await expect(authService.loginUser('auth@example.com', 'WrongPassword123')) .rejects .toThrow(AuthenticationError); }); @@ -101,7 +101,7 @@ describe('Auth Service - Login & Token', () => { role: 'user' }); - await expect(authService.loginUser({ email: 'nopass@example.com', password: 'anypassword' })) + await expect(authService.loginUser('nopass@example.com', 'anypassword')) .rejects .toThrow(AuthenticationError); }); @@ -111,7 +111,7 @@ describe('Auth Service - Login & Token', () => { let validToken; beforeEach(async () => { - const result = await authService.loginUser({ email: 'auth@example.com', password: testPassword }); + const result = await authService.loginUser('auth@example.com', testPassword); validToken = result.token; }); @@ -162,29 +162,29 @@ describe('Auth Service - Login & Token', () => { // Re-fetch user to get correct userId const currentUser = await db.findUserByEmail('auth@example.com'); const newPassword = 'NewSecurePass456'; - const result = await authService.changePassword({ userId: currentUser.userId, oldPassword: testPassword, newPassword }); + const result = await authService.changePassword(currentUser.userId, testPassword, newPassword); expect(result).toBe(true); // Verify can login with new password - const loginResult = await authService.loginUser({ email: 'auth@example.com', password: newPassword }); + const loginResult = await authService.loginUser('auth@example.com', newPassword); expect(loginResult).toHaveProperty('token'); }); it('should throw AuthenticationError for wrong old password', async () => { - await expect(authService.changePassword({ userId: testUser.userId, oldPassword: 'WrongOldPass', newPassword: 'NewPass123' })) + await expect(authService.changePassword(testUser.userId, 'WrongOldPass', 'NewPass123')) .rejects .toThrow(AuthenticationError); }); it('should throw ValidationError for weak new password', async () => { - await expect(authService.changePassword({ userId: testUser.userId, oldPassword: testPassword, newPassword: '123' })) + await expect(authService.changePassword(testUser.userId, testPassword, '123')) .rejects .toThrow(ValidationError); }); it('should throw AuthenticationError for non-existent user', async () => { - await expect(authService.changePassword({ userId: 99999, oldPassword: 'oldpass', newPassword: 'NewPass123' })) + await expect(authService.changePassword(99999, 'oldpass', 'NewPass123')) .rejects .toThrow(AuthenticationError); }); @@ -201,7 +201,7 @@ describe('Auth Service - Login & Token', () => { }); const newPassword = 'NewHashedPass789'; - await authService.changePassword({ userId: isolatedUser.userId, oldPassword: isolatedPassword, newPassword }); + await authService.changePassword(isolatedUser.userId, isolatedPassword, newPassword); const user = await db.findUserById(isolatedUser.userId); expect(user.password).not.toBe(newPassword); // Should be hashed @@ -220,15 +220,15 @@ describe('Auth Service - Login & Token', () => { }); const newPassword = 'BrandNewPass999'; - await authService.changePassword({ userId: isolatedUser.userId, oldPassword: isolatedPassword, newPassword }); + await authService.changePassword(isolatedUser.userId, isolatedPassword, newPassword); // Old password should fail - await expect(authService.loginUser({ email: 'isolated.oldpass@example.com', password: isolatedPassword })) + await expect(authService.loginUser('isolated.oldpass@example.com', isolatedPassword)) .rejects .toThrow(AuthenticationError); // New password should work - const loginResult = await authService.loginUser({ email: 'isolated.oldpass@example.com', password: newPassword }); + const loginResult = await authService.loginUser('isolated.oldpass@example.com', newPassword); expect(loginResult).toHaveProperty('token'); }); }); diff --git a/tests/unit/infrastructure.db.test.js b/tests/unit/infrastructure.db.test.js index 3b86f3b..c991c9a 100644 --- a/tests/unit/infrastructure.db.test.js +++ b/tests/unit/infrastructure.db.test.js @@ -226,7 +226,7 @@ describe('Infrastructure - MongoDB API Wrapper', () => { const mockProfile = { profileId: 1, userId: 1, categories: ['food'] }; mockModels.PreferenceProfile.findOneAndUpdate.mockResolvedValue(mockProfile); - const result = await mongoDb.updatePreferenceProfile({ userId: 1, profileId: 1, update: { categories: ['food'] } }); + const result = await mongoDb.updatePreferenceProfile(1, 1, { categories: ['food'] }); expect(mockModels.PreferenceProfile.findOneAndUpdate).toHaveBeenCalled(); expect(result).toEqual(mockProfile); diff --git a/tests/unit/models.Place.test.js b/tests/unit/models.Place.test.js index ffc2d11..a49f5e2 100644 --- a/tests/unit/models.Place.test.js +++ b/tests/unit/models.Place.test.js @@ -63,12 +63,12 @@ describe('Place Model', () => { }); test('findNearLocation returns places within distance', async () => { - const results = await Place.findNearLocation({ lat: 37.985, lng: 23.730, maxDistance: 5 }); + const results = await Place.findNearLocation(37.985, 23.730, 5); expect(results.length).toBeGreaterThanOrEqual(1); }); test('findNearLocation excludes distant places', async () => { - const results = await Place.findNearLocation({ lat: 37.985, lng: 23.730, maxDistance: 0.1 }); + const results = await Place.findNearLocation(37.985, 23.730, 0.1); expect(results.length).toBeLessThan(3); }); }); diff --git a/tests/unit/mongoDb.data.test.js b/tests/unit/mongoDb.data.test.js index 2d5906f..56f8eb1 100644 --- a/tests/unit/mongoDb.data.test.js +++ b/tests/unit/mongoDb.data.test.js @@ -116,21 +116,21 @@ describe('MongoDB Data Operations', () => { }); test('should add a favourite place', async () => { - const fav = await mongoDb.addFavouritePlace({ userId: testUser.userId, placeId: testPlace.placeId }); + const fav = await mongoDb.addFavouritePlace(testUser.userId, testPlace.placeId); expect(fav).toBeDefined(); expect(fav).toHaveProperty('favouriteId'); }); test('should prevent duplicate favourites', async () => { - await mongoDb.addFavouritePlace({ userId: testUser.userId, placeId: testPlace.placeId }); - const duplicate = await mongoDb.addFavouritePlace({ userId: testUser.userId, placeId: testPlace.placeId }); + await mongoDb.addFavouritePlace(testUser.userId, testPlace.placeId); + const duplicate = await mongoDb.addFavouritePlace(testUser.userId, testPlace.placeId); expect(duplicate).toBeNull(); }); test('should retrieve user favourite places with populated data', async () => { - await mongoDb.addFavouritePlace({ userId: testUser.userId, placeId: testPlace.placeId }); + await mongoDb.addFavouritePlace(testUser.userId, testPlace.placeId); const favourites = await mongoDb.getFavouritePlaces(testUser.userId);