rdstation-ruby-client 2.1.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
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