castle-rb 4.2.0 → 6.0.1

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 (127) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +156 -41
  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/{extractors/ip.rb → ips/extract.rb} +29 -9
  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 → ips/extract_spec.rb} +30 -2
  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/failover_auth_response.rb +0 -21
  119. data/lib/castle/headers_filter.rb +0 -35
  120. data/lib/castle/headers_formatter.rb +0 -22
  121. data/lib/castle/review.rb +0 -11
  122. data/lib/castle/utils.rb +0 -55
  123. data/lib/castle/utils/cloner.rb +0 -11
  124. data/lib/castle/utils/timestamp.rb +0 -12
  125. data/spec/lib/castle/api/request_spec.rb +0 -72
  126. data/spec/lib/castle/headers_formatter_spec.rb +0 -25
  127. data/spec/lib/castle/utils_spec.rb +0 -156
@@ -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
@@ -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,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,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
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- class Review
5
- def self.retrieve(review_id)
6
- Castle::API.request(
7
- Castle::Commands::Review.build(review_id)
8
- )
9
- end
10
- end
11
- end
@@ -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
@@ -1,11 +0,0 @@
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
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- module Utils
5
- # generates proper timestamp
6
- class Timestamp
7
- def self.call
8
- Time.now.utc.iso8601(3)
9
- end
10
- end
11
- end
12
- end
@@ -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
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- describe Castle::HeadersFormatter do
4
- subject(:formatter) { described_class }
5
-
6
- it 'removes HTTP_' do
7
- expect(formatter.call('HTTP_X_TEST')).to be_eql('X-Test')
8
- end
9
-
10
- it 'capitalizes header' do
11
- expect(formatter.call('X_TEST')).to be_eql('X-Test')
12
- end
13
-
14
- it 'ignores letter case and -_ divider' do
15
- expect(formatter.call('http-X_teST')).to be_eql('X-Test')
16
- end
17
-
18
- it 'does not remove http if there is no _- char' do
19
- expect(formatter.call('httpX_teST')).to be_eql('Httpx-Test')
20
- end
21
-
22
- it 'capitalizes' do
23
- expect(formatter.call(:clearance)).to be_eql('Clearance')
24
- end
25
- end
@@ -1,156 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- describe Castle::Utils do
4
- let(:nested_strings) { { 'a' => { 'b' => { 'c' => 3 } } } }
5
- let(:nested_symbols) { { a: { b: { c: 3 } } } }
6
- let(:nested_mixed) { { 'a' => { b: { 'c' => 3 } } } }
7
- let(:string_array_of_hashes) { { 'a' => [{ 'b' => 2 }, { 'c' => 3 }, 4] } }
8
- let(:symbol_array_of_hashes) { { a: [{ b: 2 }, { c: 3 }, 4] } }
9
- let(:mixed_array_of_hashes) { { a: [{ b: 2 }, { 'c' => 3 }, 4] } }
10
-
11
- describe '#deep_symbolize_keys' do
12
- subject { described_class.deep_symbolize_keys(hash) }
13
-
14
- context 'when nested_symbols' do
15
- let(:hash) { nested_symbols }
16
-
17
- it { is_expected.to eq(nested_symbols) }
18
- end
19
-
20
- context 'when nested_strings' do
21
- let(:hash) { nested_strings }
22
-
23
- it { is_expected.to eq(nested_symbols) }
24
- end
25
-
26
- context 'when nested_mixed' do
27
- let(:hash) { nested_mixed }
28
-
29
- it { is_expected.to eq(nested_symbols) }
30
- end
31
-
32
- context 'when string_array_of_hashes' do
33
- let(:hash) { string_array_of_hashes }
34
-
35
- it { is_expected.to eq(symbol_array_of_hashes) }
36
- end
37
-
38
- context 'when symbol_array_of_hashes' do
39
- let(:hash) { symbol_array_of_hashes }
40
-
41
- it { is_expected.to eq(symbol_array_of_hashes) }
42
- end
43
-
44
- context 'when mixed_array_of_hashes' do
45
- let(:hash) { mixed_array_of_hashes }
46
-
47
- it { is_expected.to eq(symbol_array_of_hashes) }
48
- end
49
- end
50
-
51
- describe '#cloner' do
52
- subject { described_class.deep_symbolize_keys!(Castle::Utils::Cloner.call(hash)) }
53
-
54
- context 'when nested_symbols' do
55
- let(:hash) { nested_symbols }
56
-
57
- it { is_expected.to eq(nested_symbols) }
58
- end
59
-
60
- context 'when nested_strings' do
61
- let(:hash) { nested_strings }
62
-
63
- it { is_expected.to eq(nested_symbols) }
64
- end
65
-
66
- context 'when nested_mixed' do
67
- let(:hash) { nested_mixed }
68
-
69
- it { is_expected.to eq(nested_symbols) }
70
- end
71
-
72
- context 'when string_array_of_hashes' do
73
- let(:hash) { string_array_of_hashes }
74
-
75
- it { is_expected.to eq(symbol_array_of_hashes) }
76
- end
77
-
78
- context 'when symbol_array_of_hashes' do
79
- let(:hash) { symbol_array_of_hashes }
80
-
81
- it { is_expected.to eq(symbol_array_of_hashes) }
82
- end
83
-
84
- context 'when mixed_array_of_hashes' do
85
- let(:hash) { mixed_array_of_hashes }
86
-
87
- it { is_expected.to eq(symbol_array_of_hashes) }
88
- end
89
- end
90
-
91
- describe '::replace_invalid_characters' do
92
- subject { described_class.replace_invalid_characters(input) }
93
-
94
- context 'when input is a string' do
95
- let(:input) { '1234' }
96
-
97
- it { is_expected.to eq input }
98
- end
99
-
100
- context 'when input is an array' do
101
- let(:input) { [1, 2, 3, '4'] }
102
-
103
- it { is_expected.to eq input }
104
- end
105
-
106
- context 'when input is a hash' do
107
- let(:input) { { user_id: 1 } }
108
-
109
- it { is_expected.to eq input }
110
- end
111
-
112
- context 'when input is nil' do
113
- let(:input) { nil }
114
-
115
- it { is_expected.to eq input }
116
- end
117
-
118
- context 'when input is a nested hash' do
119
- let(:input) { { user: { id: 1 } } }
120
-
121
- it { is_expected.to eq input }
122
- end
123
-
124
- context 'with invalid UTF-8 characters' do
125
- context 'when input is a hash' do
126
- let(:input) { { user_id: "inv\xC4lid" } }
127
-
128
- it { is_expected.to eq(user_id: 'inv�lid') }
129
- end
130
-
131
- context 'when input is a nested hash' do
132
- let(:input) { { user: { id: "inv\xC4lid" } } }
133
-
134
- it { is_expected.to eq(user: { id: 'inv�lid' }) }
135
- end
136
-
137
- context 'when input is an array of hashes' do
138
- let(:input) { [{ user: "inv\xC4lid" }] * 2 }
139
-
140
- it { is_expected.to eq([{ user: 'inv�lid' }, { user: 'inv�lid' }]) }
141
- end
142
-
143
- context 'when input is an array' do
144
- let(:input) { ["inv\xC4lid"] * 2 }
145
-
146
- it { is_expected.to eq(['inv�lid', 'inv�lid']) }
147
- end
148
-
149
- context 'when input is a hash with array in key' do
150
- let(:input) { { items: ["inv\xC4lid"] * 2 } }
151
-
152
- it { is_expected.to eq(items: ['inv�lid', 'inv�lid']) }
153
- end
154
- end
155
- end
156
- end