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.
- checksums.yaml +4 -4
- data/README.md +152 -99
- data/lib/xero-ruby/api/accounting_api.rb +573 -346
- data/lib/xero-ruby/api/asset_api.rb +12 -6
- data/lib/xero-ruby/api/files_api.rb +34 -17
- data/lib/xero-ruby/api/payroll_au_api.rb +58 -29
- data/lib/xero-ruby/api/payroll_nz_api.rb +136 -68
- data/lib/xero-ruby/api/payroll_uk_api.rb +140 -70
- data/lib/xero-ruby/api/project_api.rb +26 -13
- data/lib/xero-ruby/api_client.rb +99 -14
- data/lib/xero-ruby/configuration.rb +14 -1
- data/lib/xero-ruby/models/payroll_au/deduction_line.rb +0 -5
- data/lib/xero-ruby/models/payroll_uk/salary_and_wage.rb +3 -2
- data/lib/xero-ruby/models/projects/time_entry.rb +3 -2
- data/lib/xero-ruby/version.rb +2 -2
- data/spec/api_client_spec.rb +174 -7
- data/spec/api_error_spec.rb +1 -1
- data/spec/configuration_spec.rb +17 -0
- data/spec/helper_methods_spec.rb +2 -2
- metadata +22 -2
@@ -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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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={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,
|
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 `dateUtc` 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 `dateUtc` 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,
|
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,
|
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,
|
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,
|
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,
|
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
|
data/lib/xero-ruby/api_client.rb
CHANGED
@@ -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:
|
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
|
-
|
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
|
-
|
101
|
+
@config.token_set
|
92
102
|
end
|
93
103
|
|
94
104
|
def access_token
|
95
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
set_access_token(token_set[
|
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
|
-
|
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
|
-
|
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[
|
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[
|
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
|
-
|
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
|
data/lib/xero-ruby/version.rb
CHANGED
@@ -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
|
+
The version of the XeroOpenAPI document: 2.11.0
|
11
11
|
=end
|
12
12
|
|
13
13
|
module XeroRuby
|
14
|
-
VERSION = '2.
|
14
|
+
VERSION = '2.10.1'
|
15
15
|
end
|
data/spec/api_client_spec.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require '
|
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
|
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
|
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.
|
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[
|
92
|
-
expect(api_client.access_token).to eq(token_set[
|
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
|