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
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Commands::Track do
4
+ subject(:instance) { described_class.new(context) }
5
+
6
+ let(:context) { { test: { test1: '1' } } }
7
+ let(:default_payload) { { event: '$login.track' } }
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('track') }
20
+ it { expect(command.data).to be_eql(command_data) }
21
+ end
22
+
23
+ context 'user_id' do
24
+ let(:payload) { default_payload.merge({ user_id: '1234' }) }
25
+ let(:command_data) do
26
+ default_payload.merge({ user_id: '1234', context: context })
27
+ end
28
+
29
+ it { expect(command.method).to be_eql(:post) }
30
+ it { expect(command.path).to be_eql('track') }
31
+ it { expect(command.data).to be_eql(command_data) }
32
+ end
33
+
34
+ context 'properties' do
35
+ let(:payload) { default_payload.merge({ properties: { test: '1' } }) }
36
+ let(:command_data) do
37
+ default_payload.merge({ properties: { test: '1' }, context: context })
38
+ end
39
+
40
+ it { expect(command.method).to be_eql(:post) }
41
+ it { expect(command.path).to be_eql('track') }
42
+ it { expect(command.data).to be_eql(command_data) }
43
+ end
44
+
45
+ context 'traits' do
46
+ let(:payload) { default_payload.merge({ traits: { test: '1' } }) }
47
+ let(:command_data) do
48
+ default_payload.merge({ traits: { test: '1' }, context: context })
49
+ end
50
+
51
+ it { expect(command.method).to be_eql(:post) }
52
+ it { expect(command.path).to be_eql('track') }
53
+ it { expect(command.data).to be_eql(command_data) }
54
+ end
55
+
56
+ context 'active true' do
57
+ let(:payload) { default_payload.merge({ context: { active: true } }) }
58
+ let(:command_data) do
59
+ default_payload.merge({ context: context.merge(active: true) })
60
+ end
61
+
62
+ it { expect(command.method).to be_eql(:post) }
63
+ it { expect(command.path).to be_eql('track') }
64
+ it { expect(command.data).to be_eql(command_data) }
65
+ end
66
+
67
+ context 'active false' do
68
+ let(:payload) { default_payload.merge({ context: { active: false } }) }
69
+ let(:command_data) do
70
+ default_payload.merge({ context: context.merge(active: false) })
71
+ end
72
+
73
+ it { expect(command.method).to be_eql(:post) }
74
+ it { expect(command.path).to be_eql('track') }
75
+ it { expect(command.data).to be_eql(command_data) }
76
+ end
77
+
78
+ context 'active string' do
79
+ let(:payload) { default_payload.merge({ context: { active: 'string' } }) }
80
+ let(:command_data) { default_payload.merge({ context: context }) }
81
+
82
+ it { expect(command.method).to be_eql(:post) }
83
+ it { expect(command.path).to be_eql('track') }
84
+ it { expect(command.data).to be_eql(command_data) }
85
+ end
86
+ end
87
+
88
+ describe '#validate!' do
89
+ subject(:validate!) { instance.build(payload) }
90
+
91
+ context 'event not present' do
92
+ let(:payload) { {} }
93
+
94
+ it do
95
+ expect do
96
+ validate!
97
+ end.to raise_error(Castle::InvalidParametersError, 'event is missing or empty')
98
+ end
99
+ end
100
+
101
+ context 'event present' do
102
+ let(:payload) { { event: '$login.track' } }
103
+
104
+ it { expect { validate! }.not_to raise_error }
105
+ end
106
+ end
107
+ end
@@ -1,41 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spec_helper'
4
-
5
3
  describe Castle::Configuration do
6
4
  subject(:config) do
7
5
  described_class.new
8
6
  end
9
7
 
10
- describe 'api_endpoint' do
11
- context 'env' do
12
- let(:value) { 50.0 }
8
+ describe 'host' do
9
+ context 'default' do
10
+ it { expect(config.host).to be_eql('api.castle.io') }
11
+ end
13
12
 
14
- before do
15
- allow(ENV).to receive(:fetch).with(
16
- 'CASTLE_API_ENDPOINT', 'https://api.castle.io/v1'
17
- ).and_return('https://new.herokuapp.com')
18
- allow(ENV).to receive(:fetch).with(
19
- 'CASTLE_API_SECRET', ''
20
- ).and_call_original
21
- end
13
+ context 'setter' do
14
+ before { config.host = 'api.castle.dev' }
22
15
 
23
- it do
24
- expect(config.api_endpoint).to be_eql(URI('https://new.herokuapp.com'))
25
- end
16
+ it { expect(config.host).to be_eql('api.castle.dev') }
17
+ end
18
+ end
19
+
20
+ describe 'host' do
21
+ context 'default' do
22
+ it { expect(config.port).to be_eql(443) }
26
23
  end
27
24
 
28
- it do
29
- expect(config.api_endpoint).to be_eql(URI('https://api.castle.io/v1'))
25
+ context 'setter' do
26
+ before { config.port = 3001 }
27
+
28
+ it { expect(config.port).to be_eql(3001) }
30
29
  end
31
30
  end
32
31
 
33
32
  describe 'api_secret' do
34
33
  context 'env' do
35
34
  before do
36
- allow(ENV).to receive(:fetch).with(
37
- 'CASTLE_API_ENDPOINT', 'https://api.castle.io/v1'
38
- ).and_call_original
39
35
  allow(ENV).to receive(:fetch).with(
40
36
  'CASTLE_API_SECRET', ''
41
37
  ).and_return('secret_key')
@@ -64,7 +60,7 @@ describe Castle::Configuration do
64
60
 
65
61
  describe 'request_timeout' do
66
62
  it do
67
- expect(config.request_timeout).to be_eql(30.0)
63
+ expect(config.request_timeout).to be_eql(500)
68
64
  end
69
65
 
70
66
  context 'setter' do
@@ -79,19 +75,71 @@ describe Castle::Configuration do
79
75
  end
80
76
  end
81
77
 
82
- describe 'source_header' do
78
+ describe 'whitelisted' do
79
+ it do
80
+ expect(config.whitelisted.size).to be_eql(11)
81
+ end
82
+
83
+ context 'setter' do
84
+ before do
85
+ config.whitelisted = ['header']
86
+ end
87
+ it do
88
+ expect(config.whitelisted).to be_eql(['Header'])
89
+ end
90
+ end
91
+
92
+ context 'appending' do
93
+ before do
94
+ config.whitelisted += ['header']
95
+ end
96
+ it { expect(config.whitelisted).to be_include('Header') }
97
+ it { expect(config.whitelisted.size).to be_eql(12) }
98
+ end
99
+ end
100
+
101
+ describe 'blacklisted' do
83
102
  it do
84
- expect(config.source_header).to be_eql(nil)
103
+ expect(config.blacklisted.size).to be_eql(1)
85
104
  end
86
105
 
87
106
  context 'setter' do
88
- let(:value) { 'header' }
107
+ before do
108
+ config.blacklisted = ['header']
109
+ end
110
+ it do
111
+ expect(config.blacklisted).to be_eql(['Header'])
112
+ end
113
+ end
89
114
 
115
+ context 'appending' do
90
116
  before do
91
- config.source_header = value
117
+ config.blacklisted += ['header']
92
118
  end
119
+ it { expect(config.blacklisted).to be_include('Header') }
120
+ it { expect(config.blacklisted.size).to be_eql(2) }
121
+ end
122
+ end
123
+
124
+ describe 'failover_strategy' do
125
+ it do
126
+ expect(config.failover_strategy).to be_eql(:allow)
127
+ end
128
+
129
+ context 'setter' do
130
+ before do
131
+ config.failover_strategy = :deny
132
+ end
133
+ it do
134
+ expect(config.failover_strategy).to be_eql(:deny)
135
+ end
136
+ end
137
+
138
+ context 'when broken' do
93
139
  it do
94
- expect(config.source_header).to be_eql(value)
140
+ expect do
141
+ config.failover_strategy = :unicorn
142
+ end.to raise_error(Castle::ConfigurationError)
95
143
  end
96
144
  end
97
145
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::ContextMerger do
4
+ subject(:command) { described_class.new('go', { id: '1' }, :post) }
5
+
6
+ let(:first) { { test: { test1: { c: '4' } } } }
7
+
8
+ context '.new' do
9
+ subject(:instance) { described_class.new(first) }
10
+
11
+ it { expect(instance.instance_variable_get(:@main_context)).to eq(first) }
12
+ it do
13
+ expect(instance.instance_variable_get(:@main_context).object_id).not_to eq(first.object_id)
14
+ end
15
+ end
16
+
17
+ context '#call' do
18
+ subject { described_class.new(first).call(second) }
19
+
20
+ let(:result) { { test: { test1: { c: '4', d: '5' } } } }
21
+
22
+ context 'symbol keys' do
23
+ let(:second) { { test: { test1: { d: '5' } } } }
24
+
25
+ it { is_expected.to eq(result) }
26
+ end
27
+
28
+ context 'string keys' do
29
+ let(:second) { { 'test' => { 'test1' => { 'd' => '5' } } } }
30
+
31
+ it { is_expected.to eq(result) }
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::DefaultContext 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
+ it {
27
+ expect(default_context[:headers]).to be_eql(
28
+ 'X-Forwarded-For' => '1.2.3.4', 'Accept-Language' => 'en', 'User-Agent' => 'test'
29
+ )
30
+ }
31
+ it { expect(default_context[:ip]).to be_eql(ip) }
32
+ it { expect(default_context[:library][:name]).to be_eql('castle-rb') }
33
+ it { expect(default_context[:library][:version]).to be_eql(version) }
34
+ it { expect(default_context[:user_agent]).to be_eql('test') }
35
+ end
@@ -1,11 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spec_helper'
4
-
5
3
  describe Castle::Extractors::ClientId do
6
- subject(:extractor) { described_class.new(request) }
4
+ subject(:extractor) { described_class.new(request, cookies) }
7
5
 
8
6
  let(:client_id) { 'abcd' }
7
+ let(:cookies) { request.cookies }
9
8
  let(:request) { Rack::Request.new(env) }
10
9
  let(:env) do
11
10
  Rack::MockRequest.env_for('/', headers)
@@ -20,7 +19,7 @@ describe Castle::Extractors::ClientId do
20
19
  end
21
20
 
22
21
  it do
23
- expect(extractor.call(nil, '__cid')).to eql(client_id)
22
+ expect(extractor.call('__cid')).to eql(client_id)
24
23
  end
25
24
  end
26
25
 
@@ -33,7 +32,16 @@ describe Castle::Extractors::ClientId do
33
32
  end
34
33
 
35
34
  it 'appends the client_id' do
36
- expect(extractor.call(nil, '__cid')).to eql(client_id)
35
+ expect(extractor.call('__cid')).to eql(client_id)
36
+ end
37
+ end
38
+
39
+ context 'allow cookies to be undefined' do
40
+ let(:cookies) { nil }
41
+ let(:headers) { {} }
42
+
43
+ it do
44
+ expect(extractor.call('__cid')).to eql('')
37
45
  end
38
46
  end
39
47
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spec_helper'
4
-
5
3
  describe Castle::Extractors::Headers do
6
4
  subject(:extractor) { described_class.new(request) }
7
5
 
@@ -9,16 +7,19 @@ describe Castle::Extractors::Headers do
9
7
  let(:env) do
10
8
  Rack::MockRequest.env_for('/',
11
9
  'HTTP_X_FORWARDED_FOR' => '1.2.3.4',
12
- 'HTTP_TEST' => '2',
10
+ 'HTTP_OK' => 'OK',
13
11
  'TEST' => '1',
14
12
  'HTTP_COOKIE' => "__cid=#{client_id};other=efgh")
15
13
  end
16
14
  let(:request) { Rack::Request.new(env) }
17
15
 
18
- describe 'header should extract http headers but skip cookies related' do
16
+ describe 'extract http headers with whitelisted and blacklisted support' do
17
+ before do
18
+ Castle.config.whitelisted += ['TEST']
19
+ end
19
20
  it do
20
21
  expect(extractor.call).to eql(
21
- '{"X-Forwarded-For":"1.2.3.4","Test":"2"}'
22
+ 'X-Forwarded-For' => '1.2.3.4', 'Test' => '1'
22
23
  )
23
24
  end
24
25
  end
@@ -1,20 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spec_helper'
4
-
5
3
  describe Castle::Extractors::IP do
6
4
  subject(:extractor) { described_class.new(request) }
7
5
 
8
6
  let(:request) { Rack::Request.new(env) }
9
7
 
10
8
  describe 'ip' do
11
- let(:env) do
12
- Rack::MockRequest.env_for('/',
13
- 'HTTP_X_FORWARDED_FOR' => '1.2.3.4')
14
- end
9
+ let(:env) { Rack::MockRequest.env_for( '/', 'HTTP_X_FORWARDED_FOR' => '1.2.3.4') }
15
10
 
16
- it do
17
- expect(extractor.call).to eql('1.2.3.4')
18
- end
11
+ it { expect(extractor.call).to eql('1.2.3.4') }
19
12
  end
20
13
  end
@@ -0,0 +1,21 @@
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
+ end
@@ -1,21 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spec_helper'
4
-
5
3
  describe Castle::Request do
6
- subject do
7
- described_class.new(headers)
8
- end
4
+ subject { described_class.new(headers) }
9
5
 
10
6
  let(:headers) { { 'SAMPLE-HEADER' => '1' } }
11
7
  let(:api_secret) { Castle.config.api_secret }
12
8
 
13
9
  describe 'build' do
14
- let(:path) { 'endpoint' }
15
- let(:params) { { user_id: 1 } }
16
-
17
10
  context 'get' do
18
- let(:request) { subject.build_query(path) }
11
+ let(:path) { 'endpoint' }
12
+ let(:params) { { user_id: 1 } }
13
+ let(:request) { subject.build(path, params, :get) }
19
14
 
20
15
  it { expect(request.body).to be_nil }
21
16
  it { expect(request.method).to eql('GET') }
@@ -25,6 +20,8 @@ describe Castle::Request do
25
20
  end
26
21
 
27
22
  context 'post' do
23
+ let(:path) { 'endpoint' }
24
+ let(:params) { { user_id: 1 } }
28
25
  let(:request) { subject.build(path, params, :post) }
29
26
 
30
27
  it { expect(request.body).to be_eql('{"user_id":1}') }
@@ -32,6 +29,12 @@ describe Castle::Request do
32
29
  it { expect(request.path).to eql('/v1/endpoint') }
33
30
  it { expect(request.to_hash['sample-header']).to eql(['1']) }
34
31
  it { expect(request.to_hash['authorization'][0]).to match(/Basic \w/) }
32
+
33
+ context 'with non-UTF-8 charaters' do
34
+ let(:params) { { name: "\xC4" } }
35
+
36
+ it { expect(request.body).to eq '{"name":"�"}' }
37
+ end
35
38
  end
36
39
  end
37
40
  end