customerio 2.2.1 → 4.0.1

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.
@@ -0,0 +1,11 @@
1
+ require 'net/http'
2
+ require 'multi_json'
3
+
4
+ module Customerio
5
+ module Regions
6
+ Region = Struct.new(:track_url, :api_url)
7
+
8
+ US = Customerio::Regions::Region.new('https://track.customer.io', 'https://api.customer.io').freeze
9
+ EU = Customerio::Regions::Region.new('https://track-eu.customer.io', 'https://api-eu.customer.io').freeze
10
+ end
11
+ end
@@ -0,0 +1,49 @@
1
+ require 'base64'
2
+
3
+ module Customerio
4
+ class SendEmailRequest
5
+ attr_reader :message
6
+
7
+ def initialize(opts)
8
+ @message = opts.delete_if { |field| invalid_field?(field) }
9
+ @message[:attachments] = {}
10
+ @message[:headers] = {}
11
+ end
12
+
13
+ def attach(name, data, encode: true)
14
+ raise "attachment #{name} already exists" if @message[:attachments].has_key?(name)
15
+ @message[:attachments][name] = encode ? Base64.strict_encode64(data) : data
16
+ end
17
+
18
+ private
19
+
20
+ REQUIRED_FIELDS = %i(to identifiers)
21
+
22
+ OPTIONAL_FIELDS = %i(
23
+ transactional_message_id
24
+ message_data
25
+ headers
26
+ preheader
27
+ from
28
+ reply_to
29
+ bcc
30
+ subject
31
+ body
32
+ plaintext_body
33
+ amp_body
34
+ fake_bcc
35
+ disable_message_retention
36
+ send_to_unsubscribed
37
+ tracked
38
+ queue_draft
39
+ )
40
+
41
+ def invalid_field?(field)
42
+ !REQUIRED_FIELDS.include?(field) && !OPTIONAL_FIELDS.include?(field)
43
+ end
44
+
45
+ def encode(data)
46
+ Base64.strict_encode64(data)
47
+ end
48
+ end
49
+ end
@@ -1,3 +1,3 @@
1
1
  module Customerio
2
- VERSION = "2.2.1"
2
+ VERSION = "4.0.1"
3
3
  end
@@ -0,0 +1,172 @@
1
+ require 'spec_helper'
2
+ require 'multi_json'
3
+ require 'base64'
4
+ require 'tempfile'
5
+
6
+ describe Customerio::APIClient do
7
+ let(:app_key) { "appkey" }
8
+
9
+ let(:client) { Customerio::APIClient.new(app_key) }
10
+ let(:response) { double("Response", code: 200) }
11
+
12
+ def api_uri(path)
13
+ "https://api.customer.io#{path}"
14
+ end
15
+
16
+ def request_headers
17
+ { 'Authorization': "Bearer #{app_key}", 'Content-Type': 'application/json' }
18
+ end
19
+
20
+ def json(data)
21
+ MultiJson.dump(data)
22
+ end
23
+
24
+ it "the base client is initialised with the correct values when no region is passed in" do
25
+ app_key = "appkey"
26
+
27
+ expect(Customerio::BaseClient).to(
28
+ receive(:new)
29
+ .with(
30
+ { app_key: app_key },
31
+ {
32
+ region: Customerio::Regions::US,
33
+ url: Customerio::Regions::US.api_url
34
+ }
35
+ )
36
+ )
37
+
38
+ client = Customerio::APIClient.new(app_key)
39
+ end
40
+
41
+ it "raises an error when an incorrect region is passed in" do
42
+ expect {
43
+ Customerio::APIClient.new("appkey", region: :au)
44
+ }.to raise_error /region must be an instance of Customerio::Regions::Region/
45
+ end
46
+
47
+ [Customerio::Regions::US, Customerio::Regions::EU].each do |region|
48
+ it "the base client is initialised with the correct values when the region \"#{region}\" is passed in" do
49
+ app_key = "appkey"
50
+
51
+ expect(Customerio::BaseClient).to(
52
+ receive(:new)
53
+ .with(
54
+ { app_key: app_key },
55
+ {
56
+ region: region,
57
+ url: region.api_url
58
+ }
59
+ )
60
+ )
61
+
62
+ client = Customerio::APIClient.new(app_key, { region: region })
63
+ end
64
+ end
65
+
66
+ describe "#send_email" do
67
+ it "sends a POST request to the /api/send/email path" do
68
+ req = Customerio::SendEmailRequest.new(
69
+ identifiers: {
70
+ id: 'c1',
71
+ },
72
+ transactional_message_id: 1,
73
+ )
74
+
75
+ stub_request(:post, api_uri('/v1/send/email'))
76
+ .with(headers: request_headers, body: req.message)
77
+ .to_return(status: 200, body: { delivery_id: 1 }.to_json, headers: {})
78
+
79
+ client.send_email(req).should eq({ "delivery_id" => 1 })
80
+ end
81
+
82
+ it "handles validation failures (400)" do
83
+ req = Customerio::SendEmailRequest.new(
84
+ identifiers: {
85
+ id: 'c1',
86
+ },
87
+ transactional_message_id: 1,
88
+ )
89
+
90
+ err_json = { meta: { error: "example error" } }.to_json
91
+
92
+ stub_request(:post, api_uri('/v1/send/email'))
93
+ .with(headers: request_headers, body: req.message)
94
+ .to_return(status: 400, body: err_json, headers: {})
95
+
96
+ lambda { client.send_email(req) }.should(
97
+ raise_error(Customerio::InvalidResponse) { |error|
98
+ error.message.should eq "example error"
99
+ error.code.should eq "400"
100
+ }
101
+ )
102
+ end
103
+
104
+ it "handles other failures (5xx)" do
105
+ req = Customerio::SendEmailRequest.new(
106
+ identifiers: {
107
+ id: 'c1',
108
+ },
109
+ transactional_message_id: 1,
110
+ )
111
+
112
+ stub_request(:post, api_uri('/v1/send/email'))
113
+ .with(headers: request_headers, body: req.message)
114
+ .to_return(status: 500, body: "Server unavailable", headers: {})
115
+
116
+ lambda { client.send_email(req) }.should(
117
+ raise_error(Customerio::InvalidResponse) { |error|
118
+ error.message.should eq "Server unavailable"
119
+ error.code.should eq "500"
120
+ }
121
+ )
122
+ end
123
+
124
+ it "allows attaching file content without encoding" do
125
+ content = 'sample content'
126
+
127
+ req = Customerio::SendEmailRequest.new(
128
+ customer_id: 'c1',
129
+ transactional_message_id: 1,
130
+ )
131
+
132
+ req.attach('test', content, encode: false)
133
+ req.message[:attachments]['test'].should eq content
134
+
135
+ stub_request(:post, api_uri('/v1/send/email'))
136
+ .with(headers: request_headers, body: req.message)
137
+ .to_return(status: 200, body: { delivery_id: 1 }.to_json, headers: {})
138
+
139
+ client.send_email(req)
140
+ end
141
+
142
+ it "allows attaching files with encoding (default)" do
143
+ content = 'sample content'
144
+
145
+ req = Customerio::SendEmailRequest.new(
146
+ customer_id: 'c1',
147
+ transactional_message_id: 1,
148
+ )
149
+
150
+ req.attach('test', content)
151
+ req.message[:attachments]['test'].should eq Base64.strict_encode64(content)
152
+
153
+ stub_request(:post, api_uri('/v1/send/email'))
154
+ .with(headers: request_headers, body: req.message)
155
+ .to_return(status: 200, body: { delivery_id: 1 }.to_json, headers: {})
156
+
157
+ client.send_email(req)
158
+ end
159
+
160
+ it "raises error when attaching the same key again" do
161
+ req = Customerio::SendEmailRequest.new(
162
+ customer_id: 'c1',
163
+ transactional_message_id: 1,
164
+ )
165
+
166
+ req.attach('test', 'test-content')
167
+
168
+ lambda { req.attach('test', '') }.should raise_error(/attachment test already exists/)
169
+ req.message[:attachments].should eq({ "test" => Base64.strict_encode64("test-content") })
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+ require 'multi_json'
3
+ require 'base64'
4
+
5
+ describe Customerio::BaseClient do
6
+ let(:url) { "https://test.customer.io" }
7
+
8
+ let(:site_id) { "SITE_ID" }
9
+ let(:api_key) { "API_KEY" }
10
+ let(:track_client) { Customerio::BaseClient.new({ site_id: site_id, api_key: api_key }, { url: url }) }
11
+
12
+ let(:app_key) { "APP_KEY" }
13
+ let(:api_client) { Customerio::BaseClient.new({ app_key: app_key }, { url: url }) }
14
+
15
+ def api_uri(path)
16
+ "#{url}#{path}"
17
+ end
18
+
19
+ def track_client_request_headers
20
+ token = Base64.strict_encode64("#{site_id}:#{api_key}")
21
+ { 'Authorization': "Basic #{token}", 'Content-Type': 'application/json' }
22
+ end
23
+
24
+ def api_client_request_headers
25
+ { 'Authorization': "Bearer #{app_key}", 'Content-Type': 'application/json' }
26
+ end
27
+
28
+ describe "with a site ID and API key" do
29
+ it "uses the correct basic auth" do
30
+ stub_request(:put, api_uri('/some/path')).
31
+ with(headers: track_client_request_headers).
32
+ to_return(status: 200, body: "", headers: {})
33
+
34
+ track_client.request(:put, '/some/path', "")
35
+ end
36
+ end
37
+
38
+ describe "with an app key" do
39
+ it "uses the correct bearer token" do
40
+ stub_request(:put, api_uri('/some/path')).
41
+ with(headers: api_client_request_headers).
42
+ to_return(status: 200, body: "", headers: {})
43
+
44
+ api_client.request(:put, '/some/path', "")
45
+ end
46
+ end
47
+
48
+ describe "#verify_response" do
49
+ it "throws an error when the response isn't between 200 and 300" do
50
+ stub_request(:put, api_uri('/some/path')).
51
+ with(headers: api_client_request_headers).
52
+ to_return(status: 400, body: "", headers: {})
53
+
54
+ lambda { api_client.request_and_verify_response(:put, '/some/path', "") }.should(
55
+ raise_error(Customerio::InvalidResponse)
56
+ )
57
+ end
58
+
59
+ it "returns the response when the status is 200" do
60
+ stub_request(:put, api_uri('/some/path')).
61
+ with(headers: api_client_request_headers).
62
+ to_return(status: 200, body: "Test", headers: {})
63
+
64
+ api_client.request_and_verify_response(:put, '/some/path', "").body.should eq("Test")
65
+ end
66
+ end
67
+ end
data/spec/client_spec.rb CHANGED
@@ -1,39 +1,92 @@
1
1
  require 'spec_helper'
2
2
  require 'multi_json'
3
-
3
+ require 'base64'
4
4
 
5
5
  describe Customerio::Client do
6
- let(:client) { Customerio::Client.new("SITE_ID", "API_KEY", :json => false) }
7
- let(:response) { double("Response", :code => 200) }
6
+ let(:site_id) { "SITE_ID" }
7
+ let(:api_key) { "API_KEY" }
8
+
9
+ let(:client) { Customerio::Client.new(site_id, api_key) }
10
+ let(:response) { double("Response", code: 200) }
8
11
 
9
12
  def api_uri(path)
10
- "https://SITE_ID:API_KEY@track.customer.io#{path}"
13
+ "https://track.customer.io#{path}"
14
+ end
15
+
16
+ def request_headers
17
+ token = Base64.strict_encode64("#{site_id}:#{api_key}")
18
+ { 'Authorization': "Basic #{token}", 'Content-Type': 'application/json' }
11
19
  end
12
20
 
13
21
  def json(data)
14
22
  MultiJson.dump(data)
15
23
  end
16
24
 
17
- describe "json option" do
18
- let(:body) { { :id => 5, :name => "Bob" } }
25
+ it "the base client is initialised with the correct values when no region is passed in" do
26
+ site_id = "SITE_ID"
27
+ api_key = "API_KEY"
28
+
29
+ expect(Customerio::BaseClient).to(
30
+ receive(:new)
31
+ .with(
32
+ { site_id: site_id, api_key: api_key },
33
+ {
34
+ region: Customerio::Regions::US,
35
+ url: Customerio::Regions::US.track_url
36
+ }
37
+ )
38
+ )
19
39
 
20
- it "uses json by default" do
21
- client = Customerio::Client.new("SITE_ID", "API_KEY")
40
+ client = Customerio::Client.new(site_id, api_key)
41
+ end
22
42
 
23
- stub_request(:put, api_uri('/api/v1/customers/5')).
24
- with(:body => json(body),
25
- :headers => {'Content-Type'=>'application/json'}).
26
- to_return(:status => 200, :body => "", :headers => {})
43
+ it "raises an error when an incorrect region is passed in" do
44
+ expect {
45
+ Customerio::Client.new("siteid", "apikey", region: :au)
46
+ }.to raise_error /region must be an instance of Customerio::Regions::Region/
47
+ end
27
48
 
28
- client.identify(body)
49
+ [Customerio::Regions::US, Customerio::Regions::EU].each do |region|
50
+ it "the base client is initialised with the correct values when the region \"#{region}\" is passed in" do
51
+ site_id = "SITE_ID"
52
+ api_key = "API_KEY"
53
+
54
+ expect(Customerio::BaseClient).to(
55
+ receive(:new)
56
+ .with(
57
+ { site_id: site_id, api_key: api_key },
58
+ {
59
+ region: region,
60
+ url: region.track_url
61
+ }
62
+ )
63
+ )
64
+
65
+ client = Customerio::Client.new(site_id, api_key, { region: region })
29
66
  end
67
+ end
30
68
 
31
- it "allows disabling json" do
32
- client = Customerio::Client.new("SITE_ID", "API_KEY", :json => false)
69
+ it "uses json by default" do
70
+ body = { id: 5, name: "Bob" }
71
+ client = Customerio::Client.new("SITE_ID", "API_KEY")
33
72
 
34
- stub_request(:put, api_uri('/api/v1/customers/5')).
35
- with(:body => { :id => "5", :name => "Bob" }).
36
- to_return(:status => 200, :body => "", :headers => {})
73
+ stub_request(:put, api_uri('/api/v1/customers/5')).
74
+ with(body: json(body),
75
+ headers: {'Content-Type'=>'application/json'}).
76
+ to_return(status: 200, body: "", headers: {})
77
+
78
+ client.identify(body)
79
+ end
80
+
81
+ describe "headers" do
82
+ let(:body) { { id: 1, token: :test } }
83
+
84
+ it "sends the basic headers, base64 encoded with the request" do
85
+ client = Customerio::Client.new("SITE_ID", "API_KEY")
86
+
87
+ stub_request(:put, api_uri('/api/v1/customers/1')).
88
+ with(body: json(body), headers: request_headers).
89
+ to_return(status: 200, body: "", headers: {})
37
90
 
38
91
  client.identify(body)
39
92
  end
@@ -42,41 +95,54 @@ describe Customerio::Client do
42
95
  describe "#identify" do
43
96
  it "sends a PUT request to customer.io's customer API" do
44
97
  stub_request(:put, api_uri('/api/v1/customers/5')).
45
- with(:body => "id=5").
46
- to_return(:status => 200, :body => "", :headers => {})
98
+ with(body: json(id: "5")).
99
+ to_return(status: 200, body: "", headers: {})
47
100
 
48
- client.identify(:id => 5)
101
+ client.identify(id: "5")
102
+ end
103
+
104
+ it "escapes customer IDs" do
105
+ stub_request(:put, api_uri('/api/v1/customers/5%20')).
106
+ with(body: json({ id: "5 " })).
107
+ to_return(status: 200, body: "", headers: {})
108
+
109
+ client.identify(id: "5 ")
110
+
111
+ stub_request(:put, api_uri('/api/v1/customers/5%2F')).
112
+ with(body: { id: "5/" }).
113
+ to_return(status: 200, body: "", headers: {})
114
+ client.identify(id: "5/")
49
115
  end
50
116
 
51
117
  it "sends a PUT request to customer.io's customer API using json headers" do
52
- client = Customerio::Client.new("SITE_ID", "API_KEY", :json => true)
53
- body = { :id => 5, :name => "Bob" }
118
+ client = Customerio::Client.new("SITE_ID", "API_KEY", json: true)
119
+ body = { id: 5, name: "Bob" }
54
120
 
55
121
  stub_request(:put, api_uri('/api/v1/customers/5')).
56
- with(:body => json(body),
57
- :headers => {'Content-Type'=>'application/json'}).
58
- to_return(:status => 200, :body => "", :headers => {})
122
+ with(body: json(body),
123
+ headers: {'Content-Type'=>'application/json'}).
124
+ to_return(status: 200, body: "", headers: {})
59
125
 
60
126
  client.identify(body)
61
127
  end
62
128
 
63
129
  it "raises an error if PUT doesn't return a 2xx response code" do
64
130
  stub_request(:put, api_uri('/api/v1/customers/5')).
65
- with(:body => "id=5").
66
- to_return(:status => 500, :body => "", :headers => {})
131
+ with(body: json(id: 5)).
132
+ to_return(status: 500, body: "", headers: {})
67
133
 
68
- lambda { client.identify(:id => 5) }.should raise_error(Customerio::Client::InvalidResponse)
134
+ lambda { client.identify(id: 5) }.should raise_error(Customerio::InvalidResponse)
69
135
  end
70
136
 
71
137
  it "includes the HTTP response with raised errors" do
72
138
  stub_request(:put, api_uri('/api/v1/customers/5')).
73
- with(:body => "id=5").
74
- to_return(:status => 500, :body => "whatever", :headers => {})
139
+ with(body: json(id: 5)).
140
+ to_return(status: 500, body: "Server unavailable", headers: {})
75
141
 
76
- lambda { client.identify(:id => 5) }.should raise_error {|error|
77
- error.should be_a Customerio::Client::InvalidResponse
78
- error.response.code.should eq "500"
79
- error.response.body.should eq "whatever"
142
+ lambda { client.identify(id: 5) }.should raise_error {|error|
143
+ error.should be_a Customerio::InvalidResponse
144
+ error.code.should eq "500"
145
+ error.message.should eq "Server unavailable"
80
146
  }
81
147
  end
82
148
 
@@ -84,31 +150,34 @@ describe Customerio::Client do
84
150
  time = Time.now.to_i
85
151
 
86
152
  stub_request(:put, api_uri('/api/v1/customers/5')).with(
87
- :body => {
88
- :id => "5",
89
- :email => "customer@example.com",
90
- :created_at => time.to_s,
91
- :first_name => "Bob",
92
- :plan => "basic"
93
- }).to_return(:status => 200, :body => "", :headers => {})
153
+ body: {
154
+ id: 5,
155
+ email: "customer@example.com",
156
+ created_at: time,
157
+ first_name: "Bob",
158
+ plan: "basic",
159
+ anonymous_id: "anon-id"
160
+ }).to_return(status: 200, body: "", headers: {})
94
161
 
95
162
  client.identify({
96
- :id => 5,
97
- :email => "customer@example.com",
98
- :created_at => time,
99
- :first_name => "Bob",
100
- :plan => "basic"
163
+ id: 5,
164
+ anonymous_id: "anon-id",
165
+ email: "customer@example.com",
166
+ created_at: time,
167
+ first_name: "Bob",
168
+ plan: "basic"
101
169
  })
102
170
  end
103
171
 
104
172
  it "requires an id attribute" do
105
- lambda { client.identify(:email => "customer@example.com") }.should raise_error(Customerio::Client::MissingIdAttributeError)
173
+ lambda { client.identify(email: "customer@example.com") }.should raise_error(Customerio::Client::MissingIdAttributeError)
174
+ lambda { client.identify(id: "") }.should raise_error(Customerio::Client::MissingIdAttributeError)
106
175
  end
107
176
 
108
177
  it 'should not raise errors when attribute keys are strings' do
109
178
  stub_request(:put, api_uri('/api/v1/customers/5')).
110
- with(:body => "id=5").
111
- to_return(:status => 200, :body => "", :headers => {})
179
+ with(body: json(id: 5)).
180
+ to_return(status: 200, body: "", headers: {})
112
181
 
113
182
  attributes = { "id" => 5 }
114
183
 
@@ -119,257 +188,265 @@ describe Customerio::Client do
119
188
  describe "#delete" do
120
189
  it "sends a DELETE request to the customer.io's event API" do
121
190
  stub_request(:delete, api_uri('/api/v1/customers/5')).
122
- to_return(:status => 200, :body => "", :headers => {})
191
+ to_return(status: 200, body: "", headers: {})
123
192
 
124
193
  client.delete(5)
125
194
  end
195
+
196
+ it "throws an error when customer_id is missing" do
197
+ stub_request(:put, /track.customer.io/)
198
+ .to_return(status: 200, body: "", headers: {})
199
+
200
+ lambda { client.delete(" ") }.should raise_error(Customerio::Client::ParamError, "customer_id must be a non-empty string")
201
+ end
202
+
203
+ it "escapes customer IDs" do
204
+ stub_request(:delete, api_uri('/api/v1/customers/5%20')).
205
+ to_return(status: 200, body: "", headers: {})
206
+
207
+ client.delete("5 ")
208
+ end
126
209
  end
127
210
 
128
211
  describe "#suppress" do
129
212
  it "sends a POST request to the customer.io's suppress API" do
130
213
  stub_request(:post, api_uri('/api/v1/customers/5/suppress')).
131
- to_return(:status => 200, :body => "", :headers => {})
214
+ to_return(status: 200, body: "", headers: {})
132
215
 
133
216
  client.suppress(5)
134
217
  end
218
+
219
+ it "throws an error when customer_id is missing" do
220
+ stub_request(:put, /track.customer.io/)
221
+ .to_return(status: 200, body: "", headers: {})
222
+
223
+ lambda { client.suppress(" ") }.should raise_error(Customerio::Client::ParamError, "customer_id must be a non-empty string")
224
+ end
135
225
  end
136
226
 
137
227
  describe "#unsuppress" do
138
228
  it "sends a POST request to the customer.io's unsuppress API" do
139
229
  stub_request(:post, api_uri('/api/v1/customers/5/unsuppress')).
140
- to_return(:status => 200, :body => "", :headers => {})
230
+ to_return(status: 200, body: "", headers: {})
141
231
 
142
232
  client.unsuppress(5)
143
233
  end
234
+
235
+ it "throws an error when customer_id is missing" do
236
+ stub_request(:put, /track.customer.io/)
237
+ .to_return(status: 200, body: "", headers: {})
238
+
239
+ lambda { client.suppress(" ") }.should raise_error(Customerio::Client::ParamError, "customer_id must be a non-empty string")
240
+ end
144
241
  end
145
242
 
146
243
  describe "#track" do
147
244
  it "raises an error if POST doesn't return a 2xx response code" do
148
245
  stub_request(:post, api_uri('/api/v1/customers/5/events')).
149
- with(:body => "name=purchase").
150
- to_return(:status => 500, :body => "", :headers => {})
246
+ with(body: json(name: "purchase", data: {})).
247
+ to_return(status: 500, body: "", headers: {})
248
+
249
+ lambda { client.track(5, "purchase") }.should raise_error(Customerio::InvalidResponse)
250
+ end
251
+
252
+ it "throws an error when customer_id or event_name is missing" do
253
+ stub_request(:put, /track.customer.io/)
254
+ .to_return(status: 200, body: "", headers: {})
151
255
 
152
- lambda { client.track(5, "purchase") }.should raise_error(Customerio::Client::InvalidResponse)
256
+ lambda { client.track(" ", "test_event") }.should raise_error(Customerio::Client::ParamError, "customer_id must be a non-empty string")
257
+ lambda { client.track(5, " ") }.should raise_error(Customerio::Client::ParamError, "event_name must be a non-empty string")
153
258
  end
154
259
 
155
260
  it "uses the site_id and api key for basic auth and sends the event name" do
156
261
  stub_request(:post, api_uri('/api/v1/customers/5/events')).
157
- with(:body => "name=purchase").
158
- to_return(:status => 200, :body => "", :headers => {})
262
+ with(body: json(name: "purchase", data: {})).
263
+ to_return(status: 200, body: "", headers: {})
159
264
 
160
265
  client.track(5, "purchase")
161
266
  end
162
267
 
163
268
  it "sends any optional event attributes" do
164
269
  stub_request(:post, api_uri('/api/v1/customers/5/events')).
165
- with(:body => {
166
- :name => "purchase",
167
- :data => {
168
- :type => "socks",
169
- :price => "13.99"
270
+ with(body: json({
271
+ name: "purchase",
272
+ data: {
273
+ type: "socks",
274
+ price: "13.99"
170
275
  }
171
- }).
172
- to_return(:status => 200, :body => "", :headers => {})
276
+ })).
277
+ to_return(status: 200, body: "", headers: {})
173
278
 
174
- client.track(5, "purchase", :type => "socks", :price => "13.99")
279
+ client.track(5, "purchase", type: "socks", price: "13.99")
175
280
  end
176
281
 
177
282
  it "copes with arrays" do
178
283
  stub_request(:post, api_uri('/api/v1/customers/5/events')).
179
- with(:body => {
180
- :name => "event",
181
- :data => {
182
- :things => ["a", "b", "c"]
284
+ with(body: {
285
+ name: "event",
286
+ data: {
287
+ things: ["a", "b", "c"]
183
288
  }
184
289
  }).
185
- to_return(:status => 200, :body => "", :headers => {})
290
+ to_return(status: 200, body: "", headers: {})
186
291
 
187
- client.track(5, "event", :things => ["a", "b", "c"])
292
+ client.track(5, "event", things: ["a", "b", "c"])
188
293
  end
189
294
 
190
295
  it "copes with hashes" do
191
296
  stub_request(:post, api_uri('/api/v1/customers/5/events')).
192
- with(:body => {
193
- :name => "event",
194
- :data => {
195
- :stuff => { :a => "b" }
297
+ with(body: {
298
+ name: "event",
299
+ data: {
300
+ stuff: { a: "b" }
196
301
  }
197
302
  }).
198
- to_return(:status => 200, :body => "", :headers => {})
303
+ to_return(status: 200, body: "", headers: {})
199
304
 
200
- client.track(5, "event", :stuff => { :a => "b" })
305
+ client.track(5, "event", stuff: { a: "b" })
201
306
  end
202
307
 
203
308
  it "sends a POST request as json using json headers" do
204
- client = Customerio::Client.new("SITE_ID", "API_KEY", :json => true)
205
- data = { :type => "socks", :price => "13.99" }
206
- body = { :name => "purchase", :data => data }
309
+ client = Customerio::Client.new("SITE_ID", "API_KEY", json: true)
310
+ data = { type: "socks", price: "13.99" }
311
+ body = { name: "purchase", data: data }
207
312
 
208
313
  stub_request(:post, api_uri('/api/v1/customers/5/events')).
209
- with(:body => json(body),
210
- :headers => {'Content-Type'=>'application/json'}).
211
- to_return(:status => 200, :body => "", :headers => {})
314
+ with(body: json(body),
315
+ headers: {'Content-Type'=>'application/json'}).
316
+ to_return(status: 200, body: "", headers: {})
212
317
 
213
318
  client.track(5, "purchase", data)
214
319
  end
215
320
 
216
321
  it "allows sending of a timestamp" do
217
322
  stub_request(:post, api_uri('/api/v1/customers/5/events')).
218
- with(:body => {
219
- :name => "purchase",
220
- :data => {
221
- :type => "socks",
222
- :price => "13.99",
223
- :timestamp => "1561231234"
323
+ with(body: json({
324
+ name: "purchase",
325
+ data: {
326
+ type: "socks",
327
+ price: "13.99",
328
+ timestamp: 1561231234
224
329
  },
225
- :timestamp => "1561231234"
226
- }).
227
- to_return(:status => 200, :body => "", :headers => {})
330
+ timestamp: 1561231234
331
+ })).
332
+ to_return(status: 200, body: "", headers: {})
228
333
 
229
- client.track(5, "purchase", :type => "socks", :price => "13.99", :timestamp => 1561231234)
334
+ client.track(5, "purchase", type: "socks", price: "13.99", timestamp: 1561231234)
230
335
  end
231
336
 
232
337
  it "doesn't send timestamp if timestamp is in milliseconds" do
233
338
  stub_request(:post, api_uri('/api/v1/customers/5/events')).
234
- with(:body => {
235
- :name => "purchase",
236
- :data => {
237
- :type => "socks",
238
- :price => "13.99",
239
- :timestamp => "1561231234000"
339
+ with(body: json({
340
+ name: "purchase",
341
+ data: {
342
+ type: "socks",
343
+ price: "13.99",
344
+ timestamp: 1561231234000
240
345
  }
241
- }).
242
- to_return(:status => 200, :body => "", :headers => {})
346
+ })).
347
+ to_return(status: 200, body: "", headers: {})
243
348
 
244
- client.track(5, "purchase", :type => "socks", :price => "13.99", :timestamp => 1561231234000)
349
+ client.track(5, "purchase", type: "socks", price: "13.99", timestamp: 1561231234000)
245
350
  end
246
351
 
247
352
  it "doesn't send timestamp if timestamp is a date" do
248
353
  date = Time.now
249
354
 
250
355
  stub_request(:post, api_uri('/api/v1/customers/5/events')).
251
- with(:body => {
252
- :name => "purchase",
253
- :data => {
254
- :type => "socks",
255
- :price => "13.99",
256
- :timestamp => Time.now.to_s
356
+ with(body: {
357
+ name: "purchase",
358
+ data: {
359
+ type: "socks",
360
+ price: "13.99",
361
+ timestamp: Time.now.to_s
257
362
  }
258
363
  }).
259
- to_return(:status => 200, :body => "", :headers => {})
364
+ to_return(status: 200, body: "", headers: {})
260
365
 
261
- client.track(5, "purchase", :type => "socks", :price => "13.99", :timestamp => date)
366
+ client.track(5, "purchase", type: "socks", price: "13.99", timestamp: date)
262
367
  end
263
368
 
264
369
  it "doesn't send timestamp if timestamp isn't an integer" do
265
370
  stub_request(:post, api_uri('/api/v1/customers/5/events')).
266
- with(:body => {
267
- :name => "purchase",
268
- :data => {
269
- :type => "socks",
270
- :price => "13.99",
271
- :timestamp => "Hello world"
371
+ with(body: json({
372
+ name: "purchase",
373
+ data: {
374
+ type: "socks",
375
+ price: "13.99",
376
+ timestamp: "Hello world"
272
377
  }
273
- }).
378
+ })).
274
379
 
275
- to_return(:status => 200, :body => "", :headers => {})
380
+ to_return(status: 200, body: "", headers: {})
276
381
 
277
- client.track(5, "purchase", :type => "socks", :price => "13.99", :timestamp => "Hello world")
382
+ client.track(5, "purchase", type: "socks", price: "13.99", timestamp: "Hello world")
278
383
  end
279
384
 
280
385
  context "tracking an anonymous event" do
386
+ let(:anon_id) { "anon-id" }
387
+
281
388
  it "sends a POST request to the customer.io's anonymous event API" do
282
389
  stub_request(:post, api_uri('/api/v1/events')).
283
- with(:body => "name=purchase").
284
- to_return(:status => 200, :body => "", :headers => {})
390
+ with(body: { anonymous_id: anon_id, name: "purchase", data: {} }).
391
+ to_return(status: 200, body: "", headers: {})
285
392
 
286
- client.track("purchase")
393
+ client.track_anonymous(anon_id, "purchase")
287
394
  end
288
395
 
289
396
  it "sends any optional event attributes" do
290
397
  stub_request(:post, api_uri('/api/v1/events')).
291
- with(:body => {
292
- :name => "purchase",
293
- :data => {
294
- :type => "socks",
295
- :price => "13.99"
398
+ with(body: {
399
+ anonymous_id: anon_id,
400
+ name: "purchase",
401
+ data: {
402
+ type: "socks",
403
+ price: "13.99"
296
404
  }
297
405
  }).
298
- to_return(:status => 200, :body => "", :headers => {})
406
+ to_return(status: 200, body: "", headers: {})
299
407
 
300
- client.track("purchase", :type => "socks", :price => "13.99")
408
+ client.track_anonymous(anon_id, "purchase", type: "socks", price: "13.99")
301
409
  end
302
410
 
303
411
  it "allows sending of a timestamp" do
304
412
  stub_request(:post, api_uri('/api/v1/events')).
305
- with(:body => {
306
- :name => "purchase",
307
- :data => {
308
- :type => "socks",
309
- :price => "13.99",
310
- :timestamp => "1561231234"
413
+ with(body: {
414
+ anonymous_id: anon_id,
415
+ name: "purchase",
416
+ data: {
417
+ type: "socks",
418
+ price: "13.99",
419
+ timestamp: 1561231234
311
420
  },
312
- :timestamp => "1561231234"
421
+ timestamp: 1561231234
313
422
  }).
314
- to_return(:status => 200, :body => "", :headers => {})
423
+ to_return(status: 200, body: "", headers: {})
315
424
 
316
- client.track("purchase", :type => "socks", :price => "13.99", :timestamp => 1561231234)
425
+ client.track_anonymous(anon_id, "purchase", type: "socks", price: "13.99", timestamp: 1561231234)
317
426
  end
318
- end
319
- end
320
427
 
321
- describe "#anonymous_track" do
322
- it "raises an error if POST doesn't return a 2xx response code" do
323
- stub_request(:post, api_uri('/api/v1/events')).
324
- with(:body => "name=purchase").
325
- to_return(:status => 500, :body => "", :headers => {})
326
-
327
- lambda { client.anonymous_track("purchase") }.should raise_error(Customerio::Client::InvalidResponse)
328
- end
428
+ it "raises an error if POST doesn't return a 2xx response code" do
429
+ stub_request(:post, api_uri('/api/v1/events')).
430
+ with(body: { anonymous_id: anon_id, name: "purchase", data: {} }).
431
+ to_return(status: 500, body: "", headers: {})
329
432
 
330
- it "uses the site_id and api key for basic auth and sends the event name" do
331
- stub_request(:post, api_uri('/api/v1/events')).
332
- with(:body => "name=purchase").
333
- to_return(:status => 200, :body => "", :headers => {})
334
-
335
- client.anonymous_track("purchase")
336
- end
337
-
338
- it "sends any optional event attributes" do
339
- stub_request(:post, api_uri('/api/v1/events')).
340
- with(:body => {
341
- :name => "purchase",
342
- :data => {
343
- :type => "socks",
344
- :price => "27.99"
345
- },
346
- }).
347
-
348
- to_return(:status => 200, :body => "", :headers => {})
349
-
350
- client.anonymous_track("purchase", :type => "socks", :price => "27.99")
351
- end
433
+ lambda { client.track_anonymous(anon_id, "purchase") }.should raise_error(Customerio::InvalidResponse)
434
+ end
352
435
 
353
- it "allows sending of a timestamp" do
354
- stub_request(:post, api_uri('/api/v1/events')).
355
- with(:body => {
356
- :name => "purchase",
357
- :data => {
358
- :type => "socks",
359
- :price => "27.99",
360
- :timestamp => "1561235678"
361
- },
362
- :timestamp => "1561235678"
363
- }).
436
+ it "throws an error when anonymous_id is missing" do
437
+ stub_request(:post, api_uri('/api/v1/events')).
438
+ with(body: { anonymous_id: anon_id, name: "purchase", data: {} }).
439
+ to_return(status: 500, body: "", headers: {})
364
440
 
365
- to_return(:status => 200, :body => "", :headers => {})
441
+ lambda { client.track_anonymous("", "some_event") }.should raise_error(Customerio::Client::ParamError)
442
+ end
366
443
 
367
- client.anonymous_track("purchase", :type => "socks", :price => "27.99", :timestamp => 1561235678)
368
- end
444
+ it "throws an error when event_name is missing" do
445
+ stub_request(:post, api_uri('/api/v1/events')).
446
+ with(body: { anonymous_id: anon_id, name: "purchase", data: {} }).
447
+ to_return(status: 500, body: "", headers: {})
369
448
 
370
- context "too many arguments are passed" do
371
- it "throws an error" do
372
- lambda { client.anonymous_track("purchase", "text", :type => "socks", :price => "27.99") }.should raise_error(ArgumentError)
449
+ lambda { client.track_anonymous(anon_id, "") }.should raise_error(Customerio::Client::ParamError)
373
450
  end
374
451
  end
375
452
  end
@@ -377,109 +454,142 @@ describe Customerio::Client do
377
454
  describe "#devices" do
378
455
  it "allows for the creation of a new device" do
379
456
  stub_request(:put, api_uri('/api/v1/customers/5/devices')).
380
- to_return(:status => 200, :body => "", :headers => {})
457
+ to_return(status: 200, body: "", headers: {})
381
458
 
382
- client.add_device(5, "androidDeviceID", "ios", {:last_used=>1561235678})
459
+ client.add_device(5, "androidDeviceID", "ios", {last_used: 1561235678})
383
460
  client.add_device(5, "iosDeviceID", "android")
384
461
  end
385
462
  it "requires a valid customer_id when creating" do
386
463
  stub_request(:put, api_uri('/api/v1/customers/5/devices')).
387
- to_return(:status => 200, :body => "", :headers => {})
464
+ to_return(status: 200, body: "", headers: {})
388
465
 
389
466
  lambda { client.add_device("", "ios", "myDeviceID") }.should raise_error(Customerio::Client::ParamError)
390
- lambda { client.add_device(nil, "ios", "myDeviceID", {:last_used=>1561235678}) }.should raise_error(Customerio::Client::ParamError)
467
+ lambda { client.add_device(nil, "ios", "myDeviceID", {last_used: 1561235678}) }.should raise_error(Customerio::Client::ParamError)
391
468
  end
392
469
  it "requires a valid token when creating" do
393
470
  stub_request(:put, api_uri('/api/v1/customers/5/devices')).
394
- to_return(:status => 200, :body => "", :headers => {})
471
+ to_return(status: 200, body: "", headers: {})
395
472
 
396
473
  lambda { client.add_device(5, "", "ios") }.should raise_error(Customerio::Client::ParamError)
397
- lambda { client.add_device(5, nil, "ios", {:last_used=>1561235678}) }.should raise_error(Customerio::Client::ParamError)
474
+ lambda { client.add_device(5, nil, "ios", {last_used: 1561235678}) }.should raise_error(Customerio::Client::ParamError)
398
475
  end
399
476
  it "requires a valid platform when creating" do
400
477
  stub_request(:put, api_uri('/api/v1/customers/5/devices')).
401
- to_return(:status => 200, :body => "", :headers => {})
478
+ to_return(status: 200, body: "", headers: {})
402
479
 
403
480
  lambda { client.add_device(5, "token", "") }.should raise_error(Customerio::Client::ParamError)
404
- lambda { client.add_device(5, "toke", nil, {:last_used=>1561235678}) }.should raise_error(Customerio::Client::ParamError)
481
+ lambda { client.add_device(5, "toke", nil, {last_used: 1561235678}) }.should raise_error(Customerio::Client::ParamError)
405
482
  end
406
483
  it "accepts a nil data param" do
407
484
  stub_request(:put, api_uri('/api/v1/customers/5/devices')).
408
- to_return(:status => 200, :body => "", :headers => {})
485
+ to_return(status: 200, body: "", headers: {})
409
486
 
410
487
  client.add_device(5, "ios", "myDeviceID", nil)
411
488
  end
412
489
  it "fails on invalid data param" do
413
490
  stub_request(:put, api_uri('/api/v1/customers/5/devices')).
414
- to_return(:status => 200, :body => "", :headers => {})
491
+ to_return(status: 200, body: "", headers: {})
415
492
 
416
493
  lambda { client.add_device(5, "ios", "myDeviceID", 1000) }.should raise_error(Customerio::Client::ParamError)
417
494
  end
418
495
  it "supports deletion of devices by token" do
419
496
  stub_request(:delete, api_uri('/api/v1/customers/5/devices/myDeviceID')).
420
- to_return(:status => 200, :body => "", :headers => {})
497
+ to_return(status: 200, body: "", headers: {})
421
498
 
422
499
  client.delete_device(5, "myDeviceID")
423
500
  end
424
501
  it "requires a valid customer_id when deleting" do
425
502
  stub_request(:delete, api_uri('/api/v1/customers/5/devices/myDeviceID')).
426
- to_return(:status => 200, :body => "", :headers => {})
503
+ to_return(status: 200, body: "", headers: {})
427
504
 
428
505
  lambda { client.delete_device("", "myDeviceID") }.should raise_error(Customerio::Client::ParamError)
429
506
  lambda { client.delete_device(nil, "myDeviceID") }.should raise_error(Customerio::Client::ParamError)
430
507
  end
431
508
  it "requires a valid device_id when deleting" do
432
509
  stub_request(:delete, api_uri('/api/v1/customers/5/devices/myDeviceID')).
433
- to_return(:status => 200, :body => "", :headers => {})
510
+ to_return(status: 200, body: "", headers: {})
434
511
 
435
512
  lambda { client.delete_device(5, "") }.should raise_error(Customerio::Client::ParamError)
436
513
  lambda { client.delete_device(5, nil) }.should raise_error(Customerio::Client::ParamError)
437
514
  end
438
515
  end
439
516
 
440
- describe "#manual_segments" do
517
+ describe "#track_push_notification_event" do
518
+ attr_accessor :client, :attributes
441
519
 
442
- client = Customerio::Client.new("SITE_ID", "API_KEY", :json=>true)
520
+ before(:each) do
521
+ @client = Customerio::Client.new("SITE_ID", "API_KEY", :json => true)
522
+ @attributes = {
523
+ :delivery_id => 'foo',
524
+ :device_id => 'bar',
525
+ :timestamp => Time.now.to_i
526
+ }
527
+ end
443
528
 
444
- it "allows adding customers to a manual segment" do
445
- stub_request(:post, api_uri('/api/v1/segments/1/add_customers')).to_return(:status => 200, :body => "", :headers => {})
529
+ it "sends a POST request to customer.io's /push/events endpoint" do
530
+ stub_request(:post, api_uri('/push/events')).
531
+ with(
532
+ :body => json(attributes.merge({
533
+ :event => 'opened'
534
+ })),
535
+ :headers => {
536
+ 'Content-Type' => 'application/json'
537
+ }).
538
+ to_return(:status => 200, :body => "", :headers => {})
446
539
 
447
- client.add_to_segment(1, ["customer1", "customer2", "customer3"])
540
+ client.track_push_notification_event('opened', attributes)
448
541
  end
449
- it "requires a valid segment id when adding customers" do
450
- stub_request(:post, api_uri('/api/v1/segments/1/add_customers')).to_return(:status => 200, :body => "", :headers => {})
451
542
 
452
- lambda { client.add_to_segment("not_valid", ["customer1", "customer2", "customer3"]).should raise_error(Customerio::Client::ParamError) }
453
- end
454
- it "requires a valid customer list when adding customers" do
455
- stub_request(:post, api_uri('/api/v1/segments/1/add_customers')).to_return(:status => 200, :body => "", :headers => {})
543
+ it "should raise if event is invalid" do
544
+ stub_request(:post, api_uri('/push/events')).
545
+ to_return(:status => 200, :body => "", :headers => {})
456
546
 
457
- lambda { client.add_to_segment(1, "not_valid").should raise_error(Customerio::Client::ParamError) }
547
+ expect {
548
+ client.track_push_notification_event('closed', attributes.merge({ :delivery_id => nil }))
549
+ }.to raise_error(Customerio::Client::ParamError, 'event_name must be one of opened, converted, or delivered')
458
550
  end
459
- it "coerces non-string values to strings when adding customers" do
460
- stub_request(:post, api_uri('/api/v1/segments/1/add_customers')).with(:body=>json({:ids=>["1", "2", "3"]})).to_return(:status => 200, :body => "", :headers => {})
461
551
 
462
- client.add_to_segment(1, [1, 2, 3])
463
- end
464
- it "allows removing customers from a manual segment" do
465
- stub_request(:post, api_uri('/api/v1/segments/1/remove_customers')).to_return(:status => 200, :body => "", :headers => {})
552
+ it "should raise if delivery_id is invalid" do
553
+ stub_request(:post, api_uri('/push/events')).
554
+ to_return(:status => 200, :body => "", :headers => {})
466
555
 
467
- client.remove_from_segment(1, ["customer1", "customer2", "customer3"])
468
- end
469
- it "requires a valid segment id when removing customers" do
470
- stub_request(:post, api_uri('/api/v1/segments/1/remove_customers')).to_return(:status => 200, :body => "", :headers => {})
556
+ expect {
557
+ client.track_push_notification_event('opened', attributes.merge({ :delivery_id => nil }))
558
+ }.to raise_error(Customerio::Client::ParamError, 'delivery_id must be a non-empty string')
471
559
 
472
- lambda { client.remove_from_segment("not_valid", ["customer1", "customer2", "customer3"]).should raise_error(Customerio::Client::ParamError) }
560
+ expect {
561
+ client.track_push_notification_event('opened', attributes.merge({ :delivery_id => '' }))
562
+ }.to raise_error(Customerio::Client::ParamError, 'delivery_id must be a non-empty string')
473
563
  end
474
- it "requires a valid customer list when removing customers" do
475
- stub_request(:post, api_uri('/api/v1/segments/1/remove_customers')).to_return(:status => 200, :body => "", :headers => {})
476
564
 
477
- lambda { client.remove_from_segment(1, "not_valid").should raise_error(Customerio::Client::ParamError) }
565
+ it "should raise if device_id is invalid" do
566
+ stub_request(:post, api_uri('/push/events')).
567
+ to_return(:status => 200, :body => "", :headers => {})
568
+
569
+ expect {
570
+ client.track_push_notification_event('opened', attributes.merge({ :device_id => nil }))
571
+ }.to raise_error(Customerio::Client::ParamError, 'device_id must be a non-empty string')
572
+
573
+ expect {
574
+ client.track_push_notification_event('opened', attributes.merge({ :device_id => '' }))
575
+ }.to raise_error(Customerio::Client::ParamError, 'device_id must be a non-empty string')
478
576
  end
479
- it "coerces non-string values to strings when removing customers" do
480
- stub_request(:post, api_uri('/api/v1/segments/1/remove_customers')).with(:body=>json({:ids=>["1", "2", "3"]})).to_return(:status => 200, :body => "", :headers => {})
481
577
 
482
- client.remove_from_segment(1, [1, 2, 3])
578
+ it "should raise if timestamp is invalid" do
579
+ stub_request(:post, api_uri('/push/events')).
580
+ to_return(:status => 200, :body => "", :headers => {})
581
+
582
+ expect {
583
+ client.track_push_notification_event('opened', attributes.merge({ :timestamp => nil }))
584
+ }.to raise_error(Customerio::Client::ParamError, 'timestamp must be a valid timestamp')
585
+
586
+ expect {
587
+ client.track_push_notification_event('opened', attributes.merge({ :timestamp => 999999999 }))
588
+ }.to raise_error(Customerio::Client::ParamError, 'timestamp must be a valid timestamp')
589
+
590
+ expect {
591
+ client.track_push_notification_event('opened', attributes.merge({ :timestamp => 100000000000 }))
592
+ }.to raise_error(Customerio::Client::ParamError, 'timestamp must be a valid timestamp')
483
593
  end
484
594
  end
485
595
  end