castle-rb 3.6.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 (69) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +157 -0
  3. data/lib/castle-rb.rb +3 -0
  4. data/lib/castle.rb +62 -0
  5. data/lib/castle/api.rb +40 -0
  6. data/lib/castle/api/request.rb +37 -0
  7. data/lib/castle/api/request/build.rb +27 -0
  8. data/lib/castle/api/response.rb +40 -0
  9. data/lib/castle/client.rb +106 -0
  10. data/lib/castle/command.rb +5 -0
  11. data/lib/castle/commands/authenticate.rb +23 -0
  12. data/lib/castle/commands/identify.rb +23 -0
  13. data/lib/castle/commands/impersonate.rb +26 -0
  14. data/lib/castle/commands/review.rb +14 -0
  15. data/lib/castle/commands/track.rb +23 -0
  16. data/lib/castle/configuration.rb +80 -0
  17. data/lib/castle/context/default.rb +40 -0
  18. data/lib/castle/context/merger.rb +14 -0
  19. data/lib/castle/context/sanitizer.rb +23 -0
  20. data/lib/castle/errors.rb +41 -0
  21. data/lib/castle/extractors/client_id.rb +17 -0
  22. data/lib/castle/extractors/headers.rb +51 -0
  23. data/lib/castle/extractors/ip.rb +18 -0
  24. data/lib/castle/failover_auth_response.rb +21 -0
  25. data/lib/castle/header_formatter.rb +9 -0
  26. data/lib/castle/review.rb +11 -0
  27. data/lib/castle/secure_mode.rb +11 -0
  28. data/lib/castle/support/hanami.rb +19 -0
  29. data/lib/castle/support/padrino.rb +19 -0
  30. data/lib/castle/support/rails.rb +13 -0
  31. data/lib/castle/support/sinatra.rb +19 -0
  32. data/lib/castle/utils.rb +55 -0
  33. data/lib/castle/utils/cloner.rb +11 -0
  34. data/lib/castle/utils/merger.rb +23 -0
  35. data/lib/castle/utils/timestamp.rb +12 -0
  36. data/lib/castle/validators/not_supported.rb +16 -0
  37. data/lib/castle/validators/present.rb +16 -0
  38. data/lib/castle/version.rb +5 -0
  39. data/spec/lib/castle/api/request/build_spec.rb +44 -0
  40. data/spec/lib/castle/api/request_spec.rb +59 -0
  41. data/spec/lib/castle/api/response_spec.rb +58 -0
  42. data/spec/lib/castle/api_spec.rb +37 -0
  43. data/spec/lib/castle/client_spec.rb +358 -0
  44. data/spec/lib/castle/command_spec.rb +9 -0
  45. data/spec/lib/castle/commands/authenticate_spec.rb +108 -0
  46. data/spec/lib/castle/commands/identify_spec.rb +87 -0
  47. data/spec/lib/castle/commands/impersonate_spec.rb +106 -0
  48. data/spec/lib/castle/commands/review_spec.rb +24 -0
  49. data/spec/lib/castle/commands/track_spec.rb +113 -0
  50. data/spec/lib/castle/configuration_spec.rb +130 -0
  51. data/spec/lib/castle/context/default_spec.rb +41 -0
  52. data/spec/lib/castle/context/merger_spec.rb +23 -0
  53. data/spec/lib/castle/context/sanitizer_spec.rb +27 -0
  54. data/spec/lib/castle/extractors/client_id_spec.rb +62 -0
  55. data/spec/lib/castle/extractors/headers_spec.rb +89 -0
  56. data/spec/lib/castle/extractors/ip_spec.rb +27 -0
  57. data/spec/lib/castle/header_formatter_spec.rb +25 -0
  58. data/spec/lib/castle/review_spec.rb +19 -0
  59. data/spec/lib/castle/secure_mode_spec.rb +9 -0
  60. data/spec/lib/castle/utils/cloner_spec.rb +18 -0
  61. data/spec/lib/castle/utils/merger_spec.rb +13 -0
  62. data/spec/lib/castle/utils/timestamp_spec.rb +17 -0
  63. data/spec/lib/castle/utils_spec.rb +156 -0
  64. data/spec/lib/castle/validators/not_supported_spec.rb +26 -0
  65. data/spec/lib/castle/validators/present_spec.rb +33 -0
  66. data/spec/lib/castle/version_spec.rb +5 -0
  67. data/spec/lib/castle_spec.rb +66 -0
  68. data/spec/spec_helper.rb +25 -0
  69. metadata +139 -0
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ class Review
5
+ def self.retrieve(review_id)
6
+ Castle::API.request(
7
+ Castle::Commands::Review.build(review_id)
8
+ )
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module Castle
6
+ module SecureMode
7
+ def self.signature(user_id)
8
+ OpenSSL::HMAC.hexdigest('sha256', Castle.config.api_secret, user_id.to_s)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Hanami
5
+ module Action
6
+ def castle
7
+ @castle ||= ::Castle::Client.from_request(request, cookies: (cookies if defined? cookies))
8
+ end
9
+ end
10
+
11
+ def self.included(base)
12
+ base.configure do
13
+ controller.prepare do
14
+ include Castle::Hanami::Action
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Padrino
4
+ class Application
5
+ module Castle
6
+ module Helpers
7
+ def castle
8
+ @castle ||= ::Castle::Client.from_request(request)
9
+ end
10
+ end
11
+
12
+ def self.registered(app)
13
+ app.helpers Helpers
14
+ end
15
+ end
16
+
17
+ register Castle
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module CastleClient
5
+ def castle
6
+ @castle ||= request.env['castle'] || Castle::Client.from_request(request)
7
+ end
8
+ end
9
+
10
+ ActiveSupport.on_load(:action_controller) do
11
+ include CastleClient
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra/base'
4
+
5
+ module Sinatra
6
+ module Castle
7
+ module Helpers
8
+ def castle
9
+ @castle ||= ::Castle::Client.from_request(request)
10
+ end
11
+ end
12
+
13
+ def self.registered(app)
14
+ app.helpers Castle::Helpers
15
+ end
16
+ end
17
+
18
+ register Castle
19
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Utils
5
+ class << self
6
+ # Returns a new hash with all keys converted to symbols, as long as
7
+ # they respond to +to_sym+. This includes the keys from the root hash
8
+ # and from all nested hashes and arrays.
9
+ #
10
+ # hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
11
+ #
12
+ # Castle::Hash.deep_symbolize_keys(hash)
13
+ # # => {:person=>{:name=>"Rob", :age=>"28"}}
14
+ def deep_symbolize_keys(object, &block)
15
+ case object
16
+ when Hash
17
+ object.each_with_object({}) do |(key, value), result|
18
+ result[key.to_sym] = deep_symbolize_keys(value, &block)
19
+ end
20
+ when Array
21
+ object.map { |e| deep_symbolize_keys(e, &block) }
22
+ else
23
+ object
24
+ end
25
+ end
26
+
27
+ def deep_symbolize_keys!(object, &block)
28
+ case object
29
+ when Hash
30
+ object.keys.each do |key|
31
+ value = object.delete(key)
32
+ object[key.to_sym] = deep_symbolize_keys!(value, &block)
33
+ end
34
+ object
35
+ when Array
36
+ object.map! { |e| deep_symbolize_keys!(e, &block) }
37
+ else
38
+ object
39
+ end
40
+ end
41
+
42
+ def replace_invalid_characters(arg)
43
+ if arg.is_a?(::String)
44
+ arg.encode('UTF-8', invalid: :replace, undef: :replace)
45
+ elsif arg.is_a?(::Hash)
46
+ arg.each_with_object({}) { |(k, v), h| h[k] = replace_invalid_characters(v) }
47
+ elsif arg.is_a?(::Array)
48
+ arg.map(&method(:replace_invalid_characters))
49
+ else
50
+ arg
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Utils
5
+ class Cloner
6
+ def self.call(object)
7
+ Marshal.load(Marshal.dump(object))
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Utils
5
+ class Merger
6
+ def self.call(base, extra)
7
+ base_s = Castle::Utils.deep_symbolize_keys(base)
8
+ extra_s = Castle::Utils.deep_symbolize_keys(extra)
9
+
10
+ extra_s.each do |name, value|
11
+ if value.nil?
12
+ base_s.delete(name)
13
+ elsif value.is_a?(Hash) && base_s[name].is_a?(Hash)
14
+ base_s[name] = call(base_s[name], value)
15
+ else
16
+ base_s[name] = value
17
+ end
18
+ end
19
+ base_s
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Utils
5
+ # generates proper timestamp
6
+ class Timestamp
7
+ def self.call
8
+ Time.now.utc.iso8601(3)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Validators
5
+ class NotSupported
6
+ class << self
7
+ def call(options, keys)
8
+ keys.each do |key|
9
+ next unless options.key?(key)
10
+ raise Castle::InvalidParametersError, "#{key} is/are not supported"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Validators
5
+ class Present
6
+ class << self
7
+ def call(options, keys)
8
+ keys.each do |key|
9
+ next unless options[key].to_s.empty?
10
+ raise Castle::InvalidParametersError, "#{key} is missing or empty"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ VERSION = '3.6.2'
5
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::API::Request::Build do
4
+ subject(:call) { described_class.call(command, headers, api_secret) }
5
+
6
+ let(:headers) { { 'SAMPLE-HEADER' => '1' } }
7
+ let(:api_secret) { 'secret' }
8
+
9
+ describe 'call' do
10
+ context 'when get' do
11
+ let(:command) { Castle::Commands::Review.build(review_id) }
12
+ let(:review_id) { SecureRandom.uuid }
13
+
14
+ it { expect(call.body).to be_nil }
15
+ it { expect(call.method).to eql('GET') }
16
+ it { expect(call.path).to eql("/v1/#{command.path}") }
17
+ it { expect(call.to_hash).to have_key('authorization') }
18
+ it { expect(call.to_hash).to have_key('sample-header') }
19
+ it { expect(call.to_hash['sample-header']).to eql(['1']) }
20
+ end
21
+
22
+ context 'when post' do
23
+ let(:time) { Time.now.utc.iso8601(3) }
24
+ let(:command) { Castle::Commands::Track.new({}).build(event: '$login.succeeded', name: "\xC4") }
25
+ let(:expected_body) do
26
+ {
27
+ event: '$login.succeeded',
28
+ name: "�",
29
+ context: {},
30
+ sent_at: time
31
+ }
32
+ end
33
+
34
+ before { allow(Castle::Utils::Timestamp).to receive(:call).and_return(time) }
35
+
36
+ it { expect(call.body).to be_eql(expected_body.to_json) }
37
+ it { expect(call.method).to eql('POST') }
38
+ it { expect(call.path).to eql("/v1/#{command.path}") }
39
+ it { expect(call.to_hash).to have_key('authorization') }
40
+ it { expect(call.to_hash).to have_key('sample-header') }
41
+ it { expect(call.to_hash['sample-header']).to eql(['1']) }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::API::Request do
4
+ describe '#call' do
5
+ subject(:call) { described_class.call(command, api_secret, headers) }
6
+
7
+ let(:http) { instance_double('Net::HTTP') }
8
+ let(:request_build) { instance_double('Castle::API::Request::Build') }
9
+ let(:command) { Castle::Commands::Track.new({}).build(event: '$login.succeeded') }
10
+ let(:headers) { {} }
11
+ let(:api_secret) { 'secret' }
12
+ let(:expected_headers) { { 'Content-Type' => 'application/json' } }
13
+
14
+ before do
15
+ allow(described_class).to receive(:http).and_return(http)
16
+ allow(http).to receive(:request).with(request_build)
17
+ allow(Castle::API::Request::Build).to receive(:call)
18
+ .with(command, expected_headers, api_secret)
19
+ .and_return(request_build)
20
+ call
21
+ end
22
+
23
+ it do
24
+ expect(Castle::API::Request::Build).to have_received(:call)
25
+ .with(command, expected_headers, api_secret)
26
+ end
27
+ it { expect(http).to have_received(:request).with(request_build) }
28
+ end
29
+
30
+ describe '#http' do
31
+ subject(:http) { described_class.http }
32
+
33
+ context 'when ssl false' do
34
+ before do
35
+ Castle.config.host = 'localhost'
36
+ Castle.config.port = 3002
37
+ end
38
+
39
+ after do
40
+ Castle.config.host = Castle::Configuration::HOST
41
+ Castle.config.port = Castle::Configuration::PORT
42
+ end
43
+
44
+ it { expect(http).to be_instance_of(Net::HTTP) }
45
+ it { expect(http.address).to eq(Castle.config.host) }
46
+ it { expect(http.port).to eq(Castle.config.port) }
47
+ it { expect(http.use_ssl?).to be false }
48
+ it { expect(http.verify_mode).to be_nil }
49
+ end
50
+
51
+ context 'when ssl true' do
52
+ it { expect(http).to be_instance_of(Net::HTTP) }
53
+ it { expect(http.address).to eq(Castle.config.host) }
54
+ it { expect(http.port).to eq(Castle.config.port) }
55
+ it { expect(http.use_ssl?).to be true }
56
+ it { expect(http.verify_mode).to eq(OpenSSL::SSL::VERIFY_PEER) }
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::API::Response do
4
+ describe '#call' do
5
+ subject(:call) { described_class.call(response) }
6
+
7
+ context 'when success' do
8
+ let(:response) { OpenStruct.new(body: '{"user":1}', code: 200) }
9
+
10
+ it { expect(call).to eql(user: 1) }
11
+ end
12
+
13
+ context 'when response empty' do
14
+ let(:response) { OpenStruct.new(body: '', code: 200) }
15
+
16
+ it { expect(call).to eql({}) }
17
+ end
18
+
19
+ context 'when response nil' do
20
+ let(:response) { OpenStruct.new(code: 200) }
21
+
22
+ it { expect(call).to eql({}) }
23
+ end
24
+
25
+ context 'when json is malformed' do
26
+ let(:response) { OpenStruct.new(body: '{a', code: 200) }
27
+
28
+ it { expect { call }.to raise_error(Castle::ApiError) }
29
+ end
30
+ end
31
+
32
+ describe '#verify!' do
33
+ subject(:verify!) { described_class.verify!(response) }
34
+
35
+ context 'without error when response is 2xx' do
36
+ let(:response) { OpenStruct.new(code: 200) }
37
+
38
+ it { expect { verify! }.not_to raise_error }
39
+ end
40
+
41
+ shared_examples 'response_failed' do |code, error|
42
+ let(:response) { OpenStruct.new(code: code) }
43
+
44
+ it "fail when response is #{code}" do
45
+ expect { verify! }.to raise_error(error)
46
+ end
47
+ end
48
+
49
+ it_behaves_like 'response_failed', '400', Castle::BadRequestError
50
+ it_behaves_like 'response_failed', '401', Castle::UnauthorizedError
51
+ it_behaves_like 'response_failed', '403', Castle::ForbiddenError
52
+ it_behaves_like 'response_failed', '404', Castle::NotFoundError
53
+ it_behaves_like 'response_failed', '419', Castle::UserUnauthorizedError
54
+ it_behaves_like 'response_failed', '422', Castle::InvalidParametersError
55
+ it_behaves_like 'response_failed', '499', Castle::ApiError
56
+ it_behaves_like 'response_failed', '500', Castle::InternalServerError
57
+ end
58
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::API do
4
+ subject(:request) { described_class.request(command) }
5
+
6
+ let(:command) { Castle::Commands::Track.new({}).build(event: '$login.succeeded') }
7
+
8
+ context 'when request timeouts' do
9
+ before { stub_request(:any, /api.castle.io/).to_timeout }
10
+
11
+ it do
12
+ expect do
13
+ request
14
+ end.to raise_error(Castle::RequestError)
15
+ end
16
+ end
17
+
18
+ context 'when non-OK response code' do
19
+ before { stub_request(:any, /api.castle.io/).to_return(status: 400) }
20
+
21
+ it do
22
+ expect do
23
+ request
24
+ end.to raise_error(Castle::BadRequestError)
25
+ end
26
+ end
27
+
28
+ context 'when no api_secret' do
29
+ before { allow(Castle.config).to receive(:api_secret).and_return('') }
30
+
31
+ it do
32
+ expect do
33
+ request
34
+ end.to raise_error(Castle::ConfigurationError)
35
+ end
36
+ end
37
+ end