coalescing_panda 1.1.21.1 → 1.2.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 +5 -5
- data/app/controllers/coalescing_panda/oauth2_controller.rb +11 -7
- data/app/models/coalescing_panda/canvas_api_auth.rb +3 -1
- data/app/models/coalescing_panda/lti_account.rb +0 -1
- data/db/migrate/20151209155923_add_refresh_settings_to_canvas_api_auth.rb +6 -0
- data/lib/coalescing_panda/bearcat_uri.rb +24 -0
- data/lib/coalescing_panda/controller_helpers.rb +85 -83
- data/lib/coalescing_panda/engine.rb +8 -62
- data/lib/coalescing_panda/version.rb +1 -1
- data/spec/controllers/coalescing_panda/lti_controller_spec.rb +2 -2
- data/spec/controllers/coalescing_panda/oauth2_controller_spec.rb +9 -4
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/test.log +15249 -0
- data/spec/factories/accounts.rb +24 -0
- data/spec/factories/canvas_api_auths.rb +9 -0
- data/spec/models/coalescing_panda/canvas_api_auth_spec.rb +11 -1
- data/spec/spec_helper.rb +2 -1
- metadata +84 -88
- data/app/views/coalescing_panda/lti/iframe_cookie_fix.html.erb +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 829bbfc42e1c72b89558457db1a751e63cae9ad6
|
4
|
+
data.tar.gz: c045205fcd8fa84ec952e683cf8591f395ae31b8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ae89c71cbbabc5adedc5ef81a630d260eb9df0955f271d07dce2477459a28a28c7385a40899e8362a19652e9fcf333390bb9585414fd628c2e4efad5c2a8707d
|
7
|
+
data.tar.gz: 97b7a7708254b674cf679c33731b524837f1205f359a5ec7ff80033835c69e043c1fd43b339c3e2410b9878c8c1ae7e78033f434f2d7253dbb968f075839aeee
|
@@ -13,13 +13,17 @@ module CoalescingPanda
|
|
13
13
|
client_key = lti_account.oauth2_client_key
|
14
14
|
user_id = params[:user_id]
|
15
15
|
api_domain = params[:api_domain]
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
16
|
+
prefix = [oauth2_protocol, '://', api_domain].join
|
17
|
+
Rails.logger.info "Creating Bearcat client for auth token retrieval pointed to: #{prefix}"
|
18
|
+
client = Bearcat::Client.new(prefix: prefix)
|
19
|
+
token_body = client.retrieve_token(client_id, coalescing_panda.oauth2_redirect_url, client_key, params['code'])
|
20
|
+
auth = CanvasApiAuth.where('user_id = ? and api_domain = ?', user_id, api_domain).first_or_initialize
|
21
|
+
auth.api_token = token_body['access_token']
|
22
|
+
auth.refresh_token = token_body['refresh_token']
|
23
|
+
auth.expires_at = Time.now + token_body['expires_in'] if token_body['expires_in']
|
24
|
+
auth.user_id = user_id
|
25
|
+
auth.api_domain = api_domain
|
26
|
+
auth.save!
|
23
27
|
end
|
24
28
|
end
|
25
29
|
|
@@ -6,7 +6,6 @@ module CoalescingPanda
|
|
6
6
|
:foreign_key => :coalescing_panda_lti_account_id,
|
7
7
|
:class_name => 'CoalescingPanda::LtiNonce'
|
8
8
|
|
9
|
-
attr_accessible :name, :key, :secret, :oauth2_client_id, :oauth2_client_key, :settings
|
10
9
|
serialize :settings
|
11
10
|
|
12
11
|
def validate_nonce(nonce, timestamp)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class CoalescingPanda::BearcatUri
|
2
|
+
attr_accessor :uri
|
3
|
+
|
4
|
+
def initialize(uri)
|
5
|
+
Rails.logger.info "Parsing Bearcat URI: #{uri}"
|
6
|
+
@uri = URI.parse(uri)
|
7
|
+
end
|
8
|
+
|
9
|
+
def api_domain
|
10
|
+
if Rails.env.test? or Rails.env.development?
|
11
|
+
uri.port.present? ? URI.encode("#{uri.host}:#{uri.port.to_s}") : uri.host
|
12
|
+
else
|
13
|
+
uri.host
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def scheme
|
18
|
+
[uri.scheme, '://'].join
|
19
|
+
end
|
20
|
+
|
21
|
+
def prefix
|
22
|
+
[scheme, api_domain].join
|
23
|
+
end
|
24
|
+
end
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'browser'
|
2
|
-
|
3
1
|
module CoalescingPanda
|
4
2
|
module ControllerHelpers
|
5
3
|
require 'useragent'
|
@@ -9,91 +7,111 @@ module CoalescingPanda
|
|
9
7
|
if lti_authorize!(*roles)
|
10
8
|
user_id = params['user_id']
|
11
9
|
launch_presentation_return_url = @lti_account.settings[:launch_presentation_return_url] || params['launch_presentation_return_url']
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
@canvas_url = client.auth_redirect_url(client_id,
|
30
|
-
coalescing_panda.oauth2_redirect_url({key: params['oauth_consumer_key'],
|
31
|
-
user_id: user_id, api_domain: api_domain, state: session['state']}))
|
32
|
-
#delete the added params so the original oauth sig still works
|
33
|
-
@lti_params.delete('action')
|
34
|
-
@lti_params.delete('controller')
|
35
|
-
render 'coalescing_panda/oauth2/oauth2'
|
10
|
+
launch_presentation_return_url = [BearcatUri.new(request.env["HTTP_REFERER"]).prefix, launch_presentation_return_url].join unless launch_presentation_return_url.include?('http')
|
11
|
+
uri = BearcatUri.new(launch_presentation_return_url)
|
12
|
+
set_session(launch_presentation_return_url)
|
13
|
+
|
14
|
+
api_auth = CanvasApiAuth.find_by('user_id = ? and api_domain = ?', user_id, uri.api_domain)
|
15
|
+
if api_auth
|
16
|
+
begin
|
17
|
+
refresh_token(uri, api_auth) if api_auth.expired?
|
18
|
+
@client = Bearcat::Client.new(token: api_auth.api_token, prefix: uri.prefix)
|
19
|
+
@client.user_profile 'self'
|
20
|
+
rescue Footrest::HttpError::BadRequest, Footrest::HttpError::Unauthorized
|
21
|
+
# If we can't retrieve our own user profile, or the token refresh fails, something is awry on the canvas end
|
22
|
+
# and we'll need to go through the oauth flow again
|
23
|
+
render_oauth2_page uri, user_id
|
24
|
+
end
|
25
|
+
else
|
26
|
+
render_oauth2_page uri, user_id
|
36
27
|
end
|
37
28
|
end
|
38
29
|
end
|
39
30
|
|
31
|
+
def render_oauth2_page(uri, user_id)
|
32
|
+
@lti_account = params['oauth_consumer_key'] && LtiAccount.find_by_key(params['oauth_consumer_key'])
|
33
|
+
return unless @lti_account
|
34
|
+
|
35
|
+
client_id = @lti_account.oauth2_client_id
|
36
|
+
client = Bearcat::Client.new(prefix: uri.prefix)
|
37
|
+
session['state'] = SecureRandom.hex(32)
|
38
|
+
redirect_path = coalescing_panda.oauth2_redirect_path({key: params['oauth_consumer_key'], user_id: user_id, api_domain: uri.api_domain, state: session['state']})
|
39
|
+
redirect_url = [coalescing_panda_url, redirect_path.sub(/^\/lti/, '')].join
|
40
|
+
@canvas_url = client.auth_redirect_url(client_id, redirect_url)
|
41
|
+
|
42
|
+
#delete the added params so the original oauth sig still works
|
43
|
+
@lti_params = params.to_hash
|
44
|
+
@lti_params.delete('action')
|
45
|
+
@lti_params.delete('controller')
|
46
|
+
render 'coalescing_panda/oauth2/oauth2', layout: 'coalescing_panda/application'
|
47
|
+
end
|
48
|
+
|
49
|
+
def refresh_token(uri, api_auth)
|
50
|
+
refresh_client = Bearcat::Client.new(prefix: uri.prefix)
|
51
|
+
refresh_body = refresh_client.retrieve_token(@lti_account.oauth2_client_id, coalescing_panda.oauth2_redirect_url,
|
52
|
+
@lti_account.oauth2_client_key, api_auth.refresh_token, 'refresh_token')
|
53
|
+
api_auth.update({ api_token: refresh_body['access_token'], expires_at: (Time.now + refresh_body['expires_in']) })
|
54
|
+
end
|
55
|
+
|
56
|
+
def check_refresh_token
|
57
|
+
uri = BearcatUri.new(session['uri'])
|
58
|
+
api_auth = CanvasApiAuth.find_by(user_id: session['user_id'], api_domain: uri.api_domain)
|
59
|
+
@lti_account = LtiAccount.find_by(key: session['oauth_consumer_key'])
|
60
|
+
return if @lti_account.nil? || api_auth.nil? # Not all tools use oauth
|
61
|
+
|
62
|
+
refresh_token(uri, api_auth) if api_auth.expired?
|
63
|
+
rescue Footrest::HttpError::BadRequest
|
64
|
+
render_oauth2_page uri, session['user_id']
|
65
|
+
end
|
66
|
+
|
67
|
+
def set_session(launch_presentation_return_url)
|
68
|
+
session['user_id'] = params['user_id']
|
69
|
+
session['uri'] = launch_presentation_return_url
|
70
|
+
session['lis_person_sourcedid'] = params['lis_person_sourcedid']
|
71
|
+
session['oauth_consumer_key'] = params['oauth_consumer_key']
|
72
|
+
session['custom_canvas_account_id'] = params['custom_canvas_account_id']
|
73
|
+
end
|
74
|
+
|
40
75
|
def have_session?
|
41
|
-
if params['
|
76
|
+
if params['tool_consumer_instance_guid'] && session['user_id'] != params['user_id']
|
42
77
|
reset_session
|
43
78
|
logger.info("resetting session params")
|
44
79
|
session['user_id'] = params['user_id']
|
45
80
|
end
|
46
81
|
|
47
82
|
if (session['user_id'] && session['uri'])
|
48
|
-
uri =
|
49
|
-
api_domain = uri.
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
83
|
+
uri = BearcatUri.new(session['uri'])
|
84
|
+
api_auth = CanvasApiAuth.find_by('user_id = ? and api_domain = ?', session['user_id'], uri.api_domain)
|
85
|
+
if api_auth && !api_auth.expired?
|
86
|
+
@client = Bearcat::Client.new(token: api_auth.api_token, prefix: uri.prefix)
|
87
|
+
@client.user_profile 'self'
|
88
|
+
end
|
54
89
|
end
|
55
90
|
|
56
91
|
@lti_account = LtiAccount.find_by_key(session['oauth_consumer_key']) if session['oauth_consumer_key']
|
57
92
|
|
58
93
|
!!@client
|
94
|
+
rescue Footrest::HttpError::Unauthorized
|
95
|
+
false
|
59
96
|
end
|
60
97
|
|
61
98
|
def lti_authorize!(*roles)
|
62
99
|
authorized = false
|
63
|
-
use_secure_headers_override(:safari_override) if browser.safari?
|
64
100
|
if @lti_account = params['oauth_consumer_key'] && LtiAccount.find_by_key(params['oauth_consumer_key'])
|
65
|
-
sanitized_params = sanitize_params
|
66
|
-
authenticator = IMS::LTI::Services::MessageAuthenticator.new(request.original_url, sanitized_params, @lti_account.secret)
|
67
|
-
authorized = authenticator.valid_signature?
|
68
101
|
@tp = IMS::LTI::ToolProvider.new(@lti_account.key, @lti_account.secret, params)
|
69
|
-
authorized =
|
102
|
+
authorized = @tp.valid_request?(request)
|
70
103
|
end
|
71
104
|
logger.info 'not authorized on tp valid request' if !authorized
|
72
105
|
authorized = authorized && (roles.count == 0 || (roles & lti_roles).count > 0)
|
73
106
|
logger.info 'not authorized on roles' if !authorized
|
74
107
|
authorized = authorized && @lti_account.validate_nonce(params['oauth_nonce'], DateTime.strptime(params['oauth_timestamp'], '%s'))
|
75
108
|
logger.info 'not authorized on nonce' if !authorized
|
76
|
-
|
77
|
-
render :text => 'Invalid Credentials, please contact your Administrator.', :status => :unauthorized
|
78
|
-
end
|
79
|
-
authorized = authorized && check_for_iframes_problem if authorized
|
109
|
+
render :text => 'Invalid Credentials, please contact your Administrator.', :status => :unauthorized unless authorized
|
80
110
|
authorized
|
81
111
|
end
|
82
112
|
|
83
|
-
# code for method taken from panda_pal v 4.0.8
|
84
|
-
# used for safari workaround
|
85
|
-
def sanitize_params
|
86
|
-
sanitized_params = request.request_parameters
|
87
|
-
# These params come over with a safari-workaround launch. The authenticator doesn't like them, so clean them out.
|
88
|
-
safe_unexpected_params = ["full_win_launch_requested", "platform_redirect_url"]
|
89
|
-
safe_unexpected_params.each do |p|
|
90
|
-
sanitized_params.delete(p)
|
91
|
-
end
|
92
|
-
sanitized_params
|
93
|
-
end
|
94
|
-
|
95
113
|
def lti_editor_button_response(return_type, return_params)
|
96
|
-
valid_return_types = [:image_url, :iframe, :url]
|
114
|
+
valid_return_types = [:image_url, :iframe, :url, :lti_launch_url]
|
97
115
|
raise "invalid editor button return type #{return_type}" unless valid_return_types.include?(return_type)
|
98
116
|
return_params[:return_type] = return_type.to_s
|
99
117
|
return_url = "#{params['launch_presentation_return_url']}?#{return_params.to_query}"
|
@@ -133,35 +151,19 @@ module CoalescingPanda
|
|
133
151
|
end
|
134
152
|
|
135
153
|
def session_check
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
session[:safari_cookie_fixed] = true
|
148
|
-
redirect_to params[:platform_redirect_url]
|
149
|
-
return false
|
150
|
-
end
|
151
|
-
true
|
152
|
-
end
|
153
|
-
def cookies_need_iframe_fix?
|
154
|
-
@browser ||= Browser.new(request.user_agent)
|
155
|
-
@browser.safari? && !request.referrer.include?('sessionless_launch') && !session[:safari_cookie_fixed] && !params[:platform_redirect_url]
|
156
|
-
end
|
157
|
-
def fix_iframe_cookies
|
158
|
-
if params[:safari_cookie_fix].present?
|
159
|
-
session[:safari_cookie_fixed] = true
|
160
|
-
redirect_to params[:return_to]
|
161
|
-
else
|
162
|
-
use_secure_headers_override(:safari_override)
|
163
|
-
render 'coalescing_panda/lti/iframe_cookie_fix', layout: false
|
154
|
+
user_agent = UserAgent.parse(request.user_agent) # Uses useragent gem!
|
155
|
+
if user_agent.browser == 'Safari' && !request.referrer.include?('sessionless_launch') # we apply the fix..
|
156
|
+
return if session[:safari_cookie_fixed] # it is already fixed.. continue
|
157
|
+
if params[:safari_cookie_fix].present? # we should be top window and able to set cookies.. so fix the issue :)
|
158
|
+
session[:safari_cookie_fixed] = true
|
159
|
+
redirect_to params[:return_to]
|
160
|
+
else
|
161
|
+
# Redirect the top frame to your server..
|
162
|
+
query = params.to_query
|
163
|
+
render :text => "<script>var referrer = document.referrer; top.window.location='?safari_cookie_fix=true&return_to='.concat(encodeURI(referrer));</script>"
|
164
|
+
end
|
164
165
|
end
|
165
166
|
end
|
167
|
+
|
166
168
|
end
|
167
169
|
end
|
@@ -1,7 +1,3 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'secure_headers'
|
4
|
-
|
5
1
|
module CoalescingPanda
|
6
2
|
class Engine < ::Rails::Engine
|
7
3
|
config.autoload_once_paths += Dir["#{config.root}/lib/**/"]
|
@@ -9,80 +5,30 @@ module CoalescingPanda
|
|
9
5
|
|
10
6
|
initializer :append_migrations do |app|
|
11
7
|
unless app.root.to_s.match root.to_s
|
12
|
-
config.paths[
|
13
|
-
app.config.paths[
|
8
|
+
config.paths["db/migrate"].expanded.each do |expanded_path|
|
9
|
+
app.config.paths["db/migrate"] << expanded_path
|
14
10
|
end
|
15
11
|
end
|
16
12
|
end
|
17
13
|
|
18
|
-
initializer 'coalescing_panda.app_controller' do |
|
14
|
+
initializer 'coalescing_panda.app_controller' do |app|
|
19
15
|
OAUTH_10_SUPPORT = true
|
20
16
|
ActiveSupport.on_load(:action_controller) do
|
21
17
|
include CoalescingPanda::ControllerHelpers
|
22
18
|
end
|
23
19
|
end
|
24
20
|
|
25
|
-
initializer 'cloaescing_panda.route_helper' do |
|
26
|
-
ActionDispatch::Routing::Mapper.include CoalescingPanda::RouteHelpers
|
21
|
+
initializer 'cloaescing_panda.route_helper' do |route|
|
22
|
+
ActionDispatch::Routing::Mapper.send :include, CoalescingPanda::RouteHelpers
|
27
23
|
end
|
28
24
|
|
29
|
-
initializer 'coalescing_panda.route_options', after
|
25
|
+
initializer 'coalescing_panda.route_options', :after => :disable_dependency_loading do |app|
|
30
26
|
ActiveSupport.on_load(:action_controller) do
|
31
|
-
#
|
27
|
+
#force the routes to load
|
32
28
|
Rails.application.reload_routes!
|
33
|
-
CoalescingPanda
|
29
|
+
CoalescingPanda::propagate_lti_navigation
|
34
30
|
end
|
35
31
|
end
|
36
32
|
|
37
|
-
initializer :secure_headers do |_app|
|
38
|
-
connect_src = %w[self]
|
39
|
-
script_src = %w[self]
|
40
|
-
if Rails.env.development?
|
41
|
-
# Allow webpack-dev-server to work
|
42
|
-
connect_src << 'http://localhost:3035'
|
43
|
-
connect_src << 'ws://localhost:3035'
|
44
|
-
# Allow stuff like rack-mini-profiler to work in development:
|
45
|
-
# https://github.com/MiniProfiler/rack-mini-profiler/issues/327
|
46
|
-
# DON'T ENABLE THIS FOR PRODUCTION!
|
47
|
-
script_src << "'unsafe-eval'"
|
48
|
-
end
|
49
|
-
begin
|
50
|
-
SecureHeaders::Configuration.default do |config|
|
51
|
-
# The default cookie headers aren't compatable with PandaPal cookies currenntly
|
52
|
-
config.cookies = { samesite: { none: true } }
|
53
|
-
# Need to allow LTI iframes
|
54
|
-
config.x_frame_options = 'ALLOWALL'
|
55
|
-
config.x_content_type_options = 'nosniff'
|
56
|
-
config.x_xss_protection = '1; mode=block'
|
57
|
-
config.referrer_policy = %w[origin-when-cross-origin strict-origin-when-cross-origin]
|
58
|
-
config.csp = {
|
59
|
-
default_src: %w[self],
|
60
|
-
script_src: script_src,
|
61
|
-
# Certain CSS-in-JS libraries inline the CSS, so we need to use unsafe-inline for them
|
62
|
-
style_src: %w[self unsafe-inline blob: https://fonts.googleapis.com],
|
63
|
-
font_src: %w[self data: https://fonts.gstatic.com],
|
64
|
-
connect_src: connect_src
|
65
|
-
}
|
66
|
-
end
|
67
|
-
rescue AlreadyConfiguredError
|
68
|
-
Rails.logger.warn 'Could not set default secure headers configuration when there is one already, continuing with previously defined configuration'
|
69
|
-
end
|
70
|
-
SecureHeaders::Configuration.override(:safari_override) do |config|
|
71
|
-
config.cookies = SecureHeaders::OPT_OUT
|
72
|
-
# Need to allow LTI iframes
|
73
|
-
config.x_frame_options = 'ALLOWALL'
|
74
|
-
config.x_content_type_options = 'nosniff'
|
75
|
-
config.x_xss_protection = '1; mode=block'
|
76
|
-
config.referrer_policy = %w[origin-when-cross-origin strict-origin-when-cross-origin]
|
77
|
-
config.csp = {
|
78
|
-
default_src: %w[self],
|
79
|
-
script_src: script_src,
|
80
|
-
# Certain CSS-in-JS libraries inline the CSS, so we need to use unsafe-inline for them
|
81
|
-
style_src: %w[self unsafe-inline blob: https://fonts.googleapis.com],
|
82
|
-
font_src: %w[self data: https://fonts.gstatic.com],
|
83
|
-
connect_src: connect_src
|
84
|
-
}
|
85
|
-
end
|
86
|
-
end
|
87
33
|
end
|
88
34
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe CoalescingPanda::LtiController do
|
3
|
+
describe CoalescingPanda::LtiController, type: :controller do
|
4
4
|
routes { CoalescingPanda::Engine.routes }
|
5
5
|
|
6
6
|
describe '#lti_config' do
|
@@ -41,4 +41,4 @@ describe CoalescingPanda::LtiController do
|
|
41
41
|
end
|
42
42
|
|
43
43
|
|
44
|
-
end
|
44
|
+
end
|
@@ -1,15 +1,20 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe CoalescingPanda::Oauth2Controller do
|
3
|
+
describe CoalescingPanda::Oauth2Controller, type: :controller do
|
4
4
|
routes { CoalescingPanda::Engine.routes }
|
5
|
+
let(:account) { FactoryGirl.create(:account, settings: {base_url: 'foo.com'}) }
|
5
6
|
|
6
7
|
describe "#redirect" do
|
7
8
|
it 'creates a token in the db' do
|
8
9
|
ENV['OAUTH_PROTOCOL'] = 'http'
|
9
|
-
Bearcat::Client.any_instance.stub(retrieve_token: '
|
10
|
-
|
10
|
+
Bearcat::Client.any_instance.stub(retrieve_token: { 'access_token' => 'token', 'refresh_token' => 'token', 'expires_in' => 3600 })
|
11
|
+
session[:state] = 'test'
|
12
|
+
get :redirect, {user_id: 1, api_domain: 'foo.com', code: 'bar', key: account.key, state: 'test'}
|
11
13
|
auth = CoalescingPanda::CanvasApiAuth.find_by_user_id_and_api_domain(1, 'foo.com')
|
12
14
|
auth.should_not == nil
|
15
|
+
expect(auth.api_token).to eql 'token'
|
16
|
+
expect(auth.refresh_token).to eql 'token'
|
17
|
+
expect(auth.expires_at).to be > 50.minutes.from_now
|
13
18
|
end
|
14
19
|
|
15
20
|
it "doesn't create a token in the db" do
|
@@ -19,4 +24,4 @@ describe CoalescingPanda::Oauth2Controller do
|
|
19
24
|
|
20
25
|
end
|
21
26
|
|
22
|
-
end
|
27
|
+
end
|