fix: Merge dev into feat/assignment-page
This commit is contained in:
commit
bb3a242bf9
20 changed files with 207 additions and 37 deletions
|
@ -11,3 +11,10 @@ DWENGO_PORT=3000
|
||||||
|
|
||||||
DWENGO_DB_NAME=":memory:"
|
DWENGO_DB_NAME=":memory:"
|
||||||
DWENGO_DB_UPDATE=true
|
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",
|
"main": "dist/app.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env NODE_ENV=production tsc --build",
|
"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",
|
"start": "cross-env NODE_ENV=production node --env-file=.env dist/app.js",
|
||||||
"format": "prettier --write src/",
|
"format": "prettier --write src/",
|
||||||
"format-check": "prettier --check src/",
|
"format-check": "prettier --check src/",
|
||||||
|
|
|
@ -3,8 +3,6 @@ import { createGroup, deleteGroup, getAllGroups, getGroup, getGroupSubmissions,
|
||||||
import { GroupDTO } from '@dwengo-1/common/interfaces/group';
|
import { GroupDTO } from '@dwengo-1/common/interfaces/group';
|
||||||
import { requireFields } from './error-helper.js';
|
import { requireFields } from './error-helper.js';
|
||||||
import { BadRequestException } from '../exceptions/bad-request-exception.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 {
|
function checkGroupFields(classId: string, assignmentId: number, groupId: number): void {
|
||||||
requireFields({ classId, assignmentId, groupId });
|
requireFields({ classId, assignmentId, groupId });
|
||||||
|
@ -35,7 +33,11 @@ export async function putGroupHandler(req: Request, res: Response): Promise<void
|
||||||
const groupId = parseInt(req.params.groupid);
|
const groupId = parseInt(req.params.groupid);
|
||||||
checkGroupFields(classId, assignmentId, groupId);
|
checkGroupFields(classId, assignmentId, groupId);
|
||||||
|
|
||||||
const group = await putGroup(classId, assignmentId, groupId, req.body as Partial<EntityDTO<Group>>);
|
// Only members field can be changed
|
||||||
|
const members = req.body.members;
|
||||||
|
requireFields({ members });
|
||||||
|
|
||||||
|
const group = await putGroup(classId, assignmentId, groupId, { members } as Partial<GroupDTO>);
|
||||||
|
|
||||||
res.json({ group });
|
res.json({ group });
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,13 +26,15 @@ function initializeLogger(): Logger {
|
||||||
|
|
||||||
const consoleTransport = new transports.Console({
|
const consoleTransport = new transports.Console({
|
||||||
level: getEnvVar(envVars.LogLevel),
|
level: getEnvVar(envVars.LogLevel),
|
||||||
format: format.combine(format.cli(), format.colorize()),
|
format: format.combine(format.cli(), format.simple()),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (getEnvVar(envVars.RunMode) === 'dev') {
|
if (getEnvVar(envVars.RunMode) === 'dev') {
|
||||||
return createLogger({
|
logger = createLogger({
|
||||||
transports: [consoleTransport],
|
transports: [consoleTransport],
|
||||||
});
|
});
|
||||||
|
logger.debug(`Logger initialized with level ${logLevel} to console`);
|
||||||
|
return logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lokiHost = getEnvVar(envVars.LokiHost);
|
const lokiHost = getEnvVar(envVars.LokiHost);
|
||||||
|
|
|
@ -11,7 +11,7 @@ export class MikroOrmLogger extends DefaultLogger {
|
||||||
};
|
};
|
||||||
|
|
||||||
let message: string;
|
let message: string;
|
||||||
if (context?.label) {
|
if (context !== undefined && context.labels !== undefined) {
|
||||||
message = `[${namespace}] (${context.label}) ${messageArg}`;
|
message = `[${namespace}] (${context.label}) ${messageArg}`;
|
||||||
} else {
|
} else {
|
||||||
message = `[${namespace}] ${messageArg}`;
|
message = `[${namespace}] ${messageArg}`;
|
||||||
|
|
|
@ -7,8 +7,17 @@ import { GroupDTO, GroupDTOId } from '@dwengo-1/common/interfaces/group';
|
||||||
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
|
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
|
||||||
import { fetchAssignment } from './assignments.js';
|
import { fetchAssignment } from './assignments.js';
|
||||||
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
||||||
import { putObject } from './service-helper.js';
|
|
||||||
import { fetchStudents } from './students.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<void> {
|
||||||
|
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<Group> {
|
export async function fetchGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<Group> {
|
||||||
const assignment = await fetchAssignment(classId, assignmentNumber);
|
const assignment = await fetchAssignment(classId, assignmentNumber);
|
||||||
|
@ -28,15 +37,18 @@ export async function getGroup(classId: string, assignmentNumber: number, groupN
|
||||||
return mapToGroupDTO(group, group.assignment.within);
|
return mapToGroupDTO(group, group.assignment.within);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putGroup(
|
export async function putGroup(classId: string, assignmentNumber: number, groupNumber: number, groupData: Partial<GroupDTO>): Promise<GroupDTO> {
|
||||||
classId: string,
|
|
||||||
assignmentNumber: number,
|
|
||||||
groupNumber: number,
|
|
||||||
groupData: Partial<EntityDTO<Group>>
|
|
||||||
): Promise<GroupDTO> {
|
|
||||||
const group = await fetchGroup(classId, assignmentNumber, groupNumber);
|
const group = await fetchGroup(classId, assignmentNumber, groupNumber);
|
||||||
|
|
||||||
await putObject<Group>(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<EntityDTO<Group>>);
|
||||||
|
await groupRepository.getEntityManager().persistAndFlush(group);
|
||||||
|
|
||||||
return mapToGroupDTO(group, group.assignment.within);
|
return mapToGroupDTO(group, group.assignment.within);
|
||||||
}
|
}
|
||||||
|
@ -63,6 +75,9 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme
|
||||||
const memberUsernames = (groupData.members as string[]) || [];
|
const memberUsernames = (groupData.members as string[]) || [];
|
||||||
const members = await fetchStudents(memberUsernames);
|
const members = await fetchStudents(memberUsernames);
|
||||||
|
|
||||||
|
const cls = await fetchClass(classid);
|
||||||
|
await assertMembersInClass(members, cls);
|
||||||
|
|
||||||
const assignment = await fetchAssignment(classid, assignmentNumber);
|
const assignment = await fetchAssignment(classid, assignmentNumber);
|
||||||
|
|
||||||
const groupRepository = getGroupRepository();
|
const groupRepository = getGroupRepository();
|
||||||
|
|
|
@ -61,7 +61,7 @@ export function makeTestGroups(em: EntityManager, students: Student[], assignmen
|
||||||
*/
|
*/
|
||||||
group1ConditionalLearningPath = em.create(Group, {
|
group1ConditionalLearningPath = em.create(Group, {
|
||||||
assignment: getConditionalPathAssignment(),
|
assignment: getConditionalPathAssignment(),
|
||||||
groupNumber: 1,
|
groupNumber: 21005,
|
||||||
members: [getTestleerling1()],
|
members: [getTestleerling1()],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -14,14 +14,12 @@ import { makeTestQuestions } from '../tests/test_assets/questions/questions.test
|
||||||
import { makeTestStudents } from '../tests/test_assets/users/students.testdata.js';
|
import { makeTestStudents } from '../tests/test_assets/users/students.testdata.js';
|
||||||
import { makeTestTeachers } from '../tests/test_assets/users/teachers.testdata.js';
|
import { makeTestTeachers } from '../tests/test_assets/users/teachers.testdata.js';
|
||||||
import { getLogger, Logger } from '../src/logging/initalize.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';
|
import { Group } from '../src/entities/assignments/group.entity';
|
||||||
|
|
||||||
const logger: Logger = getLogger();
|
const logger: Logger = getLogger();
|
||||||
|
|
||||||
export async function seedDatabase(): Promise<void> {
|
export async function seedORM(orm: MikroORM): Promise<void> {
|
||||||
dotenv.config({ path: '.env.development.local' });
|
|
||||||
const orm = await initORM();
|
|
||||||
await orm.schema.clearDatabase();
|
await orm.schema.clearDatabase();
|
||||||
|
|
||||||
const em = forkEntityManager();
|
const em = forkEntityManager();
|
||||||
|
@ -68,8 +66,17 @@ export async function seedDatabase(): Promise<void> {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
logger.info('Development database seeded successfully!');
|
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();
|
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,11 +10,10 @@
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"type-check": "vue-tsc --build",
|
"type-check": "vue-tsc --build",
|
||||||
"format": "prettier --write src/",
|
"format": "prettier --write src/",
|
||||||
"test:e2e": "playwright test",
|
|
||||||
"format-check": "prettier --check src/",
|
"format-check": "prettier --check src/",
|
||||||
"lint": "eslint . --fix",
|
"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": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.69.0",
|
"@tanstack/react-query": "^5.69.0",
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
import MenuBar from "@/components/MenuBar.vue";
|
import MenuBar from "@/components/MenuBar.vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
import authService from "@/services/auth/auth-service.ts";
|
||||||
|
|
||||||
|
void authService.loadUser();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
interface RouteMeta {
|
interface RouteMeta {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
import auth from "@/services/auth/auth-service.ts";
|
import auth from "@/services/auth/auth-service.ts";
|
||||||
|
|
||||||
|
@ -10,9 +11,10 @@
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
|
|
||||||
const role = auth.authState.activeRole;
|
const role = auth.authState.activeRole;
|
||||||
|
const _router = useRouter(); // Zonder '_' gaf dit een linter error voor unused variable
|
||||||
|
|
||||||
const name = ref(auth.authState.user!.profile.name!);
|
const name: string = auth.authState.user!.profile.name!;
|
||||||
const initials: string = name.value
|
const initials: string = name
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.map((n) => n[0])
|
.map((n) => n[0])
|
||||||
.join("");
|
.join("");
|
||||||
|
|
|
@ -1,8 +1,24 @@
|
||||||
export const apiConfig = {
|
export const apiConfig = {
|
||||||
baseUrl:
|
baseUrl: ((): string => {
|
||||||
window.location.hostname === "localhost" && !(window.location.port === "80" || window.location.port === "")
|
if (import.meta.env.MODE === "test") {
|
||||||
? "http://localhost:3000/api"
|
// TODO Remove hardcoding
|
||||||
: window.location.origin + "/api",
|
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";
|
export const loginRoute = "/login";
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import apiClient from "@/services/api-client/api-client.ts";
|
import apiClient from "@/services/api-client/api-client.ts";
|
||||||
import type { AxiosResponse, ResponseType } from "axios";
|
import type { AxiosResponse, ResponseType } from "axios";
|
||||||
import { HttpErrorResponseException } from "@/exception/http-error-response-exception.ts";
|
import { HttpErrorResponseException } from "@/exception/http-error-response-exception.ts";
|
||||||
|
import { apiConfig } from "@/config.ts";
|
||||||
|
|
||||||
export abstract class BaseController {
|
export abstract class BaseController {
|
||||||
protected basePath: string;
|
protected basePath: string;
|
||||||
|
@ -16,9 +17,18 @@ export abstract class BaseController {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async get<T>(path: string, queryParams?: QueryParams, responseType?: ResponseType): Promise<T> {
|
protected async get<T>(path: string, queryParams?: QueryParams, responseType?: ResponseType): Promise<T> {
|
||||||
const response = await apiClient.get<T>(this.absolutePathFor(path), { params: queryParams, responseType });
|
try {
|
||||||
BaseController.assertSuccessResponse(response);
|
const response = await apiClient.get<T>(this.absolutePathFor(path), { params: queryParams, responseType });
|
||||||
return response.data;
|
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> {
|
protected async post<T>(path: string, body: unknown, queryParams?: QueryParams): Promise<T> {
|
||||||
|
|
|
@ -12,11 +12,9 @@ import App from "./App.vue";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
import { aliases, mdi } from "vuetify/iconsets/mdi";
|
import { aliases, mdi } from "vuetify/iconsets/mdi";
|
||||||
import { VueQueryPlugin, QueryClient } from "@tanstack/vue-query";
|
import { VueQueryPlugin, QueryClient } from "@tanstack/vue-query";
|
||||||
import authService from "./services/auth/auth-service.ts";
|
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
await authService.loadUser();
|
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
const link = document.createElement("link");
|
const link = document.createElement("link");
|
||||||
|
|
|
@ -142,7 +142,7 @@ const router = createRouter({
|
||||||
router.beforeEach(async (to, _from, next) => {
|
router.beforeEach(async (to, _from, next) => {
|
||||||
// Verify if user is logged in before accessing certain routes
|
// Verify if user is logged in before accessing certain routes
|
||||||
if (to.meta.requiresAuth) {
|
if (to.meta.requiresAuth) {
|
||||||
if (!authService.isLoggedIn.value) {
|
if (!authService.isLoggedIn.value && !(await authService.loadUser())) {
|
||||||
next("/login");
|
next("/login");
|
||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
|
|
|
@ -37,7 +37,20 @@ async function loadUser(): Promise<User | null> {
|
||||||
if (!activeRole) {
|
if (!activeRole) {
|
||||||
return null;
|
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 (_: 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);
|
setUserAuthInfo(user);
|
||||||
authState.activeRole = activeRole ?? null;
|
authState.activeRole = activeRole ?? null;
|
||||||
return user;
|
return user;
|
||||||
|
|
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",
|
environment: "jsdom",
|
||||||
exclude: [...configDefaults.exclude, "e2e/**"],
|
exclude: [...configDefaults.exclude, "e2e/**"],
|
||||||
root: fileURLToPath(new URL("./", import.meta.url)),
|
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.
|
||||||
|
*/
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue