oauth2-aptible 0.9.4.aptible
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 +7 -0
- data/.document +5 -0
- data/CONTRIBUTING.md +18 -0
- data/LICENSE.md +20 -0
- data/README.md +137 -0
- data/Rakefile +39 -0
- data/lib/oauth2.rb +10 -0
- data/lib/oauth2/access_token.rb +173 -0
- data/lib/oauth2/client.rb +173 -0
- data/lib/oauth2/error.rb +24 -0
- data/lib/oauth2/response.rb +90 -0
- data/lib/oauth2/strategy/assertion.rb +72 -0
- data/lib/oauth2/strategy/auth_code.rb +33 -0
- data/lib/oauth2/strategy/base.rb +16 -0
- data/lib/oauth2/strategy/client_credentials.rb +36 -0
- data/lib/oauth2/strategy/implicit.rb +29 -0
- data/lib/oauth2/strategy/password.rb +27 -0
- data/lib/oauth2/version.rb +15 -0
- data/oauth2.gemspec +27 -0
- data/spec/helper.rb +29 -0
- data/spec/oauth2/access_token_spec.rb +172 -0
- data/spec/oauth2/client_spec.rb +205 -0
- data/spec/oauth2/response_spec.rb +101 -0
- data/spec/oauth2/strategy/assertion_spec.rb +56 -0
- data/spec/oauth2/strategy/auth_code_spec.rb +88 -0
- data/spec/oauth2/strategy/base_spec.rb +7 -0
- data/spec/oauth2/strategy/client_credentials_spec.rb +81 -0
- data/spec/oauth2/strategy/implicit_spec.rb +28 -0
- data/spec/oauth2/strategy/password_spec.rb +57 -0
- metadata +174 -0
@@ -0,0 +1,205 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe OAuth2::Client do
|
4
|
+
let!(:error_value) { 'invalid_token' }
|
5
|
+
let!(:error_description_value) { 'bad bad token' }
|
6
|
+
|
7
|
+
subject do
|
8
|
+
OAuth2::Client.new('abc', 'def', :site => 'https://api.example.com') do |builder|
|
9
|
+
builder.adapter :test do |stub|
|
10
|
+
stub.get('/success') { |env| [200, {'Content-Type' => 'text/awesome'}, 'yay'] }
|
11
|
+
stub.get('/reflect') { |env| [200, {}, env[:body]] }
|
12
|
+
stub.post('/reflect') { |env| [200, {}, env[:body]] }
|
13
|
+
stub.get('/unauthorized') { |env| [401, {'Content-Type' => 'application/json'}, MultiJson.encode(:error => error_value, :error_description => error_description_value)] }
|
14
|
+
stub.get('/conflict') { |env| [409, {'Content-Type' => 'text/plain'}, 'not authorized'] }
|
15
|
+
stub.get('/redirect') { |env| [302, {'Content-Type' => 'text/plain', 'location' => '/success'}, ''] }
|
16
|
+
stub.post('/redirect') { |env| [303, {'Content-Type' => 'text/plain', 'location' => '/reflect'}, ''] }
|
17
|
+
stub.get('/error') { |env| [500, {'Content-Type' => 'text/plain'}, 'unknown error'] }
|
18
|
+
stub.get('/empty_get') { |env| [204, {}, nil] }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '#initialize' do
|
24
|
+
it 'assigns id and secret' do
|
25
|
+
expect(subject.id).to eq('abc')
|
26
|
+
expect(subject.secret).to eq('def')
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'assigns site from the options hash' do
|
30
|
+
expect(subject.site).to eq('https://api.example.com')
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'assigns Faraday::Connection#host' do
|
34
|
+
expect(subject.connection.host).to eq('api.example.com')
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'leaves Faraday::Connection#ssl unset' do
|
38
|
+
expect(subject.connection.ssl).to be_empty
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'is able to pass a block to configure the connection' do
|
42
|
+
connection = double('connection')
|
43
|
+
builder = double('builder')
|
44
|
+
allow(connection).to receive(:build).and_yield(builder)
|
45
|
+
allow(Faraday::Connection).to receive(:new).and_return(connection)
|
46
|
+
|
47
|
+
expect(builder).to receive(:adapter).with(:test)
|
48
|
+
|
49
|
+
OAuth2::Client.new('abc', 'def') do |client|
|
50
|
+
client.adapter :test
|
51
|
+
end.connection
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'defaults raise_errors to true' do
|
55
|
+
expect(subject.options[:raise_errors]).to be true
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'allows true/false for raise_errors option' do
|
59
|
+
client = OAuth2::Client.new('abc', 'def', :site => 'https://api.example.com', :raise_errors => false)
|
60
|
+
expect(client.options[:raise_errors]).to be false
|
61
|
+
client = OAuth2::Client.new('abc', 'def', :site => 'https://api.example.com', :raise_errors => true)
|
62
|
+
expect(client.options[:raise_errors]).to be true
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'allows override of raise_errors option' do
|
66
|
+
client = OAuth2::Client.new('abc', 'def', :site => 'https://api.example.com', :raise_errors => true) do |builder|
|
67
|
+
builder.adapter :test do |stub|
|
68
|
+
stub.get('/notfound') { |env| [404, {}, nil] }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
expect(client.options[:raise_errors]).to be true
|
72
|
+
expect { client.request(:get, '/notfound') }.to raise_error(OAuth2::Error)
|
73
|
+
response = client.request(:get, '/notfound', :raise_errors => false)
|
74
|
+
expect(response.status).to eq(404)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'allows get/post for access_token_method option' do
|
78
|
+
client = OAuth2::Client.new('abc', 'def', :site => 'https://api.example.com', :access_token_method => :get)
|
79
|
+
expect(client.options[:access_token_method]).to eq(:get)
|
80
|
+
client = OAuth2::Client.new('abc', 'def', :site => 'https://api.example.com', :access_token_method => :post)
|
81
|
+
expect(client.options[:access_token_method]).to eq(:post)
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'does not mutate the opts hash argument' do
|
85
|
+
opts = {:site => 'http://example.com/'}
|
86
|
+
opts2 = opts.dup
|
87
|
+
OAuth2::Client.new 'abc', 'def', opts
|
88
|
+
expect(opts).to eq(opts2)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
%w(authorize token).each do |url_type|
|
93
|
+
describe ":#{url_type}_url option" do
|
94
|
+
it "defaults to a path of /oauth/#{url_type}" do
|
95
|
+
expect(subject.send("#{url_type}_url")).to eq("https://api.example.com/oauth/#{url_type}")
|
96
|
+
end
|
97
|
+
|
98
|
+
it "is settable via the :#{url_type}_url option" do
|
99
|
+
subject.options[:"#{url_type}_url"] = '/oauth/custom'
|
100
|
+
expect(subject.send("#{url_type}_url")).to eq('https://api.example.com/oauth/custom')
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'allows a different host than the site' do
|
104
|
+
subject.options[:"#{url_type}_url"] = 'https://api.foo.com/oauth/custom'
|
105
|
+
expect(subject.send("#{url_type}_url")).to eq('https://api.foo.com/oauth/custom')
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe '#request' do
|
111
|
+
it 'works with a null response body' do
|
112
|
+
expect(subject.request(:get, 'empty_get').body).to eq('')
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'returns on a successful response' do
|
116
|
+
response = subject.request(:get, '/success')
|
117
|
+
expect(response.body).to eq('yay')
|
118
|
+
expect(response.status).to eq(200)
|
119
|
+
expect(response.headers).to eq('Content-Type' => 'text/awesome')
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'posts a body' do
|
123
|
+
response = subject.request(:post, '/reflect', :body => 'foo=bar')
|
124
|
+
expect(response.body).to eq('foo=bar')
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'follows redirects properly' do
|
128
|
+
response = subject.request(:get, '/redirect')
|
129
|
+
expect(response.body).to eq('yay')
|
130
|
+
expect(response.status).to eq(200)
|
131
|
+
expect(response.headers).to eq('Content-Type' => 'text/awesome')
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'redirects using GET on a 303' do
|
135
|
+
response = subject.request(:post, '/redirect', :body => 'foo=bar')
|
136
|
+
expect(response.body).to be_empty
|
137
|
+
expect(response.status).to eq(200)
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'obeys the :max_redirects option' do
|
141
|
+
max_redirects = subject.options[:max_redirects]
|
142
|
+
subject.options[:max_redirects] = 0
|
143
|
+
response = subject.request(:get, '/redirect')
|
144
|
+
expect(response.status).to eq(302)
|
145
|
+
subject.options[:max_redirects] = max_redirects
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'returns if raise_errors is false' do
|
149
|
+
subject.options[:raise_errors] = false
|
150
|
+
response = subject.request(:get, '/unauthorized')
|
151
|
+
|
152
|
+
expect(response.status).to eq(401)
|
153
|
+
expect(response.headers).to eq('Content-Type' => 'application/json')
|
154
|
+
expect(response.error).not_to be_nil
|
155
|
+
end
|
156
|
+
|
157
|
+
%w(/unauthorized /conflict /error).each do |error_path|
|
158
|
+
it "raises OAuth2::Error on error response to path #{error_path}" do
|
159
|
+
expect { subject.request(:get, error_path) }.to raise_error(OAuth2::Error)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'parses OAuth2 standard error response' do
|
164
|
+
begin
|
165
|
+
subject.request(:get, '/unauthorized')
|
166
|
+
rescue StandardError => e
|
167
|
+
expect(e.code).to eq(error_value)
|
168
|
+
expect(e.description).to eq(error_description_value)
|
169
|
+
expect(e.to_s).to match(/#{error_value}/)
|
170
|
+
expect(e.to_s).to match(/#{error_description_value}/)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'provides the response in the Exception' do
|
175
|
+
begin
|
176
|
+
subject.request(:get, '/error')
|
177
|
+
rescue StandardError => e
|
178
|
+
expect(e.response).not_to be_nil
|
179
|
+
expect(e.to_s).to match(/unknown error/)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'instantiates an AuthCode strategy with this client' do
|
185
|
+
expect(subject.auth_code).to be_kind_of(OAuth2::Strategy::AuthCode)
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'instantiates an Implicit strategy with this client' do
|
189
|
+
expect(subject.implicit).to be_kind_of(OAuth2::Strategy::Implicit)
|
190
|
+
end
|
191
|
+
|
192
|
+
context 'with SSL options' do
|
193
|
+
subject do
|
194
|
+
cli = OAuth2::Client.new('abc', 'def', :site => 'https://api.example.com', :ssl => {:ca_file => 'foo.pem'})
|
195
|
+
cli.connection.build do |b|
|
196
|
+
b.adapter :test
|
197
|
+
end
|
198
|
+
cli
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'passes the SSL options along to Faraday::Connection#ssl' do
|
202
|
+
expect(subject.connection.ssl.fetch(:ca_file)).to eq('foo.pem')
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe OAuth2::Response do
|
4
|
+
describe '#initialize' do
|
5
|
+
let(:status) { 200 }
|
6
|
+
let(:headers) { {'foo' => 'bar'} }
|
7
|
+
let(:body) { 'foo' }
|
8
|
+
|
9
|
+
it 'returns the status, headers and body' do
|
10
|
+
response = double('response', :headers => headers,
|
11
|
+
:status => status,
|
12
|
+
:body => body)
|
13
|
+
subject = Response.new(response)
|
14
|
+
expect(subject.headers).to eq(headers)
|
15
|
+
expect(subject.status).to eq(status)
|
16
|
+
expect(subject.body).to eq(body)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '.register_parser' do
|
21
|
+
let(:response) do
|
22
|
+
double('response', :headers => {'Content-Type' => 'application/foo-bar'},
|
23
|
+
:status => 200,
|
24
|
+
:body => 'baz')
|
25
|
+
end
|
26
|
+
before do
|
27
|
+
OAuth2::Response.register_parser(:foobar, 'application/foo-bar') do |body|
|
28
|
+
"foobar #{body}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'adds to the content types and parsers' do
|
33
|
+
expect(OAuth2::Response::PARSERS.keys).to include(:foobar)
|
34
|
+
expect(OAuth2::Response::CONTENT_TYPES.keys).to include('application/foo-bar')
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'is able to parse that content type automatically' do
|
38
|
+
expect(OAuth2::Response.new(response).parsed).to eq('foobar baz')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#parsed' do
|
43
|
+
it 'parses application/x-www-form-urlencoded body' do
|
44
|
+
headers = {'Content-Type' => 'application/x-www-form-urlencoded'}
|
45
|
+
body = 'foo=bar&answer=42'
|
46
|
+
response = double('response', :headers => headers, :body => body)
|
47
|
+
subject = Response.new(response)
|
48
|
+
expect(subject.parsed.keys.size).to eq(2)
|
49
|
+
expect(subject.parsed['foo']).to eq('bar')
|
50
|
+
expect(subject.parsed['answer']).to eq('42')
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'parses application/json body' do
|
54
|
+
headers = {'Content-Type' => 'application/json'}
|
55
|
+
body = MultiJson.encode(:foo => 'bar', :answer => 42)
|
56
|
+
response = double('response', :headers => headers, :body => body)
|
57
|
+
subject = Response.new(response)
|
58
|
+
expect(subject.parsed.keys.size).to eq(2)
|
59
|
+
expect(subject.parsed['foo']).to eq('bar')
|
60
|
+
expect(subject.parsed['answer']).to eq(42)
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'parses alternative application/json extension bodies' do
|
64
|
+
headers = {'Content-Type' => 'application/hal+json'}
|
65
|
+
body = MultiJson.encode(:foo => 'bar', :answer => 42)
|
66
|
+
response = double('response', :headers => headers, :body => body)
|
67
|
+
subject = Response.new(response)
|
68
|
+
expect(subject.parsed.keys.size).to eq(2)
|
69
|
+
expect(subject.parsed['foo']).to eq('bar')
|
70
|
+
expect(subject.parsed['answer']).to eq(42)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "doesn't try to parse other content-types" do
|
74
|
+
headers = {'Content-Type' => 'text/html'}
|
75
|
+
body = '<!DOCTYPE html><html><head></head><body></body></html>'
|
76
|
+
|
77
|
+
response = double('response', :headers => headers, :body => body)
|
78
|
+
|
79
|
+
expect(MultiJson).not_to receive(:decode)
|
80
|
+
expect(MultiJson).not_to receive(:load)
|
81
|
+
expect(Rack::Utils).not_to receive(:parse_query)
|
82
|
+
|
83
|
+
subject = Response.new(response)
|
84
|
+
expect(subject.parsed).to be_nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'xml parser registration' do
|
89
|
+
it 'tries to load multi_xml and use it' do
|
90
|
+
expect(OAuth2::Response::PARSERS[:xml]).not_to be_nil
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'is able to parse xml' do
|
94
|
+
headers = {'Content-Type' => 'text/xml'}
|
95
|
+
body = '<?xml version="1.0" standalone="yes" ?><foo><bar>baz</bar></foo>'
|
96
|
+
|
97
|
+
response = double('response', :headers => headers, :body => body)
|
98
|
+
expect(OAuth2::Response.new(response).parsed).to eq('foo' => {'bar' => 'baz'})
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe OAuth2::Strategy::Assertion do
|
4
|
+
let(:client) do
|
5
|
+
cli = OAuth2::Client.new('abc', 'def', :site => 'http://api.example.com')
|
6
|
+
cli.connection.build do |b|
|
7
|
+
b.adapter :test do |stub|
|
8
|
+
stub.post('/oauth/token') do |env|
|
9
|
+
case @mode
|
10
|
+
when 'formencoded'
|
11
|
+
[200, {'Content-Type' => 'application/x-www-form-urlencoded'}, 'expires_in=600&access_token=salmon&refresh_token=trout']
|
12
|
+
when 'json'
|
13
|
+
[200, {'Content-Type' => 'application/json'}, '{"expires_in":600,"access_token":"salmon","refresh_token":"trout"}']
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
cli
|
19
|
+
end
|
20
|
+
|
21
|
+
let(:params) { {:hmac_secret => 'foo'} }
|
22
|
+
|
23
|
+
subject { client.assertion }
|
24
|
+
|
25
|
+
describe '#authorize_url' do
|
26
|
+
it 'raises NotImplementedError' do
|
27
|
+
expect { subject.authorize_url }.to raise_error(NotImplementedError)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
%w(json formencoded).each do |mode|
|
32
|
+
describe "#get_token (#{mode})" do
|
33
|
+
before do
|
34
|
+
@mode = mode
|
35
|
+
@access = subject.get_token(params)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'returns AccessToken with same Client' do
|
39
|
+
expect(@access.client).to eq(client)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'returns AccessToken with #token' do
|
43
|
+
expect(@access.token).to eq('salmon')
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'returns AccessToken with #expires_in' do
|
47
|
+
expect(@access.expires_in).to eq(600)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'returns AccessToken with #expires_at' do
|
51
|
+
expect(@access.expires_at).not_to be_nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe OAuth2::Strategy::AuthCode do
|
4
|
+
let(:code) { 'sushi' }
|
5
|
+
let(:kvform_token) { 'expires_in=600&access_token=salmon&refresh_token=trout&extra_param=steve' }
|
6
|
+
let(:facebook_token) { kvform_token.gsub('_in', '') }
|
7
|
+
let(:json_token) { MultiJson.encode(:expires_in => 600, :access_token => 'salmon', :refresh_token => 'trout', :extra_param => 'steve') }
|
8
|
+
|
9
|
+
let(:client) do
|
10
|
+
OAuth2::Client.new('abc', 'def', :site => 'http://api.example.com') do |builder|
|
11
|
+
builder.adapter :test do |stub|
|
12
|
+
stub.get("/oauth/token?client_id=abc&client_secret=def&code=#{code}&grant_type=authorization_code") do |env|
|
13
|
+
case @mode
|
14
|
+
when 'formencoded'
|
15
|
+
[200, {'Content-Type' => 'application/x-www-form-urlencoded'}, kvform_token]
|
16
|
+
when 'json'
|
17
|
+
[200, {'Content-Type' => 'application/json'}, json_token]
|
18
|
+
when 'from_facebook'
|
19
|
+
[200, {'Content-Type' => 'application/x-www-form-urlencoded'}, facebook_token]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
stub.post('/oauth/token', 'client_id' => 'abc', 'client_secret' => 'def', 'code' => 'sushi', 'grant_type' => 'authorization_code') do |env|
|
23
|
+
case @mode
|
24
|
+
when 'formencoded'
|
25
|
+
[200, {'Content-Type' => 'application/x-www-form-urlencoded'}, kvform_token]
|
26
|
+
when 'json'
|
27
|
+
[200, {'Content-Type' => 'application/json'}, json_token]
|
28
|
+
when 'from_facebook'
|
29
|
+
[200, {'Content-Type' => 'application/x-www-form-urlencoded'}, facebook_token]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
subject { client.auth_code }
|
37
|
+
|
38
|
+
describe '#authorize_url' do
|
39
|
+
it 'includes the client_id' do
|
40
|
+
expect(subject.authorize_url).to include('client_id=abc')
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'includes the type' do
|
44
|
+
expect(subject.authorize_url).to include('response_type=code')
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'includes passed in options' do
|
48
|
+
cb = 'http://myserver.local/oauth/callback'
|
49
|
+
expect(subject.authorize_url(:redirect_uri => cb)).to include("redirect_uri=#{Rack::Utils.escape(cb)}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
%w(json formencoded from_facebook).each do |mode|
|
54
|
+
[:get, :post].each do |verb|
|
55
|
+
describe "#get_token (#{mode}, access_token_method=#{verb}" do
|
56
|
+
before do
|
57
|
+
@mode = mode
|
58
|
+
client.options[:token_method] = verb
|
59
|
+
@access = subject.get_token(code)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'returns AccessToken with same Client' do
|
63
|
+
expect(@access.client).to eq(client)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'returns AccessToken with #token' do
|
67
|
+
expect(@access.token).to eq('salmon')
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'returns AccessToken with #refresh_token' do
|
71
|
+
expect(@access.refresh_token).to eq('trout')
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'returns AccessToken with #expires_in' do
|
75
|
+
expect(@access.expires_in).to eq(600)
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'returns AccessToken with #expires_at' do
|
79
|
+
expect(@access.expires_at).to be_kind_of(Integer)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'returns AccessToken with params accessible via []' do
|
83
|
+
expect(@access['extra_param']).to eq('steve')
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|