Skip to content

Commit 21afa4d

Browse files
authored
Merge pull request #159 from HackYourFutureProjects/QA/video
qa: videocall
2 parents 194e29e + bc01851 commit 21afa4d

2 files changed

Lines changed: 371 additions & 0 deletions

File tree

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import { describe, it, expect, beforeEach, jest } from "@jest/globals";
2+
import { VideoCallController } from "../../src/controllers/videoCall.controller.js";
3+
import { NextFunction, Response } from "express";
4+
5+
type MockResponse = {
6+
status: jest.Mock;
7+
json: jest.Mock;
8+
sendStatus: jest.Mock;
9+
};
10+
11+
type MockRequest = {
12+
body?: Record<string, unknown>;
13+
params?: Record<string, string>;
14+
auth?: {
15+
userId?: string;
16+
role?: string;
17+
};
18+
};
19+
20+
const mockVideoCallService = {
21+
startCall: jest.fn<() => Promise<unknown>>(),
22+
incomingCall: jest.fn<() => Promise<unknown>>(),
23+
acceptCall: jest.fn<() => Promise<unknown>>(),
24+
declineCall: jest.fn<() => Promise<unknown>>(),
25+
endCall: jest.fn<() => Promise<unknown>>(),
26+
};
27+
28+
describe("VideoCallController", () => {
29+
let controller: VideoCallController;
30+
let mockResponse: MockResponse;
31+
let mockNext: NextFunction;
32+
33+
beforeEach(() => {
34+
controller = new VideoCallController(
35+
mockVideoCallService as unknown as VideoCallController["videoCallService"],
36+
);
37+
mockResponse = {
38+
status: jest.fn().mockReturnThis(),
39+
json: jest.fn().mockReturnThis(),
40+
sendStatus: jest.fn().mockReturnThis(),
41+
};
42+
mockNext = jest.fn();
43+
jest.clearAllMocks();
44+
});
45+
46+
describe("startVideoCallController", () => {
47+
it("should start video call successfully", async () => {
48+
const req: MockRequest = {
49+
body: {
50+
teacherId: "t1",
51+
studentId: "s1",
52+
streamCallId: "stream1",
53+
expiresAt: new Date(),
54+
},
55+
auth: { userId: "t1", role: "teacher" },
56+
};
57+
58+
const mockCall = { id: "call1", status: "ringing" };
59+
mockVideoCallService.startCall.mockResolvedValue(mockCall);
60+
61+
await controller.startVideoCallController(
62+
req as never,
63+
mockResponse as unknown as Response,
64+
mockNext,
65+
);
66+
67+
expect(mockResponse.status).toHaveBeenCalledWith(201);
68+
expect(mockResponse.json).toHaveBeenCalledWith(mockCall);
69+
});
70+
71+
it("should return 401 if no auth", async () => {
72+
const req: MockRequest = { body: {}, auth: undefined };
73+
await controller.startVideoCallController(
74+
req as never,
75+
mockResponse as unknown as Response,
76+
mockNext,
77+
);
78+
expect(mockResponse.sendStatus).toHaveBeenCalledWith(401);
79+
});
80+
81+
it("should return 403 for invalid role", async () => {
82+
const req: MockRequest = {
83+
body: {},
84+
auth: { userId: "u1", role: "admin" },
85+
};
86+
await controller.startVideoCallController(
87+
req as never,
88+
mockResponse as unknown as Response,
89+
mockNext,
90+
);
91+
expect(mockResponse.sendStatus).toHaveBeenCalledWith(403);
92+
});
93+
});
94+
95+
describe("incomingCallController", () => {
96+
it("should get incoming call for student", async () => {
97+
const req: MockRequest = { auth: { userId: "s1", role: "student" } };
98+
const mockCall = { id: "call1", status: "ringing" };
99+
mockVideoCallService.incomingCall.mockResolvedValue(mockCall);
100+
101+
await controller.incomingCallController(
102+
req as never,
103+
mockResponse as unknown as Response,
104+
mockNext,
105+
);
106+
107+
expect(mockResponse.status).toHaveBeenCalledWith(200);
108+
expect(mockResponse.json).toHaveBeenCalledWith(mockCall);
109+
});
110+
111+
it("should return 403 for invalid role", async () => {
112+
const req: MockRequest = { auth: { userId: "u1", role: "admin" } };
113+
await controller.incomingCallController(
114+
req as never,
115+
mockResponse as unknown as Response,
116+
mockNext,
117+
);
118+
expect(mockResponse.status).toHaveBeenCalledWith(403);
119+
});
120+
});
121+
122+
describe("acceptCallController", () => {
123+
it("should accept call successfully", async () => {
124+
const req: MockRequest = {
125+
params: { callId: "call1" },
126+
auth: { userId: "s1", role: "student" },
127+
};
128+
const mockCall = { id: "call1", status: "accepted" };
129+
mockVideoCallService.acceptCall.mockResolvedValue(mockCall);
130+
131+
await controller.acceptCallController(
132+
req as never,
133+
mockResponse as unknown as Response,
134+
mockNext,
135+
);
136+
137+
expect(mockResponse.status).toHaveBeenCalledWith(200);
138+
expect(mockResponse.json).toHaveBeenCalledWith(mockCall);
139+
});
140+
141+
it("should return 404 if call not found", async () => {
142+
const req: MockRequest = {
143+
params: { callId: "call1" },
144+
auth: { userId: "s1", role: "student" },
145+
};
146+
mockVideoCallService.acceptCall.mockResolvedValue(null);
147+
148+
await controller.acceptCallController(
149+
req as never,
150+
mockResponse as unknown as Response,
151+
mockNext,
152+
);
153+
expect(mockResponse.status).toHaveBeenCalledWith(404);
154+
});
155+
});
156+
157+
describe("declineCallController", () => {
158+
it("should decline call successfully", async () => {
159+
const req: MockRequest = {
160+
params: { callId: "call1" },
161+
auth: { userId: "s1", role: "student" },
162+
};
163+
const mockCall = { id: "call1", status: "declined" };
164+
mockVideoCallService.declineCall.mockResolvedValue(mockCall);
165+
166+
await controller.declineCallController(
167+
req as never,
168+
mockResponse as unknown as Response,
169+
mockNext,
170+
);
171+
172+
expect(mockResponse.status).toHaveBeenCalledWith(200);
173+
expect(mockResponse.json).toHaveBeenCalledWith(mockCall);
174+
});
175+
});
176+
177+
describe("endCallController", () => {
178+
it("should end call successfully", async () => {
179+
const req: MockRequest = {
180+
params: { callId: "call1" },
181+
auth: { userId: "t1", role: "teacher" },
182+
};
183+
const mockCall = { id: "call1", status: "ended" };
184+
mockVideoCallService.endCall.mockResolvedValue(mockCall);
185+
186+
await controller.endCallController(
187+
req as never,
188+
mockResponse as unknown as Response,
189+
mockNext,
190+
);
191+
192+
expect(mockResponse.status).toHaveBeenCalledWith(200);
193+
expect(mockResponse.json).toHaveBeenCalledWith(mockCall);
194+
});
195+
196+
it("should handle service errors", async () => {
197+
const req: MockRequest = {
198+
params: { callId: "call1" },
199+
auth: { userId: "t1" },
200+
};
201+
const error = new Error("Service error");
202+
mockVideoCallService.endCall.mockRejectedValue(error);
203+
204+
await controller.endCallController(
205+
req as never,
206+
mockResponse as unknown as Response,
207+
mockNext,
208+
);
209+
expect(mockNext).toHaveBeenCalledWith(error);
210+
});
211+
});
212+
});
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { describe, it, expect, beforeEach, jest } from "@jest/globals";
2+
import { VideoCallService } from "../../src/services/video/videoCall.service.js";
3+
import { VideoCallCommand } from "../../src/repositories/commandRepositories/videoCall.command.js";
4+
import { VideoCallQuery } from "../../src/repositories/queryRepositories/videoCall.query.js";
5+
import { HttpError } from "../../src/utils/error.util.js";
6+
import { StartVideoCallInput } from "../../src/types/video/video.types.js";
7+
8+
const mockVideoCallCommand = {
9+
startVideoCall: jest.fn<() => Promise<unknown>>(),
10+
acceptCallById: jest.fn<() => Promise<unknown>>(),
11+
declineCallById: jest.fn<() => Promise<unknown>>(),
12+
endCallById: jest.fn<() => Promise<unknown>>(),
13+
markExpiredCallAsMissed: jest.fn<() => Promise<unknown>>(),
14+
};
15+
16+
const mockVideoCallQuery = {
17+
getVideoByStreamCallId: jest.fn<() => Promise<unknown>>(),
18+
getIncomingCallForStudent: jest.fn<() => Promise<unknown>>(),
19+
getVideoById: jest.fn<() => Promise<unknown>>(),
20+
};
21+
22+
jest.mock("../../src/db/schemes/studentSchema.js", () => ({
23+
StudentModel: {},
24+
}));
25+
jest.mock("../../src/db/schemes/teacherSchema.js", () => ({
26+
TeacherModel: {},
27+
}));
28+
jest.mock("../../src/db/schemes/videoCallSchema.js", () => ({
29+
VideoCallModel: {},
30+
}));
31+
jest.mock("../../src/socket/io.holder.js", () => ({ getIO: () => null }));
32+
33+
describe("VideoCallService", () => {
34+
let service: VideoCallService;
35+
36+
beforeEach(() => {
37+
service = new VideoCallService(
38+
mockVideoCallCommand as unknown as VideoCallCommand,
39+
mockVideoCallQuery as unknown as VideoCallQuery,
40+
);
41+
jest.clearAllMocks();
42+
});
43+
44+
describe("startCall", () => {
45+
it("should throw error if not teacher", async () => {
46+
const input: StartVideoCallInput = {
47+
authRole: "student",
48+
authUserId: "s1",
49+
teacherId: "t1",
50+
studentId: "s1",
51+
streamCallId: "stream1",
52+
expiresAt: new Date(),
53+
};
54+
await expect(service.startCall(input)).rejects.toThrow(
55+
new HttpError(403, "Only teachers can start calls"),
56+
);
57+
});
58+
59+
it("should throw error if call already exists", async () => {
60+
const input: StartVideoCallInput = {
61+
authRole: "teacher",
62+
authUserId: "t1",
63+
teacherId: "t1",
64+
studentId: "s1",
65+
streamCallId: "stream1",
66+
expiresAt: new Date(),
67+
};
68+
mockVideoCallQuery.getVideoByStreamCallId.mockResolvedValue({
69+
id: "existing",
70+
});
71+
await expect(service.startCall(input)).rejects.toThrow(
72+
new HttpError(409, "This call already exists"),
73+
);
74+
});
75+
});
76+
77+
describe("incomingCall", () => {
78+
it("should return incoming call for student", async () => {
79+
const mockCall = {
80+
id: "call1",
81+
expiresAt: new Date(Date.now() + 60000),
82+
};
83+
mockVideoCallQuery.getIncomingCallForStudent.mockResolvedValue(mockCall);
84+
85+
const result = await service.incomingCall({
86+
authUserId: "s1",
87+
authRole: "student",
88+
});
89+
expect(result).toEqual(mockCall);
90+
});
91+
92+
it("should throw error if not student", async () => {
93+
await expect(
94+
service.incomingCall({ authUserId: "t1", authRole: "teacher" }),
95+
).rejects.toThrow(
96+
new HttpError(403, "This endpoint is for students only"),
97+
);
98+
});
99+
});
100+
101+
describe("acceptCall", () => {
102+
it("should throw error if not student", async () => {
103+
await expect(
104+
service.acceptCall({
105+
callId: "call1",
106+
authUserId: "t1",
107+
authRole: "teacher",
108+
}),
109+
).rejects.toThrow(
110+
new HttpError(403, "Students only can accept the call"),
111+
);
112+
});
113+
114+
it("should throw error if call not found", async () => {
115+
mockVideoCallQuery.getVideoById.mockResolvedValue(null);
116+
await expect(
117+
service.acceptCall({
118+
callId: "call1",
119+
authUserId: "s1",
120+
authRole: "student",
121+
}),
122+
).rejects.toThrow(new HttpError(404, "Call not found"));
123+
});
124+
});
125+
126+
describe("declineCall", () => {
127+
it("should throw error if not student", async () => {
128+
await expect(
129+
service.declineCall({
130+
callId: "call1",
131+
authUserId: "t1",
132+
authRole: "teacher",
133+
}),
134+
).rejects.toThrow(
135+
new HttpError(403, "Students only can decline the call"),
136+
);
137+
});
138+
});
139+
140+
describe("endCall", () => {
141+
it("should throw error if call not found", async () => {
142+
mockVideoCallQuery.getVideoById.mockResolvedValue(null);
143+
await expect(
144+
service.endCall({ callId: "call1", authUserId: "t1" }),
145+
).rejects.toThrow(new HttpError(404, "Call not found"));
146+
});
147+
148+
it("should throw error if user not participant", async () => {
149+
const mockCall = { id: "call1", teacherId: "t1", studentId: "s1" };
150+
mockVideoCallQuery.getVideoById.mockResolvedValue(mockCall);
151+
152+
await expect(
153+
service.endCall({ callId: "call1", authUserId: "other" }),
154+
).rejects.toThrow(
155+
new HttpError(403, "Only participants can end the call"),
156+
);
157+
});
158+
});
159+
});

0 commit comments

Comments
 (0)