googleauth 0.1.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.
@@ -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