diff --git a/CHANGELOG.md b/CHANGELOG.md index 0558cf6..08a8465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ +## 2.2.0 + +### Additions + +#### Configuration + +Now it is possible to configure global params like client_id and client_secret only once, so you don't need to provide them to `RDStation::Authentication` every time. + +This can be done in the following way: + +```ruby +RDStation.configure do |config| + config.client_id = YOUR_CLIENT_ID + config.client_secret = YOUR_CLIENT_SECRET +end +``` + +If you're using Rails, this can be done in `config/initializers`. + +#### Automatic refresh of access_tokens + +When an access_token expires, a new one will be obtained automatically and the request will be made again. + +For this to work, you have to use `RDStation.configure` as described above, and provide the refresh token when instantiating `RDStation::Client` (ex: RDStation::Client.new(access_token: MY_ACCESS_TOKEN, refresh_token: MY_REFRESH_TOKEN). + +You can keep track of access_token changes, by providing a callback block inconfiguration. This block will be called with an `RDStation::Authorization` object, which contains the updated `access_token` and `refresh_token`. For example: + +```ruby +RDStation.configure do |config| + config.on_access_token_refresh do |authorization| + MyStoredAuth.where(refresh_token: authorization.refresh_token).update_all(access_token: authorization.access_token) + end +end +``` + +### Deprecations + +Providing `client_id` and `client_secret` directly to `RDStation::Authentication.new` is deprecated and will be removed in future versions. Use `RDStation.configure` instead. + +Specifying refresh_token in `RDStation::Client.new(access_token: 'at', refresh_token: 'rt')` is optional right now, but will be mandatory in future versions. + ## 2.1.1 - Fixed a bug in error handling (issue [#47](https://github.com/ResultadosDigitais/rdstation-ruby-client/issues/47)) diff --git a/README.md b/README.md index c01b04a..55f746f 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,13 @@ Upgrading? Check the [migration guide](#Migration-guide) before bumping to a new 1. [Installation](#Installation) 2. [Usage](#Usage) - 1. [Authentication](#Authentication) - 2. [Contacts](#Contacts) - 3. [Events](#Events) - 4. [Fields](#Fields) - 5. [Webhooks](#Webhooks) - 6. [Errors](#Errors) + 1. [Configuration](#Configuration) + 2. [Authentication](#Authentication) + 3. [Contacts](#Contacts) + 4. [Events](#Events) + 5. [Fields](#Fields) + 6. [Webhooks](#Webhooks) + 7. [Errors](#Errors) 3. [Changelog](#Changelog) 4. [Migration guide](#Migration-guide) 1. [Upgrading from 1.2.x to 2.0.0](#Upgrading-from-1.2.x-to-2.0.0) @@ -39,6 +40,19 @@ Or install it yourself as: ## Usage +### Configuration + +Before getting youre credentials, you need to configure client_id and client_secret as following: + +```ruby +RDStation.configure do |config| + config.client_id = YOUR_CLIENT_ID + config.client_secret = YOUR_CLIENT_SECRET +end +``` + +For details on what `client_id` and `client_secret` are, check the [developers portal](https://developers.rdstation.com/en/authentication). + ### Authentication For more details, check the [developers portal](https://developers.rdstation.com/en/authentication). @@ -46,7 +60,7 @@ For more details, check the [developers portal](https://developers.rdstation.com #### Getting authentication URL ```ruby -rdstation_authentication = RDStation::Authentication.new('client_id', 'client_secret') +rdstation_authentication = RDStation::Authentication.new redirect_url = 'https://yourapp.org/auth/callback' rdstation_authentication.auth_url(redirect_url) @@ -57,17 +71,35 @@ rdstation_authentication.auth_url(redirect_url) You will need the code param that is returned from RD Station to your application after the user confirms the access at the authorization dialog. ```ruby -rdstation_authentication = RDStation::Authentication.new('client_id', 'client_secret') +rdstation_authentication = RDStation::Authentication.new rdstation_authentication.authenticate(code_returned_from_rdstation) +# => { 'access_token' => '54321', 'expires_in' => 86_400, 'refresh_token' => 'refresh' } ``` -#### Updating access_token +#### Updating an expired access_token ```ruby -rdstation_authentication = RDStation::Authentication.new('client_id', 'client_secret') +rdstation_authentication = RDStation::Authentication.new rdstation_authentication.update_access_token('refresh_token') ``` +**NOTE**: This is done automatically when a request fails due to access_token expiration. To keep track of the new token, you have to provide a callback block in configuration. For example: + +```ruby +RDStation.configure do |config| + config.client_id = YOUR_CLIENT_ID + config.client_secret = YOUR_CLIENT_SECRET + config.on_access_token_refresh do |authorization| + # authorization.access_token_expires_in is the time (in seconds for with the token is valid) + # authorization.access_token is the new token + # authorization.refresh_token is the existing refresh_token + # + # If you are using ActiveRecord, you may want to update the stored access_token, like in the following code: + MyStoredAuth.where(refresh_token: authorization.refresh_token).update_all(access_token: authorization.access_token) + end +end +``` + #### Revoking an access_token ```ruby @@ -83,7 +115,7 @@ Note: this will completely remove your credentials from RD Station (`update_acce Returns data about a specific Contact ```ruby -client = RDStation::Client.new(access_token: 'access_token') +client = RDStation::Client.new(access_token: 'access_token', refresh_token: 'refresh_token') client.contacts.by_uuid('uuid') ``` @@ -94,7 +126,7 @@ More info: https://developers.rdstation.com/pt-BR/reference/contacts#methodGetDe Returns data about a specific Contact ```ruby -client = RDStation::Client.new(access_token: 'access_token') +client = RDStation::Client.new(access_token: 'access_token', refresh_token: 'refresh_token') client.contacts.by_email('email') ``` @@ -109,7 +141,7 @@ contact_info = { name: "Joe Foo" } -client = RDStation::Client.new(access_token: 'access_token') +client = RDStation::Client.new(access_token: 'access_token', refresh_token: 'refresh_token') client.contacts.update('uuid', contact_info) ``` Contact Default Parameters @@ -139,7 +171,7 @@ contact_info = { identifier = "email" identifier_value = "joe@foo.bar" -client = RDStation::Client.new(access_token: 'access_token') +client = RDStation::Client.new(access_token: 'access_token', refresh_token: 'refresh_token') client.contacts.upsert(identifier, identifier_value, contact_info) ``` @@ -159,7 +191,7 @@ This creates a new event on RDSM: ```ruby payload = {} # hash representing the payload -client = RDStation::Client.new(access_token: 'access_token') +client = RDStation::Client.new(access_token: 'access_token', refresh_token: 'refresh_token') client.events.create(payload) ``` @@ -170,7 +202,7 @@ Endpoints to [manage Contact Fields](https://developers.rdstation.com/en/referen #### List all fields ```ruby -client = RDStation::Client.new(access_token: 'access_token') +client = RDStation::Client.new(access_token: 'access_token', refresh_token: 'refresh_token') client.fields.all ``` @@ -183,14 +215,14 @@ Choose to receive data based on certain actions, re-cast or marked as an opportu #### List all webhooks ```ruby -client = RDStation::Client.new(access_token: 'access_token') +client = RDStation::Client.new(access_token: 'access_token', refresh_token: 'refresh_token') client.webhooks.all ``` #### Getting a webhook by UUID ```ruby -client = RDStation::Client.new(access_token: 'access_token') +client = RDStation::Client.new(access_token: 'access_token', refresh_token: 'refresh_token') client.webhooks.by_uuid('WEBHOOK_UUID') ``` @@ -198,7 +230,7 @@ client.webhooks.by_uuid('WEBHOOK_UUID') ```ruby payload = {} # payload representing a webhook -client = RDStation::Client.new(access_token: 'access_token') +client = RDStation::Client.new(access_token: 'access_token', refresh_token: 'refresh_token') client.webhooks.create(payload) ``` @@ -208,7 +240,7 @@ The required strucutre of the payload is [described here](https://developers.rds ```ruby payload = {} # payload representing a webhook -client = RDStation::Client.new(access_token: 'access_token') +client = RDStation::Client.new(access_token: 'access_token', refresh_token: 'refresh_token') client.webhooks.create('WEBHOOK_UUID', payload) ``` @@ -217,7 +249,7 @@ The required strucutre of the payload is [described here](https://developers.rds #### Deleting a webhook ```ruby -client = RDStation::Client.new(access_token: 'access_token') +client = RDStation::Client.new(access_token: 'access_token', refresh_token: 'refresh_token') client.webhooks.delete('WEBHOOK_UUID') ``` diff --git a/lib/rdstation-ruby-client.rb b/lib/rdstation-ruby-client.rb index a0f2dc6..6d34809 100644 --- a/lib/rdstation-ruby-client.rb +++ b/lib/rdstation-ruby-client.rb @@ -4,8 +4,10 @@ require 'rdstation/api_response' # API requests +require 'rdstation' +require 'rdstation/retryable_request' require 'rdstation/authentication' -require 'rdstation/authorization_header' +require 'rdstation/authorization' require 'rdstation/client' require 'rdstation/contacts' require 'rdstation/events' diff --git a/lib/rdstation.rb b/lib/rdstation.rb new file mode 100644 index 0000000..9de5997 --- /dev/null +++ b/lib/rdstation.rb @@ -0,0 +1,19 @@ +module RDStation + class << self + attr_accessor :configuration + + def configure + self.configuration ||= Configuration.new + yield(configuration) + end + end + + class Configuration + attr_accessor :client_id, :client_secret + attr_reader :access_token_refresh_callback + + def on_access_token_refresh(&block) + @access_token_refresh_callback = block + end + end +end diff --git a/lib/rdstation/authentication.rb b/lib/rdstation/authentication.rb index ddf7431..31fefca 100644 --- a/lib/rdstation/authentication.rb +++ b/lib/rdstation/authentication.rb @@ -7,9 +7,10 @@ class Authentication DEFAULT_HEADERS = { 'Content-Type' => 'application/json' }.freeze REVOKE_URL = 'https://api.rd.services/auth/revoke'.freeze - def initialize(client_id, client_secret) - @client_id = client_id - @client_secret = client_secret + def initialize(client_id = nil, client_secret = nil) + warn_deprecation if client_id || client_secret + @client_id = client_id || RDStation.configuration&.client_id + @client_secret = client_secret || RDStation.configuration&.client_secret end # @@ -83,5 +84,9 @@ def post_to_auth_endpoint(params) headers: DEFAULT_HEADERS ) end + + def warn_deprecation + warn "DEPRECATION WARNING: Providing client_id and client_secret directly to RDStation::Authentication.new is deprecated and will be removed in future versions. Use RDStation.configure instead." + end end end diff --git a/lib/rdstation/authorization_header.rb b/lib/rdstation/authorization.rb similarity index 55% rename from lib/rdstation/authorization_header.rb rename to lib/rdstation/authorization.rb index 2a41e31..0e80e39 100644 --- a/lib/rdstation/authorization_header.rb +++ b/lib/rdstation/authorization.rb @@ -1,21 +1,24 @@ module RDStation - class AuthorizationHeader - - def initialize(access_token:) + class Authorization + attr_reader :refresh_token + attr_accessor :access_token, :access_token_expires_in + def initialize(access_token:, refresh_token: nil, access_token_expires_in: nil) @access_token = access_token + @refresh_token = refresh_token + @access_token_expires_in = access_token_expires_in validate_access_token access_token end - - def to_h + + def headers { "Authorization" => "Bearer #{@access_token}", "Content-Type" => "application/json" } end - + private - + def validate_access_token(access_token) access_token_msg = ':access_token is required' raise ArgumentError, access_token_msg unless access_token end - + end end \ No newline at end of file diff --git a/lib/rdstation/client.rb b/lib/rdstation/client.rb index 63c956b..7684b7b 100644 --- a/lib/rdstation/client.rb +++ b/lib/rdstation/client.rb @@ -1,23 +1,33 @@ module RDStation class Client - def initialize(access_token:) - @authorization_header = AuthorizationHeader.new(access_token: access_token) + def initialize(access_token:, refresh_token: nil) + warn_deprecation unless refresh_token + @authorization = Authorization.new( + access_token: access_token, + refresh_token: refresh_token + ) end - + def contacts - @contacts ||= RDStation::Contacts.new(authorization_header: @authorization_header) + @contacts ||= RDStation::Contacts.new(authorization: @authorization) end def events - @events ||= RDStation::Events.new(authorization_header: @authorization_header) + @events ||= RDStation::Events.new(authorization: @authorization) end def fields - @fields ||= RDStation::Fields.new(authorization_header: @authorization_header) + @fields ||= RDStation::Fields.new(authorization: @authorization) end def webhooks - @webhooks ||= RDStation::Webhooks.new(authorization_header: @authorization_header) + @webhooks ||= RDStation::Webhooks.new(authorization: @authorization) + end + + private + + def warn_deprecation + warn "DEPRECATION WARNING: Specifying refresh_token in RDStation::Client.new(access_token: 'at', refresh_token: 'rt') is optional right now, but will be mandatory in future versions. " end end end diff --git a/lib/rdstation/contacts.rb b/lib/rdstation/contacts.rb index 6668983..957f0e3 100644 --- a/lib/rdstation/contacts.rb +++ b/lib/rdstation/contacts.rb @@ -3,9 +3,10 @@ module RDStation # More info: https://developers.rdstation.com/pt-BR/reference/contacts class Contacts include HTTParty - - def initialize(authorization_header:) - @authorization_header = authorization_header + include ::RDStation::RetryableRequest + + def initialize(authorization:) + @authorization = authorization end # @@ -13,13 +14,17 @@ def initialize(authorization_header:) # The unique uuid associated to each RD Station Contact. # def by_uuid(uuid) - response = self.class.get(base_url(uuid), headers: @authorization_header.to_h) - ApiResponse.build(response) + retryable_request(@authorization) do |authorization| + response = self.class.get(base_url(uuid), headers: authorization.headers) + ApiResponse.build(response) + end end def by_email(email) - response = self.class.get(base_url("email:#{email}"), headers: @authorization_header.to_h) - ApiResponse.build(response) + retryable_request(@authorization) do |authorization| + response = self.class.get(base_url("email:#{email}"), headers: authorization.headers) + ApiResponse.build(response) + end end # The Contact hash may contain the following parameters: @@ -34,8 +39,10 @@ def by_email(email) # :website # :tags def update(uuid, contact_hash) - response = self.class.patch(base_url(uuid), :body => contact_hash.to_json, :headers => @authorization_header.to_h) - ApiResponse.build(response) + retryable_request(@authorization) do |authorization| + response = self.class.patch(base_url(uuid), :body => contact_hash.to_json, :headers => authorization.headers) + ApiResponse.build(response) + end end # @@ -47,14 +54,16 @@ def update(uuid, contact_hash) # Contact data # def upsert(identifier, identifier_value, contact_hash) - path = "#{identifier}:#{identifier_value}" - response = self.class.patch(base_url(path), body: contact_hash.to_json, headers: @authorization_header.to_h) - ApiResponse.build(response) + retryable_request(@authorization) do |authorization| + path = "#{identifier}:#{identifier_value}" + response = self.class.patch(base_url(path), body: contact_hash.to_json, headers: authorization.headers) + ApiResponse.build(response) + end end private - def base_url(path = "") + def base_url(path = '') "https://api.rd.services/platform/contacts/#{path}" end end diff --git a/lib/rdstation/events.rb b/lib/rdstation/events.rb index bb6d4bb..420368a 100644 --- a/lib/rdstation/events.rb +++ b/lib/rdstation/events.rb @@ -1,18 +1,21 @@ module RDStation class Events include HTTParty + include ::RDStation::RetryableRequest EVENTS_ENDPOINT = 'https://api.rd.services/platform/events'.freeze - def initialize(authorization_header:) - @authorization_header = authorization_header + def initialize(authorization:) + @authorization = authorization end def create(payload) - response = self.class.post(EVENTS_ENDPOINT, headers: @authorization_header.to_h, body: payload.to_json) - response_body = JSON.parse(response.body) - return response_body unless errors?(response_body) - RDStation::ErrorHandler.new(response).raise_error + retryable_request(@authorization) do |authorization| + response = self.class.post(EVENTS_ENDPOINT, headers: authorization.headers, body: payload.to_json) + response_body = JSON.parse(response.body) + return response_body unless errors?(response_body) + RDStation::ErrorHandler.new(response).raise_error + end end private diff --git a/lib/rdstation/fields.rb b/lib/rdstation/fields.rb index 2a7b5a0..130fde9 100644 --- a/lib/rdstation/fields.rb +++ b/lib/rdstation/fields.rb @@ -3,16 +3,19 @@ module RDStation # More info: https://developers.rdstation.com/pt-BR/reference/contacts class Fields include HTTParty + include ::RDStation::RetryableRequest BASE_URL = 'https://api.rd.services/platform/contacts/fields'.freeze - def initialize(authorization_header:) - @authorization_header = authorization_header + def initialize(authorization:) + @authorization = authorization end def all - response = self.class.get(BASE_URL, headers: @authorization_header.to_h) - ApiResponse.build(response) + retryable_request(@authorization) do |authorization| + response = self.class.get(BASE_URL, headers: authorization.headers) + ApiResponse.build(response) + end end end diff --git a/lib/rdstation/retryable_request.rb b/lib/rdstation/retryable_request.rb new file mode 100644 index 0000000..650fbf1 --- /dev/null +++ b/lib/rdstation/retryable_request.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module RDStation + module RetryableRequest + MAX_RETRIES = 1 + def retryable_request(authorization) + retries = 0 + begin + yield authorization + rescue ::RDStation::Error::ExpiredAccessToken => e + raise if !retry_possible?(authorization) || retries >= MAX_RETRIES + + retries += 1 + refresh_access_token(authorization) + retry + end + end + + def retry_possible?(authorization) + [ + RDStation.configuration&.client_id, + RDStation.configuration&.client_secret, + authorization.refresh_token + ].all? + end + + def refresh_access_token(authorization) + client = RDStation::Authentication.new + response = client.update_access_token(authorization.refresh_token) + authorization.access_token = response['access_token'] + authorization.access_token_expires_in = response['expires_in'] + RDStation.configuration&.access_token_refresh_callback&.call(authorization) + end + end +end diff --git a/lib/rdstation/version.rb b/lib/rdstation/version.rb index ac8b43f..55df37d 100644 --- a/lib/rdstation/version.rb +++ b/lib/rdstation/version.rb @@ -1,3 +1,3 @@ module RDStation - VERSION = '2.1.1'.freeze + VERSION = '2.2.0'.freeze end diff --git a/lib/rdstation/webhooks.rb b/lib/rdstation/webhooks.rb index daa8c0d..f3f2330 100644 --- a/lib/rdstation/webhooks.rb +++ b/lib/rdstation/webhooks.rb @@ -1,35 +1,47 @@ module RDStation class Webhooks include HTTParty + include ::RDStation::RetryableRequest - def initialize(authorization_header:) - @authorization_header = authorization_header + def initialize(authorization:) + @authorization = authorization end def all - response = self.class.get(base_url, headers: @authorization_header.to_h) - ApiResponse.build(response) + retryable_request(@authorization) do |authorization| + response = self.class.get(base_url, headers: authorization.headers) + ApiResponse.build(response) + end end def by_uuid(uuid) - response = self.class.get(base_url(uuid), headers: @authorization_header.to_h) - ApiResponse.build(response) + retryable_request(@authorization) do |authorization| + response = self.class.get(base_url(uuid), headers: authorization.headers) + ApiResponse.build(response) + end end def create(payload) - response = self.class.post(base_url, headers: @authorization_header.to_h, body: payload.to_json) - ApiResponse.build(response) + retryable_request(@authorization) do |authorization| + response = self.class.post(base_url, headers: authorization.headers, body: payload.to_json) + ApiResponse.build(response) + end end def update(uuid, payload) - response = self.class.put(base_url(uuid), headers: @authorization_header.to_h, body: payload.to_json) - ApiResponse.build(response) + retryable_request(@authorization) do |authorization| + response = self.class.put(base_url(uuid), headers: authorization.headers, body: payload.to_json) + ApiResponse.build(response) + end end def delete(uuid) - response = self.class.delete(base_url(uuid), headers: @authorization_header.to_h) - return webhook_deleted_message unless response.body - RDStation::ErrorHandler.new(response).raise_error + retryable_request(@authorization) do |authorization| + response = self.class.delete(base_url(uuid), headers: authorization.headers) + return webhook_deleted_message unless response.body + + RDStation::ErrorHandler.new(response).raise_error + end end private diff --git a/rdstation-ruby-client.gemspec b/rdstation-ruby-client.gemspec index 72ce516..eafd1ec 100644 --- a/rdstation-ruby-client.gemspec +++ b/rdstation-ruby-client.gemspec @@ -20,12 +20,13 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 2.0.0' - spec.add_development_dependency "bundler", "~> 1.3" + spec.add_development_dependency "bundler", "> 1.3" spec.add_development_dependency "rake" spec.add_development_dependency 'rspec' spec.add_development_dependency 'webmock', '~> 2.1' spec.add_development_dependency 'turn' spec.add_development_dependency 'rspec_junit_formatter' + spec.add_development_dependency 'pry' spec.add_dependency "httparty", "~> 0.12" end diff --git a/spec/lib/rdstation/authentication_spec.rb b/spec/lib/rdstation/authentication_spec.rb index 05aebac..8c85420 100644 --- a/spec/lib/rdstation/authentication_spec.rb +++ b/spec/lib/rdstation/authentication_spec.rb @@ -88,6 +88,34 @@ let(:authentication) { described_class.new('client_id', 'client_secret') } + describe '#auth_url' do + let(:configuration_client_id) { 'configuration_client_id' } + let(:configuration_client_secret) { 'configuration_client_secret' } + let(:redirect_url) { 'redirect_url' } + before do + RDStation.configure do |config| + config.client_id = configuration_client_id + config.client_secret = configuration_client_secret + end + end + + context 'when client_id and client_secret are specified in initialization' do + it 'uses those specified in initialization' do + auth = described_class.new('initialization_client_id', 'initialization_client_secret') + expected = "https://api.rd.services/auth/dialog?client_id=initialization_client_id&redirect_url=#{redirect_url}" + expect(auth.auth_url(redirect_url)).to eq expected + end + end + + context 'when client_id and client_secret are specified only in configuration' do + it 'uses those specified in configuration' do + auth = described_class.new + expected = "https://api.rd.services/auth/dialog?client_id=#{configuration_client_id}&redirect_url=#{redirect_url}" + expect(auth.auth_url(redirect_url)).to eq expected + end + end + end + describe '#authenticate' do context 'when the code is valid' do before do @@ -138,6 +166,37 @@ end.to raise_error(RDStation::Error::ExpiredCodeGrant) end end + + context 'when client_id and client_secret are specified only in configuration' do + let(:authentication) { described_class.new } + let(:configuration_client_id) { 'configuration_client_id' } + let(:configuration_client_secret) { 'configuration_client_secret' } + let(:token_request_with_valid_code_secrets_from_config) do + { + client_id: configuration_client_id, + client_secret: configuration_client_secret, + code: 'valid_code' + } + end + before do + RDStation.configure do |config| + config.client_id = configuration_client_id + config.client_secret = configuration_client_secret + end + + stub_request(:post, token_endpoint) + .with( + headers: request_headers, + body: token_request_with_valid_code_secrets_from_config.to_json + ) + .to_return(credentials_response) + end + + it 'returns the credentials' do + credentials_request = authentication.authenticate('valid_code') + expect(credentials_request).to eq(credentials) + end + end end describe '#update_access_token' do @@ -173,6 +232,37 @@ end.to raise_error(RDStation::Error::InvalidCredentials) end end + + context 'when client_id and client_secret are specified only in configuration' do + let(:authentication) { described_class.new } + let(:configuration_client_id) { 'configuration_client_id' } + let(:configuration_client_secret) { 'configuration_client_secret' } + let(:token_request_with_valid_refresh_code_secrets_from_config) do + { + client_id: configuration_client_id, + client_secret: configuration_client_secret, + refresh_token: 'valid_refresh_token' + } + end + before do + RDStation.configure do |config| + config.client_id = configuration_client_id + config.client_secret = configuration_client_secret + end + + stub_request(:post, token_endpoint) + .with( + headers: request_headers, + body: token_request_with_valid_refresh_code_secrets_from_config.to_json + ) + .to_return(credentials_response) + end + + it 'returns the credentials' do + credentials_request = authentication.update_access_token('valid_refresh_token') + expect(credentials_request).to eq(credentials) + end + end end describe ".revoke" do diff --git a/spec/lib/rdstation/authorization_header_spec.rb b/spec/lib/rdstation/authorization_spec.rb similarity index 78% rename from spec/lib/rdstation/authorization_header_spec.rb rename to spec/lib/rdstation/authorization_spec.rb index 436e149..7d3a28d 100644 --- a/spec/lib/rdstation/authorization_header_spec.rb +++ b/spec/lib/rdstation/authorization_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -RSpec.describe RDStation::AuthorizationHeader do +RSpec.describe RDStation::Authorization do describe ".initialize" do context "when access_token is nil" do @@ -12,11 +12,11 @@ end end - describe "#to_h" do + describe "#headers" do let(:access_token) { 'access_token' } it "generates the correct header" do - header = described_class.new(access_token: access_token).to_h + header = described_class.new(access_token: access_token).headers expect(header['Authorization']).to eq "Bearer #{access_token}" expect(header['Content-Type']).to eq "application/json" end diff --git a/spec/lib/rdstation/client_spec.rb b/spec/lib/rdstation/client_spec.rb index f0aeb68..4d23eb0 100644 --- a/spec/lib/rdstation/client_spec.rb +++ b/spec/lib/rdstation/client_spec.rb @@ -4,27 +4,27 @@ context "when access_token is given" do let(:access_token) { 'access_token' } let(:client) { described_class.new(access_token: access_token) } - let(:mock_authorization_header) { double(RDStation::AuthorizationHeader) } + let(:mock_authorization) { double(RDStation::Authorization) } - before { allow(RDStation::AuthorizationHeader).to receive(:new).and_return mock_authorization_header } + before { allow(RDStation::Authorization).to receive(:new).and_return mock_authorization } it 'returns Contacts endpoint' do - expect(RDStation::Contacts).to receive(:new).with({ authorization_header: mock_authorization_header }).and_call_original + expect(RDStation::Contacts).to receive(:new).with({ authorization: mock_authorization }).and_call_original expect(client.contacts).to be_instance_of RDStation::Contacts end it 'returns Events endpoint' do - expect(RDStation::Events).to receive(:new).with({ authorization_header: mock_authorization_header }).and_call_original + expect(RDStation::Events).to receive(:new).with({ authorization: mock_authorization }).and_call_original expect(client.events).to be_instance_of RDStation::Events end it 'returns Fields endpoint' do - expect(RDStation::Fields).to receive(:new).with({ authorization_header: mock_authorization_header }).and_call_original + expect(RDStation::Fields).to receive(:new).with({ authorization: mock_authorization }).and_call_original expect(client.fields).to be_instance_of RDStation::Fields end it 'returns Webhooks endpoint' do - expect(RDStation::Webhooks).to receive(:new).with({ authorization_header: mock_authorization_header }).and_call_original + expect(RDStation::Webhooks).to receive(:new).with({ authorization: mock_authorization }).and_call_original expect(client.webhooks).to be_instance_of RDStation::Webhooks end end diff --git a/spec/lib/rdstation/contacts_spec.rb b/spec/lib/rdstation/contacts_spec.rb index 677e7f5..514e8e6 100644 --- a/spec/lib/rdstation/contacts_spec.rb +++ b/spec/lib/rdstation/contacts_spec.rb @@ -16,13 +16,13 @@ let(:expired_access_token) { 'expired_access_token' } let(:contact_with_valid_token) do - described_class.new(authorization_header: RDStation::AuthorizationHeader.new(access_token: valid_access_token)) + described_class.new(authorization: RDStation::Authorization.new(access_token: valid_access_token)) end let(:contact_with_expired_token) do - described_class.new(authorization_header: RDStation::AuthorizationHeader.new(access_token: expired_access_token)) + described_class.new(authorization: RDStation::Authorization.new(access_token: expired_access_token)) end let(:contact_with_invalid_token) do - described_class.new(authorization_header: RDStation::AuthorizationHeader.new(access_token: invalid_access_token)) + described_class.new(authorization: RDStation::Authorization.new(access_token: invalid_access_token)) end @@ -109,6 +109,11 @@ end describe '#by_uuid' do + it 'calls retryable_request' do + expect(contact_with_valid_token).to receive(:retryable_request) + contact_with_valid_token.by_uuid('valid_uuid') + end + context 'with a valid auth token' do context 'when the contact exists' do let(:contact) do @@ -172,6 +177,11 @@ end describe '#by_email' do + it 'calls retryable_request' do + expect(contact_with_valid_token).to receive(:retryable_request) + contact_with_valid_token.by_email('x@xpto.com') + end + context 'with a valid auth token' do context 'when the contact exists' do let(:contact) do @@ -235,6 +245,11 @@ end describe '#update' do + it 'calls retryable_request' do + expect(contact_with_valid_token).to receive(:retryable_request) + contact_with_valid_token.update('valid_uuid', {}) + end + context 'with a valid access_token' do let(:valid_access_token) { 'valid_access_token' } let(:headers) do @@ -322,6 +337,11 @@ end describe '#upsert' do + it 'calls retryable_request' do + expect(contact_with_valid_token).to receive(:retryable_request) + contact_with_valid_token.upsert('email', 'valid@email.com', {}) + end + context 'with a valid access_token' do let(:valid_access_token) { 'valid_access_token' } diff --git a/spec/lib/rdstation/events_spec.rb b/spec/lib/rdstation/events_spec.rb index a81a2af..3f6a0a8 100644 --- a/spec/lib/rdstation/events_spec.rb +++ b/spec/lib/rdstation/events_spec.rb @@ -6,13 +6,13 @@ let(:expired_access_token) { 'expired_access_token' } let(:event_with_valid_token) do - described_class.new(authorization_header: RDStation::AuthorizationHeader.new(access_token: valid_access_token)) + described_class.new(authorization: RDStation::Authorization.new(access_token: valid_access_token)) end let(:event_with_expired_token) do - described_class.new(authorization_header: RDStation::AuthorizationHeader.new(access_token: expired_access_token)) + described_class.new(authorization: RDStation::Authorization.new(access_token: expired_access_token)) end let(:event_with_invalid_token) do - described_class.new(authorization_header: RDStation::AuthorizationHeader.new(access_token: invalid_access_token)) + described_class.new(authorization: RDStation::Authorization.new(access_token: invalid_access_token)) end let(:events_endpoint) { 'https://api.rd.services/platform/events' } @@ -108,6 +108,11 @@ } end + it 'calls retryable_request' do + expect(event_with_valid_token).to receive(:retryable_request) + event_with_valid_token.create({}) + end + context 'with a valid auth token' do before do stub_request(:post, events_endpoint) diff --git a/spec/lib/rdstation/fields_spec.rb b/spec/lib/rdstation/fields_spec.rb index 9769ad6..511110a 100644 --- a/spec/lib/rdstation/fields_spec.rb +++ b/spec/lib/rdstation/fields_spec.rb @@ -3,7 +3,7 @@ RSpec.describe RDStation::Fields do let(:valid_access_token) { 'valid_access_token' } let(:rdstation_fields_with_valid_token) do - described_class.new(authorization_header: RDStation::AuthorizationHeader.new(access_token: valid_access_token)) + described_class.new(authorization: RDStation::Authorization.new(access_token: valid_access_token)) end let(:valid_headers) do @@ -38,6 +38,11 @@ } end + it 'calls retryable_request' do + expect(rdstation_fields_with_valid_token).to receive(:retryable_request) + rdstation_fields_with_valid_token.all + end + context 'with a valid auth token' do before do stub_request(:get, fields_endpoint) diff --git a/spec/lib/rdstation/retryable_request_spec.rb b/spec/lib/rdstation/retryable_request_spec.rb new file mode 100644 index 0000000..73a8c59 --- /dev/null +++ b/spec/lib/rdstation/retryable_request_spec.rb @@ -0,0 +1,142 @@ +require 'spec_helper' + +class DummyClass + include ::RDStation::RetryableRequest +end + +RSpec.describe RDStation::RetryableRequest do + let(:subject) { DummyClass.new } + describe '.retryable_request' do + context 'when authorization has a valid refresh_token and config is provided' do + let (:access_token) { 'access_token' } + let (:new_access_token) { 'new_access_token' } + let (:refresh_token) { 'refresh_token' } + let (:auth) do + ::RDStation::Authorization.new(access_token: access_token, + refresh_token: refresh_token + ) + end + context 'original request was successful' do + it 'yields control to the given block' do + expect do |block| + subject.retryable_request(auth, &block) + end.to yield_with_args(auth) + end + end + + context 'original request raised a retryable exception' do + let (:auth_new_access_token) do + ::RDStation::Authorization.new(access_token: new_access_token, + refresh_token: refresh_token + ) + end + + let(:new_credentials) do + { + 'access_token' => new_access_token, + 'expires_in' => 86_400, + 'refresh_token' => refresh_token + } + end + let(:authentication_client) {instance_double(::RDStation::Authentication) } + + before do + RDStation.configure do |config| + config.client_id = "123" + config.client_secret = "312" + config.on_access_token_refresh do + 'callback code' + end + end + allow(::RDStation::Authentication).to receive(:new) + .with(no_args) + .and_return(authentication_client) + allow(authentication_client).to receive(:update_access_token) + .with(auth.refresh_token). + and_return(new_credentials) + end + + it 'refreshes the access_token and retries the request' do + dummy_request = double("dummy_request") + expect(dummy_request).to receive(:call).twice do |auth| + expired_token = ::RDStation::Error::ExpiredAccessToken.new({'error_message' => 'x'}) + raise expired_token unless auth.access_token == new_access_token + end + + expect(RDStation.configuration.access_token_refresh_callback) + .to receive(:call) + .once do |authorization| + expect(authorization.access_token).to eq new_access_token + end + + expect do + subject.retryable_request(auth) { |yielded_auth| dummy_request.call(yielded_auth) } + end.not_to raise_error + end + + context 'and keeps raising retryable exception event after token refreshed' do + it 'retries only once' do + dummy_request = double("dummy_request") + expect(dummy_request).to receive(:call).twice do |_| + raise ::RDStation::Error::ExpiredAccessToken.new({'error_message' => 'x'}) + end + + expect do + subject.retryable_request(auth) { |yielded_auth| dummy_request.call(yielded_auth) } + end.to raise_error ::RDStation::Error::ExpiredAccessToken + end + end + + context 'and access token refresh callback is not set' do + before do + RDStation.configure do |config| + config.on_access_token_refresh(&nil) + end + end + + it 'executes the refresh and retry without raising an error' do + dummy_request = double("dummy_request") + expect(dummy_request).to receive(:call).twice do |auth| + expired_token = ::RDStation::Error::ExpiredAccessToken.new({'error_message' => 'x'}) + raise expired_token unless auth.access_token == new_access_token + end + + expect do + subject.retryable_request(auth) { |yielded_auth| dummy_request.call(yielded_auth) } + end.not_to raise_error + end + end + end + + context 'original request raised a non retryable exception' do + it 'raises error' do + dummy_request = double("dummy_request") + expect(dummy_request).to receive(:call).once do |_| + raise RuntimeError.new("a non retryable error") + end + + expect do + subject.retryable_request(auth) { |yielded_auth| dummy_request.call(yielded_auth) } + end.to raise_error RuntimeError + end + end + end + + context 'all legacy scenarios' do + let (:access_token) { 'access_token' } + let (:auth) { ::RDStation::Authorization.new(access_token: access_token) } + + it 'implement me' do + dummy_request = double("dummy_request") + expect(dummy_request).to receive(:call).once do |_| + raise ::RDStation::Error::ExpiredAccessToken.new({'error_message' => 'x'}) + end + + expect do + subject.retryable_request(auth) { |yielded_auth| dummy_request.call(yielded_auth) } + end.to raise_error ::RDStation::Error::ExpiredAccessToken + end + end + + end +end diff --git a/spec/lib/rdstation/webhooks_spec.rb b/spec/lib/rdstation/webhooks_spec.rb index f42839f..9b69ea8 100644 --- a/spec/lib/rdstation/webhooks_spec.rb +++ b/spec/lib/rdstation/webhooks_spec.rb @@ -2,7 +2,7 @@ RSpec.describe RDStation::Webhooks do let(:webhooks_client) do - described_class.new(authorization_header: RDStation::AuthorizationHeader.new(access_token: 'access_token')) + described_class.new(authorization: RDStation::Authorization.new(access_token: 'access_token')) end let(:webhooks_endpoint) { 'https://api.rd.services/integrations/webhooks/' } @@ -23,6 +23,11 @@ end describe '#all' do + it 'calls retryable_request' do + expect(webhooks_client).to receive(:retryable_request) + webhooks_client.all + end + context 'when the request is successful' do let(:webhooks) do { @@ -77,6 +82,11 @@ let(:uuid) { '5408c5a3-4711-4f2e-8d0b-13407a3e30f3' } let(:webhooks_endpoint_by_uuid) { webhooks_endpoint + uuid } + it 'calls retryable_request' do + expect(webhooks_client).to receive(:retryable_request) + webhooks_client.by_uuid('uuid') + end + context 'when the request is successful' do let(:webhook) do { @@ -126,6 +136,11 @@ } end + it 'calls retryable_request' do + expect(webhooks_client).to receive(:retryable_request) + webhooks_client.create('payload') + end + context 'when the request is successful' do let(:webhook) do { @@ -177,6 +192,11 @@ } end + it 'calls retryable_request' do + expect(webhooks_client).to receive(:retryable_request) + webhooks_client.update('uuid', 'payload') + end + context 'when the request is successful' do let(:updated_webhook) do { @@ -219,6 +239,11 @@ let(:uuid) { '5408c5a3-4711-4f2e-8d0b-13407a3e30f3' } let(:webhooks_endpoint_by_uuid) { webhooks_endpoint + uuid } + it 'calls retryable_request' do + expect(webhooks_client).to receive(:retryable_request) + webhooks_client.delete('uuid') + end + context 'when the request is successful' do before do stub_request(:delete, webhooks_endpoint_by_uuid).with(headers: headers).to_return(status: 204) diff --git a/spec/lib/rdstation_spec.rb b/spec/lib/rdstation_spec.rb new file mode 100644 index 0000000..c19eeed --- /dev/null +++ b/spec/lib/rdstation_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +RSpec.describe RDStation do + describe '.configure' do + let(:client_id) { 'client_id' } + let(:client_secret) { 'client_secret' } + + it 'sets the configuration' do + RDStation.configure do |config| + config.client_id = client_id + config.client_secret = client_secret + end + + expect(RDStation.configuration.client_id).to eq client_id + expect(RDStation.configuration.client_secret).to eq client_secret + end + end +end