Merge branch 'dev' into release/0.2.0

This commit is contained in:
Tibo De Peuter 2025-04-24 19:58:25 +02:00
commit ce06cfc8e2
Signed by: tdpeuter
GPG key ID: 38297DE43F75FFE2
17 changed files with 495 additions and 12 deletions

View file

@ -29,6 +29,12 @@ jobs:
if: '! github.event.pull_request.draft'
runs-on: [self-hosted, Linux, X64]
permissions:
# Required to checkout the code
contents: read
# Required to put a comment into the pull-request
pull-requests: write
strategy:
matrix:
node-version: [22.x]
@ -42,4 +48,16 @@ jobs:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run test:unit -w backend
- run: npm run build
- run: npm run test:coverage -w backend
- name: 'Report Backend Coverage'
# Set if: always() to also generate the report if tests are failing
# Only works if you set `reportOnFailure: true` in your vite config as specified above
if: always()
uses: davelosert/vitest-coverage-report-action@v2
with:
name: 'Backend'
json-summary-path: './backend/coverage/coverage-summary.json'
json-final-path: './backend/coverage/coverage-final.json'
vite-config-path: './backend/vitest.config.ts'
file-coverage-mode: all

View file

@ -38,6 +38,12 @@ jobs:
if: '! github.event.pull_request.draft'
runs-on: [self-hosted, Linux, X64]
permissions:
# Required to checkout the code
contents: read
# Required to put a comment into the pull-request
pull-requests: write
strategy:
matrix:
node-version: [22.x]
@ -51,4 +57,16 @@ jobs:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run test:unit -w frontend
- run: npm run build
- run: npm run test:coverage -w frontend
- name: 'Report Frontend Coverage'
# Set if: always() to also generate the report if tests are failing
# Only works if you set `reportOnFailure: true` in your vite config as specified above
if: always()
uses: davelosert/vitest-coverage-report-action@v2
with:
name: 'Frontend'
json-summary-path: './frontend/coverage/coverage-summary.json'
json-final-path: './frontend/coverage/coverage-final.json'
vite-config-path: './frontend/vitest.config.ts'
file-coverage-mode: all

View file

@ -14,7 +14,8 @@
"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:coverage": "vitest --run --coverage.enabled true"
},
"dependencies": {
"@mikro-orm/core": "6.4.12",

View file

@ -5,5 +5,17 @@ export default defineConfig({
environment: 'node',
globals: true,
testTimeout: 100000,
coverage: {
reporter: ['text', 'json-summary', 'json'],
// If you want a coverage reports even if your tests are failing, include the reportOnFailure option
reportOnFailure: true,
exclude: ['**/*config*', '**/tests/**', 'src/*.ts', '**/dist/**', '**/node_modules/**', 'src/logging/**', 'src/routes/**'],
thresholds: {
lines: 50,
branches: 50,
functions: 50,
statements: 50,
},
},
},
});

View file

@ -13,6 +13,7 @@
"format-check": "prettier --check src/",
"lint": "eslint . --fix",
"test:unit": "vitest --run",
"test:coverage": "vitest --run --coverage.enabled true",
"test:e2e": "playwright test"
},
"dependencies": {

View file

@ -0,0 +1,64 @@
import { describe, it, expect, beforeEach } from "vitest";
import { AssignmentController } from "../../src/controllers/assignments";
describe("AssignmentController Tests", () => {
let controller: AssignmentController;
beforeEach(() => {
controller = new AssignmentController("8764b861-90a6-42e5-9732-c0d9eb2f55f9"); // Example class ID
});
it("should fetch all assignments", async () => {
const result = await controller.getAll(true);
expect(result).toHaveProperty("assignments");
expect(Array.isArray(result.assignments)).toBe(true);
expect(result.assignments.length).toBeGreaterThan(0);
});
it("should fetch an assignment by number", async () => {
const assignmentNumber = 21000; // Example assignment ID
const result = await controller.getByNumber(assignmentNumber);
expect(result).toHaveProperty("assignment");
expect(result.assignment).toHaveProperty("id", assignmentNumber);
});
it("should update an existing assignment", async () => {
const assignmentNumber = 21000;
const updatedData = { title: "Updated Assignment Title" };
const result = await controller.updateAssignment(assignmentNumber, updatedData);
expect(result).toHaveProperty("assignment");
expect(result.assignment).toHaveProperty("id", assignmentNumber);
expect(result.assignment).toHaveProperty("title", updatedData.title);
});
it("should fetch submissions for an assignment", async () => {
const assignmentNumber = 21000;
const result = await controller.getSubmissions(assignmentNumber, true);
expect(result).toHaveProperty("submissions");
expect(Array.isArray(result.submissions)).toBe(true);
});
it("should fetch questions for an assignment", async () => {
const assignmentNumber = 21000;
const result = await controller.getQuestions(assignmentNumber, true);
expect(result).toHaveProperty("questions");
expect(Array.isArray(result.questions)).toBe(true);
});
it("should fetch groups for an assignment", async () => {
const assignmentNumber = 21000;
const result = await controller.getGroups(assignmentNumber, true);
expect(result).toHaveProperty("groups");
expect(Array.isArray(result.groups)).toBe(true);
});
it("should handle fetching a non-existent assignment", async () => {
const assignmentNumber = 99999; // Non-existent assignment ID
await expect(controller.getByNumber(assignmentNumber)).rejects.toThrow();
});
it("should handle deleting a non-existent assignment", async () => {
const assignmentNumber = 99999; // Non-existent assignment ID
await expect(controller.deleteAssignment(assignmentNumber)).rejects.toThrow();
});
});

View file

@ -0,0 +1,10 @@
import { describe, expect, it } from "vitest";
import { ClassController } from "../../src/controllers/classes";
describe("Test controller classes", () => {
it("Get classes", async () => {
const controller = new ClassController();
const data = await controller.getAll(true);
expect(data.classes).to.have.length.greaterThan(0);
});
});

View file

@ -0,0 +1,13 @@
import { describe, expect, it } from "vitest";
import { GroupController } from "../../src/controllers/groups";
describe("Test controller groups", () => {
it("Get groups", async () => {
const classId = "8764b861-90a6-42e5-9732-c0d9eb2f55f9";
const assignmentNumber = 21000;
const controller = new GroupController(classId, assignmentNumber);
const data = await controller.getAll(true);
expect(data.groups).to.have.length.greaterThan(0);
});
});

View file

@ -0,0 +1,21 @@
import { beforeEach, describe, expect, it } from "vitest";
import { LearningPathController } from "../../src/controllers/learning-paths";
import { Language } from "../../src/data-objects/language";
describe("Test controller learning paths", () => {
let controller: LearningPathController;
beforeEach(async () => {
controller = new LearningPathController();
});
it("Can search for learning paths", async () => {
const data = await controller.search("kiks", Language.Dutch);
expect(data).to.have.length.greaterThan(0);
});
it("Can get learning path by id", async () => {
const data = await controller.getAllByTheme("kiks");
expect(data).to.have.length.greaterThan(0);
});
});

View file

@ -1,19 +1,39 @@
import { StudentController } from "../../src/controllers/students";
import { expect, it, describe, afterAll, beforeAll } from "vitest";
import { setup, teardown } from "../setup-backend.js";
import { beforeEach, describe, expect, it, test } from "vitest";
describe("Test controller students", () => {
beforeAll(async () => {
await setup();
});
let controller: StudentController;
afterAll(async () => {
await teardown();
beforeEach(async () => {
controller = new StudentController();
});
it("Get students", async () => {
const controller = new StudentController();
const data = await controller.getAll(true);
expect(data.students).to.have.length.greaterThan(0);
});
it("Get student by username", async () => {
const username = "testleerling1";
const data = await controller.getByUsername(username);
expect(data.student.username).to.equal(username);
});
});
const controller = new StudentController();
test.each([
{ username: "Noordkaap", firstName: "Stijn", lastName: "Meuris" },
{ username: "DireStraits", firstName: "Mark", lastName: "Knopfler" },
{ username: "Tool", firstName: "Maynard", lastName: "Keenan" },
{ username: "SmashingPumpkins", firstName: "Billy", lastName: "Corgan" },
{ username: "PinkFloyd", firstName: "David", lastName: "Gilmoure" },
{ username: "TheDoors", firstName: "Jim", lastName: "Morisson" },
// ⚠️ Deze mag niet gebruikt worden in elke test!
{ username: "Nirvana", firstName: "Kurt", lastName: "Cobain" },
// Makes sure when logged in as leerling1, there exists a corresponding user
{ username: "testleerling1", firstName: "Gerald", lastName: "Schmittinger" },
])("Get classes of student", async (student) => {
const data = await controller.getClasses(student.username, true);
expect(data.classes).to.have.length.greaterThan(0);
});

View file

@ -0,0 +1,15 @@
import { describe, expect, it } from "vitest";
import { SubmissionController } from "../../src/controllers/submissions";
import { Language } from "../../src/data-objects/language";
describe("Test controller submissions", () => {
it("Get submission by number", async () => {
const hruid = "id03";
const classId = "8764b861-90a6-42e5-9732-c0d9eb2f55f9";
const controller = new SubmissionController(hruid);
const data = await controller.getByNumber(Language.English, 1, classId, 1, 1, 1);
expect(data.submission).to.have.property("submissionNumber");
});
});

View file

@ -0,0 +1,53 @@
import { beforeEach, describe, expect, it } from "vitest";
import { TeacherController } from "../../src/controllers/teachers";
describe("Test controller teachers", () => {
let controller: TeacherController;
beforeEach(async () => {
controller = new TeacherController();
});
it("Get all teachers", async () => {
const data = await controller.getAll(true);
expect(data.teachers).to.have.length.greaterThan(0);
expect(data.teachers[0]).to.have.property("username");
expect(data.teachers[0]).to.have.property("firstName");
expect(data.teachers[0]).to.have.property("lastName");
});
it("Get teacher by username", async () => {
const username = "testleerkracht1";
const data = await controller.getByUsername(username);
expect(data.teacher.username).to.equal(username);
expect(data.teacher).to.have.property("firstName");
expect(data.teacher).to.have.property("lastName");
});
it("Get teacher by non-existent username", async () => {
const username = "nonexistentuser";
await expect(controller.getByUsername(username)).rejects.toThrow();
});
it("Handle deletion of non-existent teacher", async () => {
const username = "nonexistentuser";
await expect(controller.deleteTeacher(username)).rejects.toThrow();
});
it("Get classes for a teacher", async () => {
const username = "testleerkracht1";
const data = await controller.getClasses(username, true);
expect(data.classes).to.have.length.greaterThan(0);
expect(data.classes[0]).to.have.property("id");
expect(data.classes[0]).to.have.property("displayName");
});
it("Get students for a teacher", async () => {
const username = "testleerkracht1";
const data = await controller.getStudents(username, true);
expect(data.students).to.have.length.greaterThan(0);
expect(data.students[0]).to.have.property("username");
expect(data.students[0]).to.have.property("firstName");
expect(data.students[0]).to.have.property("lastName");
});
});

View file

@ -0,0 +1,48 @@
import { describe, it, expect, beforeEach } from "vitest";
import { ThemeController } from "../../src/controllers/themes";
describe("ThemeController Tests", () => {
let controller: ThemeController;
beforeEach(() => {
controller = new ThemeController();
});
it("should fetch all themes", async () => {
const result = await controller.getAll();
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBeGreaterThan(0);
expect(result[0]).toHaveProperty("key");
expect(result[0]).toHaveProperty("title");
expect(result[0]).toHaveProperty("description");
expect(result[0]).toHaveProperty("image");
});
it("should fetch all themes filtered by language", async () => {
const language = "en";
const result = await controller.getAll(language);
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBeGreaterThan(0);
result.forEach((theme) => {
expect(theme).toHaveProperty("key");
expect(theme).toHaveProperty("title");
expect(theme).toHaveProperty("description");
expect(theme).toHaveProperty("image");
});
});
it("should fetch HRUIDs by theme key", async () => {
const themeKey = "kiks";
const result = await controller.getHruidsByKey(themeKey);
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBeGreaterThan(0);
result.forEach((hruid) => {
expect(typeof hruid).toBe("string");
});
});
it("should handle fetching HRUIDs for a non-existent theme key", async () => {
const themeKey = "nonexistent";
await expect(controller.getHruidsByKey(themeKey)).rejects.toThrow();
});
});

View file

@ -0,0 +1,82 @@
import { describe, expect, it } from "vitest";
import {
assignmentTitleRules,
classRules,
deadlineRules,
descriptionRules,
learningPathRules,
} from "../../src/utils/assignment-rules";
describe("Validation Rules", () => {
describe("assignmentTitleRules", () => {
it("should return true for a valid title", () => {
const result = assignmentTitleRules[0]("Valid Title");
expect(result).toBe(true);
});
it("should return an error message for an empty title", () => {
const result = assignmentTitleRules[0]("");
expect(result).toBe("Title cannot be empty.");
});
});
describe("learningPathRules", () => {
it("should return true for a valid learning path", () => {
const result = learningPathRules[0]({ hruid: "123", title: "Path Title" });
expect(result).toBe(true);
});
it("should return an error message for an invalid learning path", () => {
const result = learningPathRules[0]({ hruid: "", title: "" });
expect(result).toBe("You must select a learning path.");
});
});
describe("classRules", () => {
it("should return true for a valid class", () => {
const result = classRules[0]("Class 1");
expect(result).toBe(true);
});
it("should return an error message for an empty class", () => {
const result = classRules[0]("");
expect(result).toBe("You must select at least one class.");
});
});
describe("deadlineRules", () => {
it("should return true for a valid future deadline", () => {
const futureDate = new Date(Date.now() + 1000 * 60 * 60).toISOString();
const result = deadlineRules[0](futureDate);
expect(result).toBe(true);
});
it("should return an error message for a past deadline", () => {
const pastDate = new Date(Date.now() - 1000 * 60 * 60).toISOString();
const result = deadlineRules[0](pastDate);
expect(result).toBe("The deadline must be in the future.");
});
it("should return an error message for an invalid date", () => {
const result = deadlineRules[0]("invalid-date");
expect(result).toBe("Invalid date or time.");
});
it("should return an error message for an empty deadline", () => {
const result = deadlineRules[0]("");
expect(result).toBe("You must set a deadline.");
});
});
describe("descriptionRules", () => {
it("should return true for a valid description", () => {
const result = descriptionRules[0]("This is a valid description.");
expect(result).toBe(true);
});
it("should return an error message for an empty description", () => {
const result = descriptionRules[0]("");
expect(result).toBe("Description cannot be empty.");
});
});
});

View file

@ -0,0 +1,68 @@
import { describe, it, expect } from "vitest";
import { deepEquals } from "../../src/utils/deep-equals";
describe("deepEquals", () => {
it("should return true for identical primitive values", () => {
expect(deepEquals(1, 1)).toBe(true);
expect(deepEquals("test", "test")).toBe(true);
expect(deepEquals(true, true)).toBe(true);
});
it("should return false for different primitive values", () => {
expect(deepEquals(1, 2)).toBe(false);
expect(deepEquals("test", "other")).toBe(false);
expect(deepEquals(true, false)).toBe(false);
});
it("should return true for identical objects", () => {
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
expect(deepEquals(obj1, obj2)).toBe(true);
});
it("should return false for different objects", () => {
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 3 } };
expect(deepEquals(obj1, obj2)).toBe(false);
});
it("should return true for identical arrays", () => {
const arr1 = [1, 2, [3, 4]];
const arr2 = [1, 2, [3, 4]];
expect(deepEquals(arr1, arr2)).toBe(true);
});
it("should return false for different arrays", () => {
const arr1 = [1, 2, [3, 4]];
const arr2 = [1, 2, [3, 5]];
expect(deepEquals(arr1, arr2)).toBe(false);
});
it("should return false for objects and arrays compared", () => {
expect(deepEquals({ a: 1 }, [1])).toBe(false);
});
it("should return true for null compared to null", () => {
expect(deepEquals(null, null)).toBe(true);
});
it("should return false for null compared to an object", () => {
expect(deepEquals(null, {})).toBe(false);
});
it("should return false for undefined compared to null", () => {
expect(deepEquals(undefined, null)).toBe(false);
});
it("should return true for deeply nested identical structures", () => {
const obj1 = { a: [1, { b: 2, c: [3, 4] }] };
const obj2 = { a: [1, { b: 2, c: [3, 4] }] };
expect(deepEquals(obj1, obj2)).toBe(true);
});
it("should return false for deeply nested different structures", () => {
const obj1 = { a: [1, { b: 2, c: [3, 4] }] };
const obj2 = { a: [1, { b: 2, c: [3, 5] }] };
expect(deepEquals(obj1, obj2)).toBe(false);
});
});

View file

@ -9,6 +9,43 @@ export default mergeConfig(
environment: "jsdom",
exclude: [...configDefaults.exclude, "e2e/**"],
root: fileURLToPath(new URL("./", import.meta.url)),
testTimeout: 100000,
coverage: {
reporter: ["text", "json-summary", "json"],
// If you want a coverage reports even if your tests are failing, include the reportOnFailure option
reportOnFailure: true,
exclude: [
"**/*config*",
"**/tests/**",
"playwright-report/**",
"**/dist/**",
"**/e2e/**",
"**/*config*",
"**/node_modules/**",
"src/main.ts",
"src/router/index.ts",
"src/utils/constants.ts",
"**/*.d.ts",
"src/**/*.vue",
"src/assets/**",
"src/i18n/**",
"src/data-objects/**",
"src/exception/**", // TODO Might be useful to test later
"src/queries/**", // TODO Might be useful to test later
"src/views/learning-paths/gift-adapters/**", // TODO Might be useful to test later
"src/services/auth/**", // TODO Might be useful to test later
],
thresholds: {
lines: 50,
branches: 50,
functions: 50,
statements: 50,
},
},
/*
* The test-backend server can be started for each test-file individually using `beforeAll(() => setup())`,
@ -16,6 +53,7 @@ export default mergeConfig(
globalSetup: ["./tests/setup-backend.ts"],
* In this project, the backend server is started for each test-file individually.
*/
globalSetup: ["./tests/setup-backend.ts"],
},
}),
);

View file

@ -13,7 +13,8 @@
"format-check": "npm run format-check --workspace=backend --workspace=common --workspace=frontend",
"lint": "npm run lint --workspace=backend --workspace=common --workspace=frontend",
"pretest:unit": "npm run build",
"test:unit": "npm run test:unit --workspace=backend --workspace=frontend"
"test:unit": "npm run test:unit --workspace=backend --workspace=frontend",
"test:coverage": "npm run test:coverage --workspace=backend --workspace=frontend"
},
"workspaces": [
"backend",