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,358 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Client 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
+ let(:client) { described_class.from_request(request) }
17
+ let(:request_to_context) { described_class.to_context(request) }
18
+ let(:client_with_user_timestamp) do
19
+ described_class.new(request_to_context, timestamp: time_user)
20
+ end
21
+ let(:client_with_no_timestamp) { described_class.new(request_to_context) }
22
+
23
+ let(:headers) do
24
+ {
25
+ 'Content-Length': '0', 'User-Agent': ua, 'X-Forwarded-For': ip.to_s, 'Cookie': true
26
+ }
27
+ end
28
+ let(:context) do
29
+ {
30
+ client_id: 'abcd',
31
+ active: true,
32
+ origin: 'web',
33
+ user_agent: ua,
34
+ headers: headers,
35
+ ip: ip,
36
+ library: { name: 'castle-rb', version: '2.2.0' }
37
+ }
38
+ end
39
+
40
+ let(:time_now) { Time.now }
41
+ let(:time_auto) { time_now.utc.iso8601(3) }
42
+ let(:time_user) { (Time.now - 10_000).utc.iso8601(3) }
43
+ let(:response_body) { {}.to_json }
44
+
45
+ before do
46
+ Timecop.freeze(time_now)
47
+ stub_const('Castle::VERSION', '2.2.0')
48
+ stub_request(:any, /api.castle.io/).with(
49
+ basic_auth: ['', 'secret']
50
+ ).to_return(status: 200, body: response_body, headers: {})
51
+ end
52
+
53
+ after { Timecop.return }
54
+
55
+ describe 'parses the request' do
56
+ before do
57
+ allow(Castle::API).to receive(:request).and_call_original
58
+ end
59
+
60
+ it do
61
+ client.authenticate(event: '$login.succeeded', user_id: '1234')
62
+ expect(Castle::API).to have_received(:request)
63
+ end
64
+ end
65
+
66
+ describe 'to_context' do
67
+ it do
68
+ expect(described_class.to_context(request)).to eql(context)
69
+ end
70
+ end
71
+
72
+ describe 'to_options' do
73
+ let(:options) { { user_id: '1234', user_traits: { name: 'Jo' } } }
74
+ let(:result) { { user_id: '1234', user_traits: { name: 'Jo' }, timestamp: time_auto } }
75
+
76
+ it do
77
+ expect(described_class.to_options(options)).to eql(result)
78
+ end
79
+ end
80
+
81
+ describe 'impersonate' do
82
+ let(:impersonator) { 'test@castle.io' }
83
+ let(:request_body) do
84
+ { user_id: '1234', timestamp: time_auto, sent_at: time_auto,
85
+ impersonator: impersonator, context: context }
86
+ end
87
+ let(:response_body) { { success: true }.to_json }
88
+ let(:options) { { user_id: '1234', impersonator: impersonator } }
89
+
90
+ context 'when used with symbol keys' do
91
+ before { client.impersonate(options) }
92
+
93
+ it do
94
+ assert_requested :post, 'https://api.castle.io/v1/impersonate', times: 1 do |req|
95
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
96
+ end
97
+ end
98
+ end
99
+
100
+ context 'when request is not successful' do
101
+ let(:response_body) { {}.to_json }
102
+
103
+ it { expect { client.impersonate(options) }.to raise_error(Castle::ImpersonationFailed) }
104
+ end
105
+ end
106
+
107
+ describe 'identify' do
108
+ let(:request_body) do
109
+ { user_id: '1234', timestamp: time_auto,
110
+ sent_at: time_auto, context: context, user_traits: { name: 'Jo' } }
111
+ end
112
+
113
+ before { client.identify(options) }
114
+
115
+ context 'when used with symbol keys' do
116
+ let(:options) { { user_id: '1234', user_traits: { name: 'Jo' } } }
117
+
118
+ it do
119
+ assert_requested :post, 'https://api.castle.io/v1/identify', times: 1 do |req|
120
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
121
+ end
122
+ end
123
+
124
+ context 'when passed timestamp in options and no defined timestamp' do
125
+ let(:client) { client_with_no_timestamp }
126
+ let(:options) { { user_id: '1234', user_traits: { name: 'Jo' }, timestamp: time_user } }
127
+ let(:request_body) do
128
+ { user_id: '1234', user_traits: { name: 'Jo' }, context: context,
129
+ timestamp: time_user, sent_at: time_auto }
130
+ end
131
+
132
+ it do
133
+ assert_requested :post, 'https://api.castle.io/v1/identify', times: 1 do |req|
134
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
135
+ end
136
+ end
137
+ end
138
+
139
+ context 'with client initialized with timestamp' do
140
+ let(:client) { client_with_user_timestamp }
141
+ let(:request_body) do
142
+ { user_id: '1234', timestamp: time_user, sent_at: time_auto,
143
+ context: context, user_traits: { name: 'Jo' } }
144
+ end
145
+
146
+ it do
147
+ assert_requested :post, 'https://api.castle.io/v1/identify', times: 1 do |req|
148
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ context 'when used with string keys' do
155
+ let(:options) { { 'user_id' => '1234', 'user_traits' => { 'name' => 'Jo' } } }
156
+
157
+ it do
158
+ assert_requested :post, 'https://api.castle.io/v1/identify', times: 1 do |req|
159
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ describe 'authenticate' do
166
+ let(:options) { { event: '$login.succeeded', user_id: '1234' } }
167
+ let(:request_response) { client.authenticate(options) }
168
+ let(:request_body) do
169
+ { event: '$login.succeeded', user_id: '1234', context: context,
170
+ timestamp: time_auto, sent_at: time_auto }
171
+ end
172
+
173
+ context 'when used with symbol keys' do
174
+ before { request_response }
175
+
176
+ it do
177
+ assert_requested :post, 'https://api.castle.io/v1/authenticate', times: 1 do |req|
178
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
179
+ end
180
+ end
181
+
182
+ context 'when passed timestamp in options and no defined timestamp' do
183
+ let(:client) { client_with_no_timestamp }
184
+ let(:options) { { event: '$login.succeeded', user_id: '1234', timestamp: time_user } }
185
+ let(:request_body) do
186
+ { event: '$login.succeeded', user_id: '1234', context: context,
187
+ timestamp: time_user, sent_at: time_auto }
188
+ end
189
+
190
+ it do
191
+ assert_requested :post, 'https://api.castle.io/v1/authenticate', times: 1 do |req|
192
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
193
+ end
194
+ end
195
+ end
196
+
197
+ context 'with client initialized with timestamp' do
198
+ let(:client) { client_with_user_timestamp }
199
+ let(:request_body) do
200
+ { event: '$login.succeeded', user_id: '1234', context: context,
201
+ timestamp: time_user, sent_at: time_auto }
202
+ end
203
+
204
+ it do
205
+ assert_requested :post, 'https://api.castle.io/v1/authenticate', times: 1 do |req|
206
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
207
+ end
208
+ end
209
+ end
210
+ end
211
+
212
+ context 'when used with string keys' do
213
+ let(:options) { { 'event' => '$login.succeeded', 'user_id' => '1234' } }
214
+
215
+ before { request_response }
216
+
217
+ it do
218
+ assert_requested :post, 'https://api.castle.io/v1/authenticate', times: 1 do |req|
219
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
220
+ end
221
+ end
222
+ end
223
+
224
+ context 'when tracking enabled' do
225
+ before { request_response }
226
+
227
+ it do
228
+ assert_requested :post, 'https://api.castle.io/v1/authenticate', times: 1 do |req|
229
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
230
+ end
231
+ end
232
+
233
+ it { expect(request_response[:failover]).to be false }
234
+ it { expect(request_response[:failover_reason]).to be_nil }
235
+ end
236
+
237
+ context 'when tracking disabled' do
238
+ before do
239
+ client.disable_tracking
240
+ request_response
241
+ end
242
+
243
+ it { assert_not_requested :post, 'https://api.castle.io/v1/authenticate' }
244
+ it { expect(request_response[:action]).to be_eql('allow') }
245
+ it { expect(request_response[:user_id]).to be_eql('1234') }
246
+ it { expect(request_response[:failover]).to be true }
247
+ it { expect(request_response[:failover_reason]).to be_eql('Castle set to do not track.') }
248
+ end
249
+
250
+ context 'when request with fail' do
251
+ before { allow(Castle::API).to receive(:request).and_raise(Castle::RequestError.new(Timeout::Error)) }
252
+
253
+ context 'with request error and throw strategy' do
254
+ before { allow(Castle.config).to receive(:failover_strategy).and_return(:throw) }
255
+
256
+ it { expect { request_response }.to raise_error(Castle::RequestError) }
257
+ end
258
+
259
+ context 'with request error and not throw on eg deny strategy' do
260
+ it { assert_not_requested :post, 'https://:secret@api.castle.io/v1/authenticate' }
261
+ it { expect(request_response[:action]).to be_eql('allow') }
262
+ it { expect(request_response[:user_id]).to be_eql('1234') }
263
+ it { expect(request_response[:failover]).to be true }
264
+ it { expect(request_response[:failover_reason]).to be_eql('Castle::RequestError') }
265
+ end
266
+ end
267
+
268
+ context 'when request is internal server error' do
269
+ before { allow(Castle::API).to receive(:request).and_raise(Castle::InternalServerError) }
270
+
271
+ describe 'throw strategy' do
272
+ before { allow(Castle.config).to receive(:failover_strategy).and_return(:throw) }
273
+
274
+ it { expect { request_response }.to raise_error(Castle::InternalServerError) }
275
+ end
276
+
277
+ context 'not throw on eg deny strategy' do
278
+ it { assert_not_requested :post, 'https://:secret@api.castle.io/v1/authenticate' }
279
+ it { expect(request_response[:action]).to be_eql('allow') }
280
+ it { expect(request_response[:user_id]).to be_eql('1234') }
281
+ it { expect(request_response[:failover]).to be true }
282
+ it { expect(request_response[:failover_reason]).to be_eql('Castle::InternalServerError') }
283
+ end
284
+ end
285
+ end
286
+
287
+ describe 'track' do
288
+ let(:request_body) do
289
+ { event: '$login.succeeded', context: context, user_id: '1234',
290
+ timestamp: time_auto, sent_at: time_auto }
291
+ end
292
+
293
+ before { client.track(options) }
294
+
295
+ context 'when used with symbol keys' do
296
+ let(:options) { { event: '$login.succeeded', user_id: '1234' } }
297
+
298
+ it do
299
+ assert_requested :post, 'https://api.castle.io/v1/track', times: 1 do |req|
300
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
301
+ end
302
+ end
303
+
304
+ context 'when passed timestamp in options and no defined timestamp' do
305
+ let(:client) { client_with_no_timestamp }
306
+ let(:options) { { event: '$login.succeeded', user_id: '1234', timestamp: time_user } }
307
+ let(:request_body) do
308
+ { event: '$login.succeeded', user_id: '1234', context: context,
309
+ timestamp: time_user, sent_at: time_auto }
310
+ end
311
+
312
+ it do
313
+ assert_requested :post, 'https://api.castle.io/v1/track', times: 1 do |req|
314
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
315
+ end
316
+ end
317
+ end
318
+
319
+ context 'with client initialized with timestamp' do
320
+ let(:client) { client_with_user_timestamp }
321
+ let(:request_body) do
322
+ { event: '$login.succeeded', context: context, user_id: '1234',
323
+ timestamp: time_user, sent_at: time_auto }
324
+ end
325
+
326
+ it do
327
+ assert_requested :post, 'https://api.castle.io/v1/track', times: 1 do |req|
328
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
329
+ end
330
+ end
331
+ end
332
+ end
333
+
334
+ context 'when used with string keys' do
335
+ let(:options) { { 'event' => '$login.succeeded', 'user_id' => '1234' } }
336
+
337
+ it do
338
+ assert_requested :post, 'https://api.castle.io/v1/track', times: 1 do |req|
339
+ JSON.parse(req.body) == JSON.parse(request_body.to_json)
340
+ end
341
+ end
342
+ end
343
+ end
344
+
345
+ describe 'tracked?' do
346
+ context 'when off' do
347
+ before { client.disable_tracking }
348
+
349
+ it { expect(client).not_to be_tracked }
350
+ end
351
+
352
+ context 'when on' do
353
+ before { client.enable_tracking }
354
+
355
+ it { expect(client).to be_tracked }
356
+ end
357
+ end
358
+ 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,108 @@
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', 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
+ after { Timecop.return }
14
+
15
+ describe '.build' do
16
+ subject(:command) { instance.build(payload) }
17
+
18
+ context 'with simple merger' do
19
+ let(:payload) { default_payload.merge(context: { test: { test2: '1' } }) }
20
+ let(:command_data) do
21
+ default_payload.merge(context: { test: { test1: '1', test2: '1' } })
22
+ end
23
+
24
+ it { expect(command.method).to be_eql(:post) }
25
+ it { expect(command.path).to be_eql('authenticate') }
26
+ it { expect(command.data).to be_eql(command_data) }
27
+ end
28
+
29
+ context 'with properties' do
30
+ let(:payload) { default_payload.merge(properties: { test: '1' }) }
31
+ let(:command_data) do
32
+ default_payload.merge(properties: { test: '1' }, context: context)
33
+ end
34
+
35
+ it { expect(command.method).to be_eql(:post) }
36
+ it { expect(command.path).to be_eql('authenticate') }
37
+ it { expect(command.data).to be_eql(command_data) }
38
+ end
39
+
40
+ context 'with user_traits' do
41
+ let(:payload) { default_payload.merge(user_traits: { test: '1' }) }
42
+ let(:command_data) do
43
+ default_payload.merge(user_traits: { test: '1' }, context: context)
44
+ end
45
+
46
+ it { expect(command.method).to be_eql(:post) }
47
+ it { expect(command.path).to be_eql('authenticate') }
48
+ it { expect(command.data).to be_eql(command_data) }
49
+ end
50
+
51
+ context 'when active true' do
52
+ let(:payload) { default_payload.merge(context: { active: true }) }
53
+ let(:command_data) do
54
+ default_payload.merge(context: context.merge(active: true))
55
+ end
56
+
57
+ it { expect(command.method).to be_eql(:post) }
58
+ it { expect(command.path).to be_eql('authenticate') }
59
+ it { expect(command.data).to be_eql(command_data) }
60
+ end
61
+
62
+ context 'when active false' do
63
+ let(:payload) { default_payload.merge(context: { active: false }) }
64
+ let(:command_data) do
65
+ default_payload.merge(context: context.merge(active: false))
66
+ end
67
+
68
+ it { expect(command.method).to be_eql(:post) }
69
+ it { expect(command.path).to be_eql('authenticate') }
70
+ it { expect(command.data).to be_eql(command_data) }
71
+ end
72
+
73
+ context 'when active string' do
74
+ let(:payload) { default_payload.merge(context: { active: 'string' }) }
75
+ let(:command_data) { default_payload.merge(context: context) }
76
+
77
+ it { expect(command.method).to be_eql(:post) }
78
+ it { expect(command.path).to be_eql('authenticate') }
79
+ it { expect(command.data).to be_eql(command_data) }
80
+ end
81
+ end
82
+
83
+ describe '#validate!' do
84
+ subject(:validate!) { instance.build(payload) }
85
+
86
+ context 'with event not present' do
87
+ let(:payload) { {} }
88
+
89
+ it do
90
+ expect do
91
+ validate!
92
+ end.to raise_error(Castle::InvalidParametersError, 'event is missing or empty')
93
+ end
94
+ end
95
+
96
+ context 'with user_id not present' do
97
+ let(:payload) { { event: '$login.track' } }
98
+
99
+ it { expect { validate! }.not_to raise_error }
100
+ end
101
+
102
+ context 'with event and user_id present' do
103
+ let(:payload) { { event: '$login.track', user_id: '1234' } }
104
+
105
+ it { expect { validate! }.not_to raise_error }
106
+ end
107
+ end
108
+ end