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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -0
  3. data/app/models/panda_pal/platform/canvas.rb +25 -0
  4. data/lib/panda_pal/version.rb +1 -1
  5. data/panda_pal.gemspec +1 -1
  6. metadata +2 -60
  7. data/spec/controllers/panda_pal/api_call_controller_spec.rb +0 -25
  8. data/spec/controllers/panda_pal/lti_v1_p0_controller_spec.rb +0 -194
  9. data/spec/controllers/panda_pal/lti_v1_p3_controller_spec.rb +0 -108
  10. data/spec/controllers/panda_pal/lti_v1_p3_tools_spec.rb +0 -144
  11. data/spec/core/apartment_multidb_spec.rb +0 -166
  12. data/spec/factories/panda_pal_organizations.rb +0 -9
  13. data/spec/factories/panda_pal_sessions.rb +0 -6
  14. data/spec/internal/app/models/account.rb +0 -7
  15. data/spec/internal/app/models/admin.rb +0 -6
  16. data/spec/internal/config/database.yml +0 -18
  17. data/spec/internal/config/routes.rb +0 -5
  18. data/spec/internal/config/storage.yml +0 -3
  19. data/spec/internal/db/schema.rb +0 -30
  20. data/spec/jobs/panda_pal/jobs/grade_passback_job_spec.rb +0 -83
  21. data/spec/lib/panda_pal/ags_client_spec.rb +0 -385
  22. data/spec/lib/panda_pal/helpers/secure_headers_spec.rb +0 -77
  23. data/spec/lib/panda_pal/helpers/session_replacement_spec.rb +0 -72
  24. data/spec/lib/panda_pal/lti_tools_spec.rb +0 -153
  25. data/spec/models/panda_pal/api_call_spec.rb +0 -30
  26. data/spec/models/panda_pal/lti_registration_spec.rb +0 -72
  27. data/spec/models/panda_pal/organization/settings_validation_spec.rb +0 -179
  28. data/spec/models/panda_pal/organization/task_scheduling_spec.rb +0 -145
  29. data/spec/models/panda_pal/organization_spec.rb +0 -50
  30. data/spec/models/panda_pal/platform/canvas_install_spec.rb +0 -115
  31. data/spec/models/panda_pal/platform_spec.rb +0 -349
  32. data/spec/models/panda_pal/session_account_roles_spec.rb +0 -182
  33. data/spec/models/panda_pal/session_spec.rb +0 -202
  34. data/spec/spec_helper.rb +0 -159
  35. 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,6 +0,0 @@
1
- FactoryBot.define do
2
- factory :panda_pal_session, class: 'PandaPal::Session' do
3
- panda_pal_organization { nil }
4
- data { "MyText" }
5
- end
6
- 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,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Minimal stand-in for the CanvasSync-generated Admin roster model that
4
- # Session#canvas_account_role_labels reads role labels from.
5
- class Admin < ActiveRecord::Base
6
- 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
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- Rails.application.routes.draw do
4
- mount PandaPal::Engine => "/panda_pal"
5
- end
@@ -1,3 +0,0 @@
1
- test:
2
- service: Disk
3
- root: /Users/eknapp/code/panda_pal/tmp/storage
@@ -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