rdstation-ruby-client 2.0.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +116 -4
  3. data/README.md +114 -22
  4. data/lib/rdstation-ruby-client.rb +6 -1
  5. data/lib/rdstation.rb +19 -0
  6. data/lib/rdstation/api_response.rb +1 -2
  7. data/lib/rdstation/authentication.rb +32 -3
  8. data/lib/rdstation/{authorization_header.rb → authorization.rb} +11 -8
  9. data/lib/rdstation/builder/field.rb +70 -0
  10. data/lib/rdstation/client.rb +17 -7
  11. data/lib/rdstation/contacts.rb +22 -13
  12. data/lib/rdstation/error.rb +2 -0
  13. data/lib/rdstation/error/format.rb +29 -3
  14. data/lib/rdstation/error/formatter.rb +69 -8
  15. data/lib/rdstation/error_handler.rb +6 -1
  16. data/lib/rdstation/events.rb +7 -12
  17. data/lib/rdstation/fields.rb +35 -6
  18. data/lib/rdstation/retryable_request.rb +35 -0
  19. data/lib/rdstation/version.rb +1 -1
  20. data/lib/rdstation/webhooks.rb +25 -13
  21. data/rdstation-ruby-client.gemspec +2 -1
  22. data/spec/lib/rdstation/api_response_spec.rb +34 -0
  23. data/spec/lib/rdstation/authentication_spec.rb +164 -0
  24. data/spec/lib/rdstation/{authorization_header_spec.rb → authorization_spec.rb} +3 -3
  25. data/spec/lib/rdstation/builder/field_spec.rb +69 -0
  26. data/spec/lib/rdstation/client_spec.rb +6 -6
  27. data/spec/lib/rdstation/contacts_spec.rb +23 -3
  28. data/spec/lib/rdstation/error/format_spec.rb +63 -0
  29. data/spec/lib/rdstation/error/formatter_spec.rb +113 -0
  30. data/spec/lib/rdstation/error_handler_spec.rb +23 -0
  31. data/spec/lib/rdstation/events_spec.rb +8 -3
  32. data/spec/lib/rdstation/fields_spec.rb +6 -1
  33. data/spec/lib/rdstation/retryable_request_spec.rb +142 -0
  34. data/spec/lib/rdstation/webhooks_spec.rb +26 -1
  35. data/spec/lib/rdstation_spec.rb +18 -0
  36. metadata +36 -11
@@ -13,6 +13,8 @@ module RDStation
13
13
  raise error_class, array_of_errors.first if error_class < RDStation::Error
14
14
 
15
15
  error_class.new(array_of_errors).raise_error
16
+ rescue JSON::ParserError => error
17
+ raise error_class, { 'error_message' => response.body }
16
18
  end
17
19
 
18
20
  private
@@ -30,11 +32,14 @@ module RDStation
30
32
  when 409 then RDStation::Error::Conflict
31
33
  when 415 then RDStation::Error::UnsupportedMediaType
32
34
  when 422 then RDStation::Error::UnprocessableEntity
35
+ when 429 then RDStation::Error::TooManyRequests
33
36
  when 500 then RDStation::Error::InternalServerError
34
37
  when 501 then RDStation::Error::NotImplemented
35
38
  when 502 then RDStation::Error::BadGateway
36
39
  when 503 then RDStation::Error::ServiceUnavailable
37
40
  when 500..599 then RDStation::Error::ServerError
41
+ else
42
+ RDStation::Error::UnknownError
38
43
  end
39
44
  end
40
45
 
@@ -53,7 +58,7 @@ module RDStation
53
58
  end
54
59
 
55
60
  def additional_error_attributes
56
- {
61
+ attrs = {
57
62
  'headers' => response.headers,
58
63
  'body' => JSON.parse(response.body),
59
64
  'http_status' => response.code,
@@ -1,24 +1,19 @@
1
1
  module RDStation
2
2
  class Events
3
3
  include HTTParty
4
+ include ::RDStation::RetryableRequest
4
5
 
5
6
  EVENTS_ENDPOINT = 'https://api.rd.services/platform/events'.freeze
6
7
 
7
- def initialize(authorization_header:)
8
- @authorization_header = authorization_header
8
+ def initialize(authorization:)
9
+ @authorization = authorization
9
10
  end
10
11
 
11
12
  def create(payload)
12
- response = self.class.post(EVENTS_ENDPOINT, headers: @authorization_header.to_h, body: payload.to_json)
13
- response_body = JSON.parse(response.body)
14
- return response_body unless errors?(response_body)
15
- RDStation::ErrorHandler.new(response).raise_error
16
- end
17
-
18
- private
19
-
20
- def errors?(response_body)
21
- response_body.is_a?(Array) || response_body['errors']
13
+ retryable_request(@authorization) do |authorization|
14
+ response = self.class.post(EVENTS_ENDPOINT, headers: authorization.headers, body: payload.to_json)
15
+ ApiResponse.build(response)
16
+ end
22
17
  end
23
18
  end
24
19
  end
@@ -1,19 +1,48 @@
1
1
  # encoding: utf-8
2
2
  module RDStation
3
- # More info: https://developers.rdstation.com/pt-BR/reference/contacts
3
+ # More info: https://developers.rdstation.com/pt-BR/reference/fields
4
4
  class Fields
5
5
  include HTTParty
6
+ include ::RDStation::RetryableRequest
6
7
 
7
8
  BASE_URL = 'https://api.rd.services/platform/contacts/fields'.freeze
8
-
9
- def initialize(authorization_header:)
10
- @authorization_header = authorization_header
9
+
10
+ def initialize(authorization:)
11
+ @authorization = authorization
11
12
  end
12
13
 
13
14
  def all
14
- response = self.class.get(BASE_URL, headers: @authorization_header.to_h)
15
- ApiResponse.build(response)
15
+ retryable_request(@authorization) do |authorization|
16
+ response = self.class.get(BASE_URL, headers: authorization.headers)
17
+ ApiResponse.build(response)
18
+ end
19
+ end
20
+
21
+ def create(payload)
22
+ retryable_request(@authorization) do |authorization|
23
+ response = self.class.post(BASE_URL, headers: authorization.headers, body: payload.to_json)
24
+ ApiResponse.build(response)
25
+ end
26
+ end
27
+
28
+ def update(uuid, payload)
29
+ retryable_request(@authorization) do |authorization|
30
+ response = self.class.patch(base_url(uuid), headers: authorization.headers, body: payload.to_json)
31
+ ApiResponse.build(response)
32
+ end
16
33
  end
17
34
 
35
+ def delete(uuid)
36
+ retryable_request(@authorization) do |authorization|
37
+ response = self.class.delete(base_url(uuid), headers: authorization.headers)
38
+ ApiResponse.build(response)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def base_url(path = '')
45
+ "#{BASE_URL}/#{path}"
46
+ end
18
47
  end
19
48
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RDStation
4
+ module RetryableRequest
5
+ MAX_RETRIES = 1
6
+ def retryable_request(authorization)
7
+ retries = 0
8
+ begin
9
+ yield authorization
10
+ rescue ::RDStation::Error::ExpiredAccessToken => e
11
+ raise if !retry_possible?(authorization) || retries >= MAX_RETRIES
12
+
13
+ retries += 1
14
+ refresh_access_token(authorization)
15
+ retry
16
+ end
17
+ end
18
+
19
+ def retry_possible?(authorization)
20
+ [
21
+ RDStation.configuration&.client_id,
22
+ RDStation.configuration&.client_secret,
23
+ authorization.refresh_token
24
+ ].all?
25
+ end
26
+
27
+ def refresh_access_token(authorization)
28
+ client = RDStation::Authentication.new
29
+ response = client.update_access_token(authorization.refresh_token)
30
+ authorization.access_token = response['access_token']
31
+ authorization.access_token_expires_in = response['expires_in']
32
+ RDStation.configuration&.access_token_refresh_callback&.call(authorization)
33
+ end
34
+ end
35
+ end
@@ -1,3 +1,3 @@
1
1
  module RDStation
2
- VERSION = '2.0.0'.freeze
2
+ VERSION = '2.4.0'.freeze
3
3
  end
@@ -1,35 +1,47 @@
1
1
  module RDStation
2
2
  class Webhooks
3
3
  include HTTParty
4
+ include ::RDStation::RetryableRequest
4
5
 
5
- def initialize(authorization_header:)
6
- @authorization_header = authorization_header
6
+ def initialize(authorization:)
7
+ @authorization = authorization
7
8
  end
8
9
 
9
10
  def all
10
- response = self.class.get(base_url, headers: @authorization_header.to_h)
11
- ApiResponse.build(response)
11
+ retryable_request(@authorization) do |authorization|
12
+ response = self.class.get(base_url, headers: authorization.headers)
13
+ ApiResponse.build(response)
14
+ end
12
15
  end
13
16
 
14
17
  def by_uuid(uuid)
15
- response = self.class.get(base_url(uuid), headers: @authorization_header.to_h)
16
- ApiResponse.build(response)
18
+ retryable_request(@authorization) do |authorization|
19
+ response = self.class.get(base_url(uuid), headers: authorization.headers)
20
+ ApiResponse.build(response)
21
+ end
17
22
  end
18
23
 
19
24
  def create(payload)
20
- response = self.class.post(base_url, headers: @authorization_header.to_h, body: payload.to_json)
21
- ApiResponse.build(response)
25
+ retryable_request(@authorization) do |authorization|
26
+ response = self.class.post(base_url, headers: authorization.headers, body: payload.to_json)
27
+ ApiResponse.build(response)
28
+ end
22
29
  end
23
30
 
24
31
  def update(uuid, payload)
25
- response = self.class.put(base_url(uuid), headers: @authorization_header.to_h, body: payload.to_json)
26
- ApiResponse.build(response)
32
+ retryable_request(@authorization) do |authorization|
33
+ response = self.class.put(base_url(uuid), headers: authorization.headers, body: payload.to_json)
34
+ ApiResponse.build(response)
35
+ end
27
36
  end
28
37
 
29
38
  def delete(uuid)
30
- response = self.class.delete(base_url(uuid), headers: @authorization_header.to_h)
31
- return webhook_deleted_message unless response.body
32
- RDStation::ErrorHandler.new(response).raise_error
39
+ retryable_request(@authorization) do |authorization|
40
+ response = self.class.delete(base_url(uuid), headers: authorization.headers)
41
+ return webhook_deleted_message unless response.body
42
+
43
+ RDStation::ErrorHandler.new(response).raise_error
44
+ end
33
45
  end
34
46
 
35
47
  private
@@ -20,12 +20,13 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.required_ruby_version = '>= 2.0.0'
22
22
 
23
- spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "bundler", "> 1.3"
24
24
  spec.add_development_dependency "rake"
25
25
  spec.add_development_dependency 'rspec'
26
26
  spec.add_development_dependency 'webmock', '~> 2.1'
27
27
  spec.add_development_dependency 'turn'
28
28
  spec.add_development_dependency 'rspec_junit_formatter'
29
+ spec.add_development_dependency 'pry'
29
30
 
30
31
  spec.add_dependency "httparty", "~> 0.12"
31
32
  end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe RDStation::ApiResponse do
4
+ describe ".build" do
5
+ context "when the response HTTP status is 2xx" do
6
+ let(:response) { OpenStruct.new(code: 200, body: '{}') }
7
+
8
+ it "returns the response body" do
9
+ expect(RDStation::ApiResponse.build(response)).to eq({})
10
+ end
11
+ end
12
+
13
+ shared_examples_for 'call_error_handler' do
14
+ it "calls error handler" do
15
+ error_handler = instance_double(RDStation::ErrorHandler)
16
+ allow(error_handler).to receive(:raise_error)
17
+ expect(RDStation::ErrorHandler).to receive(:new).with(response).and_return(error_handler)
18
+ RDStation::ApiResponse.build(response)
19
+ end
20
+ end
21
+
22
+ context "when the response is not in the 2xx range" do
23
+ let(:response) { OpenStruct.new(code: 404, body: '{}') }
24
+
25
+ it_behaves_like 'call_error_handler'
26
+ end
27
+
28
+ context "when the response body is not JSON-parseable" do
29
+ let(:response) { OpenStruct.new(code: 504, body: '<html><head></head><body></body></html>') }
30
+
31
+ it_behaves_like 'call_error_handler'
32
+ end
33
+ end
34
+ end
@@ -88,6 +88,34 @@ RSpec.describe RDStation::Authentication do
88
88
 
89
89
  let(:authentication) { described_class.new('client_id', 'client_secret') }
90
90
 
91
+ describe '#auth_url' do
92
+ let(:configuration_client_id) { 'configuration_client_id' }
93
+ let(:configuration_client_secret) { 'configuration_client_secret' }
94
+ let(:redirect_url) { 'redirect_url' }
95
+ before do
96
+ RDStation.configure do |config|
97
+ config.client_id = configuration_client_id
98
+ config.client_secret = configuration_client_secret
99
+ end
100
+ end
101
+
102
+ context 'when client_id and client_secret are specified in initialization' do
103
+ it 'uses those specified in initialization' do
104
+ auth = described_class.new('initialization_client_id', 'initialization_client_secret')
105
+ expected = "https://api.rd.services/auth/dialog?client_id=initialization_client_id&redirect_url=#{redirect_url}"
106
+ expect(auth.auth_url(redirect_url)).to eq expected
107
+ end
108
+ end
109
+
110
+ context 'when client_id and client_secret are specified only in configuration' do
111
+ it 'uses those specified in configuration' do
112
+ auth = described_class.new
113
+ expected = "https://api.rd.services/auth/dialog?client_id=#{configuration_client_id}&redirect_url=#{redirect_url}"
114
+ expect(auth.auth_url(redirect_url)).to eq expected
115
+ end
116
+ end
117
+ end
118
+
91
119
  describe '#authenticate' do
92
120
  context 'when the code is valid' do
93
121
  before do
@@ -138,6 +166,37 @@ RSpec.describe RDStation::Authentication do
138
166
  end.to raise_error(RDStation::Error::ExpiredCodeGrant)
139
167
  end
140
168
  end
169
+
170
+ context 'when client_id and client_secret are specified only in configuration' do
171
+ let(:authentication) { described_class.new }
172
+ let(:configuration_client_id) { 'configuration_client_id' }
173
+ let(:configuration_client_secret) { 'configuration_client_secret' }
174
+ let(:token_request_with_valid_code_secrets_from_config) do
175
+ {
176
+ client_id: configuration_client_id,
177
+ client_secret: configuration_client_secret,
178
+ code: 'valid_code'
179
+ }
180
+ end
181
+ before do
182
+ RDStation.configure do |config|
183
+ config.client_id = configuration_client_id
184
+ config.client_secret = configuration_client_secret
185
+ end
186
+
187
+ stub_request(:post, token_endpoint)
188
+ .with(
189
+ headers: request_headers,
190
+ body: token_request_with_valid_code_secrets_from_config.to_json
191
+ )
192
+ .to_return(credentials_response)
193
+ end
194
+
195
+ it 'returns the credentials' do
196
+ credentials_request = authentication.authenticate('valid_code')
197
+ expect(credentials_request).to eq(credentials)
198
+ end
199
+ end
141
200
  end
142
201
 
143
202
  describe '#update_access_token' do
@@ -173,5 +232,110 @@ RSpec.describe RDStation::Authentication do
173
232
  end.to raise_error(RDStation::Error::InvalidCredentials)
174
233
  end
175
234
  end
235
+
236
+ context 'when client_id and client_secret are specified only in configuration' do
237
+ let(:authentication) { described_class.new }
238
+ let(:configuration_client_id) { 'configuration_client_id' }
239
+ let(:configuration_client_secret) { 'configuration_client_secret' }
240
+ let(:token_request_with_valid_refresh_code_secrets_from_config) do
241
+ {
242
+ client_id: configuration_client_id,
243
+ client_secret: configuration_client_secret,
244
+ refresh_token: 'valid_refresh_token'
245
+ }
246
+ end
247
+ before do
248
+ RDStation.configure do |config|
249
+ config.client_id = configuration_client_id
250
+ config.client_secret = configuration_client_secret
251
+ end
252
+
253
+ stub_request(:post, token_endpoint)
254
+ .with(
255
+ headers: request_headers,
256
+ body: token_request_with_valid_refresh_code_secrets_from_config.to_json
257
+ )
258
+ .to_return(credentials_response)
259
+ end
260
+
261
+ it 'returns the credentials' do
262
+ credentials_request = authentication.update_access_token('valid_refresh_token')
263
+ expect(credentials_request).to eq(credentials)
264
+ end
265
+ end
266
+ end
267
+
268
+ describe ".revoke" do
269
+ let(:revoke_endpoint) { 'https://api.rd.services/auth/revoke' }
270
+ let(:request_headers) do
271
+ {
272
+ "Authorization" => "Bearer #{access_token}",
273
+ "Content-Type" => "application/x-www-form-urlencoded"
274
+ }
275
+ end
276
+
277
+ context "valid access_token" do
278
+ let(:access_token) { "valid_access_token" }
279
+
280
+ let(:ok_response) do
281
+ {
282
+ status: 200,
283
+ headers: { 'Content-Type' => 'application/json' },
284
+ body: {}.to_json
285
+ }
286
+ end
287
+
288
+ before do
289
+ stub_request(:post, revoke_endpoint)
290
+ .with(
291
+ headers: request_headers,
292
+ body: URI.encode_www_form({
293
+ token: access_token,
294
+ token_type_hint: 'access_token'
295
+ })
296
+ )
297
+ .to_return(ok_response)
298
+ end
299
+
300
+ it "returns 200 code with an empty hash in the body" do
301
+ request_response = RDStation::Authentication.revoke(access_token: access_token)
302
+ expect(request_response).to eq({})
303
+ end
304
+ end
305
+
306
+ context "invalid access token" do
307
+ let(:access_token) { "invalid_access_token" }
308
+
309
+ let(:unauthorized_response) do
310
+ {
311
+ status: 401,
312
+ headers: { 'Content-Type' => 'application/json' },
313
+ body: {
314
+ errors: {
315
+ error_type: 'UNAUTHORIZED',
316
+ error_message: 'Invalid token.'
317
+ }
318
+ }.to_json
319
+ }
320
+ end
321
+
322
+ before do
323
+ stub_request(:post, revoke_endpoint)
324
+ .with(
325
+ headers: request_headers,
326
+ body: URI.encode_www_form({
327
+ token: access_token,
328
+ token_type_hint: 'access_token'
329
+ })
330
+ )
331
+ .to_return(unauthorized_response)
332
+ end
333
+
334
+ it "raises unauthorized" do
335
+ expect do
336
+ RDStation::Authentication.revoke(access_token: access_token)
337
+ end.to raise_error(RDStation::Error::Unauthorized)
338
+ end
339
+ end
176
340
  end
177
341
  end