ams_migration 0.1.3

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.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +9 -0
  3. data/README.md +33 -0
  4. data/lib/ams_migration/canvas/converter/classic_quiz/calculated.rb +33 -0
  5. data/lib/ams_migration/canvas/converter/classic_quiz/classic_question.rb +114 -0
  6. data/lib/ams_migration/canvas/converter/classic_quiz/essay.rb +36 -0
  7. data/lib/ams_migration/canvas/converter/classic_quiz/file_upload.rb +35 -0
  8. data/lib/ams_migration/canvas/converter/classic_quiz/fill_in_blank.rb +63 -0
  9. data/lib/ams_migration/canvas/converter/classic_quiz/fill_in_multiple_blanks.rb +111 -0
  10. data/lib/ams_migration/canvas/converter/classic_quiz/matching.rb +66 -0
  11. data/lib/ams_migration/canvas/converter/classic_quiz/multiple_answer.rb +27 -0
  12. data/lib/ams_migration/canvas/converter/classic_quiz/multiple_choice.rb +57 -0
  13. data/lib/ams_migration/canvas/converter/classic_quiz/multiple_dropdown.rb +126 -0
  14. data/lib/ams_migration/canvas/converter/classic_quiz/numerical.rb +89 -0
  15. data/lib/ams_migration/canvas/converter/classic_quiz/text.rb +28 -0
  16. data/lib/ams_migration/canvas/converter/classic_quiz/true_false.rb +14 -0
  17. data/lib/ams_migration/canvas/converter/classic_quiz/unknown.rb +31 -0
  18. data/lib/ams_migration/canvas/converter/new_quiz/categorization.rb +108 -0
  19. data/lib/ams_migration/canvas/converter/new_quiz/essay.rb +78 -0
  20. data/lib/ams_migration/canvas/converter/new_quiz/file_upload.rb +33 -0
  21. data/lib/ams_migration/canvas/converter/new_quiz/fill_in_blank.rb +180 -0
  22. data/lib/ams_migration/canvas/converter/new_quiz/formula.rb +33 -0
  23. data/lib/ams_migration/canvas/converter/new_quiz/hotspot.rb +166 -0
  24. data/lib/ams_migration/canvas/converter/new_quiz/matching.rb +85 -0
  25. data/lib/ams_migration/canvas/converter/new_quiz/multiple_answer.rb +28 -0
  26. data/lib/ams_migration/canvas/converter/new_quiz/multiple_choice.rb +70 -0
  27. data/lib/ams_migration/canvas/converter/new_quiz/new_question.rb +133 -0
  28. data/lib/ams_migration/canvas/converter/new_quiz/numerical.rb +117 -0
  29. data/lib/ams_migration/canvas/converter/new_quiz/ordering.rb +68 -0
  30. data/lib/ams_migration/canvas/converter/new_quiz/stimulus.rb +35 -0
  31. data/lib/ams_migration/canvas/converter/new_quiz/true_false.rb +33 -0
  32. data/lib/ams_migration/canvas/converter/new_quiz/unknown.rb +30 -0
  33. data/lib/ams_migration/canvas/converter/question_type.rb +30 -0
  34. data/lib/ams_migration/services/README.md +26 -0
  35. data/lib/ams_migration/services/data_provider.rb +26 -0
  36. data/lib/ams_migration/services/learnosity_bank_cleaner.rb +44 -0
  37. data/lib/ams_migration/services/learnosity_data_api_client.rb +135 -0
  38. data/lib/ams_migration/services/learnosity_sink.rb +190 -0
  39. data/lib/ams_migration/services/migration_service.rb +37 -0
  40. data/lib/ams_migration/services/new_quiz_learnosity_transformer.rb +115 -0
  41. data/lib/ams_migration/version.rb +3 -0
  42. data/lib/ams_migration.rb +45 -0
  43. data/lib/factories/classic_quiz_item_converter_factory.rb +43 -0
  44. data/lib/factories/new_quiz_item_converter_factory.rb +76 -0
  45. data/spec/fixtures/sample_data/classic_quiz/calculated.json +24 -0
  46. data/spec/fixtures/sample_data/classic_quiz/essay.json +24 -0
  47. data/spec/fixtures/sample_data/classic_quiz/file_upload.json +24 -0
  48. data/spec/fixtures/sample_data/classic_quiz/fill_in_bank.json +24 -0
  49. data/spec/fixtures/sample_data/classic_quiz/fill_in_blank.json +53 -0
  50. data/spec/fixtures/sample_data/classic_quiz/fill_in_multiple_blanks.json +57 -0
  51. data/spec/fixtures/sample_data/classic_quiz/matching.json +107 -0
  52. data/spec/fixtures/sample_data/classic_quiz/multiple_answer.json +48 -0
  53. data/spec/fixtures/sample_data/classic_quiz/multiple_choice.json +56 -0
  54. data/spec/fixtures/sample_data/classic_quiz/multiple_dropdown.json +73 -0
  55. data/spec/fixtures/sample_data/classic_quiz/numerical.json +55 -0
  56. data/spec/fixtures/sample_data/classic_quiz/text.json +24 -0
  57. data/spec/fixtures/sample_data/classic_quiz/true_false.json +39 -0
  58. data/spec/fixtures/sample_data/classic_quiz/unknown.json +24 -0
  59. data/spec/fixtures/sample_data/new_quiz/bank_entry_item.json +5 -0
  60. data/spec/fixtures/sample_data/new_quiz/bank_item.json +11 -0
  61. data/spec/fixtures/sample_data/new_quiz/categorization.json +135 -0
  62. data/spec/fixtures/sample_data/new_quiz/essay.json +40 -0
  63. data/spec/fixtures/sample_data/new_quiz/file_upload.json +30 -0
  64. data/spec/fixtures/sample_data/new_quiz/fill_in_blank_association.json +162 -0
  65. data/spec/fixtures/sample_data/new_quiz/fill_in_blank_dropdown.json +180 -0
  66. data/spec/fixtures/sample_data/new_quiz/fill_in_blank_math.json +45 -0
  67. data/spec/fixtures/sample_data/new_quiz/fill_in_blank_mixed.json +188 -0
  68. data/spec/fixtures/sample_data/new_quiz/fill_in_blank_text.json +140 -0
  69. data/spec/fixtures/sample_data/new_quiz/formula.json +51 -0
  70. data/spec/fixtures/sample_data/new_quiz/hotspot_oval.json +38 -0
  71. data/spec/fixtures/sample_data/new_quiz/hotspot_polygon.json +66 -0
  72. data/spec/fixtures/sample_data/new_quiz/hotspot_square.json +38 -0
  73. data/spec/fixtures/sample_data/new_quiz/matching.json +88 -0
  74. data/spec/fixtures/sample_data/new_quiz/multiple_answer.json +57 -0
  75. data/spec/fixtures/sample_data/new_quiz/multiple_choice.json +66 -0
  76. data/spec/fixtures/sample_data/new_quiz/multiple_choice_math.json +55 -0
  77. data/spec/fixtures/sample_data/new_quiz/numerical.json +64 -0
  78. data/spec/fixtures/sample_data/new_quiz/ordering.json +60 -0
  79. data/spec/fixtures/sample_data/new_quiz/stimulus.json +17 -0
  80. data/spec/fixtures/sample_data/new_quiz/true_false.json +27 -0
  81. data/spec/fixtures/sample_data/new_quiz/unknown.json +14 -0
  82. data/spec/lib/ams_migration/services/learnosity_sink_spec.rb +198 -0
  83. data/spec/lib/canvas/converter/classic_quiz/calculated_spec.rb +26 -0
  84. data/spec/lib/canvas/converter/classic_quiz/essay_spec.rb +26 -0
  85. data/spec/lib/canvas/converter/classic_quiz/file_upload_spec.rb +23 -0
  86. data/spec/lib/canvas/converter/classic_quiz/fill_in_blank_spec.rb +30 -0
  87. data/spec/lib/canvas/converter/classic_quiz/fill_in_multiple_blanks_spec.rb +33 -0
  88. data/spec/lib/canvas/converter/classic_quiz/matching_spec.rb +38 -0
  89. data/spec/lib/canvas/converter/classic_quiz/multiple_answer_spec.rb +33 -0
  90. data/spec/lib/canvas/converter/classic_quiz/multiple_choice_spec.rb +31 -0
  91. data/spec/lib/canvas/converter/classic_quiz/multiple_dropdown_spec.rb +39 -0
  92. data/spec/lib/canvas/converter/classic_quiz/numerical_spec.rb +41 -0
  93. data/spec/lib/canvas/converter/classic_quiz/text_spec.rb +20 -0
  94. data/spec/lib/canvas/converter/classic_quiz/true_false_spec.rb +28 -0
  95. data/spec/lib/canvas/converter/classic_quiz/unknown_spec.rb +25 -0
  96. data/spec/lib/canvas/converter/common_error.rb +16 -0
  97. data/spec/lib/canvas/converter/new_quiz/categorization_spec.rb +55 -0
  98. data/spec/lib/canvas/converter/new_quiz/essay_spec.rb +26 -0
  99. data/spec/lib/canvas/converter/new_quiz/file_upload_spec.rb +24 -0
  100. data/spec/lib/canvas/converter/new_quiz/fill_in_blank_spec.rb +111 -0
  101. data/spec/lib/canvas/converter/new_quiz/formula_spec.rb +23 -0
  102. data/spec/lib/canvas/converter/new_quiz/hotspot_spec.rb +182 -0
  103. data/spec/lib/canvas/converter/new_quiz/matching_spec.rb +30 -0
  104. data/spec/lib/canvas/converter/new_quiz/multiple_answer_spec.rb +34 -0
  105. data/spec/lib/canvas/converter/new_quiz/multiple_choice_spec.rb +54 -0
  106. data/spec/lib/canvas/converter/new_quiz/numerical_spec.rb +48 -0
  107. data/spec/lib/canvas/converter/new_quiz/ordering_spec.rb +25 -0
  108. data/spec/lib/canvas/converter/new_quiz/stimulus_spec.rb +20 -0
  109. data/spec/lib/canvas/converter/new_quiz/true_false_spec.rb +24 -0
  110. data/spec/lib/canvas/converter/new_quiz/unknown_spec.rb +36 -0
  111. data/spec/lib/factories/classic_quiz_item_converter_factory_spec.rb +183 -0
  112. data/spec/lib/factories/new_quiz_item_converter_factory_spec.rb +71 -0
  113. data/spec/lib/services/learnosity_data_api_client_spec.rb +130 -0
  114. data/spec/lib/services/new_quiz_learnosity_transformer_spec.rb +196 -0
  115. data/spec/spec_helper.rb +20 -0
  116. metadata +229 -0
@@ -0,0 +1,20 @@
1
+ # Copyright (C) 2025 - present Instructure, Inc.
2
+ # This code is made publicly available for educational and reference purposes only.
3
+ # You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
4
+
5
+ require "spec_helper"
6
+ require_relative "../common_error"
7
+
8
+ describe AmsMigration::Canvas::Converter::NewQuiz::Stimulus do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", "stimulus.json")) }
10
+
11
+ subject { described_class.new(question) }
12
+
13
+ it_behaves_like "handles common errors"
14
+
15
+ it "converts to a text return value" do
16
+ conversion = subject.convert
17
+ expect(conversion[:question]).to eq('<p><strong>Instructions</strong></p><p>Here are some instructions</p><p><strong>Stimulus Title</strong></p><p>The content in <sub>question</sub>. With <strong>some</strong> funky <span style="text-decoration: underline;">extra</span> <em>words</em> <span style="color: #f1c40f;">that</span> I can <span style="background-color: #843fa1;">format</span></p><p><img src="https://dummy-inscloudgate.net/files/3ec7226b-fd6d-49df-8236-cb52001ea10b?token=eyJ0eXAiOiJKV1QiLCJhbGciOi" alt="Alternative Text" /></p>')
18
+ expect(conversion[:title]).to eq("Stimulus Title")
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ # Copyright (C) 2025 - present Instructure, Inc.
2
+ # This code is made publicly available for educational and reference purposes only.
3
+ # You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
4
+
5
+ require "spec_helper"
6
+ require_relative "../common_error"
7
+
8
+ describe AmsMigration::Canvas::Converter::NewQuiz::TrueFalse do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", "true_false.json")) }
10
+
11
+ subject { described_class.new(question) }
12
+
13
+ it_behaves_like "handles common errors"
14
+
15
+ it "converts to true false item" do
16
+ conversion = subject.convert
17
+ expect(conversion[:question]).to eq({ stimulus: '<p>This is<span style="color: #f1c40f;"> the</span> stem/stimulus <strong>with</strong> <span style="text-decoration: underline;">some</span> formatting</p><p><img src="https://dummy-inscloudgate.net/files/3ec7226b-fd6d-49df-8236-cb52001ea10b?token=eyJ0eXAiOiJKV1QiLCJhbGciOi" alt="Alternative Text" /></p>',
18
+ options: [{ label: "True", value: "0" },
19
+ { label: "False", value: "1" }],
20
+ type: "mcq",
21
+ validation: { valid_response: { score: 1, value: ["0"] },
22
+ scoring_type: "exactMatch" } })
23
+ end
24
+ end
@@ -0,0 +1,36 @@
1
+ # Copyright (C) 2025 - present Instructure, Inc.
2
+ # This code is made publicly available for educational and reference purposes only.
3
+ # You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
4
+
5
+ require "spec_helper"
6
+ require_relative "../common_error"
7
+
8
+ describe AmsMigration::Canvas::Converter::NewQuiz::Unknown do
9
+ let(:question) do
10
+ File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", "unknown.json"))
11
+ end
12
+
13
+ subject { described_class.new(question) }
14
+
15
+ it "returns errors with malformed json" do
16
+ subject = described_class.new('{"this is": "not valid json" xyz }')
17
+ conversion = subject.convert
18
+ expect(conversion[:errors].length).to be >= 1
19
+ end
20
+
21
+ it "returns with question type error" do
22
+ conversion = subject.convert
23
+ expect(conversion[:errors]).to include("Unknown question type: Unknown")
24
+ end
25
+
26
+ context "With unsupported Bank entry type" do
27
+ let(:question) do
28
+ File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", "bank_item.json"))
29
+ end
30
+
31
+ it "returns with entry type error" do
32
+ conversion = subject.convert
33
+ expect(conversion[:errors]).to include("Unsupported entry type: Bank")
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,183 @@
1
+ # Copyright (C) 2025 - present Instructure, Inc.
2
+ # This code is made publicly available for educational and reference purposes only.
3
+ # You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
4
+
5
+ require "spec_helper"
6
+
7
+ describe AmsMigration::CanvasQuizzes::ClassicQuizItemConverterFactory do
8
+ describe ".create_converter" do
9
+ subject { described_class.create_converter(item) }
10
+
11
+ context "when item is a multiple choice question" do
12
+ let(:item) do
13
+ {
14
+ "question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::MULTI_CHOICE
15
+ }
16
+ end
17
+
18
+ it "returns a MultipleChoice converter" do
19
+ expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::MultipleChoice)
20
+ end
21
+ end
22
+
23
+ context "when item is a multiple answers question" do
24
+ let(:item) do
25
+ {
26
+ "question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::MULTIPLE_ANSWERS
27
+ }
28
+ end
29
+
30
+ it "returns a MultipleAnswer converter" do
31
+ expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::MultipleAnswer)
32
+ end
33
+ end
34
+
35
+ context "when item is a matching question" do
36
+ let(:item) do
37
+ {
38
+ "question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::MATCHING
39
+ }
40
+ end
41
+
42
+ it "returns a Matching converter" do
43
+ expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::Matching)
44
+ end
45
+ end
46
+
47
+ context "when item is a fill in blank question" do
48
+ let(:item) do
49
+ {
50
+ "question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::FILL_IN_BLANK
51
+ }
52
+ end
53
+
54
+ it "returns a FillInBlank converter" do
55
+ expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::FillInBlank)
56
+ end
57
+ end
58
+
59
+ context "when item is a fill in multiple blanks question" do
60
+ let(:item) do
61
+ {
62
+ "question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::FILL_IN_MULTIPLE_BLANKS
63
+ }
64
+ end
65
+
66
+ it "returns a FillInMultipleBlanks converter" do
67
+ expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::FillInMultipleBlanks)
68
+ end
69
+ end
70
+
71
+ context "when item is a multiple dropdowns question" do
72
+ let(:item) do
73
+ {
74
+ "question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::MULTIPLE_DROPDOWNS
75
+ }
76
+ end
77
+
78
+ it "returns a MultipleDropdown converter" do
79
+ expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::MultipleDropdown)
80
+ end
81
+ end
82
+
83
+ context "when item is a true/false question" do
84
+ let(:item) do
85
+ {
86
+ "question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::TRUE_FALSE
87
+ }
88
+ end
89
+
90
+ it "returns a TrueFalse converter" do
91
+ expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::TrueFalse)
92
+ end
93
+ end
94
+
95
+ context "when item is a numerical question" do
96
+ let(:item) do
97
+ {
98
+ "question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::NUMERICAL
99
+ }
100
+ end
101
+
102
+ it "returns a Numerical converter" do
103
+ expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::Numerical)
104
+ end
105
+ end
106
+
107
+ context "when item is a text only question" do
108
+ let(:item) do
109
+ {
110
+ "question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::TEXT_ONLY
111
+ }
112
+ end
113
+
114
+ it "returns a Text converter" do
115
+ expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::Text)
116
+ end
117
+ end
118
+
119
+ context "when item is a calculated question" do
120
+ let(:item) do
121
+ {
122
+ "question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::CALCULATED
123
+ }
124
+ end
125
+
126
+ it "returns a Calculated converter" do
127
+ expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::Calculated)
128
+ end
129
+ end
130
+
131
+ context "when item is a file upload question" do
132
+ let(:item) do
133
+ {
134
+ "question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::FILE_UPLOAD
135
+ }
136
+ end
137
+
138
+ it "returns a FileUpload converter" do
139
+ expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::FileUpload)
140
+ end
141
+ end
142
+
143
+ context "when item is an essay question" do
144
+ let(:item) do
145
+ {
146
+ "question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::ESSAY
147
+ }
148
+ end
149
+
150
+ it "returns an Essay converter" do
151
+ expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::Essay)
152
+ end
153
+ end
154
+
155
+ context "when item type is unknown" do
156
+ let(:item) do
157
+ {
158
+ "question_type" => "unknown-type"
159
+ }
160
+ end
161
+
162
+ it "returns an Unknown converter" do
163
+ expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::Unknown)
164
+ end
165
+ end
166
+
167
+ context "when converting item to json" do
168
+ let(:item) do
169
+ {
170
+ "question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::MULTI_CHOICE,
171
+ "question_text" => "Sample question"
172
+ }
173
+ end
174
+
175
+ it "passes the json representation to the converter" do
176
+ allow(AmsMigration::Canvas::Converter::ClassicQuiz::MultipleChoice).to receive(:new)
177
+ described_class.create_converter(item)
178
+ expect(AmsMigration::Canvas::Converter::ClassicQuiz::MultipleChoice)
179
+ .to have_received(:new).with(item.to_json)
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,71 @@
1
+ # Copyright (C) 2025 - present Instructure, Inc.
2
+ # This code is made publicly available for educational and reference purposes only.
3
+ # You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
4
+
5
+ require "spec_helper"
6
+
7
+ describe AmsMigration::CanvasQuizzes::NewQuizItemConverterFactory do
8
+ let(:item_type) { "unknown" }
9
+ let(:item) do
10
+ JSON.parse(File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", "#{item_type}.json")))
11
+ end
12
+
13
+ describe ".create_converter" do
14
+ subject { described_class.create_converter(item) }
15
+ context "when item is a multiple choice question" do
16
+ let(:item_type) { "multiple_choice" }
17
+
18
+ it "returns a MultipleChoice converter" do
19
+ expect(subject).to be_a(AmsMigration::Canvas::Converter::NewQuiz::MultipleChoice)
20
+ end
21
+ end
22
+
23
+ context "when item is a multiple answer question" do
24
+ let(:item_type) { "multiple_answer" }
25
+
26
+ it "returns a MultipleAnswer converter" do
27
+ expect(subject).to be_a(AmsMigration::Canvas::Converter::NewQuiz::MultipleAnswer)
28
+ end
29
+ end
30
+
31
+ context "when item is a matching question" do
32
+ let(:item_type) { "matching" }
33
+
34
+ it "returns a Matching converter" do
35
+ expect(subject).to be_a(AmsMigration::Canvas::Converter::NewQuiz::Matching)
36
+ end
37
+ end
38
+
39
+ context "when item is a stimulus" do
40
+ let(:item_type) { "stimulus" }
41
+
42
+ it "returns a Stimulus converter" do
43
+ expect(subject).to be_a(AmsMigration::Canvas::Converter::NewQuiz::Stimulus)
44
+ end
45
+ end
46
+
47
+ # TODO: looks like it is not supported yet
48
+ context "when item is a bank entry" do
49
+ # let(:item_type) { 'bank_entry_item' }
50
+ let(:item) do
51
+ {
52
+ "entry_type" => "Item",
53
+ "archived" => false,
54
+ "entry" => {}
55
+ }
56
+ end
57
+
58
+ it "normalizes the item and returns appropriate converter" do
59
+ expect(subject).to be_a(AmsMigration::Canvas::Converter::NewQuiz::Unknown)
60
+ end
61
+ end
62
+
63
+ context "when item type is unknown" do
64
+ let(:item_type) { "unknown" }
65
+
66
+ it "returns an Unknown converter" do
67
+ expect(subject).to be_a(AmsMigration::Canvas::Converter::NewQuiz::Unknown)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,130 @@
1
+ # Copyright (C) 2025 - present Instructure, Inc.
2
+ # This code is made publicly available for educational and reference purposes only.
3
+ # You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
4
+
5
+ require "spec_helper"
6
+ require "webmock/rspec"
7
+ require "logger"
8
+
9
+ describe AmsMigration::Services::LearnosityDataApiClient do
10
+ let(:config) do
11
+ {
12
+ consumer_key: "test_consumer_key",
13
+ consumer_secret: "test_consumer_secret",
14
+ domain: "test.domain.com",
15
+ data_api_url: "https://data.learnosity.com/v1",
16
+ organization_id: "test_org_id",
17
+ logger: Logger.new(nil)
18
+ }
19
+ end
20
+
21
+ subject(:client) { described_class.new(config) }
22
+
23
+ describe "#post_request" do
24
+ let(:request_data) { { "test_key" => "test_value" } }
25
+ let(:endpoint) { "activities" }
26
+ let(:method) { "get" }
27
+ let(:expected_url) { "#{config[:data_api_url]}/itembank/#{endpoint}" }
28
+
29
+ context "when the request is successful" do
30
+ let(:response_body) do
31
+ {
32
+ "meta" => { "status" => true },
33
+ "data" => [{ "id" => "123" }]
34
+ }
35
+ end
36
+
37
+ before do
38
+ stub_request(:post, expected_url)
39
+ .to_return(
40
+ status: 200,
41
+ body: response_body.to_json,
42
+ headers: { "Content-Type" => "application/json" }
43
+ )
44
+ end
45
+
46
+ it "makes a POST request with the correct parameters" do
47
+ result = client.post_request(request_data, endpoint, method)
48
+
49
+ expect(result).to eq(response_body)
50
+ expect(WebMock).to have_requested(:post, expected_url)
51
+ end
52
+
53
+ it "includes organization_id in the request" do
54
+ client.post_request(request_data, endpoint, method)
55
+
56
+ expect(WebMock).to(have_requested(:post, expected_url)
57
+ .with do |req|
58
+ form_data = URI.decode_www_form(req.body).to_h
59
+ json_data = JSON.parse(form_data["request"])
60
+ expect(json_data["organisation_id"]).to eq(config[:organization_id])
61
+ end)
62
+ end
63
+
64
+ it "signs the request using Learnosity SDK" do
65
+ client.post_request(request_data, endpoint, method)
66
+
67
+ expect(WebMock).to(have_requested(:post, expected_url)
68
+ .with do |req|
69
+ form_data = URI.decode_www_form(req.body).to_h
70
+ expect(form_data.keys).to include("security")
71
+ expect(form_data.keys).to include("request")
72
+ expect(form_data.keys).to include("action")
73
+ end)
74
+ end
75
+ end
76
+
77
+ context "when the request fails" do
78
+ let(:error_response) do
79
+ {
80
+ "meta" => {
81
+ "status" => false,
82
+ "message" => "Invalid request"
83
+ },
84
+ "errors" => ["Something went wrong"]
85
+ }
86
+ end
87
+
88
+ before do
89
+ stub_request(:post, expected_url)
90
+ .to_return(
91
+ status: 400,
92
+ body: error_response.to_json,
93
+ headers: { "Content-Type" => "application/json" }
94
+ )
95
+ end
96
+
97
+ it "logs the error and returns the response" do
98
+ expect(config[:logger]).to receive(:debug).at_least(:once)
99
+ result = client.post_request(request_data, endpoint, method)
100
+ expect(result).to eq(error_response)
101
+ end
102
+ end
103
+
104
+ context "when response body is empty" do
105
+ before do
106
+ stub_request(:post, expected_url)
107
+ .to_return(
108
+ status: 200,
109
+ body: "",
110
+ headers: { "Content-Type" => "application/json" }
111
+ )
112
+ end
113
+
114
+ it "handles empty response gracefully" do
115
+ result = client.post_request(request_data, endpoint, method)
116
+ expect(result).to be_nil
117
+ end
118
+ end
119
+ end
120
+
121
+ describe "#security_packet" do
122
+ it "returns a hash with consumer_key and domain" do
123
+ security = client.send(:security_packet)
124
+ expect(security).to eq({
125
+ "consumer_key" => config[:consumer_key],
126
+ "domain" => config[:domain]
127
+ })
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,196 @@
1
+ # Copyright (C) 2025 - present Instructure, Inc.
2
+ # This code is made publicly available for educational and reference purposes only.
3
+ # You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
4
+
5
+ require "spec_helper"
6
+
7
+ describe AmsMigration::Services::NewQuizLearnosityTransformer do
8
+ let(:root_account_uuid) { "test-account-123" }
9
+ let(:transformer) { described_class.new(root_account_uuid:) }
10
+ let(:quiz_id) { 456 }
11
+ let(:quiz_title) { "Sample Quiz" }
12
+ let(:item_id) { 789 }
13
+ let(:quiz_item) do
14
+ {
15
+ id: item_id,
16
+ position: 1,
17
+ title: "Sample Question"
18
+ }
19
+ end
20
+ let(:quiz) do
21
+ {
22
+ quiz: {
23
+ id: quiz_id,
24
+ title: quiz_title
25
+ },
26
+ quiz_items: [quiz_item]
27
+ }
28
+ end
29
+ let(:converted_item) do
30
+ {
31
+ title: "Sample Question",
32
+ question: {
33
+ type: "mcq",
34
+ stimulus: "What is the answer?",
35
+ options: []
36
+ }
37
+ }
38
+ end
39
+
40
+ before do
41
+ allow(AmsMigration::CanvasQuizzes::NewQuizItemConverterFactory).to receive(:create_converter)
42
+ .with(quiz_item)
43
+ .and_return(double(convert: converted_item))
44
+ end
45
+
46
+ describe "#transform" do
47
+ subject { transformer.transform(quiz) }
48
+
49
+ it "transforms quiz into Learnosity format" do
50
+ expect(subject).to include(
51
+ activities: [
52
+ {
53
+ "reference" => "a-nq-#{root_account_uuid}-#{quiz_id}",
54
+ "data" => {
55
+ "items" => ["i-nq-#{root_account_uuid}-#{item_id}"]
56
+ },
57
+ "title" => quiz_title,
58
+ "tags" => [
59
+ { "type" => "import_original_id", "name" => quiz_id.to_s },
60
+ { "type" => "import_account_id", "name" => root_account_uuid.to_s },
61
+ { "type" => "import_system", "name" => "NQ" }
62
+ ]
63
+ }
64
+ ],
65
+ items: [
66
+ {
67
+ "reference" => "i-nq-#{root_account_uuid}-#{item_id}",
68
+ "title" => "Sample Question",
69
+ "status" => "published",
70
+ "definition" => {
71
+ "widgets" => [
72
+ { "reference" => "q-nq-#{root_account_uuid}-#{item_id}" }
73
+ ]
74
+ },
75
+ "tags" => [
76
+ { "type" => "import_original_id", "name" => item_id.to_s },
77
+ { "type" => "import_account_id", "name" => root_account_uuid.to_s },
78
+ { "type" => "import_system", "name" => "NQ" }
79
+ ]
80
+ }
81
+ ],
82
+ questions: [
83
+ {
84
+ "reference" => "q-nq-#{root_account_uuid}-#{item_id}",
85
+ "type" => "mcq",
86
+ "data" => converted_item[:question]
87
+ }
88
+ ],
89
+ features: {}
90
+ )
91
+ end
92
+
93
+ context "when question type is unsupported" do
94
+ let(:converted_item) do
95
+ {
96
+ title: "Unsupported Question",
97
+ question: {
98
+ type: "unsupported_canvas_question_type"
99
+ }
100
+ }
101
+ end
102
+
103
+ it "excludes unsupported questions from the result" do
104
+ expect(subject).to include(
105
+ activities: [
106
+ {
107
+ "reference" => "a-nq-#{root_account_uuid}-#{quiz_id}",
108
+ "data" => { "items" => [] },
109
+ "title" => quiz_title,
110
+ "tags" => [
111
+ { "type" => "import_original_id", "name" => quiz_id.to_s },
112
+ { "type" => "import_account_id", "name" => root_account_uuid.to_s },
113
+ { "type" => "import_system", "name" => "NQ" }
114
+ ]
115
+ }
116
+ ],
117
+ items: [],
118
+ questions: [],
119
+ features: {}
120
+ )
121
+ end
122
+ end
123
+
124
+ context "when question data is empty" do
125
+ let(:converted_item) do
126
+ {
127
+ title: "Empty Question",
128
+ question: {}
129
+ }
130
+ end
131
+
132
+ it "excludes empty questions from the result" do
133
+ expect(subject).to include(
134
+ activities: [
135
+ {
136
+ "reference" => "a-nq-#{root_account_uuid}-#{quiz_id}",
137
+ "data" => { "items" => [] },
138
+ "title" => quiz_title,
139
+ "tags" => [
140
+ { "type" => "import_original_id", "name" => quiz_id.to_s },
141
+ { "type" => "import_account_id", "name" => root_account_uuid.to_s },
142
+ { "type" => "import_system", "name" => "NQ" }
143
+ ]
144
+ }
145
+ ],
146
+ items: [],
147
+ questions: [],
148
+ features: {}
149
+ )
150
+ end
151
+ end
152
+
153
+ context "with multiple quiz items" do
154
+ let(:item_id_2) { 790 }
155
+ let(:quiz_item_2) do
156
+ {
157
+ id: item_id_2,
158
+ position: 2,
159
+ title: "Second Question"
160
+ }
161
+ end
162
+ let(:converted_item_2) do
163
+ {
164
+ title: "Second Question",
165
+ question: {
166
+ type: "mcq",
167
+ stimulus: "What is the second answer?",
168
+ options: []
169
+ }
170
+ }
171
+ end
172
+
173
+ before do
174
+ quiz[:quiz_items] << quiz_item_2
175
+ allow(AmsMigration::CanvasQuizzes::NewQuizItemConverterFactory).to receive(:create_converter)
176
+ .with(quiz_item_2)
177
+ .and_return(double(convert: converted_item_2))
178
+ end
179
+
180
+ it "transforms all quiz items in correct order" do
181
+ expect(subject[:activities].first["data"]["items"]).to eq([
182
+ "i-nq-#{root_account_uuid}-#{item_id}",
183
+ "i-nq-#{root_account_uuid}-#{item_id_2}"
184
+ ])
185
+ expect(subject[:items].map { |i| i["reference"] }).to eq([
186
+ "i-nq-#{root_account_uuid}-#{item_id}",
187
+ "i-nq-#{root_account_uuid}-#{item_id_2}"
188
+ ])
189
+ expect(subject[:questions].map { |q| q["reference"] }).to eq([
190
+ "q-nq-#{root_account_uuid}-#{item_id}",
191
+ "q-nq-#{root_account_uuid}-#{item_id_2}"
192
+ ])
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,20 @@
1
+ # Copyright (C) 2025 - present Instructure, Inc.
2
+ # This code is made publicly available for educational and reference purposes only.
3
+ # You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
4
+
5
+ require "bundler/setup"
6
+ require "ams_migration"
7
+ require "webmock/rspec"
8
+
9
+ RSpec.configure do |config|
10
+ # Enable flags like --only-failures and --next-failure
11
+ config.example_status_persistence_file_path = ".rspec_status"
12
+
13
+ config.expect_with :rspec do |c|
14
+ c.syntax = :expect
15
+ end
16
+ end
17
+
18
+ SPEC_ROOT = File.expand_path(__dir__)
19
+
20
+ WebMock.disable_net_connect!(allow_localhost: true)