googleauth 0.4.2 → 0.5.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.
@@ -121,7 +121,7 @@ describe Google::Auth::ServiceAccountCredentials do
121
121
 
122
122
  before(:example) do
123
123
  @key = OpenSSL::PKey::RSA.new(2048)
124
- @client = ServiceAccountCredentials.new(
124
+ @client = ServiceAccountCredentials.make_creds(
125
125
  json_key_io: StringIO.new(cred_json_text),
126
126
  scope: 'https://www.googleapis.com/auth/userinfo.profile'
127
127
  )
@@ -129,16 +129,21 @@ describe Google::Auth::ServiceAccountCredentials do
129
129
 
130
130
  def make_auth_stubs(opts = {})
131
131
  access_token = opts[:access_token] || ''
132
- Faraday::Adapter::Test::Stubs.new do |stub|
133
- stub.post('/oauth2/v3/token') do |env|
134
- params = Addressable::URI.form_unencode(env[:body])
135
- _claim, _header = JWT.decode(params.assoc('assertion').last,
136
- @key.public_key)
137
- want = ['grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer']
138
- expect(params.assoc('grant_type')).to eq(want)
139
- build_access_token_json(access_token)
140
- end
132
+ body = MultiJson.dump('access_token' => access_token,
133
+ 'token_type' => 'Bearer',
134
+ 'expires_in' => 3600)
135
+ blk = proc do |request|
136
+ params = Addressable::URI.form_unencode(request.body)
137
+ _claim, _header = JWT.decode(params.assoc('assertion').last,
138
+ @key.public_key)
141
139
  end
140
+ stub_request(:post, 'https://www.googleapis.com/oauth2/v3/token')
141
+ .with(body: hash_including(
142
+ 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer'),
143
+ &blk)
144
+ .to_return(body: body,
145
+ status: 200,
146
+ headers: { 'Content-Type' => 'application/json' })
142
147
  end
143
148
 
144
149
  def cred_json_text
@@ -276,7 +281,7 @@ describe Google::Auth::ServiceAccountJwtHeaderCredentials do
276
281
 
277
282
  before(:example) do
278
283
  @key = OpenSSL::PKey::RSA.new(2048)
279
- @client = clz.new(json_key_io: StringIO.new(cred_json_text))
284
+ @client = clz.make_creds(json_key_io: StringIO.new(cred_json_text))
280
285
  end
281
286
 
282
287
  def cred_json_text
@@ -50,16 +50,21 @@ describe Signet::OAuth2::Client do
50
50
 
51
51
  def make_auth_stubs(opts)
52
52
  access_token = opts[:access_token] || ''
53
- Faraday::Adapter::Test::Stubs.new do |stub|
54
- stub.post('/o/oauth2/token') do |env|
55
- params = Addressable::URI.form_unencode(env[:body])
56
- _claim, _header = JWT.decode(params.assoc('assertion').last,
57
- @key.public_key)
58
- want = ['grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer']
59
- expect(params.assoc('grant_type')).to eq(want)
60
- build_access_token_json(access_token)
61
- end
53
+ body = MultiJson.dump('access_token' => access_token,
54
+ 'token_type' => 'Bearer',
55
+ 'expires_in' => 3600)
56
+ blk = proc do |request|
57
+ params = Addressable::URI.form_unencode(request.body)
58
+ _claim, _header = JWT.decode(params.assoc('assertion').last,
59
+ @key.public_key)
62
60
  end
61
+ stub_request(:post, 'https://accounts.google.com/o/oauth2/token')
62
+ .with(body: hash_including(
63
+ 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer'),
64
+ &blk)
65
+ .to_return(body: body,
66
+ status: 200,
67
+ headers: { 'Content-Type' => 'application/json' })
63
68
  end
64
69
 
65
70
  it_behaves_like 'apply/apply! are OK'
@@ -0,0 +1,58 @@
1
+ # Copyright 2015, Google Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are
6
+ # met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright
9
+ # notice, this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above
11
+ # copyright notice, this list of conditions and the following disclaimer
12
+ # in the documentation and/or other materials provided with the
13
+ # distribution.
14
+ # * Neither the name of Google Inc. nor the names of its
15
+ # contributors may be used to endorse or promote products derived from
16
+ # this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+ spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
31
+ $LOAD_PATH.unshift(spec_dir)
32
+ $LOAD_PATH.uniq!
33
+
34
+ require 'googleauth'
35
+ require 'googleauth/stores/file_token_store'
36
+ require 'spec_helper'
37
+ require 'fakefs/safe'
38
+ require 'fakefs/spec_helpers'
39
+ require 'googleauth/stores/store_examples'
40
+
41
+ module FakeFS
42
+ class File
43
+ # FakeFS doesn't implement. And since we don't need to actually lock,
44
+ # just stub out...
45
+ def flock(*)
46
+ end
47
+ end
48
+ end
49
+
50
+ describe Google::Auth::Stores::FileTokenStore do
51
+ include FakeFS::SpecHelpers
52
+
53
+ let(:store) do
54
+ Google::Auth::Stores::FileTokenStore.new(file: '/tokens.yaml')
55
+ end
56
+
57
+ it_behaves_like 'token store'
58
+ end
@@ -0,0 +1,50 @@
1
+ # Copyright 2015, Google Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are
6
+ # met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright
9
+ # notice, this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above
11
+ # copyright notice, this list of conditions and the following disclaimer
12
+ # in the documentation and/or other materials provided with the
13
+ # distribution.
14
+ # * Neither the name of Google Inc. nor the names of its
15
+ # contributors may be used to endorse or promote products derived from
16
+ # this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+ spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
31
+ $LOAD_PATH.unshift(spec_dir)
32
+ $LOAD_PATH.uniq!
33
+
34
+ require 'googleauth'
35
+ require 'googleauth/stores/redis_token_store'
36
+ require 'spec_helper'
37
+ require 'fakeredis/rspec'
38
+ require 'googleauth/stores/store_examples'
39
+
40
+ describe Google::Auth::Stores::RedisTokenStore do
41
+ let(:redis) do
42
+ Redis.new
43
+ end
44
+
45
+ let(:store) do
46
+ Google::Auth::Stores::RedisTokenStore.new(redis: redis)
47
+ end
48
+
49
+ it_behaves_like 'token store'
50
+ end
@@ -0,0 +1,58 @@
1
+ # Copyright 2015, Google Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are
6
+ # met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright
9
+ # notice, this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above
11
+ # copyright notice, this list of conditions and the following disclaimer
12
+ # in the documentation and/or other materials provided with the
13
+ # distribution.
14
+ # * Neither the name of Google Inc. nor the names of its
15
+ # contributors may be used to endorse or promote products derived from
16
+ # this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+ spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
31
+ $LOAD_PATH.unshift(spec_dir)
32
+ $LOAD_PATH.uniq!
33
+
34
+ require 'spec_helper'
35
+
36
+ shared_examples 'token store' do
37
+ before(:each) do
38
+ store.store('default', 'test')
39
+ end
40
+
41
+ it 'should return a stored value' do
42
+ expect(store.load('default')).to eq 'test'
43
+ end
44
+
45
+ it 'should return nil for missing tokens' do
46
+ expect(store.load('notavalidkey')).to be_nil
47
+ end
48
+
49
+ it 'should return nil for deleted tokens' do
50
+ store.delete('default')
51
+ expect(store.load('default')).to be_nil
52
+ end
53
+
54
+ it 'should save overwrite values on store' do
55
+ store.store('default', 'test2')
56
+ expect(store.load('default')).to eq 'test2'
57
+ end
58
+ end
@@ -0,0 +1,314 @@
1
+ # Copyright 2015, Google Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are
6
+ # met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright
9
+ # notice, this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above
11
+ # copyright notice, this list of conditions and the following disclaimer
12
+ # in the documentation and/or other materials provided with the
13
+ # distribution.
14
+ # * Neither the name of Google Inc. nor the names of its
15
+ # contributors may be used to endorse or promote products derived from
16
+ # this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+ spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
31
+ $LOAD_PATH.unshift(spec_dir)
32
+ $LOAD_PATH.uniq!
33
+
34
+ require 'googleauth'
35
+ require 'googleauth/user_authorizer'
36
+ require 'uri'
37
+ require 'multi_json'
38
+ require 'spec_helper'
39
+
40
+ describe Google::Auth::UserAuthorizer do
41
+ include TestHelpers
42
+
43
+ let(:client_id) { Google::Auth::ClientId.new('testclient', 'notasecret') }
44
+ let(:scope) { %w(email profile) }
45
+ let(:token_store) { DummyTokenStore.new }
46
+ let(:callback_uri) { 'https://www.example.com/oauth/callback' }
47
+ let(:authorizer) do
48
+ Google::Auth::UserAuthorizer.new(client_id,
49
+ scope,
50
+ token_store,
51
+ callback_uri)
52
+ end
53
+
54
+ shared_examples 'valid authorization url' do
55
+ it 'should have a valid base URI' do
56
+ expect(uri).to match %r{https://accounts.google.com/o/oauth2/auth}
57
+ end
58
+
59
+ it 'should request offline access' do
60
+ expect(URI(uri).query).to match(/access_type=offline/)
61
+ end
62
+
63
+ it 'should request response type code' do
64
+ expect(URI(uri).query).to match(/response_type=code/)
65
+ end
66
+
67
+ it 'should force approval' do
68
+ expect(URI(uri).query).to match(/approval_prompt=force/)
69
+ end
70
+
71
+ it 'should include granted scopes' do
72
+ expect(URI(uri).query).to match(/include_granted_scopes=true/)
73
+ end
74
+
75
+ it 'should include the correct client id' do
76
+ expect(URI(uri).query).to match(/client_id=testclient/)
77
+ end
78
+
79
+ it 'should not include a client secret' do
80
+ expect(URI(uri).query).to_not match(/client_secret/)
81
+ end
82
+
83
+ it 'should include the callback uri' do
84
+ expect(URI(uri).query).to match(
85
+ %r{redirect_uri=https://www.example.com/oauth/callback})
86
+ end
87
+
88
+ it 'should include the scope' do
89
+ expect(URI(uri).query).to match(/scope=email%20profile/)
90
+ end
91
+ end
92
+
93
+ context 'when generating authorization URLs with user ID & state' do
94
+ let(:uri) do
95
+ authorizer.get_authorization_url(login_hint: 'user1', state: 'mystate')
96
+ end
97
+
98
+ it_behaves_like 'valid authorization url'
99
+
100
+ it 'includes a login hint' do
101
+ expect(URI(uri).query).to match(/login_hint=user1/)
102
+ end
103
+
104
+ it 'includes the app state' do
105
+ expect(URI(uri).query).to match(/state=mystate/)
106
+ end
107
+ end
108
+
109
+ context 'when generating authorization URLs with user ID and no state' do
110
+ let(:uri) { authorizer.get_authorization_url(login_hint: 'user1') }
111
+
112
+ it_behaves_like 'valid authorization url'
113
+
114
+ it 'includes a login hint' do
115
+ expect(URI(uri).query).to match(/login_hint=user1/)
116
+ end
117
+
118
+ it 'does not include the state parameter' do
119
+ expect(URI(uri).query).to_not match(/state/)
120
+ end
121
+ end
122
+
123
+ context 'when generating authorization URLs with no user ID and no state' do
124
+ let(:uri) { authorizer.get_authorization_url }
125
+
126
+ it_behaves_like 'valid authorization url'
127
+
128
+ it 'does not include the login hint parameter' do
129
+ expect(URI(uri).query).to_not match(/login_hint/)
130
+ end
131
+
132
+ it 'does not include the state parameter' do
133
+ expect(URI(uri).query).to_not match(/state/)
134
+ end
135
+ end
136
+
137
+ context 'when retrieving tokens' do
138
+ let(:token_json) do
139
+ MultiJson.dump(
140
+ access_token: 'accesstoken',
141
+ refresh_token: 'refreshtoken',
142
+ expiration_time_millis: 1_441_234_742_000)
143
+ end
144
+
145
+ context 'with a valid user id' do
146
+ let(:credentials) do
147
+ token_store.store('user1', token_json)
148
+ authorizer.get_credentials('user1')
149
+ end
150
+
151
+ it 'should return an instance of UserRefreshCredentials' do
152
+ expect(credentials).to be_instance_of(
153
+ Google::Auth::UserRefreshCredentials)
154
+ end
155
+
156
+ it 'should return credentials with a valid refresh token' do
157
+ expect(credentials.refresh_token).to eq 'refreshtoken'
158
+ end
159
+
160
+ it 'should return credentials with a valid access token' do
161
+ expect(credentials.access_token).to eq 'accesstoken'
162
+ end
163
+
164
+ it 'should return credentials with a valid client ID' do
165
+ expect(credentials.client_id).to eq 'testclient'
166
+ end
167
+
168
+ it 'should return credentials with a valid client secret' do
169
+ expect(credentials.client_secret).to eq 'notasecret'
170
+ end
171
+
172
+ it 'should return credentials with a valid scope' do
173
+ expect(credentials.scope).to eq %w(email profile)
174
+ end
175
+
176
+ it 'should return credentials with a valid expiration time' do
177
+ expect(credentials.expires_at).to eq Time.at(1_441_234_742)
178
+ end
179
+ end
180
+
181
+ context 'with an invalid user id' do
182
+ it 'should return nil' do
183
+ expect(authorizer.get_credentials('notauser')).to be_nil
184
+ end
185
+ end
186
+ end
187
+
188
+ context 'when saving tokens' do
189
+ let(:expiry) { Time.now.to_i }
190
+ let(:credentials) do
191
+ Google::Auth::UserRefreshCredentials.new(
192
+ client_id: client_id.id,
193
+ client_secret: client_id.secret,
194
+ scope: scope,
195
+ refresh_token: 'refreshtoken',
196
+ access_token: 'accesstoken',
197
+ expires_at: expiry
198
+ )
199
+ end
200
+
201
+ let(:token_json) do
202
+ authorizer.store_credentials('user1', credentials)
203
+ token_store.load('user1')
204
+ end
205
+
206
+ it 'should persist in the token store' do
207
+ expect(token_json).to_not be_nil
208
+ end
209
+
210
+ it 'should persist the refresh token' do
211
+ expect(MultiJson.load(token_json)['refresh_token']).to eq 'refreshtoken'
212
+ end
213
+
214
+ it 'should persist the access token' do
215
+ expect(MultiJson.load(token_json)['access_token']).to eq 'accesstoken'
216
+ end
217
+
218
+ it 'should persist the client id' do
219
+ expect(MultiJson.load(token_json)['client_id']).to eq 'testclient'
220
+ end
221
+
222
+ it 'should persist the scope' do
223
+ expect(MultiJson.load(token_json)['scope']).to include('email', 'profile')
224
+ end
225
+
226
+ it 'should persist the expiry as milliseconds' do
227
+ expected_expiry = expiry * 1000
228
+ expect(MultiJson.load(token_json)['expiration_time_millis']).to eql(
229
+ expected_expiry)
230
+ end
231
+ end
232
+
233
+ context 'with valid authorization code' do
234
+ let(:token_json) do
235
+ MultiJson.dump('access_token' => '1/abc123',
236
+ 'token_type' => 'Bearer',
237
+ 'expires_in' => 3600)
238
+ end
239
+
240
+ before(:example) do
241
+ stub_request(:post, 'https://www.googleapis.com/oauth2/v3/token')
242
+ .to_return(body: token_json, status: 200, headers: {
243
+ 'Content-Type' => 'application/json' })
244
+ end
245
+
246
+ it 'should exchange a code for credentials' do
247
+ credentials = authorizer.get_credentials_from_code(
248
+ user_id: 'user1', code: 'code')
249
+ expect(credentials.access_token).to eq '1/abc123'
250
+ end
251
+
252
+ it 'should not store credentials when get only requested' do
253
+ authorizer.get_credentials_from_code(user_id: 'user1', code: 'code')
254
+ expect(token_store.load('user1')).to be_nil
255
+ end
256
+
257
+ it 'should store credentials when requested' do
258
+ authorizer.get_and_store_credentials_from_code(
259
+ user_id: 'user1', code: 'code')
260
+ expect(token_store.load('user1')).to_not be_nil
261
+ end
262
+ end
263
+
264
+ context 'with invalid authorization code' do
265
+ before(:example) do
266
+ stub_request(:post, 'https://www.googleapis.com/oauth2/v3/token')
267
+ .to_return(status: 400)
268
+ end
269
+
270
+ it 'should raise an authorization error' do
271
+ expect do
272
+ authorizer.get_credentials_from_code(user_id: 'user1', code: 'badcode')
273
+ end.to raise_error Signet::AuthorizationError
274
+ end
275
+
276
+ it 'should not store credentials when exchange fails' do
277
+ expect do
278
+ authorizer.get_credentials_from_code(user_id: 'user1', code: 'badcode')
279
+ end.to raise_error Signet::AuthorizationError
280
+ expect(token_store.load('user1')).to be_nil
281
+ end
282
+ end
283
+
284
+ context 'when reovking authorization' do
285
+ let(:token_json) do
286
+ MultiJson.dump(
287
+ access_token: 'accesstoken',
288
+ refresh_token: 'refreshtoken',
289
+ expiration_time_millis: 1_441_234_742_000)
290
+ end
291
+
292
+ before(:example) do
293
+ token_store.store('user1', token_json)
294
+ stub_request(
295
+ :get, 'https://accounts.google.com/o/oauth2/revoke?token=refreshtoken')
296
+ .to_return(status: 200)
297
+ end
298
+
299
+ it 'should revoke the grant' do
300
+ authorizer.revoke_authorization('user1')
301
+ expect(a_request(
302
+ :get, 'https://accounts.google.com/o/oauth2/revoke?token=refreshtoken'))
303
+ .to have_been_made
304
+ end
305
+
306
+ it 'should remove the token from storage' do
307
+ authorizer.revoke_authorization('user1')
308
+ expect(token_store.load('user1')).to be_nil
309
+ end
310
+ end
311
+
312
+ # TODO: - Test that tokens are monitored
313
+ # TODO - Test scope enforcement (auth if upgrade required)
314
+ end