diff --git a/backend/src/controllers/students.ts b/backend/src/controllers/students.ts index 9a60ac7b..1fbfeec3 100644 --- a/backend/src/controllers/students.ts +++ b/backend/src/controllers/students.ts @@ -122,7 +122,7 @@ export async function createStudentRequestHandler(req: Request, res: Response): requireFields({ username, classId }); await createClassJoinRequest(username, classId); - res.status(201).send(); + res.status(201); } export async function getStudentRequestHandler(req: Request, res: Response): Promise { @@ -144,12 +144,12 @@ export async function updateClassJoinRequestHandler(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; requireFields({ username, classId }); await deleteClassJoinRequest(username, classId); - res.status(204).send(); + res.status(204); } diff --git a/backend/src/data/classes/class-join-request-repository.ts b/backend/src/data/classes/class-join-request-repository.ts index c1443c1c..04152a26 100644 --- a/backend/src/data/classes/class-join-request-repository.ts +++ b/backend/src/data/classes/class-join-request-repository.ts @@ -10,6 +10,9 @@ export class ClassJoinRequestRepository extends DwengoEntityRepository { return this.findAll({ where: { class: clazz } }); } + public findByStudentAndClass(requester: Student, clazz: Class): Promise { + return this.findOne({ requester, class: clazz }); + } public deleteBy(requester: Student, clazz: Class): Promise { return this.deleteWhere({ requester: requester, class: clazz }); } diff --git a/backend/src/routes/student-join-requests.ts b/backend/src/routes/student-join-requests.ts index 45d20f91..7147fd95 100644 --- a/backend/src/routes/student-join-requests.ts +++ b/backend/src/routes/student-join-requests.ts @@ -2,7 +2,6 @@ import express from "express"; import { createStudentRequestHandler, deleteClassJoinRequestHandler, getStudentRequestHandler, - updateClassJoinRequestHandler } from "../controllers/students"; const router = express.Router({ mergeParams: true }); @@ -11,8 +10,6 @@ router.get('/', getStudentRequestHandler); router.post('/:classId', createStudentRequestHandler); -router.put('/:classId', updateClassJoinRequestHandler); - router.delete('/:classId', deleteClassJoinRequestHandler); export default router; diff --git a/backend/src/routes/students.ts b/backend/src/routes/students.ts index d443abd9..b2d0fc93 100644 --- a/backend/src/routes/students.ts +++ b/backend/src/routes/students.ts @@ -39,6 +39,6 @@ router.get('/:username/groups', getStudentGroupsHandler); // A list of questions a user has created router.get('/:username/questions', getStudentQuestionsHandler); -router.use('/:username/join-requests', joinRequestRouter) +router.use('/:username/joinRequests', joinRequestRouter) export default router; diff --git a/backend/src/services/students.ts b/backend/src/services/students.ts index 14816298..9360ed54 100644 --- a/backend/src/services/students.ts +++ b/backend/src/services/students.ts @@ -140,6 +140,12 @@ export async function createClassJoinRequest(studentUsername: string, classId: s 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({ requester: student, class: cls, @@ -159,6 +165,7 @@ export async function getJoinRequestsByStudent(studentUsername: string) { return requests.map(mapToStudentRequestDTO); } +// TODO naar teacher export async function updateClassJoinRequestStatus( studentUsername: string, classId: string, accepted: boolean = true) { const requestRepo = getClassJoinRequestRepository(); const classRepo = getClassRepository(); @@ -193,7 +200,7 @@ export async function deleteClassJoinRequest(studentUsername: string, classId: s throw new NotFoundException('Class not found'); } - const request = await requestRepo.findOne({ requester: student, class: cls }); + const request = await requestRepo.findByStudentAndClass(student, cls); if (!request) { throw new NotFoundException('Join request not found'); diff --git a/backend/tests/controllers/student.test.ts b/backend/tests/controllers/student.test.ts index 89d146de..4dfa12c1 100644 --- a/backend/tests/controllers/student.test.ts +++ b/backend/tests/controllers/student.test.ts @@ -16,7 +16,7 @@ import { deleteClassJoinRequestHandler } from '../../src/controllers/students.js'; 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', () => { let req: Partial; @@ -71,7 +71,21 @@ describe('Student controllers', () => { // 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: {} }; await expect(() => createStudentHandler(req as Request, res as Response)) @@ -179,44 +193,37 @@ describe('Student controllers', () => { it('Create join request', async () => { req = { - params: { username: 'DireStraits', classId: '' }, + params: { username: 'Noordkaap', classId: 'id02' }, }; await createStudentRequestHandler(req as Request, res as Response); expect(statusMock).toHaveBeenCalledWith(201); - expect(jsonMock).toHaveBeenCalled(); }); - /* - - it('Update join request status (accept)', async () => { + it('Create join request duplicate', async () => { req = { - params: { classId }, - query: { username }, + params: { username: 'Tool', classId: 'id02' }, }; - await updateClassJoinRequestHandler(req as Request, res as Response); - - expect(statusMock).toHaveBeenCalledWith(200); - expect(jsonMock).toHaveBeenCalled(); - const result = jsonMock.mock.lastCall?.[0]; - console.log('[UPDATED REQUEST]', result); + await expect(() => createStudentRequestHandler(req as Request, res as Response)) + .rejects + .toThrow(ConflictException); }); + it('Delete join request', async () => { req = { - params: { classId }, - query: { username }, + params: { username: 'Noordkaap', classId: 'id02' }, }; await deleteClassJoinRequestHandler(req as Request, res as Response); expect(statusMock).toHaveBeenCalledWith(204); - expect(sendMock).toHaveBeenCalled(); + + await expect(() => deleteClassJoinRequestHandler(req as Request, res as Response)) + .rejects + .toThrow(NotFoundException); }); - */ - - }); diff --git a/frontend/src/controllers/base-controller.ts b/frontend/src/controllers/base-controller.ts index adc0c8c0..a18edce0 100644 --- a/frontend/src/controllers/base-controller.ts +++ b/frontend/src/controllers/base-controller.ts @@ -28,12 +28,17 @@ export class BaseController { return res.json(); } - protected async post(path: string, body: unknown): Promise { - const res = await fetch(`${this.baseUrl}${path}`, { + protected async post(path: string, body?: unknown): Promise { + const options: RequestInit = { method: "POST", 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) { const errorData = await res.json().catch(() => ({})); diff --git a/frontend/src/controllers/students.ts b/frontend/src/controllers/students.ts index 38cfef89..0c00fdc2 100644 --- a/frontend/src/controllers/students.ts +++ b/frontend/src/controllers/students.ts @@ -2,7 +2,7 @@ import { BaseController } from "@/controllers/base-controller.ts"; export class StudentController extends BaseController { constructor() { - super("students"); + super("student"); } getAll(full = true) { @@ -10,15 +10,15 @@ export class StudentController extends BaseController { } getByUsername(username: string) { - return this.get(`/${username}`); + return this.get<{ student: any }>(`/${username}`); } createStudent(data: any) { - return this.post("/", data); + return this.post<{ student: any }>("/", data); } deleteStudent(username: string) { - return this.delete(`/${username}`); + return this.delete<{ student: any }>(`/${username}`); } getClasses(username: string, full = true) { @@ -40,4 +40,16 @@ export class StudentController extends BaseController { getQuestions(username: string, full = true) { 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(`/${username}/joinRequests/${classId}`); + } + + deleteJoinRequest(username: string, classId: string) { + return this.delete(`/${username}/joinRequests/${classId}`); + } } diff --git a/frontend/src/controllers/teachers.ts b/frontend/src/controllers/teachers.ts index e4f34027..b6ed482a 100644 --- a/frontend/src/controllers/teachers.ts +++ b/frontend/src/controllers/teachers.ts @@ -2,7 +2,7 @@ import { BaseController } from "@/controllers/base-controller.ts"; export class TeacherController extends BaseController { constructor() { - super("teachers"); + super("teacher"); } getAll(full = false) { diff --git a/frontend/src/queries/students.ts b/frontend/src/queries/students.ts index 40304dcf..15264191 100644 --- a/frontend/src/queries/students.ts +++ b/frontend/src/queries/students.ts @@ -1,6 +1,6 @@ import { computed, toValue } 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"; const studentController = getStudentController(); @@ -75,7 +75,7 @@ export function useCreateStudentMutation() { return useMutation({ mutationFn: (data: any) => studentController.createStudent(data), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['students'] }); + await queryClient.invalidateQueries({ queryKey: ['students'] }); }, onError: (err) => { alert("Create student failed:", err); @@ -92,10 +92,13 @@ export function useDeleteStudentMutation() { return useMutation({ mutationFn: (username: string) => studentController.deleteStudent(username), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['students'] }); + await queryClient.invalidateQueries({ queryKey: ['students'] }); }, onError: (err) => { alert("Delete student failed:", err); }, }); } + + + diff --git a/frontend/tests/controllers/student.test.ts b/frontend/tests/controllers/student.test.ts new file mode 100644 index 00000000..85e64170 --- /dev/null +++ b/frontend/tests/controllers/student.test.ts @@ -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(); + + }); +});