panda_pal 5.19.0.beta1 → 5.19.0.beta2
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 +25 -0
- 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,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
|