googleauth 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,66 @@
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
+ require 'googleauth/service_account'
31
+ require 'googleauth/compute_engine'
32
+
33
+ module Google
34
+ # Module Auth provides classes that provide Google-specific authorization
35
+ # used to access Google APIs.
36
+ module Auth
37
+ NOT_FOUND_ERROR = <<END
38
+ Could not load the default credentials. Browse to
39
+ https://developers.google.com/accounts/docs/application-default-credentials
40
+ for more information
41
+ END
42
+
43
+ # Obtains the default credentials implementation to use in this
44
+ # environment.
45
+ #
46
+ # Use this to obtain the Application Default Credentials for accessing
47
+ # Google APIs. Application Default Credentials are described in detail
48
+ # at http://goo.gl/IUuyuX.
49
+ #
50
+ # If supplied, scope is used to create the credentials instance, when it
51
+ # can applied. E.g, on compute engine, the scope is ignored.
52
+ #
53
+ # @param scope [string|array] the scope(s) to access
54
+ # @param options [hash] allows override of the connection being used
55
+ def get_application_default(scope, options = {})
56
+ creds = ServiceAccountCredentials.from_env(scope)
57
+ return creds unless creds.nil?
58
+ creds = ServiceAccountCredentials.from_well_known_path(scope)
59
+ return creds unless creds.nil?
60
+ fail NOT_FOUND_ERROR unless GCECredentials.on_gce?(options)
61
+ GCECredentials.new
62
+ end
63
+
64
+ module_function :get_application_default
65
+ end
66
+ end
@@ -0,0 +1,86 @@
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
+ require 'faraday'
31
+ require 'googleauth/signet'
32
+ require 'memoist'
33
+
34
+ module Google
35
+ # Module Auth provides classes that provide Google-specific authorization
36
+ # used to access Google APIs.
37
+ module Auth
38
+ # Extends Signet::OAuth2::Client so that the auth token is obtained from
39
+ # the GCE metadata server.
40
+ class GCECredentials < Signet::OAuth2::Client
41
+ # The IP Address is used in the URIs to speed up failures on non-GCE
42
+ # systems.
43
+ COMPUTE_AUTH_TOKEN_URI = 'http://169.254.169.254/computeMetadata/v1/'\
44
+ 'instance/service-accounts/default/token'
45
+ COMPUTE_CHECK_URI = 'http://169.254.169.254'
46
+
47
+ class << self
48
+ extend Memoist
49
+
50
+ # Detect if this appear to be a GCE instance, by checking if metadata
51
+ # is available
52
+ def on_gce?(options = {})
53
+ c = options[:connection] || Faraday.default_connection
54
+ resp = c.get(COMPUTE_CHECK_URI) do |req|
55
+ # Comment from: oauth2client/client.py
56
+ #
57
+ # Note: the explicit `timeout` below is a workaround. The underlying
58
+ # issue is that resolving an unknown host on some networks will take
59
+ # 20-30 seconds; making this timeout short fixes the issue, but
60
+ # could lead to false negatives in the event that we are on GCE, but
61
+ # the metadata resolution was particularly slow. The latter case is
62
+ # "unlikely".
63
+ req.options.timeout = 0.1
64
+ end
65
+ return false unless resp.status == 200
66
+ return false unless resp.headers.key?('Metadata-Flavor')
67
+ return resp.headers['Metadata-Flavor'] == 'Google'
68
+ rescue Faraday::TimeoutError, Faraday::ConnectionFailed
69
+ return false
70
+ end
71
+
72
+ memoize :on_gce?
73
+ end
74
+
75
+ # Overrides the super class method to change how access tokens are
76
+ # fetched.
77
+ def fetch_access_token(options = {})
78
+ c = options[:connection] || Faraday.default_connection
79
+ c.headers = { 'Metadata-Flavor' => 'Google' }
80
+ resp = c.get(COMPUTE_AUTH_TOKEN_URI)
81
+ Signet::OAuth2.parse_credentials(resp.body,
82
+ resp.headers['content-type'])
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,118 @@
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
+ require 'googleauth/signet'
31
+ require 'memoist'
32
+ require 'multi_json'
33
+ require 'openssl'
34
+ require 'rbconfig'
35
+
36
+ # Reads the private key and client email fields from service account JSON key.
37
+ def read_json_key(json_key_io)
38
+ json_key = MultiJson.load(json_key_io.read)
39
+ fail 'missing client_email' unless json_key.key?('client_email')
40
+ fail 'missing private_key' unless json_key.key?('private_key')
41
+ [json_key['private_key'], json_key['client_email']]
42
+ end
43
+
44
+ module Google
45
+ # Module Auth provides classes that provide Google-specific authorization
46
+ # used to access Google APIs.
47
+ module Auth
48
+ # Authenticates requests using Google's Service Account credentials.
49
+ #
50
+ # This class allows authorizing requests for service accounts directly
51
+ # from credentials from a json key file downloaded from the developer
52
+ # console (via 'Generate new Json Key').
53
+ #
54
+ # cf [Application Default Credentials](http://goo.gl/mkAHpZ)
55
+ class ServiceAccountCredentials < Signet::OAuth2::Client
56
+ ENV_VAR = 'GOOGLE_APPLICATION_CREDENTIALS'
57
+ NOT_FOUND_ERROR =
58
+ "Unable to read the credential file specified by #{ENV_VAR}"
59
+ TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/v3/token'
60
+ WELL_KNOWN_PATH = 'gcloud/application_default_credentials.json'
61
+ WELL_KNOWN_ERROR = 'Unable to read the default credential file'
62
+
63
+ class << self
64
+ extend Memoist
65
+
66
+ # determines if the current OS is windows
67
+ def windows?
68
+ RbConfig::CONFIG['host_os'] =~ /Windows|mswin/
69
+ end
70
+ memoize :windows?
71
+
72
+ # Creates an instance from the path specified in an environment
73
+ # variable.
74
+ #
75
+ # @param scope [string|array] the scope(s) to access
76
+ def from_env(scope)
77
+ return nil unless ENV.key?(ENV_VAR)
78
+ path = ENV[ENV_VAR]
79
+ fail 'file #{path} does not exist' unless File.exist?(path)
80
+ File.open(path) do |f|
81
+ return new(scope, f)
82
+ end
83
+ rescue StandardError => e
84
+ raise "#{NOT_FOUND_ERROR}: #{e}"
85
+ end
86
+
87
+ # Creates an instance from a well known path.
88
+ #
89
+ # @param scope [string|array] the scope(s) to access
90
+ def from_well_known_path(scope)
91
+ home_var, base = windows? ? 'APPDATA' : 'HOME', WELL_KNOWN_PATH
92
+ root = ENV[home_var].nil? ? '' : ENV[home_var]
93
+ base = File.join('.config', base) unless windows?
94
+ path = File.join(root, base)
95
+ return nil unless File.exist?(path)
96
+ File.open(path) do |f|
97
+ return new(scope, f)
98
+ end
99
+ rescue StandardError => e
100
+ raise "#{WELL_KNOWN_ERROR}: #{e}"
101
+ end
102
+ end
103
+
104
+ # Initializes a ServiceAccountCredentials.
105
+ #
106
+ # @param scope [string|array] the scope(s) to access
107
+ # @param json_key_io [IO] an IO from which the JSON key can be read
108
+ def initialize(scope, json_key_io)
109
+ private_key, client_email = read_json_key(json_key_io)
110
+ super(token_credential_uri: TOKEN_CRED_URI,
111
+ audience: TOKEN_CRED_URI, # TODO: confirm this
112
+ scope: scope,
113
+ issuer: client_email,
114
+ signing_key: OpenSSL::PKey::RSA.new(private_key))
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,63 @@
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
+ require 'signet/oauth_2/client'
31
+
32
+ module Signet
33
+ # OAuth2 supports OAuth2 authentication.
34
+ module OAuth2
35
+ AUTH_METADATA_KEY = :Authorization
36
+ # Signet::OAuth2::Client creates an OAuth2 client
37
+ #
38
+ # This reopens Client to add #apply and #apply! methods which update a
39
+ # hash with the fetched authentication token.
40
+ class Client
41
+ # Updates a_hash updated with the authentication token
42
+ def apply!(a_hash, opts = {})
43
+ # fetch the access token there is currently not one, or if the client
44
+ # has expired
45
+ fetch_access_token!(opts) if access_token.nil? || expired?
46
+ a_hash[AUTH_METADATA_KEY] = "Bearer #{access_token}"
47
+ end
48
+
49
+ # Returns a clone of a_hash updated with the authentication token
50
+ def apply(a_hash, opts = {})
51
+ a_copy = a_hash.clone
52
+ apply!(a_copy, opts)
53
+ a_copy
54
+ end
55
+
56
+ # Returns a reference to the #apply method, suitable for passing as
57
+ # a closure
58
+ def updater_proc
59
+ lambda(&method(:apply))
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,36 @@
1
+ # Copyright 2014, 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
+ module Google
31
+ # Module Auth provides classes that provide Google-specific authorization
32
+ # used to access Google APIs.
33
+ module Auth
34
+ VERSION = '0.1.0'
35
+ end
36
+ end
@@ -0,0 +1,169 @@
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 'faraday'
35
+ require 'spec_helper'
36
+
37
+ def build_json_response(payload)
38
+ [200,
39
+ { 'Content-Type' => 'application/json; charset=utf-8' },
40
+ MultiJson.dump(payload)]
41
+ end
42
+
43
+ def build_access_token_json(token)
44
+ build_json_response('access_token' => token,
45
+ 'token_type' => 'Bearer',
46
+ 'expires_in' => 3600)
47
+ end
48
+
49
+ WANTED_AUTH_KEY = :Authorization
50
+
51
+ shared_examples 'apply/apply! are OK' do
52
+ # tests that use these examples need to define
53
+ #
54
+ # @client which should be an auth client
55
+ #
56
+ # @make_auth_stubs, which should stub out the expected http behaviour of the
57
+ # auth client
58
+ describe '#fetch_access_token' do
59
+ it 'should set access_token to the fetched value' do
60
+ token = '1/abcdef1234567890'
61
+ stubs = make_auth_stubs access_token: token
62
+ c = Faraday.new do |b|
63
+ b.adapter(:test, stubs)
64
+ end
65
+
66
+ @client.fetch_access_token!(connection: c)
67
+ expect(@client.access_token).to eq(token)
68
+ stubs.verify_stubbed_calls
69
+ end
70
+ end
71
+
72
+ describe '#apply!' do
73
+ it 'should update the target hash with fetched access token' do
74
+ token = '1/abcdef1234567890'
75
+ stubs = make_auth_stubs access_token: token
76
+ c = Faraday.new do |b|
77
+ b.adapter(:test, stubs)
78
+ end
79
+
80
+ md = { foo: 'bar' }
81
+ @client.apply!(md, connection: c)
82
+ want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
83
+ expect(md).to eq(want)
84
+ stubs.verify_stubbed_calls
85
+ end
86
+ end
87
+
88
+ describe 'updater_proc' do
89
+ it 'should provide a proc that updates a hash with the access token' do
90
+ token = '1/abcdef1234567890'
91
+ stubs = make_auth_stubs access_token: token
92
+ c = Faraday.new do |b|
93
+ b.adapter(:test, stubs)
94
+ end
95
+
96
+ md = { foo: 'bar' }
97
+ the_proc = @client.updater_proc
98
+ got = the_proc.call(md, connection: c)
99
+ want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
100
+ expect(got).to eq(want)
101
+ stubs.verify_stubbed_calls
102
+ end
103
+ end
104
+
105
+ describe '#apply' do
106
+ it 'should not update the original hash with the access token' do
107
+ token = '1/abcdef1234567890'
108
+ stubs = make_auth_stubs access_token: token
109
+ c = Faraday.new do |b|
110
+ b.adapter(:test, stubs)
111
+ end
112
+
113
+ md = { foo: 'bar' }
114
+ @client.apply(md, connection: c)
115
+ want = { foo: 'bar' }
116
+ expect(md).to eq(want)
117
+ stubs.verify_stubbed_calls
118
+ end
119
+
120
+ it 'should add the token to the returned hash' do
121
+ token = '1/abcdef1234567890'
122
+ stubs = make_auth_stubs access_token: token
123
+ c = Faraday.new do |b|
124
+ b.adapter(:test, stubs)
125
+ end
126
+
127
+ md = { foo: 'bar' }
128
+ got = @client.apply(md, connection: c)
129
+ want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
130
+ expect(got).to eq(want)
131
+ stubs.verify_stubbed_calls
132
+ end
133
+
134
+ it 'should not fetch a new token if the current is not expired' do
135
+ token = '1/abcdef1234567890'
136
+ stubs = make_auth_stubs access_token: token
137
+ c = Faraday.new do |b|
138
+ b.adapter(:test, stubs)
139
+ end
140
+
141
+ n = 5 # arbitrary
142
+ n.times do |_t|
143
+ md = { foo: 'bar' }
144
+ got = @client.apply(md, connection: c)
145
+ want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
146
+ expect(got).to eq(want)
147
+ end
148
+ stubs.verify_stubbed_calls
149
+ end
150
+
151
+ it 'should fetch a new token if the current one is expired' do
152
+ token_1 = '1/abcdef1234567890'
153
+ token_2 = '2/abcdef1234567890'
154
+
155
+ [token_1, token_2].each do |t|
156
+ stubs = make_auth_stubs access_token: t
157
+ c = Faraday.new do |b|
158
+ b.adapter(:test, stubs)
159
+ end
160
+ md = { foo: 'bar' }
161
+ got = @client.apply(md, connection: c)
162
+ want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{t}" }
163
+ expect(got).to eq(want)
164
+ stubs.verify_stubbed_calls
165
+ @client.expires_at -= 3601 # default is to expire in 1hr
166
+ end
167
+ end
168
+ end
169
+ end