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.
- checksums.yaml +4 -4
- data/README.md +158 -43
- data/lib/castle.rb +46 -21
- data/lib/castle/api.rb +24 -12
- data/lib/castle/api/approve_device.rb +25 -0
- data/lib/castle/api/authenticate.rb +34 -0
- data/lib/castle/api/end_impersonation.rb +29 -0
- data/lib/castle/api/get_device.rb +25 -0
- data/lib/castle/api/get_devices_for_user.rb +25 -0
- data/lib/castle/api/identify.rb +26 -0
- data/lib/castle/api/report_device.rb +25 -0
- data/lib/castle/api/review.rb +24 -0
- data/lib/castle/api/start_impersonation.rb +29 -0
- data/lib/castle/api/track.rb +26 -0
- data/lib/castle/client.rb +52 -45
- data/lib/castle/{extractors/client_id.rb → client_id/extract.rb} +2 -2
- data/lib/castle/commands/approve_device.rb +21 -0
- data/lib/castle/commands/authenticate.rb +13 -13
- data/lib/castle/commands/end_impersonation.rb +25 -0
- data/lib/castle/commands/get_device.rb +21 -0
- data/lib/castle/commands/get_devices_for_user.rb +21 -0
- data/lib/castle/commands/identify.rb +12 -13
- data/lib/castle/commands/report_device.rb +21 -0
- data/lib/castle/commands/review.rb +6 -3
- data/lib/castle/commands/start_impersonation.rb +25 -0
- data/lib/castle/commands/track.rb +12 -13
- data/lib/castle/configuration.rb +45 -28
- data/lib/castle/context/{default.rb → get_default.rb} +5 -6
- data/lib/castle/context/{merger.rb → merge.rb} +3 -3
- data/lib/castle/context/prepare.rb +18 -0
- data/lib/castle/context/{sanitizer.rb → sanitize.rb} +1 -1
- data/lib/castle/core/get_connection.rb +25 -0
- data/lib/castle/{api/response.rb → core/process_response.rb} +4 -2
- data/lib/castle/core/process_webhook.rb +20 -0
- data/lib/castle/core/send_request.rb +50 -0
- data/lib/castle/errors.rb +2 -0
- data/lib/castle/events.rb +1 -1
- data/lib/castle/failover/prepare_response.rb +23 -0
- data/lib/castle/failover/strategy.rb +20 -0
- data/lib/castle/headers/extract.rb +47 -0
- data/lib/castle/headers/filter.rb +37 -0
- data/lib/castle/headers/format.rb +24 -0
- data/lib/castle/ip/extract.rb +83 -0
- data/lib/castle/logger.rb +19 -0
- data/lib/castle/payload/prepare.rb +27 -0
- data/lib/castle/secure_mode.rb +6 -2
- data/lib/castle/session.rb +18 -0
- data/lib/castle/singleton_configuration.rb +9 -0
- data/lib/castle/utils/clean_invalid_chars.rb +24 -0
- data/lib/castle/utils/clone.rb +15 -0
- data/lib/castle/utils/deep_symbolize_keys.rb +45 -0
- data/lib/castle/utils/get_timestamp.rb +15 -0
- data/lib/castle/utils/{merger.rb → merge.rb} +3 -3
- data/lib/castle/utils/secure_compare.rb +22 -0
- data/lib/castle/validators/not_supported.rb +1 -0
- data/lib/castle/validators/present.rb +1 -0
- data/lib/castle/verdict.rb +13 -0
- data/lib/castle/version.rb +1 -1
- data/lib/castle/webhooks/verify.rb +43 -0
- data/spec/integration/rails/rails_spec.rb +33 -7
- data/spec/integration/rails/support/application.rb +3 -1
- data/spec/integration/rails/support/home_controller.rb +47 -5
- data/spec/lib/castle/api/approve_device_spec.rb +21 -0
- data/spec/lib/castle/api/authenticate_spec.rb +140 -0
- data/spec/lib/castle/api/end_impersonation_spec.rb +59 -0
- data/spec/lib/castle/api/get_device_spec.rb +19 -0
- data/spec/lib/castle/api/get_devices_for_user_spec.rb +19 -0
- data/spec/lib/castle/api/identify_spec.rb +68 -0
- data/spec/lib/castle/api/report_device_spec.rb +21 -0
- data/spec/lib/castle/{review_spec.rb → api/review_spec.rb} +3 -3
- data/spec/lib/castle/api/start_impersonation_spec.rb +59 -0
- data/spec/lib/castle/api/track_spec.rb +68 -0
- data/spec/lib/castle/api_spec.rb +16 -1
- data/spec/lib/castle/{extractors/client_id_spec.rb → client_id/extract_spec.rb} +2 -2
- data/spec/lib/castle/client_spec.rb +41 -23
- data/spec/lib/castle/commands/approve_device_spec.rb +24 -0
- data/spec/lib/castle/commands/authenticate_spec.rb +7 -16
- data/spec/lib/castle/commands/end_impersonation_spec.rb +82 -0
- data/spec/lib/castle/commands/get_device_spec.rb +24 -0
- data/spec/lib/castle/commands/get_devices_for_user_spec.rb +24 -0
- data/spec/lib/castle/commands/identify_spec.rb +5 -16
- data/spec/lib/castle/commands/report_device_spec.rb +24 -0
- data/spec/lib/castle/commands/review_spec.rb +1 -1
- data/spec/lib/castle/commands/{impersonate_spec.rb → start_impersonation_spec.rb} +9 -34
- data/spec/lib/castle/commands/track_spec.rb +5 -16
- data/spec/lib/castle/configuration_spec.rb +9 -138
- data/spec/lib/castle/context/{default_spec.rb → get_default_spec.rb} +1 -2
- data/spec/lib/castle/context/{merger_spec.rb → merge_spec.rb} +1 -1
- data/spec/lib/castle/context/prepare_spec.rb +44 -0
- data/spec/lib/castle/context/{sanitizer_spec.rb → sanitize_spec.rb} +1 -1
- data/spec/lib/castle/core/get_connection_spec.rb +59 -0
- data/spec/lib/castle/{api/response_spec.rb → core/process_response_spec.rb} +56 -1
- data/spec/lib/castle/core/process_webhook_spec.rb +46 -0
- data/spec/lib/castle/core/send_request_spec.rb +102 -0
- data/spec/lib/castle/failover/strategy_spec.rb +12 -0
- data/spec/lib/castle/{extractors/headers_spec.rb → headers/extract_spec.rb} +18 -18
- data/spec/lib/castle/{headers_filter_spec.rb → headers/filter_spec.rb} +6 -5
- data/spec/lib/castle/headers/format_spec.rb +25 -0
- data/spec/lib/castle/{extractors/ip_spec.rb → ip/extract_spec.rb} +35 -7
- data/spec/lib/castle/logger_spec.rb +42 -0
- data/spec/lib/castle/payload/prepare_spec.rb +54 -0
- data/spec/lib/castle/session_spec.rb +88 -0
- data/spec/lib/castle/singleton_configuration_spec.rb +18 -0
- data/spec/lib/castle/utils/clean_invalid_chars_spec.rb +69 -0
- data/spec/lib/castle/utils/{cloner_spec.rb → clone_spec.rb} +3 -3
- data/spec/lib/castle/utils/deep_symbolize_keys_spec.rb +50 -0
- data/spec/lib/castle/utils/{timestamp_spec.rb → get_timestamp_spec.rb} +1 -1
- data/spec/lib/castle/utils/{merger_spec.rb → merge_spec.rb} +3 -3
- data/spec/lib/castle/verdict_spec.rb +9 -0
- data/spec/lib/castle/webhooks/verify_spec.rb +69 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/support/shared_examples/configuration.rb +129 -0
- metadata +133 -56
- data/lib/castle/api/request.rb +0 -42
- data/lib/castle/api/session.rb +0 -39
- data/lib/castle/commands/impersonate.rb +0 -26
- data/lib/castle/extractors/headers.rb +0 -45
- data/lib/castle/extractors/ip.rb +0 -68
- data/lib/castle/failover_auth_response.rb +0 -21
- data/lib/castle/headers_filter.rb +0 -35
- data/lib/castle/headers_formatter.rb +0 -22
- data/lib/castle/review.rb +0 -11
- data/lib/castle/utils.rb +0 -55
- data/lib/castle/utils/cloner.rb +0 -11
- data/lib/castle/utils/timestamp.rb +0 -12
- data/spec/lib/castle/api/request_spec.rb +0 -72
- data/spec/lib/castle/headers_formatter_spec.rb +0 -25
- 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,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
|
5
|
+
class Merge
|
6
6
|
def self.call(base, extra)
|
7
|
-
base_s = Castle::Utils.
|
8
|
-
extra_s = Castle::Utils.
|
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
|
data/lib/castle/version.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
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
|
@@ -1,9 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class HomeController < ActionController::Base
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
-
|
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
|