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

MarkUsProject / Markus / 23774049070

31 Mar 2026 12:13AM UTC coverage: 91.727% (+0.01%) from 91.713%
23774049070

Pull #7886

github

web-flow
Merge ca5a7b83f 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 %.

114 of 116 new or added lines in 2 files covered. (98.28%)

23 existing lines in 2 files now uncovered.

45265 of 48539 relevant lines covered (93.25%)

130.06 hits per line

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

99.32
/spec/controllers/api/grade_entry_forms_controller_spec.rb
1
describe Api::GradeEntryFormsController do
1✔
2
  let(:course) { create(:course) }
55✔
3
  let(:grade_entry_form) { create(:grade_entry_form, course: course) }
22✔
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)
50✔
36
      instructor.reset_api_key
50✔
37
      request.env['HTTP_AUTHORIZATION'] = "MarkUsAuth #{instructor.api_key.strip}"
50✔
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' }
9✔
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
        context 'with students, grades, and multiple items' do
1✔
203
          let!(:item1) do
1✔
204
            create(:grade_entry_item, grade_entry_form: grade_entry_form, out_of: 10, name: 'Q1', position: 1)
3✔
205
          end
206
          let!(:item2) do
1✔
207
            create(:grade_entry_item, grade_entry_form: grade_entry_form, out_of: 5, name: 'Q2', position: 2)
3✔
208
          end
209
          let!(:graded_student) { create(:student, course: course) }
4✔
210
          let!(:ungraded_student) { create(:student, course: course) }
4✔
211
          let!(:hidden_student) { create(:student, course: course, hidden: true) }
4✔
212

213
          before do
1✔
214
            ges = grade_entry_form.grade_entry_students.find_by(role: graded_student)
3✔
215
            create(:grade, grade_entry_student: ges, grade_entry_item: item1, grade: 8.0)
3✔
216
            create(:grade, grade_entry_student: ges, grade_entry_item: item2, grade: 4.0)
3✔
217
            get :show, params: { id: grade_entry_form.id, course_id: course.id }
3✔
218
          end
219

220
          it 'returns items in order with correct fields' do
1✔
221
            items = response.parsed_body['grade_entry_items']
1✔
222
            expect(items.pluck('name')).to eq(%w[Q1 Q2])
1✔
223
            expect(items.first).to include('name' => 'Q1', 'out_of' => 10, 'bonus' => false)
1✔
224
          end
225

226
          it 'returns graded student with grades hash and ungraded student with empty hash' do
1✔
227
            students = response.parsed_body['students']
1✔
228
            graded = students.find { |s| s['user_name'] == graded_student.user_name }
3✔
229
            ungraded = students.find { |s| s['user_name'] == ungraded_student.user_name }
2✔
230
            expect(graded['grades']).to eq({ 'Q1' => 8.0, 'Q2' => 4.0 })
1✔
231
            expect(ungraded['grades']).to eq({})
1✔
232
          end
233

234
          it 'excludes hidden students' do
1✔
235
            user_names = response.parsed_body['students'].pluck('user_name')
1✔
236
            expect(user_names).not_to include(hidden_student.user_name)
1✔
237
          end
238
        end
239

240
        context 'with show_total' do
1✔
241
          let!(:grade_entry_item) { create(:grade_entry_item, grade_entry_form: form, out_of: 10, name: 'Q1') }
3✔
242
          let!(:student) { create(:student, course: course) }
3✔
243

244
          before do
1✔
245
            ges = form.grade_entry_students.find_by(role: student)
2✔
246
            create(:grade, grade_entry_student: ges, grade_entry_item: grade_entry_item, grade: 7.0)
2✔
247
          end
248

249
          context 'enabled' do
1✔
250
            let(:form) { create(:grade_entry_form, course: course, show_total: true) }
2✔
251

252
            it 'includes total_grade per student' do
1✔
253
              get :show, params: { id: form.id, course_id: course.id }
1✔
254
              expect(response.parsed_body['students'].first['total_grade']).to eq(7.0)
1✔
255
            end
256
          end
257

258
          context 'disabled' do
1✔
259
            let(:form) { create(:grade_entry_form, course: course, show_total: false) }
2✔
260

261
            it 'omits total_grade' do
1✔
262
              get :show, params: { id: form.id, course_id: course.id }
1✔
263
              expect(response.parsed_body['students'].first).not_to have_key('total_grade')
1✔
264
            end
265
          end
266
        end
267

268
        context 'with user_name filter' do
1✔
269
          let!(:grade_entry_item) do
1✔
270
            create(:grade_entry_item, grade_entry_form: grade_entry_form, out_of: 10, name: 'Q1')
2✔
271
          end
272
          let!(:student1) { create(:student, course: course) }
3✔
273
          let!(:student2) { create(:student, course: course) }
3✔
274

275
          before do
1✔
276
            ges1 = grade_entry_form.grade_entry_students.find_by(role: student1)
2✔
277
            create(:grade, grade_entry_student: ges1, grade_entry_item: grade_entry_item, grade: 9.0)
2✔
278
            ges2 = grade_entry_form.grade_entry_students.find_by(role: student2)
2✔
279
            create(:grade, grade_entry_student: ges2, grade_entry_item: grade_entry_item, grade: 6.0)
2✔
280
          end
281

282
          it 'returns only the matching student' do
1✔
283
            get :show, params: { id: grade_entry_form.id, course_id: course.id, user_name: student1.user_name }
1✔
284
            students = response.parsed_body['students']
1✔
285
            expect(students.length).to eq(1)
1✔
286
            expect(students.first['user_name']).to eq(student1.user_name)
1✔
287
          end
288

289
          it 'returns 422 for nonexistent user_name' do
1✔
290
            get :show, params: { id: grade_entry_form.id, course_id: course.id, user_name: 'nonexistent' }
1✔
291
            expect(response).to have_http_status(:unprocessable_content)
1✔
292
          end
293
        end
294
      end
295

296
      context 'expecting an xml response' do
1✔
297
        before { request.env['HTTP_ACCEPT'] = 'application/xml' }
2✔
298

299
        it 'returns xml with grade_entry_form root' do
1✔
300
          get :show, params: { id: grade_entry_form.id, course_id: course.id }
1✔
301
          expect(response).to have_http_status(:ok)
1✔
302
          expect(Hash.from_xml(response.body)).to have_key('grade_entry_form')
1✔
303
        end
304
      end
305

306
      it 'returns CSV when download=csv' do
1✔
307
        get :show, params: { id: grade_entry_form.id, course_id: course.id, download: 'csv' }
1✔
308
        expect(response).to have_http_status(:ok)
1✔
309
        expect(response.content_type).to include('text/csv')
1✔
310
      end
311

312
      it 'returns 404 for non-existant grade entry form' do
1✔
313
        get :show, params: { id: -1, course_id: course.id }
1✔
314
        expect(response).to have_http_status(:not_found)
1✔
315
      end
316

317
      it 'returns 403 for a grade entry form in a different course' do
1✔
318
        other_form = create(:grade_entry_form, course: create(:course))
1✔
319
        get :show, params: { id: other_form.id, course_id: other_form.course_id }
1✔
320
        expect(response).to have_http_status(:forbidden)
1✔
321
      end
322
    end
323

324
    context 'POST create' do
1✔
325
      let(:params) do
1✔
326
        { short_identifier: 'A0', description: 'Test', course_id: course.id }
11✔
327
      end
328
      let(:full_params) do
1✔
UNCOV
329
        { short_identifier: 'A0', course_id: course.id, description: 'Test',
×
330
          due_date: '2012-03-26 18:04:39', is_hidden: false,
331
          grade_entry_items: [
332
            { name: 'col1', out_of: 10, bonus: false },
333
            { name: 'col2', out_of: 2, bonus: true }
334
          ] }
335
      end
336

337
      context 'with minimal required params' do
1✔
338
        it 'should respond with 201' do
1✔
339
          post :create, params: params
1✔
340
          expect(response).to have_http_status(:created)
1✔
341
        end
342

343
        it 'should create an assignment' do
1✔
344
          expect(GradeEntryForm.find_by(short_identifier: params[:short_identifier])).to be_nil
1✔
345
          post :create, params: params
1✔
346
          expect(GradeEntryForm.find_by(short_identifier: params[:short_identifier])).not_to be_nil
1✔
347
        end
348

349
        context 'for a different course' do
1✔
350
          it 'should response with 403' do
1✔
351
            post :create, params: { **params, course_id: create(:course).id }
1✔
352
            expect(response).to have_http_status(:forbidden)
1✔
353
          end
354
        end
355
      end
356

357
      context 'with all params' do
1✔
358
        it 'should respond with 201' do
1✔
359
          post :create, params: params
1✔
360
          expect(response).to have_http_status(:created)
1✔
361
        end
362

363
        it 'should create an assignment' do
1✔
364
          expect(GradeEntryForm.find_by(short_identifier: params[:short_identifier])).to be_nil
1✔
365
          post :create, params: params
1✔
366
          expect(GradeEntryForm.find_by(short_identifier: params[:short_identifier])).not_to be_nil
1✔
367
        end
368
      end
369

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

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

383
        context 'missing description' do
1✔
384
          it 'should respond with 422' do
1✔
385
            post :create, params: params.slice(:short_identifier, :due_date, :course_id)
1✔
386
            expect(response).to have_http_status(:unprocessable_content)
1✔
387
          end
388

389
          it 'should not create an assignment' do
1✔
390
            post :create, params: params.slice(:short_identifier, :due_date, :course_id)
1✔
391
            expect(GradeEntryForm.find_by(short_identifier: params[:short_identifier])).to be_nil
1✔
392
          end
393
        end
394
      end
395

396
      context 'where short_identifier is already taken' do
1✔
397
        it 'should respond with 409' do
1✔
398
          grade_entry_form = create(:grade_entry_form, course: course)
1✔
399
          post :create, params: { **params, short_identifier: grade_entry_form.short_identifier }
1✔
400
          expect(response).to have_http_status(:conflict)
1✔
401
        end
402
      end
403

404
      context 'where due_date is invalid' do
1✔
405
        it 'should respond with 422' do
1✔
406
          post :create, params: { **params, due_date: 'not a real date' }
1✔
407
          expect(response).to have_http_status(:unprocessable_content)
1✔
408
        end
409
      end
410
    end
411

412
    context 'PUT update' do
1✔
413
      it 'should update an existing assignment' do
1✔
414
        new_desc = grade_entry_form.description + 'more!'
1✔
415
        put :update, params: { id: grade_entry_form.id, description: new_desc, course_id: course.id }
1✔
416
        expect(response).to have_http_status(:ok)
1✔
417
      end
418

419
      it 'should not update a short identifier' do
1✔
420
        new_short_id = grade_entry_form.short_identifier + 'more!'
1✔
421
        put :update, params: { id: grade_entry_form.id, short_identifier: new_short_id, course_id: course.id }
1✔
422
        expect(response).to have_http_status(:internal_server_error)
1✔
423
      end
424

425
      it 'should not update an assignment that does not exist' do
1✔
426
        new_desc = grade_entry_form.description + 'more!'
1✔
427
        put :update, params: { id: -1, description: new_desc, course_id: course.id }
1✔
428
        expect(response).to have_http_status(:not_found)
1✔
429
      end
430

431
      context 'for a different course' do
1✔
432
        let(:grade_entry_form) { create(:grade_entry_form, course: create(:course)) }
2✔
433

434
        it 'should response with 403' do
1✔
435
          new_desc = grade_entry_form.description + 'more!'
1✔
436
          put :update, params: { id: grade_entry_form.id, description: new_desc, course_id: grade_entry_form.course.id }
1✔
437
          expect(response).to have_http_status(:forbidden)
1✔
438
        end
439
      end
440
    end
441

442
    context 'PUT update_grades' do
1✔
443
      let(:student) { create(:student) }
3✔
444
      let(:grade_params) do
1✔
UNCOV
445
        { short_identifier: 'A0', course_id: course.id, description: 'Test',
×
446
          due_date: '2012-03-26 18:04:39', is_hidden: false,
447
          id: grade_entry_form.id,
448
          grade_entry_items: [
449
            { name: 'col1', out_of: 10, bonus: false },
450
            { name: 'col2', out_of: 2, bonus: true }
451
          ] }
452
      end
453

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

456
      it 'creates new grades' do
1✔
457
        put :update_grades,
1✔
458
            params: { course_id: course.id, id: grade_entry_form.id, user_name: student.user_name,
459
                      grade_entry_items: { col1: 2 } }
460
        expect(grade_entry_form.grade_entry_students.find_by(role: student).grades.count).to eq(1)
1✔
461
      end
462

463
      it 'updates existing grades' do
1✔
464
        put :update_grades,
1✔
465
            params: { course_id: course.id, id: grade_entry_form.id, user_name: student.user_name,
466
                      grade_entry_items: { col1: 2 } }
467
        put :update_grades,
1✔
468
            params: { course_id: course.id, id: grade_entry_form.id, user_name: student.user_name,
469
                      grade_entry_items: { col1: 5 } }
470
        expect(grade_entry_form.grade_entry_students.find_by(role: student).grades.first.grade).to eq(5)
1✔
471
      end
472
    end
473

474
    context 'DELETE Destroy' do
1✔
475
      it 'does not delete a non-existing grade entry form' do
1✔
476
        delete :destroy, params: { course_id: course.id, id: -1 }
1✔
477
        expect(response).to have_http_status(:not_found)
1✔
478
      end
479

480
      it 'successfully deletes a grade entry form with no 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: nil)
489
        delete :destroy, params: { course_id: course.id, id: 4 }
1✔
490
        expect(response).to have_http_status(:ok)
1✔
491
        expect(course.grade_entry_forms).not_to exist(form.id)
1✔
492
      end
493

494
      it 'does not delete a grade entry form with non-nil grades' do
1✔
495
        form = create(:grade_entry_form, course_id: course.id, id: 4)
1✔
496
        first_student = create(:student)
1✔
497
        second_student = create(:student)
1✔
498
        grade_entry_item = create(:grade_entry_item, out_of: 10, grade_entry_form: form)
1✔
499
        create(:grade, grade_entry_student: form.grade_entry_students.find_by(role: first_student),
1✔
500
                       grade_entry_item: grade_entry_item, grade: nil)
501
        create(:grade, grade_entry_student: form.grade_entry_students.find_by(role: second_student),
1✔
502
                       grade_entry_item: grade_entry_item, grade: 0.2)
503
        delete :destroy, params: { course_id: course.id, id: 4 }
1✔
504
        expect(response).to have_http_status(:conflict)
1✔
505
        expect(course.grade_entry_forms).to exist(form.id)
1✔
506
      end
507
    end
508
  end
509
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