1- const mongoose = require ( 'mongoose' ) ;
21const EducationTask = require ( '../models/educationTask' ) ;
32const { mockReq, mockRes } = require ( '../test' ) ;
43const studentTaskController = require ( './studentTaskController' ) ;
54
65const 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
1627describe ( '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