castle-rb 3.6.2

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 (69) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +157 -0
  3. data/lib/castle-rb.rb +3 -0
  4. data/lib/castle.rb +62 -0
  5. data/lib/castle/api.rb +40 -0
  6. data/lib/castle/api/request.rb +37 -0
  7. data/lib/castle/api/request/build.rb +27 -0
  8. data/lib/castle/api/response.rb +40 -0
  9. data/lib/castle/client.rb +106 -0
  10. data/lib/castle/command.rb +5 -0
  11. data/lib/castle/commands/authenticate.rb +23 -0
  12. data/lib/castle/commands/identify.rb +23 -0
  13. data/lib/castle/commands/impersonate.rb +26 -0
  14. data/lib/castle/commands/review.rb +14 -0
  15. data/lib/castle/commands/track.rb +23 -0
  16. data/lib/castle/configuration.rb +80 -0
  17. data/lib/castle/context/default.rb +40 -0
  18. data/lib/castle/context/merger.rb +14 -0
  19. data/lib/castle/context/sanitizer.rb +23 -0
  20. data/lib/castle/errors.rb +41 -0
  21. data/lib/castle/extractors/client_id.rb +17 -0
  22. data/lib/castle/extractors/headers.rb +51 -0
  23. data/lib/castle/extractors/ip.rb +18 -0
  24. data/lib/castle/failover_auth_response.rb +21 -0
  25. data/lib/castle/header_formatter.rb +9 -0
  26. data/lib/castle/review.rb +11 -0
  27. data/lib/castle/secure_mode.rb +11 -0
  28. data/lib/castle/support/hanami.rb +19 -0
  29. data/lib/castle/support/padrino.rb +19 -0
  30. data/lib/castle/support/rails.rb +13 -0
  31. data/lib/castle/support/sinatra.rb +19 -0
  32. data/lib/castle/utils.rb +55 -0
  33. data/lib/castle/utils/cloner.rb +11 -0
  34. data/lib/castle/utils/merger.rb +23 -0
  35. data/lib/castle/utils/timestamp.rb +12 -0
  36. data/lib/castle/validators/not_supported.rb +16 -0
  37. data/lib/castle/validators/present.rb +16 -0
  38. data/lib/castle/version.rb +5 -0
  39. data/spec/lib/castle/api/request/build_spec.rb +44 -0
  40. data/spec/lib/castle/api/request_spec.rb +59 -0
  41. data/spec/lib/castle/api/response_spec.rb +58 -0
  42. data/spec/lib/castle/api_spec.rb +37 -0
  43. data/spec/lib/castle/client_spec.rb +358 -0
  44. data/spec/lib/castle/command_spec.rb +9 -0
  45. data/spec/lib/castle/commands/authenticate_spec.rb +108 -0
  46. data/spec/lib/castle/commands/identify_spec.rb +87 -0
  47. data/spec/lib/castle/commands/impersonate_spec.rb +106 -0
  48. data/spec/lib/castle/commands/review_spec.rb +24 -0
  49. data/spec/lib/castle/commands/track_spec.rb +113 -0
  50. data/spec/lib/castle/configuration_spec.rb +130 -0
  51. data/spec/lib/castle/context/default_spec.rb +41 -0
  52. data/spec/lib/castle/context/merger_spec.rb +23 -0
  53. data/spec/lib/castle/context/sanitizer_spec.rb +27 -0
  54. data/spec/lib/castle/extractors/client_id_spec.rb +62 -0
  55. data/spec/lib/castle/extractors/headers_spec.rb +89 -0
  56. data/spec/lib/castle/extractors/ip_spec.rb +27 -0
  57. data/spec/lib/castle/header_formatter_spec.rb +25 -0
  58. data/spec/lib/castle/review_spec.rb +19 -0
  59. data/spec/lib/castle/secure_mode_spec.rb +9 -0
  60. data/spec/lib/castle/utils/cloner_spec.rb +18 -0
  61. data/spec/lib/castle/utils/merger_spec.rb +13 -0
  62. data/spec/lib/castle/utils/timestamp_spec.rb +17 -0
  63. data/spec/lib/castle/utils_spec.rb +156 -0
  64. data/spec/lib/castle/validators/not_supported_spec.rb +26 -0
  65. data/spec/lib/castle/validators/present_spec.rb +33 -0
  66. data/spec/lib/castle/version_spec.rb +5 -0
  67. data/spec/lib/castle_spec.rb +66 -0
  68. data/spec/spec_helper.rb +25 -0
  69. metadata +139 -0
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Context::Default do
4
+ subject { described_class.new(request, nil) }
5
+
6
+ let(:ip) { '1.2.3.4' }
7
+ let(:cookie_id) { 'abcd' }
8
+
9
+ let(:env) do
10
+ Rack::MockRequest.env_for('/',
11
+ 'HTTP_X_FORWARDED_FOR' => ip,
12
+ 'HTTP_ACCEPT_LANGUAGE' => 'en',
13
+ 'HTTP_USER_AGENT' => 'test',
14
+ 'HTTP_COOKIE' => "__cid=#{cookie_id};other=efgh")
15
+ end
16
+ let(:request) { Rack::Request.new(env) }
17
+ let(:default_context) { subject.call }
18
+ let(:version) { '2.2.0' }
19
+
20
+ before do
21
+ stub_const('Castle::VERSION', version)
22
+ end
23
+
24
+ it { expect(default_context[:active]).to be_eql(true) }
25
+ it { expect(default_context[:origin]).to be_eql('web') }
26
+
27
+ it do
28
+ expect(default_context[:headers]).to be_eql(
29
+ 'X-Forwarded-For' => '1.2.3.4',
30
+ 'Accept-Language' => 'en',
31
+ 'User-Agent' => 'test',
32
+ 'Content-Length' => '0',
33
+ 'Cookie' => true
34
+ )
35
+ end
36
+
37
+ it { expect(default_context[:ip]).to be_eql(ip) }
38
+ it { expect(default_context[:library][:name]).to be_eql('castle-rb') }
39
+ it { expect(default_context[:library][:version]).to be_eql(version) }
40
+ it { expect(default_context[:user_agent]).to be_eql('test') }
41
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Context::Merger do
4
+ let(:first) { { test: { test1: { c: '4' } } } }
5
+
6
+ describe '#call' do
7
+ subject { described_class.call(first, second) }
8
+
9
+ let(:result) { { test: { test1: { c: '4', d: '5' } } } }
10
+
11
+ context 'with symbol keys' do
12
+ let(:second) { { test: { test1: { d: '5' } } } }
13
+
14
+ it { is_expected.to eq(result) }
15
+ end
16
+
17
+ context 'with string keys' do
18
+ let(:second) { { 'test' => { 'test1' => { 'd' => '5' } } } }
19
+
20
+ it { is_expected.to eq(result) }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Context::Sanitizer do
4
+ let(:paylod) { { test: 'test' } }
5
+
6
+ describe '#call' do
7
+ subject { described_class.call(context) }
8
+
9
+ context 'when active true' do
10
+ let(:context) { paylod.merge(active: true) }
11
+
12
+ it { is_expected.to eql(context) }
13
+ end
14
+
15
+ context 'when active false' do
16
+ let(:context) { paylod.merge(active: false) }
17
+
18
+ it { is_expected.to eql(context) }
19
+ end
20
+
21
+ context 'when active string' do
22
+ let(:context) { paylod.merge(active: 'uknown') }
23
+
24
+ it { is_expected.to eql(paylod) }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Extractors::ClientId do
4
+ subject(:extractor) { described_class.new(request, cookies) }
5
+
6
+ let(:client_id_cookie) { 'abcd' }
7
+ let(:client_id_header) { 'abcde' }
8
+ let(:cookies) { request.cookies }
9
+ let(:request) { Rack::Request.new(env) }
10
+ let(:env) do
11
+ Rack::MockRequest.env_for('/', headers)
12
+ end
13
+
14
+ context 'with client_id' do
15
+ let(:headers) do
16
+ {
17
+ 'HTTP_X_FORWARDED_FOR' => '1.2.3.4',
18
+ 'HTTP_COOKIE' => "__cid=#{client_id_cookie};other=efgh"
19
+ }
20
+ end
21
+
22
+ it do
23
+ expect(extractor.call).to eql(client_id_cookie)
24
+ end
25
+ end
26
+
27
+ context 'with X-Castle-Client-Id header' do
28
+ let(:headers) do
29
+ {
30
+ 'HTTP_X_FORWARDED_FOR' => '1.2.3.4',
31
+ 'HTTP_X_CASTLE_CLIENT_ID' => client_id_header
32
+ }
33
+ end
34
+
35
+ it 'appends the client_id' do
36
+ expect(extractor.call).to eql(client_id_header)
37
+ end
38
+ end
39
+
40
+ context 'when cookies undefined' do
41
+ let(:cookies) { nil }
42
+ let(:headers) { {} }
43
+
44
+ it do
45
+ expect(extractor.call).to eql('')
46
+ end
47
+ end
48
+
49
+ context 'with X-Castle-Client-Id header and cookies client' do
50
+ let(:headers) do
51
+ {
52
+ 'HTTP_X_FORWARDED_FOR' => '1.2.3.4',
53
+ 'HTTP_X_CASTLE_CLIENT_ID' => client_id_header,
54
+ 'HTTP_COOKIE' => "__cid=#{client_id_cookie};other=efgh"
55
+ }
56
+ end
57
+
58
+ it 'appends the client_id' do
59
+ expect(extractor.call).to eql(client_id_header)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Extractors::Headers do
4
+ subject(:headers) { described_class.new(request).call }
5
+
6
+ let(:client_id) { 'abcd' }
7
+ let(:env) do
8
+ result = Rack::MockRequest.env_for(
9
+ '/',
10
+ 'Action-Dispatch.request.content-Type' => 'application/json',
11
+ 'HTTP_AUTHORIZATION' => 'Basic 123456',
12
+ 'HTTP_COOKIE' => "__cid=#{client_id};other=efgh",
13
+ 'HTTP_ACCEPT' => 'application/json',
14
+ 'HTTP_X_FORWARDED_FOR' => '1.2.3.4',
15
+ 'HTTP_USER_AGENT' => 'Mozilla 1234',
16
+ 'TEST' => '1'
17
+ )
18
+ result[:HTTP_OK] = 'OK'
19
+ result
20
+ end
21
+ let(:request) { Rack::Request.new(env) }
22
+
23
+ context 'when whitelist is not set in the configuration' do
24
+ it do
25
+ is_expected.to eq('Accept' => 'application/json',
26
+ 'Authorization' => true,
27
+ 'Cookie' => true,
28
+ 'Content-Length' => '0',
29
+ 'Ok' => 'OK',
30
+ 'User-Agent' => 'Mozilla 1234',
31
+ 'X-Forwarded-For' => '1.2.3.4')
32
+ end
33
+ end
34
+
35
+ context 'when whitelist is set in the configuration' do
36
+ before { Castle.config.whitelisted = %w[Accept OK] }
37
+
38
+ it do
39
+ is_expected.to eq('Accept' => 'application/json',
40
+ 'Authorization' => true,
41
+ 'Cookie' => true,
42
+ 'Content-Length' => true,
43
+ 'Ok' => 'OK',
44
+ 'User-Agent' => 'Mozilla 1234',
45
+ 'X-Forwarded-For' => true)
46
+ end
47
+ end
48
+
49
+ context 'when blacklist is set in the configuration' do
50
+ context 'with a User-Agent' do
51
+ before { Castle.config.blacklisted = %w[User-Agent] }
52
+
53
+ it do
54
+ is_expected.to eq('Accept' => 'application/json',
55
+ 'Authorization' => true,
56
+ 'Cookie' => true,
57
+ 'Content-Length' => '0',
58
+ 'Ok' => 'OK',
59
+ 'User-Agent' => 'Mozilla 1234',
60
+ 'X-Forwarded-For' => '1.2.3.4')
61
+ end
62
+ end
63
+
64
+ context 'with a different header' do
65
+ before { Castle.config.blacklisted = %w[Accept] }
66
+
67
+ it do
68
+ is_expected.to eq('Accept' => true,
69
+ 'Authorization' => true,
70
+ 'Cookie' => true,
71
+ 'Content-Length' => '0',
72
+ 'Ok' => 'OK',
73
+ 'User-Agent' => 'Mozilla 1234',
74
+ 'X-Forwarded-For' => '1.2.3.4')
75
+ end
76
+ end
77
+ end
78
+
79
+ context 'when a header is both whitelisted and blacklisted' do
80
+ before do
81
+ Castle.config.whitelisted = %w[Accept]
82
+ Castle.config.blacklisted = %w[Accept]
83
+ end
84
+
85
+ it do
86
+ expect(headers['Accept']).to eq(true)
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Extractors::IP do
4
+ subject(:extractor) { described_class.new(request) }
5
+
6
+ let(:request) { Rack::Request.new(env) }
7
+
8
+ describe 'ip' do
9
+ context 'when regular ip' do
10
+ let(:env) { Rack::MockRequest.env_for('/', 'HTTP_X_FORWARDED_FOR' => '1.2.3.5') }
11
+
12
+ it { expect(extractor.call).to eql('1.2.3.5') }
13
+ end
14
+
15
+ context 'when cf remote_ip' do
16
+ let(:env) do
17
+ Rack::MockRequest.env_for(
18
+ '/',
19
+ 'HTTP_CF_CONNECTING_IP' => '1.2.3.4',
20
+ 'HTTP_X_FORWARDED_FOR' => '1.2.3.5'
21
+ )
22
+ end
23
+
24
+ it { expect(extractor.call).to eql('1.2.3.4') }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::HeaderFormatter do
4
+ subject(:formatter) { described_class.new }
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 'removes HTTP_' do
23
+ expect(formatter.call(:clearance)).to be_eql('Clearance')
24
+ end
25
+ end
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::SecureMode do
4
+ it 'has signature' do
5
+ expect(described_class.signature('test')).to eql(
6
+ '0329a06b62cd16b33eb6792be8c60b158d89a2ee3a876fce9a881ebb488c0914'
7
+ )
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Utils::Cloner do
4
+ subject(:cloner) { described_class }
5
+
6
+ describe 'call' do
7
+ let(:nested) { { c: '3' } }
8
+ let(:first) { { test: { test1: { c: '4' }, test2: nested, a: '1', b: '2' } } }
9
+ let(:result) { { test: { test1: { c: '4' }, test2: { c: '3' }, a: '1', b: '2' } } }
10
+ let(:cloned) { cloner.call(first) }
11
+
12
+ before { cloned }
13
+ it do
14
+ nested[:test] = 'sample'
15
+ expect(cloned).to be_eql(result)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Utils::Timestamp do
4
+ subject { described_class.call }
5
+
6
+ let(:time_string) { '2018-01-10T14:14:24.407Z' }
7
+ let(:time) { Time.parse(time_string) }
8
+
9
+ before { Timecop.freeze(time) }
10
+ after { Timecop.return }
11
+
12
+ describe '#call' do
13
+ it do
14
+ is_expected.to eql(time_string)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,156 @@
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 '#deep_symbolize_keys' 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