rdstation-ruby-client 2.1.0 → 2.5.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 (40) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +121 -1
  3. data/README.md +106 -22
  4. data/Rakefile +4 -0
  5. data/lib/rdstation-ruby-client.rb +6 -1
  6. data/lib/rdstation.rb +19 -0
  7. data/lib/rdstation/api_response.rb +1 -2
  8. data/lib/rdstation/authentication.rb +8 -3
  9. data/lib/rdstation/{authorization_header.rb → authorization.rb} +11 -8
  10. data/lib/rdstation/builder/field.rb +70 -0
  11. data/lib/rdstation/client.rb +17 -7
  12. data/lib/rdstation/contacts.rb +22 -13
  13. data/lib/rdstation/error.rb +3 -0
  14. data/lib/rdstation/error/format.rb +29 -3
  15. data/lib/rdstation/error/formatter.rb +69 -8
  16. data/lib/rdstation/error_handler.rb +6 -1
  17. data/lib/rdstation/error_handler/invalid_refresh_token.rb +24 -0
  18. data/lib/rdstation/error_handler/unauthorized.rb +2 -0
  19. data/lib/rdstation/events.rb +7 -12
  20. data/lib/rdstation/fields.rb +35 -6
  21. data/lib/rdstation/retryable_request.rb +35 -0
  22. data/lib/rdstation/version.rb +1 -1
  23. data/lib/rdstation/webhooks.rb +25 -13
  24. data/rdstation-ruby-client.gemspec +2 -1
  25. data/spec/lib/rdstation/api_response_spec.rb +34 -0
  26. data/spec/lib/rdstation/authentication_spec.rb +105 -2
  27. data/spec/lib/rdstation/{authorization_header_spec.rb → authorization_spec.rb} +3 -3
  28. data/spec/lib/rdstation/builder/field_spec.rb +69 -0
  29. data/spec/lib/rdstation/client_spec.rb +6 -6
  30. data/spec/lib/rdstation/contacts_spec.rb +23 -3
  31. data/spec/lib/rdstation/error/format_spec.rb +63 -0
  32. data/spec/lib/rdstation/error/formatter_spec.rb +113 -0
  33. data/spec/lib/rdstation/error_handler/invalid_refresh_token_spec.rb +53 -0
  34. data/spec/lib/rdstation/error_handler_spec.rb +23 -0
  35. data/spec/lib/rdstation/events_spec.rb +8 -3
  36. data/spec/lib/rdstation/fields_spec.rb +6 -1
  37. data/spec/lib/rdstation/retryable_request_spec.rb +142 -0
  38. data/spec/lib/rdstation/webhooks_spec.rb +26 -1
  39. data/spec/lib/rdstation_spec.rb +18 -0
  40. metadata +36 -8
@@ -0,0 +1,24 @@
1
+ module RDStation
2
+ class ErrorHandler
3
+ class InvalidRefreshToken
4
+ attr_reader :errors
5
+
6
+ ERROR_CODE = 'INVALID_REFRESH_TOKEN'.freeze
7
+
8
+ def initialize(errors)
9
+ @errors = errors
10
+ end
11
+
12
+ def raise_error
13
+ return if invalid_refresh_token_error.empty?
14
+ raise RDStation::Error::InvalidRefreshToken, invalid_refresh_token_error
15
+ end
16
+
17
+ private
18
+
19
+ def invalid_refresh_token_error
20
+ errors.find { |error| error['error_type'] == ERROR_CODE }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,6 +1,7 @@
1
1
  require_relative 'expired_access_token'
2
2
  require_relative 'expired_code_grant'
3
3
  require_relative 'invalid_credentials'
4
+ require_relative 'invalid_refresh_token'
4
5
 
5
6
  module RDStation
6
7
  class ErrorHandler
@@ -9,6 +10,7 @@ module RDStation
9
10
  ErrorHandler::ExpiredAccessToken,
10
11
  ErrorHandler::ExpiredCodeGrant,
11
12
  ErrorHandler::InvalidCredentials,
13
+ ErrorHandler::InvalidRefreshToken,
12
14
  ].freeze
13
15
 
14
16
  def initialize(array_of_errors)
@@ -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.1.0'.freeze
2
+ VERSION = '2.5.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
@@ -158,19 +217,63 @@ RSpec.describe RDStation::Authentication do
158
217
  end
159
218
 
160
219
  context 'when the refresh token is invalid' do
220
+ let(:invalid_refresh_token_response) do
221
+ {
222
+ status: 401,
223
+ headers: { 'Content-Type' => 'application/json' },
224
+ body: {
225
+ errors: {
226
+ error_type: 'INVALID_REFRESH_TOKEN',
227
+ error_message: 'The provided refresh token is invalid or was revoked.'
228
+ }
229
+ }.to_json
230
+ }
231
+ end
232
+
161
233
  before do
162
234
  stub_request(:post, token_endpoint)
163
235
  .with(
164
236
  headers: request_headers,
165
237
  body: token_request_with_invalid_refresh_token.to_json
166
238
  )
167
- .to_return(invalid_code_response)
239
+ .to_return(invalid_refresh_token_response)
168
240
  end
169
241
 
170
242
  it 'returns an auth error' do
171
243
  expect do
172
244
  authentication.update_access_token('invalid_refresh_token')
173
- end.to raise_error(RDStation::Error::InvalidCredentials)
245
+ end.to raise_error(RDStation::Error::InvalidRefreshToken)
246
+ end
247
+ end
248
+
249
+ context 'when client_id and client_secret are specified only in configuration' do
250
+ let(:authentication) { described_class.new }
251
+ let(:configuration_client_id) { 'configuration_client_id' }
252
+ let(:configuration_client_secret) { 'configuration_client_secret' }
253
+ let(:token_request_with_valid_refresh_code_secrets_from_config) do
254
+ {
255
+ client_id: configuration_client_id,
256
+ client_secret: configuration_client_secret,
257
+ refresh_token: 'valid_refresh_token'
258
+ }
259
+ end
260
+ before do
261
+ RDStation.configure do |config|
262
+ config.client_id = configuration_client_id
263
+ config.client_secret = configuration_client_secret
264
+ end
265
+
266
+ stub_request(:post, token_endpoint)
267
+ .with(
268
+ headers: request_headers,
269
+ body: token_request_with_valid_refresh_code_secrets_from_config.to_json
270
+ )
271
+ .to_return(credentials_response)
272
+ end
273
+
274
+ it 'returns the credentials' do
275
+ credentials_request = authentication.update_access_token('valid_refresh_token')
276
+ expect(credentials_request).to eq(credentials)
174
277
  end
175
278
  end
176
279
  end
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- RSpec.describe RDStation::AuthorizationHeader do
3
+ RSpec.describe RDStation::Authorization do
4
4
 
5
5
  describe ".initialize" do
6
6
  context "when access_token is nil" do
@@ -12,11 +12,11 @@ RSpec.describe RDStation::AuthorizationHeader do
12
12
  end
13
13
  end
14
14
 
15
- describe "#to_h" do
15
+ describe "#headers" do
16
16
  let(:access_token) { 'access_token' }
17
17
 
18
18
  it "generates the correct header" do
19
- header = described_class.new(access_token: access_token).to_h
19
+ header = described_class.new(access_token: access_token).headers
20
20
  expect(header['Authorization']).to eq "Bearer #{access_token}"
21
21
  expect(header['Content-Type']).to eq "application/json"
22
22
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe RDStation::Builder::Field do
6
+ def valid_builder
7
+ described_class.new('cf_identifier')
8
+ end
9
+
10
+ describe 'when create a builder' do
11
+ context 'valid' do
12
+ let(:initial_parameters) do
13
+ 'cf_api_identifier'
14
+ end
15
+
16
+ let(:builder) { described_class.new(initial_parameters) }
17
+
18
+ let(:expected_result) do
19
+ {
20
+ 'api_identifier' => 'cf_api_identifier',
21
+ 'data_type' => 'STRING',
22
+ 'presentation_type' => 'TEXT_INPUT',
23
+ 'label' => { 'pt-BR' => 'My label' },
24
+ 'name' => { 'pt-BR' => 'My name' }
25
+ }
26
+ end
27
+
28
+ it 'returns an hash of required values' do
29
+ builder.label 'pt-BR', 'My label'
30
+ builder.name 'pt-BR', 'My name'
31
+ builder.data_type 'STRING'
32
+ builder.presentation_type 'TEXT_INPUT'
33
+
34
+ result = builder.build
35
+ expect(result).to eq(expected_result)
36
+ end
37
+ end
38
+
39
+ context 'invalid' do
40
+ it 'using invalid api_identifier ' do
41
+ expect { described_class.new('invald_identifier') }.to raise_error(
42
+ 'api_identifier is not in a valid format, need start with "cf_"'
43
+ )
44
+ end
45
+
46
+ it 'using invalid data_type ' do
47
+ expect { valid_builder.data_type('invalid_data_type') }.to raise_error(
48
+ 'Not valid data_type - ["STRING", "INTEGER", "BOOLEAN", "STRING[]"]'
49
+ )
50
+ end
51
+
52
+ it 'using invalid presentation_type ' do
53
+ expect { valid_builder.presentation_type('invalid presentation_type') }.to raise_error(
54
+ 'Not valid presentation_type - ["TEXT_INPUT", "TEXT_AREA", "URL_INPUT", "PHONE_INPUT", "EMAIL_INPUT", "CHECK_BOX", "NUMBER_INPUT", "COMBO_BOX", "RADIO_BUTTON", "MULTIPLE_CHOICE"]'
55
+ )
56
+ end
57
+
58
+ it 'without api_identifier' do
59
+ expect { described_class.new(nil) }.to raise_error('api_identifier required')
60
+ end
61
+
62
+ it 'without required fields' do
63
+ expect { valid_builder.build }.to raise_error(
64
+ 'Required fields are missing - ["data_type", "presentation_type", "label", "name"]'
65
+ )
66
+ end
67
+ end
68
+ end
69
+ end