castle-rb 4.1.0 → 6.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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +158 -43
  3. data/lib/castle.rb +46 -21
  4. data/lib/castle/api.rb +24 -12
  5. data/lib/castle/api/approve_device.rb +25 -0
  6. data/lib/castle/api/authenticate.rb +34 -0
  7. data/lib/castle/api/end_impersonation.rb +29 -0
  8. data/lib/castle/api/get_device.rb +25 -0
  9. data/lib/castle/api/get_devices_for_user.rb +25 -0
  10. data/lib/castle/api/identify.rb +26 -0
  11. data/lib/castle/api/report_device.rb +25 -0
  12. data/lib/castle/api/review.rb +24 -0
  13. data/lib/castle/api/start_impersonation.rb +29 -0
  14. data/lib/castle/api/track.rb +26 -0
  15. data/lib/castle/client.rb +52 -45
  16. data/lib/castle/{extractors/client_id.rb → client_id/extract.rb} +2 -2
  17. data/lib/castle/commands/approve_device.rb +21 -0
  18. data/lib/castle/commands/authenticate.rb +13 -13
  19. data/lib/castle/commands/end_impersonation.rb +25 -0
  20. data/lib/castle/commands/get_device.rb +21 -0
  21. data/lib/castle/commands/get_devices_for_user.rb +21 -0
  22. data/lib/castle/commands/identify.rb +12 -13
  23. data/lib/castle/commands/report_device.rb +21 -0
  24. data/lib/castle/commands/review.rb +6 -3
  25. data/lib/castle/commands/start_impersonation.rb +25 -0
  26. data/lib/castle/commands/track.rb +12 -13
  27. data/lib/castle/configuration.rb +45 -28
  28. data/lib/castle/context/{default.rb → get_default.rb} +5 -6
  29. data/lib/castle/context/{merger.rb → merge.rb} +3 -3
  30. data/lib/castle/context/prepare.rb +18 -0
  31. data/lib/castle/context/{sanitizer.rb → sanitize.rb} +1 -1
  32. data/lib/castle/core/get_connection.rb +25 -0
  33. data/lib/castle/{api/response.rb → core/process_response.rb} +4 -2
  34. data/lib/castle/core/process_webhook.rb +20 -0
  35. data/lib/castle/core/send_request.rb +50 -0
  36. data/lib/castle/errors.rb +2 -0
  37. data/lib/castle/events.rb +1 -1
  38. data/lib/castle/failover/prepare_response.rb +23 -0
  39. data/lib/castle/failover/strategy.rb +20 -0
  40. data/lib/castle/headers/extract.rb +47 -0
  41. data/lib/castle/headers/filter.rb +37 -0
  42. data/lib/castle/headers/format.rb +24 -0
  43. data/lib/castle/ip/extract.rb +83 -0
  44. data/lib/castle/logger.rb +19 -0
  45. data/lib/castle/payload/prepare.rb +27 -0
  46. data/lib/castle/secure_mode.rb +6 -2
  47. data/lib/castle/session.rb +18 -0
  48. data/lib/castle/singleton_configuration.rb +9 -0
  49. data/lib/castle/utils/clean_invalid_chars.rb +24 -0
  50. data/lib/castle/utils/clone.rb +15 -0
  51. data/lib/castle/utils/deep_symbolize_keys.rb +45 -0
  52. data/lib/castle/utils/get_timestamp.rb +15 -0
  53. data/lib/castle/utils/{merger.rb → merge.rb} +3 -3
  54. data/lib/castle/utils/secure_compare.rb +22 -0
  55. data/lib/castle/validators/not_supported.rb +1 -0
  56. data/lib/castle/validators/present.rb +1 -0
  57. data/lib/castle/verdict.rb +13 -0
  58. data/lib/castle/version.rb +1 -1
  59. data/lib/castle/webhooks/verify.rb +43 -0
  60. data/spec/integration/rails/rails_spec.rb +33 -7
  61. data/spec/integration/rails/support/application.rb +3 -1
  62. data/spec/integration/rails/support/home_controller.rb +47 -5
  63. data/spec/lib/castle/api/approve_device_spec.rb +21 -0
  64. data/spec/lib/castle/api/authenticate_spec.rb +140 -0
  65. data/spec/lib/castle/api/end_impersonation_spec.rb +59 -0
  66. data/spec/lib/castle/api/get_device_spec.rb +19 -0
  67. data/spec/lib/castle/api/get_devices_for_user_spec.rb +19 -0
  68. data/spec/lib/castle/api/identify_spec.rb +68 -0
  69. data/spec/lib/castle/api/report_device_spec.rb +21 -0
  70. data/spec/lib/castle/{review_spec.rb → api/review_spec.rb} +3 -3
  71. data/spec/lib/castle/api/start_impersonation_spec.rb +59 -0
  72. data/spec/lib/castle/api/track_spec.rb +68 -0
  73. data/spec/lib/castle/api_spec.rb +16 -1
  74. data/spec/lib/castle/{extractors/client_id_spec.rb → client_id/extract_spec.rb} +2 -2
  75. data/spec/lib/castle/client_spec.rb +41 -23
  76. data/spec/lib/castle/commands/approve_device_spec.rb +24 -0
  77. data/spec/lib/castle/commands/authenticate_spec.rb +7 -16
  78. data/spec/lib/castle/commands/end_impersonation_spec.rb +82 -0
  79. data/spec/lib/castle/commands/get_device_spec.rb +24 -0
  80. data/spec/lib/castle/commands/get_devices_for_user_spec.rb +24 -0
  81. data/spec/lib/castle/commands/identify_spec.rb +5 -16
  82. data/spec/lib/castle/commands/report_device_spec.rb +24 -0
  83. data/spec/lib/castle/commands/review_spec.rb +1 -1
  84. data/spec/lib/castle/commands/{impersonate_spec.rb → start_impersonation_spec.rb} +9 -34
  85. data/spec/lib/castle/commands/track_spec.rb +5 -16
  86. data/spec/lib/castle/configuration_spec.rb +9 -138
  87. data/spec/lib/castle/context/{default_spec.rb → get_default_spec.rb} +1 -2
  88. data/spec/lib/castle/context/{merger_spec.rb → merge_spec.rb} +1 -1
  89. data/spec/lib/castle/context/prepare_spec.rb +44 -0
  90. data/spec/lib/castle/context/{sanitizer_spec.rb → sanitize_spec.rb} +1 -1
  91. data/spec/lib/castle/core/get_connection_spec.rb +59 -0
  92. data/spec/lib/castle/{api/response_spec.rb → core/process_response_spec.rb} +56 -1
  93. data/spec/lib/castle/core/process_webhook_spec.rb +46 -0
  94. data/spec/lib/castle/core/send_request_spec.rb +102 -0
  95. data/spec/lib/castle/failover/strategy_spec.rb +12 -0
  96. data/spec/lib/castle/{extractors/headers_spec.rb → headers/extract_spec.rb} +18 -18
  97. data/spec/lib/castle/{headers_filter_spec.rb → headers/filter_spec.rb} +6 -5
  98. data/spec/lib/castle/headers/format_spec.rb +25 -0
  99. data/spec/lib/castle/{extractors/ip_spec.rb → ip/extract_spec.rb} +35 -7
  100. data/spec/lib/castle/logger_spec.rb +42 -0
  101. data/spec/lib/castle/payload/prepare_spec.rb +54 -0
  102. data/spec/lib/castle/session_spec.rb +88 -0
  103. data/spec/lib/castle/singleton_configuration_spec.rb +18 -0
  104. data/spec/lib/castle/utils/clean_invalid_chars_spec.rb +69 -0
  105. data/spec/lib/castle/utils/{cloner_spec.rb → clone_spec.rb} +3 -3
  106. data/spec/lib/castle/utils/deep_symbolize_keys_spec.rb +50 -0
  107. data/spec/lib/castle/utils/{timestamp_spec.rb → get_timestamp_spec.rb} +1 -1
  108. data/spec/lib/castle/utils/{merger_spec.rb → merge_spec.rb} +3 -3
  109. data/spec/lib/castle/verdict_spec.rb +9 -0
  110. data/spec/lib/castle/webhooks/verify_spec.rb +69 -0
  111. data/spec/spec_helper.rb +2 -0
  112. data/spec/support/shared_examples/configuration.rb +129 -0
  113. metadata +133 -56
  114. data/lib/castle/api/request.rb +0 -42
  115. data/lib/castle/api/session.rb +0 -39
  116. data/lib/castle/commands/impersonate.rb +0 -26
  117. data/lib/castle/extractors/headers.rb +0 -45
  118. data/lib/castle/extractors/ip.rb +0 -68
  119. data/lib/castle/failover_auth_response.rb +0 -21
  120. data/lib/castle/headers_filter.rb +0 -35
  121. data/lib/castle/headers_formatter.rb +0 -22
  122. data/lib/castle/review.rb +0 -11
  123. data/lib/castle/utils.rb +0 -55
  124. data/lib/castle/utils/cloner.rb +0 -11
  125. data/lib/castle/utils/timestamp.rb +0 -12
  126. data/spec/lib/castle/api/request_spec.rb +0 -72
  127. data/spec/lib/castle/headers_formatter_spec.rb +0 -25
  128. data/spec/lib/castle/utils_spec.rb +0 -156
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Utils
5
+ module CleanInvalidChars
6
+ class << self
7
+ def call(arg)
8
+ case arg
9
+ when ::String
10
+ arg.encode('UTF-8', invalid: :replace, undef: :replace)
11
+ when ::Hash
12
+ arg.transform_values do |v|
13
+ Castle::Utils::CleanInvalidChars.call(v)
14
+ end
15
+ when ::Array
16
+ arg.map { |el| Castle::Utils::CleanInvalidChars.call(el) }
17
+ else
18
+ arg
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Utils
5
+ # Clones any object
6
+ class Clone
7
+ class << self
8
+ # Returns a cloned object of any type
9
+ def call(object)
10
+ Marshal.load(Marshal.dump(object))
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Utils
5
+ module DeepSymbolizeKeys
6
+ class << self
7
+ # Returns a new hash with all keys converted to symbols, as long as
8
+ # they respond to +to_sym+. This includes the keys from the root hash
9
+ # and from all nested hashes and arrays.
10
+ #
11
+ # hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
12
+ #
13
+ # Castle::Hash.deep_symbolize_keys(hash)
14
+ # # => {:person=>{:name=>"Rob", :age=>"28"}}
15
+ def call(object, &block)
16
+ case object
17
+ when Hash
18
+ object.each_with_object({}) do |(key, value), result|
19
+ result[key.to_sym] = Castle::Utils::DeepSymbolizeKeys.call(value, &block)
20
+ end
21
+ when Array
22
+ object.map { |e| Castle::Utils::DeepSymbolizeKeys.call(e, &block) }
23
+ else
24
+ object
25
+ end
26
+ end
27
+
28
+ def call!(object, &block)
29
+ case object
30
+ when Hash
31
+ object.each_key do |key|
32
+ value = object.delete(key)
33
+ object[key.to_sym] = Castle::Utils::DeepSymbolizeKeys.call!(value, &block)
34
+ end
35
+ object
36
+ when Array
37
+ object.map! { |e| Castle::Utils::DeepSymbolizeKeys.call!(e, &block) }
38
+ else
39
+ object
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Utils
5
+ # Generates a timestamp
6
+ class GetTimestamp
7
+ class << self
8
+ # Returns current time as ISO8601 formatted string
9
+ def call
10
+ Time.now.utc.iso8601(3)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Castle
4
4
  module Utils
5
- class Merger
5
+ class Merge
6
6
  def self.call(base, extra)
7
- base_s = Castle::Utils.deep_symbolize_keys(base)
8
- extra_s = Castle::Utils.deep_symbolize_keys(extra)
7
+ base_s = Castle::Utils::DeepSymbolizeKeys.call(base)
8
+ extra_s = Castle::Utils::DeepSymbolizeKeys.call(extra)
9
9
 
10
10
  extra_s.each do |name, value|
11
11
  if value.nil?
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Utils
5
+ # Code borrowed from ActiveSupport
6
+ class SecureCompare
7
+ class << self
8
+ # @param str_a [String] first string to be compared
9
+ # @param str_b [String] second string to be compared
10
+ def call(str_a, str_b)
11
+ return false unless str_a.bytesize == str_b.bytesize
12
+
13
+ l = str_a.unpack "C#{str_a.bytesize}"
14
+
15
+ res = 0
16
+ str_b.each_byte { |byte| res |= byte ^ l.shift }
17
+ res.zero?
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Castle
4
4
  module Validators
5
+ # Checks if required keys are supported
5
6
  class NotSupported
6
7
  class << self
7
8
  def call(options, keys)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Castle
4
4
  module Validators
5
+ # Checks if required keys are present
5
6
  class Present
6
7
  class << self
7
8
  def call(options, keys)
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ # handles verdict consts
5
+ module Verdict
6
+ # allow
7
+ ALLOW = 'allow'
8
+ # deny
9
+ DENY = 'deny'
10
+ # challenge
11
+ CHALLENGE = 'challenge'
12
+ end
13
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castle
4
- VERSION = '4.1.0'
4
+ VERSION = '6.0.0'
5
5
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module Webhooks
5
+ # Verify a webhook
6
+ class Verify
7
+ class << self
8
+ # Checks if webhook is valid
9
+ # @param webhook [Request]
10
+ # @param config [Castle::Configuration, Castle::SingletonConfiguration]
11
+ def call(webhook, config = Castle.config)
12
+ expected_signature = compute_signature(webhook, config.api_secret)
13
+ signature = webhook.env['HTTP_X_CASTLE_SIGNATURE']
14
+ verify_signature(signature, expected_signature)
15
+ end
16
+
17
+ private
18
+
19
+ # Computes a webhook signature using provided user_id
20
+ # @param webhook [Request]
21
+ # @param api_secret [String]
22
+ def compute_signature(webhook, api_secret)
23
+ Base64.encode64(
24
+ OpenSSL::HMAC.digest(
25
+ OpenSSL::Digest.new('sha256'),
26
+ api_secret,
27
+ Castle::Core::ProcessWebhook.call(webhook)
28
+ )
29
+ ).strip
30
+ end
31
+
32
+ # Check if the signatures are matching
33
+ # @param signature [String] first signature to be compared
34
+ # @param expected_signature [String] second signature to be compared
35
+ def verify_signature(signature, expected_signature)
36
+ return if Castle::Utils::SecureCompare.call(signature, expected_signature)
37
+
38
+ raise Castle::WebhookVerificationError, 'Signature not matching the expected signature'
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -4,7 +4,7 @@ require 'spec_helper'
4
4
  require_relative 'support/all'
5
5
 
6
6
  RSpec.describe HomeController, type: :request do
7
- describe '#index' do
7
+ context 'with index pages' do
8
8
  let(:request) do
9
9
  {
10
10
  'event' => '$login.succeeded',
@@ -16,7 +16,6 @@ RSpec.describe HomeController, type: :request do
16
16
  'context' => {
17
17
  'client_id' => '',
18
18
  'active' => true,
19
- 'origin' => 'web',
20
19
  'headers' => {
21
20
  'Accept' => 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
22
21
  'Authorization' => true,
@@ -45,17 +44,44 @@ RSpec.describe HomeController, type: :request do
45
44
  before do
46
45
  Timecop.freeze(now)
47
46
  stub_request(:post, 'https://api.castle.io/v1/track')
48
- get '/', headers: headers
49
47
  end
50
48
 
51
49
  after { Timecop.return }
52
50
 
53
- it do
54
- assert_requested :post, 'https://api.castle.io/v1/track', times: 1 do |req|
55
- JSON.parse(req.body) == request
51
+ describe '#index1' do
52
+ before { get '/index1', headers: headers }
53
+
54
+ it do
55
+ assert_requested :post, 'https://api.castle.io/v1/track', times: 1 do |req|
56
+ JSON.parse(req.body) == request
57
+ end
56
58
  end
59
+
60
+ it { expect(response).to be_successful }
57
61
  end
58
62
 
59
- it { expect(response).to be_successful }
63
+ describe '#index2' do
64
+ before { get '/index2', headers: headers }
65
+
66
+ it do
67
+ assert_requested :post, 'https://api.castle.io/v1/track', times: 1 do |req|
68
+ JSON.parse(req.body) == request
69
+ end
70
+ end
71
+
72
+ it { expect(response).to be_successful }
73
+ end
74
+
75
+ describe '#index3' do
76
+ before { get '/index3', headers: headers }
77
+
78
+ it do
79
+ assert_requested :post, 'https://api.castle.io/v1/track', times: 1 do |req|
80
+ JSON.parse(req.body) == request
81
+ end
82
+ end
83
+
84
+ it { expect(response).to be_successful }
85
+ end
60
86
  end
61
87
  end
@@ -10,6 +10,8 @@ class TestApp < Rails::Application
10
10
  Rails.logger = config.logger
11
11
 
12
12
  routes.draw do
13
- get '/' => 'home#index'
13
+ get '/index1' => 'home#index1'
14
+ get '/index2' => 'home#index2'
15
+ get '/index3' => 'home#index3'
14
16
  end
15
17
  end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class HomeController < ActionController::Base
4
- def index
5
- request_context = ::Castle::Client.to_context(request)
6
- track_options = ::Castle::Client.to_options(
4
+ # prepare context and calling track with client example
5
+ def index1
6
+ request_context = ::Castle::Context::Prepare.call(request)
7
+ payload = {
7
8
  event: '$login.succeeded',
8
9
  user_id: '123',
9
10
  properties: {
@@ -12,9 +13,50 @@ class HomeController < ActionController::Base
12
13
  user_traits: {
13
14
  key: 'value'
14
15
  }
16
+ }
17
+ client = ::Castle::Client.new(context: request_context)
18
+ client.track(payload)
19
+
20
+ render inline: 'hello'
21
+ end
22
+
23
+ # prepare payload and calling track with client example
24
+ def index2
25
+ payload = ::Castle::Payload::Prepare.call(
26
+ {
27
+ event: '$login.succeeded',
28
+ user_id: '123',
29
+ properties: {
30
+ key: 'value'
31
+ },
32
+ user_traits: {
33
+ key: 'value'
34
+ }
35
+ },
36
+ request
37
+ )
38
+ client = ::Castle::Client.new
39
+ client.track(payload)
40
+
41
+ render inline: 'hello'
42
+ end
43
+
44
+ # prepare payload and calling track with direct API::Track service
45
+ def index3
46
+ payload = ::Castle::Payload::Prepare.call(
47
+ {
48
+ event: '$login.succeeded',
49
+ user_id: '123',
50
+ properties: {
51
+ key: 'value'
52
+ },
53
+ user_traits: {
54
+ key: 'value'
55
+ }
56
+ },
57
+ request
15
58
  )
16
- client = ::Castle::Client.new(request_context)
17
- client.track(track_options)
59
+ Castle::API::Track.call(payload)
18
60
 
19
61
  render inline: 'hello'
20
62
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::API::ApproveDevice do
4
+ before do
5
+ stub_request(:any, /api.castle.io/).with(
6
+ basic_auth: ['', 'secret']
7
+ ).to_return(status: 200, body: '{}', headers: {})
8
+ end
9
+
10
+ describe '.call' do
11
+ subject(:retrieve) { described_class.call(device_token: device_token) }
12
+
13
+ let(:device_token) { '1234' }
14
+
15
+ before { retrieve }
16
+
17
+ it do
18
+ assert_requested :put, "https://api.castle.io/v1/devices/#{device_token}/approve", times: 1
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::API::Authenticate do
4
+ subject(:call_subject) { described_class.call(options) }
5
+
6
+ let(:ip) { '1.2.3.4' }
7
+ let(:cookie_id) { 'abcd' }
8
+ let(:ua) { 'Chrome' }
9
+ let(:env) do
10
+ Rack::MockRequest.env_for(
11
+ '/',
12
+ 'HTTP_USER_AGENT' => ua,
13
+ 'HTTP_X_FORWARDED_FOR' => ip,
14
+ 'HTTP_COOKIE' => "__cid=#{cookie_id};other=efgh"
15
+ )
16
+ end
17
+ let(:request) { Rack::Request.new(env) }
18
+ let(:context) { Castle::Context::Prepare.call(request) }
19
+ let(:time_now) { Time.now }
20
+ let(:time_auto) { time_now.utc.iso8601(3) }
21
+ let(:time_user) { (Time.now - 10_000).utc.iso8601(3) }
22
+ let(:response_body) { {}.to_json }
23
+
24
+ before do
25
+ Timecop.freeze(time_now)
26
+ stub_const('Castle::VERSION', '2.2.0')
27
+ end
28
+
29
+ after { Timecop.return }
30
+
31
+ describe '.call' do
32
+ let(:request_body) do
33
+ { event: '$login.succeeded', context: context, user_id: '1234',
34
+ sent_at: time_auto }
35
+ end
36
+
37
+ context 'when used with symbol keys' do
38
+ before do
39
+ stub_request(:any, /api.castle.io/).with(
40
+ basic_auth: ['', 'secret']
41
+ ).to_return(status: 200, body: response_body, headers: {})
42
+ call_subject
43
+ end
44
+
45
+ let(:options) { { event: '$login.succeeded', user_id: '1234', context: context } }
46
+
47
+ it do
48
+ assert_requested :post, 'https://api.castle.io/v1/authenticate', times: 1 do |req|
49
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
50
+ end
51
+ end
52
+
53
+ context 'when passed timestamp in options and no defined timestamp' do
54
+ let(:options) do
55
+ { event: '$login.succeeded', user_id: '1234', timestamp: time_user, context: context }
56
+ end
57
+ let(:request_body) do
58
+ { event: '$login.succeeded', user_id: '1234', context: context,
59
+ timestamp: time_user, sent_at: time_auto }
60
+ end
61
+
62
+ it do
63
+ assert_requested :post, 'https://api.castle.io/v1/authenticate', times: 1 do |req|
64
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ context 'when denied' do
71
+ let(:failover_appendix) { { failover: false, failover_reason: nil } }
72
+
73
+ let(:options) { { event: '$login.succeeded', user_id: '1234', context: context } }
74
+
75
+ context 'when denied without any risk policy' do
76
+ let(:response_body) { deny_response_without_rp.to_json }
77
+ let(:deny_response_without_rp) do
78
+ {
79
+ action: 'deny',
80
+ user_id: '12345',
81
+ device_token: 'abcdefg1234'
82
+ }
83
+ end
84
+ let(:deny_without_rp_failover_result) do
85
+ deny_response_without_rp.merge(failover_appendix)
86
+ end
87
+
88
+ before do
89
+ stub_request(:any, /api.castle.io/).with(
90
+ basic_auth: ['', 'secret']
91
+ ).to_return(status: 200, body: deny_response_without_rp.to_json, headers: {})
92
+ call_subject
93
+ end
94
+
95
+ it do
96
+ assert_requested :post, 'https://api.castle.io/v1/authenticate', times: 1 do |req|
97
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
98
+ end
99
+ end
100
+
101
+ it { expect(call_subject).to eql(deny_without_rp_failover_result) }
102
+ end
103
+
104
+ context 'when denied with risk policy' do
105
+ let(:deny_response_with_rp) do
106
+ {
107
+ action: 'deny',
108
+ user_id: '12345',
109
+ device_token: 'abcdefg1234',
110
+ risk_policy: {
111
+ id: 'q-rbeMzBTdW2Fd09sbz55A',
112
+ revision_id: 'pke4zqO2TnqVr-NHJOAHEg',
113
+ name: 'Block Users from X',
114
+ type: 'bot'
115
+ }
116
+ }
117
+ end
118
+ let(:response_body) { deny_response_with_rp.to_json }
119
+ let(:deny_with_rp_failover_result) do
120
+ deny_response_with_rp.merge(failover_appendix)
121
+ end
122
+
123
+ before do
124
+ stub_request(:any, /api.castle.io/).with(
125
+ basic_auth: ['', 'secret']
126
+ ).to_return(status: 200, body: deny_response_with_rp.to_json, headers: {})
127
+ call_subject
128
+ end
129
+
130
+ it do
131
+ assert_requested :post, 'https://api.castle.io/v1/authenticate', times: 1 do |req|
132
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
133
+ end
134
+ end
135
+
136
+ it { expect(call_subject).to eql(deny_with_rp_failover_result) }
137
+ end
138
+ end
139
+ end
140
+ end