panda_pal 5.18.2 → 5.19.0.beta1

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.
@@ -0,0 +1,72 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe PandaPal::LtiRegistration, :isolated_lti_config do
4
+ let(:organization) { create(:panda_pal_organization) }
5
+
6
+ before { PandaPal.define_tool(:translate) }
7
+
8
+ def register(**attrs)
9
+ organization.lti_registrations.create!(tool: 'translate', client_id: '10000000000001', **attrs)
10
+ end
11
+
12
+ describe '.for_launch' do
13
+ it 'prefers a deployment-specific registration' do
14
+ key_wide = register
15
+ dedicated = register(deployment_id: 'dep-1')
16
+
17
+ expect(described_class.for_launch('10000000000001', 'dep-1')).to eq(dedicated)
18
+ expect(described_class.for_launch('10000000000001', 'dep-2')).to eq(key_wide)
19
+ end
20
+
21
+ it 'matches a key-wide registration when the launch has no deployment_id' do
22
+ key_wide = register
23
+
24
+ expect(described_class.for_launch('10000000000001', nil)).to eq(key_wide)
25
+ end
26
+
27
+ it 'matches numeric client_ids' do
28
+ key_wide = register
29
+
30
+ expect(described_class.for_launch(10000000000001, nil)).to eq(key_wide)
31
+ end
32
+
33
+ it 'returns nil for unknown or blank client_ids' do
34
+ register
35
+
36
+ expect(described_class.for_launch('99', nil)).to be_nil
37
+ expect(described_class.for_launch(nil, 'dep-1')).to be_nil
38
+ end
39
+ end
40
+
41
+ describe 'validations' do
42
+ it 'rejects a tool that was never defined' do
43
+ registration = organization.lti_registrations.build(tool: 'nope', client_id: '1')
44
+
45
+ expect(registration).not_to be_valid
46
+ expect(registration.errors[:tool]).to be_present
47
+ end
48
+
49
+ it 'allows only one key-wide registration per client_id' do
50
+ register
51
+
52
+ expect { register }.to raise_error(ActiveRecord::RecordInvalid)
53
+ end
54
+
55
+ it 'allows only one registration per client_id and deployment' do
56
+ register(deployment_id: 'dep-1')
57
+
58
+ expect { register(deployment_id: 'dep-1') }.to raise_error(ActiveRecord::RecordInvalid)
59
+ expect { register(deployment_id: 'dep-2') }.not_to raise_error
60
+ end
61
+
62
+ it 'stores a blank deployment_id as nil' do
63
+ expect(register(deployment_id: '').deployment_id).to be_nil
64
+ end
65
+ end
66
+
67
+ it 'is destroyed with its organization' do
68
+ register
69
+
70
+ expect { organization.destroy }.to change(described_class, :count).by(-1)
71
+ end
72
+ end
@@ -0,0 +1,115 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'PandaPal::Platform::Canvas::OrgExtension#install_lti', :isolated_lti_config do
4
+ let(:organization) { create(:panda_pal_organization, key: '10000000000001') }
5
+ let(:client) { double('Bearcat::Client') }
6
+ let(:host) { 'https://tool.example.com' }
7
+ let(:installed_tools) { [] }
8
+
9
+ before do
10
+ # _install_lti only checks that Bearcat exists; the client itself is stubbed.
11
+ stub_const('Bearcat', Module.new)
12
+ c = client
13
+ organization.define_singleton_method(:bearcat_client) { c }
14
+
15
+ PandaPal.lti_environments = { domain: 'tool.example.com' }
16
+ PandaPal.lti_options = { title: 'Shared Title' }
17
+ PandaPal.define_tool(:translate, title: 'Translate Title')
18
+ PandaPal.stage_placement(:editor_button, tool: :translate, url: :panda_pal)
19
+
20
+ allow(client).to receive(:account_external_tools).with('self').and_return(double(all_pages_each: installed_tools))
21
+ end
22
+
23
+ after { Apartment::Tenant.reset }
24
+
25
+ describe 'LTI 1.3' do
26
+ let(:developer_key) do
27
+ { id: 10000000000002, api_key: 'dev-key-secret', developer_key_account_binding: { workflow_state: 'on' } }
28
+ end
29
+
30
+ before do
31
+ allow(client).to receive(:get).and_return([])
32
+ allow(HTTParty).to receive(:get) do |url, **|
33
+ double(body: { target_link_uri: "#{host}/panda_pal/v1p3/resource_link_request", extensions: [] }.to_json, url: url)
34
+ end
35
+ end
36
+
37
+ it 'creates a Developer Key from the tool config, installs it, and registers it without touching the org key' do
38
+ expect(client).to receive(:post).with('api/lti/accounts/self/developer_keys/tool_configuration', hash_including(
39
+ developer_key: hash_including(name: 'Translate Title'),
40
+ )).and_return({ developer_key: developer_key })
41
+ expect(client).to receive(:create_account_external_tool).with('self', { client_id: 10000000000002 }).and_return({ id: 7, deployment_id: 'dep-9' })
42
+
43
+ organization.install_lti(host: host, tool: :translate)
44
+
45
+ expect(HTTParty).to have_received(:get).with(a_string_including('v1p3/config?tool=translate'), format: :plain)
46
+ expect(organization.reload.key).to eq('10000000000001')
47
+ expect(organization.lti_registrations.pluck(:tool, :client_id, :deployment_id)).to eq([['translate', '10000000000002', nil]])
48
+ end
49
+
50
+ it 'records the deployment_id for a dedicated deployment' do
51
+ allow(client).to receive(:post).and_return({ developer_key: developer_key })
52
+ allow(client).to receive(:create_account_external_tool).and_return({ id: 7, deployment_id: 'dep-9' })
53
+
54
+ organization.install_lti(host: host, tool: :translate, dedicated_deployment: true)
55
+
56
+ expect(organization.lti_registrations.pluck(:deployment_id)).to eq(['dep-9'])
57
+ end
58
+
59
+ context 'when the tool is already registered and installed' do
60
+ let(:installed_tools) { [{ id: 7, developer_key_id: 2, deployment_id: 'dep-9' }] }
61
+
62
+ before do
63
+ organization.lti_registrations.create!(tool: 'translate', client_id: '10000000000002')
64
+ allow(client).to receive(:get).and_return([developer_key])
65
+ end
66
+
67
+ it 'updates the registered Developer Key in place and reuses its install' do
68
+ expect(client).not_to receive(:post)
69
+ expect(client).not_to receive(:create_account_external_tool)
70
+ expect(client).to receive(:put).with('api/lti/developer_keys/10000000000002/tool_configuration', anything).and_return(developer_key)
71
+
72
+ organization.install_lti(host: host, tool: :translate, exists: :update)
73
+
74
+ expect(organization.lti_registrations.count).to eq(1)
75
+ end
76
+ end
77
+
78
+ it 'parses dotted versions (1.3 used to resolve to v1p1)' do
79
+ allow(client).to receive(:post).and_return({ developer_key: developer_key })
80
+ allow(client).to receive(:create_account_external_tool).and_return({ id: 7, deployment_id: 'dep-9' })
81
+
82
+ expect { organization.install_lti(host: host, tool: :translate, version: '1.3') }.not_to raise_error
83
+ expect(organization.lti_registrations.count).to eq(1)
84
+ end
85
+
86
+ it 'raises for a tool that was never defined' do
87
+ expect { organization.install_lti(host: host, tool: :nope) }.to raise_error(PandaPal::UnknownLtiTool)
88
+ end
89
+ end
90
+
91
+ describe 'LTI 1.0' do
92
+ let(:installed_tools) do
93
+ [
94
+ { id: 1, consumer_key: organization.key, custom_fields: {} },
95
+ { id: 2, consumer_key: organization.key, custom_fields: { PandaPal::TOOL_CUSTOM_FIELD => 'translate' } },
96
+ ]
97
+ end
98
+
99
+ it "updates only the matching tool's install, pointing it at that tool's config" do
100
+ expect(client).to receive(:edit_account_external_tool).with('self', 2, hash_including(
101
+ name: 'Translate Title',
102
+ consumer_key: organization.key,
103
+ config_url: a_string_including('v1p0/config?tool=translate'),
104
+ ))
105
+
106
+ organization.install_lti(host: host, tool: :translate, version: '1.0', exists: :update)
107
+ end
108
+
109
+ it 'still matches the default install by consumer key alone' do
110
+ expect(client).to receive(:edit_account_external_tool).with('self', 1, hash_including(name: 'Shared Title'))
111
+
112
+ organization.install_lti(host: host, version: 'v1p0', exists: :update)
113
+ end
114
+ end
115
+ end
data/spec/spec_helper.rb CHANGED
@@ -27,6 +27,7 @@ require "panda_pal/spec_helper"
27
27
  ActiveRecord::Schema.verbose = false
28
28
 
29
29
  Dir[File.dirname(__FILE__) + "/factories/**/*.rb"].each {|f| require f }
30
+ Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f }
30
31
 
31
32
  RSpec.configure do |config|
32
33
  config.include PandaPal::SpecHelper
@@ -0,0 +1,24 @@
1
+ # PandaPal's LTI configuration (placements, tools, options, ...) lives in class
2
+ # variables that host apps set once at boot. Specs that change it tag themselves
3
+ # `:isolated_lti_config` so whatever they register is rolled back afterwards and
4
+ # never leaks into later spec files.
5
+ module IsolatedLtiConfig
6
+ VARS = %i[
7
+ @@lti_navigation
8
+ @@lti_tools
9
+ @@lti_options
10
+ @@lti_custom_params
11
+ @@lti_environments
12
+ ].freeze
13
+ end
14
+
15
+ RSpec.shared_context 'isolated LTI config', isolated_lti_config: true do
16
+ around do |example|
17
+ saved = IsolatedLtiConfig::VARS.to_h { |var| [var, PandaPal.class_variable_get(var).deep_dup] }
18
+ begin
19
+ example.run
20
+ ensure
21
+ saved.each { |var, value| PandaPal.class_variable_set(var, value) }
22
+ end
23
+ end
24
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: panda_pal
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.18.2
4
+ version: 5.19.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Instructure CustomDev
@@ -113,6 +113,20 @@ dependencies:
113
113
  - - ">="
114
114
  - !ruby/object:Gem::Version
115
115
  version: '0'
116
+ - !ruby/object:Gem::Dependency
117
+ name: json-jwt
118
+ requirement: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '1.15'
123
+ type: :runtime
124
+ prerelease: false
125
+ version_requirements: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '1.15'
116
130
  - !ruby/object:Gem::Dependency
117
131
  name: httparty
118
132
  requirement: !ruby/object:Gem::Requirement
@@ -158,6 +172,7 @@ files:
158
172
  - app/lib/panda_pal/launch_url_helpers.rb
159
173
  - app/lib/panda_pal/lti_jwt_validator.rb
160
174
  - app/models/panda_pal/api_call.rb
175
+ - app/models/panda_pal/lti_registration.rb
161
176
  - app/models/panda_pal/organization.rb
162
177
  - app/models/panda_pal/organization_concerns/multi_database_sharding.rb
163
178
  - app/models/panda_pal/organization_concerns/organization_builder.rb
@@ -183,6 +198,8 @@ files:
183
198
  - db/migrate/20171205194657_remove_old_organization_settings.rb
184
199
  - db/migrate/20220721095653_create_panda_pal_api_calls.rb
185
200
  - db/migrate/20250401214421_rename_session_session_key.rb
201
+ - db/migrate/20261006000000_create_panda_pal_lti_registrations.rb
202
+ - db/migrate/20261006000001_add_tool_to_panda_pal_sessions.rb
186
203
  - lib/panda_pal.rb
187
204
  - lib/panda_pal/concerns/ability_helper.rb
188
205
  - lib/panda_pal/engine.rb
@@ -202,6 +219,7 @@ files:
202
219
  - spec/controllers/panda_pal/api_call_controller_spec.rb
203
220
  - spec/controllers/panda_pal/lti_v1_p0_controller_spec.rb
204
221
  - spec/controllers/panda_pal/lti_v1_p3_controller_spec.rb
222
+ - spec/controllers/panda_pal/lti_v1_p3_tools_spec.rb
205
223
  - spec/core/apartment_multidb_spec.rb
206
224
  - spec/factories/panda_pal_organizations.rb
207
225
  - spec/factories/panda_pal_sessions.rb
@@ -215,14 +233,18 @@ files:
215
233
  - spec/lib/panda_pal/ags_client_spec.rb
216
234
  - spec/lib/panda_pal/helpers/secure_headers_spec.rb
217
235
  - spec/lib/panda_pal/helpers/session_replacement_spec.rb
236
+ - spec/lib/panda_pal/lti_tools_spec.rb
218
237
  - spec/models/panda_pal/api_call_spec.rb
238
+ - spec/models/panda_pal/lti_registration_spec.rb
219
239
  - spec/models/panda_pal/organization/settings_validation_spec.rb
220
240
  - spec/models/panda_pal/organization/task_scheduling_spec.rb
221
241
  - spec/models/panda_pal/organization_spec.rb
242
+ - spec/models/panda_pal/platform/canvas_install_spec.rb
222
243
  - spec/models/panda_pal/platform_spec.rb
223
244
  - spec/models/panda_pal/session_account_roles_spec.rb
224
245
  - spec/models/panda_pal/session_spec.rb
225
246
  - spec/spec_helper.rb
247
+ - spec/support/isolated_lti_config.rb
226
248
  homepage: http://instructure.com
227
249
  licenses:
228
250
  - MIT
@@ -248,6 +270,7 @@ test_files:
248
270
  - spec/controllers/panda_pal/api_call_controller_spec.rb
249
271
  - spec/controllers/panda_pal/lti_v1_p0_controller_spec.rb
250
272
  - spec/controllers/panda_pal/lti_v1_p3_controller_spec.rb
273
+ - spec/controllers/panda_pal/lti_v1_p3_tools_spec.rb
251
274
  - spec/core/apartment_multidb_spec.rb
252
275
  - spec/factories/panda_pal_organizations.rb
253
276
  - spec/factories/panda_pal_sessions.rb
@@ -261,11 +284,15 @@ test_files:
261
284
  - spec/lib/panda_pal/ags_client_spec.rb
262
285
  - spec/lib/panda_pal/helpers/secure_headers_spec.rb
263
286
  - spec/lib/panda_pal/helpers/session_replacement_spec.rb
287
+ - spec/lib/panda_pal/lti_tools_spec.rb
264
288
  - spec/models/panda_pal/api_call_spec.rb
289
+ - spec/models/panda_pal/lti_registration_spec.rb
265
290
  - spec/models/panda_pal/organization/settings_validation_spec.rb
266
291
  - spec/models/panda_pal/organization/task_scheduling_spec.rb
267
292
  - spec/models/panda_pal/organization_spec.rb
293
+ - spec/models/panda_pal/platform/canvas_install_spec.rb
268
294
  - spec/models/panda_pal/platform_spec.rb
269
295
  - spec/models/panda_pal/session_account_roles_spec.rb
270
296
  - spec/models/panda_pal/session_spec.rb
271
297
  - spec/spec_helper.rb
298
+ - spec/support/isolated_lti_config.rb