xero-ruby 2.8.1 → 2.10.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -31,7 +31,8 @@ module XeroRuby
31
31
  # @param project_create_or_update [ProjectCreateOrUpdate] Create a new project with ProjectCreateOrUpdate object
32
32
  # @param [Hash] opts the optional parameters
33
33
  # @return [Array<(Project, Integer, Hash)>] Project data, response status code and response headers
34
- def create_project_with_http_info(xero_tenant_id, project_create_or_update, opts = {})
34
+ def create_project_with_http_info(xero_tenant_id, project_create_or_update, options = {})
35
+ opts = options.dup
35
36
  if @api_client.config.debugging
36
37
  @api_client.config.logger.debug 'Calling API: ProjectApi.create_project ...'
37
38
  end
@@ -111,7 +112,8 @@ module XeroRuby
111
112
  # @param time_entry_create_or_update [TimeEntryCreateOrUpdate] The time entry object you are creating
112
113
  # @param [Hash] opts the optional parameters
113
114
  # @return [Array<(TimeEntry, Integer, Hash)>] TimeEntry data, response status code and response headers
114
- def create_time_entry_with_http_info(xero_tenant_id, project_id, time_entry_create_or_update, opts = {})
115
+ def create_time_entry_with_http_info(xero_tenant_id, project_id, time_entry_create_or_update, options = {})
116
+ opts = options.dup
115
117
  if @api_client.config.debugging
116
118
  @api_client.config.logger.debug 'Calling API: ProjectApi.create_time_entry ...'
117
119
  end
@@ -195,7 +197,8 @@ module XeroRuby
195
197
  # @param time_entry_id [String] You can specify an individual task by appending the id to the endpoint
196
198
  # @param [Hash] opts the optional parameters
197
199
  # @return [Array<(nil, Integer, Hash)>] nil, response status code and response headers
198
- def delete_time_entry_with_http_info(xero_tenant_id, project_id, time_entry_id, opts = {})
200
+ def delete_time_entry_with_http_info(xero_tenant_id, project_id, time_entry_id, options = {})
201
+ opts = options.dup
199
202
  if @api_client.config.debugging
200
203
  @api_client.config.logger.debug 'Calling API: ProjectApi.delete_time_entry ...'
201
204
  end
@@ -273,7 +276,8 @@ module XeroRuby
273
276
  # @param project_id [String] You can specify an individual project by appending the projectId to the endpoint
274
277
  # @param [Hash] opts the optional parameters
275
278
  # @return [Array<(Project, Integer, Hash)>] Project data, response status code and response headers
276
- def get_project_with_http_info(xero_tenant_id, project_id, opts = {})
279
+ def get_project_with_http_info(xero_tenant_id, project_id, options = {})
280
+ opts = options.dup
277
281
  if @api_client.config.debugging
278
282
  @api_client.config.logger.debug 'Calling API: ProjectApi.get_project ...'
279
283
  end
@@ -351,7 +355,8 @@ module XeroRuby
351
355
  # @option opts [Integer] :page set to 1 by default. The requested number of the page in paged response - Must be a number greater than 0.
352
356
  # @option opts [Integer] :page_size Optional, it is set to 50 by default. The number of items to return per page in a paged response - Must be a number between 1 and 500.
353
357
  # @return [Array<(ProjectUsers, Integer, Hash)>] ProjectUsers data, response status code and response headers
354
- def get_project_users_with_http_info(xero_tenant_id, opts = {})
358
+ def get_project_users_with_http_info(xero_tenant_id, options = {})
359
+ opts = options.dup
355
360
  if @api_client.config.debugging
356
361
  @api_client.config.logger.debug 'Calling API: ProjectApi.get_project_users ...'
357
362
  end
@@ -441,7 +446,8 @@ module XeroRuby
441
446
  # @option opts [Integer] :page set to 1 by default. The requested number of the page in paged response - Must be a number greater than 0.
442
447
  # @option opts [Integer] :page_size Optional, it is set to 50 by default. The number of items to return per page in a paged response - Must be a number between 1 and 500.
443
448
  # @return [Array<(Projects, Integer, Hash)>] Projects data, response status code and response headers
444
- def get_projects_with_http_info(xero_tenant_id, opts = {})
449
+ def get_projects_with_http_info(xero_tenant_id, options = {})
450
+ opts = options.dup
445
451
  if @api_client.config.debugging
446
452
  @api_client.config.logger.debug 'Calling API: ProjectApi.get_projects ...'
447
453
  end
@@ -528,7 +534,8 @@ module XeroRuby
528
534
  # @param task_id [String] You can specify an individual task by appending the taskId to the endpoint, i.e. GET https://.../tasks/{taskID}
529
535
  # @param [Hash] opts the optional parameters
530
536
  # @return [Array<(Task, Integer, Hash)>] Task data, response status code and response headers
531
- def get_task_with_http_info(xero_tenant_id, project_id, task_id, opts = {})
537
+ def get_task_with_http_info(xero_tenant_id, project_id, task_id, options = {})
538
+ opts = options.dup
532
539
  if @api_client.config.debugging
533
540
  @api_client.config.logger.debug 'Calling API: ProjectApi.get_task ...'
534
541
  end
@@ -616,7 +623,8 @@ module XeroRuby
616
623
  # @option opts [String] :task_ids taskIdsSearch for all tasks that match a comma separated list of taskIds, i.e. GET https://.../tasks?taskIds&#x3D;{taskID},{taskID}
617
624
  # @option opts [ChargeType] :charge_type
618
625
  # @return [Array<(Tasks, Integer, Hash)>] Tasks data, response status code and response headers
619
- def get_tasks_with_http_info(xero_tenant_id, project_id, opts = {})
626
+ def get_tasks_with_http_info(xero_tenant_id, project_id, options = {})
627
+ opts = options.dup
620
628
  if @api_client.config.debugging
621
629
  @api_client.config.logger.debug 'Calling API: ProjectApi.get_tasks ...'
622
630
  end
@@ -716,7 +724,8 @@ module XeroRuby
716
724
  # @option opts [DateTime] :date_after_utc ISO 8601 UTC date. Finds all time entries on or after this date filtered on the &#x60;dateUtc&#x60; field.
717
725
  # @option opts [DateTime] :date_before_utc ISO 8601 UTC date. Finds all time entries on or before this date filtered on the &#x60;dateUtc&#x60; field.
718
726
  # @return [Array<(TimeEntries, Integer, Hash)>] TimeEntries data, response status code and response headers
719
- def get_time_entries_with_http_info(xero_tenant_id, project_id, opts = {})
727
+ def get_time_entries_with_http_info(xero_tenant_id, project_id, options = {})
728
+ opts = options.dup
720
729
  if @api_client.config.debugging
721
730
  @api_client.config.logger.debug 'Calling API: ProjectApi.get_time_entries ...'
722
731
  end
@@ -804,7 +813,8 @@ module XeroRuby
804
813
  # @param time_entry_id [String] You can specify an individual time entry by appending the id to the endpoint
805
814
  # @param [Hash] opts the optional parameters
806
815
  # @return [Array<(TimeEntry, Integer, Hash)>] TimeEntry data, response status code and response headers
807
- def get_time_entry_with_http_info(xero_tenant_id, project_id, time_entry_id, opts = {})
816
+ def get_time_entry_with_http_info(xero_tenant_id, project_id, time_entry_id, options = {})
817
+ opts = options.dup
808
818
  if @api_client.config.debugging
809
819
  @api_client.config.logger.debug 'Calling API: ProjectApi.get_time_entry ...'
810
820
  end
@@ -886,7 +896,8 @@ module XeroRuby
886
896
  # @param project_patch [ProjectPatch] Update the status of an existing Project
887
897
  # @param [Hash] opts the optional parameters
888
898
  # @return [Array<(nil, Integer, Hash)>] nil, response status code and response headers
889
- def patch_project_with_http_info(xero_tenant_id, project_id, project_patch, opts = {})
899
+ def patch_project_with_http_info(xero_tenant_id, project_id, project_patch, options = {})
900
+ opts = options.dup
890
901
  if @api_client.config.debugging
891
902
  @api_client.config.logger.debug 'Calling API: ProjectApi.patch_project ...'
892
903
  end
@@ -970,7 +981,8 @@ module XeroRuby
970
981
  # @param project_create_or_update [ProjectCreateOrUpdate] Request of type ProjectCreateOrUpdate
971
982
  # @param [Hash] opts the optional parameters
972
983
  # @return [Array<(nil, Integer, Hash)>] nil, response status code and response headers
973
- def update_project_with_http_info(xero_tenant_id, project_id, project_create_or_update, opts = {})
984
+ def update_project_with_http_info(xero_tenant_id, project_id, project_create_or_update, options = {})
985
+ opts = options.dup
974
986
  if @api_client.config.debugging
975
987
  @api_client.config.logger.debug 'Calling API: ProjectApi.update_project ...'
976
988
  end
@@ -1056,7 +1068,8 @@ module XeroRuby
1056
1068
  # @param time_entry_create_or_update [TimeEntryCreateOrUpdate] The time entry object you are updating
1057
1069
  # @param [Hash] opts the optional parameters
1058
1070
  # @return [Array<(nil, Integer, Hash)>] nil, response status code and response headers
1059
- def update_time_entry_with_http_info(xero_tenant_id, project_id, time_entry_id, time_entry_create_or_update, opts = {})
1071
+ def update_time_entry_with_http_info(xero_tenant_id, project_id, time_entry_id, time_entry_create_or_update, options = {})
1072
+ opts = options.dup
1060
1073
  if @api_client.config.debugging
1061
1074
  @api_client.config.logger.debug 'Calling API: ProjectApi.update_time_entry ...'
1062
1075
  end
@@ -16,6 +16,8 @@ require 'tempfile'
16
16
  require 'find'
17
17
  require 'faraday'
18
18
  require 'base64'
19
+ require 'cgi'
20
+ require 'json/jwt'
19
21
 
20
22
  module XeroRuby
21
23
  class ApiClient
@@ -31,13 +33,15 @@ module XeroRuby
31
33
 
32
34
  # Initializes the ApiClient
33
35
  # @option config [Configuration] Configuration for initializing the object, default to Configuration.default
34
- def initialize(config: Configuration.default, credentials: {})
36
+ def initialize(config: {}, credentials: {})
35
37
  @client_id = credentials[:client_id]
36
38
  @client_secret = credentials[:client_secret]
37
39
  @redirect_uri = credentials[:redirect_uri]
38
40
  @scopes = credentials[:scopes]
39
41
  @state = credentials[:state]
40
- @config = config
42
+ default_config = Configuration.default.clone
43
+ @config = append_to_default_config(default_config, config)
44
+
41
45
  @user_agent = "xero-ruby-#{VERSION}"
42
46
  @default_headers = {
43
47
  'Content-Type' => 'application/json',
@@ -45,8 +49,14 @@ module XeroRuby
45
49
  }
46
50
  end
47
51
 
52
+ def append_to_default_config(default_config, user_config)
53
+ config = default_config
54
+ user_config.each{|k,v| config.send("#{k}=", v)}
55
+ config
56
+ end
57
+
48
58
  def authorization_url
49
- url = "#{@config.login_url}?response_type=code&client_id=#{@client_id}&redirect_uri=#{@redirect_uri}&scope=#{@scopes}"
59
+ url = "#{@config.login_url}?response_type=code&client_id=#{@client_id}&redirect_uri=#{@redirect_uri}&scope=#{CGI.escape(@scopes)}"
50
60
  url << "&state=#{@state}" if @state
51
61
  return url
52
62
  end
@@ -88,22 +98,41 @@ module XeroRuby
88
98
 
89
99
  # Token Helpers
90
100
  def token_set
91
- XeroRuby.configure.token_set
101
+ @config.token_set
92
102
  end
93
103
 
94
104
  def access_token
95
- XeroRuby.configure.access_token
105
+ @config.access_token
106
+ end
107
+
108
+ def id_token
109
+ @config.id_token
110
+ end
111
+
112
+ def decoded_access_token
113
+ decode_jwt(@config.access_token)
114
+ end
115
+
116
+ def decoded_id_token
117
+ decode_jwt(@config.id_token)
96
118
  end
97
119
 
98
120
  def set_token_set(token_set)
99
- # helper to set the token_set on a client once the user
100
- # has a valid token set ( access_token & refresh_token )
101
- XeroRuby.configure.token_set = token_set
102
- set_access_token(token_set['access_token'])
121
+ token_set = token_set.with_indifferent_access
122
+ @config.token_set = token_set
123
+
124
+ set_access_token(token_set[:access_token]) if token_set[:access_token]
125
+ set_id_token(token_set[:id_token]) if token_set[:id_token]
126
+
127
+ return true
103
128
  end
104
129
 
105
130
  def set_access_token(access_token)
106
- XeroRuby.configure.access_token = access_token
131
+ @config.access_token = access_token
132
+ end
133
+
134
+ def set_id_token(id_token)
135
+ @config.id_token = id_token
107
136
  end
108
137
 
109
138
  def get_token_set_from_callback(params)
@@ -112,20 +141,55 @@ module XeroRuby
112
141
  code: params['code'],
113
142
  redirect_uri: @redirect_uri
114
143
  }
115
- return token_request(data, '/token')
144
+ token_set = token_request(data, '/token')
145
+
146
+ validate_tokens(token_set)
147
+ validate_state(params)
148
+ return token_set
149
+ end
150
+
151
+ def validate_tokens(token_set)
152
+ token_set = token_set.with_indifferent_access
153
+ id_token = token_set[:id_token]
154
+ access_token = token_set[:access_token]
155
+ if id_token || access_token
156
+ decode_jwt(access_token) if access_token
157
+ decode_jwt(id_token) if id_token
158
+ end
159
+ return true
160
+ end
161
+
162
+ def validate_state(params)
163
+ if params[:state] != @state
164
+ raise StandardError.new "WARNING: @config.state: #{@state} and OAuth callback state: #{params['state']} do not match!"
165
+ end
166
+ return true
167
+ end
168
+
169
+ def decode_jwt(tkn)
170
+ jwks_data = JSON.parse(Faraday.get('https://identity.xero.com/.well-known/openid-configuration/jwks').body)
171
+ jwk_set = JSON::JWK::Set.new(jwks_data)
172
+ JSON::JWT.decode(tkn, jwk_set)
173
+ end
174
+
175
+ def token_expired?
176
+ token_expiry = Time.at(decoded_access_token['exp'])
177
+ token_expiry < Time.now
116
178
  end
117
179
 
118
180
  def refresh_token_set(token_set)
181
+ token_set = token_set.with_indifferent_access
119
182
  data = {
120
183
  grant_type: 'refresh_token',
121
- refresh_token: token_set['refresh_token']
184
+ refresh_token: token_set[:refresh_token]
122
185
  }
123
186
  return token_request(data, '/token')
124
187
  end
125
188
 
126
189
  def revoke_token(token_set)
190
+ token_set = token_set.with_indifferent_access
127
191
  data = {
128
- token: token_set['refresh_token']
192
+ token: token_set[:refresh_token]
129
193
  }
130
194
  return token_request(data, '/revocation')
131
195
  end
@@ -174,7 +238,26 @@ module XeroRuby
174
238
  :client_key => @config.ssl_client_key
175
239
  }
176
240
 
177
- connection = Faraday.new(:url => config.base_url, :ssl => ssl_options) do |conn|
241
+ case api_client
242
+ when "AccountingApi"
243
+ method_base_url = @config.accounting_url
244
+ when "AssetApi"
245
+ method_base_url = @config.asset_url
246
+ when "FilesApi"
247
+ method_base_url = @config.files_url
248
+ when "PayrollAuApi"
249
+ method_base_url = @config.payroll_au_url
250
+ when "PayrollNzApi"
251
+ method_base_url = @config.payroll_nz_url
252
+ when "PayrollUkApi"
253
+ method_base_url = @config.payroll_uk_url
254
+ when "ProjectApi"
255
+ method_base_url = @config.project_url
256
+ else
257
+ method_base_url = @config.accounting_url
258
+ end
259
+
260
+ connection = Faraday.new(:url => method_base_url, :ssl => ssl_options) do |conn|
178
261
  conn.basic_auth(config.username, config.password)
179
262
  if opts[:header_params]["Content-Type"] == "multipart/form-data"
180
263
  conn.request :multipart
@@ -257,6 +340,8 @@ module XeroRuby
257
340
  end
258
341
  end
259
342
  request.headers = header_params
343
+ timeout = @config.timeout
344
+ request.options.timeout = timeout if timeout > 0
260
345
  request.body = req_body
261
346
  request.url url
262
347
  request.params = query_params
@@ -60,8 +60,11 @@ module XeroRuby
60
60
  # @return [String]
61
61
  attr_accessor :password
62
62
 
63
- # Defines the access token (Bearer) used with OAuth2.
63
+ # Defines the access token (Bearer) used with OAuth2 authorization
64
64
  attr_accessor :access_token
65
+
66
+ # Defines OpenID Connect id_token containing Xero user authentication detail
67
+ attr_accessor :id_token
65
68
 
66
69
  # Defines the token set used with OAuth2. May include id/access/refresh token & other meta info.
67
70
  attr_accessor :token_set
@@ -146,6 +149,8 @@ module XeroRuby
146
149
  @payroll_au_url = 'https://api.xero.com/payroll.xro/1.0/'
147
150
  @payroll_nz_url = 'https://api.xero.com/payroll.xro/2.0/'
148
151
  @payroll_uk_url = 'https://api.xero.com/payroll.xro/2.0/'
152
+ @access_token = nil
153
+ @id_token = nil
149
154
  @api_key = {}
150
155
  @api_key_prefix = {}
151
156
  @timeout = 0
@@ -191,6 +196,14 @@ module XeroRuby
191
196
  def base_url=(api_url)
192
197
  @base_url = api_url
193
198
  end
199
+
200
+ def access_token=(access_token)
201
+ @access_token = access_token
202
+ end
203
+
204
+ def id_token=(id_token)
205
+ @id_token = id_token
206
+ end
194
207
 
195
208
  # Gets API key (with prefix if set).
196
209
  # @param [String] param_name the parameter name of API key auth
@@ -97,10 +97,6 @@ module XeroRuby::PayrollAu
97
97
  invalid_properties.push('invalid value for "deduction_type_id", deduction_type_id cannot be nil.')
98
98
  end
99
99
 
100
- if @calculation_type.nil?
101
- invalid_properties.push('invalid value for "calculation_type", calculation_type cannot be nil.')
102
- end
103
-
104
100
  invalid_properties
105
101
  end
106
102
 
@@ -108,7 +104,6 @@ module XeroRuby::PayrollAu
108
104
  # @return true if the model is valid
109
105
  def valid?
110
106
  return false if @deduction_type_id.nil?
111
- return false if @calculation_type.nil?
112
107
  true
113
108
  end
114
109
 
@@ -46,6 +46,7 @@ module XeroRuby::PayrollUk
46
46
  # The type of the payment of the corresponding salary and wages
47
47
  attr_accessor :payment_type
48
48
  SALARY = "Salary".freeze
49
+ HOURLY = "Hourly".freeze
49
50
 
50
51
  class EnumAttributeValidator
51
52
  attr_reader :datatype
@@ -193,7 +194,7 @@ module XeroRuby::PayrollUk
193
194
  status_validator = EnumAttributeValidator.new('String', ["Active", "Pending", "History"])
194
195
  return false unless status_validator.valid?(@status)
195
196
  return false if @payment_type.nil?
196
- payment_type_validator = EnumAttributeValidator.new('String', ["Salary"])
197
+ payment_type_validator = EnumAttributeValidator.new('String', ["Salary", "Hourly"])
197
198
  return false unless payment_type_validator.valid?(@payment_type)
198
199
  true
199
200
  end
@@ -211,7 +212,7 @@ module XeroRuby::PayrollUk
211
212
  # Custom attribute writer method checking allowed values (enum).
212
213
  # @param [Object] payment_type Object to be assigned
213
214
  def payment_type=(payment_type)
214
- validator = EnumAttributeValidator.new('String', ["Salary"])
215
+ validator = EnumAttributeValidator.new('String', ["Salary", "Hourly"])
215
216
  unless validator.valid?(payment_type)
216
217
  fail ArgumentError, "invalid value for \"payment_type\", must be one of #{validator.allowable_values}."
217
218
  end
@@ -44,6 +44,7 @@ module XeroRuby::Projects
44
44
  attr_accessor :status
45
45
  ACTIVE = "ACTIVE".freeze
46
46
  LOCKED = "LOCKED".freeze
47
+ INVOICED = "INVOICED".freeze
47
48
 
48
49
  class EnumAttributeValidator
49
50
  attr_reader :datatype
@@ -159,7 +160,7 @@ module XeroRuby::Projects
159
160
  # Check to see if the all the properties in the model are valid
160
161
  # @return true if the model is valid
161
162
  def valid?
162
- status_validator = EnumAttributeValidator.new('String', ["ACTIVE", "LOCKED"])
163
+ status_validator = EnumAttributeValidator.new('String', ["ACTIVE", "LOCKED", "INVOICED"])
163
164
  return false unless status_validator.valid?(@status)
164
165
  true
165
166
  end
@@ -167,7 +168,7 @@ module XeroRuby::Projects
167
168
  # Custom attribute writer method checking allowed values (enum).
168
169
  # @param [Object] status Object to be assigned
169
170
  def status=(status)
170
- validator = EnumAttributeValidator.new('String', ["ACTIVE", "LOCKED"])
171
+ validator = EnumAttributeValidator.new('String', ["ACTIVE", "LOCKED", "INVOICED"])
171
172
  unless validator.valid?(status)
172
173
  fail ArgumentError, "invalid value for \"status\", must be one of #{validator.allowable_values}."
173
174
  end
@@ -7,9 +7,9 @@ Contact: api@xero.com
7
7
  Generated by: https://openapi-generator.tech
8
8
  OpenAPI Generator version: 4.3.1
9
9
 
10
- The version of the XeroOpenAPI document: 2.10.3
10
+ The version of the XeroOpenAPI document: 2.11.0
11
11
  =end
12
12
 
13
13
  module XeroRuby
14
- VERSION = '2.8.1'
14
+ VERSION = '2.10.1'
15
15
  end
@@ -1,4 +1,4 @@
1
- require './spec_helper'
1
+ require 'spec_helper'
2
2
 
3
3
  describe XeroRuby::ApiClient do
4
4
  context 'initialization' do
@@ -47,7 +47,7 @@ describe XeroRuby::ApiClient do
47
47
  state: 'i-am-customer-state'
48
48
  }
49
49
  api_client = XeroRuby::ApiClient.new(credentials: creds)
50
- expect(api_client.authorization_url).to eq('https://login.xero.com/identity/connect/authorize?response_type=code&client_id=abc&redirect_uri=https://mydomain.com/callback&scope=openid profile email accounting.transactions accounting.settings&state=i-am-customer-state')
50
+ expect(api_client.authorization_url).to eq('https://login.xero.com/identity/connect/authorize?response_type=code&client_id=abc&redirect_uri=https://mydomain.com/callback&scope=openid+profile+email+accounting.transactions+accounting.settings&state=i-am-customer-state')
51
51
  end
52
52
 
53
53
  it "Does not append state if it is not provided" do
@@ -58,7 +58,20 @@ describe XeroRuby::ApiClient do
58
58
  scopes: 'openid profile email accounting.transactions accounting.settings'
59
59
  }
60
60
  api_client = XeroRuby::ApiClient.new(credentials: creds)
61
- expect(api_client.authorization_url).to eq('https://login.xero.com/identity/connect/authorize?response_type=code&client_id=abc&redirect_uri=https://mydomain.com/callback&scope=openid profile email accounting.transactions accounting.settings')
61
+ expect(api_client.authorization_url).to eq('https://login.xero.com/identity/connect/authorize?response_type=code&client_id=abc&redirect_uri=https://mydomain.com/callback&scope=openid+profile+email+accounting.transactions+accounting.settings')
62
+ end
63
+
64
+ it "Validates state on callback matches @config.state" do
65
+ creds = {
66
+ client_id: 'abc',
67
+ client_secret: '123',
68
+ redirect_uri: 'https://mydomain.com/callback',
69
+ scopes: 'openid profile email accounting.transactions accounting.settings',
70
+ state: "custom-state"
71
+ }
72
+ api_client = XeroRuby::ApiClient.new(credentials: creds)
73
+ altered_state = {'state': 'not-original-state'}
74
+ expect{api_client.validate_state(altered_state)}.to raise_error(StandardError, 'WARNING: @config.state: custom-state and OAuth callback state: do not match!')
62
75
  end
63
76
  end
64
77
  end
@@ -66,7 +79,7 @@ describe XeroRuby::ApiClient do
66
79
 
67
80
  describe 'api_client helper functions' do
68
81
  let(:api_client) { XeroRuby::ApiClient.new }
69
- let(:token_set) { {access_token: 'eyx.jibberjabber', refresh_token: 'REFRESHMENTS'} }
82
+ let(:token_set) { {'access_token': 'eyx.authorization.data', 'id_token': 'eyx.authentication.data', 'refresh_token': 'REFRESHMENTS'} }
70
83
  let(:connections) {
71
84
  [{
72
85
  "id" => "xxx-yyy-zzz",
@@ -84,12 +97,17 @@ describe XeroRuby::ApiClient do
84
97
 
85
98
  it "#set_token_set" do
86
99
  api_client.set_token_set(token_set)
87
- expect(api_client.token_set).to eq(token_set)
100
+ expect(api_client.token_set).to eq(token_set.with_indifferent_access)
88
101
  end
89
102
 
90
103
  it "#set_access_token" do
91
- api_client.set_access_token(token_set[:access_token])
92
- expect(api_client.access_token).to eq(token_set[:access_token])
104
+ api_client.set_access_token(token_set['access_token'])
105
+ expect(api_client.access_token).to eq(token_set['access_token'])
106
+ end
107
+
108
+ it "#set_id_token" do
109
+ api_client.set_id_token(token_set['id_token'])
110
+ expect(api_client.id_token).to eq(token_set['id_token'])
93
111
  end
94
112
 
95
113
  it "#refresh_token_set" do
@@ -137,6 +155,17 @@ describe XeroRuby::ApiClient do
137
155
  api_client.connections
138
156
  expect(api_client.config.base_url).to eq('https://api.xero.com')
139
157
  end
158
+
159
+ it "does not mutate the original opts hash" do
160
+ expect(api_client).to receive(:call_api).and_return('')
161
+ opts = {
162
+ where: {
163
+ invoice_number: ['=', "INV-0060"]
164
+ }
165
+ }
166
+ api_client.accounting_api.get_invoices('active_tenant_id', opts)
167
+ expect(opts).to eq({:where=>{:invoice_number=>["=", "INV-0060"]}})
168
+ end
140
169
  end
141
170
 
142
171
  describe '#deserialize' do
@@ -360,4 +389,142 @@ describe XeroRuby::ApiClient do
360
389
  expect(api_client.sanitize_filename('.\sun.gif')).to eq('sun.gif')
361
390
  end
362
391
  end
392
+
393
+ describe 'token helper methods' do
394
+ let(:api_client) { XeroRuby::ApiClient.new }
395
+ let(:id_token){'eyJhbGciOiJSUzI1NiIsImtpZCI6IjFDQUY4RTY2NzcyRDZEQzAyOEQ2NzI2RkQwMjYxNTgxNTcwRUZDMTkiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJISy1PWm5jdGJjQW8xbkp2MENZVmdWY09fQmsifQ.eyJuYmYiOjE2MTk3MTQwNDMsImV4cCI6MTYxOTcxNDM0MywiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS54ZXJvLmNvbSIsImF1ZCI6IkFEQjVBNzdEQTZCNjRFOTI4RDg0MDkwOTlBMzlDQTdCIiwiaWF0IjoxNjE5NzE0MDQzLCJhdF9oYXNoIjoiMXJNamVvUTJiOUxUNFU0ZlBXbEZJZyIsInNpZCI6ImY0YTY4ZDc0ZmM3OTQzMjc4YTgzMTg0NGM5ZWRmNzFiIiwic3ViIjoiZGI0ZjBmMzdiNTg1NTMwZTkxZjNiOWNiYjUwMzQwZTgiLCJhdXRoX3RpbWUiOjE2MTk3MTM5ODcsInhlcm9fdXNlcmlkIjoiZmFhODNlYzktZjZhNy00ODlmLTg5MTEtZTNmY2UwM2ExMTg2IiwiZ2xvYmFsX3Nlc3Npb25faWQiOiJmNGE2OGQ3NGZjNzk0MzI3OGE4MzE4NDRjOWVkZjcxYiIsInByZWZlcnJlZF91c2VybmFtZSI6ImNocmlzLmtuaWdodEB4ZXJvLmNvbSIsImVtYWlsIjoiY2hyaXMua25pZ2h0QHhlcm8uY29tIiwiZ2l2ZW5fbmFtZSI6IkNocmlzdG9waGVyIiwiZmFtaWx5X25hbWUiOiJLbmlnaHQifQ.hF04tCE1Qd-al355fQyCjWqTVWKnguor4RD1sC7rKH7zV3r3_nGwnGLMm2A96fov06fig0zusTX8onev0qFLZy-jlEXDp1f19LHhT15sBy0KH6dB0lGMrM14BnDuEP4NUGeP06nAPhQHHLw2oCc9hzYXorRVOSFDw43jgAC0vxRgDvJwgKgv6TDVEmpvwP0S4R7A0VbnFemHP_HY8gLHd7RpN7rrYmpJC4cofztdptDNLTF8Qup8qVlFdQgpJPQEQ95N1m6W-unvrh_dlO6AVMjXBjC1BJ10IGzoCCr8DSVyz2UMPnUT3oIYFVTlDc2K-ZJYkW86pigITMCdvR1hKg'}
396
+ let(:access_token){'eyJhbGciOiJSUzI1NiIsImtpZCI6IjFDQUY4RTY2NzcyRDZEQzAyOEQ2NzI2RkQwMjYxNTgxNTcwRUZDMTkiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJISy1PWm5jdGJjQW8xbkp2MENZVmdWY09fQmsifQ.eyJuYmYiOjE2MTk3MTQwNDMsImV4cCI6MTYxOTcxNTg0MywiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS54ZXJvLmNvbSIsImF1ZCI6Imh0dHBzOi8vaWRlbnRpdHkueGVyby5jb20vcmVzb3VyY2VzIiwiY2xpZW50X2lkIjoiQURCNUE3N0RBNkI2NEU5MjhEODQwOTA5OUEzOUNBN0IiLCJzdWIiOiJkYjRmMGYzN2I1ODU1MzBlOTFmM2I5Y2JiNTAzNDBlOCIsImF1dGhfdGltZSI6MTYxOTcxMzk4NywieGVyb191c2VyaWQiOiJmYWE4M2VjOS1mNmE3LTQ4OWYtODkxMS1lM2ZjZTAzYTExODYiLCJnbG9iYWxfc2Vzc2lvbl9pZCI6ImY0YTY4ZDc0ZmM3OTQzMjc4YTgzMTg0NGM5ZWRmNzFiIiwianRpIjoiZmFmNGNkYzQ5MjM0YzhmZDE0OTA0ZjRlOWEyMWY4YmYiLCJhdXRoZW50aWNhdGlvbl9ldmVudF9pZCI6IjI0MmRjNWIyLTIwZTMtNGFjNS05NjU3LWExMGI5ZTI0ZGI1NSIsInNjb3BlIjpbImVtYWlsIiwicHJvZmlsZSIsIm9wZW5pZCIsImFjY291bnRpbmcucmVwb3J0cy5yZWFkIiwiZmlsZXMiLCJwYXlyb2xsLmVtcGxveWVlcyIsInBheXJvbGwucGF5cnVucyIsInBheXJvbGwucGF5c2xpcCIsInBheXJvbGwudGltZXNoZWV0cyIsInByb2plY3RzLnJlYWQiLCJwcm9qZWN0cyIsImFjY291bnRpbmcuc2V0dGluZ3MiLCJhY2NvdW50aW5nLmF0dGFjaG1lbnRzIiwiYWNjb3VudGluZy50cmFuc2FjdGlvbnMiLCJhY2NvdW50aW5nLmpvdXJuYWxzLnJlYWQiLCJhc3NldHMucmVhZCIsImFzc2V0cyIsImFjY291bnRpbmcuY29udGFjdHMiLCJwYXlyb2xsLnNldHRpbmdzIiwib2ZmbGluZV9hY2Nlc3MiXX0.vNV-YsgHFWKFBmyYdhg7tztdsPc9ykObadQcGFoFXJ8qCBerR3h7XXKzWAP3KzFzhOCcIpWU8Q081zuYBNxahPeeLRLUuc_3MwgwE72esE5vGuxa2_-_QidtNvMCgsX-ie_LcX7FE_KI-sXB_EZ8fDk6WAMIPC9d3GejgeuH5Uh6rZkhowN2jm5pZjEOEy_QE7PScBO0XEbiZNUsarvBUSdKuSTvVVLHzHzs0bHMRfgKEkqZySNtZlac-oyaL3PVba1S7A_vbRcNWpYR_VrKGf2g9LHSI3EA5j3Beto4pKukU-bk6rLBGul37u4tM17U-wyJLsFmt6ZC_SEJKgmluQ'}
397
+ let(:tkn_set) {{'id_token': id_token, 'access_token': access_token, 'refresh_token': 'abc123xyz'}}
398
+
399
+ before do
400
+ api_client.set_token_set(tkn_set)
401
+ end
402
+
403
+ it '#token_expired? for an expired token' do
404
+ expect(api_client.token_expired?).to eq(true)
405
+ end
406
+
407
+ it '#token_expired? for a just expired token' do
408
+ allow(api_client).to receive(:decoded_access_token).and_return({"exp"=>Time.now.to_i})
409
+ expect(api_client.token_expired?).to eq(true)
410
+ end
411
+
412
+ it '#token_expired? for a non-expired token' do
413
+ allow(api_client).to receive(:decoded_access_token).and_return({"exp"=>(Time.now + 30.minutes).to_i})
414
+ expect(api_client.token_expired?).to eq(false)
415
+ end
416
+
417
+ it '#token_expired? for an almost expired token' do
418
+ allow(api_client).to receive(:decoded_access_token).and_return({"exp"=>(Time.now + 30.seconds).to_i})
419
+ expect(api_client.token_expired?).to eq(false)
420
+ end
421
+
422
+ it '#validate_tokens' do
423
+ expect(api_client.validate_tokens(tkn_set)).to eq(true)
424
+ end
425
+ it '#access_token' do
426
+ expect(api_client.access_token).to eq(access_token)
427
+ end
428
+ it '#decoded_access_token' do
429
+ expect(api_client.decoded_access_token['aud']).to eq("https://identity.xero.com/resources")
430
+ end
431
+ it '#id_token' do
432
+ expect(api_client.id_token).to eq(tkn_set[:id_token])
433
+ end
434
+ it '#decoded_id_token' do
435
+ expect(api_client.decoded_id_token['email']).to eq('chris.knight@xero.com')
436
+ end
437
+
438
+ it 'decoding an invalid access_token' do
439
+ api_client.set_access_token("#{access_token}.NotAValidJWTstring")
440
+ expect{api_client.decoded_access_token}.to raise_error(JSON::JWT::InvalidFormat)
441
+ end
442
+
443
+ it 'decoding an invalid id_token' do
444
+ api_client.set_id_token("#{id_token}.NotAValidJWTstring")
445
+ expect{api_client.decoded_id_token}.to raise_error(JSON::JWT::InvalidFormat)
446
+ end
447
+ end
448
+
449
+
450
+ describe 'thread safety in the XeroClient' do
451
+ let(:creds) {{
452
+ client_id: 'abc',
453
+ client_secret: '123',
454
+ redirect_uri: 'https://mydomain.com/callback',
455
+ scopes: 'openid profile email accounting.transactions'
456
+ }}
457
+ let(:api_client_1) {XeroRuby::ApiClient.new(credentials: creds)}
458
+ let(:api_client_2) {XeroRuby::ApiClient.new(credentials: creds)}
459
+ let(:api_client_3) {XeroRuby::ApiClient.new(credentials: creds)}
460
+
461
+ let(:tkn_set_1){{'id_token': "abc.123.1", 'access_token': "xxx.yyy.zzz.111"}}
462
+ let(:tkn_set_2){{'id_token': "efg.456.2", 'access_token': "xxx.yyy.zzz.222"}}
463
+
464
+ describe 'when configuration is changed, other instantiations of the client are not affected' do
465
+ it 'applies to #set_access_token' do
466
+ expect(api_client_1.access_token).to eq(nil)
467
+ expect(api_client_2.access_token).to eq(nil)
468
+ expect(api_client_3.access_token).to eq(nil)
469
+
470
+ api_client_1.set_access_token("ACCESS_TOKEN_1")
471
+ expect(api_client_1.access_token).to eq("ACCESS_TOKEN_1")
472
+ expect(api_client_2.access_token).to eq(nil)
473
+ expect(api_client_3.access_token).to eq(nil)
474
+
475
+ api_client_2.set_access_token("ACCESS_TOKEN_2")
476
+ expect(api_client_1.access_token).to eq("ACCESS_TOKEN_1")
477
+ expect(api_client_2.access_token).to eq("ACCESS_TOKEN_2")
478
+ expect(api_client_3.access_token).to eq(nil)
479
+
480
+ api_client_3.set_access_token("ACCESS_TOKEN_3")
481
+ expect(api_client_1.access_token).to eq("ACCESS_TOKEN_1")
482
+ expect(api_client_2.access_token).to eq("ACCESS_TOKEN_2")
483
+ expect(api_client_3.access_token).to eq("ACCESS_TOKEN_3")
484
+ end
485
+
486
+ it 'applies to #set_id_token' do
487
+ expect(api_client_1.id_token).to eq(nil)
488
+ expect(api_client_2.id_token).to eq(nil)
489
+
490
+ api_client_1.set_id_token("id_token_1")
491
+ expect(api_client_1.id_token).to eq("id_token_1")
492
+ expect(api_client_2.id_token).to eq(nil)
493
+
494
+ api_client_2.set_id_token("id_token_2")
495
+ expect(api_client_1.id_token).to eq("id_token_1")
496
+ expect(api_client_2.id_token).to eq("id_token_2")
497
+ end
498
+
499
+ it 'applies to #set_token_set' do
500
+ expect(api_client_1.token_set).to eq(nil)
501
+ expect(api_client_2.token_set).to eq(nil)
502
+
503
+ api_client_1.set_token_set(tkn_set_1)
504
+ expect(api_client_1.token_set).to eq(tkn_set_1.with_indifferent_access)
505
+ expect(api_client_2.token_set).to eq(nil)
506
+
507
+ api_client_2.set_token_set(tkn_set_2)
508
+ expect(api_client_1.token_set).to eq(tkn_set_1.with_indifferent_access)
509
+ expect(api_client_2.token_set).to eq(tkn_set_2.with_indifferent_access)
510
+ end
511
+
512
+ it 'applies to #base_url' do
513
+ expect(api_client_1.config.base_url).to eq(nil)
514
+ expect(api_client_2.config.base_url).to eq(nil)
515
+
516
+ api_client_1.accounting_api
517
+ expect(api_client_1.config.base_url).to eq(api_client_1.config.accounting_url)
518
+ expect(api_client_2.config.base_url).to eq(nil)
519
+
520
+ api_client_2.files_api
521
+ expect(api_client_1.config.base_url).to eq(api_client_1.config.accounting_url)
522
+ expect(api_client_2.config.base_url).to eq(api_client_1.config.files_url)
523
+
524
+ api_client_2.project_api
525
+ expect(api_client_1.config.base_url).to eq(api_client_1.config.accounting_url)
526
+ expect(api_client_2.config.base_url).to eq(api_client_1.config.project_url)
527
+ end
528
+ end
529
+ end
363
530
  end