From cbd214c4458f99a3ae927c6ed2622f8bf1d2f817 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 16 Apr 2025 22:02:35 +0200 Subject: [PATCH 01/29] test(frontend): Student controller Co-authored-by: Gabriellvl --- frontend/tests/controllers/student-controller.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 frontend/tests/controllers/student-controller.test.ts diff --git a/frontend/tests/controllers/student-controller.test.ts b/frontend/tests/controllers/student-controller.test.ts new file mode 100644 index 00000000..63516f9f --- /dev/null +++ b/frontend/tests/controllers/student-controller.test.ts @@ -0,0 +1,8 @@ +import { StudentController } from '../../src/controllers/students'; +import { expect, it } from 'vitest'; + +it('Get students', async () => { + const controller = new StudentController(); + const data = await controller.getAll(true); + expect(data.students).to.have.length.greaterThan(0); +}); From 37be3421370c771de4a919e6e842ad17e74734a9 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 16 Apr 2025 22:03:35 +0200 Subject: [PATCH 02/29] test(frontend): Start backend automatisch op --- frontend/tests/setup-backend.ts | 39 +++++++++++++++++++++++++++++++++ frontend/vitest.config.ts | 2 ++ 2 files changed, 41 insertions(+) create mode 100644 frontend/tests/setup-backend.ts diff --git a/frontend/tests/setup-backend.ts b/frontend/tests/setup-backend.ts new file mode 100644 index 00000000..6acf7aa4 --- /dev/null +++ b/frontend/tests/setup-backend.ts @@ -0,0 +1,39 @@ +import { spawn } from 'child_process'; +import { ChildProcess } from 'node:child_process'; + +let backendProcess: ChildProcess; + +export async function setup(): Promise { + // Spin up the database + spawn('docker', ['compose', 'up', 'db', '--detach'], { + cwd: '..', + stdio: "pipe", + }); + + backendProcess = spawn('npm', ['run', 'dev'], { + cwd: '../backend', + stdio: "pipe", + }); + + // Wait until you can curl the backend + let backendReady = false; + while (!backendReady) { + try { + await fetch('http://localhost:3000/api') + backendReady = true; + } catch (_) { + // Ignore the error + } + } +} + +export async function teardown(): Promise { + if (backendProcess) { + backendProcess.kill(); + } + + spawn('docker', ['compose', 'down'], { + cwd: '..', + stdio: "pipe" + }); +} diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index ba2d72b6..2a75f180 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -9,6 +9,8 @@ export default mergeConfig( environment: "jsdom", exclude: [...configDefaults.exclude, "e2e/**"], root: fileURLToPath(new URL("./", import.meta.url)), + // Startup the backend server, because it is needed for some tests + globalSetup: [ "./tests/setup-backend.ts" ] }, }), ); From 087215c4fd580d1376f904ab1cfc7a295729ede3 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 17 Apr 2025 11:57:58 +0200 Subject: [PATCH 03/29] refactor(frontend): waitForEndpoint ipv loop --- frontend/tests/setup-backend.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/frontend/tests/setup-backend.ts b/frontend/tests/setup-backend.ts index 6acf7aa4..9c5d5d05 100644 --- a/frontend/tests/setup-backend.ts +++ b/frontend/tests/setup-backend.ts @@ -3,6 +3,17 @@ import { ChildProcess } from 'node:child_process'; let backendProcess: ChildProcess; +async function waitForEndpoint(url: string, delay = 1000): Promise { + try { + await fetch(url); + } catch { + // Endpoint is not ready yet + await new Promise((resolve) => setTimeout(resolve, delay)); + // Retry + await waitForEndpoint(url, delay); + } +} + export async function setup(): Promise { // Spin up the database spawn('docker', ['compose', 'up', 'db', '--detach'], { @@ -16,15 +27,7 @@ export async function setup(): Promise { }); // Wait until you can curl the backend - let backendReady = false; - while (!backendReady) { - try { - await fetch('http://localhost:3000/api') - backendReady = true; - } catch (_) { - // Ignore the error - } - } + await waitForEndpoint('http://localhost:3000/api'); } export async function teardown(): Promise { From 8c338d8d56ac226a09b96bc294992bf316517402 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Thu, 17 Apr 2025 10:11:42 +0000 Subject: [PATCH 04/29] style: fix linting issues met Prettier --- .../controllers/student-controller.test.ts | 6 +++--- frontend/tests/setup-backend.ts | 20 +++++++++---------- frontend/vitest.config.ts | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/frontend/tests/controllers/student-controller.test.ts b/frontend/tests/controllers/student-controller.test.ts index 63516f9f..abd5985f 100644 --- a/frontend/tests/controllers/student-controller.test.ts +++ b/frontend/tests/controllers/student-controller.test.ts @@ -1,7 +1,7 @@ -import { StudentController } from '../../src/controllers/students'; -import { expect, it } from 'vitest'; +import { StudentController } from "../../src/controllers/students"; +import { expect, it } from "vitest"; -it('Get students', async () => { +it("Get students", async () => { const controller = new StudentController(); const data = await controller.getAll(true); expect(data.students).to.have.length.greaterThan(0); diff --git a/frontend/tests/setup-backend.ts b/frontend/tests/setup-backend.ts index 9c5d5d05..cd9f2e96 100644 --- a/frontend/tests/setup-backend.ts +++ b/frontend/tests/setup-backend.ts @@ -1,5 +1,5 @@ -import { spawn } from 'child_process'; -import { ChildProcess } from 'node:child_process'; +import { spawn } from "child_process"; +import { ChildProcess } from "node:child_process"; let backendProcess: ChildProcess; @@ -16,18 +16,18 @@ async function waitForEndpoint(url: string, delay = 1000): Promise { export async function setup(): Promise { // Spin up the database - spawn('docker', ['compose', 'up', 'db', '--detach'], { - cwd: '..', + spawn("docker", ["compose", "up", "db", "--detach"], { + cwd: "..", stdio: "pipe", }); - backendProcess = spawn('npm', ['run', 'dev'], { - cwd: '../backend', + backendProcess = spawn("npm", ["run", "dev"], { + cwd: "../backend", stdio: "pipe", }); // Wait until you can curl the backend - await waitForEndpoint('http://localhost:3000/api'); + await waitForEndpoint("http://localhost:3000/api"); } export async function teardown(): Promise { @@ -35,8 +35,8 @@ export async function teardown(): Promise { backendProcess.kill(); } - spawn('docker', ['compose', 'down'], { - cwd: '..', - stdio: "pipe" + spawn("docker", ["compose", "down"], { + cwd: "..", + stdio: "pipe", }); } diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index 2a75f180..2269cd64 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -10,7 +10,7 @@ export default mergeConfig( exclude: [...configDefaults.exclude, "e2e/**"], root: fileURLToPath(new URL("./", import.meta.url)), // Startup the backend server, because it is needed for some tests - globalSetup: [ "./tests/setup-backend.ts" ] + globalSetup: ["./tests/setup-backend.ts"], }, }), ); From 93624cdc6a1d037dd9fdc3c6ff0e8cbc35efa459 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 17 Apr 2025 12:09:23 +0200 Subject: [PATCH 05/29] chore(frontend): Max retries toevoegen Frontend unittesten hoogstens 60 retries van 1 seconde --- frontend/tests/setup-backend.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/tests/setup-backend.ts b/frontend/tests/setup-backend.ts index cd9f2e96..2b7e621d 100644 --- a/frontend/tests/setup-backend.ts +++ b/frontend/tests/setup-backend.ts @@ -3,14 +3,14 @@ import { ChildProcess } from "node:child_process"; let backendProcess: ChildProcess; -async function waitForEndpoint(url: string, delay = 1000): Promise { +async function waitForEndpoint(url: string, delay = 1000, retries = 60): Promise { try { await fetch(url); } catch { // Endpoint is not ready yet await new Promise((resolve) => setTimeout(resolve, delay)); // Retry - await waitForEndpoint(url, delay); + await waitForEndpoint(url, delay, retries - 1); } } From 5b02cf36c1e898b79b2f4dc82fa6bf587d8ee7c1 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 17 Apr 2025 12:14:21 +0200 Subject: [PATCH 06/29] chore(frontend): Verbose output --- frontend/tests/setup-backend.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/tests/setup-backend.ts b/frontend/tests/setup-backend.ts index 2b7e621d..c40f88c6 100644 --- a/frontend/tests/setup-backend.ts +++ b/frontend/tests/setup-backend.ts @@ -18,12 +18,12 @@ export async function setup(): Promise { // Spin up the database spawn("docker", ["compose", "up", "db", "--detach"], { cwd: "..", - stdio: "pipe", + stdio: "inherit", }); backendProcess = spawn("npm", ["run", "dev"], { cwd: "../backend", - stdio: "pipe", + stdio: "inherit", }); // Wait until you can curl the backend @@ -37,6 +37,6 @@ export async function teardown(): Promise { spawn("docker", ["compose", "down"], { cwd: "..", - stdio: "pipe", + stdio: "inherit" }); } From b9773c2c339f04156cf35c2e7df4718285906e83 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Thu, 17 Apr 2025 10:17:15 +0000 Subject: [PATCH 07/29] style: fix linting issues met Prettier --- frontend/tests/setup-backend.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/tests/setup-backend.ts b/frontend/tests/setup-backend.ts index c40f88c6..36671088 100644 --- a/frontend/tests/setup-backend.ts +++ b/frontend/tests/setup-backend.ts @@ -37,6 +37,6 @@ export async function teardown(): Promise { spawn("docker", ["compose", "down"], { cwd: "..", - stdio: "inherit" + stdio: "inherit", }); } From eef48a9987be97a22f1400bad39b01ed35d988ba Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 17 Apr 2025 12:36:48 +0200 Subject: [PATCH 08/29] chore(backend): Precompile dependencies --- backend/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index 40d49982..3edb386b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,7 +7,8 @@ "main": "dist/app.js", "scripts": { "build": "cross-env NODE_ENV=production tsc --build", - "dev": "cross-env NODE_ENV=development tsx tool/seed.ts; tsx watch --env-file=.env.development.local src/app.ts", + "predev": "pushd .. && npm run build && popd", + "dev": "cross-env NODE_ENV=development tsx tool/seed.ts && tsx watch --env-file=.env.development.local src/app.ts", "start": "cross-env NODE_ENV=production node --env-file=.env dist/app.js", "format": "prettier --write src/", "format-check": "prettier --check src/", From dd7841e8192f4002d0bbf86a3c49be9b73763ea4 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 17 Apr 2025 12:39:58 +0200 Subject: [PATCH 09/29] fixup! chore(backend): Precompile dependencies --- backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index 3edb386b..b31dbd64 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,7 +7,7 @@ "main": "dist/app.js", "scripts": { "build": "cross-env NODE_ENV=production tsc --build", - "predev": "pushd .. && npm run build && popd", + "predev": "cross-env pushd .. && npm run build && popd", "dev": "cross-env NODE_ENV=development tsx tool/seed.ts && tsx watch --env-file=.env.development.local src/app.ts", "start": "cross-env NODE_ENV=production node --env-file=.env dist/app.js", "format": "prettier --write src/", From 5381f81f5a897e26451b0d3e64975e3b69d65f50 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 17 Apr 2025 12:42:38 +0200 Subject: [PATCH 10/29] fixup! chore(backend): Precompile dependencies --- backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index b31dbd64..07ef3a74 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,7 +7,7 @@ "main": "dist/app.js", "scripts": { "build": "cross-env NODE_ENV=production tsc --build", - "predev": "cross-env pushd .. && npm run build && popd", + "predev": "cross-env cd .. && npm run build && cd backend", "dev": "cross-env NODE_ENV=development tsx tool/seed.ts && tsx watch --env-file=.env.development.local src/app.ts", "start": "cross-env NODE_ENV=production node --env-file=.env dist/app.js", "format": "prettier --write src/", From b90c326b5a76682c605feac378daceec01407451 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 20 Apr 2025 17:33:47 +0200 Subject: [PATCH 11/29] fix(backend): Error logging --- backend/src/logging/initalize.ts | 6 ++++-- backend/src/logging/mikroOrmLogger.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/src/logging/initalize.ts b/backend/src/logging/initalize.ts index 5c94a25f..f261d308 100644 --- a/backend/src/logging/initalize.ts +++ b/backend/src/logging/initalize.ts @@ -26,13 +26,15 @@ function initializeLogger(): Logger { const consoleTransport = new transports.Console({ level: getEnvVar(envVars.LogLevel), - format: format.combine(format.cli(), format.colorize()), + format: format.combine(format.cli(), format.simple()) }); if (getEnvVar(envVars.RunMode) === 'dev') { - return createLogger({ + logger = createLogger({ transports: [consoleTransport], }); + logger.debug(`Logger initialized with level ${logLevel} to console`); + return logger; } const lokiHost = getEnvVar(envVars.LokiHost); diff --git a/backend/src/logging/mikroOrmLogger.ts b/backend/src/logging/mikroOrmLogger.ts index 9cb797a8..0fc18b87 100644 --- a/backend/src/logging/mikroOrmLogger.ts +++ b/backend/src/logging/mikroOrmLogger.ts @@ -11,7 +11,7 @@ export class MikroOrmLogger extends DefaultLogger { }; let message: string; - if (context?.label) { + if (context !== undefined && context.labels !== undefined) { message = `[${namespace}] (${context.label}) ${messageArg}`; } else { message = `[${namespace}] ${messageArg}`; From e4945fac66c89c4b6c9b7cb27bbaa77a709a7a15 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 20 Apr 2025 18:29:41 +0200 Subject: [PATCH 12/29] fix(frontend): Controller tests slagen --- backend/package.json | 2 +- frontend/tests/setup-backend.ts | 25 +++++++++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/backend/package.json b/backend/package.json index 07ef3a74..377f58f3 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,7 +7,7 @@ "main": "dist/app.js", "scripts": { "build": "cross-env NODE_ENV=production tsc --build", - "predev": "cross-env cd .. && npm run build && cd backend", + "predev": "tsc --build ../common/tsconfig.json", "dev": "cross-env NODE_ENV=development tsx tool/seed.ts && tsx watch --env-file=.env.development.local src/app.ts", "start": "cross-env NODE_ENV=production node --env-file=.env dist/app.js", "format": "prettier --write src/", diff --git a/frontend/tests/setup-backend.ts b/frontend/tests/setup-backend.ts index 36671088..722accb5 100644 --- a/frontend/tests/setup-backend.ts +++ b/frontend/tests/setup-backend.ts @@ -1,6 +1,7 @@ import { spawn } from "child_process"; -import { ChildProcess } from "node:child_process"; +import { ChildProcess, execSync } from 'node:child_process'; +let wasRunningBefore: boolean; let backendProcess: ChildProcess; async function waitForEndpoint(url: string, delay = 1000, retries = 60): Promise { @@ -15,12 +16,14 @@ async function waitForEndpoint(url: string, delay = 1000, retries = 60): Promise } export async function setup(): Promise { - // Spin up the database - spawn("docker", ["compose", "up", "db", "--detach"], { - cwd: "..", - stdio: "inherit", - }); + // Check if the database container is already running + const containerCheck = execSync("docker ps --filter 'name=db' --format '{{.Names}}'"); + wasRunningBefore = !(containerCheck.toString().includes("db")); + // Spin up the database + execSync("docker compose up db --detach"); + + // Spin up the backend backendProcess = spawn("npm", ["run", "dev"], { cwd: "../backend", stdio: "inherit", @@ -35,8 +38,10 @@ export async function teardown(): Promise { backendProcess.kill(); } - spawn("docker", ["compose", "down"], { - cwd: "..", - stdio: "inherit", - }); + if (wasRunningBefore) { + spawn("docker", ["compose", "down"], { + cwd: "..", + stdio: "inherit", + }); + } } From 04ac71dbeda8ff41d78d1d2fa4cf230af033c2e5 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 20 Apr 2025 20:27:04 +0200 Subject: [PATCH 13/29] test(frontend): Backend in memory --- backend/.env.test | 2 ++ backend/tool/seed.ts | 8 ++++---- backend/tool/startTestApp.ts | 33 +++++++++++++++++++++++++++++++++ frontend/tests/setup-backend.ts | 33 ++++++++++++++------------------- 4 files changed, 53 insertions(+), 23 deletions(-) create mode 100644 backend/tool/startTestApp.ts diff --git a/backend/.env.test b/backend/.env.test index 535628cd..9020e581 100644 --- a/backend/.env.test +++ b/backend/.env.test @@ -11,3 +11,5 @@ DWENGO_PORT=3000 DWENGO_DB_NAME=":memory:" DWENGO_DB_UPDATE=true + +DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000 diff --git a/backend/tool/seed.ts b/backend/tool/seed.ts index 3ded9379..c781cdf4 100644 --- a/backend/tool/seed.ts +++ b/backend/tool/seed.ts @@ -15,13 +15,13 @@ import { makeTestStudents } from '../tests/test_assets/users/students.testdata.j import { makeTestTeachers } from '../tests/test_assets/users/teachers.testdata.js'; import { getLogger, Logger } from '../src/logging/initalize.js'; import { Collection } from '@mikro-orm/core'; -import { Group } from '../dist/entities/assignments/group.entity.js'; +import { Group } from '../src/entities/assignments/group.entity.js'; const logger: Logger = getLogger(); -export async function seedDatabase(): Promise { - dotenv.config({ path: '.env.development.local' }); - const orm = await initORM(); +export async function seedDatabase(envFile = '.env.development.local', testMode = false): Promise { + dotenv.config({ path: envFile }); + const orm = await initORM(testMode); await orm.schema.clearDatabase(); const em = forkEntityManager(); diff --git a/backend/tool/startTestApp.ts b/backend/tool/startTestApp.ts new file mode 100644 index 00000000..6dab054d --- /dev/null +++ b/backend/tool/startTestApp.ts @@ -0,0 +1,33 @@ +import express, { Express } from 'express'; +import { initORM } from '../src/orm.js'; +import apiRouter from '../src/routes/router.js'; +import { errorHandler } from '../src/middleware/error-handling/error-handler.js'; +import dotenv from 'dotenv'; +import cors from '../src/middleware/cors'; +import { authenticateUser } from '../src/middleware/auth/auth'; +import { seedDatabase } from './seed'; + +const envFile = '../.env.test'; +console.log(`Using env file: ${envFile}`); + +dotenv.config({ path: envFile }); + +const app: Express = express(); + +app.use(express.json()); +app.use(cors); +app.use(authenticateUser); + +app.use('/api', apiRouter); +app.use(errorHandler); + +async function startServer(): Promise { + await seedDatabase(envFile, true); + await initORM(true); + + app.listen(9876, () => { + console.log('Server is running on http://localhost:9876/api'); + }); +} + +await startServer(); diff --git a/frontend/tests/setup-backend.ts b/frontend/tests/setup-backend.ts index 722accb5..3b7aee24 100644 --- a/frontend/tests/setup-backend.ts +++ b/frontend/tests/setup-backend.ts @@ -1,7 +1,6 @@ import { spawn } from "child_process"; -import { ChildProcess, execSync } from 'node:child_process'; +import { ChildProcess, spawnSync } from 'node:child_process'; -let wasRunningBefore: boolean; let backendProcess: ChildProcess; async function waitForEndpoint(url: string, delay = 1000, retries = 60): Promise { @@ -16,32 +15,28 @@ async function waitForEndpoint(url: string, delay = 1000, retries = 60): Promise } export async function setup(): Promise { - // Check if the database container is already running - const containerCheck = execSync("docker ps --filter 'name=db' --format '{{.Names}}'"); - wasRunningBefore = !(containerCheck.toString().includes("db")); - - // Spin up the database - execSync("docker compose up db --detach"); - - // Spin up the backend - backendProcess = spawn("npm", ["run", "dev"], { + // Precompile needed packages + spawnSync("npm", ["run", "predev"], { cwd: "../backend", stdio: "inherit", }); + // Spin up the backend + backendProcess = spawn("tsx", ["--env-file=.env.development.example", "tool/startTestApp.ts"], { + cwd: "../backend", + stdio: "inherit", + env: { + ...process.env, + NODE_ENV: 'test', + } + }); + // Wait until you can curl the backend - await waitForEndpoint("http://localhost:3000/api"); + await waitForEndpoint("http://localhost:9876/api"); } export async function teardown(): Promise { if (backendProcess) { backendProcess.kill(); } - - if (wasRunningBefore) { - spawn("docker", ["compose", "down"], { - cwd: "..", - stdio: "inherit", - }); - } } From 2d5988552fb91352026b43e6e76330ba6a3b55b7 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 20 Apr 2025 20:29:34 +0200 Subject: [PATCH 14/29] chore(frontend): Configureerbare API URL --- frontend/src/config.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 53d6f253..60a63db4 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -1,8 +1,16 @@ export const apiConfig = { - baseUrl: - window.location.hostname === "localhost" && !(window.location.port === "80" || window.location.port === "") - ? "http://localhost:3000/api" - : window.location.origin + "/api", + baseUrl: ((): string => { + if (import.meta.env.VITE_API_BASE_URL) { + return import.meta.env.VITE_API_BASE_URL; + } + + if (window.location.hostname === "localhost" && !(window.location.port === "80" || window.location.port === "")) { + return "http://localhost:3000/api"; + } + + // Fallback to the current origin with "/api" suffix + return `${window.location.origin}/api`; + })(), }; export const loginRoute = "/login"; From b24f57797526595b81cac85b68e7bb81589e648c Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 20 Apr 2025 20:48:06 +0200 Subject: [PATCH 15/29] chore: Cleanup --- backend/.env.test | 5 +++++ backend/tool/seed.ts | 13 +++++++++---- backend/tool/startTestApp.ts | 10 +++------- frontend/package.json | 2 +- frontend/src/controllers/base-controller.ts | 14 +++++++++++--- frontend/tests/setup-backend.ts | 2 +- 6 files changed, 30 insertions(+), 16 deletions(-) diff --git a/backend/.env.test b/backend/.env.test index 9020e581..4444ec29 100644 --- a/backend/.env.test +++ b/backend/.env.test @@ -12,4 +12,9 @@ DWENGO_PORT=3000 DWENGO_DB_NAME=":memory:" DWENGO_DB_UPDATE=true +DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student +DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs +DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher +DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs + DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000 diff --git a/backend/tool/seed.ts b/backend/tool/seed.ts index c781cdf4..2ef42850 100644 --- a/backend/tool/seed.ts +++ b/backend/tool/seed.ts @@ -14,14 +14,12 @@ import { makeTestQuestions } from '../tests/test_assets/questions/questions.test import { makeTestStudents } from '../tests/test_assets/users/students.testdata.js'; import { makeTestTeachers } from '../tests/test_assets/users/teachers.testdata.js'; import { getLogger, Logger } from '../src/logging/initalize.js'; -import { Collection } from '@mikro-orm/core'; +import { Collection, MikroORM } from '@mikro-orm/core'; import { Group } from '../src/entities/assignments/group.entity.js'; const logger: Logger = getLogger(); -export async function seedDatabase(envFile = '.env.development.local', testMode = false): Promise { - dotenv.config({ path: envFile }); - const orm = await initORM(testMode); +export async function seedORM(orm: MikroORM): Promise { await orm.schema.clearDatabase(); const em = forkEntityManager(); @@ -67,6 +65,13 @@ export async function seedDatabase(envFile = '.env.development.local', testMode ]); logger.info('Development database seeded successfully!'); +} + +export async function seedDatabase(envFile = '.env.development.local', testMode = false): Promise { + dotenv.config({ path: envFile }); + const orm = await initORM(testMode); + + await seedORM(orm); await orm.close(); } diff --git a/backend/tool/startTestApp.ts b/backend/tool/startTestApp.ts index 6dab054d..a5d0f852 100644 --- a/backend/tool/startTestApp.ts +++ b/backend/tool/startTestApp.ts @@ -5,10 +5,9 @@ import { errorHandler } from '../src/middleware/error-handling/error-handler.js' import dotenv from 'dotenv'; import cors from '../src/middleware/cors'; import { authenticateUser } from '../src/middleware/auth/auth'; -import { seedDatabase } from './seed'; +import { seedORM } from './seed'; const envFile = '../.env.test'; -console.log(`Using env file: ${envFile}`); dotenv.config({ path: envFile }); @@ -22,12 +21,9 @@ app.use('/api', apiRouter); app.use(errorHandler); async function startServer(): Promise { - await seedDatabase(envFile, true); - await initORM(true); + await seedORM(await initORM(true)); - app.listen(9876, () => { - console.log('Server is running on http://localhost:9876/api'); - }); + app.listen(9876); } await startServer(); diff --git a/frontend/package.json b/frontend/package.json index b6bd5deb..cb29c772 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,7 @@ "format": "prettier --write src/", "format-check": "prettier --check src/", "lint": "eslint . --fix", - "test:unit": "vitest --run", + "test:unit": "VITE_API_BASE_URL='http://localhost:9876/api' vitest --run", "test:e2e": "playwright test" }, "dependencies": { diff --git a/frontend/src/controllers/base-controller.ts b/frontend/src/controllers/base-controller.ts index 3d22e099..32561753 100644 --- a/frontend/src/controllers/base-controller.ts +++ b/frontend/src/controllers/base-controller.ts @@ -1,6 +1,7 @@ import apiClient from "@/services/api-client/api-client.ts"; import type { AxiosResponse, ResponseType } from "axios"; import { HttpErrorResponseException } from "@/exception/http-error-response-exception.ts"; +import { apiConfig } from '@/config.ts'; export abstract class BaseController { protected basePath: string; @@ -16,9 +17,16 @@ export abstract class BaseController { } protected async get(path: string, queryParams?: QueryParams, responseType?: ResponseType): Promise { - const response = await apiClient.get(this.absolutePathFor(path), { params: queryParams, responseType }); - BaseController.assertSuccessResponse(response); - return response.data; + try { + const response = await apiClient.get(this.absolutePathFor(path), { params: queryParams, responseType }); + BaseController.assertSuccessResponse(response); + return response.data; + } catch (error) { + if (error instanceof HttpErrorResponseException) { + throw error; + } + throw new Error(`An unexpected error occurred while fetching data from ${apiConfig.baseUrl}${this.absolutePathFor(path)}: ${error}`); + } } protected async post(path: string, body: unknown, queryParams?: QueryParams): Promise { diff --git a/frontend/tests/setup-backend.ts b/frontend/tests/setup-backend.ts index 3b7aee24..4eedfda1 100644 --- a/frontend/tests/setup-backend.ts +++ b/frontend/tests/setup-backend.ts @@ -22,7 +22,7 @@ export async function setup(): Promise { }); // Spin up the backend - backendProcess = spawn("tsx", ["--env-file=.env.development.example", "tool/startTestApp.ts"], { + backendProcess = spawn("tsx", ["--env-file=.env.test", "tool/startTestApp.ts"], { cwd: "../backend", stdio: "inherit", env: { From 6017d9c54f95c2bdb16cfbbef7cfe3d8d541365d Mon Sep 17 00:00:00 2001 From: Lint Action Date: Sun, 20 Apr 2025 18:54:34 +0000 Subject: [PATCH 16/29] style: fix linting issues met Prettier --- backend/src/logging/initalize.ts | 2 +- frontend/src/config.ts | 5 ++++- frontend/src/controllers/base-controller.ts | 6 ++++-- frontend/tests/setup-backend.ts | 6 +++--- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/backend/src/logging/initalize.ts b/backend/src/logging/initalize.ts index f261d308..f89518c4 100644 --- a/backend/src/logging/initalize.ts +++ b/backend/src/logging/initalize.ts @@ -26,7 +26,7 @@ function initializeLogger(): Logger { const consoleTransport = new transports.Console({ level: getEnvVar(envVars.LogLevel), - format: format.combine(format.cli(), format.simple()) + format: format.combine(format.cli(), format.simple()), }); if (getEnvVar(envVars.RunMode) === 'dev') { diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 60a63db4..76e7545c 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -4,7 +4,10 @@ export const apiConfig = { return import.meta.env.VITE_API_BASE_URL; } - if (window.location.hostname === "localhost" && !(window.location.port === "80" || window.location.port === "")) { + if ( + window.location.hostname === "localhost" && + !(window.location.port === "80" || window.location.port === "") + ) { return "http://localhost:3000/api"; } diff --git a/frontend/src/controllers/base-controller.ts b/frontend/src/controllers/base-controller.ts index 32561753..64f2363d 100644 --- a/frontend/src/controllers/base-controller.ts +++ b/frontend/src/controllers/base-controller.ts @@ -1,7 +1,7 @@ import apiClient from "@/services/api-client/api-client.ts"; import type { AxiosResponse, ResponseType } from "axios"; import { HttpErrorResponseException } from "@/exception/http-error-response-exception.ts"; -import { apiConfig } from '@/config.ts'; +import { apiConfig } from "@/config.ts"; export abstract class BaseController { protected basePath: string; @@ -25,7 +25,9 @@ export abstract class BaseController { if (error instanceof HttpErrorResponseException) { throw error; } - throw new Error(`An unexpected error occurred while fetching data from ${apiConfig.baseUrl}${this.absolutePathFor(path)}: ${error}`); + throw new Error( + `An unexpected error occurred while fetching data from ${apiConfig.baseUrl}${this.absolutePathFor(path)}: ${error}`, + ); } } diff --git a/frontend/tests/setup-backend.ts b/frontend/tests/setup-backend.ts index 4eedfda1..71cc1480 100644 --- a/frontend/tests/setup-backend.ts +++ b/frontend/tests/setup-backend.ts @@ -1,5 +1,5 @@ import { spawn } from "child_process"; -import { ChildProcess, spawnSync } from 'node:child_process'; +import { ChildProcess, spawnSync } from "node:child_process"; let backendProcess: ChildProcess; @@ -27,8 +27,8 @@ export async function setup(): Promise { stdio: "inherit", env: { ...process.env, - NODE_ENV: 'test', - } + NODE_ENV: "test", + }, }); // Wait until you can curl the backend From 196875538d2f022758452a7e8ad685712fb25639 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Mon, 21 Apr 2025 12:34:46 +0200 Subject: [PATCH 17/29] fix(frontend): Selectieve testen in IDE --- frontend/package.json | 3 +-- frontend/src/config.ts | 5 +++++ frontend/tests/setup-backend.ts | 8 ++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 52997cb9..b6bd5deb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,8 +12,7 @@ "format": "prettier --write src/", "format-check": "prettier --check src/", "lint": "eslint . --fix", - "pretest:unit": "tsx ../docs/api/generate.ts && npm run build", - "test:unit": "VITE_API_BASE_URL='http://localhost:9876/api' vitest --run", + "test:unit": "vitest --run", "test:e2e": "playwright test" }, "dependencies": { diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 76e7545c..cff1d771 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -1,5 +1,10 @@ export const apiConfig = { baseUrl: ((): string => { + if (import.meta.env.MODE === "test") { + // TODO Remove hardcoding + return "http://localhost:9876/api"; + } + if (import.meta.env.VITE_API_BASE_URL) { return import.meta.env.VITE_API_BASE_URL; } diff --git a/frontend/tests/setup-backend.ts b/frontend/tests/setup-backend.ts index 71cc1480..d093fd57 100644 --- a/frontend/tests/setup-backend.ts +++ b/frontend/tests/setup-backend.ts @@ -16,14 +16,14 @@ async function waitForEndpoint(url: string, delay = 1000, retries = 60): Promise export async function setup(): Promise { // Precompile needed packages - spawnSync("npm", ["run", "predev"], { - cwd: "../backend", + spawnSync("npx", ["tsc", "--build", "tsconfig.json"], { + cwd: `../common`, stdio: "inherit", }); // Spin up the backend - backendProcess = spawn("tsx", ["--env-file=.env.test", "tool/startTestApp.ts"], { - cwd: "../backend", + backendProcess = spawn("npx", ["tsx", "--env-file=.env.test", "tool/startTestApp.ts"], { + cwd: `../backend`, stdio: "inherit", env: { ...process.env, From e59dead5a9e630939864fa6ac3abc98bc44643b7 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Mon, 21 Apr 2025 12:44:39 +0200 Subject: [PATCH 18/29] refactor(frontend): Start test-app voor individuele testfiles --- .../controllers/student-controller.test.ts | 21 ++++++++++++++----- frontend/vitest.config.ts | 9 ++++++-- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/frontend/tests/controllers/student-controller.test.ts b/frontend/tests/controllers/student-controller.test.ts index abd5985f..578c832e 100644 --- a/frontend/tests/controllers/student-controller.test.ts +++ b/frontend/tests/controllers/student-controller.test.ts @@ -1,8 +1,19 @@ import { StudentController } from "../../src/controllers/students"; -import { expect, it } from "vitest"; +import { expect, it, describe, afterAll, beforeAll } from 'vitest'; +import { setup, teardown } from '../setup-backend.js'; -it("Get students", async () => { - const controller = new StudentController(); - const data = await controller.getAll(true); - expect(data.students).to.have.length.greaterThan(0); +describe('Test controller students', () => { + beforeAll(async () => { + await setup(); + }); + + afterAll(async () => { + await teardown(); + }); + + it("Get students", async () => { + const controller = new StudentController(); + const data = await controller.getAll(true); + expect(data.students).to.have.length.greaterThan(0); + }); }); diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index 2269cd64..51fc91ab 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -9,8 +9,13 @@ export default mergeConfig( environment: "jsdom", exclude: [...configDefaults.exclude, "e2e/**"], root: fileURLToPath(new URL("./", import.meta.url)), - // Startup the backend server, because it is needed for some tests - globalSetup: ["./tests/setup-backend.ts"], + + /* + * The test-backend server can be started for each test-file individually using `beforeAll(() => setup())`, + * or for all tests once using: + globalSetup: ["./tests/setup-backend.ts"], + * In this project, the backend server is started for each test-file individually. + */ }, }), ); From 6edb86507cc122bb7a35de7e7e8c142bd75f61ff Mon Sep 17 00:00:00 2001 From: Lint Action Date: Mon, 21 Apr 2025 10:47:51 +0000 Subject: [PATCH 19/29] style: fix linting issues met Prettier --- frontend/tests/controllers/student-controller.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/tests/controllers/student-controller.test.ts b/frontend/tests/controllers/student-controller.test.ts index 578c832e..89c8224e 100644 --- a/frontend/tests/controllers/student-controller.test.ts +++ b/frontend/tests/controllers/student-controller.test.ts @@ -1,8 +1,8 @@ import { StudentController } from "../../src/controllers/students"; -import { expect, it, describe, afterAll, beforeAll } from 'vitest'; -import { setup, teardown } from '../setup-backend.js'; +import { expect, it, describe, afterAll, beforeAll } from "vitest"; +import { setup, teardown } from "../setup-backend.js"; -describe('Test controller students', () => { +describe("Test controller students", () => { beforeAll(async () => { await setup(); }); From 57b74163beddf025a1364cfa4337858cd8a72f2f Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Mon, 21 Apr 2025 12:49:42 +0200 Subject: [PATCH 20/29] chore(frontend): Verwijder test-app stdio --- frontend/tests/setup-backend.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/tests/setup-backend.ts b/frontend/tests/setup-backend.ts index d093fd57..6a0a256d 100644 --- a/frontend/tests/setup-backend.ts +++ b/frontend/tests/setup-backend.ts @@ -18,13 +18,11 @@ export async function setup(): Promise { // Precompile needed packages spawnSync("npx", ["tsc", "--build", "tsconfig.json"], { cwd: `../common`, - stdio: "inherit", }); // Spin up the backend backendProcess = spawn("npx", ["tsx", "--env-file=.env.test", "tool/startTestApp.ts"], { cwd: `../backend`, - stdio: "inherit", env: { ...process.env, NODE_ENV: "test", From f9a5028b240d571e6cabe8effa7e25b9918373f6 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Mon, 21 Apr 2025 13:03:59 +0200 Subject: [PATCH 21/29] fixup! fix(backend): Error logging --- backend/tool/seed.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/tool/seed.ts b/backend/tool/seed.ts index 30e305b1..00e3c0bf 100644 --- a/backend/tool/seed.ts +++ b/backend/tool/seed.ts @@ -77,4 +77,6 @@ export async function seedDatabase(envFile = '.env.development.local', testMode await orm.close(); } -seedDatabase().catch(logger.error); +seedDatabase().catch((err) => { + logger.error(err); +}); From 2578555ace1204c8c6e2ea1b69055040f7b6c9f2 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 22 Apr 2025 11:04:24 +0200 Subject: [PATCH 22/29] fix(frontend): Login state blijft behouden voor andere tabs binnen dezelfde browser. --- frontend/src/App.vue | 3 +++ frontend/src/main.ts | 2 -- frontend/src/router/index.ts | 2 +- frontend/src/services/auth/auth-service.ts | 14 +++++++++++++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index b1207448..dbb62e79 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -3,6 +3,9 @@ import MenuBar from "@/components/MenuBar.vue"; import { useRoute } from "vue-router"; import { computed } from "vue"; + import authService from "@/services/auth/auth-service.ts"; + + void authService.loadUser(); const route = useRoute(); interface RouteMeta { diff --git a/frontend/src/main.ts b/frontend/src/main.ts index e4bdde40..b5315634 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -12,11 +12,9 @@ import App from "./App.vue"; import router from "./router"; import { aliases, mdi } from "vuetify/iconsets/mdi"; import { VueQueryPlugin, QueryClient } from "@tanstack/vue-query"; -import authService from "./services/auth/auth-service.ts"; const app = createApp(App); -await authService.loadUser(); app.use(router); const link = document.createElement("link"); diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 1eeb8865..901d376f 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -138,7 +138,7 @@ const router = createRouter({ router.beforeEach(async (to, _from, next) => { // Verify if user is logged in before accessing certain routes if (to.meta.requiresAuth) { - if (!authService.isLoggedIn.value) { + if (!authService.isLoggedIn.value && !await authService.loadUser()) { next("/login"); } else { next(); diff --git a/frontend/src/services/auth/auth-service.ts b/frontend/src/services/auth/auth-service.ts index 9151e0a9..92b5002d 100644 --- a/frontend/src/services/auth/auth-service.ts +++ b/frontend/src/services/auth/auth-service.ts @@ -37,7 +37,19 @@ async function loadUser(): Promise { if (!activeRole) { return null; } - const user = await (await getUserManagers())[activeRole].getUser(); + + const userManager = (await getUserManagers())[activeRole]; + let user = await userManager.getUser(); // Load the user from the local storage. + if (!user) { // If the user is not in the local storage, he could still be authenticated in Keycloak. + try { + user = await userManager.signinSilent() + } catch (e: unknown) { + // When the user was previously logged in and then logged out, signinSilent throws an error. + // In that case, the user is not authenticated anymore, so we set him to null. + user = null; + } + } + setUserAuthInfo(user); authState.activeRole = activeRole ?? null; return user; From 868c5d9afe867daf897780a98c7e735ca3bb5649 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Tue, 22 Apr 2025 09:12:11 +0000 Subject: [PATCH 23/29] style: fix linting issues met Prettier --- frontend/src/router/index.ts | 2 +- frontend/src/services/auth/auth-service.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 901d376f..8092ffd5 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -138,7 +138,7 @@ const router = createRouter({ router.beforeEach(async (to, _from, next) => { // Verify if user is logged in before accessing certain routes if (to.meta.requiresAuth) { - if (!authService.isLoggedIn.value && !await authService.loadUser()) { + if (!authService.isLoggedIn.value && !(await authService.loadUser())) { next("/login"); } else { next(); diff --git a/frontend/src/services/auth/auth-service.ts b/frontend/src/services/auth/auth-service.ts index 92b5002d..0d67380c 100644 --- a/frontend/src/services/auth/auth-service.ts +++ b/frontend/src/services/auth/auth-service.ts @@ -40,9 +40,10 @@ async function loadUser(): Promise { const userManager = (await getUserManagers())[activeRole]; let user = await userManager.getUser(); // Load the user from the local storage. - if (!user) { // If the user is not in the local storage, he could still be authenticated in Keycloak. + if (!user) { + // If the user is not in the local storage, he could still be authenticated in Keycloak. try { - user = await userManager.signinSilent() + user = await userManager.signinSilent(); } catch (e: unknown) { // When the user was previously logged in and then logged out, signinSilent throws an error. // In that case, the user is not authenticated anymore, so we set him to null. From 97b07ff2e56b3cd4a6aa6c827e853597916fd708 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 22 Apr 2025 11:16:33 +0200 Subject: [PATCH 24/29] fix(frontend): linting probleem --- frontend/src/services/auth/auth-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/services/auth/auth-service.ts b/frontend/src/services/auth/auth-service.ts index 0d67380c..a813cd6e 100644 --- a/frontend/src/services/auth/auth-service.ts +++ b/frontend/src/services/auth/auth-service.ts @@ -44,7 +44,7 @@ async function loadUser(): Promise { // If the user is not in the local storage, he could still be authenticated in Keycloak. try { user = await userManager.signinSilent(); - } catch (e: unknown) { + } catch (_: unknown) { // When the user was previously logged in and then logged out, signinSilent throws an error. // In that case, the user is not authenticated anymore, so we set him to null. user = null; From f1e9e3a8d64fa78d9e92d5770bd9aefef0a3dbfd Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Tue, 22 Apr 2025 14:04:39 +0200 Subject: [PATCH 25/29] fix: verbod op toevoegen van student aan groep wanneer die student niet in de klas zit --- backend/src/services/groups.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/src/services/groups.ts b/backend/src/services/groups.ts index 0c73c8c5..1a957772 100644 --- a/backend/src/services/groups.ts +++ b/backend/src/services/groups.ts @@ -9,6 +9,8 @@ import { fetchAssignment } from './assignments.js'; import { NotFoundException } from '../exceptions/not-found-exception.js'; import { putObject } from './service-helper.js'; import { fetchStudents } from './students.js'; +import { fetchClass } from './classes.js'; +import { BadRequestException } from '../exceptions/bad-request-exception.js'; export async function fetchGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise { const assignment = await fetchAssignment(classId, assignmentNumber); @@ -60,9 +62,15 @@ export async function getExistingGroupFromGroupDTO(groupData: GroupDTO): Promise } export async function createGroup(groupData: GroupDTO, classid: string, assignmentNumber: number): Promise { + const cls = await fetchClass(classid); + const memberUsernames = (groupData.members as string[]) || []; const members = await fetchStudents(memberUsernames); + if (!members.every(student => cls.students.contains(student))) { + throw new BadRequestException("It is not allowed to add a student to a group when the student is not part of the class"); + } + const assignment = await fetchAssignment(classid, assignmentNumber); const groupRepository = getGroupRepository(); From 6fe20dc2fed920a81eebf85e0b7f7e4575457d35 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Tue, 22 Apr 2025 14:21:25 +0200 Subject: [PATCH 26/29] fix: verboden om PUT request naar group te sturen met studenten die niet tot klas behoren --- backend/src/controllers/groups.ts | 6 +++++- backend/src/services/groups.ts | 27 ++++++++++++++++++++------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/backend/src/controllers/groups.ts b/backend/src/controllers/groups.ts index 53bc96ec..2f755e21 100644 --- a/backend/src/controllers/groups.ts +++ b/backend/src/controllers/groups.ts @@ -35,7 +35,11 @@ export async function putGroupHandler(req: Request, res: Response): Promise>); + // only members field can be changed + const members = req.body.members; + requireFields({ members }); + + const group = await putGroup(classId, assignmentId, groupId, { members } as Partial); res.json({ group }); } diff --git a/backend/src/services/groups.ts b/backend/src/services/groups.ts index 1a957772..1b98b4f3 100644 --- a/backend/src/services/groups.ts +++ b/backend/src/services/groups.ts @@ -11,6 +11,14 @@ import { putObject } from './service-helper.js'; import { fetchStudents } from './students.js'; import { fetchClass } from './classes.js'; import { BadRequestException } from '../exceptions/bad-request-exception.js'; +import { Student } from '../entities/users/student.entity.js'; +import { Class } from '../entities/classes/class.entity.js'; + +async function assertMembersInClass(members: Student[], cls: Class): Promise { + if (!members.every(student => cls.students.contains(student))) { + throw new BadRequestException("Student does not belong to class"); + } +} export async function fetchGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise { const assignment = await fetchAssignment(classId, assignmentNumber); @@ -34,11 +42,19 @@ export async function putGroup( classId: string, assignmentNumber: number, groupNumber: number, - groupData: Partial> + groupData: Partial, ): Promise { const group = await fetchGroup(classId, assignmentNumber, groupNumber); - await putObject(group, groupData, getGroupRepository()); + const memberUsernames = groupData.members as string[]; + const members = await fetchStudents(memberUsernames); + + const cls = await fetchClass(classId); + await assertMembersInClass(members, cls); + + const groupRepository = getGroupRepository(); + groupRepository.assign(group, { members } as Partial>); + await groupRepository.getEntityManager().persistAndFlush(group); return mapToGroupDTO(group, group.assignment.within); } @@ -62,14 +78,11 @@ export async function getExistingGroupFromGroupDTO(groupData: GroupDTO): Promise } export async function createGroup(groupData: GroupDTO, classid: string, assignmentNumber: number): Promise { - const cls = await fetchClass(classid); - const memberUsernames = (groupData.members as string[]) || []; const members = await fetchStudents(memberUsernames); - if (!members.every(student => cls.students.contains(student))) { - throw new BadRequestException("It is not allowed to add a student to a group when the student is not part of the class"); - } + const cls = await fetchClass(classid); + await assertMembersInClass(members, cls) const assignment = await fetchAssignment(classid, assignmentNumber); From 283c2e6f78807d13e961370728f9155168a52636 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Tue, 22 Apr 2025 14:21:51 +0200 Subject: [PATCH 27/29] fix: group test asset met lage id veranderd naar hoge id --- backend/tests/test_assets/assignments/groups.testdata.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/test_assets/assignments/groups.testdata.ts b/backend/tests/test_assets/assignments/groups.testdata.ts index 16674843..4361383b 100644 --- a/backend/tests/test_assets/assignments/groups.testdata.ts +++ b/backend/tests/test_assets/assignments/groups.testdata.ts @@ -61,7 +61,7 @@ export function makeTestGroups(em: EntityManager, students: Student[], assignmen */ group1ConditionalLearningPath = em.create(Group, { assignment: getConditionalPathAssignment(), - groupNumber: 1, + groupNumber: 21005, members: [getTestleerling1()], }); From f35802f3c3f9f7e80c965da0a00df36266b9d04e Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Tue, 22 Apr 2025 14:26:00 +0200 Subject: [PATCH 28/29] fix: fixed linter issues --- backend/src/controllers/groups.ts | 4 +--- backend/src/services/groups.ts | 1 - frontend/src/components/MenuBar.vue | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/backend/src/controllers/groups.ts b/backend/src/controllers/groups.ts index 2f755e21..217510f6 100644 --- a/backend/src/controllers/groups.ts +++ b/backend/src/controllers/groups.ts @@ -3,8 +3,6 @@ import { createGroup, deleteGroup, getAllGroups, getGroup, getGroupSubmissions, import { GroupDTO } from '@dwengo-1/common/interfaces/group'; import { requireFields } from './error-helper.js'; import { BadRequestException } from '../exceptions/bad-request-exception.js'; -import { EntityDTO } from '@mikro-orm/core'; -import { Group } from '../entities/assignments/group.entity.js'; function checkGroupFields(classId: string, assignmentId: number, groupId: number): void { requireFields({ classId, assignmentId, groupId }); @@ -35,7 +33,7 @@ export async function putGroupHandler(req: Request, res: Response): Promise Date: Tue, 22 Apr 2025 12:31:04 +0000 Subject: [PATCH 29/29] style: fix linting issues met Prettier --- backend/src/services/groups.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/backend/src/services/groups.ts b/backend/src/services/groups.ts index 8e2ee9ab..382780d8 100644 --- a/backend/src/services/groups.ts +++ b/backend/src/services/groups.ts @@ -14,8 +14,8 @@ import { Student } from '../entities/users/student.entity.js'; import { Class } from '../entities/classes/class.entity.js'; async function assertMembersInClass(members: Student[], cls: Class): Promise { - if (!members.every(student => cls.students.contains(student))) { - throw new BadRequestException("Student does not belong to class"); + if (!members.every((student) => cls.students.contains(student))) { + throw new BadRequestException('Student does not belong to class'); } } @@ -37,12 +37,7 @@ export async function getGroup(classId: string, assignmentNumber: number, groupN return mapToGroupDTO(group, group.assignment.within); } -export async function putGroup( - classId: string, - assignmentNumber: number, - groupNumber: number, - groupData: Partial, -): Promise { +export async function putGroup(classId: string, assignmentNumber: number, groupNumber: number, groupData: Partial): Promise { const group = await fetchGroup(classId, assignmentNumber, groupNumber); const memberUsernames = groupData.members as string[]; @@ -81,7 +76,7 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme const members = await fetchStudents(memberUsernames); const cls = await fetchClass(classid); - await assertMembersInClass(members, cls) + await assertMembersInClass(members, cls); const assignment = await fetchAssignment(classid, assignmentNumber);