customerio 2.2.1 → 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.
- checksums.yaml +5 -15
- data/.circleci/config.yml +61 -0
- data/CHANGELOG.markdown +14 -0
- data/README.md +39 -14
- data/customerio.gemspec +2 -2
- data/lib/customerio.rb +3 -0
- data/lib/customerio/api.rb +34 -0
- data/lib/customerio/base_client.rb +87 -0
- data/lib/customerio/client.rb +45 -143
- data/lib/customerio/requests/send_email_request.rb +49 -0
- data/lib/customerio/version.rb +1 -1
- data/spec/api_client_spec.rb +130 -0
- data/spec/base_client_spec.rb +67 -0
- data/spec/client_spec.rb +244 -224
- metadata +39 -33
- data/.travis.yml +0 -9
@@ -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
|
data/lib/customerio/version.rb
CHANGED
@@ -0,0 +1,130 @@
|
|
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
|
+
describe "#send_email" do
|
25
|
+
it "sends a POST request to the /api/send/email path" do
|
26
|
+
req = Customerio::SendEmailRequest.new(
|
27
|
+
identifiers: {
|
28
|
+
id: 'c1',
|
29
|
+
},
|
30
|
+
transactional_message_id: 1,
|
31
|
+
)
|
32
|
+
|
33
|
+
stub_request(:post, api_uri('/v1/send/email'))
|
34
|
+
.with(headers: request_headers, body: req.message)
|
35
|
+
.to_return(status: 200, body: { delivery_id: 1 }.to_json, headers: {})
|
36
|
+
|
37
|
+
client.send_email(req).should eq({ "delivery_id" => 1 })
|
38
|
+
end
|
39
|
+
|
40
|
+
it "handles validation failures (400)" do
|
41
|
+
req = Customerio::SendEmailRequest.new(
|
42
|
+
identifiers: {
|
43
|
+
id: 'c1',
|
44
|
+
},
|
45
|
+
transactional_message_id: 1,
|
46
|
+
)
|
47
|
+
|
48
|
+
err_json = { meta: { error: "example error" } }.to_json
|
49
|
+
|
50
|
+
stub_request(:post, api_uri('/v1/send/email'))
|
51
|
+
.with(headers: request_headers, body: req.message)
|
52
|
+
.to_return(status: 400, body: err_json, headers: {})
|
53
|
+
|
54
|
+
lambda { client.send_email(req) }.should(
|
55
|
+
raise_error(Customerio::InvalidResponse) { |error|
|
56
|
+
error.message.should eq "example error"
|
57
|
+
error.code.should eq "400"
|
58
|
+
}
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "handles other failures (5xx)" do
|
63
|
+
req = Customerio::SendEmailRequest.new(
|
64
|
+
identifiers: {
|
65
|
+
id: 'c1',
|
66
|
+
},
|
67
|
+
transactional_message_id: 1,
|
68
|
+
)
|
69
|
+
|
70
|
+
stub_request(:post, api_uri('/v1/send/email'))
|
71
|
+
.with(headers: request_headers, body: req.message)
|
72
|
+
.to_return(status: 500, body: "Server unavailable", headers: {})
|
73
|
+
|
74
|
+
lambda { client.send_email(req) }.should(
|
75
|
+
raise_error(Customerio::InvalidResponse) { |error|
|
76
|
+
error.message.should eq "Server unavailable"
|
77
|
+
error.code.should eq "500"
|
78
|
+
}
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
it "allows attaching file content without encoding" do
|
83
|
+
content = 'sample content'
|
84
|
+
|
85
|
+
req = Customerio::SendEmailRequest.new(
|
86
|
+
customer_id: 'c1',
|
87
|
+
transactional_message_id: 1,
|
88
|
+
)
|
89
|
+
|
90
|
+
req.attach('test', content, encode: false)
|
91
|
+
req.message[:attachments]['test'].should eq content
|
92
|
+
|
93
|
+
stub_request(:post, api_uri('/v1/send/email'))
|
94
|
+
.with(headers: request_headers, body: req.message)
|
95
|
+
.to_return(status: 200, body: { delivery_id: 1 }.to_json, headers: {})
|
96
|
+
|
97
|
+
client.send_email(req)
|
98
|
+
end
|
99
|
+
|
100
|
+
it "allows attaching files with encoding (default)" do
|
101
|
+
content = 'sample content'
|
102
|
+
|
103
|
+
req = Customerio::SendEmailRequest.new(
|
104
|
+
customer_id: 'c1',
|
105
|
+
transactional_message_id: 1,
|
106
|
+
)
|
107
|
+
|
108
|
+
req.attach('test', content)
|
109
|
+
req.message[:attachments]['test'].should eq Base64.strict_encode64(content)
|
110
|
+
|
111
|
+
stub_request(:post, api_uri('/v1/send/email'))
|
112
|
+
.with(headers: request_headers, body: req.message)
|
113
|
+
.to_return(status: 200, body: { delivery_id: 1 }.to_json, headers: {})
|
114
|
+
|
115
|
+
client.send_email(req)
|
116
|
+
end
|
117
|
+
|
118
|
+
it "raises error when attaching the same key again" do
|
119
|
+
req = Customerio::SendEmailRequest.new(
|
120
|
+
customer_id: 'c1',
|
121
|
+
transactional_message_id: 1,
|
122
|
+
)
|
123
|
+
|
124
|
+
req.attach('test', 'test-content')
|
125
|
+
|
126
|
+
lambda { req.attach('test', '') }.should raise_error(/attachment test already exists/)
|
127
|
+
req.message[:attachments].should eq({ "test" => Base64.strict_encode64("test-content") })
|
128
|
+
end
|
129
|
+
end
|
130
|
+
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,48 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'multi_json'
|
3
|
-
|
3
|
+
require 'base64'
|
4
4
|
|
5
5
|
describe Customerio::Client do
|
6
|
-
let(:
|
7
|
-
let(:
|
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://
|
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
|
-
|
18
|
-
|
25
|
+
it "uses json by default" do
|
26
|
+
body = { id: 5, name: "Bob" }
|
27
|
+
client = Customerio::Client.new("SITE_ID", "API_KEY")
|
19
28
|
|
20
|
-
|
21
|
-
|
29
|
+
stub_request(:put, api_uri('/api/v1/customers/5')).
|
30
|
+
with(body: json(body),
|
31
|
+
headers: {'Content-Type'=>'application/json'}).
|
32
|
+
to_return(status: 200, body: "", headers: {})
|
22
33
|
|
23
|
-
|
24
|
-
|
25
|
-
:headers => {'Content-Type'=>'application/json'}).
|
26
|
-
to_return(:status => 200, :body => "", :headers => {})
|
34
|
+
client.identify(body)
|
35
|
+
end
|
27
36
|
|
28
|
-
|
29
|
-
|
37
|
+
describe "headers" do
|
38
|
+
let(:body) { { id: 1, token: :test } }
|
30
39
|
|
31
|
-
it "
|
32
|
-
client = Customerio::Client.new("SITE_ID", "API_KEY"
|
40
|
+
it "sends the basic headers, base64 encoded with the request" do
|
41
|
+
client = Customerio::Client.new("SITE_ID", "API_KEY")
|
33
42
|
|
34
|
-
stub_request(:put, api_uri('/api/v1/customers/
|
35
|
-
with(
|
36
|
-
to_return(:
|
43
|
+
stub_request(:put, api_uri('/api/v1/customers/1')).
|
44
|
+
with(body: json(body), headers: request_headers).
|
45
|
+
to_return(status: 200, body: "", headers: {})
|
37
46
|
|
38
47
|
client.identify(body)
|
39
48
|
end
|
@@ -42,41 +51,54 @@ describe Customerio::Client do
|
|
42
51
|
describe "#identify" do
|
43
52
|
it "sends a PUT request to customer.io's customer API" do
|
44
53
|
stub_request(:put, api_uri('/api/v1/customers/5')).
|
45
|
-
with(:
|
46
|
-
to_return(:
|
54
|
+
with(body: json(id: "5")).
|
55
|
+
to_return(status: 200, body: "", headers: {})
|
56
|
+
|
57
|
+
client.identify(id: "5")
|
58
|
+
end
|
59
|
+
|
60
|
+
it "escapes customer IDs" do
|
61
|
+
stub_request(:put, api_uri('/api/v1/customers/5%20')).
|
62
|
+
with(body: json({ id: "5 " })).
|
63
|
+
to_return(status: 200, body: "", headers: {})
|
64
|
+
|
65
|
+
client.identify(id: "5 ")
|
47
66
|
|
48
|
-
|
67
|
+
stub_request(:put, api_uri('/api/v1/customers/5%2F')).
|
68
|
+
with(body: { id: "5/" }).
|
69
|
+
to_return(status: 200, body: "", headers: {})
|
70
|
+
client.identify(id: "5/")
|
49
71
|
end
|
50
72
|
|
51
73
|
it "sends a PUT request to customer.io's customer API using json headers" do
|
52
|
-
client = Customerio::Client.new("SITE_ID", "API_KEY", :
|
53
|
-
body = { :
|
74
|
+
client = Customerio::Client.new("SITE_ID", "API_KEY", json: true)
|
75
|
+
body = { id: 5, name: "Bob" }
|
54
76
|
|
55
77
|
stub_request(:put, api_uri('/api/v1/customers/5')).
|
56
|
-
with(:
|
57
|
-
:
|
58
|
-
to_return(:
|
78
|
+
with(body: json(body),
|
79
|
+
headers: {'Content-Type'=>'application/json'}).
|
80
|
+
to_return(status: 200, body: "", headers: {})
|
59
81
|
|
60
82
|
client.identify(body)
|
61
83
|
end
|
62
84
|
|
63
85
|
it "raises an error if PUT doesn't return a 2xx response code" do
|
64
86
|
stub_request(:put, api_uri('/api/v1/customers/5')).
|
65
|
-
with(:
|
66
|
-
to_return(:
|
87
|
+
with(body: json(id: 5)).
|
88
|
+
to_return(status: 500, body: "", headers: {})
|
67
89
|
|
68
|
-
lambda { client.identify(:
|
90
|
+
lambda { client.identify(id: 5) }.should raise_error(Customerio::InvalidResponse)
|
69
91
|
end
|
70
92
|
|
71
93
|
it "includes the HTTP response with raised errors" do
|
72
94
|
stub_request(:put, api_uri('/api/v1/customers/5')).
|
73
|
-
with(:
|
74
|
-
to_return(:
|
95
|
+
with(body: json(id: 5)).
|
96
|
+
to_return(status: 500, body: "Server unavailable", headers: {})
|
75
97
|
|
76
|
-
lambda { client.identify(:
|
77
|
-
error.should be_a Customerio::
|
78
|
-
error.
|
79
|
-
error.
|
98
|
+
lambda { client.identify(id: 5) }.should raise_error {|error|
|
99
|
+
error.should be_a Customerio::InvalidResponse
|
100
|
+
error.code.should eq "500"
|
101
|
+
error.message.should eq "Server unavailable"
|
80
102
|
}
|
81
103
|
end
|
82
104
|
|
@@ -84,31 +106,32 @@ describe Customerio::Client do
|
|
84
106
|
time = Time.now.to_i
|
85
107
|
|
86
108
|
stub_request(:put, api_uri('/api/v1/customers/5')).with(
|
87
|
-
:
|
88
|
-
:
|
89
|
-
:
|
90
|
-
:
|
91
|
-
:
|
92
|
-
:
|
93
|
-
}).to_return(:
|
109
|
+
body: json({
|
110
|
+
id: 5,
|
111
|
+
email: "customer@example.com",
|
112
|
+
created_at: time,
|
113
|
+
first_name: "Bob",
|
114
|
+
plan: "basic"
|
115
|
+
})).to_return(status: 200, body: "", headers: {})
|
94
116
|
|
95
117
|
client.identify({
|
96
|
-
:
|
97
|
-
:
|
98
|
-
:
|
99
|
-
:
|
100
|
-
:
|
118
|
+
id: 5,
|
119
|
+
email: "customer@example.com",
|
120
|
+
created_at: time,
|
121
|
+
first_name: "Bob",
|
122
|
+
plan: "basic"
|
101
123
|
})
|
102
124
|
end
|
103
125
|
|
104
126
|
it "requires an id attribute" do
|
105
|
-
lambda { client.identify(:
|
127
|
+
lambda { client.identify(email: "customer@example.com") }.should raise_error(Customerio::Client::MissingIdAttributeError)
|
128
|
+
lambda { client.identify(id: "") }.should raise_error(Customerio::Client::MissingIdAttributeError)
|
106
129
|
end
|
107
130
|
|
108
131
|
it 'should not raise errors when attribute keys are strings' do
|
109
132
|
stub_request(:put, api_uri('/api/v1/customers/5')).
|
110
|
-
with(:
|
111
|
-
to_return(:
|
133
|
+
with(body: json(id: 5)).
|
134
|
+
to_return(status: 200, body: "", headers: {})
|
112
135
|
|
113
136
|
attributes = { "id" => 5 }
|
114
137
|
|
@@ -119,201 +142,237 @@ describe Customerio::Client do
|
|
119
142
|
describe "#delete" do
|
120
143
|
it "sends a DELETE request to the customer.io's event API" do
|
121
144
|
stub_request(:delete, api_uri('/api/v1/customers/5')).
|
122
|
-
to_return(:
|
145
|
+
to_return(status: 200, body: "", headers: {})
|
123
146
|
|
124
147
|
client.delete(5)
|
125
148
|
end
|
149
|
+
|
150
|
+
it "throws an error when customer_id is missing" do
|
151
|
+
stub_request(:put, /track.customer.io/)
|
152
|
+
.to_return(status: 200, body: "", headers: {})
|
153
|
+
|
154
|
+
lambda { client.delete(" ") }.should raise_error(Customerio::Client::ParamError, "customer_id must be a non-empty string")
|
155
|
+
end
|
156
|
+
|
157
|
+
it "escapes customer IDs" do
|
158
|
+
stub_request(:delete, api_uri('/api/v1/customers/5%20')).
|
159
|
+
to_return(status: 200, body: "", headers: {})
|
160
|
+
|
161
|
+
client.delete("5 ")
|
162
|
+
end
|
126
163
|
end
|
127
164
|
|
128
165
|
describe "#suppress" do
|
129
166
|
it "sends a POST request to the customer.io's suppress API" do
|
130
167
|
stub_request(:post, api_uri('/api/v1/customers/5/suppress')).
|
131
|
-
to_return(:
|
168
|
+
to_return(status: 200, body: "", headers: {})
|
132
169
|
|
133
170
|
client.suppress(5)
|
134
171
|
end
|
172
|
+
|
173
|
+
it "throws an error when customer_id is missing" do
|
174
|
+
stub_request(:put, /track.customer.io/)
|
175
|
+
.to_return(status: 200, body: "", headers: {})
|
176
|
+
|
177
|
+
lambda { client.suppress(" ") }.should raise_error(Customerio::Client::ParamError, "customer_id must be a non-empty string")
|
178
|
+
end
|
135
179
|
end
|
136
180
|
|
137
181
|
describe "#unsuppress" do
|
138
182
|
it "sends a POST request to the customer.io's unsuppress API" do
|
139
183
|
stub_request(:post, api_uri('/api/v1/customers/5/unsuppress')).
|
140
|
-
to_return(:
|
184
|
+
to_return(status: 200, body: "", headers: {})
|
141
185
|
|
142
186
|
client.unsuppress(5)
|
143
187
|
end
|
188
|
+
|
189
|
+
it "throws an error when customer_id is missing" do
|
190
|
+
stub_request(:put, /track.customer.io/)
|
191
|
+
.to_return(status: 200, body: "", headers: {})
|
192
|
+
|
193
|
+
lambda { client.suppress(" ") }.should raise_error(Customerio::Client::ParamError, "customer_id must be a non-empty string")
|
194
|
+
end
|
144
195
|
end
|
145
196
|
|
146
197
|
describe "#track" do
|
147
198
|
it "raises an error if POST doesn't return a 2xx response code" do
|
148
199
|
stub_request(:post, api_uri('/api/v1/customers/5/events')).
|
149
|
-
with(:
|
150
|
-
to_return(:
|
200
|
+
with(body: json(name: "purchase", data: {})).
|
201
|
+
to_return(status: 500, body: "", headers: {})
|
202
|
+
|
203
|
+
lambda { client.track(5, "purchase") }.should raise_error(Customerio::InvalidResponse)
|
204
|
+
end
|
205
|
+
|
206
|
+
it "throws an error when customer_id or event_name is missing" do
|
207
|
+
stub_request(:put, /track.customer.io/)
|
208
|
+
.to_return(status: 200, body: "", headers: {})
|
151
209
|
|
152
|
-
lambda { client.track(
|
210
|
+
lambda { client.track(" ", "test_event") }.should raise_error(Customerio::Client::ParamError, "customer_id must be a non-empty string")
|
211
|
+
lambda { client.track(5, " ") }.should raise_error(Customerio::Client::ParamError, "event_name must be a non-empty string")
|
153
212
|
end
|
154
213
|
|
155
214
|
it "uses the site_id and api key for basic auth and sends the event name" do
|
156
215
|
stub_request(:post, api_uri('/api/v1/customers/5/events')).
|
157
|
-
with(:
|
158
|
-
to_return(:
|
216
|
+
with(body: json(name: "purchase", data: {})).
|
217
|
+
to_return(status: 200, body: "", headers: {})
|
159
218
|
|
160
219
|
client.track(5, "purchase")
|
161
220
|
end
|
162
221
|
|
163
222
|
it "sends any optional event attributes" do
|
164
223
|
stub_request(:post, api_uri('/api/v1/customers/5/events')).
|
165
|
-
with(:
|
166
|
-
:
|
167
|
-
:
|
168
|
-
:
|
169
|
-
:
|
224
|
+
with(body: json({
|
225
|
+
name: "purchase",
|
226
|
+
data: {
|
227
|
+
type: "socks",
|
228
|
+
price: "13.99"
|
170
229
|
}
|
171
|
-
}).
|
172
|
-
to_return(:
|
230
|
+
})).
|
231
|
+
to_return(status: 200, body: "", headers: {})
|
173
232
|
|
174
|
-
client.track(5, "purchase", :
|
233
|
+
client.track(5, "purchase", type: "socks", price: "13.99")
|
175
234
|
end
|
176
235
|
|
177
236
|
it "copes with arrays" do
|
178
237
|
stub_request(:post, api_uri('/api/v1/customers/5/events')).
|
179
|
-
with(:
|
180
|
-
:
|
181
|
-
:
|
182
|
-
:
|
238
|
+
with(body: {
|
239
|
+
name: "event",
|
240
|
+
data: {
|
241
|
+
things: ["a", "b", "c"]
|
183
242
|
}
|
184
243
|
}).
|
185
|
-
to_return(:
|
244
|
+
to_return(status: 200, body: "", headers: {})
|
186
245
|
|
187
|
-
client.track(5, "event", :
|
246
|
+
client.track(5, "event", things: ["a", "b", "c"])
|
188
247
|
end
|
189
248
|
|
190
249
|
it "copes with hashes" do
|
191
250
|
stub_request(:post, api_uri('/api/v1/customers/5/events')).
|
192
|
-
with(:
|
193
|
-
:
|
194
|
-
:
|
195
|
-
:
|
251
|
+
with(body: {
|
252
|
+
name: "event",
|
253
|
+
data: {
|
254
|
+
stuff: { a: "b" }
|
196
255
|
}
|
197
256
|
}).
|
198
|
-
to_return(:
|
257
|
+
to_return(status: 200, body: "", headers: {})
|
199
258
|
|
200
|
-
client.track(5, "event", :
|
259
|
+
client.track(5, "event", stuff: { a: "b" })
|
201
260
|
end
|
202
261
|
|
203
262
|
it "sends a POST request as json using json headers" do
|
204
|
-
client = Customerio::Client.new("SITE_ID", "API_KEY", :
|
205
|
-
data = { :
|
206
|
-
body = { :
|
263
|
+
client = Customerio::Client.new("SITE_ID", "API_KEY", json: true)
|
264
|
+
data = { type: "socks", price: "13.99" }
|
265
|
+
body = { name: "purchase", data: data }
|
207
266
|
|
208
267
|
stub_request(:post, api_uri('/api/v1/customers/5/events')).
|
209
|
-
with(:
|
210
|
-
:
|
211
|
-
to_return(:
|
268
|
+
with(body: json(body),
|
269
|
+
headers: {'Content-Type'=>'application/json'}).
|
270
|
+
to_return(status: 200, body: "", headers: {})
|
212
271
|
|
213
272
|
client.track(5, "purchase", data)
|
214
273
|
end
|
215
274
|
|
216
275
|
it "allows sending of a timestamp" do
|
217
276
|
stub_request(:post, api_uri('/api/v1/customers/5/events')).
|
218
|
-
with(:
|
219
|
-
:
|
220
|
-
:
|
221
|
-
:
|
222
|
-
:
|
223
|
-
:
|
277
|
+
with(body: json({
|
278
|
+
name: "purchase",
|
279
|
+
data: {
|
280
|
+
type: "socks",
|
281
|
+
price: "13.99",
|
282
|
+
timestamp: 1561231234
|
224
283
|
},
|
225
|
-
:
|
226
|
-
}).
|
227
|
-
to_return(:
|
284
|
+
timestamp: 1561231234
|
285
|
+
})).
|
286
|
+
to_return(status: 200, body: "", headers: {})
|
228
287
|
|
229
|
-
client.track(5, "purchase", :
|
288
|
+
client.track(5, "purchase", type: "socks", price: "13.99", timestamp: 1561231234)
|
230
289
|
end
|
231
290
|
|
232
291
|
it "doesn't send timestamp if timestamp is in milliseconds" do
|
233
292
|
stub_request(:post, api_uri('/api/v1/customers/5/events')).
|
234
|
-
with(:
|
235
|
-
:
|
236
|
-
:
|
237
|
-
:
|
238
|
-
:
|
239
|
-
:
|
293
|
+
with(body: json({
|
294
|
+
name: "purchase",
|
295
|
+
data: {
|
296
|
+
type: "socks",
|
297
|
+
price: "13.99",
|
298
|
+
timestamp: 1561231234000
|
240
299
|
}
|
241
|
-
}).
|
242
|
-
to_return(:
|
300
|
+
})).
|
301
|
+
to_return(status: 200, body: "", headers: {})
|
243
302
|
|
244
|
-
client.track(5, "purchase", :
|
303
|
+
client.track(5, "purchase", type: "socks", price: "13.99", timestamp: 1561231234000)
|
245
304
|
end
|
246
305
|
|
247
306
|
it "doesn't send timestamp if timestamp is a date" do
|
248
307
|
date = Time.now
|
249
308
|
|
250
309
|
stub_request(:post, api_uri('/api/v1/customers/5/events')).
|
251
|
-
with(:
|
252
|
-
:
|
253
|
-
:
|
254
|
-
:
|
255
|
-
:
|
256
|
-
:
|
310
|
+
with(body: {
|
311
|
+
name: "purchase",
|
312
|
+
data: {
|
313
|
+
type: "socks",
|
314
|
+
price: "13.99",
|
315
|
+
timestamp: Time.now.to_s
|
257
316
|
}
|
258
317
|
}).
|
259
|
-
to_return(:
|
318
|
+
to_return(status: 200, body: "", headers: {})
|
260
319
|
|
261
|
-
client.track(5, "purchase", :
|
320
|
+
client.track(5, "purchase", type: "socks", price: "13.99", timestamp: date)
|
262
321
|
end
|
263
322
|
|
264
323
|
it "doesn't send timestamp if timestamp isn't an integer" do
|
265
324
|
stub_request(:post, api_uri('/api/v1/customers/5/events')).
|
266
|
-
with(:
|
267
|
-
:
|
268
|
-
:
|
269
|
-
:
|
270
|
-
:
|
271
|
-
:
|
325
|
+
with(body: json({
|
326
|
+
name: "purchase",
|
327
|
+
data: {
|
328
|
+
type: "socks",
|
329
|
+
price: "13.99",
|
330
|
+
timestamp: "Hello world"
|
272
331
|
}
|
273
|
-
}).
|
332
|
+
})).
|
274
333
|
|
275
|
-
to_return(:
|
334
|
+
to_return(status: 200, body: "", headers: {})
|
276
335
|
|
277
|
-
client.track(5, "purchase", :
|
336
|
+
client.track(5, "purchase", type: "socks", price: "13.99", timestamp: "Hello world")
|
278
337
|
end
|
279
338
|
|
280
339
|
context "tracking an anonymous event" do
|
281
340
|
it "sends a POST request to the customer.io's anonymous event API" do
|
282
341
|
stub_request(:post, api_uri('/api/v1/events')).
|
283
|
-
with(:
|
284
|
-
to_return(:
|
342
|
+
with(body: json({ name: "purchase", data: {} })).
|
343
|
+
to_return(status: 200, body: "", headers: {})
|
285
344
|
|
286
|
-
client.
|
345
|
+
client.anonymous_track("purchase")
|
287
346
|
end
|
288
347
|
|
289
348
|
it "sends any optional event attributes" do
|
290
349
|
stub_request(:post, api_uri('/api/v1/events')).
|
291
|
-
with(:
|
292
|
-
:
|
293
|
-
:
|
294
|
-
:
|
295
|
-
:
|
350
|
+
with(body: json({
|
351
|
+
name: "purchase",
|
352
|
+
data: {
|
353
|
+
type: "socks",
|
354
|
+
price: "13.99"
|
296
355
|
}
|
297
|
-
}).
|
298
|
-
to_return(:
|
356
|
+
})).
|
357
|
+
to_return(status: 200, body: "", headers: {})
|
299
358
|
|
300
|
-
client.
|
359
|
+
client.anonymous_track("purchase", type: "socks", price: "13.99")
|
301
360
|
end
|
302
361
|
|
303
362
|
it "allows sending of a timestamp" do
|
304
363
|
stub_request(:post, api_uri('/api/v1/events')).
|
305
|
-
with(:
|
306
|
-
:
|
307
|
-
:
|
308
|
-
:
|
309
|
-
:
|
310
|
-
:
|
364
|
+
with(body: json({
|
365
|
+
name: "purchase",
|
366
|
+
data: {
|
367
|
+
type: "socks",
|
368
|
+
price: "13.99",
|
369
|
+
timestamp: 1561231234
|
311
370
|
},
|
312
|
-
:
|
313
|
-
}).
|
314
|
-
to_return(:
|
371
|
+
timestamp: 1561231234
|
372
|
+
})).
|
373
|
+
to_return(status: 200, body: "", headers: {})
|
315
374
|
|
316
|
-
client.
|
375
|
+
client.anonymous_track("purchase", type: "socks", price: "13.99", timestamp: 1561231234)
|
317
376
|
end
|
318
377
|
end
|
319
378
|
end
|
@@ -321,55 +380,62 @@ describe Customerio::Client do
|
|
321
380
|
describe "#anonymous_track" do
|
322
381
|
it "raises an error if POST doesn't return a 2xx response code" do
|
323
382
|
stub_request(:post, api_uri('/api/v1/events')).
|
324
|
-
with(:
|
325
|
-
to_return(:
|
383
|
+
with(body: json(name: "purchase", data: {})).
|
384
|
+
to_return(status: 500, body: "", headers: {})
|
326
385
|
|
327
|
-
lambda { client.anonymous_track("purchase") }.should raise_error(Customerio::
|
386
|
+
lambda { client.anonymous_track("purchase") }.should raise_error(Customerio::InvalidResponse)
|
387
|
+
end
|
388
|
+
|
389
|
+
it "throws an error when event_name is missing" do
|
390
|
+
stub_request(:put, /track.customer.io/)
|
391
|
+
.to_return(status: 200, body: "", headers: {})
|
392
|
+
|
393
|
+
lambda { client.anonymous_track(" ") }.should raise_error(Customerio::Client::ParamError, "event_name must be a non-empty string")
|
328
394
|
end
|
329
395
|
|
330
396
|
it "uses the site_id and api key for basic auth and sends the event name" do
|
331
397
|
stub_request(:post, api_uri('/api/v1/events')).
|
332
|
-
with(:
|
333
|
-
to_return(:
|
398
|
+
with(body: json(name: "purchase", data: {})).
|
399
|
+
to_return(status: 200, body: "", headers: {})
|
334
400
|
|
335
401
|
client.anonymous_track("purchase")
|
336
402
|
end
|
337
403
|
|
338
404
|
it "sends any optional event attributes" do
|
339
405
|
stub_request(:post, api_uri('/api/v1/events')).
|
340
|
-
with(:
|
341
|
-
:
|
342
|
-
:
|
343
|
-
:
|
344
|
-
:
|
406
|
+
with(body: {
|
407
|
+
name: "purchase",
|
408
|
+
data: {
|
409
|
+
type: "socks",
|
410
|
+
price: "27.99"
|
345
411
|
},
|
346
412
|
}).
|
347
413
|
|
348
|
-
to_return(:
|
414
|
+
to_return(status: 200, body: "", headers: {})
|
349
415
|
|
350
|
-
client.anonymous_track("purchase", :
|
416
|
+
client.anonymous_track("purchase", type: "socks", price: "27.99")
|
351
417
|
end
|
352
418
|
|
353
419
|
it "allows sending of a timestamp" do
|
354
420
|
stub_request(:post, api_uri('/api/v1/events')).
|
355
|
-
with(:
|
356
|
-
:
|
357
|
-
:
|
358
|
-
:
|
359
|
-
:
|
360
|
-
:
|
421
|
+
with(body: json({
|
422
|
+
name: "purchase",
|
423
|
+
data: {
|
424
|
+
type: "socks",
|
425
|
+
price: "27.99",
|
426
|
+
timestamp: 1561235678
|
361
427
|
},
|
362
|
-
:
|
363
|
-
}).
|
428
|
+
timestamp: 1561235678
|
429
|
+
})).
|
364
430
|
|
365
|
-
to_return(:
|
431
|
+
to_return(status: 200, body: "", headers: {})
|
366
432
|
|
367
|
-
client.anonymous_track("purchase", :
|
433
|
+
client.anonymous_track("purchase", type: "socks", price: "27.99", timestamp: 1561235678)
|
368
434
|
end
|
369
435
|
|
370
436
|
context "too many arguments are passed" do
|
371
437
|
it "throws an error" do
|
372
|
-
lambda { client.anonymous_track("purchase", "text", :
|
438
|
+
lambda { client.anonymous_track("purchase", "text", type: "socks", price: "27.99") }.should raise_error(ArgumentError)
|
373
439
|
end
|
374
440
|
end
|
375
441
|
end
|
@@ -377,109 +443,63 @@ describe Customerio::Client do
|
|
377
443
|
describe "#devices" do
|
378
444
|
it "allows for the creation of a new device" do
|
379
445
|
stub_request(:put, api_uri('/api/v1/customers/5/devices')).
|
380
|
-
to_return(:
|
446
|
+
to_return(status: 200, body: "", headers: {})
|
381
447
|
|
382
|
-
client.add_device(5, "androidDeviceID", "ios", {:
|
448
|
+
client.add_device(5, "androidDeviceID", "ios", {last_used: 1561235678})
|
383
449
|
client.add_device(5, "iosDeviceID", "android")
|
384
450
|
end
|
385
451
|
it "requires a valid customer_id when creating" do
|
386
452
|
stub_request(:put, api_uri('/api/v1/customers/5/devices')).
|
387
|
-
to_return(:
|
453
|
+
to_return(status: 200, body: "", headers: {})
|
388
454
|
|
389
455
|
lambda { client.add_device("", "ios", "myDeviceID") }.should raise_error(Customerio::Client::ParamError)
|
390
|
-
lambda { client.add_device(nil, "ios", "myDeviceID", {:
|
456
|
+
lambda { client.add_device(nil, "ios", "myDeviceID", {last_used: 1561235678}) }.should raise_error(Customerio::Client::ParamError)
|
391
457
|
end
|
392
458
|
it "requires a valid token when creating" do
|
393
459
|
stub_request(:put, api_uri('/api/v1/customers/5/devices')).
|
394
|
-
to_return(:
|
460
|
+
to_return(status: 200, body: "", headers: {})
|
395
461
|
|
396
462
|
lambda { client.add_device(5, "", "ios") }.should raise_error(Customerio::Client::ParamError)
|
397
|
-
lambda { client.add_device(5, nil, "ios", {:
|
463
|
+
lambda { client.add_device(5, nil, "ios", {last_used: 1561235678}) }.should raise_error(Customerio::Client::ParamError)
|
398
464
|
end
|
399
465
|
it "requires a valid platform when creating" do
|
400
466
|
stub_request(:put, api_uri('/api/v1/customers/5/devices')).
|
401
|
-
to_return(:
|
467
|
+
to_return(status: 200, body: "", headers: {})
|
402
468
|
|
403
469
|
lambda { client.add_device(5, "token", "") }.should raise_error(Customerio::Client::ParamError)
|
404
|
-
lambda { client.add_device(5, "toke", nil, {:
|
470
|
+
lambda { client.add_device(5, "toke", nil, {last_used: 1561235678}) }.should raise_error(Customerio::Client::ParamError)
|
405
471
|
end
|
406
472
|
it "accepts a nil data param" do
|
407
473
|
stub_request(:put, api_uri('/api/v1/customers/5/devices')).
|
408
|
-
to_return(:
|
474
|
+
to_return(status: 200, body: "", headers: {})
|
409
475
|
|
410
476
|
client.add_device(5, "ios", "myDeviceID", nil)
|
411
477
|
end
|
412
478
|
it "fails on invalid data param" do
|
413
479
|
stub_request(:put, api_uri('/api/v1/customers/5/devices')).
|
414
|
-
to_return(:
|
480
|
+
to_return(status: 200, body: "", headers: {})
|
415
481
|
|
416
482
|
lambda { client.add_device(5, "ios", "myDeviceID", 1000) }.should raise_error(Customerio::Client::ParamError)
|
417
483
|
end
|
418
484
|
it "supports deletion of devices by token" do
|
419
485
|
stub_request(:delete, api_uri('/api/v1/customers/5/devices/myDeviceID')).
|
420
|
-
to_return(:
|
486
|
+
to_return(status: 200, body: "", headers: {})
|
421
487
|
|
422
488
|
client.delete_device(5, "myDeviceID")
|
423
489
|
end
|
424
490
|
it "requires a valid customer_id when deleting" do
|
425
491
|
stub_request(:delete, api_uri('/api/v1/customers/5/devices/myDeviceID')).
|
426
|
-
to_return(:
|
492
|
+
to_return(status: 200, body: "", headers: {})
|
427
493
|
|
428
494
|
lambda { client.delete_device("", "myDeviceID") }.should raise_error(Customerio::Client::ParamError)
|
429
495
|
lambda { client.delete_device(nil, "myDeviceID") }.should raise_error(Customerio::Client::ParamError)
|
430
496
|
end
|
431
497
|
it "requires a valid device_id when deleting" do
|
432
498
|
stub_request(:delete, api_uri('/api/v1/customers/5/devices/myDeviceID')).
|
433
|
-
to_return(:
|
499
|
+
to_return(status: 200, body: "", headers: {})
|
434
500
|
|
435
501
|
lambda { client.delete_device(5, "") }.should raise_error(Customerio::Client::ParamError)
|
436
502
|
lambda { client.delete_device(5, nil) }.should raise_error(Customerio::Client::ParamError)
|
437
503
|
end
|
438
504
|
end
|
439
|
-
|
440
|
-
describe "#manual_segments" do
|
441
|
-
|
442
|
-
client = Customerio::Client.new("SITE_ID", "API_KEY", :json=>true)
|
443
|
-
|
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 => {})
|
446
|
-
|
447
|
-
client.add_to_segment(1, ["customer1", "customer2", "customer3"])
|
448
|
-
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
|
-
|
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 => {})
|
456
|
-
|
457
|
-
lambda { client.add_to_segment(1, "not_valid").should raise_error(Customerio::Client::ParamError) }
|
458
|
-
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
|
-
|
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 => {})
|
466
|
-
|
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 => {})
|
471
|
-
|
472
|
-
lambda { client.remove_from_segment("not_valid", ["customer1", "customer2", "customer3"]).should raise_error(Customerio::Client::ParamError) }
|
473
|
-
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
|
-
|
477
|
-
lambda { client.remove_from_segment(1, "not_valid").should raise_error(Customerio::Client::ParamError) }
|
478
|
-
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
|
-
|
482
|
-
client.remove_from_segment(1, [1, 2, 3])
|
483
|
-
end
|
484
|
-
end
|
485
505
|
end
|