castle-rb 2.3.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +55 -9
  3. data/lib/castle.rb +17 -7
  4. data/lib/castle/api.rb +20 -22
  5. data/lib/castle/client.rb +50 -19
  6. data/lib/castle/command.rb +5 -0
  7. data/lib/castle/commands/authenticate.rb +25 -0
  8. data/lib/castle/commands/identify.rb +30 -0
  9. data/lib/castle/commands/review.rb +13 -0
  10. data/lib/castle/commands/track.rb +25 -0
  11. data/lib/castle/commands/with_context.rb +28 -0
  12. data/lib/castle/configuration.rb +46 -14
  13. data/lib/castle/context_merger.rb +13 -0
  14. data/lib/castle/default_context.rb +28 -0
  15. data/lib/castle/errors.rb +2 -0
  16. data/lib/castle/extractors/client_id.rb +4 -14
  17. data/lib/castle/extractors/headers.rb +6 -18
  18. data/lib/castle/failover_auth_response.rb +21 -0
  19. data/lib/castle/header_formatter.rb +9 -0
  20. data/lib/castle/request.rb +7 -13
  21. data/lib/castle/response.rb +2 -0
  22. data/lib/castle/review.rb +11 -0
  23. data/lib/castle/secure_mode.rb +11 -0
  24. data/lib/castle/support/hanami.rb +19 -0
  25. data/lib/castle/support/padrino.rb +1 -1
  26. data/lib/castle/support/rails.rb +1 -1
  27. data/lib/castle/support/sinatra.rb +4 -2
  28. data/lib/castle/utils.rb +55 -0
  29. data/lib/castle/utils/cloner.rb +11 -0
  30. data/lib/castle/utils/merger.rb +23 -0
  31. data/lib/castle/version.rb +1 -1
  32. data/spec/lib/castle/api_spec.rb +16 -25
  33. data/spec/lib/castle/client_spec.rb +175 -39
  34. data/spec/lib/castle/command_spec.rb +9 -0
  35. data/spec/lib/castle/commands/authenticate_spec.rb +106 -0
  36. data/spec/lib/castle/commands/identify_spec.rb +85 -0
  37. data/spec/lib/castle/commands/review_spec.rb +24 -0
  38. data/spec/lib/castle/commands/track_spec.rb +107 -0
  39. data/spec/lib/castle/configuration_spec.rb +75 -27
  40. data/spec/lib/castle/context_merger_spec.rb +34 -0
  41. data/spec/lib/castle/default_context_spec.rb +35 -0
  42. data/spec/lib/castle/extractors/client_id_spec.rb +13 -5
  43. data/spec/lib/castle/extractors/headers_spec.rb +6 -5
  44. data/spec/lib/castle/extractors/ip_spec.rb +2 -9
  45. data/spec/lib/castle/header_formatter_spec.rb +21 -0
  46. data/spec/lib/castle/request_spec.rb +12 -9
  47. data/spec/lib/castle/response_spec.rb +1 -3
  48. data/spec/lib/castle/review_spec.rb +23 -0
  49. data/spec/lib/castle/secure_mode_spec.rb +9 -0
  50. data/spec/lib/castle/utils/cloner_spec.rb +18 -0
  51. data/spec/lib/castle/utils/merger_spec.rb +13 -0
  52. data/spec/lib/castle/utils_spec.rb +156 -0
  53. data/spec/lib/castle/version_spec.rb +1 -5
  54. data/spec/lib/castle_spec.rb +8 -15
  55. data/spec/spec_helper.rb +3 -9
  56. metadata +46 -12
  57. data/lib/castle/cookie_store.rb +0 -52
  58. data/lib/castle/headers.rb +0 -39
  59. data/lib/castle/support.rb +0 -11
  60. data/lib/castle/system.rb +0 -36
  61. data/spec/lib/castle/headers_spec.rb +0 -82
  62. data/spec/lib/castle/system_spec.rb +0 -70
@@ -1,64 +1,200 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spec_helper'
4
-
5
- class Request < Rack::Request
6
- def delegate?
7
- false
8
- end
9
- end
10
-
11
3
  describe Castle::Client do
12
4
  let(:ip) { '1.2.3.4' }
13
5
  let(:cookie_id) { 'abcd' }
14
6
  let(:env) do
15
- Rack::MockRequest.env_for('/',
16
- 'HTTP_X_FORWARDED_FOR' => '1.2.3.4',
17
- 'HTTP_COOKIE' => "__cid=#{cookie_id};other=efgh")
7
+ Rack::MockRequest.env_for(
8
+ '/',
9
+ 'HTTP_X_FORWARDED_FOR' => ip,
10
+ 'HTTP_COOKIE' => "__cid=#{cookie_id};other=efgh"
11
+ )
12
+ end
13
+ let(:request) { Rack::Request.new(env) }
14
+ let(:client) { described_class.new(request) }
15
+ let(:headers) { { 'X-Forwarded-For' => ip.to_s } }
16
+ let(:context) do
17
+ {
18
+ client_id: 'abcd',
19
+ active: true,
20
+ origin: 'web',
21
+ headers: headers,
22
+ ip: ip,
23
+ library: { name: 'castle-rb', version: '2.2.0' }
24
+ }
18
25
  end
19
- let(:request) { Request.new(env) }
20
- let(:client) { described_class.new(request, nil) }
21
- let(:review_id) { '12356789' }
22
26
 
23
- describe 'parses the request' do
24
- let(:api_data) { [cookie_id, ip, "{\"X-Forwarded-For\":\"#{ip}\"}"] }
27
+ before do
28
+ stub_const('Castle::VERSION', '2.2.0')
29
+ stub_request(:any, /api.castle.io/).with(
30
+ basic_auth: ['', 'secret']
31
+ ).to_return(status: 200, body: '{}', headers: {})
32
+ end
25
33
 
34
+ describe 'parses the request' do
26
35
  before do
27
- allow(Castle::API).to receive(:new).with(*api_data).and_call_original
36
+ allow(Castle::API).to receive(:new).and_call_original
28
37
  end
29
38
 
30
39
  it do
31
- client.authenticate(name: '$login.succeeded', user_id: '1234')
32
- expect(Castle::API).to have_received(:new).with(*api_data)
40
+ client.authenticate(event: '$login.succeeded', user_id: '1234')
41
+ expect(Castle::API).to have_received(:new)
33
42
  end
34
43
  end
35
44
 
36
- it 'identifies' do
37
- client.identify(user_id: '1234', traits: { name: 'Jo' })
38
- assert_requested :post, 'https://:secret@api.castle.io/v1/identify',
39
- times: 1,
40
- body: { user_id: '1234', traits: { name: 'Jo' } }
45
+ describe 'identify' do
46
+ let(:request_body) { { user_id: '1234', context: context, traits: { name: 'Jo' } } }
47
+
48
+ before { client.identify(options) }
49
+
50
+ context 'symbol keys' do
51
+ let(:options) { { user_id: '1234', traits: { name: 'Jo' } } }
52
+
53
+ it do
54
+ assert_requested :post, 'https://api.castle.io/v1/identify', times: 1 do |req|
55
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
56
+ end
57
+ end
58
+ end
59
+
60
+ context 'string keys' do
61
+ let(:options) { { 'user_id' => '1234', 'traits' => { 'name' => 'Jo' } } }
62
+
63
+ it do
64
+ assert_requested :post, 'https://api.castle.io/v1/identify', times: 1 do |req|
65
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
66
+ end
67
+ end
68
+ end
41
69
  end
42
70
 
43
- it 'authenticates' do
44
- client.authenticate(name: '$login.succeeded', user_id: '1234')
45
- assert_requested :post, 'https://:secret@api.castle.io/v1/authenticate',
46
- times: 1,
47
- body: { name: '$login.succeeded', user_id: '1234' }
71
+ describe 'authenticate' do
72
+ let(:options) { { event: '$login.succeeded', user_id: '1234' } }
73
+ let(:request_response) { client.authenticate(options) }
74
+ let(:request_body) { { event: '$login.succeeded', user_id: '1234', context: context } }
75
+
76
+ context 'symbol keys' do
77
+ before { request_response }
78
+
79
+ it do
80
+ assert_requested :post, 'https://api.castle.io/v1/authenticate', times: 1 do |req|
81
+ req.body == request_body.to_json
82
+ end
83
+ end
84
+ end
85
+
86
+ context 'string keys' do
87
+ let(:options) { { 'event' => '$login.succeeded', 'user_id' => '1234' } }
88
+
89
+ before { request_response }
90
+
91
+ it do
92
+ assert_requested :post, 'https://api.castle.io/v1/authenticate', times: 1 do |req|
93
+ req.body == request_body.to_json
94
+ end
95
+ end
96
+ end
97
+
98
+ context 'tracking enabled' do
99
+ before { request_response }
100
+
101
+ it do
102
+ assert_requested :post, 'https://api.castle.io/v1/authenticate', times: 1 do |req|
103
+ req.body == request_body.to_json
104
+ end
105
+ end
106
+
107
+ it { expect(request_response['failover']).to be false }
108
+ it { expect(request_response['failover_reason']).to be_nil }
109
+ end
110
+
111
+ context 'tracking disabled' do
112
+ before do
113
+ client.disable_tracking
114
+ request_response
115
+ end
116
+
117
+ it { assert_not_requested :post, 'https://api.castle.io/v1/authenticate' }
118
+ it { expect(request_response['action']).to be_eql('allow') }
119
+ it { expect(request_response['user_id']).to be_eql('1234') }
120
+ it { expect(request_response['failover']).to be true }
121
+ it { expect(request_response['failover_reason']).to be_eql('Castle set to do not track.') }
122
+ end
123
+
124
+ context 'when request with fail' do
125
+ before { allow(client.api).to receive(:request).and_raise(Castle::RequestError) }
126
+
127
+ context 'with request error and throw strategy' do
128
+ before { allow(Castle.config).to receive(:failover_strategy).and_return(:throw) }
129
+
130
+ it { expect { request_response }.to raise_error(Castle::RequestError) }
131
+ end
132
+
133
+ context 'with request error and not throw on eg deny strategy' do
134
+ it { assert_not_requested :post, 'https://:secret@api.castle.io/v1/authenticate' }
135
+ it { expect(request_response['action']).to be_eql('allow') }
136
+ it { expect(request_response['user_id']).to be_eql('1234') }
137
+ it { expect(request_response['failover']).to be true }
138
+ it { expect(request_response['failover_reason']).to be_eql('Castle::RequestError') }
139
+ end
140
+ end
141
+
142
+ context 'when request is internal server error' do
143
+ before { allow(client.api).to receive(:request).and_raise(Castle::InternalServerError) }
144
+
145
+ context 'throw strategy' do
146
+ before { allow(Castle.config).to receive(:failover_strategy).and_return(:throw) }
147
+
148
+ it { expect { request_response }.to raise_error(Castle::InternalServerError) }
149
+ end
150
+
151
+ context 'not throw on eg deny strategy' do
152
+ it { assert_not_requested :post, 'https://:secret@api.castle.io/v1/authenticate' }
153
+ it { expect(request_response['action']).to be_eql('allow') }
154
+ it { expect(request_response['user_id']).to be_eql('1234') }
155
+ it { expect(request_response['failover']).to be true }
156
+ it { expect(request_response['failover_reason']).to be_eql('Castle::InternalServerError') }
157
+ end
158
+ end
48
159
  end
49
160
 
50
- it 'tracks' do
51
- client.track(name: '$login.succeeded', user_id: '1234')
52
- assert_requested :post, 'https://:secret@api.castle.io/v1/track',
53
- times: 1,
54
- body: { name: '$login.succeeded', user_id: '1234' }
161
+ describe 'track' do
162
+ let(:request_body) { { event: '$login.succeeded', context: context, user_id: '1234' } }
163
+
164
+ before { client.track(options) }
165
+
166
+ context 'symbol keys' do
167
+ let(:options) { { event: '$login.succeeded', user_id: '1234' } }
168
+
169
+ it do
170
+ assert_requested :post, 'https://api.castle.io/v1/track', times: 1 do |req|
171
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
172
+ end
173
+ end
174
+ end
175
+
176
+ context 'string keys' do
177
+ let(:options) { { 'event' => '$login.succeeded', 'user_id' => '1234' } }
178
+
179
+ it do
180
+ assert_requested :post, 'https://api.castle.io/v1/track', times: 1 do |req|
181
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
182
+ end
183
+ end
184
+ end
55
185
  end
56
186
 
57
- it 'fetches review' do
58
- client.fetch_review(review_id)
187
+ describe 'tracked?' do
188
+ context 'off' do
189
+ before { client.disable_tracking }
190
+
191
+ it { expect(client).not_to be_tracked }
192
+ end
193
+
194
+ context 'on' do
195
+ before { client.enable_tracking }
59
196
 
60
- assert_requested :get,
61
- "https://:secret@api.castle.io/v1/reviews/#{review_id}",
62
- times: 1
197
+ it { expect(client).to be_tracked }
198
+ end
63
199
  end
64
200
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Command do
4
+ subject(:command) { described_class.new('go', { id: '1' }, :post) }
5
+
6
+ it { expect(command.path).to be_eql('go') }
7
+ it { expect(command.data).to be_eql(id: '1') }
8
+ it { expect(command.method).to be_eql(:post) }
9
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Commands::Authenticate do
4
+ subject(:instance) { described_class.new(context) }
5
+
6
+ let(:context) { { test: { test1: '1' } } }
7
+ let(:default_payload) { { event: '$login.authenticate', user_id: '1234' } }
8
+
9
+ describe '.build' do
10
+ subject(:command) { instance.build(payload) }
11
+
12
+ context 'simple merger' do
13
+ let(:payload) { default_payload.merge({ context: { test: { test2: '1' } } }) }
14
+ let(:command_data) do
15
+ default_payload.merge({ context: { test: { test1: '1', test2: '1' } } })
16
+ end
17
+
18
+ it { expect(command.method).to be_eql(:post) }
19
+ it { expect(command.path).to be_eql('authenticate') }
20
+ it { expect(command.data).to be_eql(command_data) }
21
+ end
22
+
23
+ context 'properties' do
24
+ let(:payload) { default_payload.merge({ properties: { test: '1' } }) }
25
+ let(:command_data) do
26
+ default_payload.merge({ properties: { test: '1' }, context: context })
27
+ end
28
+
29
+ it { expect(command.method).to be_eql(:post) }
30
+ it { expect(command.path).to be_eql('authenticate') }
31
+ it { expect(command.data).to be_eql(command_data) }
32
+ end
33
+
34
+ context 'traits' do
35
+ let(:payload) { default_payload.merge({ traits: { test: '1' } }) }
36
+ let(:command_data) do
37
+ default_payload.merge({ traits: { test: '1' }, context: context })
38
+ end
39
+
40
+ it { expect(command.method).to be_eql(:post) }
41
+ it { expect(command.path).to be_eql('authenticate') }
42
+ it { expect(command.data).to be_eql(command_data) }
43
+ end
44
+
45
+ context 'active true' do
46
+ let(:payload) { default_payload.merge({ context: { active: true } }) }
47
+ let(:command_data) do
48
+ default_payload.merge({ context: context.merge(active: true) })
49
+ end
50
+
51
+ it { expect(command.method).to be_eql(:post) }
52
+ it { expect(command.path).to be_eql('authenticate') }
53
+ it { expect(command.data).to be_eql(command_data) }
54
+ end
55
+
56
+ context 'active false' do
57
+ let(:payload) { default_payload.merge({ context: { active: false } }) }
58
+ let(:command_data) do
59
+ default_payload.merge({ context: context.merge(active: false) })
60
+ end
61
+
62
+ it { expect(command.method).to be_eql(:post) }
63
+ it { expect(command.path).to be_eql('authenticate') }
64
+ it { expect(command.data).to be_eql(command_data) }
65
+ end
66
+
67
+ context 'active string' do
68
+ let(:payload) { default_payload.merge({ context: { active: 'string' } }) }
69
+ let(:command_data) { default_payload.merge({ context: context }) }
70
+
71
+ it { expect(command.method).to be_eql(:post) }
72
+ it { expect(command.path).to be_eql('authenticate') }
73
+ it { expect(command.data).to be_eql(command_data) }
74
+ end
75
+ end
76
+
77
+ describe '#validate!' do
78
+ subject(:validate!) { instance.build(payload) }
79
+
80
+ context 'event not present' do
81
+ let(:payload) { {} }
82
+
83
+ it do
84
+ expect do
85
+ validate!
86
+ end.to raise_error(Castle::InvalidParametersError, 'event is missing or empty')
87
+ end
88
+ end
89
+
90
+ context 'user_id not present' do
91
+ let(:payload) { { event: '$login.track' } }
92
+
93
+ it do
94
+ expect do
95
+ validate!
96
+ end.to raise_error(Castle::InvalidParametersError, 'user_id is missing or empty')
97
+ end
98
+ end
99
+
100
+ context 'event and user_id present' do
101
+ let(:payload) { { event: '$login.track', user_id: '1234' } }
102
+
103
+ it { expect { validate! }.not_to raise_error }
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,85 @@
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' } }
8
+
9
+ describe '.build' do
10
+ subject(:command) { instance.build(payload) }
11
+
12
+ context 'simple merger' do
13
+ let(:payload) { default_payload.merge({ context: { test: { test2: '1' } } }) }
14
+ let(:command_data) do
15
+ default_payload.merge({ context: { test: { test1: '1', test2: '1' } } })
16
+ end
17
+
18
+ it { expect(command.method).to be_eql(:post) }
19
+ it { expect(command.path).to be_eql('identify') }
20
+ it { expect(command.data).to be_eql(command_data) }
21
+ end
22
+
23
+ context 'traits' do
24
+ let(:payload) { default_payload.merge({ traits: { test: '1' } }) }
25
+ let(:command_data) do
26
+ default_payload.merge({ traits: { test: '1' }, context: context })
27
+ end
28
+
29
+ it { expect(command.method).to be_eql(:post) }
30
+ it { expect(command.path).to be_eql('identify') }
31
+ it { expect(command.data).to be_eql(command_data) }
32
+ end
33
+
34
+ context 'active true' do
35
+ let(:payload) { default_payload.merge({ context: { active: true } }) }
36
+ let(:command_data) do
37
+ default_payload.merge({ context: context.merge(active: true) })
38
+ end
39
+
40
+ it { expect(command.method).to be_eql(:post) }
41
+ it { expect(command.path).to be_eql('identify') }
42
+ it { expect(command.data).to be_eql(command_data) }
43
+ end
44
+
45
+ context 'active false' do
46
+ let(:payload) { default_payload.merge({ context: { active: false } }) }
47
+ let(:command_data) do
48
+ default_payload.merge({ context: context.merge(active: false) })
49
+ end
50
+
51
+ it { expect(command.method).to be_eql(:post) }
52
+ it { expect(command.path).to be_eql('identify') }
53
+ it { expect(command.data).to be_eql(command_data) }
54
+ end
55
+
56
+ context 'active string' do
57
+ let(:payload) { default_payload.merge({ context: { active: 'string' } }) }
58
+ let(:command_data) { default_payload.merge({ context: context }) }
59
+
60
+ it { expect(command.method).to be_eql(:post) }
61
+ it { expect(command.path).to be_eql('identify') }
62
+ it { expect(command.data).to be_eql(command_data) }
63
+ end
64
+ end
65
+
66
+ describe '#validate!' do
67
+ subject(:validate!) { instance.build(payload) }
68
+
69
+ context 'user_id not present' do
70
+ let(:payload) { {} }
71
+
72
+ it do
73
+ expect do
74
+ validate!
75
+ end.to raise_error(Castle::InvalidParametersError, 'user_id is missing or empty')
76
+ end
77
+ end
78
+
79
+ context 'user_id present' do
80
+ let(:payload) { { user_id: '1234' } }
81
+
82
+ it { expect { validate! }.not_to raise_error }
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Commands::Review do
4
+ subject(:instance) { described_class.new }
5
+
6
+ let(:context) { {} }
7
+ let(:review_id) { '1234' }
8
+
9
+ describe '.build' do
10
+ subject(:command) { instance.build(review_id) }
11
+
12
+ context 'without review_id' do
13
+ let(:review_id) { '' }
14
+
15
+ it { expect { command }.to raise_error(Castle::InvalidParametersError) }
16
+ end
17
+
18
+ context 'with review_id' do
19
+ it { expect(command.method).to be_eql(:get) }
20
+ it { expect(command.path).to be_eql("reviews/#{review_id}") }
21
+ it { expect(command.data).to be_nil }
22
+ end
23
+ end
24
+ end