castle-rb 2.3.2 → 3.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 (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