diff --git a/server/__tests__/videoCall/videoCall.controller.test.ts b/server/__tests__/videoCall/videoCall.controller.test.ts new file mode 100644 index 0000000..93d2bd6 --- /dev/null +++ b/server/__tests__/videoCall/videoCall.controller.test.ts @@ -0,0 +1,212 @@ +import { describe, it, expect, beforeEach, jest } from "@jest/globals"; +import { VideoCallController } from "../../src/controllers/videoCall.controller.js"; +import { NextFunction, Response } from "express"; + +type MockResponse = { + status: jest.Mock; + json: jest.Mock; + sendStatus: jest.Mock; +}; + +type MockRequest = { + body?: Record; + params?: Record; + auth?: { + userId?: string; + role?: string; + }; +}; + +const mockVideoCallService = { + startCall: jest.fn<() => Promise>(), + incomingCall: jest.fn<() => Promise>(), + acceptCall: jest.fn<() => Promise>(), + declineCall: jest.fn<() => Promise>(), + endCall: jest.fn<() => Promise>(), +}; + +describe("VideoCallController", () => { + let controller: VideoCallController; + let mockResponse: MockResponse; + let mockNext: NextFunction; + + beforeEach(() => { + controller = new VideoCallController( + mockVideoCallService as unknown as VideoCallController["videoCallService"], + ); + mockResponse = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + sendStatus: jest.fn().mockReturnThis(), + }; + mockNext = jest.fn(); + jest.clearAllMocks(); + }); + + describe("startVideoCallController", () => { + it("should start video call successfully", async () => { + const req: MockRequest = { + body: { + teacherId: "t1", + studentId: "s1", + streamCallId: "stream1", + expiresAt: new Date(), + }, + auth: { userId: "t1", role: "teacher" }, + }; + + const mockCall = { id: "call1", status: "ringing" }; + mockVideoCallService.startCall.mockResolvedValue(mockCall); + + await controller.startVideoCallController( + req as never, + mockResponse as unknown as Response, + mockNext, + ); + + expect(mockResponse.status).toHaveBeenCalledWith(201); + expect(mockResponse.json).toHaveBeenCalledWith(mockCall); + }); + + it("should return 401 if no auth", async () => { + const req: MockRequest = { body: {}, auth: undefined }; + await controller.startVideoCallController( + req as never, + mockResponse as unknown as Response, + mockNext, + ); + expect(mockResponse.sendStatus).toHaveBeenCalledWith(401); + }); + + it("should return 403 for invalid role", async () => { + const req: MockRequest = { + body: {}, + auth: { userId: "u1", role: "admin" }, + }; + await controller.startVideoCallController( + req as never, + mockResponse as unknown as Response, + mockNext, + ); + expect(mockResponse.sendStatus).toHaveBeenCalledWith(403); + }); + }); + + describe("incomingCallController", () => { + it("should get incoming call for student", async () => { + const req: MockRequest = { auth: { userId: "s1", role: "student" } }; + const mockCall = { id: "call1", status: "ringing" }; + mockVideoCallService.incomingCall.mockResolvedValue(mockCall); + + await controller.incomingCallController( + req as never, + mockResponse as unknown as Response, + mockNext, + ); + + expect(mockResponse.status).toHaveBeenCalledWith(200); + expect(mockResponse.json).toHaveBeenCalledWith(mockCall); + }); + + it("should return 403 for invalid role", async () => { + const req: MockRequest = { auth: { userId: "u1", role: "admin" } }; + await controller.incomingCallController( + req as never, + mockResponse as unknown as Response, + mockNext, + ); + expect(mockResponse.status).toHaveBeenCalledWith(403); + }); + }); + + describe("acceptCallController", () => { + it("should accept call successfully", async () => { + const req: MockRequest = { + params: { callId: "call1" }, + auth: { userId: "s1", role: "student" }, + }; + const mockCall = { id: "call1", status: "accepted" }; + mockVideoCallService.acceptCall.mockResolvedValue(mockCall); + + await controller.acceptCallController( + req as never, + mockResponse as unknown as Response, + mockNext, + ); + + expect(mockResponse.status).toHaveBeenCalledWith(200); + expect(mockResponse.json).toHaveBeenCalledWith(mockCall); + }); + + it("should return 404 if call not found", async () => { + const req: MockRequest = { + params: { callId: "call1" }, + auth: { userId: "s1", role: "student" }, + }; + mockVideoCallService.acceptCall.mockResolvedValue(null); + + await controller.acceptCallController( + req as never, + mockResponse as unknown as Response, + mockNext, + ); + expect(mockResponse.status).toHaveBeenCalledWith(404); + }); + }); + + describe("declineCallController", () => { + it("should decline call successfully", async () => { + const req: MockRequest = { + params: { callId: "call1" }, + auth: { userId: "s1", role: "student" }, + }; + const mockCall = { id: "call1", status: "declined" }; + mockVideoCallService.declineCall.mockResolvedValue(mockCall); + + await controller.declineCallController( + req as never, + mockResponse as unknown as Response, + mockNext, + ); + + expect(mockResponse.status).toHaveBeenCalledWith(200); + expect(mockResponse.json).toHaveBeenCalledWith(mockCall); + }); + }); + + describe("endCallController", () => { + it("should end call successfully", async () => { + const req: MockRequest = { + params: { callId: "call1" }, + auth: { userId: "t1", role: "teacher" }, + }; + const mockCall = { id: "call1", status: "ended" }; + mockVideoCallService.endCall.mockResolvedValue(mockCall); + + await controller.endCallController( + req as never, + mockResponse as unknown as Response, + mockNext, + ); + + expect(mockResponse.status).toHaveBeenCalledWith(200); + expect(mockResponse.json).toHaveBeenCalledWith(mockCall); + }); + + it("should handle service errors", async () => { + const req: MockRequest = { + params: { callId: "call1" }, + auth: { userId: "t1" }, + }; + const error = new Error("Service error"); + mockVideoCallService.endCall.mockRejectedValue(error); + + await controller.endCallController( + req as never, + mockResponse as unknown as Response, + mockNext, + ); + expect(mockNext).toHaveBeenCalledWith(error); + }); + }); +}); diff --git a/server/__tests__/videoCall/videoCall.service.test.ts b/server/__tests__/videoCall/videoCall.service.test.ts new file mode 100644 index 0000000..4633252 --- /dev/null +++ b/server/__tests__/videoCall/videoCall.service.test.ts @@ -0,0 +1,159 @@ +import { describe, it, expect, beforeEach, jest } from "@jest/globals"; +import { VideoCallService } from "../../src/services/video/videoCall.service.js"; +import { VideoCallCommand } from "../../src/repositories/commandRepositories/videoCall.command.js"; +import { VideoCallQuery } from "../../src/repositories/queryRepositories/videoCall.query.js"; +import { HttpError } from "../../src/utils/error.util.js"; +import { StartVideoCallInput } from "../../src/types/video/video.types.js"; + +const mockVideoCallCommand = { + startVideoCall: jest.fn<() => Promise>(), + acceptCallById: jest.fn<() => Promise>(), + declineCallById: jest.fn<() => Promise>(), + endCallById: jest.fn<() => Promise>(), + markExpiredCallAsMissed: jest.fn<() => Promise>(), +}; + +const mockVideoCallQuery = { + getVideoByStreamCallId: jest.fn<() => Promise>(), + getIncomingCallForStudent: jest.fn<() => Promise>(), + getVideoById: jest.fn<() => Promise>(), +}; + +jest.mock("../../src/db/schemes/studentSchema.js", () => ({ + StudentModel: {}, +})); +jest.mock("../../src/db/schemes/teacherSchema.js", () => ({ + TeacherModel: {}, +})); +jest.mock("../../src/db/schemes/videoCallSchema.js", () => ({ + VideoCallModel: {}, +})); +jest.mock("../../src/socket/io.holder.js", () => ({ getIO: () => null })); + +describe("VideoCallService", () => { + let service: VideoCallService; + + beforeEach(() => { + service = new VideoCallService( + mockVideoCallCommand as unknown as VideoCallCommand, + mockVideoCallQuery as unknown as VideoCallQuery, + ); + jest.clearAllMocks(); + }); + + describe("startCall", () => { + it("should throw error if not teacher", async () => { + const input: StartVideoCallInput = { + authRole: "student", + authUserId: "s1", + teacherId: "t1", + studentId: "s1", + streamCallId: "stream1", + expiresAt: new Date(), + }; + await expect(service.startCall(input)).rejects.toThrow( + new HttpError(403, "Only teachers can start calls"), + ); + }); + + it("should throw error if call already exists", async () => { + const input: StartVideoCallInput = { + authRole: "teacher", + authUserId: "t1", + teacherId: "t1", + studentId: "s1", + streamCallId: "stream1", + expiresAt: new Date(), + }; + mockVideoCallQuery.getVideoByStreamCallId.mockResolvedValue({ + id: "existing", + }); + await expect(service.startCall(input)).rejects.toThrow( + new HttpError(409, "This call already exists"), + ); + }); + }); + + describe("incomingCall", () => { + it("should return incoming call for student", async () => { + const mockCall = { + id: "call1", + expiresAt: new Date(Date.now() + 60000), + }; + mockVideoCallQuery.getIncomingCallForStudent.mockResolvedValue(mockCall); + + const result = await service.incomingCall({ + authUserId: "s1", + authRole: "student", + }); + expect(result).toEqual(mockCall); + }); + + it("should throw error if not student", async () => { + await expect( + service.incomingCall({ authUserId: "t1", authRole: "teacher" }), + ).rejects.toThrow( + new HttpError(403, "This endpoint is for students only"), + ); + }); + }); + + describe("acceptCall", () => { + it("should throw error if not student", async () => { + await expect( + service.acceptCall({ + callId: "call1", + authUserId: "t1", + authRole: "teacher", + }), + ).rejects.toThrow( + new HttpError(403, "Students only can accept the call"), + ); + }); + + it("should throw error if call not found", async () => { + mockVideoCallQuery.getVideoById.mockResolvedValue(null); + await expect( + service.acceptCall({ + callId: "call1", + authUserId: "s1", + authRole: "student", + }), + ).rejects.toThrow(new HttpError(404, "Call not found")); + }); + }); + + describe("declineCall", () => { + it("should throw error if not student", async () => { + await expect( + service.declineCall({ + callId: "call1", + authUserId: "t1", + authRole: "teacher", + }), + ).rejects.toThrow( + new HttpError(403, "Students only can decline the call"), + ); + }); + }); + + describe("endCall", () => { + it("should throw error if call not found", async () => { + mockVideoCallQuery.getVideoById.mockResolvedValue(null); + await expect( + service.endCall({ callId: "call1", authUserId: "t1" }), + ).rejects.toThrow(new HttpError(404, "Call not found")); + }); + + it("should throw error if user not participant", async () => { + const mockCall = { id: "call1", teacherId: "t1", studentId: "s1" }; + mockVideoCallQuery.getVideoById.mockResolvedValue(mockCall); + + await expect( + service.endCall({ callId: "call1", authUserId: "other" }), + ).rejects.toThrow( + new HttpError(403, "Only participants can end the call"), + ); + }); + }); +});