Merge pull request #198 from SELab-2/test/linking
test: Linking backend en frontend voor unit tests
This commit is contained in:
		
						commit
						37c9e622e6
					
				
					 12 changed files with 156 additions and 20 deletions
				
			
		|  | @ -11,3 +11,10 @@ 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 | ||||
|  |  | |||
|  | @ -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": "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/", | ||||
|         "format-check": "prettier --check src/", | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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}`; | ||||
|  |  | |||
|  | @ -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'; | ||||
| 
 | ||||
| const logger: Logger = getLogger(); | ||||
| 
 | ||||
| export async function seedDatabase(): Promise<void> { | ||||
|     dotenv.config({ path: '.env.development.local' }); | ||||
|     const orm = await initORM(); | ||||
| export async function seedORM(orm: MikroORM): Promise<void> { | ||||
|     await orm.schema.clearDatabase(); | ||||
| 
 | ||||
|     const em = forkEntityManager(); | ||||
|  | @ -68,8 +66,17 @@ export async function seedDatabase(): Promise<void> { | |||
|     ]); | ||||
| 
 | ||||
|     logger.info('Development database seeded successfully!'); | ||||
| } | ||||
| 
 | ||||
| export async function seedDatabase(envFile = '.env.development.local', testMode = false): Promise<void> { | ||||
|     dotenv.config({ path: envFile }); | ||||
|     const orm = await initORM(testMode); | ||||
| 
 | ||||
|     await seedORM(orm); | ||||
| 
 | ||||
|     await orm.close(); | ||||
| } | ||||
| 
 | ||||
| seedDatabase().catch(logger.error); | ||||
| seedDatabase().catch((err) => { | ||||
|     logger.error(err); | ||||
| }); | ||||
|  |  | |||
							
								
								
									
										29
									
								
								backend/tool/startTestApp.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								backend/tool/startTestApp.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | |||
| 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 { seedORM } from './seed'; | ||||
| 
 | ||||
| const envFile = '../.env.test'; | ||||
| 
 | ||||
| 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<void> { | ||||
|     await seedORM(await initORM(true)); | ||||
| 
 | ||||
|     app.listen(9876); | ||||
| } | ||||
| 
 | ||||
| await startServer(); | ||||
|  | @ -10,14 +10,12 @@ | |||
|         "preview": "vite preview", | ||||
|         "type-check": "vue-tsc --build", | ||||
|         "format": "prettier --write src/", | ||||
|         "test:e2e": "playwright test", | ||||
|         "format-check": "prettier --check src/", | ||||
|         "lint": "eslint . --fix", | ||||
|         "pretest:unit": "tsx ../docs/api/generate.ts && npm run build", | ||||
|         "test:unit": "vitest --run" | ||||
|         "test:unit": "vitest --run", | ||||
|         "test:e2e": "playwright test" | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@dwengo-1/common": "^0.1.1", | ||||
|         "@tanstack/react-query": "^5.69.0", | ||||
|         "@tanstack/vue-query": "^5.69.0", | ||||
|         "axios": "^1.8.2", | ||||
|  |  | |||
|  | @ -1,8 +1,24 @@ | |||
| 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.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; | ||||
|         } | ||||
| 
 | ||||
|         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"; | ||||
|  |  | |||
|  | @ -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,18 @@ export abstract class BaseController { | |||
|     } | ||||
| 
 | ||||
|     protected async get<T>(path: string, queryParams?: QueryParams, responseType?: ResponseType): Promise<T> { | ||||
|         try { | ||||
|             const response = await apiClient.get<T>(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<T>(path: string, body: unknown, queryParams?: QueryParams): Promise<T> { | ||||
|  |  | |||
							
								
								
									
										19
									
								
								frontend/tests/controllers/student-controller.test.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								frontend/tests/controllers/student-controller.test.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| import { StudentController } from "../../src/controllers/students"; | ||||
| import { expect, it, describe, afterAll, beforeAll } from "vitest"; | ||||
| import { setup, teardown } from "../setup-backend.js"; | ||||
| 
 | ||||
| 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); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										40
									
								
								frontend/tests/setup-backend.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								frontend/tests/setup-backend.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,40 @@ | |||
| import { spawn } from "child_process"; | ||||
| import { ChildProcess, spawnSync } from "node:child_process"; | ||||
| 
 | ||||
| let backendProcess: ChildProcess; | ||||
| 
 | ||||
| async function waitForEndpoint(url: string, delay = 1000, retries = 60): Promise<void> { | ||||
|     try { | ||||
|         await fetch(url); | ||||
|     } catch { | ||||
|         // Endpoint is not ready yet
 | ||||
|         await new Promise((resolve) => setTimeout(resolve, delay)); | ||||
|         // Retry
 | ||||
|         await waitForEndpoint(url, delay, retries - 1); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export async function setup(): Promise<void> { | ||||
|     // Precompile needed packages
 | ||||
|     spawnSync("npx", ["tsc", "--build", "tsconfig.json"], { | ||||
|         cwd: `../common`, | ||||
|     }); | ||||
| 
 | ||||
|     // Spin up the backend
 | ||||
|     backendProcess = spawn("npx", ["tsx", "--env-file=.env.test", "tool/startTestApp.ts"], { | ||||
|         cwd: `../backend`, | ||||
|         env: { | ||||
|             ...process.env, | ||||
|             NODE_ENV: "test", | ||||
|         }, | ||||
|     }); | ||||
| 
 | ||||
|     // Wait until you can curl the backend
 | ||||
|     await waitForEndpoint("http://localhost:9876/api"); | ||||
| } | ||||
| 
 | ||||
| export async function teardown(): Promise<void> { | ||||
|     if (backendProcess) { | ||||
|         backendProcess.kill(); | ||||
|     } | ||||
| } | ||||
|  | @ -9,6 +9,13 @@ export default mergeConfig( | |||
|             environment: "jsdom", | ||||
|             exclude: [...configDefaults.exclude, "e2e/**"], | ||||
|             root: fileURLToPath(new URL("./", import.meta.url)), | ||||
| 
 | ||||
|             /* | ||||
|              * 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. | ||||
|              */ | ||||
|         }, | ||||
|     }), | ||||
| ); | ||||
|  |  | |||
		Reference in a new issue
	
	 GitHub
							GitHub