Merge pull request #198 from SELab-2/test/linking

test: Linking backend en frontend voor unit tests
This commit is contained in:
Tibo De Peuter 2025-04-21 14:28:02 +02:00 committed by GitHub
commit 37c9e622e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 156 additions and 20 deletions

View file

@ -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

View file

@ -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/",

View file

@ -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);

View file

@ -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}`;

View file

@ -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);
});

View 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();

View file

@ -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",

View file

@ -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";

View file

@ -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> {
const response = await apiClient.get<T>(this.absolutePathFor(path), { params: queryParams, responseType });
BaseController.assertSuccessResponse(response);
return response.data;
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> {

View 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);
});
});

View 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();
}
}

View file

@ -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.
*/
},
}),
);