finapps_core 2.0.2

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