quiz_api_client 4.23.0 → 4.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 530745dd8b2114222d9d503cd841439525f7679384290a71d9313f4fa6fe94a2
4
- data.tar.gz: 6e6b594abea72e7f49947a70dd45229ba2948fcba301a8e90ed9be1fa05716ec
3
+ metadata.gz: 271c4523cdeb2e7861c0292366ce7db0e6f87ab37a1406d5a9bc5fb001ad935e
4
+ data.tar.gz: 3e290ab1b995fbd19c9b00e0b73a8cea69974a15f77859900a4a8d66f1876c85
5
5
  SHA512:
6
- metadata.gz: a603aaaa981cf20b625656fda29046a726cd36132b21e872e3dc9616aaeb3220c58b60fa43ad20ea9b9a015b2a6cc46625e83623f404ab8c59c71aa75b07b0e1
7
- data.tar.gz: 132bef5130d34e4005845e8af45805de26f882f4b46f6515f61b5f57bdd4a0f47f07d731a99b34a510c59ecec4979d7a54e1200eefca66e4f3cce93bd8afdd49
6
+ metadata.gz: 2f125d17891e1fddacaaab43282ac302e88668277e00df91cd184358cca2ee5c0303cf97345eb2a592e53d8237d126e13319b6169db5e091548f3434153624e0
7
+ data.tar.gz: 904d69175eee4889c4c12f17ca57d1306f909fe585171b1d100229e0036c57b9e70686e05a602fb782258dac16d2f1ff8fd041fb8f6f8c8ec8361f0415c1877d
data/README.md CHANGED
@@ -187,3 +187,37 @@ Then, run `bundle exec rake spec` to run the tests.
187
187
  You can also run `bundle exec rake console` for an interactive prompt that will allow you to experiment.
188
188
 
189
189
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
190
+
191
+ ## Development with Quiz LTI
192
+
193
+ Copy this repo inside the Quiz LTI docker container:
194
+
195
+ ```shell
196
+ docker cp . quiz-lti-web:/quiz_api_client_ruby
197
+ ```
198
+
199
+ Tell Quiz LTI to use the gem, find the line containing `gem 'quiz_api_client'` in `app.rb`, update it to:
200
+
201
+ ```shell
202
+ gem 'quiz_api_client', path: '/quiz_api_client_ruby'
203
+ ```
204
+
205
+ Run bundle install the container:
206
+
207
+ ```shell
208
+ docker exec quiz-lti-web bundle install
209
+ ```
210
+
211
+ Restart the container:
212
+
213
+ ```shell
214
+ docker restart quiz-lti-web
215
+ ```
216
+
217
+ Your changes should be reflected in the container!
218
+
219
+ If you update the code afterwards you'll need to copy and restart the container:
220
+
221
+ ```shell
222
+ docker cp . quiz-lti-web:gems/quiz_api_client_ruby && docker restart quiz-lti-web
223
+ ```
@@ -2,7 +2,7 @@ module QuizApiClient
2
2
  class Config
3
3
  DEFAULT_ALLOWABLE_RESPONSE_CODES = [401, 422].freeze
4
4
  DEFAULT_PROTOCOL = 'https'.freeze
5
- ERROR_HANDLERS = %i[sentry_raven].freeze
5
+ ERROR_HANDLERS = %i[sentry_raven sentry_rails].freeze
6
6
  METRICS_HANDLERS = %i[inststatsd].freeze
7
7
 
8
8
  class InvalidErrorHandler < StandardError; end
@@ -48,6 +48,10 @@ module QuizApiClient
48
48
  when :sentry_raven
49
49
  require 'sentry-raven'
50
50
  Raven.extra_context(context)
51
+ when :sentry_rails
52
+ require 'sentry-ruby'
53
+ require 'sentry-rails'
54
+ Sentry.set_extras(context)
51
55
  end
52
56
  end
53
57
 
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuizApiClient
4
+ module Services
5
+ class ContentMigrationJobService < BaseApiService
6
+ def create(params:, token: nil)
7
+ post_to_quiz_api(params: params, token: token)
8
+ end
9
+
10
+ def retry(params:, token: nil)
11
+ retry_on_quiz_api(params: params, token: token)
12
+ end
13
+
14
+ def show(params:, token: nil)
15
+ get_from_quiz_api(params: params, token: token)
16
+ end
17
+
18
+ private
19
+
20
+ def post_to_quiz_api(params:, token:)
21
+ client(token: token).post(
22
+ '/api/content_migration/jobs',
23
+ course: params
24
+ )
25
+ end
26
+
27
+ def retry_on_quiz_api(params:, token:)
28
+ client(token: token).post(
29
+ '/api/content_migration/jobs/retry',
30
+ params
31
+ )
32
+ end
33
+
34
+ def get_from_quiz_api(params:, token:)
35
+ client(token: token).get(
36
+ "/api/content_migration/jobs/#{params[:external_content_migration_id]}"
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,3 +1,3 @@
1
1
  module QuizApiClient
2
- VERSION = '4.23.0'.freeze
2
+ VERSION = '4.25.0'.freeze
3
3
  end
@@ -129,6 +129,10 @@ module QuizApiClient
129
129
  def youtube_service
130
130
  @_youtube_service ||= Services::YoutubeService.new(config)
131
131
  end
132
+
133
+ def content_migration_job_service
134
+ @_content_migration_job_service ||= Services::ContentMigrationJobService.new(config)
135
+ end
132
136
  end
133
137
  end
134
138
 
@@ -168,3 +172,4 @@ require 'quiz_api_client/services/session_items_service'
168
172
  require 'quiz_api_client/services/session_item_results_service'
169
173
  require 'quiz_api_client/services/shared_banks'
170
174
  require 'quiz_api_client/services/youtube_service'
175
+ require 'quiz_api_client/services/content_migration_job_service'
@@ -360,6 +360,162 @@ describe QuizApiClient::HttpClient do
360
360
  expect { client.get('/') }.to raise_error(QuizApiClient::HttpClient::RequestFailed)
361
361
  end
362
362
 
363
+ it 'handles an Net::ReadTimeout error' do
364
+ stub_request(:get, url).to_raise(Net::ReadTimeout)
365
+ expect_metrics_calls(client.config, :get, 0)
366
+ expect_raise_error_call(client.config, :get, url_for_path(path), nil, kind_of(Net::ReadTimeout))
367
+ expect { client.get('/') }.to raise_error(QuizApiClient::HttpClient::RequestFailed)
368
+ end
369
+ end
370
+ context 'with :sentry_rails error handler' do
371
+ let(:error_handler) { :sentry_rails }
372
+ let(:path) { '/' }
373
+ let(:url) { "http://api.quiz.docker#{path}" }
374
+ let(:error_context) do
375
+ {
376
+ quiz_api_client: {
377
+ request: {
378
+ method: :get,
379
+ url: url
380
+ }
381
+ }
382
+ }
383
+ end
384
+
385
+ before do
386
+ client.config.error_handler = error_handler
387
+ end
388
+
389
+ context 'with non-success responses' do
390
+ let(:body) { '[2]' }
391
+ let(:code) { 404 }
392
+ let(:path) { '/api/quizzes' }
393
+ let(:context_url) { url }
394
+ let(:method) { :get }
395
+ let(:error_context) do
396
+ {
397
+ quiz_api_client: {
398
+ request: {
399
+ method: method,
400
+ url: context_url
401
+ },
402
+ response: {
403
+ body: body,
404
+ code: code
405
+ }
406
+ }
407
+ }
408
+ end
409
+ let(:mock_response) do
410
+ instance_double(
411
+ 'HTTParty::Response',
412
+ success?: false,
413
+ body: body,
414
+ code: code
415
+ )
416
+ end
417
+ let(:success_response) do
418
+ # rubocop:disable Metrics/LineLength
419
+ instance_double(
420
+ 'HTTParty::Response',
421
+ body: body,
422
+ code: 200,
423
+ parsed_response: [1],
424
+ headers: {
425
+ 'link' => '<http://api.quiz.docker/api/quizzes?page=2>; rel="last", <http://api.quiz.docker/api/quizzes?page=2>; rel="next"',
426
+ 'content-type' => ['application/json']
427
+ }
428
+ )
429
+ # rubocop:enable Metrics/LineLength
430
+ end
431
+
432
+ context ':get request' do
433
+ it 'raises error' do
434
+ expect(client).to receive(:successful_response?).with(mock_response).and_return(false)
435
+ expect(client.class).to receive(:get).and_return(mock_response)
436
+ expect_metrics_calls(client.config, method, code)
437
+ expect_raise_error_call(client.config, method, url_for_path(path), mock_response, nil)
438
+ expect { client.get(path, all: false, query: { sort: 'alpha' }) }
439
+ .to raise_error(QuizApiClient::HttpClient::RequestFailed)
440
+ end
441
+ end
442
+
443
+ context ':post request' do
444
+ let(:method) { :post }
445
+
446
+ it 'raises error' do
447
+ expect(client).to receive(:successful_response?).with(mock_response).and_return(false)
448
+ expect(client.class).to receive(:post).and_return(mock_response)
449
+ expect_metrics_calls(client.config, method, code)
450
+ expect_raise_error_call(client.config, method, url_for_path(path), mock_response, nil)
451
+ expect { client.post(path) }.to raise_error(QuizApiClient::HttpClient::RequestFailed)
452
+ end
453
+ end
454
+
455
+ context ':put request' do
456
+ let(:method) { :put }
457
+
458
+ it 'raises error' do
459
+ expect(client).to receive(:successful_response?).with(mock_response).and_return(false)
460
+ expect(client.class).to receive(:put).and_return(mock_response)
461
+ expect_metrics_calls(client.config, method, code)
462
+ expect_raise_error_call(client.config, method, url_for_path(path), mock_response, nil)
463
+ expect { client.put(path) }.to raise_error(QuizApiClient::HttpClient::RequestFailed)
464
+ end
465
+ end
466
+
467
+ context ':patch request' do
468
+ let(:method) { :patch }
469
+
470
+ it 'raises error' do
471
+ expect(client).to receive(:successful_response?).with(mock_response).and_return(false)
472
+ expect(client.class).to receive(:patch).and_return(mock_response)
473
+ expect_metrics_calls(client.config, method, code)
474
+ expect_raise_error_call(client.config, method, url_for_path(path), mock_response, nil)
475
+ expect { client.patch(path) }.to raise_error(QuizApiClient::HttpClient::RequestFailed)
476
+ end
477
+ end
478
+
479
+ context ':delete request' do
480
+ let(:method) { :delete }
481
+
482
+ it 'raises error' do
483
+ expect(client).to receive(:successful_response?).with(mock_response).and_return(false)
484
+ expect(client.class).to receive(:delete).and_return(mock_response)
485
+ expect_metrics_calls(client.config, method, code)
486
+ expect_raise_error_call(client.config, method, url_for_path(path), mock_response, nil)
487
+ expect { client.delete(path) }.to raise_error(QuizApiClient::HttpClient::RequestFailed)
488
+ end
489
+ end
490
+
491
+ context 'with pagination' do
492
+ let(:context_url) { "#{url}?page=2" }
493
+
494
+ it 'raises error if one of the linked pages does not return 200 response' do
495
+ stub_quiz_api path, headers: { link: link_header(path, 1, 2) }
496
+ stub_quiz_api path, item: 2, query: { page: 2 }, headers: { link: link_header(path, 2, 2) }, status: code
497
+ allow(HTTParty::Response).to receive(:new).and_return(success_response, mock_response)
498
+ expect(client).to receive(:successful_response?).and_return(true, false).twice
499
+ expect_raise_error_call(client.config, method, context_url, mock_response, nil)
500
+ expect { client.get('/api/quizzes', all: true) }.to raise_error(QuizApiClient::HttpClient::RequestFailed)
501
+ end
502
+ end
503
+ end
504
+
505
+ it 'handles an HTTParty::Error error' do
506
+ stub_request(:get, url).to_raise(HTTParty::Error)
507
+ expect_metrics_calls(client.config, :get, 0)
508
+ expect_raise_error_call(client.config, :get, url_for_path(path), nil, kind_of(HTTParty::Error))
509
+ expect { client.get('/') }.to raise_error(QuizApiClient::HttpClient::RequestFailed)
510
+ end
511
+
512
+ it 'handles an Errno::ECONNREFUSED error' do
513
+ stub_request(:get, url).to_raise(Errno::ECONNREFUSED)
514
+ expect_metrics_calls(client.config, :get, 0)
515
+ expect_raise_error_call(client.config, :get, url_for_path(path), nil, kind_of(Errno::ECONNREFUSED))
516
+ expect { client.get('/') }.to raise_error(QuizApiClient::HttpClient::RequestFailed)
517
+ end
518
+
363
519
  it 'handles an Net::ReadTimeout error' do
364
520
  stub_request(:get, url).to_raise(Net::ReadTimeout)
365
521
  expect_metrics_calls(client.config, :get, 0)
@@ -162,5 +162,12 @@ describe QuizApiClient do
162
162
  subject.interaction_types_service
163
163
  end
164
164
  end
165
+
166
+ describe '#content_migration_job_service' do
167
+ it 'should create the service' do
168
+ expect(QuizApiClient::Services::ContentMigrationJobService).to receive(:new).with(subject.config)
169
+ subject.content_migration_job_service
170
+ end
171
+ end
165
172
  end
166
173
  end
@@ -0,0 +1,125 @@
1
+ describe QuizApiClient::Services::ContentMigrationJobService do
2
+ let(:host) { 'api.host' }
3
+ let(:config) { QuizApiClient::Config.new { |c| c.host = host } }
4
+ let(:subject) { described_class.new(config) }
5
+
6
+ describe '#create' do
7
+ let(:params) do
8
+ {
9
+ canvas_course_id: 123,
10
+ external_content_migration_id: 'abc-123',
11
+ settings: {
12
+ import_quizzes_next: true
13
+ }
14
+ }
15
+ end
16
+
17
+ let(:expected_url) { "https://#{host}/api/content_migration/jobs" }
18
+
19
+ let(:stubbed_response) do
20
+ {
21
+ 'id' => '1',
22
+ 'external_content_migration_id' => 'abc-123',
23
+ 'status' => 'queued',
24
+ 'created_at' => '2024-01-01T00:00:00.000Z',
25
+ 'updated_at' => '2024-01-01T00:00:00.000Z'
26
+ }
27
+ end
28
+
29
+ before do
30
+ stub_request(:post, expected_url)
31
+ .with(body: { course: params }.to_json)
32
+ .to_return(
33
+ status: 201,
34
+ body: stubbed_response.to_json,
35
+ headers: { 'Content-Type' => 'application/json' }
36
+ )
37
+ end
38
+
39
+ it 'posts to the correct endpoint and returns the response' do
40
+ expect(
41
+ subject.create(params: params, token: 'token').parsed_response
42
+ ).to eq(stubbed_response)
43
+ end
44
+ end
45
+
46
+ describe '#retry' do
47
+ let(:params) do
48
+ {
49
+ external_content_migration_id: 'abc-123'
50
+ }
51
+ end
52
+
53
+ let(:expected_url) { "https://#{host}/api/content_migration/jobs/retry" }
54
+
55
+ let(:stubbed_response) do
56
+ {
57
+ 'id' => '1',
58
+ 'external_content_migration_id' => 'abc-123',
59
+ 'status' => 'queued',
60
+ 'created_at' => '2024-01-01T00:00:00.000Z',
61
+ 'updated_at' => '2024-01-01T00:00:00.000Z'
62
+ }
63
+ end
64
+
65
+ before do
66
+ stub_request(:post, expected_url)
67
+ .with(body: params.to_json)
68
+ .to_return(
69
+ status: 200,
70
+ body: stubbed_response.to_json,
71
+ headers: { 'Content-Type' => 'application/json' }
72
+ )
73
+ end
74
+
75
+ it 'posts to the correct endpoint and returns the response' do
76
+ expect(
77
+ subject.retry(params: params, token: 'token').parsed_response
78
+ ).to eq(stubbed_response)
79
+ end
80
+ end
81
+
82
+ describe '#show' do
83
+ let(:params) { { external_content_migration_id: 'abc-123' } }
84
+ let(:expected_url) { "https://#{host}/api/content_migration/jobs/#{params[:external_content_migration_id]}" }
85
+ let(:stubbed_response) do
86
+ {
87
+ 'id' => '1',
88
+ 'external_content_migration_id' => 'abc-123',
89
+ 'status' => 'completed',
90
+ 'created_at' => '2024-01-01T00:00:00.000Z',
91
+ 'updated_at' => '2024-01-01T00:00:00.000Z'
92
+ }
93
+ end
94
+
95
+ context 'on success' do
96
+ before do
97
+ stub_request(:get, expected_url)
98
+ .to_return(
99
+ status: 200,
100
+ body: stubbed_response.to_json,
101
+ headers: { 'Content-Type' => 'application/json' }
102
+ )
103
+ end
104
+
105
+ it 'gets from /api/content_migration/jobs/:external_content_migration_id' do
106
+ result = subject.show(params: params, token: 'token')
107
+ expect(result.parsed_response).to eql(stubbed_response)
108
+ end
109
+ end
110
+
111
+ context 'on failure' do
112
+ let(:status_code) { 401 }
113
+
114
+ before do
115
+ stub_request(:get, expected_url)
116
+ .to_return(status: status_code)
117
+ end
118
+
119
+ it 'returns a response with the correct code' do
120
+ response = subject.show(params: params, token: 'token')
121
+ expect(response.code).to eq(status_code)
122
+ end
123
+ end
124
+ end
125
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quiz_api_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.23.0
4
+ version: 4.25.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Slaughter
@@ -15,7 +15,7 @@ authors:
15
15
  autorequire:
16
16
  bindir: exe
17
17
  cert_chain: []
18
- date: 2025-09-30 00:00:00.000000000 Z
18
+ date: 2026-04-29 00:00:00.000000000 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: httparty
@@ -115,6 +115,20 @@ dependencies:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
117
  version: 0.74.0
118
+ - !ruby/object:Gem::Dependency
119
+ name: sentry-rails
120
+ requirement: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '6.5'
125
+ type: :development
126
+ prerelease: false
127
+ version_requirements: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '6.5'
118
132
  - !ruby/object:Gem::Dependency
119
133
  name: sentry-raven
120
134
  requirement: !ruby/object:Gem::Requirement
@@ -129,6 +143,20 @@ dependencies:
129
143
  - - "~>"
130
144
  - !ruby/object:Gem::Version
131
145
  version: '3.1'
146
+ - !ruby/object:Gem::Dependency
147
+ name: sentry-ruby
148
+ requirement: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '6.5'
153
+ type: :development
154
+ prerelease: false
155
+ version_requirements: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '6.5'
132
160
  - !ruby/object:Gem::Dependency
133
161
  name: simplecov
134
162
  requirement: !ruby/object:Gem::Requirement
@@ -186,6 +214,7 @@ files:
186
214
  - lib/quiz_api_client/services/bank_service.rb
187
215
  - lib/quiz_api_client/services/base_api_service.rb
188
216
  - lib/quiz_api_client/services/content_exports_service.rb
217
+ - lib/quiz_api_client/services/content_migration_job_service.rb
189
218
  - lib/quiz_api_client/services/courses_service.rb
190
219
  - lib/quiz_api_client/services/interaction_types_service.rb
191
220
  - lib/quiz_api_client/services/item_analyses_service.rb
@@ -221,6 +250,7 @@ files:
221
250
  - spec/services/bank_service_spec.rb
222
251
  - spec/services/base_api_service_spec.rb
223
252
  - spec/services/content_exports_service_spec.rb
253
+ - spec/services/content_migration_job_service_spec.rb
224
254
  - spec/services/courses_service_spec.rb
225
255
  - spec/services/interaction_types_service_spec.rb
226
256
  - spec/services/item_analyses_service_spec.rb
@@ -278,6 +308,7 @@ test_files:
278
308
  - spec/services/bank_service_spec.rb
279
309
  - spec/services/base_api_service_spec.rb
280
310
  - spec/services/content_exports_service_spec.rb
311
+ - spec/services/content_migration_job_service_spec.rb
281
312
  - spec/services/courses_service_spec.rb
282
313
  - spec/services/interaction_types_service_spec.rb
283
314
  - spec/services/item_analyses_service_spec.rb