Merge remote-tracking branch 'origin/feat/endpoints-beschermen-met-authenticatie-#105' into feat/endpoints-beschermen-met-authenticatie-#105

# Conflicts:
#	backend/src/middleware/auth/checks/auth-checks.ts
#	backend/src/middleware/auth/checks/class-auth-checks.ts
#	backend/src/routes/teachers.ts
#	frontend/src/views/assignments/UserAssignments.vue
This commit is contained in:
Gabriellvl 2025-05-15 20:58:21 +02:00
commit 7da52284e6
40 changed files with 1042 additions and 541 deletions

View file

@ -8,6 +8,7 @@
### Dwengo ###
DWENGO_PORT=3000
DWENGO_RUN_MODE=test
DWENGO_DB_NAME=":memory:"
DWENGO_DB_UPDATE=true

View file

@ -62,6 +62,11 @@ export async function getAllSubmissionsHandler(req: Request, res: Response): Pro
// TODO: gerald moet nog dingen toevoegen aan de databank voor dat dit gefinaliseerd kan worden
export async function createSubmissionHandler(req: Request, res: Response): Promise<void> {
const submitter = req.body.submitter;
const usernameSubmitter = req.body.submitter.username;
const group = req.body.group;
requireFields({ group, submitter, usernameSubmitter });
const submissionDTO = req.body as SubmissionDTO;
const submission = await createSubmission(submissionDTO);

View file

@ -7,7 +7,6 @@ import {
getJoinRequestsByClass,
getStudentsByTeacher,
getTeacher,
getTeacherQuestions,
updateClassJoinRequestStatus,
} from '../services/teachers.js';
import { requireFields } from './error-helper.js';
@ -70,16 +69,6 @@ export async function getTeacherStudentHandler(req: Request, res: Response): Pro
res.json({ students });
}
export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username;
const full = req.query.full === 'true';
requireFields({ username });
const questions = await getTeacherQuestions(username, full);
res.json({ questions });
}
export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.classId;
requireFields({ classId });

View file

@ -2,7 +2,6 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { LearningObject } from '../../entities/content/learning-object.entity.js';
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
import { Language } from '@dwengo-1/common/util/language';
import { Teacher } from '../../entities/users/teacher.entity.js';
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
@ -32,11 +31,4 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
}
);
}
public async findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> {
return this.find(
{ admins: teacher },
{ populate: ['admins'] } // Make sure to load admin relations
);
}
}

View file

@ -26,6 +26,9 @@ export class Assignment {
@Property({ type: 'string' })
learningPathHruid!: string;
@Property({ type: 'datetime', nullable: true })
deadline?: Date;
@Enum({
items: () => Language,
})

View file

@ -20,6 +20,7 @@ export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO {
description: assignment.description,
learningPath: assignment.learningPathHruid,
language: assignment.learningPathLanguage,
deadline: assignment.deadline ?? new Date(),
groups: assignment.groups.map((group) => mapToGroupDTO(group, assignment.within)),
};
}
@ -31,6 +32,7 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi
description: assignmentData.description,
learningPathHruid: assignmentData.learningPath,
learningPathLanguage: languageMap[assignmentData.language],
deadline: assignmentData.deadline,
groups: [],
});
}

View file

@ -4,6 +4,7 @@ import * as express from 'express';
import { RequestHandler } from 'express';
import { UnauthorizedException } from '../../../exceptions/unauthorized-exception.js';
import { ForbiddenException } from '../../../exceptions/forbidden-exception.js';
import { envVars, getEnvVar } from '../../../util/envVars.js';
import {AccountType} from "@dwengo-1/common/util/account-types";
/**
@ -15,6 +16,17 @@ import {AccountType} from "@dwengo-1/common/util/account-types";
export function authorize<P, ResBody, ReqBody, ReqQuery, Locals extends Record<string, unknown>>(
accessCondition: (auth: AuthenticationInfo, req: AuthenticatedRequest<P, ResBody, ReqBody, ReqQuery, Locals>) => boolean | Promise<boolean>
): RequestHandler<P, ResBody, ReqBody, ReqQuery, Locals> {
// Bypass authentication during testing
if (getEnvVar(envVars.RunMode) === 'test') {
return async (
_req: AuthenticatedRequest<P, ResBody, ReqBody, ReqQuery, Locals>,
_res: express.Response,
next: express.NextFunction
): Promise<void> => {
next();
};
}
return async (
req: AuthenticatedRequest<P, ResBody, ReqBody, ReqQuery, Locals>,
_res: express.Response,

View file

@ -3,7 +3,7 @@ import { AuthenticationInfo } from '../authentication-info.js';
import { AuthenticatedRequest } from '../authenticated-request.js';
import { fetchClass } from '../../../services/classes.js';
import { mapToUsername } from '../../../interfaces/user.js';
import {getAllInvitations} from "../../../services/teacher-invitations";
import { getAllInvitations } from '../../../services/teacher-invitations.js';
import {AccountType} from "@dwengo-1/common/util/account-types";
async function teaches(teacherUsername: string, classId: string): Promise<boolean> {
@ -51,7 +51,7 @@ export const onlyAllowIfInClassOrInvited = authorize(async (auth: Authentication
const clazz = await fetchClass(classId);
if (auth.accountType === AccountType.Teacher) {
const invitations = await getAllInvitations(auth.username, false);
return clazz.teachers.map(mapToUsername).includes(auth.username) || invitations.some(invitation => invitation.classId === classId);
return clazz.teachers.map(mapToUsername).includes(auth.username) || invitations.some((invitation) => invitation.classId === classId);
}
return clazz.students.map(mapToUsername).includes(auth.username);
});

View file

@ -5,25 +5,31 @@ import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/aut
const router = express.Router();
// Returns auth configuration for frontend
router.get('/config', handleGetFrontendAuthConfig)
router.get('/config', handleGetFrontendAuthConfig);
router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => {
/* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */
/* #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
res.json({ message: 'If you see this, you should be authenticated!' });
});
router.get('/testStudentsOnly', studentsOnly, (_req, res) => {
/* #swagger.security = [{ "student": [ ] }] */
/* #swagger.security = [{ "studentProduction": [ ] }, { "studentStaging": [ ] }, { "studentDev": [ ] }] */
res.json({ message: 'If you see this, you should be a student!' });
});
router.get('/testTeachersOnly', teachersOnly, (_req, res) => {
/* #swagger.security = [{ "teacher": [ ] }] */
/* #swagger.security = [{ "teacherProduction": [ ] }, { "teacherStaging": [ ] }, { "teacherDev": [ ] }] */
res.json({ message: 'If you see this, you should be a teacher!' });
});
// This endpoint is called by the client when the user has just logged in.
// It creates or updates the user entity based on the authentication data the endpoint was called with.
router.post('/hello', authenticatedOnly, postHelloHandler);
router.post(
'/hello',
authenticatedOnly,
/*
#swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }]
*/ postHelloHandler
);
export default router;

View file

@ -15,7 +15,7 @@ import {
} from '../controllers/classes.js';
import assignmentRouter from './assignments.js';
import { adminOnly, teachersOnly } from '../middleware/auth/checks/auth-checks.js';
import {onlyAllowIfInClass, onlyAllowIfInClassOrInvited} from '../middleware/auth/checks/class-auth-checks.js';
import { onlyAllowIfInClass, onlyAllowIfInClassOrInvited } from '../middleware/auth/checks/class-auth-checks.js';
const router = express.Router();

View file

@ -18,12 +18,30 @@ router.get('/', (_, res: Response) => {
});
});
router.use('/student', studentRouter /* #swagger.tags = ['Student'] */);
router.use('/teacher', teacherRouter /* #swagger.tags = ['Teacher'] */);
router.use('/class', classRouter /* #swagger.tags = ['Class'] */);
router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */);
router.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */);
router.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */);
router.use('/learningObject', learningObjectRoutes /* #swagger.tags = ['Learning Object'] */);
router.use(
'/class',
classRouter /* #swagger.tags = ['Class'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
);
router.use(
'/learningObject',
learningObjectRoutes /* #swagger.tags = ['Learning Object'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
);
router.use(
'/learningPath',
learningPathRoutes /* #swagger.tags = ['Learning Path'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
);
router.use(
'/student',
studentRouter /* #swagger.tags = ['Student'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
);
router.use(
'/teacher',
teacherRouter /* #swagger.tags = ['Teacher'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
);
router.use(
'/theme',
themeRoutes /* #swagger.tags = ['Theme'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
);
export default router;

View file

@ -6,7 +6,6 @@ import {
getStudentJoinRequestHandler,
getTeacherClassHandler,
getTeacherHandler,
getTeacherQuestionHandler,
getTeacherStudentHandler,
updateStudentJoinRequestHandler,
} from '../controllers/teachers.js';
@ -29,8 +28,6 @@ router.get('/:username/classes', preventImpersonation, getTeacherClassHandler);
router.get('/:username/students', preventImpersonation, getTeacherStudentHandler);
router.get('/:username/questions', preventImpersonation, getTeacherQuestionHandler);
router.get('/:username/joinRequests/:classId', onlyAllowTeacherOfClass, getStudentJoinRequestHandler);
router.put('/:username/joinRequests/:classId/:studentUsername', onlyAllowTeacherOfClass, updateStudentJoinRequestHandler);

View file

@ -43,7 +43,7 @@ export async function fetchStudent(username: string): Promise<Student> {
const user = await studentRepository.findByUsername(username);
if (!user) {
throw new NotFoundException('Student with username not found');
throw new NotFoundException(`Student with username ${username} not found`);
}
return user;

View file

@ -1,12 +1,5 @@
import {
getClassJoinRequestRepository,
getClassRepository,
getLearningObjectRepository,
getQuestionRepository,
getTeacherRepository,
} from '../data/repositories.js';
import { getClassJoinRequestRepository, getClassRepository, getTeacherRepository } from '../data/repositories.js';
import { mapToClassDTO } from '../interfaces/class.js';
import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js';
import { mapToTeacher, mapToTeacherDTO } from '../interfaces/teacher.js';
import { Teacher } from '../entities/users/teacher.entity.js';
import { fetchStudent } from './students.js';
@ -15,10 +8,6 @@ import { mapToStudentRequestDTO } from '../interfaces/student-request.js';
import { TeacherRepository } from '../data/users/teacher-repository.js';
import { ClassRepository } from '../data/classes/class-repository.js';
import { Class } from '../entities/classes/class.entity.js';
import { LearningObjectRepository } from '../data/content/learning-object-repository.js';
import { LearningObject } from '../entities/content/learning-object.entity.js';
import { QuestionRepository } from '../data/questions/question-repository.js';
import { Question } from '../entities/questions/question.entity.js';
import { ClassJoinRequestRepository } from '../data/classes/class-join-request-repository.js';
import { Student } from '../entities/users/student.entity.js';
import { NotFoundException } from '../exceptions/not-found-exception.js';
@ -26,7 +15,6 @@ import { addClassStudent, fetchClass, getClassStudentsDTO } from './classes.js';
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
import { ClassDTO } from '@dwengo-1/common/interfaces/class';
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
import { ConflictException } from '../exceptions/conflict-exception.js';
@ -127,28 +115,6 @@ export async function getStudentsByTeacher(username: string, full: boolean): Pro
return students.map((student) => student.username);
}
export async function getTeacherQuestions(username: string, full: boolean): Promise<QuestionDTO[] | QuestionId[]> {
const teacher: Teacher = await fetchTeacher(username);
// Find all learning objects that this teacher manages
const learningObjectRepository: LearningObjectRepository = getLearningObjectRepository();
const learningObjects: LearningObject[] = await learningObjectRepository.findAllByTeacher(teacher);
if (!learningObjects || learningObjects.length === 0) {
return [];
}
// Fetch all questions related to these learning objects
const questionRepository: QuestionRepository = getQuestionRepository();
const questions: Question[] = await questionRepository.findAllByLearningObjects(learningObjects);
if (full) {
return questions.map(mapToQuestionDTO);
}
return questions.map(mapToQuestionDTOId);
}
export async function getJoinRequestsByClass(classId: string): Promise<ClassJoinRequestDTO[]> {
const classRepository: ClassRepository = getClassRepository();
const cls: Class | null = await classRepository.findById(classId);

View file

@ -0,0 +1,76 @@
import { setupTestApp } from '../setup-tests.js';
import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest';
import { Request, Response } from 'express';
import { getAssignmentHandler, getAllAssignmentsHandler, getAssignmentsSubmissionsHandler } from '../../src/controllers/assignments.js';
import { NotFoundException } from '../../src/exceptions/not-found-exception';
import { getClass01 } from '../test_assets/classes/classes.testdata';
import { getAssignment01 } from '../test_assets/assignments/assignments.testdata';
function createRequestObject(
classid: string,
assignmentid: string
): {
query: { full: string };
params: { classid: string; id: string };
} {
return {
params: {
classid: classid,
id: assignmentid,
},
query: {
full: 'true',
},
};
}
describe('Assignment controllers', () => {
let req: Partial<Request>;
let res: Partial<Response>;
let jsonMock: Mock;
let statusMock: Mock;
beforeAll(async () => {
await setupTestApp();
});
beforeEach(async () => {
jsonMock = vi.fn();
statusMock = vi.fn().mockReturnThis();
res = {
json: jsonMock,
status: statusMock,
};
});
it('return error non-existing assignment', async () => {
req = createRequestObject('doesnotexist', '43000'); // Should not exist
await expect(async () => getAssignmentHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
});
it('should return an assignment', async () => {
const assignment = getAssignment01();
req = createRequestObject(assignment.within.classId as string, (assignment.id ?? 1).toString());
await getAssignmentHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ assignment: expect.anything() }));
});
it('should return a list of assignments', async () => {
req = createRequestObject(getClass01().classId as string, 'irrelevant');
await getAllAssignmentsHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ assignments: expect.anything() }));
});
it('should return a list of submissions for an assignment', async () => {
const assignment = getAssignment01();
req = createRequestObject(assignment.within.classId as string, (assignment.id ?? 1).toString());
await getAssignmentsSubmissionsHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ submissions: expect.anything() }));
});
});

View file

@ -1,8 +1,17 @@
import { setupTestApp } from '../setup-tests.js';
import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest';
import {
createClassHandler,
deleteClassHandler,
getAllClassesHandler,
getClassHandler,
getClassStudentsHandler,
getTeacherInvitationsHandler,
} from '../../src/controllers/classes.js';
import { Request, Response } from 'express';
import { createClassHandler, deleteClassHandler } from '../../src/controllers/classes';
import { NotFoundException } from '../../src/exceptions/not-found-exception';
import { BadRequestException } from '../../src/exceptions/bad-request-exception';
import { getClass01 } from '../test_assets/classes/classes.testdata';
describe('Class controllers', () => {
let req: Partial<Request>;
let res: Partial<Response>;
@ -44,4 +53,71 @@ describe('Class controllers', () => {
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ class: expect.anything() }));
});
it('Error class not found', async () => {
req = {
params: { id: 'doesnotexist' },
};
await expect(async () => getClassHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
});
it('Error create a class without name', async () => {
req = {
body: {},
};
await expect(async () => createClassHandler(req as Request, res as Response)).rejects.toThrow(BadRequestException);
});
it('return list of students', async () => {
req = {
params: { id: getClass01().classId as string },
query: {},
};
await getClassStudentsHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ students: expect.anything() }));
});
it('Error students on a non-existent class', async () => {
req = {
params: { id: 'doesnotexist' },
query: {},
};
await expect(async () => getClassStudentsHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
});
it('should return 200 and a list of teacher-invitations', async () => {
const classId = getClass01().classId as string;
req = {
params: { id: classId },
query: {},
};
await getTeacherInvitationsHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ invitations: expect.anything() }));
});
it('Error teacher-invitations on a non-existent class', async () => {
req = {
params: { id: 'doesnotexist' },
query: {},
};
await expect(async () => getTeacherInvitationsHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
});
it('should return a list of classes', async () => {
req = {
query: {},
};
await getAllClassesHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ classes: expect.anything() }));
});
});

View file

@ -0,0 +1,140 @@
import { setupTestApp } from '../setup-tests.js';
import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest';
import { Request, Response } from 'express';
import {
createGroupHandler,
deleteGroupHandler,
getAllGroupsHandler,
getGroupHandler,
getGroupSubmissionsHandler,
} from '../../src/controllers/groups.js';
import { NotFoundException } from '../../src/exceptions/not-found-exception';
import { getClass01 } from '../test_assets/classes/classes.testdata';
import { getAssignment01, getAssignment02 } from '../test_assets/assignments/assignments.testdata';
import { getTestGroup01 } from '../test_assets/assignments/groups.testdata';
function createRequestObject(
classid: string,
assignmentid: string,
groupNumber: string
): {
query: { full: string };
params: { classid: string; groupid: string; assignmentid: string };
} {
return {
params: {
classid: classid,
assignmentid: assignmentid,
groupid: groupNumber,
},
query: {
full: 'true',
},
};
}
describe('Group controllers', () => {
let req: Partial<Request>;
let res: Partial<Response>;
let jsonMock: Mock;
let statusMock: Mock;
beforeAll(async () => {
await setupTestApp();
});
beforeEach(async () => {
jsonMock = vi.fn();
statusMock = vi.fn().mockReturnThis();
res = {
json: jsonMock,
status: statusMock,
};
});
it('Error not found on a non-existing group', async () => {
req = {
params: {
classid: 'id01',
assignmentid: '1',
groupid: '154981', // Should not exist
},
query: {},
};
await expect(async () => getGroupHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
});
it('should return 404 not found on a non-existing assignment', async () => {
req = {
params: {
classid: 'id01',
assignmentid: '1000', // Should not exist
groupid: '42000', // Should not exist
},
query: {},
};
await expect(async () => getGroupHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
});
it('should return 404 not found ont a non-existing class', async () => {
req = {
params: {
classid: 'doesnotexist', // Should not exist
assignmentid: '1000', // Should not exist
groupid: '42000', // Should not exist
},
query: {},
};
await expect(async () => getGroupHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
});
it('should return an existing group', async () => {
const group = getTestGroup01();
const classId = getClass01().classId as string;
req = createRequestObject(classId, (group.assignment.id ?? 1).toString(), (group.groupNumber ?? 1).toString());
await getGroupHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ group: expect.anything() }));
});
it('Create and delete', async () => {
const assignment = getAssignment02();
const classId = assignment.within.classId as string;
req = createRequestObject(classId, (assignment.id ?? 1).toString(), '1');
req.body = {
members: ['Noordkaap', 'DireStraits'],
};
await createGroupHandler(req as Request, res as Response);
await deleteGroupHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ group: expect.anything() }));
});
it('should return the submissions for a group', async () => {
const group = getTestGroup01();
const classId = getClass01().classId as string;
req = createRequestObject(classId, (group.assignment.id ?? 1).toString(), (group.groupNumber ?? 1).toString());
await getGroupSubmissionsHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ submissions: expect.anything() }));
});
it('should return a list of groups for an assignment', async () => {
const assignment = getAssignment01();
const classId = assignment.within.classId as string;
req = createRequestObject(classId, (assignment.id ?? 1).toString(), '1');
await getAllGroupsHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ groups: expect.anything() }));
});
});

View file

@ -0,0 +1,61 @@
import { setupTestApp } from '../setup-tests.js';
import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest';
import { getSubmissionHandler, getAllSubmissionsHandler } from '../../src/controllers/submissions.js';
import { Request, Response } from 'express';
import { NotFoundException } from '../../src/exceptions/not-found-exception';
import { getClass02 } from '../test_assets/classes/classes.testdata';
function createRequestObject(
hruid: string,
submissionNumber: string
): {
query: { language: string; version: string };
params: { hruid: string; id: string };
} {
return {
params: {
hruid: hruid,
id: submissionNumber,
},
query: {
language: 'en',
version: '1',
},
};
}
describe('Submission controllers', () => {
let req: Partial<Request>;
let res: Partial<Response>;
let jsonMock: Mock;
let statusMock: Mock;
beforeAll(async () => {
await setupTestApp();
});
beforeEach(async () => {
jsonMock = vi.fn();
statusMock = vi.fn().mockReturnThis();
res = {
json: jsonMock,
status: statusMock,
};
});
it('error submission is not found', async () => {
req = createRequestObject('id01', '1000000');
await expect(async () => getSubmissionHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
});
it('should return a list of submissions for a learning object', async () => {
req = createRequestObject(getClass02().classId as string, 'irrelevant');
await getAllSubmissionsHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ submissions: expect.anything() }));
});
});

View file

@ -6,13 +6,20 @@ import { testLearningPathWithConditions } from '../content/learning-paths.testda
import { getClassWithTestleerlingAndTestleerkracht } from '../classes/classes.testdata';
export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assignment[] {
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + 7);
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - 7);
const today = new Date();
today.setHours(23, 59);
assignment01 = em.create(Assignment, {
id: 21000,
within: classes[0],
title: 'dire straits',
description: 'reading',
learningPathHruid: 'id02',
learningPathHruid: 'un_ai',
learningPathLanguage: Language.English,
deadline: today,
groups: [],
});
@ -23,6 +30,7 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
description: 'reading',
learningPathHruid: 'id01',
learningPathLanguage: Language.English,
deadline: futureDate,
groups: [],
});
@ -33,6 +41,7 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
description: 'will be deleted',
learningPathHruid: 'id02',
learningPathLanguage: Language.English,
deadline: pastDate,
groups: [],
});
@ -43,6 +52,7 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
description: 'with a description',
learningPathHruid: 'id01',
learningPathLanguage: Language.English,
deadline: pastDate,
groups: [],
});
@ -53,6 +63,7 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
description: 'You have to do the testing learning path with a condition.',
learningPathHruid: testLearningPathWithConditions.hruid,
learningPathLanguage: testLearningPathWithConditions.language as Language,
deadline: futureDate,
groups: [],
});