xero-ruby 2.8.1 → 2.10.1

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