finapps_core 2.0.2

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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +21 -0
  3. data/.gitignore +53 -0
  4. data/.hound.yml +2 -0
  5. data/.rspec +4 -0
  6. data/.rubocop.yml +271 -0
  7. data/.ruby-gemset +1 -0
  8. data/.ruby-version +1 -0
  9. data/.travis.yml +14 -0
  10. data/Gemfile +3 -0
  11. data/Gemfile.lock +98 -0
  12. data/LICENSE +21 -0
  13. data/README.md +21 -0
  14. data/Rakefile +2 -0
  15. data/finapps_core.gemspec +39 -0
  16. data/lib/core_extensions/object/blank.rb +69 -0
  17. data/lib/core_extensions/object/is_integer.rb +10 -0
  18. data/lib/core_extensions/string/json_to_hash.rb +10 -0
  19. data/lib/finapps_core.rb +30 -0
  20. data/lib/finapps_core/error.rb +17 -0
  21. data/lib/finapps_core/middleware/middleware.rb +22 -0
  22. data/lib/finapps_core/middleware/request/accept_json.rb +14 -0
  23. data/lib/finapps_core/middleware/request/no_encoding_basic_authentication.rb +21 -0
  24. data/lib/finapps_core/middleware/request/tenant_authentication.rb +20 -0
  25. data/lib/finapps_core/middleware/request/user_agent.rb +15 -0
  26. data/lib/finapps_core/middleware/response/custom_logger.rb +39 -0
  27. data/lib/finapps_core/middleware/response/raise_error.rb +46 -0
  28. data/lib/finapps_core/rest/base_client.rb +118 -0
  29. data/lib/finapps_core/rest/configuration.rb +32 -0
  30. data/lib/finapps_core/rest/connection.rb +35 -0
  31. data/lib/finapps_core/rest/credentials.rb +21 -0
  32. data/lib/finapps_core/rest/defaults.rb +19 -0
  33. data/lib/finapps_core/rest/resources.rb +62 -0
  34. data/lib/finapps_core/utils/loggeable.rb +14 -0
  35. data/lib/finapps_core/utils/parameter_filter.rb +32 -0
  36. data/lib/finapps_core/version.rb +4 -0
  37. data/lib/tasks/releaser.rake +9 -0
  38. data/spec/core_extensions/object/blank_spec.rb +44 -0
  39. data/spec/core_extensions/object/is_integer_spec.rb +17 -0
  40. data/spec/middleware/request/accept_json_spec.rb +12 -0
  41. data/spec/middleware/request/no_encoding_basic_authentication_spec.rb +32 -0
  42. data/spec/middleware/request/tenant_authentication_spec.rb +34 -0
  43. data/spec/middleware/request/user_agent_spec.rb +12 -0
  44. data/spec/middleware/response/raise_error_spec.rb +24 -0
  45. data/spec/rest/base_client_spec.rb +110 -0
  46. data/spec/rest/configuration_spec.rb +43 -0
  47. data/spec/rest/credentials_spec.rb +20 -0
  48. data/spec/rest/relevance_ruleset_names.json +47 -0
  49. data/spec/rest/timeout_spec.rb +7 -0
  50. data/spec/spec_helper.rb +37 -0
  51. data/spec/spec_helpers/client.rb +8 -0
  52. data/spec/support/fake_api.rb +34 -0
  53. data/spec/support/fixtures/error.json +5 -0
  54. data/spec/support/fixtures/relevance_ruleset_names.json +47 -0
  55. data/spec/support/fixtures/resource.json +3 -0
  56. data/spec/support/fixtures/resource_not_found.json +5 -0
  57. data/spec/support/fixtures/resources.json +11 -0
  58. data/spec/support/fixtures/unauthorized.json +5 -0
  59. data/spec/utils/parameter_filter_spec.rb +23 -0
  60. metadata +353 -0
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ module FinAppsCore
3
+ module REST
4
+ # Represents the client configuration options
5
+ class Configuration # :nodoc:
6
+ using ObjectExtensions
7
+
8
+ attr_accessor :host,
9
+ :tenant_identifier, :tenant_token,
10
+ :user_identifier, :user_token,
11
+ :proxy, :timeout, :retry_limit, :log_level
12
+
13
+ def initialize(options={})
14
+ non_nil_options = options.select {|_, value| !value.nil? }
15
+ FinAppsCore::REST::Defaults::DEFAULTS.merge(non_nil_options)
16
+ .each {|key, value| public_send("#{key}=", value) }
17
+ raise FinAppsCore::InvalidArgumentsError.new "Invalid argument. {host: #{host}}" unless valid_host?
18
+ raise FinAppsCore::InvalidArgumentsError.new "Invalid argument. {timeout: #{timeout}}" unless timeout.integer?
19
+ end
20
+
21
+ def valid_user_credentials?
22
+ FinAppsCore::REST::Credentials.new(user_identifier, user_token).valid?
23
+ end
24
+
25
+ private
26
+
27
+ def valid_host?
28
+ host.start_with?('http://', 'https://')
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+ module FinAppsCore
3
+ module REST
4
+ module Connection # :nodoc:
5
+ # @return [Faraday::Connection]
6
+ def faraday(config, logger)
7
+ options = {
8
+ url: "#{config.host}/v#{Defaults::API_VERSION}/",
9
+ request: {open_timeout: config.timeout,
10
+ timeout: config.timeout}
11
+ }
12
+
13
+ Faraday.new(options) do |conn|
14
+ conn.request :accept_json
15
+ conn.request :user_agent
16
+ conn.request :tenant_authentication, config.tenant_identifier, config.tenant_token
17
+ conn.request :json
18
+ conn.request :retry
19
+ conn.request :multipart
20
+ conn.request :url_encoded
21
+ conn.request :no_encoding_basic_authentication, config.user_token if config.valid_user_credentials?
22
+
23
+ conn.use FinAppsCore::Middleware::RaiseError
24
+ conn.response :rashify
25
+ conn.response :json, content_type: /\bjson$/
26
+ conn.response :custom_logger, logger, bodies: (ENV['SILENT_LOG_BODIES'] != 'true')
27
+
28
+ # Adapter (ensure that the adapter is always last.)
29
+ conn.adapter :typhoeus
30
+ end
31
+ end
32
+ module_function :faraday # becomes available as a *private instance method* to classes that mix in the module
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ module FinAppsCore
3
+ module REST
4
+ # represents both tenant and user credentials
5
+ class Credentials
6
+ using ObjectExtensions
7
+ using StringExtensions
8
+
9
+ attr_reader :identifier, :token
10
+
11
+ def initialize(identifier, token)
12
+ @identifier = identifier
13
+ @token = token
14
+ end
15
+
16
+ def valid?
17
+ identifier.present? && token.present?
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ module FinAppsCore
3
+ module REST
4
+ module Defaults
5
+ API_VERSION = '2'
6
+
7
+ # noinspection SpellCheckingInspection
8
+ DEFAULTS = {
9
+ host: 'https://api.financialapps.com',
10
+ user_identifier: nil,
11
+ user_token: nil,
12
+ timeout: 30,
13
+ proxy: nil,
14
+ retry_limit: 1,
15
+ log_level: Logger::INFO
16
+ }.freeze
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+ module FinAppsCore
3
+ module REST
4
+ class Resources # :nodoc:
5
+ include FinAppsCore::Utils::ParameterFilter
6
+ require 'erb'
7
+
8
+ attr_reader :client
9
+
10
+ # @param [FinAppsCore::REST::Client] client
11
+ # @return [FinAppsCore::REST::Resources]
12
+ def initialize(client)
13
+ raise MissingArgumentsError.new 'Missing argument: client.' if client.nil?
14
+ @client = client
15
+ end
16
+
17
+ def list(path=nil)
18
+ path = end_point.to_s if path.nil?
19
+ request_with_body(path, :get, {})
20
+ end
21
+
22
+ def create(params={}, path=nil)
23
+ request_with_body(path, :post, params)
24
+ end
25
+
26
+ def update(params={}, path=nil)
27
+ request_with_body(path, :put, params)
28
+ end
29
+
30
+ def show(id=nil, path=nil)
31
+ request_without_body(path, :get, id)
32
+ end
33
+
34
+ def destroy(id=nil, path=nil)
35
+ request_without_body(path, :delete, id)
36
+ end
37
+
38
+ def request_without_body(path, method, id)
39
+ raise MissingArgumentsError.new 'Missing argument: id.' if id.nil? && path.nil?
40
+ path = "#{end_point}/:id".sub ':id', ERB::Util.url_encode(id) if path.nil?
41
+ request_with_body path, method, {}
42
+ end
43
+
44
+ def request_with_body(path, method, params)
45
+ path = end_point if path.nil?
46
+ logger.debug "#{self.class.name}##{__method__} => path: #{path} params: #{skip_sensitive_data(params)}"
47
+
48
+ client.send_request path, method, params
49
+ end
50
+
51
+ protected
52
+
53
+ def logger
54
+ client.logger
55
+ end
56
+
57
+ def end_point
58
+ self.class.name.split('::').last.downcase
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ module FinAppsCore
3
+ module Utils
4
+ # Adds logging capabilities when included into other classes
5
+ module Loggeable
6
+ def logger
7
+ @logger ||= begin
8
+ require 'logger'
9
+ ::Logger.new(STDOUT)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ module FinAppsCore
3
+ module Utils
4
+ module ParameterFilter
5
+ using StringExtensions
6
+ PROTECTED_KEYS = %w(login login1 username password password1 password_confirm token
7
+ x-finapps-token authorization).freeze
8
+
9
+ def skip_sensitive_data(hash)
10
+ if hash.is_a? String
11
+ hash = hash.json_to_hash
12
+ end
13
+ if hash.is_a? Hash
14
+ filtered_hash = hash.clone
15
+ filtered_hash.each do |key, value|
16
+ if PROTECTED_KEYS.include? key.to_s.downcase
17
+ filtered_hash[key] = '[REDACTED]'
18
+ elsif value.is_a?(Hash)
19
+ filtered_hash[key] = skip_sensitive_data(value)
20
+ elsif value.is_a?(Array)
21
+ filtered_hash[key] = value.map {|v| v.is_a?(Hash) ? skip_sensitive_data(v) : v }
22
+ end
23
+ end
24
+
25
+ filtered_hash
26
+ else
27
+ hash
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module FinAppsCore
3
+ VERSION = '2.0.2'
4
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ desc 'Bumps the version to the next patch level, tags and pushes the code to
3
+ origin repository and releases the gem. BOOM!'
4
+
5
+ # https://github.com/svenfuchs/gem-release
6
+
7
+ task :release do
8
+ system 'gem bump --tag --release'
9
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+ RSpec.describe ObjectExtensions do
3
+ context 'when refining Object' do
4
+ using ObjectExtensions
5
+
6
+ describe '#blank?' do
7
+ # An object is blank if it's false, empty, or a whitespace string.
8
+ context 'for false' do
9
+ it { expect(false.blank?).to eq(true) }
10
+ end
11
+ context 'for empty arrays' do
12
+ it { expect([].blank?).to eq(true) }
13
+ end
14
+ context 'for empty hashes' do
15
+ it { expect({}.blank?).to eq(true) }
16
+ end
17
+ context 'for whitespace string' do
18
+ it { expect(''.blank?).to eq(true) }
19
+ end
20
+ end
21
+
22
+ describe '#present?' do
23
+ # An object is present if it's not blank.
24
+ context 'for not blank objects' do
25
+ it { expect(1.present?).to eq(true) }
26
+ end
27
+
28
+ context 'for blank objects' do
29
+ it { expect(false.present?).to eq(false) }
30
+ end
31
+ end
32
+
33
+ describe '#presence' do
34
+ # Returns the receiver if it's present otherwise returns +nil+.
35
+ context 'returns the receiver when the receiver is present' do
36
+ it { expect(true.presence).to eq(true) }
37
+ end
38
+
39
+ context 'returns nil when the receiver is not present' do
40
+ it { expect(false.presence).to be_nil }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ RSpec.describe ObjectExtensions do
3
+ context 'when refining Object' do
4
+ using ObjectExtensions
5
+
6
+ describe '#integer?' do
7
+ context 'for integers' do
8
+ subject { 1 + rand(10) }
9
+ it { expect(subject.integer?).to eq(true) }
10
+ end
11
+ context 'for non integers' do
12
+ subject { rand }
13
+ it { expect(subject.integer?).to eq(false) }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ RSpec.describe FinAppsCore::Middleware::AcceptJson do
3
+ let(:fake_app) { proc {|env| env } }
4
+ describe '#call' do
5
+ subject { FinAppsCore::Middleware::AcceptJson.new(fake_app) }
6
+ env = {request_headers: {}}
7
+
8
+ it('generates a UserAgent header') do
9
+ expect(subject.call(env)[:request_headers][FinAppsCore::Middleware::AcceptJson::KEY]).to eq('application/json')
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ RSpec.describe FinAppsCore::Middleware::NoEncodingBasicAuthentication do
3
+ let(:valid_credentials) { VALID_CREDENTIALS }
4
+ let(:key) { FinAppsCore::Middleware::NoEncodingBasicAuthentication::KEY }
5
+
6
+ describe '#call' do
7
+ fake_app = proc {|env| env }
8
+
9
+ context 'when credentials were provided' do
10
+ let(:middleware) do
11
+ FinAppsCore::Middleware::NoEncodingBasicAuthentication.new(fake_app, VALID_CREDENTIALS[:token])
12
+ end
13
+ let(:expected_header) { "Basic #{valid_credentials[:token]}" }
14
+
15
+ context 'when header was not previously set' do
16
+ let(:request_env) { {request_headers: {}} }
17
+ subject(:actual_header) { middleware.call(request_env)[:request_headers][key] }
18
+
19
+ it('generates a header') { expect(actual_header).to eq(expected_header) }
20
+ end
21
+
22
+ context 'when header was previously set' do
23
+ let(:existing_header) { {FinAppsCore::Middleware::NoEncodingBasicAuthentication::KEY => 'foo'} }
24
+ let(:request_env) { {request_headers: existing_header} }
25
+ subject(:actual_header) { middleware.call(request_env)[:request_headers][key] }
26
+
27
+ it('does not override existing header') { expect(actual_header).to eq('foo') }
28
+ it('does not generate a header') { expect(actual_header).to_not eq(expected_header) }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ RSpec.describe FinAppsCore::Middleware::TenantAuthentication do
3
+ let(:valid_tenant_options) { VALID_CREDENTIALS }
4
+ let(:key) { FinAppsCore::Middleware::TenantAuthentication::KEY }
5
+
6
+ describe '#call' do
7
+ fake_app = proc {|env| env }
8
+
9
+ context 'when company credentials were provided' do
10
+ let(:middleware) do
11
+ FinAppsCore::Middleware::TenantAuthentication.new(fake_app,
12
+ VALID_CREDENTIALS[:identifier],
13
+ VALID_CREDENTIALS[:token])
14
+ end
15
+ let(:expected_header) { "#{valid_tenant_options[:identifier]}=#{valid_tenant_options[:token]}" }
16
+
17
+ context 'when header was not previously set' do
18
+ let(:request_env) { {request_headers: {}} }
19
+ subject(:actual_header) { middleware.call(request_env)[:request_headers][key] }
20
+
21
+ it('generates a Tenant Authentication header') { expect(actual_header).to eq(expected_header) }
22
+ end
23
+
24
+ context 'when header was previously set' do
25
+ let(:existing_header) { {FinAppsCore::Middleware::TenantAuthentication::KEY => 'foo'} }
26
+ let(:request_env) { {request_headers: existing_header} }
27
+ subject(:actual_header) { middleware.call(request_env)[:request_headers][key] }
28
+
29
+ it('does not override existing Tenant Authentication header') { expect(actual_header).to eq('foo') }
30
+ it('does not generate a Tenant Authentication header') { expect(actual_header).to_not eq(expected_header) }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ RSpec.describe FinAppsCore::Middleware::UserAgent do
3
+ let(:fake_app) { proc {|env| env } }
4
+ describe '#call' do
5
+ subject { FinAppsCore::Middleware::UserAgent.new(fake_app) }
6
+ env = {request_headers: {}}
7
+
8
+ it('generates a UserAgent header') do
9
+ expect(subject.call(env)[:request_headers][FinAppsCore::Middleware::UserAgent::KEY]).to start_with('finapps-ruby')
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ RSpec.describe FinAppsCore::Middleware::RaiseError do
3
+ let(:fake_app) { proc {|env| env } }
4
+ Env = Struct.new(:status, :response_headers, :body)
5
+
6
+ describe '#on_complete' do
7
+ subject { FinAppsCore::Middleware::RaiseError.new(fake_app) }
8
+
9
+ context 'for successful requests' do
10
+ let(:env) { Env.new(200) }
11
+ it { expect { subject.on_complete(env) }.not_to raise_error }
12
+ end
13
+ context 'for client errors' do
14
+ let(:env) { Env.new(401, {}, '{"messages":["Invalid User Identifier or Credentials"]}') }
15
+ error_message = 'the server responded with status 401'
16
+ it { expect { subject.on_complete(env) }.to raise_error(Faraday::Error::ClientError, error_message) }
17
+ end
18
+ context 'for connection failed error' do
19
+ let(:env) { Env.new(407) }
20
+ error_message = '407 "Proxy Authentication Required"'
21
+ it { expect { subject.on_complete(env) }.to raise_error(Faraday::Error::ConnectionFailed, error_message) }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+ RSpec.describe FinAppsCore::REST::BaseClient do
3
+ let(:valid_tenant_options) do
4
+ {tenant_identifier: VALID_CREDENTIALS[:identifier],
5
+ tenant_token: VALID_CREDENTIALS[:token]}
6
+ end
7
+ subject { FinAppsCore::REST::BaseClient.new(valid_tenant_options) }
8
+
9
+ RESPONSE = 0
10
+ ERROR_MESSAGES = 1
11
+ let(:return_array) { %i(RESPONSE ERROR_MESSAGES) }
12
+
13
+ describe '#new' do
14
+ it 'assigns @config' do
15
+ expect(subject.config).to be_a(FinAppsCore::REST::Configuration)
16
+ end
17
+ end
18
+
19
+ describe '#connection' do
20
+ it 'created a Faraday connection object' do
21
+ expect(subject.connection).to be_a(Faraday::Connection)
22
+ end
23
+
24
+ it 'memoizes the results' do
25
+ first = subject.connection
26
+ second = subject.connection
27
+ expect(first.object_id).to eq(second.object_id)
28
+ end
29
+ end
30
+
31
+ describe '#send_request' do
32
+ it 'should raise FinAppsCore::InvalidArgumentsError if method is NOT supported' do
33
+ expect { subject.send_request('fake_path', :option) }.to raise_error(FinAppsCore::InvalidArgumentsError,
34
+ 'Method not supported: option.')
35
+ end
36
+
37
+ it 'should raise FinAppsCore::MissingArgumentsError if method is NOT provided' do
38
+ expect { subject.send_request(nil, :get) }.to raise_error(FinAppsCore::MissingArgumentsError,
39
+ 'Missing argument: path.')
40
+ end
41
+
42
+ it 'should raise FinAppsCore::MissingArgumentsError if path is NOT provided' do
43
+ expect { subject.send_request('fake_path', nil) }.to raise_error(FinAppsCore::MissingArgumentsError,
44
+ 'Missing argument: method.')
45
+ end
46
+
47
+ context 'when method and path are provided' do
48
+ subject { FinAppsCore::REST::BaseClient.new(valid_tenant_options).send_request('relevance/ruleset/names', :get) }
49
+ let(:return_array) { %i(RESPONSE ERROR_MESSAGES) }
50
+
51
+ it('returns an array of 2 items') do
52
+ expect(subject).to be_a(Array)
53
+ expect(subject.size).to eq(return_array.length)
54
+ end
55
+
56
+ context 'for unsupported methods' do
57
+ subject { FinAppsCore::REST::BaseClient.new(valid_tenant_options).send_request('users', :options) }
58
+
59
+ it do
60
+ expect { subject.send_request(nil, :get) }
61
+ .to raise_error(FinAppsCore::InvalidArgumentsError, 'Method not supported: options.')
62
+ end
63
+ end
64
+
65
+ context 'for client errors' do
66
+ subject { FinAppsCore::REST::BaseClient.new(valid_tenant_options).send_request('client_error', :get) }
67
+
68
+ it('result is null') { expect(subject[RESPONSE]).to be_nil }
69
+ it('error_messages is an array') { expect(subject[ERROR_MESSAGES]).to be_a(Array) }
70
+ it('error_messages gets populated') { expect(subject[ERROR_MESSAGES].first).to eq 'Password Minimum size is 8' }
71
+ end
72
+
73
+ context 'for server errors' do
74
+ subject { FinAppsCore::REST::BaseClient.new(valid_tenant_options).send_request('server_error', :get) }
75
+
76
+ it('the result should be nil') { expect(subject[RESPONSE]).to be_nil }
77
+ it { expect(subject[ERROR_MESSAGES]).not_to be_nil }
78
+ it { expect(subject[ERROR_MESSAGES]).to be_a(Array) }
79
+ it { expect(subject[ERROR_MESSAGES].first).to eq 'the server responded with status 500' }
80
+ end
81
+
82
+ context 'for proxy errors' do
83
+ subject { FinAppsCore::REST::BaseClient.new(valid_tenant_options).send_request('proxy_error', :get) }
84
+
85
+ it { expect { subject }.to raise_error(Faraday::ConnectionFailed, '407 "Proxy Authentication Required"') }
86
+ end
87
+ end
88
+
89
+ context 'if a block is provided' do
90
+ it('gets executed on the response') do
91
+ expect(subject.send_request('relevance/ruleset/names', :get, &:status)[RESPONSE]).to eq(200)
92
+ expect(subject.send_request('relevance/ruleset/names', :get) {|r| r.body.length }[RESPONSE]).to eq(45)
93
+ end
94
+ end
95
+ end
96
+
97
+ describe '#method_missing' do
98
+ context 'for unsupported methods' do
99
+ it { expect { subject.unsupported }.to raise_error(NoMethodError) }
100
+ end
101
+ end
102
+
103
+ describe '#respond_to_missing?' do
104
+ context 'for supported methods' do
105
+ [:get, :post, :put, :delete].each do |method|
106
+ it("responds to #{method}") { expect(subject).to respond_to(method) }
107
+ end
108
+ end
109
+ end
110
+ end