castle-rb 2.3.2 → 3.0.0

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 (62) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +55 -9
  3. data/lib/castle.rb +17 -7
  4. data/lib/castle/api.rb +20 -22
  5. data/lib/castle/client.rb +50 -19
  6. data/lib/castle/command.rb +5 -0
  7. data/lib/castle/commands/authenticate.rb +25 -0
  8. data/lib/castle/commands/identify.rb +30 -0
  9. data/lib/castle/commands/review.rb +13 -0
  10. data/lib/castle/commands/track.rb +25 -0
  11. data/lib/castle/commands/with_context.rb +28 -0
  12. data/lib/castle/configuration.rb +46 -14
  13. data/lib/castle/context_merger.rb +13 -0
  14. data/lib/castle/default_context.rb +28 -0
  15. data/lib/castle/errors.rb +2 -0
  16. data/lib/castle/extractors/client_id.rb +4 -14
  17. data/lib/castle/extractors/headers.rb +6 -18
  18. data/lib/castle/failover_auth_response.rb +21 -0
  19. data/lib/castle/header_formatter.rb +9 -0
  20. data/lib/castle/request.rb +7 -13
  21. data/lib/castle/response.rb +2 -0
  22. data/lib/castle/review.rb +11 -0
  23. data/lib/castle/secure_mode.rb +11 -0
  24. data/lib/castle/support/hanami.rb +19 -0
  25. data/lib/castle/support/padrino.rb +1 -1
  26. data/lib/castle/support/rails.rb +1 -1
  27. data/lib/castle/support/sinatra.rb +4 -2
  28. data/lib/castle/utils.rb +55 -0
  29. data/lib/castle/utils/cloner.rb +11 -0
  30. data/lib/castle/utils/merger.rb +23 -0
  31. data/lib/castle/version.rb +1 -1
  32. data/spec/lib/castle/api_spec.rb +16 -25
  33. data/spec/lib/castle/client_spec.rb +175 -39
  34. data/spec/lib/castle/command_spec.rb +9 -0
  35. data/spec/lib/castle/commands/authenticate_spec.rb +106 -0
  36. data/spec/lib/castle/commands/identify_spec.rb +85 -0
  37. data/spec/lib/castle/commands/review_spec.rb +24 -0
  38. data/spec/lib/castle/commands/track_spec.rb +107 -0
  39. data/spec/lib/castle/configuration_spec.rb +75 -27
  40. data/spec/lib/castle/context_merger_spec.rb +34 -0
  41. data/spec/lib/castle/default_context_spec.rb +35 -0
  42. data/spec/lib/castle/extractors/client_id_spec.rb +13 -5
  43. data/spec/lib/castle/extractors/headers_spec.rb +6 -5
  44. data/spec/lib/castle/extractors/ip_spec.rb +2 -9
  45. data/spec/lib/castle/header_formatter_spec.rb +21 -0
  46. data/spec/lib/castle/request_spec.rb +12 -9
  47. data/spec/lib/castle/response_spec.rb +1 -3
  48. data/spec/lib/castle/review_spec.rb +23 -0
  49. data/spec/lib/castle/secure_mode_spec.rb +9 -0
  50. data/spec/lib/castle/utils/cloner_spec.rb +18 -0
  51. data/spec/lib/castle/utils/merger_spec.rb +13 -0
  52. data/spec/lib/castle/utils_spec.rb +156 -0
  53. data/spec/lib/castle/version_spec.rb +1 -5
  54. data/spec/lib/castle_spec.rb +8 -15
  55. data/spec/spec_helper.rb +3 -9
  56. metadata +46 -12
  57. data/lib/castle/cookie_store.rb +0 -52
  58. data/lib/castle/headers.rb +0 -39
  59. data/lib/castle/support.rb +0 -11
  60. data/lib/castle/system.rb +0 -36
  61. data/spec/lib/castle/headers_spec.rb +0 -82
  62. data/spec/lib/castle/system_spec.rb +0 -70
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ class ContextMerger
5
+ def initialize(context)
6
+ @main_context = Castle::Utils::Cloner.call(context)
7
+ end
8
+
9
+ def call(request_context)
10
+ Castle::Utils::Merger.call(@main_context, request_context)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ class DefaultContext
5
+ def initialize(request, cookies = nil)
6
+ @client_id = Extractors::ClientId.new(request, cookies || request.cookies).call('__cid')
7
+ @headers = Extractors::Headers.new(request).call
8
+ @ip = Extractors::IP.new(request).call
9
+ end
10
+
11
+ def call
12
+ context = {
13
+ client_id: @client_id,
14
+ active: true,
15
+ origin: 'web',
16
+ headers: @headers || {},
17
+ ip: @ip,
18
+ library: {
19
+ name: 'castle-rb',
20
+ version: Castle::VERSION
21
+ }
22
+ }
23
+ context[:locale] = @headers['Accept-Language'] if @headers['Accept-Language']
24
+ context[:user_agent] = @headers['User-Agent'] if @headers['User-Agent']
25
+ context
26
+ end
27
+ end
28
+ end
data/lib/castle/errors.rb CHANGED
@@ -24,4 +24,6 @@ module Castle
24
24
  class InvalidParametersError < Castle::ApiError; end
25
25
  # api error unauthorized 401
26
26
  class UnauthorizedError < Castle::ApiError; end
27
+ # all internal server errors
28
+ class InternalServerError < Castle::ApiError; end
27
29
  end
@@ -4,25 +4,15 @@ module Castle
4
4
  module Extractors
5
5
  # used for extraction of cookies and headers from the request
6
6
  class ClientId
7
- def initialize(request)
7
+ def initialize(request, cookies)
8
8
  @request = request
9
+ @cookies = cookies || {}
9
10
  end
10
11
 
11
- def call(response, name)
12
- extract_cookie(response)[name] ||
12
+ def call(name)
13
+ @cookies[name] ||
13
14
  @request.env.fetch('HTTP_X_CASTLE_CLIENT_ID', '')
14
15
  end
15
-
16
- private
17
-
18
- # Extract the cookie set by the Castle JavaScript
19
- def extract_cookie(response)
20
- if response.class.name == 'ActionDispatch::Cookies::CookieJar'
21
- Castle::CookieStore::Rack.new(response)
22
- else
23
- Castle::CookieStore::Base.new(@request, response)
24
- end
25
- end
26
16
  end
27
17
  end
28
18
  end
@@ -7,29 +7,17 @@ module Castle
7
7
  def initialize(request)
8
8
  @request = request
9
9
  @request_env = @request.env
10
- @disabled_headers = ['Cookie']
10
+ @formatter = HeaderFormatter.new
11
11
  end
12
12
 
13
13
  # Serialize HTTP headers
14
14
  def call
15
- headers = http_headers.each_with_object({}) do |header, acc|
16
- name = format_header_name(header)
17
- unless @disabled_headers.include?(name)
18
- acc[name] = @request_env[header]
19
- end
15
+ @request_env.keys.each_with_object({}) do |header, acc|
16
+ name = @formatter.call(header)
17
+ next unless Castle.config.whitelisted.include?(name)
18
+ next if Castle.config.blacklisted.include?(name)
19
+ acc[name] = @request_env[header]
20
20
  end
21
-
22
- JSON.generate(headers)
23
- end
24
-
25
- private
26
-
27
- def format_header_name(header)
28
- header.gsub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
29
- end
30
-
31
- def http_headers
32
- @request_env.keys.grep(/^HTTP_/)
33
21
  end
34
22
  end
35
23
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ # generate failover authentication response
5
+ class FailoverAuthResponse
6
+ def initialize(user_id, strategy: Castle.config.failover_strategy, reason:)
7
+ @strategy = strategy
8
+ @reason = reason
9
+ @user_id = user_id
10
+ end
11
+
12
+ def generate
13
+ {
14
+ 'action' => @strategy.to_s,
15
+ 'user_id' => @user_id,
16
+ 'failover' => true,
17
+ 'failover_reason' => @reason
18
+ }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ class HeaderFormatter
5
+ def call(header)
6
+ header.gsub(/^HTTP(?:_|-)/i, '').split(/_|-/).map(&:capitalize).join('-')
7
+ end
8
+ end
9
+ end
@@ -3,30 +3,24 @@
3
3
  module Castle
4
4
  # generate api request
5
5
  class Request
6
- def initialize(headers)
6
+ def initialize(headers = {})
7
7
  @config = Castle.config
8
8
  @headers = headers
9
9
  end
10
10
 
11
- def build_query(endpoint)
12
- request = Net::HTTP::Get.new(
13
- "#{@config.api_endpoint.path}/#{endpoint}", @headers
14
- )
15
- add_basic_auth(request)
16
- request
17
- end
18
-
19
11
  def build(endpoint, args, method)
20
- request = Net::HTTP.const_get(method.to_s.capitalize).new(
21
- "#{@config.api_endpoint.path}/#{endpoint}", @headers
22
- )
23
- request.body = args.to_json
12
+ request = Net::HTTP.const_get(method.to_s.capitalize).new(build_url(endpoint), @headers)
13
+ request.body = ::Castle::Utils.replace_invalid_characters(args).to_json unless method == :get
24
14
  add_basic_auth(request)
25
15
  request
26
16
  end
27
17
 
28
18
  private
29
19
 
20
+ def build_url(endpoint)
21
+ "/#{@config.url_prefix}/#{endpoint}"
22
+ end
23
+
30
24
  def add_basic_auth(request)
31
25
  request.basic_auth('', @config.api_secret)
32
26
  end
@@ -36,6 +36,8 @@ module Castle
36
36
 
37
37
  return if response_code.between?(200, 299)
38
38
 
39
+ raise Castle::InternalServerError if response_code.between?(500, 599)
40
+
39
41
  error = RESPONSE_ERRORS.fetch(response_code, Castle::ApiError)
40
42
  raise error, @response[:message]
41
43
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ class Review
5
+ def self.retrieve(review_id)
6
+ command = Castle::Commands::Review.new.build(review_id)
7
+
8
+ API.new.request(command)
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.new(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
@@ -5,7 +5,7 @@ module Padrino
5
5
  module Castle
6
6
  module Helpers
7
7
  def castle
8
- @castle ||= ::Castle::Client.new(request, response)
8
+ @castle ||= ::Castle::Client.new(request)
9
9
  end
10
10
  end
11
11
 
@@ -3,7 +3,7 @@
3
3
  module Castle
4
4
  module CastleClient
5
5
  def castle
6
- @castle ||= request.env['castle'] || Castle::Client.new(request, response)
6
+ @castle ||= request.env['castle'] || Castle::Client.new(request)
7
7
  end
8
8
  end
9
9
 
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'sinatra/base'
4
+
3
5
  module Sinatra
4
6
  module Castle
5
7
  module Helpers
6
8
  def castle
7
- @castle ||= ::Castle::Client.new(request, response)
9
+ @castle ||= ::Castle::Client.new(request)
8
10
  end
9
11
  end
10
12
 
11
13
  def self.registered(app)
12
- app.helpers Helpers
14
+ app.helpers Castle::Helpers
13
15
  end
14
16
  end
15
17
 
@@ -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(first, second)
7
+ Castle::Utils.deep_symbolize_keys!(first)
8
+ Castle::Utils.deep_symbolize_keys!(second)
9
+
10
+ second.each do |name, value|
11
+ if value.nil?
12
+ first.delete(name)
13
+ elsif value.is_a?(Hash) && first[name].is_a?(Hash)
14
+ call(first[name], value)
15
+ else
16
+ first[name] = value
17
+ end
18
+ end
19
+ first
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castle
4
- VERSION = '2.3.2'
4
+ VERSION = '3.0.0'
5
5
  end
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spec_helper'
4
-
5
3
  describe Castle::API do
6
- let(:api) { described_class.new('abcd', '1.2.3.4', '{}') }
7
- let(:api_endpoint) { 'http://new.herokuapp.com:3000/v2' }
4
+ let(:api) { described_class.new('X-Castle-Client-Id' => 'abcd', 'X-Castle-Ip' => '1.2.3.4') }
5
+ let(:command) { Castle::Command.new('authenticate', '1234', :post) }
6
+ let(:result_headers) do
7
+ { 'Accept' => '*/*',
8
+ 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
9
+ 'User-Agent' => 'Ruby', 'X-Castle-Client-Id' => 'abcd',
10
+ 'X-Castle-Ip' => '1.2.3.4' }
11
+ end
8
12
 
9
13
  describe 'handles timeout' do
10
14
  before do
@@ -12,7 +16,7 @@ describe Castle::API do
12
16
  end
13
17
  it do
14
18
  expect do
15
- api.request('authenticate', user_id: '1234')
19
+ api.request(command)
16
20
  end.to raise_error(Castle::RequestError)
17
21
  end
18
22
  end
@@ -21,34 +25,21 @@ describe Castle::API do
21
25
  before do
22
26
  stub_request(:any, /api.castle.io/).to_return(status: 400)
23
27
  end
24
- it 'handles non-OK response code' do
28
+ it do
25
29
  expect do
26
- api.request('authenticate', user_id: '1234')
30
+ api.request(command)
27
31
  end.to raise_error(Castle::BadRequestError)
28
32
  end
29
33
  end
30
34
 
31
- describe 'handles custom API endpoint' do
32
- before do
33
- stub_request(:any, /new.herokuapp.com/)
34
- Castle.config.api_endpoint = api_endpoint
35
- end
36
- it do
37
- api.request('authenticate', user_id: '1234')
38
- path = "#{api_endpoint.gsub(/new/, ':secret@new')}/authenticate"
39
- assert_requested :post, path, times: 1
40
- end
41
- end
42
-
43
- describe 'handles query request' do
35
+ describe 'handles missing configuration' do
44
36
  before do
45
- stub_request(:any, /new.herokuapp.com/)
46
- Castle.config.api_endpoint = api_endpoint
37
+ allow(Castle.config).to receive(:api_secret).and_return('')
47
38
  end
48
39
  it do
49
- api.request_query('review/1')
50
- path = "#{api_endpoint.gsub(/new/, ':secret@new')}/review/1"
51
- assert_requested :get, path, times: 1
40
+ expect do
41
+ api.request(command)
42
+ end.to raise_error(Castle::ConfigurationError)
52
43
  end
53
44
  end
54
45
  end