feat: class join req controller + fixes tests

This commit is contained in:
Gabriellvl 2025-03-29 15:09:57 +01:00
parent 3093a6c131
commit f679a324ab
11 changed files with 116 additions and 41 deletions

View file

@ -122,7 +122,7 @@ export async function createStudentRequestHandler(req: Request, res: Response):
requireFields({ username, classId }); requireFields({ username, classId });
await createClassJoinRequest(username, classId); await createClassJoinRequest(username, classId);
res.status(201).send(); res.status(201);
} }
export async function getStudentRequestHandler(req: Request, res: Response): Promise<void> { export async function getStudentRequestHandler(req: Request, res: Response): Promise<void> {
@ -144,12 +144,12 @@ export async function updateClassJoinRequestHandler(req: Request, res: Response)
} }
export async function deleteClassJoinRequestHandler(req: Request, res: Response) { export async function deleteClassJoinRequestHandler(req: Request, res: Response) {
const username = req.query.username as string; const username = req.params.username as string;
const classId = req.params.classId; const classId = req.params.classId;
requireFields({ username, classId }); requireFields({ username, classId });
await deleteClassJoinRequest(username, classId); await deleteClassJoinRequest(username, classId);
res.status(204).send(); res.status(204);
} }

View file

@ -10,6 +10,9 @@ export class ClassJoinRequestRepository extends DwengoEntityRepository<ClassJoin
public findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> { public findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> {
return this.findAll({ where: { class: clazz } }); return this.findAll({ where: { class: clazz } });
} }
public findByStudentAndClass(requester: Student, clazz: Class): Promise<ClassJoinRequest | null> {
return this.findOne({ requester, class: clazz });
}
public deleteBy(requester: Student, clazz: Class): Promise<void> { public deleteBy(requester: Student, clazz: Class): Promise<void> {
return this.deleteWhere({ requester: requester, class: clazz }); return this.deleteWhere({ requester: requester, class: clazz });
} }

View file

@ -2,7 +2,6 @@ import express from "express";
import { import {
createStudentRequestHandler, deleteClassJoinRequestHandler, createStudentRequestHandler, deleteClassJoinRequestHandler,
getStudentRequestHandler, getStudentRequestHandler,
updateClassJoinRequestHandler
} from "../controllers/students"; } from "../controllers/students";
const router = express.Router({ mergeParams: true }); const router = express.Router({ mergeParams: true });
@ -11,8 +10,6 @@ router.get('/', getStudentRequestHandler);
router.post('/:classId', createStudentRequestHandler); router.post('/:classId', createStudentRequestHandler);
router.put('/:classId', updateClassJoinRequestHandler);
router.delete('/:classId', deleteClassJoinRequestHandler); router.delete('/:classId', deleteClassJoinRequestHandler);
export default router; export default router;

View file

@ -39,6 +39,6 @@ router.get('/:username/groups', getStudentGroupsHandler);
// A list of questions a user has created // A list of questions a user has created
router.get('/:username/questions', getStudentQuestionsHandler); router.get('/:username/questions', getStudentQuestionsHandler);
router.use('/:username/join-requests', joinRequestRouter) router.use('/:username/joinRequests', joinRequestRouter)
export default router; export default router;

View file

@ -140,6 +140,12 @@ export async function createClassJoinRequest(studentUsername: string, classId: s
throw new NotFoundException("Class with id not found"); throw new NotFoundException("Class with id not found");
} }
const req = await requestRepo.findByStudentAndClass(student, cls);
if (req){
throw new ConflictException("Request with student and class already exist");
}
const request = requestRepo.create({ const request = requestRepo.create({
requester: student, requester: student,
class: cls, class: cls,
@ -159,6 +165,7 @@ export async function getJoinRequestsByStudent(studentUsername: string) {
return requests.map(mapToStudentRequestDTO); return requests.map(mapToStudentRequestDTO);
} }
// TODO naar teacher
export async function updateClassJoinRequestStatus( studentUsername: string, classId: string, accepted: boolean = true) { export async function updateClassJoinRequestStatus( studentUsername: string, classId: string, accepted: boolean = true) {
const requestRepo = getClassJoinRequestRepository(); const requestRepo = getClassJoinRequestRepository();
const classRepo = getClassRepository(); const classRepo = getClassRepository();
@ -193,7 +200,7 @@ export async function deleteClassJoinRequest(studentUsername: string, classId: s
throw new NotFoundException('Class not found'); throw new NotFoundException('Class not found');
} }
const request = await requestRepo.findOne({ requester: student, class: cls }); const request = await requestRepo.findByStudentAndClass(student, cls);
if (!request) { if (!request) {
throw new NotFoundException('Join request not found'); throw new NotFoundException('Join request not found');

View file

@ -16,7 +16,7 @@ import {
deleteClassJoinRequestHandler deleteClassJoinRequestHandler
} from '../../src/controllers/students.js'; } from '../../src/controllers/students.js';
import {TEST_STUDENTS} from "../test_assets/users/students.testdata"; import {TEST_STUDENTS} from "../test_assets/users/students.testdata";
import {BadRequestException, NotFoundException} from "../../src/exceptions"; import {BadRequestException, ConflictException, NotFoundException} from "../../src/exceptions";
describe('Student controllers', () => { describe('Student controllers', () => {
let req: Partial<Request>; let req: Partial<Request>;
@ -71,7 +71,21 @@ describe('Student controllers', () => {
// TODO create duplicate student id // TODO create duplicate student id
it('Create student no body 400', async () => { it('Create duplicate student', async () => {
req = {
body: {
username: 'DireStraits',
firstName: 'dupe',
lastName: 'dupe'
}
};
await expect(() => createStudentHandler(req as Request, res as Response))
.rejects
.toThrowError(ConflictException);
});
it('Create student no body', async () => {
req = { body: {} }; req = { body: {} };
await expect(() => createStudentHandler(req as Request, res as Response)) await expect(() => createStudentHandler(req as Request, res as Response))
@ -179,44 +193,37 @@ describe('Student controllers', () => {
it('Create join request', async () => { it('Create join request', async () => {
req = { req = {
params: { username: 'DireStraits', classId: '' }, params: { username: 'Noordkaap', classId: 'id02' },
}; };
await createStudentRequestHandler(req as Request, res as Response); await createStudentRequestHandler(req as Request, res as Response);
expect(statusMock).toHaveBeenCalledWith(201); expect(statusMock).toHaveBeenCalledWith(201);
expect(jsonMock).toHaveBeenCalled();
}); });
/* it('Create join request duplicate', async () => {
it('Update join request status (accept)', async () => {
req = { req = {
params: { classId }, params: { username: 'Tool', classId: 'id02' },
query: { username },
}; };
await updateClassJoinRequestHandler(req as Request, res as Response); await expect(() => createStudentRequestHandler(req as Request, res as Response))
.rejects
expect(statusMock).toHaveBeenCalledWith(200); .toThrow(ConflictException);
expect(jsonMock).toHaveBeenCalled();
const result = jsonMock.mock.lastCall?.[0];
console.log('[UPDATED REQUEST]', result);
}); });
it('Delete join request', async () => { it('Delete join request', async () => {
req = { req = {
params: { classId }, params: { username: 'Noordkaap', classId: 'id02' },
query: { username },
}; };
await deleteClassJoinRequestHandler(req as Request, res as Response); await deleteClassJoinRequestHandler(req as Request, res as Response);
expect(statusMock).toHaveBeenCalledWith(204); expect(statusMock).toHaveBeenCalledWith(204);
expect(sendMock).toHaveBeenCalled();
await expect(() => deleteClassJoinRequestHandler(req as Request, res as Response))
.rejects
.toThrow(NotFoundException);
}); });
*/
}); });

View file

@ -28,12 +28,17 @@ export class BaseController {
return res.json(); return res.json();
} }
protected async post<T>(path: string, body: unknown): Promise<T> { protected async post<T>(path: string, body?: unknown): Promise<T> {
const res = await fetch(`${this.baseUrl}${path}`, { const options: RequestInit = {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(body), };
});
if (body !== undefined) {
options.body = JSON.stringify(body);
}
const res = await fetch(`${this.baseUrl}${path}`, options);
if (!res.ok) { if (!res.ok) {
const errorData = await res.json().catch(() => ({})); const errorData = await res.json().catch(() => ({}));

View file

@ -2,7 +2,7 @@ import { BaseController } from "@/controllers/base-controller.ts";
export class StudentController extends BaseController { export class StudentController extends BaseController {
constructor() { constructor() {
super("students"); super("student");
} }
getAll(full = true) { getAll(full = true) {
@ -10,15 +10,15 @@ export class StudentController extends BaseController {
} }
getByUsername(username: string) { getByUsername(username: string) {
return this.get<any>(`/${username}`); return this.get<{ student: any }>(`/${username}`);
} }
createStudent(data: any) { createStudent(data: any) {
return this.post<any>("/", data); return this.post<{ student: any }>("/", data);
} }
deleteStudent(username: string) { deleteStudent(username: string) {
return this.delete<any>(`/${username}`); return this.delete<{ student: any }>(`/${username}`);
} }
getClasses(username: string, full = true) { getClasses(username: string, full = true) {
@ -40,4 +40,16 @@ export class StudentController extends BaseController {
getQuestions(username: string, full = true) { getQuestions(username: string, full = true) {
return this.get<{ questions: any[] }>(`/${username}/questions`, { full }); return this.get<{ questions: any[] }>(`/${username}/questions`, { full });
} }
getJoinRequests(username: string) {
return this.get<{ requests: any[] }>(`/${username}/joinRequests`);
}
createJoinRequest(username: string, classId: string) {
return this.post<any>(`/${username}/joinRequests/${classId}`);
}
deleteJoinRequest(username: string, classId: string) {
return this.delete<any>(`/${username}/joinRequests/${classId}`);
}
} }

View file

@ -2,7 +2,7 @@ import { BaseController } from "@/controllers/base-controller.ts";
export class TeacherController extends BaseController { export class TeacherController extends BaseController {
constructor() { constructor() {
super("teachers"); super("teacher");
} }
getAll(full = false) { getAll(full = false) {

View file

@ -1,6 +1,6 @@
import { computed, toValue } from "vue"; import { computed, toValue } from "vue";
import type { MaybeRefOrGetter } from "vue"; import type { MaybeRefOrGetter } from "vue";
import { useQuery } from "@tanstack/vue-query"; import {useMutation, useQuery, useQueryClient} from "@tanstack/vue-query";
import { getStudentController } from "@/controllers/controllers.ts"; import { getStudentController } from "@/controllers/controllers.ts";
const studentController = getStudentController(); const studentController = getStudentController();
@ -75,7 +75,7 @@ export function useCreateStudentMutation() {
return useMutation({ return useMutation({
mutationFn: (data: any) => studentController.createStudent(data), mutationFn: (data: any) => studentController.createStudent(data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['students'] }); await queryClient.invalidateQueries({ queryKey: ['students'] });
}, },
onError: (err) => { onError: (err) => {
alert("Create student failed:", err); alert("Create student failed:", err);
@ -92,10 +92,13 @@ export function useDeleteStudentMutation() {
return useMutation({ return useMutation({
mutationFn: (username: string) => studentController.deleteStudent(username), mutationFn: (username: string) => studentController.deleteStudent(username),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['students'] }); await queryClient.invalidateQueries({ queryKey: ['students'] });
}, },
onError: (err) => { onError: (err) => {
alert("Delete student failed:", err); alert("Delete student failed:", err);
}, },
}); });
} }

View file

@ -0,0 +1,41 @@
import { describe, it, expect, beforeAll } from 'vitest';
import {getStudentController} from "../../src/controllers/controllers";
const controller = getStudentController();
describe('StudentController', () => {
const newStudent = {
username: 'TestStudent',
firstName: 'Testy',
lastName: 'McTestface',
};
beforeAll(() => {
// Zet eventueel mock server op hier als je dat gebruikt
});
it('creates a student and fetches it by username', async () => {
// Create student
const created = await controller.createStudent(newStudent);
expect(created).toBeDefined();
expect(created.username).toBe(newStudent.username);
// Fetch same student
const fetched = await controller.getByUsername(newStudent.username);
expect(fetched).toBeDefined();
expect(fetched.student).toBeDefined();
const student = fetched.student;
expect(student.username).toBe(newStudent.username);
expect(student.firstName).toBe(newStudent.firstName);
expect(student.lastName).toBe(newStudent.lastName);
await controller.deleteStudent(newStudent.username);
await expect(controller.getByUsername(newStudent.username)).rejects.toThrow();
});
});