panda_pal 5.19.0.beta1 → 5.19.0.beta3
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 +4 -4
- data/README.md +13 -0
- data/app/models/panda_pal/platform/canvas.rb +74 -20
- data/lib/panda_pal/version.rb +1 -1
- data/panda_pal.gemspec +1 -1
- metadata +2 -60
- data/spec/controllers/panda_pal/api_call_controller_spec.rb +0 -25
- data/spec/controllers/panda_pal/lti_v1_p0_controller_spec.rb +0 -194
- data/spec/controllers/panda_pal/lti_v1_p3_controller_spec.rb +0 -108
- data/spec/controllers/panda_pal/lti_v1_p3_tools_spec.rb +0 -144
- data/spec/core/apartment_multidb_spec.rb +0 -166
- data/spec/factories/panda_pal_organizations.rb +0 -9
- data/spec/factories/panda_pal_sessions.rb +0 -6
- data/spec/internal/app/models/account.rb +0 -7
- data/spec/internal/app/models/admin.rb +0 -6
- data/spec/internal/config/database.yml +0 -18
- data/spec/internal/config/routes.rb +0 -5
- data/spec/internal/config/storage.yml +0 -3
- data/spec/internal/db/schema.rb +0 -30
- data/spec/jobs/panda_pal/jobs/grade_passback_job_spec.rb +0 -83
- data/spec/lib/panda_pal/ags_client_spec.rb +0 -385
- data/spec/lib/panda_pal/helpers/secure_headers_spec.rb +0 -77
- data/spec/lib/panda_pal/helpers/session_replacement_spec.rb +0 -72
- data/spec/lib/panda_pal/lti_tools_spec.rb +0 -153
- data/spec/models/panda_pal/api_call_spec.rb +0 -30
- data/spec/models/panda_pal/lti_registration_spec.rb +0 -72
- data/spec/models/panda_pal/organization/settings_validation_spec.rb +0 -179
- data/spec/models/panda_pal/organization/task_scheduling_spec.rb +0 -145
- data/spec/models/panda_pal/organization_spec.rb +0 -50
- data/spec/models/panda_pal/platform/canvas_install_spec.rb +0 -115
- data/spec/models/panda_pal/platform_spec.rb +0 -349
- data/spec/models/panda_pal/session_account_roles_spec.rb +0 -182
- data/spec/models/panda_pal/session_spec.rb +0 -202
- data/spec/spec_helper.rb +0 -159
- data/spec/support/isolated_lti_config.rb +0 -24
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
require 'spec_helper'
|
|
2
|
-
require 'json/jwt'
|
|
3
|
-
|
|
4
|
-
RSpec.describe "PandaPal::LtiV1P3Controller tools", type: :request, isolated_lti_config: true do
|
|
5
|
-
let(:organization) { create(:panda_pal_organization, key: '10000000000001') }
|
|
6
|
-
let(:issuer) { 'https://canvas.instructure.com' }
|
|
7
|
-
let(:platform_key) { OpenSSL::PKey::RSA.new(2048) }
|
|
8
|
-
let(:platform_jwk) { JSON::JWK.new(platform_key.public_key) }
|
|
9
|
-
|
|
10
|
-
before do
|
|
11
|
-
PandaPal.lti_environments = { domain: 'tool.example.com' }
|
|
12
|
-
PandaPal.lti_options = { title: 'Shared Title' }
|
|
13
|
-
PandaPal.stage_placement(:course_navigation, text: 'Settings')
|
|
14
|
-
PandaPal.define_tool(:translate, title: 'Translate Title')
|
|
15
|
-
# Only the translate tool routes editor_button anywhere real (the mount
|
|
16
|
-
# point) - the default tool would fall back to a missing editor_button_url.
|
|
17
|
-
PandaPal.stage_placement(:editor_button, tool: :translate, url: :panda_pal, text: 'Translate')
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
after { Apartment::Tenant.reset }
|
|
21
|
-
|
|
22
|
-
describe 'GET /v1p3/config' do
|
|
23
|
-
it "serves only the default tool's placements without a tool param" do
|
|
24
|
-
get '/panda_pal/v1p3/config'
|
|
25
|
-
|
|
26
|
-
body = JSON.parse(response.body)
|
|
27
|
-
placements = body.dig('extensions', 0, 'settings', 'placements').map { |p| p['placement'] }
|
|
28
|
-
|
|
29
|
-
expect(body['title']).to eq('Shared Title')
|
|
30
|
-
expect(placements).to eq(['course_navigation'])
|
|
31
|
-
expect(body['custom_fields']).not_to have_key(PandaPal::TOOL_CUSTOM_FIELD)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
it "serves the requested tool's title, placements, and tool custom field" do
|
|
35
|
-
get '/panda_pal/v1p3/config', params: { tool: 'translate' }
|
|
36
|
-
|
|
37
|
-
body = JSON.parse(response.body)
|
|
38
|
-
placements = body.dig('extensions', 0, 'settings', 'placements')
|
|
39
|
-
|
|
40
|
-
expect(body['title']).to eq('Translate Title')
|
|
41
|
-
expect(placements.map { |p| p['placement'] }).to eq(['editor_button'])
|
|
42
|
-
expect(placements[0]['target_link_uri']).to include('/panda_pal/v1p3/resource_link_request?launch_type=editor_button')
|
|
43
|
-
expect(body['custom_fields'][PandaPal::TOOL_CUSTOM_FIELD]).to eq('translate')
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
it 'renders 404 for an unknown tool' do
|
|
47
|
-
get '/panda_pal/v1p3/config', params: { tool: 'nope' }
|
|
48
|
-
|
|
49
|
-
expect(response).to have_http_status(:not_found)
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
describe 'POST /v1p3/resource_link_request' do
|
|
54
|
-
let(:nonce) { SecureRandom.uuid }
|
|
55
|
-
|
|
56
|
-
let!(:login_session) do
|
|
57
|
-
# What LtiV1P3Controller#login leaves behind in the public schema.
|
|
58
|
-
PandaPal::Session.create!(panda_pal_organization_id: 0, data: {
|
|
59
|
-
lti_platform: PandaPal::Platform::Canvas.new(iss: issuer).serialize,
|
|
60
|
-
lti_oauth_nonce: nonce,
|
|
61
|
-
})
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
before do
|
|
65
|
-
allow_any_instance_of(PandaPal::Platform::Canvas).to receive(:public_jwks).and_return(JSON::JWK::Set.new(platform_jwk))
|
|
66
|
-
# Launch validation records nonces in Rails.cache - keep them in memory
|
|
67
|
-
# rather than in the dummy app's file store.
|
|
68
|
-
allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def id_token(client_id:, deployment_id: 'dep-1', placement: 'editor_button')
|
|
72
|
-
claims = {
|
|
73
|
-
iss: issuer,
|
|
74
|
-
aud: client_id,
|
|
75
|
-
sub: 'user-1',
|
|
76
|
-
nonce: nonce,
|
|
77
|
-
iat: Time.now.to_i,
|
|
78
|
-
exp: Time.now.to_i + 300,
|
|
79
|
-
PandaPal::LtiConstants::Claims::DEPLOYMENT_ID => deployment_id,
|
|
80
|
-
PandaPal::LtiConstants::Claims::MESSAGE_TYPE => 'LtiResourceLinkRequest',
|
|
81
|
-
PandaPal::LtiConstants::Canvas::PLACEMENT => placement,
|
|
82
|
-
}
|
|
83
|
-
jwt = JSON::JWT.new(claims)
|
|
84
|
-
jwt.kid = platform_jwk[:kid]
|
|
85
|
-
jwt.sign(platform_key, :RS256).to_s
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def launch!(**token_opts)
|
|
89
|
-
post '/panda_pal/v1p3/resource_link_request', params: { id_token: id_token(**token_opts), state: login_session.session_key }
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def launched_session
|
|
93
|
-
organization.switch_tenant { PandaPal::Session.order(:id).last }
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
it "resolves a registered tool's client_id to its organization and routes by that tool" do
|
|
97
|
-
organization.lti_registrations.create!(tool: 'translate', client_id: '10000000000002')
|
|
98
|
-
|
|
99
|
-
launch!(client_id: '10000000000002')
|
|
100
|
-
|
|
101
|
-
expect(response).to have_http_status(:found)
|
|
102
|
-
expect(response.headers['Location']).to include('/panda_pal')
|
|
103
|
-
expect(launched_session.lti_tool).to eq(:translate)
|
|
104
|
-
expect(launched_session.panda_pal_organization).to eq(organization)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
it 'prefers a deployment-specific registration' do
|
|
108
|
-
other_org = create(:panda_pal_organization)
|
|
109
|
-
other_org.lti_registrations.create!(tool: 'translate', client_id: '10000000000002')
|
|
110
|
-
organization.lti_registrations.create!(tool: 'translate', client_id: '10000000000002', deployment_id: 'dep-1')
|
|
111
|
-
|
|
112
|
-
launch!(client_id: '10000000000002', deployment_id: 'dep-1')
|
|
113
|
-
|
|
114
|
-
expect(response).to have_http_status(:found)
|
|
115
|
-
expect(launched_session.panda_pal_organization).to eq(organization)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
it "still resolves the organization's own key as the default tool" do
|
|
119
|
-
PandaPal.stage_placement(:course_navigation, url: :panda_pal)
|
|
120
|
-
|
|
121
|
-
launch!(client_id: organization.key, placement: 'course_navigation')
|
|
122
|
-
|
|
123
|
-
expect(response).to have_http_status(:found)
|
|
124
|
-
expect(launched_session.lti_tool).to eq(PandaPal::DEFAULT_TOOL)
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
it 'rejects a registration whose tool is no longer defined' do
|
|
128
|
-
registration = organization.lti_registrations.create!(tool: 'translate', client_id: '10000000000002')
|
|
129
|
-
registration.update_column(:tool, 'removed')
|
|
130
|
-
|
|
131
|
-
launch!(client_id: '10000000000002')
|
|
132
|
-
|
|
133
|
-
expect(response).to have_http_status(:unauthorized)
|
|
134
|
-
expect(response.body).to include('Unrecognized LTI tool')
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
it 'rejects an unregistered client_id' do
|
|
138
|
-
launch!(client_id: '99999')
|
|
139
|
-
|
|
140
|
-
expect(response).to have_http_status(:unauthorized)
|
|
141
|
-
expect(response.body).to include('Unrecognized Organization')
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
end
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'spec_helper'
|
|
4
|
-
|
|
5
|
-
RSpec.describe PandaPal::Organization, type: :model do
|
|
6
|
-
context "across multiple DBs" do
|
|
7
|
-
with_multiple_shards(
|
|
8
|
-
shards: { alt: "panda_pal_test2" },
|
|
9
|
-
tenant_names: %w[on_primary alt:on_alt],
|
|
10
|
-
transactional: false,
|
|
11
|
-
determine_shard: ->(org) { org.name.include?("alt") ? "alt" : nil }
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
let!(:org1) { create(:panda_pal_organization, name: "on_primary") }
|
|
15
|
-
let!(:org2) { create(:panda_pal_organization, name: "on_alt") }
|
|
16
|
-
|
|
17
|
-
it "successfully creates tenants on multiple shards" do
|
|
18
|
-
expect(1).to eql(1)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
it "only creates schemas on the target shard" do
|
|
22
|
-
org1.switch_tenant do
|
|
23
|
-
schemas = PandaPal::ApiCall.connection.exec_query("SELECT schema_name FROM information_schema.schemata").pluck("schema_name")
|
|
24
|
-
expect(schemas).to include("on_primary")
|
|
25
|
-
expect(schemas).to_not include("on_alt")
|
|
26
|
-
end
|
|
27
|
-
org2.switch_tenant do
|
|
28
|
-
schemas = PandaPal::ApiCall.connection.exec_query("SELECT schema_name FROM information_schema.schemata").pluck("schema_name")
|
|
29
|
-
expect(schemas).to include("on_alt")
|
|
30
|
-
expect(schemas).to_not include("on_primary")
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
it "creates records on the target shard and schema" do
|
|
35
|
-
org1.switch_tenant
|
|
36
|
-
PandaPal::ApiCall.create!(logic: "from_primary")
|
|
37
|
-
expect(PandaPal::ApiCall.count).to eq(1)
|
|
38
|
-
expect(PandaPal::ApiCall.first.logic).to eq("from_primary")
|
|
39
|
-
|
|
40
|
-
org2.switch_tenant
|
|
41
|
-
PandaPal::ApiCall.create!(logic: "from_alt")
|
|
42
|
-
expect(PandaPal::ApiCall.count).to eq(1)
|
|
43
|
-
expect(PandaPal::ApiCall.first.logic).to eq("from_alt")
|
|
44
|
-
|
|
45
|
-
org1.switch_tenant
|
|
46
|
-
expect(PandaPal::ApiCall.count).to eq(1)
|
|
47
|
-
expect(PandaPal::ApiCall.first.logic).to eq("from_primary")
|
|
48
|
-
expect(PandaPal::ApiCall.connection.exec_query("SELECT current_database()").pluck("current_database")[0]).to eq("panda_pal_test1")
|
|
49
|
-
|
|
50
|
-
org2.switch_tenant
|
|
51
|
-
expect(PandaPal::ApiCall.count).to eq(1)
|
|
52
|
-
expect(PandaPal::ApiCall.first.logic).to eq("from_alt")
|
|
53
|
-
expect(PandaPal::ApiCall.connection.exec_query("SELECT current_database()").pluck("current_database")[0]).to eq("panda_pal_test2")
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
context "connection pool lifecycle" do
|
|
57
|
-
def alt_pool
|
|
58
|
-
ActiveRecord::Base.connection_handler.retrieve_connection_pool(
|
|
59
|
-
ActiveRecord::Base.connection_specification_name,
|
|
60
|
-
role: ActiveRecord::Base.current_role,
|
|
61
|
-
shard: "apt:alt",
|
|
62
|
-
)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def count_establish_connection_calls
|
|
66
|
-
calls = Queue.new
|
|
67
|
-
allow(Apartment).to receive(:establish_connection).and_wrap_original do |original, *args, **kwargs|
|
|
68
|
-
calls << true
|
|
69
|
-
original.call(*args, **kwargs)
|
|
70
|
-
end
|
|
71
|
-
yield
|
|
72
|
-
calls.size
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def switch_in_threads(tenant, count)
|
|
76
|
-
start = Queue.new
|
|
77
|
-
threads = count.times.map do
|
|
78
|
-
Thread.new do
|
|
79
|
-
start.pop
|
|
80
|
-
Apartment::Tenant.switch(tenant) { PandaPal::ApiCall.count }
|
|
81
|
-
ensure
|
|
82
|
-
ActiveRecord::Base.connection_handler.clear_active_connections!
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
count.times { start << true }
|
|
86
|
-
threads.map(&:value)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
it "reuses an existing shard pool whose connections have all been closed" do
|
|
90
|
-
org2.switch_tenant { PandaPal::ApiCall.count }
|
|
91
|
-
pool = alt_pool
|
|
92
|
-
ActiveRecord::Base.connection_handler.clear_active_connections!
|
|
93
|
-
pool.disconnect!
|
|
94
|
-
|
|
95
|
-
calls = count_establish_connection_calls do
|
|
96
|
-
org2.switch_tenant { expect(PandaPal::ApiCall.count).to eq(0) }
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
expect(calls).to eq(0)
|
|
100
|
-
expect(alt_pool).to equal(pool)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
it "establishes a missing shard pool once when many threads need it at the same time" do
|
|
104
|
-
org2.switch_tenant { PandaPal::ApiCall.count }
|
|
105
|
-
ActiveRecord::Base.connection_handler.clear_active_connections!
|
|
106
|
-
ActiveRecord::Base.connection_handler.remove_connection_pool(
|
|
107
|
-
ActiveRecord::Base.connection_specification_name,
|
|
108
|
-
role: ActiveRecord::Base.current_role,
|
|
109
|
-
shard: "apt:alt",
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
results = nil
|
|
113
|
-
calls = count_establish_connection_calls do
|
|
114
|
-
results = switch_in_threads("alt:on_alt", 8)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
expect(results).to eq([0] * 8)
|
|
118
|
-
expect(calls).to eq(1)
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# load_async is a Rails 7.0+ feature.
|
|
123
|
-
context "load_async", skip: (ActiveRecord::Relation.method_defined?(:load_async) ? false : "requires Rails 7.0+ load_async") do
|
|
124
|
-
it "works across shards" do
|
|
125
|
-
qs = []
|
|
126
|
-
PandaPal::Organization.all.each do |org|
|
|
127
|
-
org.switch_tenant do
|
|
128
|
-
PandaPal::ApiCall.create!(logic: org.name)
|
|
129
|
-
qs << PandaPal::ApiCall.select("logic, pg_sleep(0.5)").load_async
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
calls = qs.map(&:to_a).flatten
|
|
133
|
-
expect(calls.pluck(:logic)).to match_array(["on_primary", "on_alt"])
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
context "tenant switch error wrapping" do
|
|
139
|
-
around do |example|
|
|
140
|
-
original = Apartment.with_multi_server_setup
|
|
141
|
-
Apartment.configure { |c| c.with_multi_server_setup = true }
|
|
142
|
-
Apartment::Tenant.reload!
|
|
143
|
-
example.run
|
|
144
|
-
ensure
|
|
145
|
-
Apartment.configure { |c| c.with_multi_server_setup = original }
|
|
146
|
-
Apartment::Tenant.reload!
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
it "does not mask a connection pool timeout as Apartment::TenantNotFound (PFS-31252)" do
|
|
150
|
-
adapter = Apartment::Tenant.adapter
|
|
151
|
-
previous_tenant = adapter.current
|
|
152
|
-
|
|
153
|
-
allow(ActiveRecord::Base).to receive(:connection) do
|
|
154
|
-
# Un-stub before raising: ActiveRecord::Base.connection is inherited by
|
|
155
|
-
# ros-apartment's SeparateDbConnectionHandler (used for schema create/drop),
|
|
156
|
-
# so leaving the stub in place past this one intended failure would misroute
|
|
157
|
-
# unrelated connection lookups made elsewhere.
|
|
158
|
-
allow(ActiveRecord::Base).to receive(:connection).and_call_original
|
|
159
|
-
raise ActiveRecord::ConnectionTimeoutError, "could not obtain a connection from the pool within 5.000 seconds"
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
expect { adapter.connect_to_new("alt:some_org") }.to raise_error(ActiveRecord::ConnectionTimeoutError)
|
|
163
|
-
expect(adapter.current).to eq(previous_tenant)
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
end
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
FactoryBot.define do
|
|
2
|
-
factory :panda_pal_organization, class: 'PandaPal::Organization' do
|
|
3
|
-
name { SecureRandom.hex }
|
|
4
|
-
key { SecureRandom.hex }
|
|
5
|
-
secret { SecureRandom.hex }
|
|
6
|
-
canvas_account_id { SecureRandom.random_number(10000) }
|
|
7
|
-
salesforce_id { SecureRandom.hex }
|
|
8
|
-
end
|
|
9
|
-
end
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Minimal stand-in for the CanvasSync-generated Account. Present so that
|
|
4
|
-
# Session#canvas_account_ancestry exercises its local-mirror branch (the one
|
|
5
|
-
# host apps actually hit) rather than the Canvas API fallback.
|
|
6
|
-
class Account < ActiveRecord::Base
|
|
7
|
-
end
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
default: &default
|
|
2
|
-
database: panda-pal-test
|
|
3
|
-
adapter: postgresql
|
|
4
|
-
encoding: unicode
|
|
5
|
-
# For details on connection pooling, see Rails configuration guide
|
|
6
|
-
# http://guides.rubyonrails.org/configuring.html#database-pooling
|
|
7
|
-
pool: <%= ENV.fetch("DB_POOL_SIZE", nil) || (ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i + 10) %>
|
|
8
|
-
username: <%= ENV.fetch("DB_USERNAME", "") %>
|
|
9
|
-
password: <%= ENV.fetch("DB_PASSWORD", "") %>
|
|
10
|
-
host: <%= ENV.fetch("DB_ADDRESS", "localhost") %>
|
|
11
|
-
|
|
12
|
-
test:
|
|
13
|
-
test1:
|
|
14
|
-
<<: *default
|
|
15
|
-
database: panda_pal_test1
|
|
16
|
-
test2:
|
|
17
|
-
<<: *default
|
|
18
|
-
database: panda_pal_test2
|
data/spec/internal/db/schema.rb
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
ActiveRecord::Schema.define do
|
|
4
|
-
# Set up any tables you need to exist for your test suite that don't belong
|
|
5
|
-
# in migrations.
|
|
6
|
-
|
|
7
|
-
# Stand-ins for the CanvasSync-generated, tenant-scoped models that
|
|
8
|
-
# Session#canvas_account_role_labels duck-types on. Only the columns it
|
|
9
|
-
# actually reads are declared. `accounts` deliberately has NO `ancestry`
|
|
10
|
-
# column: that gem is optional in host apps, so the traversal has to work
|
|
11
|
-
# from `canvas_parent_account_id` alone.
|
|
12
|
-
create_table :accounts, force: true do |t|
|
|
13
|
-
t.bigint :canvas_id, null: false
|
|
14
|
-
t.bigint :canvas_parent_account_id
|
|
15
|
-
t.string :name
|
|
16
|
-
t.string :workflow_state, default: "active"
|
|
17
|
-
t.timestamps
|
|
18
|
-
end
|
|
19
|
-
add_index :accounts, :canvas_id, unique: true
|
|
20
|
-
|
|
21
|
-
create_table :admins, force: true do |t|
|
|
22
|
-
t.bigint :canvas_id
|
|
23
|
-
t.bigint :canvas_user_id, null: false
|
|
24
|
-
t.bigint :canvas_account_id, null: false
|
|
25
|
-
t.string :role_name
|
|
26
|
-
t.string :workflow_state, default: "active"
|
|
27
|
-
t.timestamps
|
|
28
|
-
end
|
|
29
|
-
add_index :admins, [ :canvas_user_id, :canvas_account_id ]
|
|
30
|
-
end
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
require 'spec_helper'
|
|
2
|
-
|
|
3
|
-
module PandaPal::Jobs
|
|
4
|
-
RSpec.describe GradePassbackJob, type: :job do
|
|
5
|
-
let(:organization) { create(:panda_pal_organization) }
|
|
6
|
-
let(:job) { described_class.new }
|
|
7
|
-
|
|
8
|
-
describe '#perform' do
|
|
9
|
-
context 'with missing required params' do
|
|
10
|
-
it 'raises MissingGradePassbackParams when passback_guid is missing' do
|
|
11
|
-
expect(IMS::LTI::ToolProvider).not_to receive(:new)
|
|
12
|
-
|
|
13
|
-
expect {
|
|
14
|
-
job.perform(organization, { passback_url: 'http://example.com/passback', score: 0.9 })
|
|
15
|
-
}.to raise_error(PandaPal::Jobs::MissingGradePassbackParams)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
it 'raises MissingGradePassbackParams when passback_url is missing' do
|
|
19
|
-
expect(IMS::LTI::ToolProvider).not_to receive(:new)
|
|
20
|
-
|
|
21
|
-
expect {
|
|
22
|
-
job.perform(organization, { passback_guid: 'guid-123', score: 0.9 })
|
|
23
|
-
}.to raise_error(PandaPal::Jobs::MissingGradePassbackParams)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
it 'raises MissingGradePassbackParams when both score and total_score are missing' do
|
|
27
|
-
expect(IMS::LTI::ToolProvider).not_to receive(:new)
|
|
28
|
-
|
|
29
|
-
expect {
|
|
30
|
-
job.perform(organization, { passback_guid: 'guid-123', passback_url: 'http://example.com/passback' })
|
|
31
|
-
}.to raise_error(PandaPal::Jobs::MissingGradePassbackParams)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
it 'raises MissingGradePassbackParams when opts is entirely empty' do
|
|
35
|
-
expect(IMS::LTI::ToolProvider).not_to receive(:new)
|
|
36
|
-
|
|
37
|
-
expect {
|
|
38
|
-
job.perform(organization, {})
|
|
39
|
-
}.to raise_error(PandaPal::Jobs::MissingGradePassbackParams)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
context 'with valid options' do
|
|
44
|
-
let(:opts) { { passback_guid: 'guid-123', passback_url: 'http://example.com/passback', score: 0.9 } }
|
|
45
|
-
let(:tool_provider_double) { double('IMS::LTI::ToolProvider') }
|
|
46
|
-
|
|
47
|
-
before do
|
|
48
|
-
allow(IMS::LTI::ToolProvider).to receive(:new)
|
|
49
|
-
.with(organization.key, organization.secret, hash_including(
|
|
50
|
-
'lis_result_sourcedid' => opts[:passback_guid],
|
|
51
|
-
'lis_outcome_service_url' => opts[:passback_url]
|
|
52
|
-
))
|
|
53
|
-
.and_return(tool_provider_double)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
it 'posts to the LMS and does not raise when the response is successful' do
|
|
57
|
-
success_result = double('OutcomeResponse', success?: true)
|
|
58
|
-
expect(tool_provider_double).to receive(:post_extended_replace_result!).with(opts).and_return(success_result)
|
|
59
|
-
|
|
60
|
-
expect {
|
|
61
|
-
job.perform(organization, opts)
|
|
62
|
-
}.not_to raise_error
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
it 'raises GradePassbackFailure when the tool provider reports failure' do
|
|
66
|
-
failure_result = double(
|
|
67
|
-
'OutcomeResponse',
|
|
68
|
-
success?: false,
|
|
69
|
-
response_code: 500,
|
|
70
|
-
code_major: 'failure',
|
|
71
|
-
severity: 'error',
|
|
72
|
-
description: 'boom'
|
|
73
|
-
)
|
|
74
|
-
expect(tool_provider_double).to receive(:post_extended_replace_result!).with(opts).and_return(failure_result)
|
|
75
|
-
|
|
76
|
-
expect {
|
|
77
|
-
job.perform(organization, opts)
|
|
78
|
-
}.to raise_error(PandaPal::Jobs::GradePassbackFailure)
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|