Skip to content

Commit f2700f8

Browse files
author
Siri Sudheeksha Vavila
committed
refactor(test): reduce duplication in studentTaskController spec
1 parent cb857a0 commit f2700f8

1 file changed

Lines changed: 81 additions & 161 deletions

File tree

src/controllers/studentTaskController.spec.js

Lines changed: 81 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,34 @@
1-
const mongoose = require('mongoose');
21
const EducationTask = require('../models/educationTask');
32
const { mockReq, mockRes } = require('../test');
43
const studentTaskController = require('./studentTaskController');
54

65
const VALID_TASK_ID = '507f1f77bcf86cd799439011';
7-
const VALID_STUDENT_ID = '65cf6c3706d8ac105827bb2e'; // matches mockReq.body.requestor.requestorId
6+
const VALID_STUDENT_ID = '65cf6c3706d8ac105827bb2e';
7+
8+
// Shared helpers to reduce repetition
9+
const makeTask = (overrides = {}) => ({
10+
status: 'assigned',
11+
loggedHours: 0,
12+
suggestedTotalHours: 5,
13+
...overrides,
14+
});
815

9-
const makeSut = () => {
10-
const { logHours } = studentTaskController();
11-
return { logHours };
12-
};
16+
const makeUpdated = (overrides = {}) => ({
17+
loggedHours: 1,
18+
suggestedTotalHours: 5,
19+
status: 'in_progress',
20+
...overrides,
21+
});
1322

14-
const flushPromises = () => new Promise(setImmediate);
23+
const spyFindOne = (result) => jest.spyOn(EducationTask, 'findOne').mockResolvedValueOnce(result);
24+
const spyFindOneAndUpdate = (result) =>
25+
jest.spyOn(EducationTask, 'findOneAndUpdate').mockResolvedValueOnce(result);
1526

1627
describe('studentTaskController - logHours', () => {
28+
let logHours;
29+
1730
beforeEach(() => {
31+
logHours = studentTaskController().logHours;
1832
mockReq.params.taskId = VALID_TASK_ID;
1933
mockReq.body.requestor = { requestorId: VALID_STUDENT_ID };
2034
mockReq.body.hours = 1;
@@ -25,117 +39,76 @@ describe('studentTaskController - logHours', () => {
2539
});
2640

2741
describe('Input validation', () => {
28-
test('Returns 400 if taskId is missing', async () => {
29-
const { logHours } = makeSut();
30-
mockReq.params.taskId = '';
31-
await logHours(mockReq, mockRes);
32-
expect(mockRes.status).toHaveBeenCalledWith(400);
33-
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Invalid Task ID' });
34-
});
35-
36-
test('Returns 400 if taskId is not a valid ObjectId', async () => {
37-
const { logHours } = makeSut();
38-
mockReq.params.taskId = 'not-an-objectid';
39-
await logHours(mockReq, mockRes);
40-
expect(mockRes.status).toHaveBeenCalledWith(400);
41-
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Invalid Task ID' });
42-
});
43-
44-
test('Returns 400 if studentId is missing', async () => {
45-
const { logHours } = makeSut();
46-
mockReq.body.requestor = {};
47-
await logHours(mockReq, mockRes);
48-
expect(mockRes.status).toHaveBeenCalledWith(400);
49-
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Invalid Student ID' });
50-
});
51-
52-
test('Returns 400 if studentId is not a valid ObjectId', async () => {
53-
const { logHours } = makeSut();
54-
mockReq.body.requestor = { requestorId: 'not-an-objectid' };
55-
await logHours(mockReq, mockRes);
56-
expect(mockRes.status).toHaveBeenCalledWith(400);
57-
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Invalid Student ID' });
58-
});
59-
60-
test('Returns 400 if hours is zero', async () => {
61-
const { logHours } = makeSut();
62-
mockReq.body.hours = 0;
63-
await logHours(mockReq, mockRes);
64-
expect(mockRes.status).toHaveBeenCalledWith(400);
65-
expect(mockRes.json).toHaveBeenCalledWith({ error: 'hours must be a positive number' });
66-
});
67-
68-
test('Returns 400 if hours is negative', async () => {
69-
const { logHours } = makeSut();
70-
mockReq.body.hours = -1;
71-
await logHours(mockReq, mockRes);
72-
expect(mockRes.status).toHaveBeenCalledWith(400);
73-
expect(mockRes.json).toHaveBeenCalledWith({ error: 'hours must be a positive number' });
74-
});
75-
76-
test('Returns 400 if hours is not a number', async () => {
77-
const { logHours } = makeSut();
78-
mockReq.body.hours = 'abc';
79-
await logHours(mockReq, mockRes);
80-
expect(mockRes.status).toHaveBeenCalledWith(400);
81-
expect(mockRes.json).toHaveBeenCalledWith({ error: 'hours must be a positive number' });
42+
test.each([
43+
['taskId is missing', { params: { taskId: '' } }, 400, { error: 'Invalid Task ID' }],
44+
[
45+
'taskId is not a valid ObjectId',
46+
{ params: { taskId: 'bad-id' } },
47+
400,
48+
{ error: 'Invalid Task ID' },
49+
],
50+
[
51+
'studentId is missing',
52+
{ body: { requestor: {}, hours: 1 } },
53+
400,
54+
{ error: 'Invalid Student ID' },
55+
],
56+
[
57+
'studentId is not a valid ObjectId',
58+
{ body: { requestor: { requestorId: 'bad' }, hours: 1 } },
59+
400,
60+
{ error: 'Invalid Student ID' },
61+
],
62+
[
63+
'hours is zero',
64+
{ body: { requestor: { requestorId: VALID_STUDENT_ID }, hours: 0 } },
65+
400,
66+
{ error: 'hours must be a positive number' },
67+
],
68+
[
69+
'hours is negative',
70+
{ body: { requestor: { requestorId: VALID_STUDENT_ID }, hours: -1 } },
71+
400,
72+
{ error: 'hours must be a positive number' },
73+
],
74+
[
75+
'hours is not a number',
76+
{ body: { requestor: { requestorId: VALID_STUDENT_ID }, hours: 'abc' } },
77+
400,
78+
{ error: 'hours must be a positive number' },
79+
],
80+
])('Returns %i when %s', async (_, reqOverrides, expectedStatus, expectedBody) => {
81+
Object.assign(mockReq, reqOverrides);
82+
if (reqOverrides.params) Object.assign(mockReq.params, reqOverrides.params);
83+
await logHours(mockReq, mockRes);
84+
expect(mockRes.status).toHaveBeenCalledWith(expectedStatus);
85+
expect(mockRes.json).toHaveBeenCalledWith(expectedBody);
8286
});
8387
});
8488

8589
describe('Database interactions', () => {
8690
test('Returns 404 if task is not found', async () => {
87-
const { logHours } = makeSut();
88-
jest.spyOn(EducationTask, 'findOne').mockResolvedValueOnce(null);
91+
spyFindOne(null);
8992
await logHours(mockReq, mockRes);
9093
expect(mockRes.status).toHaveBeenCalledWith(404);
9194
expect(mockRes.json).toHaveBeenCalledWith({
9295
error: 'Task not found or does not belong to you',
9396
});
9497
});
9598

96-
test('Returns 400 if task status is completed', async () => {
97-
const { logHours } = makeSut();
98-
jest.spyOn(EducationTask, 'findOne').mockResolvedValueOnce({
99-
_id: mongoose.Types.ObjectId(VALID_TASK_ID),
100-
studentId: mongoose.Types.ObjectId(VALID_STUDENT_ID),
101-
status: 'completed',
102-
loggedHours: 3,
103-
suggestedTotalHours: 5,
104-
});
99+
test.each([
100+
['completed', makeTask({ status: 'completed', loggedHours: 3 })],
101+
['graded', makeTask({ status: 'graded', loggedHours: 5 })],
102+
])('Returns 400 if task status is %s', async (_, task) => {
103+
spyFindOne(task);
105104
await logHours(mockReq, mockRes);
106105
expect(mockRes.status).toHaveBeenCalledWith(400);
107106
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Cannot log hours for a completed task' });
108107
});
109108

110-
test('Returns 400 if task status is graded', async () => {
111-
const { logHours } = makeSut();
112-
jest.spyOn(EducationTask, 'findOne').mockResolvedValueOnce({
113-
_id: mongoose.Types.ObjectId(VALID_TASK_ID),
114-
studentId: mongoose.Types.ObjectId(VALID_STUDENT_ID),
115-
status: 'graded',
116-
loggedHours: 5,
117-
suggestedTotalHours: 5,
118-
});
119-
await logHours(mockReq, mockRes);
120-
expect(mockRes.status).toHaveBeenCalledWith(400);
121-
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Cannot log hours for a completed task' });
122-
});
123-
124-
test('Returns 200 and logs hours successfully for assigned task', async () => {
125-
const { logHours } = makeSut();
126-
jest.spyOn(EducationTask, 'findOne').mockResolvedValueOnce({
127-
_id: mongoose.Types.ObjectId(VALID_TASK_ID),
128-
studentId: mongoose.Types.ObjectId(VALID_STUDENT_ID),
129-
status: 'assigned',
130-
loggedHours: 0,
131-
suggestedTotalHours: 5,
132-
});
133-
jest.spyOn(EducationTask, 'findOneAndUpdate').mockResolvedValueOnce({
134-
_id: mongoose.Types.ObjectId(VALID_TASK_ID),
135-
loggedHours: 1,
136-
suggestedTotalHours: 5,
137-
status: 'in_progress',
138-
});
109+
test('Returns 200 and transitions assigned -> in_progress on first log', async () => {
110+
spyFindOne(makeTask());
111+
spyFindOneAndUpdate(makeUpdated());
139112
await logHours(mockReq, mockRes);
140113
expect(mockRes.status).toHaveBeenCalledWith(200);
141114
expect(mockRes.json).toHaveBeenCalledWith({
@@ -148,21 +121,9 @@ describe('studentTaskController - logHours', () => {
148121
});
149122

150123
test('Returns 200 and caps loggedHours at suggestedTotalHours', async () => {
151-
const { logHours } = makeSut();
152124
mockReq.body.hours = 3;
153-
jest.spyOn(EducationTask, 'findOne').mockResolvedValueOnce({
154-
_id: mongoose.Types.ObjectId(VALID_TASK_ID),
155-
studentId: mongoose.Types.ObjectId(VALID_STUDENT_ID),
156-
status: 'in_progress',
157-
loggedHours: 4,
158-
suggestedTotalHours: 5,
159-
});
160-
jest.spyOn(EducationTask, 'findOneAndUpdate').mockResolvedValueOnce({
161-
_id: mongoose.Types.ObjectId(VALID_TASK_ID),
162-
loggedHours: 5,
163-
suggestedTotalHours: 5,
164-
status: 'in_progress',
165-
});
125+
spyFindOne(makeTask({ status: 'in_progress', loggedHours: 4 }));
126+
spyFindOneAndUpdate(makeUpdated({ loggedHours: 5 }));
166127
await logHours(mockReq, mockRes);
167128
expect(mockRes.status).toHaveBeenCalledWith(200);
168129
expect(mockRes.json).toHaveBeenCalledWith({
@@ -172,7 +133,6 @@ describe('studentTaskController - logHours', () => {
172133
status: 'in_progress',
173134
canMarkDone: true,
174135
});
175-
// confirm the update was capped at 5, not 7
176136
expect(EducationTask.findOneAndUpdate).toHaveBeenCalledWith(
177137
expect.any(Object),
178138
{ $set: { loggedHours: 5, status: 'in_progress' } },
@@ -181,63 +141,23 @@ describe('studentTaskController - logHours', () => {
181141
});
182142

183143
test('Returns 404 if findOneAndUpdate returns null', async () => {
184-
const { logHours } = makeSut();
185-
jest.spyOn(EducationTask, 'findOne').mockResolvedValueOnce({
186-
_id: mongoose.Types.ObjectId(VALID_TASK_ID),
187-
studentId: mongoose.Types.ObjectId(VALID_STUDENT_ID),
188-
status: 'assigned',
189-
loggedHours: 0,
190-
suggestedTotalHours: 5,
191-
});
192-
jest.spyOn(EducationTask, 'findOneAndUpdate').mockResolvedValueOnce(null);
144+
spyFindOne(makeTask());
145+
spyFindOneAndUpdate(null);
193146
await logHours(mockReq, mockRes);
194147
expect(mockRes.status).toHaveBeenCalledWith(404);
195148
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Task not found during update' });
196149
});
197150

198-
test('Returns 200 with canMarkDone true when loggedHours meets suggestedTotalHours', async () => {
199-
const { logHours } = makeSut();
200-
jest.spyOn(EducationTask, 'findOne').mockResolvedValueOnce({
201-
_id: mongoose.Types.ObjectId(VALID_TASK_ID),
202-
studentId: mongoose.Types.ObjectId(VALID_STUDENT_ID),
203-
status: 'in_progress',
204-
loggedHours: 4,
205-
suggestedTotalHours: 5,
206-
});
207-
jest.spyOn(EducationTask, 'findOneAndUpdate').mockResolvedValueOnce({
208-
_id: mongoose.Types.ObjectId(VALID_TASK_ID),
209-
loggedHours: 5,
210-
suggestedTotalHours: 5,
211-
status: 'in_progress',
212-
});
213-
await logHours(mockReq, mockRes);
214-
expect(mockRes.status).toHaveBeenCalledWith(200);
215-
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({ canMarkDone: true }));
216-
});
217-
218151
test('Returns 200 with canMarkDone false when suggestedTotalHours is 0', async () => {
219-
const { logHours } = makeSut();
220-
jest.spyOn(EducationTask, 'findOne').mockResolvedValueOnce({
221-
_id: mongoose.Types.ObjectId(VALID_TASK_ID),
222-
studentId: mongoose.Types.ObjectId(VALID_STUDENT_ID),
223-
status: 'assigned',
224-
loggedHours: 0,
225-
suggestedTotalHours: 0,
226-
});
227-
jest.spyOn(EducationTask, 'findOneAndUpdate').mockResolvedValueOnce({
228-
_id: mongoose.Types.ObjectId(VALID_TASK_ID),
229-
loggedHours: 1,
230-
suggestedTotalHours: 0,
231-
status: 'in_progress',
232-
});
152+
spyFindOne(makeTask({ suggestedTotalHours: 0 }));
153+
spyFindOneAndUpdate(makeUpdated({ suggestedTotalHours: 0 }));
233154
await logHours(mockReq, mockRes);
234155
expect(mockRes.status).toHaveBeenCalledWith(200);
235156
expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({ canMarkDone: false }));
236157
});
237158

238159
test('Returns 500 if findOne throws an error', async () => {
239-
const { logHours } = makeSut();
240-
jest.spyOn(EducationTask, 'findOne').mockRejectedValueOnce(new Error('DB error'));
160+
spyFindOne(Promise.reject(new Error('DB error')));
241161
await logHours(mockReq, mockRes);
242162
expect(mockRes.status).toHaveBeenCalledWith(500);
243163
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Internal server error' });

0 commit comments

Comments
 (0)