castle-rb 5.0.0 → 7.1.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 (144) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +113 -39
  3. data/lib/castle.rb +49 -29
  4. data/lib/castle/api.rb +20 -16
  5. data/lib/castle/api/approve_device.rb +20 -0
  6. data/lib/castle/api/authenticate.rb +37 -0
  7. data/lib/castle/api/end_impersonation.rb +24 -0
  8. data/lib/castle/api/filter.rb +37 -0
  9. data/lib/castle/api/get_device.rb +20 -0
  10. data/lib/castle/api/get_devices_for_user.rb +20 -0
  11. data/lib/castle/api/log.rb +37 -0
  12. data/lib/castle/api/report_device.rb +20 -0
  13. data/lib/castle/api/risk.rb +37 -0
  14. data/lib/castle/api/start_impersonation.rb +24 -0
  15. data/lib/castle/api/track.rb +21 -0
  16. data/lib/castle/client.rb +74 -68
  17. data/lib/castle/{extractors/client_id.rb → client_id/extract.rb} +2 -2
  18. data/lib/castle/commands/approve_device.rb +17 -0
  19. data/lib/castle/commands/authenticate.rb +13 -13
  20. data/lib/castle/commands/end_impersonation.rb +25 -0
  21. data/lib/castle/commands/filter.rb +22 -0
  22. data/lib/castle/commands/get_device.rb +17 -0
  23. data/lib/castle/commands/get_devices_for_user.rb +17 -0
  24. data/lib/castle/commands/log.rb +22 -0
  25. data/lib/castle/commands/report_device.rb +17 -0
  26. data/lib/castle/commands/risk.rb +22 -0
  27. data/lib/castle/commands/start_impersonation.rb +25 -0
  28. data/lib/castle/commands/track.rb +12 -13
  29. data/lib/castle/configuration.rb +31 -23
  30. data/lib/castle/context/{default.rb → get_default.rb} +5 -6
  31. data/lib/castle/context/{merger.rb → merge.rb} +3 -3
  32. data/lib/castle/context/prepare.rb +18 -0
  33. data/lib/castle/context/{sanitizer.rb → sanitize.rb} +1 -1
  34. data/lib/castle/core/get_connection.rb +27 -0
  35. data/lib/castle/{api/response.rb → core/process_response.rb} +8 -3
  36. data/lib/castle/core/process_webhook.rb +25 -0
  37. data/lib/castle/core/send_request.rb +42 -0
  38. data/lib/castle/errors.rb +38 -12
  39. data/lib/castle/failover/prepare_response.rb +28 -0
  40. data/lib/castle/failover/strategy.rb +23 -0
  41. data/lib/castle/{extractors/headers.rb → headers/extract.rb} +8 -6
  42. data/lib/castle/headers/filter.rb +40 -0
  43. data/lib/castle/headers/format.rb +24 -0
  44. data/lib/castle/{extractors/ip.rb → ips/extract.rb} +11 -7
  45. data/lib/castle/logger.rb +19 -0
  46. data/lib/castle/payload/prepare.rb +26 -0
  47. data/lib/castle/secure_mode.rb +7 -2
  48. data/lib/castle/session.rb +18 -0
  49. data/lib/castle/singleton_configuration.rb +9 -0
  50. data/lib/castle/support/hanami.rb +2 -6
  51. data/lib/castle/support/rails.rb +1 -3
  52. data/lib/castle/utils/clean_invalid_chars.rb +22 -0
  53. data/lib/castle/utils/clone.rb +15 -0
  54. data/lib/castle/utils/deep_symbolize_keys.rb +45 -0
  55. data/lib/castle/utils/get_timestamp.rb +15 -0
  56. data/lib/castle/utils/{merger.rb → merge.rb} +3 -3
  57. data/lib/castle/utils/secure_compare.rb +22 -0
  58. data/lib/castle/validators/not_supported.rb +1 -0
  59. data/lib/castle/validators/present.rb +1 -0
  60. data/lib/castle/verdict.rb +15 -0
  61. data/lib/castle/version.rb +1 -1
  62. data/lib/castle/webhooks/verify.rb +45 -0
  63. data/spec/integration/rails/rails_spec.rb +42 -14
  64. data/spec/integration/rails/support/application.rb +3 -1
  65. data/spec/integration/rails/support/home_controller.rb +50 -6
  66. data/spec/lib/castle/api/approve_device_spec.rb +21 -0
  67. data/spec/lib/castle/api/authenticate_spec.rb +136 -0
  68. data/spec/lib/castle/api/end_impersonation_spec.rb +65 -0
  69. data/spec/lib/castle/api/filter_spec.rb +5 -0
  70. data/spec/lib/castle/api/get_device_spec.rb +19 -0
  71. data/spec/lib/castle/api/get_devices_for_user_spec.rb +19 -0
  72. data/spec/lib/castle/api/log_spec.rb +5 -0
  73. data/spec/lib/castle/api/report_device_spec.rb +21 -0
  74. data/spec/lib/castle/api/risk_spec.rb +5 -0
  75. data/spec/lib/castle/api/start_impersonation_spec.rb +65 -0
  76. data/spec/lib/castle/api/track_spec.rb +72 -0
  77. data/spec/lib/castle/api_spec.rb +14 -15
  78. data/spec/lib/castle/{extractors/client_id_spec.rb → client_id/extract_spec.rb} +6 -15
  79. data/spec/lib/castle/client_spec.rb +110 -92
  80. data/spec/lib/castle/commands/approve_device_spec.rb +24 -0
  81. data/spec/lib/castle/commands/authenticate_spec.rb +15 -31
  82. data/spec/lib/castle/commands/end_impersonation_spec.rb +79 -0
  83. data/spec/lib/castle/commands/filter_spec.rb +72 -0
  84. data/spec/lib/castle/commands/get_device_spec.rb +24 -0
  85. data/spec/lib/castle/commands/{review_spec.rb → get_devices_for_user_spec.rb} +7 -7
  86. data/spec/lib/castle/commands/log_spec.rb +73 -0
  87. data/spec/lib/castle/commands/report_device_spec.rb +24 -0
  88. data/spec/lib/castle/commands/risk_spec.rb +73 -0
  89. data/spec/lib/castle/commands/{impersonate_spec.rb → start_impersonation_spec.rb} +13 -41
  90. data/spec/lib/castle/commands/track_spec.rb +14 -34
  91. data/spec/lib/castle/configuration_spec.rb +8 -141
  92. data/spec/lib/castle/context/{default_spec.rb → get_default_spec.rb} +9 -10
  93. data/spec/lib/castle/context/{merger_spec.rb → merge_spec.rb} +1 -1
  94. data/spec/lib/castle/context/prepare_spec.rb +43 -0
  95. data/spec/lib/castle/context/{sanitizer_spec.rb → sanitize_spec.rb} +1 -1
  96. data/spec/lib/castle/core/get_connection_spec.rb +43 -0
  97. data/spec/lib/castle/{api/response_spec.rb → core/process_response_spec.rb} +49 -1
  98. data/spec/lib/castle/core/process_webhook_spec.rb +46 -0
  99. data/spec/lib/castle/{api/request_spec.rb → core/send_request_spec.rb} +16 -37
  100. data/spec/lib/castle/failover/strategy_spec.rb +12 -0
  101. data/spec/lib/castle/{extractors/headers_spec.rb → headers/extract_spec.rb} +7 -9
  102. data/spec/lib/castle/headers/filter_spec.rb +39 -0
  103. data/spec/lib/castle/headers/format_spec.rb +25 -0
  104. data/spec/lib/castle/{extractors/ip_spec.rb → ips/extract_spec.rb} +5 -14
  105. data/spec/lib/castle/logger_spec.rb +38 -0
  106. data/spec/lib/castle/payload/prepare_spec.rb +55 -0
  107. data/spec/lib/castle/session_spec.rb +65 -0
  108. data/spec/lib/castle/singleton_configuration_spec.rb +14 -0
  109. data/spec/lib/castle/utils/clean_invalid_chars_spec.rb +69 -0
  110. data/spec/lib/castle/utils/{cloner_spec.rb → clone_spec.rb} +3 -3
  111. data/spec/lib/castle/utils/deep_symbolize_keys_spec.rb +50 -0
  112. data/spec/lib/castle/utils/{timestamp_spec.rb → get_timestamp_spec.rb} +1 -1
  113. data/spec/lib/castle/utils/merge_spec.rb +15 -0
  114. data/spec/lib/castle/validators/present_spec.rb +5 -6
  115. data/spec/lib/castle/verdict_spec.rb +9 -0
  116. data/spec/lib/castle/webhooks/verify_spec.rb +53 -0
  117. data/spec/lib/castle_spec.rb +4 -10
  118. data/spec/spec_helper.rb +3 -3
  119. data/spec/support/shared_examples/action_request.rb +155 -0
  120. data/spec/support/shared_examples/configuration.rb +101 -0
  121. metadata +144 -67
  122. data/lib/castle/api/connection.rb +0 -24
  123. data/lib/castle/api/request.rb +0 -42
  124. data/lib/castle/api/session.rb +0 -20
  125. data/lib/castle/commands/identify.rb +0 -23
  126. data/lib/castle/commands/impersonate.rb +0 -26
  127. data/lib/castle/commands/review.rb +0 -14
  128. data/lib/castle/events.rb +0 -49
  129. data/lib/castle/failover_auth_response.rb +0 -21
  130. data/lib/castle/headers_filter.rb +0 -35
  131. data/lib/castle/headers_formatter.rb +0 -22
  132. data/lib/castle/review.rb +0 -11
  133. data/lib/castle/utils.rb +0 -55
  134. data/lib/castle/utils/cloner.rb +0 -11
  135. data/lib/castle/utils/timestamp.rb +0 -12
  136. data/spec/lib/castle/api/connection_spec.rb +0 -59
  137. data/spec/lib/castle/api/session_spec.rb +0 -86
  138. data/spec/lib/castle/commands/identify_spec.rb +0 -88
  139. data/spec/lib/castle/events_spec.rb +0 -5
  140. data/spec/lib/castle/headers_filter_spec.rb +0 -38
  141. data/spec/lib/castle/headers_formatter_spec.rb +0 -25
  142. data/spec/lib/castle/review_spec.rb +0 -19
  143. data/spec/lib/castle/utils/merger_spec.rb +0 -13
  144. data/spec/lib/castle/utils_spec.rb +0 -156
@@ -1,86 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- describe Castle::API::Session do
4
- describe '.call' do
5
- context 'when ssl false' do
6
- let(:localhost) { 'localhost' }
7
- let(:port) { 3002 }
8
-
9
- before do
10
- Castle.config.url = 'http://localhost:3002'
11
- stub_request(:get, 'localhost:3002/test').to_return(status: 200, body: '{}', headers: {})
12
- end
13
-
14
- context 'with block' do
15
- let(:api_url) { '/test' }
16
- let(:request) { Net::HTTP::Get.new(api_url) }
17
-
18
- before do
19
- allow(Net::HTTP)
20
- .to receive(:new)
21
- .with(localhost, port)
22
- .and_call_original
23
-
24
- described_class.call do |http|
25
- http.request(request)
26
- end
27
- end
28
-
29
- it do
30
- expect(Net::HTTP)
31
- .to have_received(:new)
32
- .with(localhost, port)
33
- end
34
-
35
- it do
36
- expect(a_request(:get, 'localhost:3002/test'))
37
- .to have_been_made.once
38
- end
39
- end
40
-
41
- context 'without block' do
42
- before { described_class.call }
43
-
44
- it do
45
- expect(a_request(:get, 'localhost:3002/test'))
46
- .not_to have_been_made
47
- end
48
- end
49
- end
50
-
51
- context 'when ssl true' do
52
- let(:localhost) { 'localhost' }
53
- let(:port) { 443 }
54
-
55
- before do
56
- Castle.config.url = 'https://localhost'
57
- stub_request(:get, 'https://localhost/test').to_return(status: 200, body: '{}', headers: {})
58
- end
59
-
60
- context 'with block' do
61
- let(:api_url) { '/test' }
62
- let(:request) { Net::HTTP::Get.new(api_url) }
63
-
64
- before do
65
- allow(Net::HTTP)
66
- .to receive(:new)
67
- .with(localhost, port)
68
- .and_call_original
69
-
70
- allow(Net::HTTP)
71
- .to receive(:start)
72
-
73
- described_class.call do |http|
74
- http.request(request)
75
- end
76
- end
77
-
78
- it do
79
- expect(Net::HTTP)
80
- .to have_received(:new)
81
- .with(localhost, port)
82
- end
83
- end
84
- end
85
- end
86
- end
@@ -1,88 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- describe Castle::Commands::Identify do
4
- subject(:instance) { described_class.new(context) }
5
-
6
- let(:context) { { test: { test1: '1' } } }
7
- let(:default_payload) { { user_id: '1234', sent_at: time_auto } }
8
-
9
- let(:time_now) { Time.now }
10
- let(:time_auto) { time_now.utc.iso8601(3) }
11
-
12
- before { Timecop.freeze(time_now) }
13
-
14
- after { Timecop.return }
15
-
16
- describe '.build' do
17
- subject(:command) { instance.build(payload) }
18
-
19
- context 'with simple merger' do
20
- let(:payload) { default_payload.merge(context: { test: { test2: '1' } }) }
21
- let(:command_data) do
22
- default_payload.merge(context: { test: { test1: '1', test2: '1' } })
23
- end
24
-
25
- it { expect(command.method).to be_eql(:post) }
26
- it { expect(command.path).to be_eql('identify') }
27
- it { expect(command.data).to be_eql(command_data) }
28
- end
29
-
30
- context 'with user_traits' do
31
- let(:payload) { default_payload.merge(user_traits: { test: '1' }) }
32
- let(:command_data) do
33
- default_payload.merge(user_traits: { test: '1' }, context: context)
34
- end
35
-
36
- it { expect(command.method).to be_eql(:post) }
37
- it { expect(command.path).to be_eql('identify') }
38
- it { expect(command.data).to be_eql(command_data) }
39
- end
40
-
41
- context 'when active true' do
42
- let(:payload) { default_payload.merge(context: { active: true }) }
43
- let(:command_data) do
44
- default_payload.merge(context: context.merge(active: true))
45
- end
46
-
47
- it { expect(command.method).to be_eql(:post) }
48
- it { expect(command.path).to be_eql('identify') }
49
- it { expect(command.data).to be_eql(command_data) }
50
- end
51
-
52
- context 'when active false' do
53
- let(:payload) { default_payload.merge(context: { active: false }) }
54
- let(:command_data) do
55
- default_payload.merge(context: context.merge(active: false))
56
- end
57
-
58
- it { expect(command.method).to be_eql(:post) }
59
- it { expect(command.path).to be_eql('identify') }
60
- it { expect(command.data).to be_eql(command_data) }
61
- end
62
-
63
- context 'when active string' do
64
- let(:payload) { default_payload.merge(context: { active: 'string' }) }
65
- let(:command_data) { default_payload.merge(context: context) }
66
-
67
- it { expect(command.method).to be_eql(:post) }
68
- it { expect(command.path).to be_eql('identify') }
69
- it { expect(command.data).to be_eql(command_data) }
70
- end
71
- end
72
-
73
- describe '#validate!' do
74
- subject(:validate!) { instance.build(payload) }
75
-
76
- context 'with user_id not present' do
77
- let(:payload) { {} }
78
-
79
- it { expect { validate! }.not_to raise_error }
80
- end
81
-
82
- context 'with user_id present' do
83
- let(:payload) { { user_id: '1234' } }
84
-
85
- it { expect { validate! }.not_to raise_error }
86
- end
87
- end
88
- end
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- describe Castle::Events do
4
- it { expect(described_class::LOGIN_SUCCEEDED).to eq('$login.succeeded') }
5
- end
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- describe Castle::HeadersFilter do
4
- subject(:headers) { described_class.new(request).call }
5
-
6
- let(:env) do
7
- result = Rack::MockRequest.env_for(
8
- '/',
9
- 'Action-Dispatch.request.content-Type' => 'application/json',
10
- 'HTTP_AUTHORIZATION' => 'Basic 123456',
11
- 'HTTP_COOKIE' => '__cid=abcd;other=efgh',
12
- 'HTTP_ACCEPT' => 'application/json',
13
- 'HTTP_X_FORWARDED_FOR' => '1.2.3.4',
14
- 'HTTP_USER_AGENT' => 'Mozilla 1234',
15
- 'TEST' => '1',
16
- 'REMOTE_ADDR' => '1.2.3.4'
17
- )
18
- result[:HTTP_OK] = 'OK'
19
- result
20
- end
21
- let(:filtered) do
22
- {
23
- 'Accept' => 'application/json',
24
- 'Authorization' => 'Basic 123456',
25
- 'Cookie' => '__cid=abcd;other=efgh',
26
- 'Content-Length' => '0',
27
- 'Ok' => 'OK',
28
- 'User-Agent' => 'Mozilla 1234',
29
- 'Remote-Addr' => '1.2.3.4',
30
- 'X-Forwarded-For' => '1.2.3.4'
31
- }
32
- end
33
- let(:request) { Rack::Request.new(env) }
34
-
35
- context 'with list of header' do
36
- it { expect(headers).to eq(filtered) }
37
- end
38
- 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,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- describe Castle::Review 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 '#retrieve' do
11
- subject(:retrieve) { described_class.retrieve(review_id) }
12
-
13
- let(:review_id) { '1234' }
14
-
15
- before { retrieve }
16
-
17
- it { assert_requested :get, "https://api.castle.io/v1/reviews/#{review_id}", times: 1 }
18
- end
19
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- describe Castle::Utils::Merger do
4
- subject(:merger) { described_class }
5
-
6
- describe 'call' do
7
- let(:first) { { test: { test1: { c: '4' }, test2: { c: '3' }, a: '1', b: '2' } } }
8
- let(:second) { { test2: '2', test: { 'test1' => { d: '5' }, test2: '6', a: nil, b: '3' } } }
9
- let(:result) { { test2: '2', test: { test1: { c: '4', d: '5' }, test2: '6', b: '3' } } }
10
-
11
- it { expect(merger.call(first, second)).to be_eql(result) }
12
- end
13
- 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