• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MarkUsProject / Markus / 23774787472

31 Mar 2026 12:39AM UTC coverage: 91.726% (+0.01%) from 91.713%
23774787472

Pull #7886

github

web-flow
Merge 7a88251dd into 166782a29
Pull Request #7886: TICKET-604: Return structured JSON from grade entry forms show endpoint

948 of 1842 branches covered (51.47%)

Branch coverage included in aggregate %.

107 of 109 new or added lines in 2 files covered. (98.17%)

23 existing lines in 2 files now uncovered.

45258 of 48532 relevant lines covered (93.25%)

130.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.31
/spec/controllers/api/grade_entry_forms_controller_spec.rb
1
describe Api::GradeEntryFormsController do
1✔
2
  let(:course) { create(:course) }
53✔
3
  let(:grade_entry_form) { create(:grade_entry_form, course: course) }
20✔
4

5
  context 'An unauthenticated request' do
1✔
6
    before do
1✔
7
      request.env['HTTP_AUTHORIZATION'] = 'garbage http_header'
4✔
8
      request.env['HTTP_ACCEPT'] = 'application/xml'
4✔
9
    end
10

11
    it 'should fail to authenticate a GET index request' do
1✔
12
      get :index, params: { course_id: course.id }
1✔
13
      expect(response).to have_http_status(:forbidden)
1✔
14
    end
15

16
    it 'should fail to authenticate a GET show request' do
1✔
17
      get :show, params: { id: grade_entry_form.id, course_id: course.id }
1✔
18
      expect(response).to have_http_status(:forbidden)
1✔
19
    end
20

21
    it 'should fail to authenticate a POST create request' do
1✔
22
      post :create, params: { course_id: course.id }
1✔
23

24
      expect(response).to have_http_status(:forbidden)
1✔
25
    end
26

27
    it 'should fail to authenticate a PUT update request' do
1✔
28
      put :update, params: { id: grade_entry_form.id, course_id: course.id }
1✔
29
      expect(response).to have_http_status(:forbidden)
1✔
30
    end
31
  end
32

33
  context 'An authenticated request requesting' do
1✔
34
    before do
1✔
35
      instructor = create(:instructor, course: course)
48✔
36
      instructor.reset_api_key
48✔
37
      request.env['HTTP_AUTHORIZATION'] = "MarkUsAuth #{instructor.api_key.strip}"
48✔
38
    end
39

40
    context 'GET index' do
1✔
41
      context 'expecting an xml response' do
1✔
42
        before do
1✔
43
          request.env['HTTP_ACCEPT'] = 'application/xml'
9✔
44
        end
45

46
        context 'with a single grade entry form' do
1✔
47
          before do
1✔
48
            grade_entry_form
3✔
49
            get :index, params: { course_id: course.id }
3✔
50
          end
51

52
          it 'should be successful' do
1✔
53
            expect(response).to have_http_status(:ok)
1✔
54
          end
55

56
          it 'should return xml content' do
1✔
57
            expect(Hash.from_xml(response.body)
1✔
58
                       .dig('grade_entry_forms', 'grade_entry_form', 'id')).to eq(grade_entry_form.id.to_s)
59
          end
60

61
          it 'should return all default fields' do
1✔
62
            keys = Hash.from_xml(response.body).dig('grade_entry_forms', 'grade_entry_form').keys.map(&:to_sym)
1✔
63
            expect(keys).to contain_exactly(:grade_entry_items, *Api::GradeEntryFormsController::DEFAULT_FIELDS)
1✔
64
          end
65
        end
66

67
        context 'with a single grade entry form in a different course' do
1✔
68
          before do
1✔
69
            create(:grade_entry_form, course: create(:course))
2✔
70
            get :index, params: { course_id: course.id }
2✔
71
          end
72

73
          it 'should be successful' do
1✔
74
            expect(response).to have_http_status(:ok)
1✔
75
          end
76

77
          it 'should return empty content' do
1✔
78
            expect(Hash.from_xml(response.body)['grade_entry_forms']).to be_nil
1✔
79
          end
80
        end
81

82
        context 'with multiple assignments' do
1✔
83
          before do
1✔
84
            create_list(:grade_entry_form, 5, course: course)
2✔
85
            get :index, params: { course_id: course.id }
2✔
86
          end
87

88
          it 'should return xml content about all grade entry forms' do
1✔
89
            expect(Hash.from_xml(response.body).dig('grade_entry_forms', 'grade_entry_form').length).to eq(5)
1✔
90
          end
91

92
          it 'should return all default fields for all grade entry forms' do
1✔
93
            keys = Hash.from_xml(response.body)
1✔
94
                       .dig('grade_entry_forms', 'grade_entry_form')
95
                       .map { |h| h.keys.map(&:to_sym) }
5✔
96
            expect(keys).to all(contain_exactly(:grade_entry_items, *Api::GradeEntryFormsController::DEFAULT_FIELDS))
1✔
97
          end
98
        end
99

100
        context 'with multiple grade entry forms in a different course' do
1✔
101
          before do
1✔
102
            create_list(:grade_entry_form, 5, course: create(:course))
2✔
103
            get :index, params: { course_id: course.id }
2✔
104
          end
105

106
          it 'should be successful' do
1✔
107
            expect(response).to have_http_status(:ok)
1✔
108
          end
109

110
          it 'should return empty content' do
1✔
111
            expect(Hash.from_xml(response.body)['grade_entry_forms']).to be_nil
1✔
112
          end
113
        end
114
      end
115

116
      context 'expecting a json response' do
1✔
117
        before do
1✔
118
          request.env['HTTP_ACCEPT'] = 'application/json'
9✔
119
        end
120

121
        context 'with a single grade entry form' do
1✔
122
          before do
1✔
123
            grade_entry_form
3✔
124
            get :index, params: { course_id: course.id }
3✔
125
          end
126

127
          it 'should be successful' do
1✔
128
            expect(response).to have_http_status(:ok)
1✔
129
          end
130

131
          it 'should return json content' do
1✔
132
            expect(response.parsed_body&.first&.dig('id')).to eq(grade_entry_form.id)
1✔
133
          end
134

135
          it 'should return all default fields' do
1✔
136
            keys = response.parsed_body&.first&.keys&.map(&:to_sym)
1✔
137
            expect(keys).to contain_exactly(:grade_entry_items, *Api::GradeEntryFormsController::DEFAULT_FIELDS)
1✔
138
          end
139
        end
140

141
        context 'with a single grade entry form in a different course' do
1✔
142
          before do
1✔
143
            create(:grade_entry_form, course: create(:course))
2✔
144
            get :index, params: { course_id: course.id }
2✔
145
          end
146

147
          it 'should be successful' do
1✔
148
            expect(response).to have_http_status(:ok)
1✔
149
          end
150

151
          it 'should return empty content' do
1✔
152
            expect(response.parsed_body&.first&.dig('id')).to be_nil
1✔
153
          end
154
        end
155

156
        context 'with multiple grade entry forms' do
1✔
157
          before do
1✔
158
            create_list(:grade_entry_form, 5, course: course)
2✔
159
            get :index, params: { course_id: course.id }
2✔
160
          end
161

162
          it 'should return xml content about all grade entry forms' do
1✔
163
            expect(response.parsed_body.length).to eq(5)
1✔
164
          end
165

166
          it 'should return all default fields for all grade entry forms' do
1✔
167
            keys = response.parsed_body.map { |h| h.keys.map(&:to_sym) }
6✔
168
            expect(keys).to all(contain_exactly(:grade_entry_items, *Api::GradeEntryFormsController::DEFAULT_FIELDS))
1✔
169
          end
170
        end
171

172
        context 'with a multiple grade entry forms in a different course' do
1✔
173
          before do
1✔
174
            create_list(:grade_entry_form, 5, course: create(:course))
2✔
175
            get :index, params: { course_id: course.id }
2✔
176
          end
177

178
          it 'should be successful' do
1✔
179
            expect(response).to have_http_status(:ok)
1✔
180
          end
181

182
          it 'should return empty content' do
1✔
183
            expect(response.parsed_body&.first&.dig('id')).to be_nil
1✔
184
          end
185
        end
186
      end
187
    end
188

189
    context 'GET show' do
1✔
190
      context 'expecting a json response' do
1✔
191
        before { request.env['HTTP_ACCEPT'] = 'application/json' }
7✔
192

193
        it 'returns form metadata, items, and empty students when no students exist' do
1✔
194
          get :show, params: { id: grade_entry_form.id, course_id: course.id }
1✔
195
          expect(response).to have_http_status(:ok)
1✔
196
          body = response.parsed_body
1✔
197
          expect(body.keys.map(&:to_sym)).to include(*Api::GradeEntryFormsController::DEFAULT_FIELDS)
1✔
198
          expect(body['grade_entry_items']).to eq([])
1✔
199
          expect(body['students']).to eq([])
1✔
200
        end
201

202
        it 'returns items, graded/ungraded students, and excludes hidden students' do
1✔
203
          item1 = create(:grade_entry_item, grade_entry_form: grade_entry_form, out_of: 10, name: 'Q1', position: 1)
1✔
204
          item2 = create(:grade_entry_item, grade_entry_form: grade_entry_form, out_of: 5, name: 'Q2', position: 2)
1✔
205
          graded_student = create(:student, course: course)
1✔
206
          ungraded_student = create(:student, course: course)
1✔
207
          hidden_student = create(:student, course: course, hidden: true)
1✔
208
          ges = grade_entry_form.grade_entry_students.find_by(role: graded_student)
1✔
209
          create(:grade, grade_entry_student: ges, grade_entry_item: item1, grade: 8.0)
1✔
210
          create(:grade, grade_entry_student: ges, grade_entry_item: item2, grade: 4.0)
1✔
211

212
          get :show, params: { id: grade_entry_form.id, course_id: course.id }
1✔
213

214
          items = response.parsed_body['grade_entry_items']
1✔
215
          expect(items.pluck('name')).to eq(%w[Q1 Q2])
1✔
216
          expect(items.first).to include('name' => 'Q1', 'out_of' => 10, 'bonus' => false)
1✔
217

218
          students = response.parsed_body['students']
1✔
219
          graded = students.find { |s| s['user_name'] == graded_student.user_name }
2✔
220
          ungraded = students.find { |s| s['user_name'] == ungraded_student.user_name }
3✔
221
          expect(graded['grades']).to eq({ 'Q1' => 8.0, 'Q2' => 4.0 })
1✔
222
          expect(ungraded['grades']).to eq({})
1✔
223
          expect(students.pluck('user_name')).not_to include(hidden_student.user_name)
1✔
224
        end
225

226
        context 'with show_total' do
1✔
227
          let!(:grade_entry_item) { create(:grade_entry_item, grade_entry_form: form, out_of: 10, name: 'Q1') }
3✔
228
          let!(:student) { create(:student, course: course) }
3✔
229

230
          before do
1✔
231
            ges = form.grade_entry_students.find_by(role: student)
2✔
232
            create(:grade, grade_entry_student: ges, grade_entry_item: grade_entry_item, grade: 7.0)
2✔
233
          end
234

235
          context 'enabled' do
1✔
236
            let(:form) { create(:grade_entry_form, course: course, show_total: true) }
2✔
237

238
            it 'includes total_grade per student' do
1✔
239
              get :show, params: { id: form.id, course_id: course.id }
1✔
240
              expect(response.parsed_body['students'].first['total_grade']).to eq(7.0)
1✔
241
            end
242
          end
243

244
          context 'disabled' do
1✔
245
            let(:form) { create(:grade_entry_form, course: course, show_total: false) }
2✔
246

247
            it 'omits total_grade' do
1✔
248
              get :show, params: { id: form.id, course_id: course.id }
1✔
249
              expect(response.parsed_body['students'].first).not_to have_key('total_grade')
1✔
250
            end
251
          end
252
        end
253

254
        context 'with user_name filter' do
1✔
255
          let!(:grade_entry_item) do
1✔
256
            create(:grade_entry_item, grade_entry_form: grade_entry_form, out_of: 10, name: 'Q1')
2✔
257
          end
258
          let!(:student1) { create(:student, course: course) }
3✔
259
          let!(:student2) { create(:student, course: course) }
3✔
260

261
          before do
1✔
262
            ges1 = grade_entry_form.grade_entry_students.find_by(role: student1)
2✔
263
            create(:grade, grade_entry_student: ges1, grade_entry_item: grade_entry_item, grade: 9.0)
2✔
264
            ges2 = grade_entry_form.grade_entry_students.find_by(role: student2)
2✔
265
            create(:grade, grade_entry_student: ges2, grade_entry_item: grade_entry_item, grade: 6.0)
2✔
266
          end
267

268
          it 'returns only the matching student' do
1✔
269
            get :show, params: { id: grade_entry_form.id, course_id: course.id, user_name: student1.user_name }
1✔
270
            students = response.parsed_body['students']
1✔
271
            expect(students.length).to eq(1)
1✔
272
            expect(students.first['user_name']).to eq(student1.user_name)
1✔
273
          end
274

275
          it 'returns 422 for nonexistent user_name' do
1✔
276
            get :show, params: { id: grade_entry_form.id, course_id: course.id, user_name: 'nonexistent' }
1✔
277
            expect(response).to have_http_status(:unprocessable_content)
1✔
278
          end
279
        end
280
      end
281

282
      context 'expecting an xml response' do
1✔
283
        before { request.env['HTTP_ACCEPT'] = 'application/xml' }
2✔
284

285
        it 'returns xml with grade_entry_form root' do
1✔
286
          get :show, params: { id: grade_entry_form.id, course_id: course.id }
1✔
287
          expect(response).to have_http_status(:ok)
1✔
288
          expect(Hash.from_xml(response.body)).to have_key('grade_entry_form')
1✔
289
        end
290
      end
291

292
      it 'returns CSV when download=csv' do
1✔
293
        get :show, params: { id: grade_entry_form.id, course_id: course.id, download: 'csv' }
1✔
294
        expect(response).to have_http_status(:ok)
1✔
295
        expect(response.content_type).to include('text/csv')
1✔
296
      end
297

298
      it 'returns 404 for non-existant grade entry form' do
1✔
299
        get :show, params: { id: -1, course_id: course.id }
1✔
300
        expect(response).to have_http_status(:not_found)
1✔
301
      end
302

303
      it 'returns 403 for a grade entry form in a different course' do
1✔
304
        other_form = create(:grade_entry_form, course: create(:course))
1✔
305
        get :show, params: { id: other_form.id, course_id: other_form.course_id }
1✔
306
        expect(response).to have_http_status(:forbidden)
1✔
307
      end
308
    end
309

310
    context 'POST create' do
1✔
311
      let(:params) do
1✔
312
        { short_identifier: 'A0', description: 'Test', course_id: course.id }
11✔
313
      end
314
      let(:full_params) do
1✔
UNCOV
315
        { short_identifier: 'A0', course_id: course.id, description: 'Test',
×
316
          due_date: '2012-03-26 18:04:39', is_hidden: false,
317
          grade_entry_items: [
318
            { name: 'col1', out_of: 10, bonus: false },
319
            { name: 'col2', out_of: 2, bonus: true }
320
          ] }
321
      end
322

323
      context 'with minimal required params' do
1✔
324
        it 'should respond with 201' do
1✔
325
          post :create, params: params
1✔
326
          expect(response).to have_http_status(:created)
1✔
327
        end
328

329
        it 'should create an assignment' do
1✔
330
          expect(GradeEntryForm.find_by(short_identifier: params[:short_identifier])).to be_nil
1✔
331
          post :create, params: params
1✔
332
          expect(GradeEntryForm.find_by(short_identifier: params[:short_identifier])).not_to be_nil
1✔
333
        end
334

335
        context 'for a different course' do
1✔
336
          it 'should response with 403' do
1✔
337
            post :create, params: { **params, course_id: create(:course).id }
1✔
338
            expect(response).to have_http_status(:forbidden)
1✔
339
          end
340
        end
341
      end
342

343
      context 'with all params' do
1✔
344
        it 'should respond with 201' do
1✔
345
          post :create, params: params
1✔
346
          expect(response).to have_http_status(:created)
1✔
347
        end
348

349
        it 'should create an assignment' do
1✔
350
          expect(GradeEntryForm.find_by(short_identifier: params[:short_identifier])).to be_nil
1✔
351
          post :create, params: params
1✔
352
          expect(GradeEntryForm.find_by(short_identifier: params[:short_identifier])).not_to be_nil
1✔
353
        end
354
      end
355

356
      context 'with missing params' do
1✔
357
        context 'missing short_id' do
1✔
358
          it 'should respond with 422' do
1✔
359
            post :create, params: params.slice(:description, :due_date, :course_id)
1✔
360
            expect(response).to have_http_status(:unprocessable_content)
1✔
361
          end
362

363
          it 'should not create an assignment' do
1✔
364
            post :create, params: params.slice(:description, :due_date, :course_id)
1✔
365
            expect(Assignment.find_by(description: params[:description])).to be_nil
1✔
366
          end
367
        end
368

369
        context 'missing description' do
1✔
370
          it 'should respond with 422' do
1✔
371
            post :create, params: params.slice(:short_identifier, :due_date, :course_id)
1✔
372
            expect(response).to have_http_status(:unprocessable_content)
1✔
373
          end
374

375
          it 'should not create an assignment' do
1✔
376
            post :create, params: params.slice(:short_identifier, :due_date, :course_id)
1✔
377
            expect(GradeEntryForm.find_by(short_identifier: params[:short_identifier])).to be_nil
1✔
378
          end
379
        end
380
      end
381

382
      context 'where short_identifier is already taken' do
1✔
383
        it 'should respond with 409' do
1✔
384
          grade_entry_form = create(:grade_entry_form, course: course)
1✔
385
          post :create, params: { **params, short_identifier: grade_entry_form.short_identifier }
1✔
386
          expect(response).to have_http_status(:conflict)
1✔
387
        end
388
      end
389

390
      context 'where due_date is invalid' do
1✔
391
        it 'should respond with 422' do
1✔
392
          post :create, params: { **params, due_date: 'not a real date' }
1✔
393
          expect(response).to have_http_status(:unprocessable_content)
1✔
394
        end
395
      end
396
    end
397

398
    context 'PUT update' do
1✔
399
      it 'should update an existing assignment' do
1✔
400
        new_desc = grade_entry_form.description + 'more!'
1✔
401
        put :update, params: { id: grade_entry_form.id, description: new_desc, course_id: course.id }
1✔
402
        expect(response).to have_http_status(:ok)
1✔
403
      end
404

405
      it 'should not update a short identifier' do
1✔
406
        new_short_id = grade_entry_form.short_identifier + 'more!'
1✔
407
        put :update, params: { id: grade_entry_form.id, short_identifier: new_short_id, course_id: course.id }
1✔
408
        expect(response).to have_http_status(:internal_server_error)
1✔
409
      end
410

411
      it 'should not update an assignment that does not exist' do
1✔
412
        new_desc = grade_entry_form.description + 'more!'
1✔
413
        put :update, params: { id: -1, description: new_desc, course_id: course.id }
1✔
414
        expect(response).to have_http_status(:not_found)
1✔
415
      end
416

417
      context 'for a different course' do
1✔
418
        let(:grade_entry_form) { create(:grade_entry_form, course: create(:course)) }
2✔
419

420
        it 'should response with 403' do
1✔
421
          new_desc = grade_entry_form.description + 'more!'
1✔
422
          put :update, params: { id: grade_entry_form.id, description: new_desc, course_id: grade_entry_form.course.id }
1✔
423
          expect(response).to have_http_status(:forbidden)
1✔
424
        end
425
      end
426
    end
427

428
    context 'PUT update_grades' do
1✔
429
      let(:student) { create(:student) }
3✔
430
      let(:grade_params) do
1✔
UNCOV
431
        { short_identifier: 'A0', course_id: course.id, description: 'Test',
×
432
          due_date: '2012-03-26 18:04:39', is_hidden: false,
433
          id: grade_entry_form.id,
434
          grade_entry_items: [
435
            { name: 'col1', out_of: 10, bonus: false },
436
            { name: 'col2', out_of: 2, bonus: true }
437
          ] }
438
      end
439

440
      before { create(:grade_entry_item, grade_entry_form: grade_entry_form, out_of: 5, name: 'col1') }
3✔
441

442
      it 'creates new grades' do
1✔
443
        put :update_grades,
1✔
444
            params: { course_id: course.id, id: grade_entry_form.id, user_name: student.user_name,
445
                      grade_entry_items: { col1: 2 } }
446
        expect(grade_entry_form.grade_entry_students.find_by(role: student).grades.count).to eq(1)
1✔
447
      end
448

449
      it 'updates existing grades' do
1✔
450
        put :update_grades,
1✔
451
            params: { course_id: course.id, id: grade_entry_form.id, user_name: student.user_name,
452
                      grade_entry_items: { col1: 2 } }
453
        put :update_grades,
1✔
454
            params: { course_id: course.id, id: grade_entry_form.id, user_name: student.user_name,
455
                      grade_entry_items: { col1: 5 } }
456
        expect(grade_entry_form.grade_entry_students.find_by(role: student).grades.first.grade).to eq(5)
1✔
457
      end
458
    end
459

460
    context 'DELETE Destroy' do
1✔
461
      it 'does not delete a non-existing grade entry form' do
1✔
462
        delete :destroy, params: { course_id: course.id, id: -1 }
1✔
463
        expect(response).to have_http_status(:not_found)
1✔
464
      end
465

466
      it 'successfully deletes a grade entry form with no non-nil grades' do
1✔
467
        form = create(:grade_entry_form, course_id: course.id, id: 4)
1✔
468
        first_student = create(:student)
1✔
469
        second_student = create(:student)
1✔
470
        grade_entry_item = create(:grade_entry_item, out_of: 10, grade_entry_form: form)
1✔
471
        create(:grade, grade_entry_student: form.grade_entry_students.find_by(role: first_student),
1✔
472
                       grade_entry_item: grade_entry_item, grade: nil)
473
        create(:grade, grade_entry_student: form.grade_entry_students.find_by(role: second_student),
1✔
474
                       grade_entry_item: grade_entry_item, grade: nil)
475
        delete :destroy, params: { course_id: course.id, id: 4 }
1✔
476
        expect(response).to have_http_status(:ok)
1✔
477
        expect(course.grade_entry_forms).not_to exist(form.id)
1✔
478
      end
479

480
      it 'does not delete a grade entry form with non-nil grades' do
1✔
481
        form = create(:grade_entry_form, course_id: course.id, id: 4)
1✔
482
        first_student = create(:student)
1✔
483
        second_student = create(:student)
1✔
484
        grade_entry_item = create(:grade_entry_item, out_of: 10, grade_entry_form: form)
1✔
485
        create(:grade, grade_entry_student: form.grade_entry_students.find_by(role: first_student),
1✔
486
                       grade_entry_item: grade_entry_item, grade: nil)
487
        create(:grade, grade_entry_student: form.grade_entry_students.find_by(role: second_student),
1✔
488
                       grade_entry_item: grade_entry_item, grade: 0.2)
489
        delete :destroy, params: { course_id: course.id, id: 4 }
1✔
490
        expect(response).to have_http_status(:conflict)
1✔
491
        expect(course.grade_entry_forms).to exist(form.id)
1✔
492
      end
493
    end
494
  end
495
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc