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. | ||||||
|  |              */ | ||||||
|         }, |         }, | ||||||
|     }), |     }), | ||||||
| ); | ); | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Joyelle Ndagijimana
						Joyelle Ndagijimana