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
@@ -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