castle-rb 2.3.2 → 3.0.0

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