panda_pal 5.4.9 → 5.6.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab247586547b242a21faf1f5d26b8c58eee77f0893bca00c68b36c984f7d646e
4
- data.tar.gz: 8929cb3f413b2bd3870b6f92fd7c73eba886e0d5f0c403e46d383d949b213b5d
3
+ metadata.gz: 57786e37a6af09156402d16afed167b7984105dde04a1e1cc32aad5d8505567a
4
+ data.tar.gz: 1d9cc2abbe1dce34ce0d52aa0234f6f8e909dca3a5c9eabd59b57d2b91de9f90
5
5
  SHA512:
6
- metadata.gz: a446ff7233316b5bdd0629b8529391d92c58a6596bb5cdffa166c5f5727e50e27c6ebd11781b45abcbc0659a8f1b01349425d165dfbb7cefe5e98a613778ee00
7
- data.tar.gz: 7dd81c73ae5d4c704289ecb172f5f4342531a2998a6e68d568dafc8ac88494d0f7e0638b35885a09c682dbac2cd9d670f1d984e9f9644e6b096f69bb5db5ed4d
6
+ metadata.gz: 1943d3e91f8c510f28eeedd1793a8853545800608b098aa29e217a3ab7a155ad6e6ea414e6c661b13206b0980b527425564612c91f65a97e17537436ac30e449
7
+ data.tar.gz: d762fe3b44e97fda8eaded8595ebf4f53a8a2e551de8fd5ab8e74433068dcb2a52fcc0dcbf5af637ca0e28c13ee88d3d751a3860bd8d873d61f85a3199d44055
data/README.md CHANGED
@@ -21,6 +21,16 @@ PandaPal::stage_navigation(:account_navigation, {
21
21
 
22
22
  Configuration data for an installation can be set by creating a `PandaPal::Organization` record. Due to the nature of the data segregation, once created, the name of the organization should not be changed (and will raise a validation error).
23
23
 
24
+ ### Canvas Installation
25
+ As of version 5.5.0, LTIs can be installed into Canvas via the console:
26
+ ```ruby
27
+ org.install_lti(
28
+ host: "https://your_lti.herokuapp.com",
29
+ context: "account/self", # (Optional) Or "account/3", "course/1", etc
30
+ exists: :error, # (Optional) Action to take if an LTI with the same Key already exists. Options are :error, :replace, :duplicate
31
+ )
32
+ ```
33
+
24
34
  ### LTI 1.3 Configuration
25
35
  LTI 1.3 has some additional configuration steps required to setup an LTI:
26
36
 
@@ -86,6 +96,35 @@ end
86
96
 
87
97
  XML for an installation can be generated by visiting `/lti/config` in your application.
88
98
 
99
+ ### Generated API Methods
100
+ It's common to need to manually trigger logic during UAT. PandaPal 5.6.0+ adds a feature to enable clients to do this themselves.
101
+ This is done by a deployer accessing the console and creating a `PandaPal::ApiCall`. This can be done like so:
102
+ ```ruby
103
+ org.create_api(<<~RUBY, expiration: 30.days.from_now, uses: 10)
104
+ arg1 = p[:some_param] # `p` is an in-scope variable that is a Hash of the params sent to the triggering request.
105
+ PandaPal::Organization.current.trigger_canvas_sync()
106
+ { foo: "bar" } # Will be returned to the client. Can be anything that is JSON serializable.
107
+ RUBY
108
+ # OR
109
+ org.create_api(:symbol_of_method_on_organization, expiration: 30.days.from_now, uses: 10)
110
+ ```
111
+ This will return a URL like such: `/panda_pal/call_logic?some_param=TODO&token=JWT.TOKEN.IS.HERE` that can be either GET'd or POST'd.
112
+ The URL generator will *attempt* to determine which params the logic accepts and insert them as `param=TODO` in the generated URL.
113
+
114
+ When triggered, the return value of the code will be wrapped and sent to the client:
115
+ ```json
116
+ {
117
+ "status": "ok",
118
+ "uses_remaining": 9,
119
+ "result": {
120
+ "foo": "bar",
121
+ }
122
+ }
123
+ ```
124
+
125
+ #### Revoking
126
+ A Call URI may be revoked by deleting the `PandaPal::ApiCall` object. `uses:` and logic are stored in the DB (and can thus be altered), but `expiration:` is stored in the JWT and is thus immutable.
127
+
89
128
  ### Routing
90
129
 
91
130
  The following routes should be added to the routes.rb file of the implementing LTI. (substituting `account_navigation` with the other staged navigation routes, if necessary)
@@ -0,0 +1,36 @@
1
+ require 'jwt'
2
+ require_dependency "panda_pal/application_controller"
3
+
4
+ module PandaPal
5
+ class ApiCallController < ApplicationController
6
+ rescue_from StandardError do |err|
7
+ render json: { status: 'error' }, status: 500
8
+ end
9
+ rescue_from ::JWT::DecodeError do |err|
10
+ render json: { status: 'error', error: "Invalid JWT" }, status: 403
11
+ end
12
+
13
+ around_action do |blk|
14
+ payload = ApiCall.decode_jwt(params[:token])
15
+ org_id = payload['organization_id']
16
+ if org_id
17
+ PandaPal::Organization.find(org_id).switch_tenant do
18
+ blk.call
19
+ end
20
+ else
21
+ blk.call
22
+ end
23
+ end
24
+
25
+ def call
26
+ ac = ApiCall.from_jwt(params.require(:token))
27
+ result = ac.call(params.to_unsafe_h)
28
+
29
+ render json: {
30
+ status: 'ok',
31
+ uses_remaining: ac.uses_remaining,
32
+ result: result,
33
+ }
34
+ end
35
+ end
36
+ end
@@ -1,6 +1,6 @@
1
1
  module PandaPal
2
2
  module LaunchUrlHelpers
3
- def self.absolute_launch_url(launch_type, host:, launch_handler: nil, default_auto_launch: false)
3
+ def self.absolute_launch_url(launch_type, host: uri_host, launch_handler: nil, default_auto_launch: false)
4
4
  opts = PandaPal.lti_paths[launch_type]
5
5
  auto_launch = opts[:auto_launch] != nil ? opts[:auto_launch] : default_auto_launch
6
6
  auto_launch = auto_launch && launch_handler.present?
@@ -18,12 +18,10 @@ module PandaPal
18
18
  end
19
19
 
20
20
  def self.url_for(options)
21
- request = Thread.current[:controller]&.request
22
- host = "#{request.scheme}://#{request.host_with_port}"
23
21
  if options.is_a?(Symbol)
24
- Rails.application.routes.url_helpers.send(options, host: host)
22
+ Rails.application.routes.url_helpers.send(options, host: uri_host)
25
23
  else
26
- options[:host] ||= host
24
+ options[:host] ||= uri_host
27
25
  Rails.application.routes.url_helpers.url_for(options)
28
26
  end
29
27
  end
@@ -74,7 +72,23 @@ module PandaPal
74
72
  uri.to_s
75
73
  end
76
74
 
77
- private
75
+ def self.with_uri_host(uri)
76
+ uri = URI.parse(uri) unless uri.is_a?(URI)
77
+ raise "host: param must have a protocal and no path" if uri.path.present?
78
+
79
+ initial = Thread.current[:panda_pal_access_uri]
80
+ begin
81
+ Thread.current[:panda_pal_access_uri] = uri
82
+ yield
83
+ ensure
84
+ Thread.current[:panda_pal_access_uri] = initial
85
+ end
86
+ end
87
+
88
+ def self.uri_host
89
+ request = Thread.current[:controller]&.request
90
+ Thread.current[:panda_pal_access_uri]&.to_s || "#{request.scheme}://#{request.host_with_port}"
91
+ end
78
92
 
79
93
  def self.resolve_route(key, *arguments, engine: 'PandaPal', **kwargs)
80
94
  return key if key.is_a?(String)
@@ -0,0 +1,58 @@
1
+ require 'jwt'
2
+
3
+ module PandaPal
4
+ class ApiCall < ActiveRecord::Base
5
+ # TODO Add Rate Limiting?
6
+
7
+ def self.decode_jwt(jwt)
8
+ jwt_payload, jwt_header = ::JWT.decode(jwt, Rails.application.secret_key_base, true, { algorithm: 'HS256' })
9
+ jwt_payload
10
+ end
11
+
12
+ def self.from_jwt(jwt)
13
+ jwt = decode_jwt(jwt) if jwt.is_a?(String)
14
+ ApiCall.find(jwt['api_call_id'])
15
+ end
16
+
17
+ before_validation on: [:update] do
18
+ errors.add(:expiration, 'is not changeable') if expiration_changed?
19
+ end
20
+
21
+ def call(params, internal: false)
22
+ if !internal && uses_remaining != nil
23
+ self.uses_remaining -= 1
24
+ if uses_remaining <= 0
25
+ destroy!
26
+ else
27
+ save!
28
+ end
29
+ end
30
+
31
+ prc = eval("->(p) {
32
+ #{logic}
33
+ }")
34
+
35
+ prc.call(params)
36
+ end
37
+
38
+ def call_url(host: nil, params: {}, **kwargs)
39
+ func_params = logic.scan(/\bp\[:(\w+)\]/).flatten
40
+ phash = {}
41
+ func_params.each.with_index do |p, i|
42
+ phash[p.to_sym] = params[p.to_sym] || "TODO"
43
+ end
44
+
45
+ PandaPal::Engine.routes.url_helpers.call_logic_url(self, token: jwt_token(**kwargs), only_path: !host, host: host, **phash, format: nil)
46
+ end
47
+
48
+ def jwt_token(expiration: self.expiration)
49
+ payload = {
50
+ api_call_id: id,
51
+ organization_id: Organization.current&.id,
52
+ }
53
+ payload[:exp] = expiration.iso8601 if expiration.present?
54
+ ::JWT.encode(payload, Rails.application.secret_key_base, 'HS256')
55
+ end
56
+
57
+ end
58
+ end
@@ -1,6 +1,3 @@
1
- require_relative './organization_concerns/settings_validation'
2
- require_relative './organization_concerns/task_scheduling'
3
-
4
1
  module PandaPal
5
2
  module OrganizationConcerns; end
6
3
 
@@ -57,8 +54,63 @@ module PandaPal
57
54
  include ext
58
55
  end
59
56
 
57
+ def create_api(logic, expiration: nil, uses: nil, host: nil)
58
+ switch_tenant do
59
+ logic = "current_organization.#{logic}" if logic.is_a?(Symbol)
60
+ ac = ApiCall.create!(
61
+ logic: logic,
62
+ expiration: expiration,
63
+ uses_remaining: uses,
64
+ )
65
+ ac.call_url(host: host)
66
+ end
67
+ end
68
+
69
+ def lti_platform
70
+ lti_platform_type.new(self)
71
+ end
72
+
73
+ def lti_platform_type
74
+ platform = PandaPal.lti_options[:platform] || 'canvas.instructure.com'
75
+ case platform
76
+ when 'canvas.instructure.com'
77
+ PandaPal::Platform::Canvas
78
+ # when 'bridgeapp.com'
79
+ # TODO Add support for Bridge?
80
+ else
81
+ raise "Unknown platform '#{platform}'"
82
+ end
83
+ end
84
+
85
+ def rename!(new_name)
86
+ do_switch = Apartment::Tenant.current == name
87
+ ActiveRecord::Base.connection.execute(
88
+ "ALTER SCHEMA \"#{name}\" RENAME TO \"#{new_name}\";"
89
+ )
90
+ self.class.where(id: id).update_all(name: new_name)
91
+ reload
92
+ switch_tenant if do_switch
93
+ end
94
+
95
+ def respond_to_missing?(name, include_private = false)
96
+ (platform_org_extension_module&.instance_method(name) rescue nil) || super
97
+ end
98
+
99
+ def method_missing(method, *args, &block)
100
+ ext = platform_org_extension_module
101
+ if (ext.instance_method(method) rescue nil)
102
+ ext.instance_method(method).bind_call(self, *args, &block)
103
+ else
104
+ super
105
+ end
106
+ end
107
+
60
108
  private
61
109
 
110
+ def platform_org_extension_module
111
+ defined?(lti_platform_type::OrgExtension) ? lti_platform_type::OrgExtension : nil
112
+ end
113
+
62
114
  def create_schema
63
115
  Apartment::Tenant.create name
64
116
  end
@@ -13,8 +13,6 @@ unless defined?(Sidekiq.schedule)
13
13
  return
14
14
  end
15
15
 
16
- require_relative 'settings_validation'
17
-
18
16
  module PandaPal
19
17
  module OrganizationConcerns
20
18
  module TaskScheduling
@@ -68,6 +66,10 @@ module PandaPal
68
66
  }
69
67
  end
70
68
 
69
+ def task_scheduled?(name_or_method)
70
+ _schedule_descriptors.key?(name_or_method.to_s)
71
+ end
72
+
71
73
  def remove_scheduled_task(name_or_method)
72
74
  dval = _schedule_descriptors.delete(name_or_method.to_s)
73
75
  Rails.logger.warn("No task with key '#{name_or_method}' to delete!") unless dval.present?
@@ -1,3 +1,8 @@
1
+ begin
2
+ require 'bearcat'
3
+ rescue LoadError
4
+ end
5
+
1
6
  module PandaPal
2
7
  class Platform
3
8
  def public_jwks
@@ -8,13 +13,24 @@ module PandaPal
8
13
  rescue
9
14
  nil
10
15
  end
16
+
17
+ protected
18
+
19
+ def self.find_org_setting(paths, org = current_organization)
20
+ paths.each do |p|
21
+ p = p.split('.').map(&:to_sym)
22
+ val = org.settings.dig(*p)
23
+ return val if val.present?
24
+ end
25
+ nil
26
+ end
11
27
  end
12
28
 
13
29
  class Platform::Canvas < Platform
14
- attr_accessor :base_url
30
+ attr_accessor :organization
15
31
 
16
- def initialize(base_url)
17
- @base_url = base_url
32
+ def initialize(org)
33
+ @organization = org
18
34
  end
19
35
 
20
36
  def host
@@ -32,9 +48,128 @@ module PandaPal
32
48
  def grant_url
33
49
  "#{base_url}/login/oauth2/token"
34
50
  end
35
- end
36
51
 
37
- class Platform
38
- CANVAS = Platform::Canvas.new('https://canvas.instructure.com')
52
+ def base_url; org.canvas_url; end
53
+
54
+ module OrgExtension
55
+ def install_lti(host: nil, context: :root_account, version: 'v1p1', exists: :error)
56
+ raise "Automatically installing this LTI requires Bearcat." unless defined?(Bearcat)
57
+
58
+ context = context.to_s
59
+ context = 'account/self' if context == 'root_account'
60
+ cid, ctype = context.split(/[\.\/]/).reverse
61
+ ctype ||= 'account'
62
+
63
+ existing_installs = bearcat_client.send(:"#{ctype}_external_tools", cid).all_pages_each.filter do |cet|
64
+ cet[:consumer_key] == self.key
65
+ end
66
+
67
+ if existing_installs.present?
68
+ case exists
69
+ when :error
70
+ raise "Tool with key #{self.key} already installed"
71
+ when :duplicate
72
+ when :replace
73
+ existing_installs.each do |install|
74
+ bearcat_client.send(:"delete_#{ctype}_external_tool", cid, install[:id])
75
+ end
76
+ else
77
+ raise "exists: #{exists} is not supported"
78
+ end
79
+ end
80
+
81
+ # TODO LTI 1.3 Support
82
+
83
+ conf = {
84
+ name: PandaPal.lti_options[:title],
85
+ description: PandaPal.lti_options[:description],
86
+ consumer_key: self.key,
87
+ shared_secret: self.secret,
88
+ privacy_level: "public",
89
+ config_type: 'by_url',
90
+ config_url: PandaPal::LaunchUrlHelpers.resolve_route(:v1p0_config_url, host: host),
91
+ }
92
+
93
+ bearcat_client.send(:"create_#{ctype}_external_tool", cid, conf)
94
+ end
95
+
96
+ def lti_api_configuration(host: nil)
97
+ PandaPal::LaunchUrlHelpers.with_uri_host(host) do
98
+ domain = PandaPal.lti_properties[:domain] || host.host
99
+ launch_url = PandaPal.lti_options[:secure_launch_url] ||
100
+ "#{domain}#{PandaPal.lti_options[:secure_launch_path]}" ||
101
+ PandaPal.lti_options[:launch_url] ||
102
+ "#{domain}#{PandaPal.lti_options[:launch_path]}" ||
103
+ domain
104
+
105
+ lti_json = {
106
+ name: PandaPal.lti_options[:title],
107
+ description: PandaPal.lti_options[:description],
108
+ domain: host.host,
109
+ url: launch_url,
110
+ consumer_key: self.key,
111
+ shared_secret: self.secret,
112
+ privacy_level: "public",
113
+
114
+ custom_fields: {},
115
+
116
+ environments: PandaPal.lti_environments,
117
+ }
118
+
119
+ lti_json = lti_json.with_indifferent_access
120
+ lti_json.merge!(PandaPal.lti_properties)
121
+
122
+ (PandaPal.lti_options[:custom_fields] || []).each do |k, v|
123
+ lti_json[:custom_fields][k] = v
124
+ end
125
+
126
+ PandaPal.lti_paths.each do |k, options|
127
+ options = PandaPal::LaunchUrlHelpers.normalize_lti_launch_desc(options)
128
+ options[:url] = PandaPal::LaunchUrlHelpers.absolute_launch_url(
129
+ k.to_sym,
130
+ host: host,
131
+ launch_handler: :v1p0_launch_path,
132
+ default_auto_launch: false
133
+ ).to_s
134
+ lti_json[k] = options
135
+ end
136
+
137
+ lti_json
138
+ end
139
+ end
140
+
141
+ def canvas_url
142
+ PandaPal::Platform.find_org_setting([
143
+ "canvas.base_url",
144
+ "canvas_url",
145
+ "canvas_base_url",
146
+ "canvas.url",
147
+ "base_url",
148
+ ], self) || (Rails.env.development? && 'http://localhost:3000') || 'https://canvas.instructure.com'
149
+ end
150
+
151
+ def canvas_api_token
152
+ PandaPal::Platform.find_org_setting([
153
+ "canvas.api_token",
154
+ "canvas.api_key",
155
+ "canvas.token",
156
+ "canvas_api_token",
157
+ "canvas_token",
158
+ "api_token",
159
+ ], self)
160
+ end
161
+
162
+ if defined?(Bearcat)
163
+ def bearcat_client
164
+ return canvas_sync_client if defined?(canvas_sync_client)
165
+
166
+ Bearcat::Client.new(
167
+ prefix: canvas_url,
168
+ token: canvas_token,
169
+ master_rate_limit: (Rails.env.production? && !!defined?(Redis) && ENV['REDIS_URL'].present?),
170
+ )
171
+ end
172
+ end
173
+ end
39
174
  end
40
175
  end
@@ -10,7 +10,7 @@ module PandaPal
10
10
 
11
11
  class DataSerializer
12
12
  def self.load(str)
13
- return nil unless str.present?
13
+ return {} unless str.present?
14
14
  begin
15
15
  parsed = JSON.parse(str)
16
16
  rescue JSON::ParserError
data/config/routes.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  PandaPal::Engine.routes.draw do
2
2
  get '/config' => 'lti_v1_p0#tool_config' # Legacy Support
3
3
 
4
+ get "/call_logic" => "api_call#call"
5
+ post "/call_logic" => "api_call#call"
6
+
4
7
  scope '/v1p0', as: 'v1p0' do
5
8
  get '/config' => 'lti_v1_p0#tool_config'
6
9
  post '/launch' => 'lti_v1_p0#launch'
@@ -0,0 +1,11 @@
1
+ class CreatePandaPalApiCalls < PandaPal::MiscHelper::MigrationClass
2
+ def change
3
+ create_table :panda_pal_api_calls do |t|
4
+ t.text :logic
5
+ t.string :expiration
6
+ t.integer :uses_remaining
7
+
8
+ t.timestamps null: false
9
+ end
10
+ end
11
+ end
@@ -15,13 +15,11 @@ module PandaPal
15
15
  end
16
16
 
17
17
  initializer :append_migrations do |app|
18
- unless app.root.to_s.match root.to_s
19
- config.paths["db/migrate"].expanded.each do |expanded_path|
20
- app.config.paths["db/migrate"] << expanded_path
21
- end
22
- # Apartment will modify this, but it doesn't fully support engine migrations, so we'll reset it here
23
- ActiveRecord::Migrator.migrations_paths = Rails.application.paths['db/migrate'].to_a
18
+ config.paths["db/migrate"].expanded.each do |expanded_path|
19
+ app.config.paths["db/migrate"] << expanded_path
24
20
  end
21
+ # Apartment will modify this, but it doesn't fully support engine migrations, so we'll reset it here
22
+ ActiveRecord::Migrator.migrations_paths = Rails.application.paths['db/migrate'].to_a
25
23
  end
26
24
 
27
25
  initializer 'interop dependencies' do
@@ -70,7 +68,7 @@ module PandaPal
70
68
  end
71
69
 
72
70
  initializer "panda_pal.assets.precompile" do |app|
73
- app.config.assets.precompile << "panda_pal_manifest.js"
71
+ app.config.assets.precompile << "panda_pal_manifest.js" rescue nil
74
72
  end
75
73
 
76
74
  initializer :secure_headers do |app|
@@ -13,14 +13,7 @@ module PandaPal::Helpers
13
13
  end
14
14
 
15
15
  def current_lti_platform
16
- return @current_lti_platform if @current_lti_platform.present?
17
- # TODO: (Future) This could be expanded more to take better advantage of the LTI 1.3 Multi-Tenancy model.
18
- if (canvas_url = current_organization&.settings&.dig(:canvas, :base_url)).present?
19
- @current_lti_platform ||= PandaPal::Platform::Canvas.new(canvas_url)
20
- end
21
- @current_lti_platform ||= PandaPal::Platform::Canvas.new('http://localhost:3000') if Rails.env.development?
22
- @current_lti_platform ||= PandaPal::Platform::CANVAS
23
- @current_lti_platform
16
+ @current_lti_platform ||= current_organization.lti_platform
24
17
  end
25
18
 
26
19
  def lti_launch_params
@@ -41,7 +34,7 @@ module PandaPal::Helpers
41
34
  authorized = false
42
35
  # We should verify the timestamp is recent (within 5 minutes). The approved timestamp is part of the signature,
43
36
  # so we don't need to worry about malicious users messing with it. We should deny requests that come too long
44
- # after the approved timestamp.
37
+ # after the approved timestamp.
45
38
  good_timestamp = params['oauth_timestamp'] && params['oauth_timestamp'].to_i > Time.now.to_i - 300
46
39
  if @organization = good_timestamp && params['oauth_consumer_key'] && PandaPal::Organization.find_by_key(params['oauth_consumer_key'])
47
40
  sanitized_params = request.request_parameters
@@ -1,3 +1,3 @@
1
1
  module PandaPal
2
- VERSION = "5.4.9"
2
+ VERSION = "5.6.0"
3
3
  end
data/panda_pal.gemspec CHANGED
@@ -1,11 +1,16 @@
1
1
  $:.push File.expand_path("../lib", __FILE__)
2
2
 
3
- require "panda_pal/version"
3
+ begin
4
+ require "panda_pal/version"
5
+ version = PandaPal::VERSION
6
+ rescue LoadError
7
+ version = "0.0.0.docker"
8
+ end
4
9
 
5
10
  # Describe your gem and declare its dependencies:
6
11
  Gem::Specification.new do |s|
7
12
  s.name = "panda_pal"
8
- s.version = PandaPal::VERSION
13
+ s.version = version
9
14
  s.authors = ["Instructure ProServe"]
10
15
  s.email = ["pseng@instructure.com"]
11
16
  s.homepage = "http://instructure.com"
@@ -22,10 +27,15 @@ Gem::Specification.new do |s|
22
27
  s.add_dependency 'attr_encrypted', '~> 3.0.0'
23
28
  s.add_dependency 'secure_headers', '~> 6.1'
24
29
  s.add_dependency 'json-jwt'
30
+ s.add_dependency 'jwt'
25
31
  s.add_dependency 'httparty'
26
32
 
33
+ s.add_development_dependency "rails", "~> 5.2"
34
+ s.add_development_dependency 'pg'
27
35
  s.add_development_dependency 'sidekiq'
28
36
  s.add_development_dependency 'sidekiq-scheduler'
37
+ s.add_development_dependency 'ros-apartment', '~> 2.11'
38
+ s.add_development_dependency 'ros-apartment-sidekiq', '~> 1.2'
29
39
  s.add_development_dependency 'rspec-rails'
30
40
  s.add_development_dependency 'factory_girl_rails'
31
41
  end
@@ -0,0 +1,27 @@
1
+ require 'rails_helper'
2
+
3
+ module PandaPal
4
+ RSpec.describe ApiCallController, type: :controller do
5
+ let!(:api_call) { ApiCall.create!(logic: '"#{p[:foo]}_bar"') }
6
+
7
+ def json_body
8
+ JSON.parse(response.body)
9
+ end
10
+
11
+ describe "#call" do
12
+ it "calls the function with parameters" do
13
+ get :call, params: { token: api_call.jwt_token, foo: 'bar', use_route: 'panda_pal' }
14
+ expect(response).to be_successful
15
+ expect(json_body["status"]).to eq 'ok'
16
+ expect(json_body["result"]).to eq 'bar_bar'
17
+ end
18
+
19
+ it "fails given a bad JWT" do
20
+ get :call, params: { token: "bad.jwt.token", foo: 'bar', use_route: 'panda_pal' }
21
+ expect(response).not_to be_successful
22
+ expect(json_body["status"]).to eq 'error'
23
+ expect(json_body["error"]).to eq 'Invalid JWT'
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,25 +1,25 @@
1
- # SQLite version 3.x
2
- # gem install sqlite3
3
- #
4
- # Ensure the SQLite 3 gem is defined in your Gemfile
5
- # gem 'sqlite3'
6
- #
1
+
7
2
  default: &default
8
3
  adapter: postgresql
9
- pool: 5
10
- timeout: 5000
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
11
 
12
12
  development:
13
13
  <<: *default
14
14
  database: panda_pal_development
15
15
 
16
- # Warning: The database defined as "test" will be erased and
17
- # re-generated from your development database when you run "rake".
18
- # Do not set this db to the same as development or production.
19
16
  test:
20
17
  <<: *default
21
18
  database: panda_pal_test
22
19
 
23
20
  production:
24
21
  <<: *default
25
- database: db/production.sqlite3
22
+ host: <%= ENV.fetch('DB_ADDRESS', 'localhost') %>
23
+ database: panda_pal_production
24
+ username: <%= ENV.fetch("DB_USERNAME", "panda_pal_specs_postgres_user") %>
25
+ password: <%= ENV.fetch("DB_PASSWORD", 'panda_pal_specs_postgres_password') %>