castle-rb 4.2.1 → 7.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 +160 -45
- data/lib/castle.rb +49 -28
- data/lib/castle/api.rb +21 -14
- data/lib/castle/api/approve_device.rb +20 -0
- data/lib/castle/api/authenticate.rb +37 -0
- data/lib/castle/api/end_impersonation.rb +24 -0
- data/lib/castle/api/filter.rb +37 -0
- data/lib/castle/api/get_device.rb +20 -0
- data/lib/castle/api/get_devices_for_user.rb +20 -0
- data/lib/castle/api/log.rb +37 -0
- data/lib/castle/api/report_device.rb +20 -0
- data/lib/castle/api/risk.rb +37 -0
- data/lib/castle/api/start_impersonation.rb +24 -0
- data/lib/castle/api/track.rb +21 -0
- data/lib/castle/client.rb +78 -51
- data/lib/castle/{extractors/client_id.rb → client_id/extract.rb} +2 -2
- data/lib/castle/commands/approve_device.rb +17 -0
- data/lib/castle/commands/authenticate.rb +13 -13
- data/lib/castle/commands/end_impersonation.rb +25 -0
- data/lib/castle/commands/filter.rb +23 -0
- data/lib/castle/commands/get_device.rb +17 -0
- data/lib/castle/commands/get_devices_for_user.rb +17 -0
- data/lib/castle/commands/log.rb +23 -0
- data/lib/castle/commands/report_device.rb +17 -0
- data/lib/castle/commands/risk.rb +23 -0
- data/lib/castle/commands/start_impersonation.rb +25 -0
- data/lib/castle/commands/track.rb +12 -13
- data/lib/castle/configuration.rb +57 -32
- 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 +27 -0
- data/lib/castle/{api/response.rb → core/process_response.rb} +8 -3
- data/lib/castle/core/process_webhook.rb +25 -0
- data/lib/castle/core/send_request.rb +42 -0
- data/lib/castle/errors.rb +38 -12
- data/lib/castle/failover/prepare_response.rb +18 -0
- data/lib/castle/failover/strategy.rb +23 -0
- data/lib/castle/headers/extract.rb +47 -0
- data/lib/castle/headers/filter.rb +40 -0
- data/lib/castle/headers/format.rb +24 -0
- data/lib/castle/{extractors/ip.rb → ips/extract.rb} +31 -9
- data/lib/castle/logger.rb +19 -0
- data/lib/castle/payload/prepare.rb +26 -0
- data/lib/castle/secure_mode.rb +7 -2
- data/lib/castle/session.rb +18 -0
- data/lib/castle/singleton_configuration.rb +9 -0
- data/lib/castle/support/hanami.rb +2 -6
- data/lib/castle/support/rails.rb +1 -3
- data/lib/castle/utils/clean_invalid_chars.rb +22 -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 +15 -0
- data/lib/castle/version.rb +1 -1
- data/lib/castle/webhooks/verify.rb +45 -0
- data/spec/integration/rails/rails_spec.rb +42 -14
- data/spec/integration/rails/support/application.rb +3 -1
- data/spec/integration/rails/support/home_controller.rb +50 -6
- data/spec/lib/castle/api/approve_device_spec.rb +21 -0
- data/spec/lib/castle/api/authenticate_spec.rb +136 -0
- data/spec/lib/castle/api/end_impersonation_spec.rb +65 -0
- data/spec/lib/castle/api/filter_spec.rb +5 -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/log_spec.rb +5 -0
- data/spec/lib/castle/api/report_device_spec.rb +21 -0
- data/spec/lib/castle/api/risk_spec.rb +5 -0
- data/spec/lib/castle/api/start_impersonation_spec.rb +65 -0
- data/spec/lib/castle/api/track_spec.rb +72 -0
- data/spec/lib/castle/api_spec.rb +14 -15
- data/spec/lib/castle/{extractors/client_id_spec.rb → client_id/extract_spec.rb} +6 -15
- data/spec/lib/castle/client_spec.rb +108 -93
- data/spec/lib/castle/commands/approve_device_spec.rb +24 -0
- data/spec/lib/castle/commands/authenticate_spec.rb +15 -31
- data/spec/lib/castle/commands/end_impersonation_spec.rb +79 -0
- data/spec/lib/castle/commands/filter_spec.rb +99 -0
- data/spec/lib/castle/commands/get_device_spec.rb +24 -0
- data/spec/lib/castle/commands/{review_spec.rb → get_devices_for_user_spec.rb} +7 -7
- data/spec/lib/castle/commands/log_spec.rb +100 -0
- data/spec/lib/castle/commands/report_device_spec.rb +24 -0
- data/spec/lib/castle/commands/risk_spec.rb +100 -0
- data/spec/lib/castle/commands/start_impersonation_spec.rb +79 -0
- data/spec/lib/castle/commands/track_spec.rb +14 -34
- data/spec/lib/castle/configuration_spec.rb +8 -141
- data/spec/lib/castle/context/{default_spec.rb → get_default_spec.rb} +9 -10
- data/spec/lib/castle/context/{merger_spec.rb → merge_spec.rb} +1 -1
- data/spec/lib/castle/context/prepare_spec.rb +43 -0
- data/spec/lib/castle/context/{sanitizer_spec.rb → sanitize_spec.rb} +1 -1
- data/spec/lib/castle/core/get_connection_spec.rb +43 -0
- data/spec/lib/castle/{api/response_spec.rb → core/process_response_spec.rb} +49 -1
- data/spec/lib/castle/core/process_webhook_spec.rb +46 -0
- data/spec/lib/castle/core/send_request_spec.rb +77 -0
- data/spec/lib/castle/failover/strategy_spec.rb +12 -0
- data/spec/lib/castle/{extractors/headers_spec.rb → headers/extract_spec.rb} +18 -20
- data/spec/lib/castle/headers/filter_spec.rb +39 -0
- data/spec/lib/castle/headers/format_spec.rb +25 -0
- data/spec/lib/castle/{extractors/ip_spec.rb → ips/extract_spec.rb} +27 -8
- data/spec/lib/castle/logger_spec.rb +38 -0
- data/spec/lib/castle/payload/prepare_spec.rb +55 -0
- data/spec/lib/castle/session_spec.rb +65 -0
- data/spec/lib/castle/singleton_configuration_spec.rb +14 -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/merge_spec.rb +15 -0
- data/spec/lib/castle/validators/present_spec.rb +5 -6
- data/spec/lib/castle/verdict_spec.rb +9 -0
- data/spec/lib/castle/webhooks/verify_spec.rb +53 -0
- data/spec/lib/castle_spec.rb +4 -10
- data/spec/spec_helper.rb +3 -3
- data/spec/support/shared_examples/action_request.rb +152 -0
- data/spec/support/shared_examples/configuration.rb +101 -0
- metadata +146 -64
- data/lib/castle/api/request.rb +0 -42
- data/lib/castle/api/session.rb +0 -39
- data/lib/castle/commands/identify.rb +0 -23
- data/lib/castle/commands/impersonate.rb +0 -26
- data/lib/castle/commands/review.rb +0 -14
- data/lib/castle/events.rb +0 -49
- data/lib/castle/extractors/headers.rb +0 -45
- 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/commands/identify_spec.rb +0 -88
- data/spec/lib/castle/commands/impersonate_spec.rb +0 -107
- data/spec/lib/castle/events_spec.rb +0 -5
- data/spec/lib/castle/headers_filter_spec.rb +0 -37
- data/spec/lib/castle/headers_formatter_spec.rb +0 -25
- data/spec/lib/castle/review_spec.rb +0 -19
- data/spec/lib/castle/utils/merger_spec.rb +0 -13
- data/spec/lib/castle/utils_spec.rb +0 -156
data/lib/castle/api/request.rb
DELETED
@@ -1,42 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Castle
|
4
|
-
module API
|
5
|
-
# this class is responsible for making requests to api
|
6
|
-
module Request
|
7
|
-
# Default headers that we add to passed ones
|
8
|
-
DEFAULT_HEADERS = {
|
9
|
-
'Content-Type' => 'application/json'
|
10
|
-
}.freeze
|
11
|
-
|
12
|
-
private_constant :DEFAULT_HEADERS
|
13
|
-
|
14
|
-
class << self
|
15
|
-
def call(command, api_secret, headers)
|
16
|
-
Castle::API::Session.get.request(
|
17
|
-
build(
|
18
|
-
command,
|
19
|
-
headers.merge(DEFAULT_HEADERS),
|
20
|
-
api_secret
|
21
|
-
)
|
22
|
-
)
|
23
|
-
end
|
24
|
-
|
25
|
-
def build(command, headers, api_secret)
|
26
|
-
request_obj = Net::HTTP.const_get(
|
27
|
-
command.method.to_s.capitalize
|
28
|
-
).new("#{Castle.config.url_prefix}/#{command.path}", headers)
|
29
|
-
|
30
|
-
unless command.method == :get
|
31
|
-
request_obj.body = ::Castle::Utils.replace_invalid_characters(
|
32
|
-
command.data
|
33
|
-
).to_json
|
34
|
-
end
|
35
|
-
|
36
|
-
request_obj.basic_auth('', api_secret)
|
37
|
-
request_obj
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
data/lib/castle/api/session.rb
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'singleton'
|
4
|
-
|
5
|
-
module Castle
|
6
|
-
module API
|
7
|
-
# this class keeps http config object
|
8
|
-
# and provides start/finish methods for persistent connection usage
|
9
|
-
# when there is a need of sending multiple requests at once
|
10
|
-
class Session
|
11
|
-
include Singleton
|
12
|
-
|
13
|
-
attr_accessor :http
|
14
|
-
|
15
|
-
def initialize
|
16
|
-
reset
|
17
|
-
end
|
18
|
-
|
19
|
-
def reset
|
20
|
-
@http = Net::HTTP.new(Castle.config.host, Castle.config.port)
|
21
|
-
@http.read_timeout = Castle.config.request_timeout / 1000.0
|
22
|
-
|
23
|
-
if Castle.config.port == 443
|
24
|
-
@http.use_ssl = true
|
25
|
-
@http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
26
|
-
end
|
27
|
-
|
28
|
-
@http
|
29
|
-
end
|
30
|
-
|
31
|
-
class << self
|
32
|
-
# @return [Net::HTTP]
|
33
|
-
def get
|
34
|
-
instance.http
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
@@ -1,23 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Castle
|
4
|
-
module Commands
|
5
|
-
class Identify
|
6
|
-
def initialize(context)
|
7
|
-
@context = context
|
8
|
-
end
|
9
|
-
|
10
|
-
def build(options = {})
|
11
|
-
Castle::Validators::NotSupported.call(options, %i[properties])
|
12
|
-
context = Castle::Context::Merger.call(@context, options[:context])
|
13
|
-
context = Castle::Context::Sanitizer.call(context)
|
14
|
-
|
15
|
-
Castle::Command.new(
|
16
|
-
'identify',
|
17
|
-
options.merge(context: context, sent_at: Castle::Utils::Timestamp.call),
|
18
|
-
:post
|
19
|
-
)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
@@ -1,26 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Castle
|
4
|
-
module Commands
|
5
|
-
# builder for impersonate command
|
6
|
-
class Impersonate
|
7
|
-
def initialize(context)
|
8
|
-
@context = context
|
9
|
-
end
|
10
|
-
|
11
|
-
def build(options = {})
|
12
|
-
Castle::Validators::Present.call(options, %i[user_id])
|
13
|
-
context = Castle::Context::Merger.call(@context, options[:context])
|
14
|
-
context = Castle::Context::Sanitizer.call(context)
|
15
|
-
|
16
|
-
Castle::Validators::Present.call(context, %i[user_agent ip])
|
17
|
-
|
18
|
-
Castle::Command.new(
|
19
|
-
'impersonate',
|
20
|
-
options.merge(context: context, sent_at: Castle::Utils::Timestamp.call),
|
21
|
-
options[:reset] ? :delete : :post
|
22
|
-
)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
@@ -1,14 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Castle
|
4
|
-
module Commands
|
5
|
-
class Review
|
6
|
-
class << self
|
7
|
-
def build(review_id)
|
8
|
-
Castle::Validators::Present.call({ review_id: review_id }, %i[review_id])
|
9
|
-
Castle::Command.new("reviews/#{review_id}", nil, :get)
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
data/lib/castle/events.rb
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Castle
|
4
|
-
# list of events based on https://docs.castle.io/api_reference/#list-of-recognized-events
|
5
|
-
module Events
|
6
|
-
# Record when a user succesfully logs in.
|
7
|
-
LOGIN_SUCCEEDED = '$login.succeeded'
|
8
|
-
# Record when a user failed to log in.
|
9
|
-
LOGIN_FAILED = '$login.failed'
|
10
|
-
# Record when a user logs out.
|
11
|
-
LOGOUT_SUCCEEDED = '$logout.succeeded'
|
12
|
-
# Record when a user updated their profile (including password, email, phone, etc).
|
13
|
-
PROFILE_UPDATE_SUCCEEDED = '$profile_update.succeeded'
|
14
|
-
# Record errors when updating profile.
|
15
|
-
PROFILE_UPDATE_FAILED = '$profile_update.failed'
|
16
|
-
# Capture account creation, both when a user signs up as well as when created manually
|
17
|
-
# by an administrator.
|
18
|
-
REGISTRATION_SUCCEEDED = '$registration.succeeded'
|
19
|
-
# Record when an account failed to be created.
|
20
|
-
REGISTRATION_FAILED = '$registration.failed'
|
21
|
-
# The user completed all of the steps in the password reset process and the password was
|
22
|
-
# successfully reset.Password resets do not required knowledge of the current password.
|
23
|
-
PASSWORD_RESET_SUCCEEDED = '$password_reset.succeeded'
|
24
|
-
# Use to record when a user failed to reset their password.
|
25
|
-
PASSWORD_RESET_FAILED = '$password_reset.failed'
|
26
|
-
# The user successfully requested a password reset.
|
27
|
-
PASSWORD_RESET_REQUEST_SUCCCEEDED = '$password_reset_request.succeeded'
|
28
|
-
# The user failed to request a password reset.
|
29
|
-
PASSWORD_RESET_REQUEST_FAILED = '$password_reset_request.failed'
|
30
|
-
# User account has been reset.
|
31
|
-
INCIDENT_MITIGATED = '$incident.mitigated'
|
32
|
-
# User confirmed malicious activity.
|
33
|
-
REVIEW_ESCALATED = '$review.escalated'
|
34
|
-
# User confirmed safe activity.
|
35
|
-
REVIEW_RESOLVED = '$review.resolved'
|
36
|
-
# Record when a user is prompted with additional verification, such as two-factor
|
37
|
-
# authentication or a captcha.
|
38
|
-
CHALLENGE_REQUESTED = '$challenge.requested'
|
39
|
-
# Record when additional verification was successful.
|
40
|
-
CHALLENGE_SUCCEEDED = '$challenge.succeeded'
|
41
|
-
# Record when additional verification failed.
|
42
|
-
CHALLENGE_FAILED = '$challenge.failed'
|
43
|
-
# Record when a user attempts an in-app transaction, such as a purchase or withdrawal.
|
44
|
-
TRANSACTION_ATTEMPTED = '$transaction.attempted'
|
45
|
-
# Record when a user session is extended, or use any time you want
|
46
|
-
# to re-authenticate a user mid-session.
|
47
|
-
SESSION_EXTENDED = '$session.extended'
|
48
|
-
end
|
49
|
-
end
|
@@ -1,45 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Castle
|
4
|
-
module Extractors
|
5
|
-
# used for extraction of cookies and headers from the request
|
6
|
-
class Headers
|
7
|
-
# Headers that we will never scrub, even if they land on the configuration blacklist.
|
8
|
-
ALWAYS_WHITELISTED = %w[User-Agent].freeze
|
9
|
-
|
10
|
-
# Headers that will always be scrubbed, even if whitelisted.
|
11
|
-
ALWAYS_BLACKLISTED = %w[Cookie Authorization].freeze
|
12
|
-
|
13
|
-
private_constant :ALWAYS_WHITELISTED, :ALWAYS_BLACKLISTED
|
14
|
-
|
15
|
-
# @param headers [Hash]
|
16
|
-
def initialize(headers)
|
17
|
-
@headers = headers
|
18
|
-
@no_whitelist = Castle.config.whitelisted.empty?
|
19
|
-
end
|
20
|
-
|
21
|
-
# Serialize HTTP headers
|
22
|
-
# @return [Hash]
|
23
|
-
def call
|
24
|
-
@headers.each_with_object({}) do |(name, value), acc|
|
25
|
-
acc[name] = header_value(name, value)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
# scrub header value
|
32
|
-
# @param name [String]
|
33
|
-
# @param value [String]
|
34
|
-
# @return [TrueClass | FalseClass | String]
|
35
|
-
def header_value(name, value)
|
36
|
-
return true if ALWAYS_BLACKLISTED.include?(name)
|
37
|
-
return value if ALWAYS_WHITELISTED.include?(name)
|
38
|
-
return true if Castle.config.blacklisted.include?(name)
|
39
|
-
return value if @no_whitelist || Castle.config.whitelisted.include?(name)
|
40
|
-
|
41
|
-
true
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
@@ -1,21 +0,0 @@
|
|
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
|
@@ -1,35 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Castle
|
4
|
-
# used for preparing valuable headers list
|
5
|
-
class HeadersFilter
|
6
|
-
# headers filter
|
7
|
-
# HTTP_ - this is how Rack prefixes incoming HTTP headers
|
8
|
-
# CONTENT_LENGTH - for responses without Content-Length or Transfer-Encoding header
|
9
|
-
# REMOTE_ADDR - ip address header returned by web server
|
10
|
-
VALUABLE_HEADERS = /^
|
11
|
-
HTTP(?:_|-).*|
|
12
|
-
CONTENT(?:_|-)LENGTH|
|
13
|
-
REMOTE(?:_|-)ADDR
|
14
|
-
$/xi.freeze
|
15
|
-
|
16
|
-
private_constant :VALUABLE_HEADERS
|
17
|
-
|
18
|
-
# @param request [Rack::Request]
|
19
|
-
def initialize(request)
|
20
|
-
@request_env = request.env
|
21
|
-
@formatter = HeadersFormatter
|
22
|
-
end
|
23
|
-
|
24
|
-
# Serialize HTTP headers
|
25
|
-
# @return [Hash]
|
26
|
-
def call
|
27
|
-
@request_env.keys.each_with_object({}) do |header_name, acc|
|
28
|
-
next unless header_name.match(VALUABLE_HEADERS)
|
29
|
-
|
30
|
-
formatted_name = @formatter.call(header_name)
|
31
|
-
acc[formatted_name] = @request_env[header_name]
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
@@ -1,22 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Castle
|
4
|
-
# formats header name
|
5
|
-
class HeadersFormatter
|
6
|
-
class << self
|
7
|
-
# @param header [String]
|
8
|
-
# @return [String]
|
9
|
-
def call(header)
|
10
|
-
format(header.to_s.gsub(/^HTTP(?:_|-)/i, ''))
|
11
|
-
end
|
12
|
-
|
13
|
-
private
|
14
|
-
|
15
|
-
# @param header [String]
|
16
|
-
# @return [String]
|
17
|
-
def format(header)
|
18
|
-
header.split(/_|-/).map(&:capitalize).join('-')
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
data/lib/castle/review.rb
DELETED
data/lib/castle/utils.rb
DELETED
@@ -1,55 +0,0 @@
|
|
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
|
data/lib/castle/utils/cloner.rb
DELETED
@@ -1,72 +0,0 @@
|
|
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(:session) { instance_double('Castle::API::Session') }
|
8
|
-
let(:http) { instance_double('Net::HTTP') }
|
9
|
-
let(:command) { Castle::Commands::Track.new({}).build(event: '$login.succeeded') }
|
10
|
-
let(:headers) { {} }
|
11
|
-
let(:api_secret) { 'secret' }
|
12
|
-
let(:request_build) { {} }
|
13
|
-
let(:expected_headers) { { 'Content-Type' => 'application/json' } }
|
14
|
-
|
15
|
-
before do
|
16
|
-
allow(Castle::API::Session).to receive(:instance).and_return(session)
|
17
|
-
allow(session).to receive(:http).and_return(http)
|
18
|
-
allow(http).to receive(:request)
|
19
|
-
allow(described_class).to receive(:build).and_return(request_build)
|
20
|
-
call
|
21
|
-
end
|
22
|
-
|
23
|
-
it do
|
24
|
-
expect(described_class).to have_received(:build).with(command, expected_headers, api_secret)
|
25
|
-
end
|
26
|
-
|
27
|
-
it { expect(http).to have_received(:request).with(request_build) }
|
28
|
-
end
|
29
|
-
|
30
|
-
describe '#build' do
|
31
|
-
subject(:build) { described_class.build(command, headers, api_secret) }
|
32
|
-
|
33
|
-
let(:headers) { { 'SAMPLE-HEADER' => '1' } }
|
34
|
-
let(:api_secret) { 'secret' }
|
35
|
-
|
36
|
-
context 'when get' do
|
37
|
-
let(:command) { Castle::Commands::Review.build(review_id) }
|
38
|
-
let(:review_id) { SecureRandom.uuid }
|
39
|
-
|
40
|
-
it { expect(build.body).to be_nil }
|
41
|
-
it { expect(build.method).to eql('GET') }
|
42
|
-
it { expect(build.path).to eql("/v1/#{command.path}") }
|
43
|
-
it { expect(build.to_hash).to have_key('authorization') }
|
44
|
-
it { expect(build.to_hash).to have_key('sample-header') }
|
45
|
-
it { expect(build.to_hash['sample-header']).to eql(['1']) }
|
46
|
-
end
|
47
|
-
|
48
|
-
context 'when post' do
|
49
|
-
let(:time) { Time.now.utc.iso8601(3) }
|
50
|
-
let(:command) do
|
51
|
-
Castle::Commands::Track.new({}).build(event: '$login.succeeded', name: "\xC4")
|
52
|
-
end
|
53
|
-
let(:expected_body) do
|
54
|
-
{
|
55
|
-
event: '$login.succeeded',
|
56
|
-
name: '�',
|
57
|
-
context: {},
|
58
|
-
sent_at: time
|
59
|
-
}
|
60
|
-
end
|
61
|
-
|
62
|
-
before { allow(Castle::Utils::Timestamp).to receive(:call).and_return(time) }
|
63
|
-
|
64
|
-
it { expect(build.body).to be_eql(expected_body.to_json) }
|
65
|
-
it { expect(build.method).to eql('POST') }
|
66
|
-
it { expect(build.path).to eql("/v1/#{command.path}") }
|
67
|
-
it { expect(build.to_hash).to have_key('authorization') }
|
68
|
-
it { expect(build.to_hash).to have_key('sample-header') }
|
69
|
-
it { expect(build.to_hash['sample-header']).to eql(['1']) }
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|