clever_tap 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +48 -0
  5. data/.travis.yml +6 -0
  6. data/Gemfile +16 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +164 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +8 -0
  11. data/bin/setup +8 -0
  12. data/clever_tap.gemspec +33 -0
  13. data/lib/clever_tap.rb +79 -0
  14. data/lib/clever_tap/client.rb +113 -0
  15. data/lib/clever_tap/config.rb +25 -0
  16. data/lib/clever_tap/entity.rb +87 -0
  17. data/lib/clever_tap/event.rb +30 -0
  18. data/lib/clever_tap/failed_response.rb +28 -0
  19. data/lib/clever_tap/profile.rb +7 -0
  20. data/lib/clever_tap/response.rb +28 -0
  21. data/lib/clever_tap/successful_response.rb +30 -0
  22. data/lib/clever_tap/uploader.rb +72 -0
  23. data/lib/clever_tap/version.rb +3 -0
  24. data/lib/clevertap-ruby.rb +1 -0
  25. data/spec/factories/profile.rb +36 -0
  26. data/spec/integrations/clever_tap_spec.rb +81 -0
  27. data/spec/rubocop_spec.rb +12 -0
  28. data/spec/shared/clever_tap_client.rb +13 -0
  29. data/spec/shared/entity.rb +105 -0
  30. data/spec/spec_helper.rb +18 -0
  31. data/spec/units/clever_tap_client_spec.rb +279 -0
  32. data/spec/units/clever_tap_spec.rb +88 -0
  33. data/spec/units/event_spec.rb +43 -0
  34. data/spec/units/failed_response_spec.rb +31 -0
  35. data/spec/units/profile_spec.rb +29 -0
  36. data/spec/units/response_spec.rb +48 -0
  37. data/spec/units/successful_response_spec.rb +112 -0
  38. data/spec/units/uploader_spec.rb +129 -0
  39. data/spec/vcr_cassettes/CleverTap/uploading_a_many_profiles/when_only_some_are_valid/partially_succeds.yml +42 -0
  40. data/spec/vcr_cassettes/CleverTap/uploading_a_profile/when_is_invalid/fails.yml +41 -0
  41. data/spec/vcr_cassettes/CleverTap/uploading_a_profile/when_is_valid/succeed.yml +35 -0
  42. data/spec/vcr_cassettes/CleverTap/uploading_an_event/when_is_valid/succeed.yml +34 -0
  43. data/spec/vcr_cassettes/CleverTap_Client/_upload/when_upload_records_are_homogenous/and_invalid_records/calls_on_failed_upload_once.yml +38 -0
  44. data/spec/vcr_cassettes/CleverTap_Client/_upload/when_upload_records_are_homogenous/and_invalid_records/returns_an_array_with_one_failed_Response_object.yml +38 -0
  45. data/spec/vcr_cassettes/CleverTap_Client/_upload/when_upload_records_are_homogenous/and_valid_records/and_objects_do_not_fit_upload_limit_/calls_on_successful_upload_proc_twice.yml +67 -0
  46. data/spec/vcr_cassettes/CleverTap_Client/_upload/when_upload_records_are_homogenous/and_valid_records/and_objects_do_not_fit_upload_limit_/returns_an_array_with_two_successful_Response_objects.yml +67 -0
  47. data/spec/vcr_cassettes/CleverTap_Client/_upload/when_upload_records_are_homogenous/and_valid_records/and_objects_fit_upload_limit_/calls_on_successful_upload_proc_once.yml +36 -0
  48. data/spec/vcr_cassettes/CleverTap_Client/_upload/when_upload_records_are_homogenous/and_valid_records/and_objects_fit_upload_limit_/returns_an_array_with_one_successful_Response_object.yml +36 -0
  49. data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_age_is_invalid/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +48 -0
  50. data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_education_status_is_invalid/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +49 -0
  51. data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_email_is_invalid/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +48 -0
  52. data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_employment_status_is_invalid/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +48 -0
  53. data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_marital_status_is_invalid/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +48 -0
  54. data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_phone_is_invalid/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +48 -0
  55. data/spec/vcr_cassettes/CleverTap_Uploader/_call/when_the_creation_date_field_is_missing/behaves_like_validation_failure/failed_to_upload_the_profiles.yml +48 -0
  56. data/spec/vcr_cassettes/CleverTap_Uploader/_call/with_invalid_credentials/failed_to_upload_the_profiles.yml +36 -0
  57. data/spec/vcr_cassettes/CleverTap_Uploader/_call/with_valid_data/makes_successful_upload.yml +36 -0
  58. data/spec/vcr_config.rb +13 -0
  59. metadata +199 -0
@@ -0,0 +1,13 @@
1
+ shared_examples 'configured `Client`' do
2
+ it 'preserves credentials in `Client`' do
3
+ expect(subject.account_id).to eq account_id
4
+ expect(subject.passcode).to eq account_passcode
5
+ end
6
+ end
7
+
8
+ shared_examples 'configured `Client`' do
9
+ it 'preserves credentials in `Client`' do
10
+ expect(subject.account_id).to eq account_id
11
+ expect(subject.passcode).to eq account_passcode
12
+ end
13
+ end
@@ -0,0 +1,105 @@
1
+ shared_examples_for 'setting allowed identities for' do |type|
2
+ type_data = type == 'event' ? 'evtData' : 'profileData'
3
+ described_class::ALLOWED_IDENTITIES.each do |id|
4
+ context "when `identity` set as `#{id}` in the event" do
5
+ let!(:params_ext) { params.merge!(identity: id) }
6
+ let!(:data_ext) { data.merge!(id => '1414') }
7
+
8
+ it { is_expected.to include(id => '1414') }
9
+ it { expect(subject[type_data]).not_to include(id => '1414') }
10
+ end
11
+ end
12
+ end
13
+
14
+ shared_examples_for 'choosing identity for' do |type|
15
+ evt_name = type == 'event' ? { name: 'Evt' } : {}
16
+ before { CleverTap.setup { |c| c.identity_field = 'ID' } }
17
+ let(:params) { { data: data }.merge!(evt_name) }
18
+ let(:data) { { 'ID' => 1, 'Name' => 'John' } }
19
+
20
+ context 'when custom `identity` from config' do
21
+ it { is_expected.to include 'identity' => '1' }
22
+ end
23
+
24
+ context 'when `identity` different from ALLOWED_IDENTITIES and config' do
25
+ let!(:params_ext) { params.merge!(identity: 'email') }
26
+ let!(:data_ext) { data.merge!('email' => 'example@email.com') }
27
+
28
+ it { is_expected.to include 'identity' => '1' }
29
+ end
30
+
31
+ context 'when `identity` missing from `data`' do
32
+ let(:data) { { 'Name' => 'John' } }
33
+
34
+ it { expect { subject }.to raise_error CleverTap::MissingIdentityError }
35
+ end
36
+
37
+ it_behaves_like 'setting allowed identities for', type
38
+ end
39
+
40
+ shared_examples_for 'choosing timestamp' do
41
+ let(:data) { { 'FBID' => 1, 'Name' => 'John' } }
42
+ let(:params) { { data: data, identity: 'FBID', name: 'evt' } }
43
+
44
+ context 'when no `timestamp_field`' do
45
+ it { is_expected.not_to include 'ts' }
46
+ end
47
+
48
+ context 'when specific `timestamp` field' do
49
+ let!(:data_ext) { data.merge!('Open Time' => open_time) }
50
+ let!(:params_ext) { params.merge!(timestamp_field: 'Open Time') }
51
+
52
+ context 'and `timestamp_field` is Unix timestamp' do
53
+ let(:open_time) { '1508241881' }
54
+ it { is_expected.to include('ts' => open_time.to_i) }
55
+ end
56
+
57
+ context 'and `timestamp_field` is `DateTime` timestamp' do
58
+ let(:open_time) { Time.now }
59
+ it { is_expected.to include('ts' => open_time.to_i) }
60
+ end
61
+ end
62
+
63
+ context 'when `custom_timestamp` specified' do
64
+ let!(:params_ext) { params.merge!(custom_timestamp: open_time) }
65
+
66
+ context 'and `custom_timestamp` is Unix timestamp' do
67
+ let(:open_time) { '1508241881' }
68
+ it { is_expected.to include('ts' => open_time.to_i) }
69
+ end
70
+
71
+ context 'and `custom_timestamp` is `DateTime` timestamp' do
72
+ let(:open_time) { Time.now }
73
+ it { is_expected.to include('ts' => open_time.to_i) }
74
+ end
75
+ end
76
+ end
77
+
78
+ shared_examples_for 'proper type' do
79
+ let(:data) { { 'FBID' => '1414', 'Name' => 'John' } }
80
+ let(:params) { { data: data, name: 'e', identity: 'FBID' } }
81
+
82
+ it { is_expected.to include described_class::TYPE_KEY_STRING => described_class::TYPE_VALUE_STRING }
83
+ end
84
+
85
+ shared_examples_for 'constructing data for' do |type|
86
+ obj_type = type == 'event' ? 'evtData' : 'profileData'
87
+ evt_name = type == 'event' ? { name: 'Evt' } : {}
88
+
89
+ let(:data) { { 'FBID' => '1414', 'Name' => 'John' } }
90
+ let(:params) { { data: data, identity: 'FBID' }.merge!(evt_name) }
91
+
92
+ context 'when no `data` param in `params` hash' do
93
+ let(:params) { {}.merge!(evt_name) }
94
+ it { expect { subject }.to raise_error CleverTap::NoDataError }
95
+ end
96
+
97
+ context 'when `data` empty hash in `params` hash' do
98
+ let(:params) { { data: {} }.merge!(evt_name) }
99
+ it { expect { subject }.to raise_error CleverTap::NoDataError }
100
+ end
101
+
102
+ context 'when `data` available in `params` hash' do
103
+ it { is_expected.to include(obj_type => { 'Name' => 'John' }) }
104
+ end
105
+ end
@@ -0,0 +1,18 @@
1
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
2
+
3
+ require 'clever_tap'
4
+
5
+ require 'vcr'
6
+ require 'vcr_config'
7
+
8
+ require 'factories/profile'
9
+ require 'pry-byebug'
10
+
11
+ # Use for recording VCR cassettes
12
+ AUTH_ACCOUNT_ID = ENV['CLEVER_TAP_ACCOUNT_ID'] || 'fake-id'
13
+ AUTH_PASSCODE = ENV['CLEVER_TAP_PASSCODE'] || 'fake-passcode'
14
+
15
+ RSpec.configure do |config|
16
+ config.expect_with(:rspec) { |c| c.syntax = :expect }
17
+ config.mock_with(:rspec) { |c| c.syntax = :expect }
18
+ end
@@ -0,0 +1,279 @@
1
+ require 'spec_helper'
2
+ require 'shared/clever_tap_client'
3
+
4
+ describe CleverTap::Client, vcr: true do
5
+ subject { described_class.new('123456', 'passcode') }
6
+
7
+ describe 'authentication' do
8
+ it 'send right `account_id`' do
9
+ expect(subject.connection.headers['X-CleverTap-Account-Id']).to eq('123456')
10
+ end
11
+
12
+ it 'send right `passcode`' do
13
+ expect(subject.connection.headers['X-CleverTap-Passcode']).to eq('passcode')
14
+ end
15
+ end
16
+
17
+ describe 'customisations' do
18
+ context 'with different adapter' do
19
+ subject do
20
+ described_class.new('123456', 'passcode') { |config| config.adapter(:test) }
21
+ end
22
+
23
+ it 'override the default adapter' do
24
+ handlers = subject.connection.builder.handlers
25
+
26
+ expect(handlers.count).to eq(1)
27
+ expect(handlers.first).to eq(Faraday::Adapter::Test)
28
+ end
29
+ end
30
+
31
+ context 'without an adapter' do
32
+ subject { described_class.new('123456', 'passcode') }
33
+
34
+ it 'use Net::HTTP adapter' do
35
+ handlers = subject.connection.builder.handlers
36
+
37
+ expect(handlers.count).to eq(1)
38
+ expect(handlers.first).to eq(Faraday::Adapter::NetHttp)
39
+ end
40
+ end
41
+ end
42
+
43
+ describe '#new' do
44
+ let(:identity_field) { 'ID' }
45
+ let(:account_id) { 'ABC1234' }
46
+ let(:account_passcode) { 'AcCPasScoDe123' }
47
+
48
+ context 'when credentials set in `CleverTap.setup`' do
49
+ subject { CleverTap::Client.new }
50
+
51
+ before do
52
+ CleverTap.setup do |config|
53
+ config.identity_field = identity_field
54
+ config.account_id = account_id
55
+ config.account_passcode = account_passcode
56
+ end
57
+ end
58
+
59
+ it_behaves_like 'configured `Client`'
60
+ end
61
+
62
+ context 'when credentials set in `CleverTap::Client.new`' do
63
+ subject { CleverTap::Client.new(account_id, account_passcode) }
64
+ it_behaves_like 'configured `Client`'
65
+ end
66
+ end
67
+
68
+ def event_factory(id, name)
69
+ CleverTap::Event.new(
70
+ data: { 'FBID' => id.to_s, 'Name' => name.to_s },
71
+ name: 'Web Event',
72
+ identity: 'FBID'
73
+ )
74
+ end
75
+
76
+ def profile_factory
77
+ CleverTap::Profile.new(
78
+ data: { 'ID' => '1414', 'Name' => 'John', 'Phone' => '+44+441234' }
79
+ )
80
+ end
81
+
82
+ describe '#upload' do
83
+ let(:success_proc) { proc { 'sample proc' } }
84
+ let(:response) { subject.upload([event1, event2]) }
85
+
86
+ subject do
87
+ client = described_class.new
88
+ client.on_successful_upload(&success_proc)
89
+ client
90
+ end
91
+
92
+ before do
93
+ CleverTap.setup do |c|
94
+ c.identity_field = 'ID'
95
+ c.account_id = AUTH_ACCOUNT_ID
96
+ c.account_passcode = AUTH_PASSCODE
97
+ end
98
+ end
99
+
100
+ context 'when upload records are homogenous' do
101
+ context 'and valid records' do
102
+ let(:event1) { event_factory('1414', 'John') }
103
+ let(:event2) { event_factory('1515', 'Jill') }
104
+
105
+ context 'and objects fit `upload_limit`' do
106
+ VCR.use_cassette('upload within Event limit') do
107
+ it 'returns an array with one successful `Response` object' do
108
+ expect(response.count).to eq 1
109
+ expect(response.first.success).to be true
110
+ expect(response.first).to be_a(CleverTap::Response)
111
+ end
112
+
113
+ it 'calls `on_successful_upload` proc once' do
114
+ expect(success_proc).to receive(:call).once
115
+ subject.upload([event1, event2])
116
+ end
117
+ end
118
+ end
119
+
120
+ context 'and objects do not fit `upload_limit`' do
121
+ before do
122
+ allow(CleverTap::Event)
123
+ .to receive(:upload_limit).and_return(1)
124
+ end
125
+
126
+ VCR.use_cassette('upload out of Event limit') do
127
+ it 'returns an array with two successful `Response` objects' do
128
+ expect(response.count).to eq 2
129
+ expect(response.all?(&:success)).to be true
130
+ expect(
131
+ response.all? { |r| r.class == CleverTap::Response }
132
+ ).to be true
133
+ end
134
+
135
+ it 'calls `on_successful_upload` proc twice' do
136
+ expect(success_proc).to receive(:call).twice
137
+ subject.upload([event1, event2])
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ context 'and invalid records' do
144
+ let(:failure_proc) { proc { 'sample proc' } }
145
+ let(:response) { subject.upload([profile]) }
146
+
147
+ subject do
148
+ client = described_class.new
149
+ client.on_failed_upload(&failure_proc)
150
+ client
151
+ end
152
+
153
+ let(:profile) { profile_factory }
154
+
155
+ it 'returns an array with one failed `Response` object' do
156
+ expect(response.count).to eq 1
157
+ expect(response.first.class).to eq CleverTap::Response
158
+ expect(response.first.success).to be false
159
+ end
160
+
161
+ it 'calls `on_failed_upload` once' do
162
+ expect(failure_proc).to receive(:call).once
163
+ subject.upload([profile])
164
+ end
165
+ end
166
+ end
167
+
168
+ context 'when objects are not homogenous' do
169
+ let(:event1) { event_factory('1414', 'John') }
170
+ let(:event2) { {} }
171
+
172
+ it 'raises `NotConsistentArrayError`' do
173
+ expect { subject.upload([event1, event2]) }.to raise_error CleverTap::NotConsistentArrayError
174
+ end
175
+ end
176
+ end
177
+
178
+ describe '#request_body' do
179
+ subject { described_class.new('123456', 'passcode') }
180
+ let(:records) do
181
+ [
182
+ { 'ID' => '123', 'Name' => 'John' },
183
+ { 'ID' => '456', 'Name' => 'Jill' }
184
+ ]
185
+ end
186
+
187
+ it 'converts records hash to json' do
188
+ expect(subject.send(:request_body, records))
189
+ .to eq({ 'd' => records }.to_json)
190
+ end
191
+ end
192
+
193
+ describe '#determine_type' do
194
+ subject { described_class.new('123456', 'passcode') }
195
+
196
+ context 'when records are of the same type' do
197
+ let(:records) { [{}, {}] }
198
+
199
+ it 'returns the class of the elements' do
200
+ expect(subject.send(:determine_type, records)).to eq Hash
201
+ end
202
+ end
203
+
204
+ context 'when records are of different type' do
205
+ let(:records) { [{}, []] }
206
+
207
+ it 'raises `NotConsistentArrayError`' do
208
+ expect { subject.send(:determine_type, records) }.to raise_error CleverTap::NotConsistentArrayError
209
+ end
210
+ end
211
+ end
212
+
213
+ describe '#ensure_array' do
214
+ subject { described_class.new('123456', 'passcode') }
215
+ let(:records) { %w[sample sample2] }
216
+ let(:record) { 'sample' }
217
+
218
+ it 'returns an array when an array passed' do
219
+ expect(subject.send(:ensure_array, records)).to eq records
220
+ end
221
+
222
+ it 'returns an array when a single element passed' do
223
+ expect(subject.send(:ensure_array, record)).to eq [record]
224
+ end
225
+ end
226
+
227
+ describe 'setting `account_id` and `passcode`' do
228
+ subject { described_class.new(client_id, client_passcode) }
229
+ let(:client_id) { '123456' }
230
+ let(:client_passcode) { 'passcode' }
231
+ let(:config_id) { 'config_account_id' }
232
+ let(:config_passcode) { 'config_passcode' }
233
+
234
+ def config_client(account, pass)
235
+ CleverTap.setup do |c|
236
+ c.account_id = account
237
+ c.account_passcode = pass
238
+ end
239
+ end
240
+
241
+ context 'when credentials provided in configuration' do
242
+ before { config_client(config_id, config_passcode) }
243
+
244
+ context 'and in initialization as well' do
245
+ it 'has initialization values' do
246
+ expect(subject.send(:assign_account_id, client_id)).to eq client_id
247
+ expect(subject.send(:assign_passcode, client_passcode)).to eq client_passcode
248
+ end
249
+ end
250
+
251
+ context 'and not in initialization' do
252
+ it 'has initialization values' do
253
+ expect(subject.send(:assign_account_id, nil)).to eq config_id
254
+ expect(subject.send(:assign_passcode, nil)).to eq config_passcode
255
+ end
256
+ end
257
+ end
258
+
259
+ context 'when credentials not provided in configuration' do
260
+ before { config_client(nil, nil) }
261
+
262
+ context 'but provided in initialization' do
263
+ it 'has initialization values' do
264
+ expect(subject.send(:assign_account_id, client_id)).to eq client_id
265
+ expect(subject.send(:assign_passcode, client_passcode)).to eq client_passcode
266
+ end
267
+ end
268
+
269
+ context 'and not provided in initialization as well' do
270
+ before { config_client(nil, nil) }
271
+ subject { described_class.new }
272
+
273
+ it 'raises a `RuntimeError` error' do
274
+ expect { subject }.to raise_error RuntimeError
275
+ end
276
+ end
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,88 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe CleverTap do
4
+ describe '#new' do
5
+ context 'with valid arguments' do
6
+ context 'with keyword arguments' do
7
+ subject(:clever_tap) do
8
+ CleverTap.new(account_id: 'foo',
9
+ passcode: 'passcode',
10
+ identity_field: 'ID',
11
+ configure_faraday: configure_faraday)
12
+ end
13
+
14
+ let(:configure_faraday) { proc {} }
15
+
16
+ it('persist the account_id') { expect(clever_tap.config.account_id).to eq('foo') }
17
+ it('persist the passcode') { expect(clever_tap.config.passcode).to eq('passcode') }
18
+ it('persist the identity field') { expect(clever_tap.config.identity_field).to eq('ID') }
19
+ it('persist the faraday config') { expect(clever_tap.config.configure_faraday).to eq(configure_faraday) }
20
+ end
21
+
22
+ context 'with block' do
23
+ subject(:clever_tap) do
24
+ CleverTap.new do |config|
25
+ config.account_id = 'foo'
26
+ config.passcode = 'passcode'
27
+ config.identity_field = 'ID'
28
+ config.configure_faraday(&configure_faraday)
29
+ end
30
+ end
31
+
32
+ let(:configure_faraday) { proc {} }
33
+
34
+ it('persist the account_id') { expect(clever_tap.config.account_id).to eq('foo') }
35
+ it('persist the passcode') { expect(clever_tap.config.passcode).to eq('passcode') }
36
+ it('persist the identity field') { expect(clever_tap.config.identity_field).to eq('ID') }
37
+ it('persist the faraday config') { expect(clever_tap.config.configure_faraday).to eq(configure_faraday) }
38
+ end
39
+ end
40
+
41
+ context 'with invalid arguments' do
42
+ it 'require `account_id`' do
43
+ expect { CleverTap.new(passcode: 'foo') }
44
+ .to raise_error(RuntimeError, /account_id/)
45
+ end
46
+
47
+ it 'require `passcode`' do
48
+ expect { CleverTap.new(account_id: 'bar') }
49
+ .to raise_error(RuntimeError, /passcode/)
50
+ end
51
+ end
52
+ end
53
+
54
+ describe '#client' do
55
+ subject(:clever_tap) { CleverTap.new(account_id: 'foo', passcode: 'passcode', &configure_faraday) }
56
+ let(:configure_faraday) { proc { |_faraday| } }
57
+
58
+ it 'initialize client with a right auth data' do
59
+ expect(CleverTap::Client).to receive(:new).with('foo', 'passcode', &configure_faraday)
60
+
61
+ clever_tap.client
62
+ end
63
+
64
+ it 'cache the client between calls' do
65
+ id = clever_tap.client.object_id
66
+ expect(clever_tap.client.object_id).to eq(id)
67
+ end
68
+ end
69
+
70
+ describe '.config' do
71
+ let(:identity_field) { 'ID' }
72
+ let(:account_id) { 'ABC1234' }
73
+ let(:account_passcode) { 'AcCPasScoDe123' }
74
+ let(:remove_identity) { true }
75
+
76
+ it 'sets config variables' do
77
+ described_class.setup do |config|
78
+ config.identity_field = identity_field
79
+ config.account_id = account_id
80
+ config.account_passcode = account_passcode
81
+ end
82
+
83
+ expect(described_class.identity_field).to eq identity_field
84
+ expect(described_class.account_id).to eq account_id
85
+ expect(described_class.account_passcode).to eq account_passcode
86
+ end
87
+ end
88
+ end