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

MarkUsProject / Markus / 23815623133

31 Mar 2026 07:30PM UTC coverage: 91.726% (+0.01%) from 91.713%
23815623133

Pull #7886

github

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

106 of 108 new or added lines in 2 files covered. (98.15%)

23 existing lines in 3 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.29
/spec/controllers/api/grade_entry_forms_controller_spec.rb
1
describe Api::GradeEntryFormsController do
1✔
2
  let(:course) { create(:course) }
52✔
3
  let(:grade_entry_form) { create(:grade_entry_form, course: course) }
19✔
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)
47✔
36
      instructor.reset_api_key
47✔
37
      request.env['HTTP_AUTHORIZATION'] = "MarkUsAuth #{instructor.api_key.strip}"
47✔
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
      it 'returns CSV by default' do
1✔
283
        get :show, params: { id: grade_entry_form.id, course_id: course.id }
1✔
284
        expect(response).to have_http_status(:ok)
1✔
285
        expect(response.content_type).to include('text/csv')
1✔
286
      end
287

288
      it 'returns 404 for non-existant grade entry form' do
1✔
289
        get :show, params: { id: -1, course_id: course.id }
1✔
290
        expect(response).to have_http_status(:not_found)
1✔
291
      end
292

293
      it 'returns 403 for a grade entry form in a different course' do
1✔
294
        other_form = create(:grade_entry_form, course: create(:course))
1✔
295
        get :show, params: { id: other_form.id, course_id: other_form.course_id }
1✔
296
        expect(response).to have_http_status(:forbidden)
1✔
297
      end
298
    end
299

300
    context 'POST create' do
1✔
301
      let(:params) do
1✔
302
        { short_identifier: 'A0', description: 'Test', course_id: course.id }
11✔
303
      end
304
      let(:full_params) do
1✔
305
        { short_identifier: 'A0', course_id: course.id, description: 'Test',
×
306
          due_date: '2012-03-26 18:04:39', is_hidden: false,
307
          grade_entry_items: [
308
            { name: 'col1', out_of: 10, bonus: false },
309
            { name: 'col2', out_of: 2, bonus: true }
310
          ] }
311
      end
312

313
      context 'with minimal required params' do
1✔
314
        it 'should respond with 201' do
1✔
315
          post :create, params: params
1✔
316
          expect(response).to have_http_status(:created)
1✔
317
        end
318

319
        it 'should create an assignment' do
1✔
320
          expect(GradeEntryForm.find_by(short_identifier: params[:short_identifier])).to be_nil
1✔
321
          post :create, params: params
1✔
322
          expect(GradeEntryForm.find_by(short_identifier: params[:short_identifier])).not_to be_nil
1✔
323
        end
324

325
        context 'for a different course' do
1✔
326
          it 'should response with 403' do
1✔
327
            post :create, params: { **params, course_id: create(:course).id }
1✔
328
            expect(response).to have_http_status(:forbidden)
1✔
329
          end
330
        end
331
      end
332

333
      context 'with all params' do
1✔
334
        it 'should respond with 201' do
1✔
335
          post :create, params: params
1✔
336
          expect(response).to have_http_status(:created)
1✔
337
        end
338

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

346
      context 'with missing params' do
1✔
347
        context 'missing short_id' do
1✔
348
          it 'should respond with 422' do
1✔
349
            post :create, params: params.slice(:description, :due_date, :course_id)
1✔
350
            expect(response).to have_http_status(:unprocessable_content)
1✔
351
          end
352

353
          it 'should not create an assignment' do
1✔
354
            post :create, params: params.slice(:description, :due_date, :course_id)
1✔
355
            expect(Assignment.find_by(description: params[:description])).to be_nil
1✔
356
          end
357
        end
358

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

365
          it 'should not create an assignment' do
1✔
366
            post :create, params: params.slice(:short_identifier, :due_date, :course_id)
1✔
367
            expect(GradeEntryForm.find_by(short_identifier: params[:short_identifier])).to be_nil
1✔
368
          end
369
        end
370
      end
371

372
      context 'where short_identifier is already taken' do
1✔
373
        it 'should respond with 409' do
1✔
374
          grade_entry_form = create(:grade_entry_form, course: course)
1✔
375
          post :create, params: { **params, short_identifier: grade_entry_form.short_identifier }
1✔
376
          expect(response).to have_http_status(:conflict)
1✔
377
        end
378
      end
379

380
      context 'where due_date is invalid' do
1✔
381
        it 'should respond with 422' do
1✔
382
          post :create, params: { **params, due_date: 'not a real date' }
1✔
383
          expect(response).to have_http_status(:unprocessable_content)
1✔
384
        end
385
      end
386
    end
387

388
    context 'PUT update' do
1✔
389
      it 'should update an existing assignment' do
1✔
390
        new_desc = grade_entry_form.description + 'more!'
1✔
391
        put :update, params: { id: grade_entry_form.id, description: new_desc, course_id: course.id }
1✔
392
        expect(response).to have_http_status(:ok)
1✔
393
      end
394

395
      it 'should not update a short identifier' do
1✔
396
        new_short_id = grade_entry_form.short_identifier + 'more!'
1✔
397
        put :update, params: { id: grade_entry_form.id, short_identifier: new_short_id, course_id: course.id }
1✔
398
        expect(response).to have_http_status(:internal_server_error)
1✔
399
      end
400

401
      it 'should not update an assignment that does not exist' do
1✔
402
        new_desc = grade_entry_form.description + 'more!'
1✔
403
        put :update, params: { id: -1, description: new_desc, course_id: course.id }
1✔
404
        expect(response).to have_http_status(:not_found)
1✔
405
      end
406

407
      context 'for a different course' do
1✔
408
        let(:grade_entry_form) { create(:grade_entry_form, course: create(:course)) }
2✔
409

410
        it 'should response with 403' do
1✔
411
          new_desc = grade_entry_form.description + 'more!'
1✔
412
          put :update, params: { id: grade_entry_form.id, description: new_desc, course_id: grade_entry_form.course.id }
1✔
413
          expect(response).to have_http_status(:forbidden)
1✔
414
        end
415
      end
416
    end
417

418
    context 'PUT update_grades' do
1✔
419
      let(:student) { create(:student) }
3✔
420
      let(:grade_params) do
1✔
UNCOV
421
        { short_identifier: 'A0', course_id: course.id, description: 'Test',
×
422
          due_date: '2012-03-26 18:04:39', is_hidden: false,
423
          id: grade_entry_form.id,
424
          grade_entry_items: [
425
            { name: 'col1', out_of: 10, bonus: false },
426
            { name: 'col2', out_of: 2, bonus: true }
427
          ] }
428
      end
429

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

432
      it 'creates new grades' do
1✔
433
        put :update_grades,
1✔
434
            params: { course_id: course.id, id: grade_entry_form.id, user_name: student.user_name,
435
                      grade_entry_items: { col1: 2 } }
436
        expect(grade_entry_form.grade_entry_students.find_by(role: student).grades.count).to eq(1)
1✔
437
      end
438

439
      it 'updates existing grades' do
1✔
440
        put :update_grades,
1✔
441
            params: { course_id: course.id, id: grade_entry_form.id, user_name: student.user_name,
442
                      grade_entry_items: { col1: 2 } }
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: 5 } }
446
        expect(grade_entry_form.grade_entry_students.find_by(role: student).grades.first.grade).to eq(5)
1✔
447
      end
448
    end
449

450
    context 'DELETE Destroy' do
1✔
451
      it 'does not delete a non-existing grade entry form' do
1✔
452
        delete :destroy, params: { course_id: course.id, id: -1 }
1✔
453
        expect(response).to have_http_status(:not_found)
1✔
454
      end
455

456
      it 'successfully deletes a grade entry form with no non-nil grades' do
1✔
457
        form = create(:grade_entry_form, course_id: course.id, id: 4)
1✔
458
        first_student = create(:student)
1✔
459
        second_student = create(:student)
1✔
460
        grade_entry_item = create(:grade_entry_item, out_of: 10, grade_entry_form: form)
1✔
461
        create(:grade, grade_entry_student: form.grade_entry_students.find_by(role: first_student),
1✔
462
                       grade_entry_item: grade_entry_item, grade: nil)
463
        create(:grade, grade_entry_student: form.grade_entry_students.find_by(role: second_student),
1✔
464
                       grade_entry_item: grade_entry_item, grade: nil)
465
        delete :destroy, params: { course_id: course.id, id: 4 }
1✔
466
        expect(response).to have_http_status(:ok)
1✔
467
        expect(course.grade_entry_forms).not_to exist(form.id)
1✔
468
      end
469

470
      it 'does not delete a grade entry form with non-nil grades' do
1✔
471
        form = create(:grade_entry_form, course_id: course.id, id: 4)
1✔
472
        first_student = create(:student)
1✔
473
        second_student = create(:student)
1✔
474
        grade_entry_item = create(:grade_entry_item, out_of: 10, grade_entry_form: form)
1✔
475
        create(:grade, grade_entry_student: form.grade_entry_students.find_by(role: first_student),
1✔
476
                       grade_entry_item: grade_entry_item, grade: nil)
477
        create(:grade, grade_entry_student: form.grade_entry_students.find_by(role: second_student),
1✔
478
                       grade_entry_item: grade_entry_item, grade: 0.2)
479
        delete :destroy, params: { course_id: course.id, id: 4 }
1✔
480
        expect(response).to have_http_status(:conflict)
1✔
481
        expect(course.grade_entry_forms).to exist(form.id)
1✔
482
      end
483
    end
484
  end
485
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