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.
Files changed (144) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +160 -45
  3. data/lib/castle.rb +49 -28
  4. data/lib/castle/api.rb +21 -14
  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 +78 -51
  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 +23 -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 +23 -0
  25. data/lib/castle/commands/report_device.rb +17 -0
  26. data/lib/castle/commands/risk.rb +23 -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 +57 -32
  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 +18 -0
  40. data/lib/castle/failover/strategy.rb +23 -0
  41. data/lib/castle/headers/extract.rb +47 -0
  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} +31 -9
  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 +108 -93
  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 +99 -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 +100 -0
  87. data/spec/lib/castle/commands/report_device_spec.rb +24 -0
  88. data/spec/lib/castle/commands/risk_spec.rb +100 -0
  89. data/spec/lib/castle/commands/start_impersonation_spec.rb +79 -0
  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/core/send_request_spec.rb +77 -0
  100. data/spec/lib/castle/failover/strategy_spec.rb +12 -0
  101. data/spec/lib/castle/{extractors/headers_spec.rb → headers/extract_spec.rb} +18 -20
  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} +27 -8
  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 +152 -0
  120. data/spec/support/shared_examples/configuration.rb +101 -0
  121. metadata +146 -64
  122. data/lib/castle/api/request.rb +0 -42
  123. data/lib/castle/api/session.rb +0 -39
  124. data/lib/castle/commands/identify.rb +0 -23
  125. data/lib/castle/commands/impersonate.rb +0 -26
  126. data/lib/castle/commands/review.rb +0 -14
  127. data/lib/castle/events.rb +0 -49
  128. data/lib/castle/extractors/headers.rb +0 -45
  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/request_spec.rb +0 -72
  137. data/spec/lib/castle/commands/identify_spec.rb +0 -88
  138. data/spec/lib/castle/commands/impersonate_spec.rb +0 -107
  139. data/spec/lib/castle/events_spec.rb +0 -5
  140. data/spec/lib/castle/headers_filter_spec.rb +0 -37
  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
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Headers::Format do
4
+ subject(:format) { described_class }
5
+
6
+ it 'removes HTTP_' do
7
+ expect(format.call('HTTP_X_TEST')).to be_eql('X-Test')
8
+ end
9
+
10
+ it 'capitalizes header' do
11
+ expect(format.call('X_TEST')).to be_eql('X-Test')
12
+ end
13
+
14
+ it 'ignores letter case and -_ divider' do
15
+ expect(format.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(format.call('httpX_teST')).to be_eql('Httpx-Test')
20
+ end
21
+
22
+ it 'capitalizes' do
23
+ expect(format.call(:clearance)).to be_eql('Clearance')
24
+ end
25
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- describe Castle::Extractors::IP do
3
+ describe Castle::IPs::Extract do
4
4
  subject(:extractor) { described_class.new(headers) }
5
5
 
6
6
  describe 'ip' do
@@ -43,9 +43,7 @@ describe Castle::Extractors::IP do
43
43
  end
44
44
 
45
45
  context 'with all the trusted proxies' do
46
- let(:http_x_header) do
47
- '127.0.0.1,10.0.0.1,172.31.0.1,192.168.0.1,::1,fd00::,localhost,unix,unix:/tmp/sock'
48
- end
46
+ let(:http_x_header) { '127.0.0.1,10.0.0.1,172.31.0.1,192.168.0.1' }
49
47
 
50
48
  let(:headers) { { 'Remote-Addr' => '127.0.0.1', 'X-Forwarded-For' => http_x_header } }
51
49
 
@@ -54,12 +52,33 @@ describe Castle::Extractors::IP do
54
52
  end
55
53
  end
56
54
 
55
+ context 'with trust_proxy_chain option' do
56
+ let(:http_x_header) { '6.6.6.6, 2.2.2.3, 6.6.6.5' }
57
+
58
+ let(:headers) { { 'Remote-Addr' => '6.6.6.4', 'X-Forwarded-For' => http_x_header } }
59
+
60
+ before { Castle.config.trust_proxy_chain = true }
61
+
62
+ it 'selects first available header' do
63
+ expect(extractor.call).to eql('6.6.6.6')
64
+ end
65
+ end
66
+
67
+ context 'with trusted_proxy_depth option' do
68
+ let(:http_x_header) { '6.6.6.6, 2.2.2.3, 6.6.6.5' }
69
+
70
+ let(:headers) { { 'Remote-Addr' => '6.6.6.4', 'X-Forwarded-For' => http_x_header } }
71
+
72
+ before { Castle.config.trusted_proxy_depth = 1 }
73
+
74
+ it 'selects first available header' do
75
+ expect(extractor.call).to eql('2.2.2.3')
76
+ end
77
+ end
78
+
57
79
  context 'when list of not trusted ips provided in X_FORWARDED_FOR' do
58
80
  let(:headers) do
59
- {
60
- 'X-Forwarded-For' => '6.6.6.6, 2.2.2.3, 192.168.0.7',
61
- 'Client-Ip' => '6.6.6.6'
62
- }
81
+ { 'X-Forwarded-For' => '6.6.6.6, 2.2.2.3, 192.168.0.7', 'Client-Ip' => '6.6.6.6' }
63
82
  end
64
83
 
65
84
  it 'does not allow to spoof ip' do
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # tmp logger for testing
4
+ class TmpLogger
5
+ # @param _message [String]
6
+ def info(_message); end
7
+ end
8
+
9
+ describe Castle::Logger do
10
+ subject(:log) { described_class.call(message, data) }
11
+
12
+ let(:message) { 'https://localhost/test:' }
13
+ let(:integration_logger) { TmpLogger.new }
14
+ let(:data) { { a: 1 }.to_json }
15
+ let(:logger_message) { "[CASTLE] #{message} #{data}" }
16
+
17
+ before { allow(integration_logger).to receive(:info).and_call_original }
18
+
19
+ describe '.call' do
20
+ context 'without logger' do
21
+ before do
22
+ Castle.config.logger = nil
23
+ log
24
+ end
25
+
26
+ it { expect(integration_logger).not_to have_received(:info) }
27
+ end
28
+
29
+ context 'with logger' do
30
+ before do
31
+ Castle.config.logger = integration_logger
32
+ log
33
+ end
34
+
35
+ it { expect(integration_logger).to have_received(:info).with(logger_message) }
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Payload::Prepare do
4
+ let(:ip) { '1.2.3.4' }
5
+ let(:cookie_id) { 'abcd' }
6
+ let(:ua) { 'Chrome' }
7
+ let(:env) do
8
+ Rack::MockRequest.env_for(
9
+ '/',
10
+ 'HTTP_USER_AGENT' => ua,
11
+ 'HTTP_X_FORWARDED_FOR' => ip,
12
+ 'HTTP_COOKIE' => "__cid=#{cookie_id};other=efgh"
13
+ )
14
+ end
15
+ let(:request) { Rack::Request.new(env) }
16
+
17
+ let(:headers) do
18
+ { 'Content-Length': '0', 'User-Agent': ua, 'X-Forwarded-For': ip.to_s, 'Cookie': true }
19
+ end
20
+ let(:context) do
21
+ {
22
+ client_id: 'abcd',
23
+ active: true,
24
+ user_agent: ua,
25
+ headers: headers,
26
+ ip: ip,
27
+ library: {
28
+ name: 'castle-rb',
29
+ version: '2.2.0'
30
+ }
31
+ }
32
+ end
33
+
34
+ let(:time_now) { Time.now }
35
+ let(:time_formatted) { time_now.utc.iso8601(3) }
36
+ let(:payload_options) { { user_id: '1234', user_traits: { name: 'Jo' } } }
37
+ let(:result) do
38
+ { user_id: '1234', user_traits: { name: 'Jo' }, timestamp: time_formatted, context: context }
39
+ end
40
+
41
+ before do
42
+ Timecop.freeze(time_now)
43
+ stub_const('Castle::VERSION', '2.2.0')
44
+ end
45
+
46
+ after { Timecop.return }
47
+
48
+ describe '#call' do
49
+ subject(:generated) { described_class.call(payload_options, request) }
50
+
51
+ context 'when active true' do
52
+ it { is_expected.to eql(result) }
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::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.base_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).to receive(:new).with(localhost, port).and_call_original
20
+
21
+ described_class.call { |http| http.request(request) }
22
+ end
23
+
24
+ it { expect(Net::HTTP).to have_received(:new).with(localhost, port) }
25
+
26
+ it { expect(a_request(:get, 'localhost:3002/test')).to have_been_made.once }
27
+ end
28
+
29
+ context 'without block' do
30
+ before { described_class.call }
31
+
32
+ it { expect(a_request(:get, 'localhost:3002/test')).not_to have_been_made }
33
+ end
34
+ end
35
+
36
+ context 'when ssl true' do
37
+ let(:localhost) { 'localhost' }
38
+ let(:port) { 443 }
39
+
40
+ before do
41
+ Castle.config.base_url = 'https://localhost'
42
+ stub_request(:get, 'https://localhost/test').to_return(
43
+ status: 200,
44
+ body: '{}',
45
+ headers: {}
46
+ )
47
+ end
48
+
49
+ context 'with block' do
50
+ let(:api_url) { '/test' }
51
+ let(:request) { Net::HTTP::Get.new(api_url) }
52
+
53
+ before do
54
+ allow(Net::HTTP).to receive(:new).with(localhost, port).and_call_original
55
+
56
+ allow(Net::HTTP).to receive(:start)
57
+
58
+ described_class.call { |http| http.request(request) }
59
+ end
60
+
61
+ it { expect(Net::HTTP).to have_received(:new).with(localhost, port) }
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::SingletonConfiguration do
4
+ subject(:config) { described_class.instance }
5
+
6
+ it_behaves_like 'configuration_host'
7
+ it_behaves_like 'configuration_request_timeout'
8
+ it_behaves_like 'configuration_allowlisted'
9
+ it_behaves_like 'configuration_denylisted'
10
+ it_behaves_like 'configuration_failover_strategy'
11
+ it_behaves_like 'configuration_api_secret'
12
+
13
+ it { expect(config.api_secret).to be_eql('secret') }
14
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Utils::CleanInvalidChars do
4
+ describe '::call' do
5
+ subject { described_class.call(input) }
6
+
7
+ context 'when input is a string' do
8
+ let(:input) { '1234' }
9
+
10
+ it { is_expected.to eq input }
11
+ end
12
+
13
+ context 'when input is an array' do
14
+ let(:input) { [1, 2, 3, '4'] }
15
+
16
+ it { is_expected.to eq input }
17
+ end
18
+
19
+ context 'when input is a hash' do
20
+ let(:input) { { user_id: 1 } }
21
+
22
+ it { is_expected.to eq input }
23
+ end
24
+
25
+ context 'when input is nil' do
26
+ let(:input) { nil }
27
+
28
+ it { is_expected.to eq input }
29
+ end
30
+
31
+ context 'when input is a nested hash' do
32
+ let(:input) { { user: { id: 1 } } }
33
+
34
+ it { is_expected.to eq input }
35
+ end
36
+
37
+ context 'with invalid UTF-8 characters' do
38
+ context 'when input is a hash' do
39
+ let(:input) { { user_id: "inv\xC4lid" } }
40
+
41
+ it { is_expected.to eq(user_id: 'inv�lid') }
42
+ end
43
+
44
+ context 'when input is a nested hash' do
45
+ let(:input) { { user: { id: "inv\xC4lid" } } }
46
+
47
+ it { is_expected.to eq(user: { id: 'inv�lid' }) }
48
+ end
49
+
50
+ context 'when input is an array of hashes' do
51
+ let(:input) { [{ user: "inv\xC4lid" }] * 2 }
52
+
53
+ it { is_expected.to eq([{ user: 'inv�lid' }, { user: 'inv�lid' }]) }
54
+ end
55
+
56
+ context 'when input is an array' do
57
+ let(:input) { ["inv\xC4lid"] * 2 }
58
+
59
+ it { is_expected.to eq(%w[inv�lid inv�lid]) }
60
+ end
61
+
62
+ context 'when input is a hash with array in key' do
63
+ let(:input) { { items: ["inv\xC4lid"] * 2 } }
64
+
65
+ it { is_expected.to eq(items: %w[inv�lid inv�lid]) }
66
+ end
67
+ end
68
+ end
69
+ end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- describe Castle::Utils::Cloner do
4
- subject(:cloner) { described_class }
3
+ describe Castle::Utils::Clone do
4
+ subject(:clone) { described_class }
5
5
 
6
6
  describe 'call' do
7
7
  let(:nested) { { c: '3' } }
8
8
  let(:first) { { test: { test1: { c: '4' }, test2: nested, a: '1', b: '2' } } }
9
9
  let(:result) { { test: { test1: { c: '4' }, test2: { c: '3' }, a: '1', b: '2' } } }
10
- let(:cloned) { cloner.call(first) }
10
+ let(:cloned) { clone.call(first) }
11
11
 
12
12
  before { cloned }
13
13
 
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Utils::DeepSymbolizeKeys 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 '::call' do
12
+ subject { described_class.call(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
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- describe Castle::Utils::Timestamp do
3
+ describe Castle::Utils::GetTimestamp do
4
4
  subject(:timestamp) { described_class.call }
5
5
 
6
6
  let(:time_string) { '2018-01-10T14:14:24.407Z' }
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Utils::Merge do
4
+ subject(:merge) { described_class }
5
+
6
+ describe 'call' do
7
+ let(:first) { { test: { test1: { c: '4' }, test2: { c: '3' }, a: '1', b: '2' } } }
8
+ let(:second) do
9
+ { test2: '2', test: { 'test1' => { d: '5' }, :test2 => '6', :a => nil, :b => '3' } }
10
+ end
11
+ let(:result) { { test2: '2', test: { test1: { c: '4', d: '5' }, test2: '6', b: '3' } } }
12
+
13
+ it { expect(merge.call(first, second)).to be_eql(result) }
14
+ end
15
+ end
@@ -8,9 +8,7 @@ describe Castle::Validators::Present do
8
8
  let(:keys) { %i[second third] }
9
9
 
10
10
  it do
11
- expect do
12
- call
13
- end.to raise_error(Castle::InvalidParametersError, 'third is missing or empty')
11
+ expect { call }.to raise_error(Castle::InvalidParametersError, 'third is missing or empty')
14
12
  end
15
13
  end
16
14
 
@@ -18,9 +16,10 @@ describe Castle::Validators::Present do
18
16
  let(:keys) { %i[second invalid] }
19
17
 
20
18
  it do
21
- expect do
22
- call
23
- end.to raise_error(Castle::InvalidParametersError, 'invalid is missing or empty')
19
+ expect { call }.to raise_error(
20
+ Castle::InvalidParametersError,
21
+ 'invalid is missing or empty'
22
+ )
24
23
  end
25
24
  end
26
25