customerio 2.0.0 → 3.1.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 -13
- data/.circleci/config.yml +61 -0
- data/.github/workflows/main.yml +30 -0
- data/CHANGELOG.markdown +100 -49
- data/README.md +67 -9
- data/customerio.gemspec +3 -2
- data/lib/customerio.rb +4 -0
- data/lib/customerio/api.rb +35 -0
- data/lib/customerio/base_client.rb +87 -0
- data/lib/customerio/client.rb +55 -106
- data/lib/customerio/regions.rb +11 -0
- data/lib/customerio/requests/send_email_request.rb +49 -0
- data/lib/customerio/version.rb +1 -1
- data/spec/api_client_spec.rb +172 -0
- data/spec/base_client_spec.rb +67 -0
- data/spec/client_spec.rb +303 -175
- data/spec/spec_helper.rb +2 -2
- metadata +43 -34
- data/.travis.yml +0 -9
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'multi_json'
|
3
|
+
|
4
|
+
module Customerio
|
5
|
+
DEFAULT_TIMEOUT = 10
|
6
|
+
|
7
|
+
class InvalidRequest < RuntimeError; end
|
8
|
+
class InvalidResponse < RuntimeError
|
9
|
+
attr_reader :code, :response
|
10
|
+
|
11
|
+
def initialize(code, body, response=nil)
|
12
|
+
@message = body
|
13
|
+
@code = code
|
14
|
+
@response = response
|
15
|
+
|
16
|
+
super(@message)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class BaseClient
|
21
|
+
def initialize(auth, options = {})
|
22
|
+
@auth = auth
|
23
|
+
@timeout = options[:timeout] || DEFAULT_TIMEOUT
|
24
|
+
@base_uri = options[:url]
|
25
|
+
end
|
26
|
+
|
27
|
+
def request(method, path, body = nil, headers = {})
|
28
|
+
execute(method, path, body, headers)
|
29
|
+
end
|
30
|
+
|
31
|
+
def request_and_verify_response(method, path, body = nil, headers = {})
|
32
|
+
verify_response(request(method, path, body, headers))
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def execute(method, path, body = nil, headers = {})
|
38
|
+
uri = URI.join(@base_uri, path)
|
39
|
+
|
40
|
+
session = Net::HTTP.new(uri.host, uri.port)
|
41
|
+
session.use_ssl = (uri.scheme == 'https')
|
42
|
+
session.open_timeout = @timeout
|
43
|
+
session.read_timeout = @timeout
|
44
|
+
|
45
|
+
req = request_class(method).new(uri.path)
|
46
|
+
|
47
|
+
if @auth.has_key?(:site_id) && @auth.has_key?(:api_key)
|
48
|
+
req.initialize_http_header(headers)
|
49
|
+
req.basic_auth @auth[:site_id], @auth[:api_key]
|
50
|
+
else
|
51
|
+
headers['Authorization'] = "Bearer #{@auth[:app_key]}"
|
52
|
+
req.initialize_http_header(headers)
|
53
|
+
end
|
54
|
+
|
55
|
+
if !body.nil?
|
56
|
+
req.add_field('Content-Type', 'application/json')
|
57
|
+
req.body = MultiJson.dump(body)
|
58
|
+
end
|
59
|
+
|
60
|
+
session.start do |http|
|
61
|
+
http.request(req)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def request_class(method)
|
66
|
+
case method
|
67
|
+
when :post
|
68
|
+
Net::HTTP::Post
|
69
|
+
when :put
|
70
|
+
Net::HTTP::Put
|
71
|
+
when :delete
|
72
|
+
Net::HTTP::Delete
|
73
|
+
else
|
74
|
+
raise InvalidRequest.new("Invalid request method #{method.inspect}")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def verify_response(response)
|
79
|
+
case response
|
80
|
+
when Net::HTTPSuccess then
|
81
|
+
response
|
82
|
+
else
|
83
|
+
raise InvalidResponse.new(response.code, response.body, response)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/customerio/client.rb
CHANGED
@@ -1,29 +1,16 @@
|
|
1
|
-
require
|
2
|
-
require 'multi_json'
|
1
|
+
require "addressable/uri"
|
3
2
|
|
4
3
|
module Customerio
|
5
|
-
DEFAULT_BASE_URI = 'https://track.customer.io'
|
6
|
-
DEFAULT_TIMEOUT = 10
|
7
|
-
|
8
4
|
class Client
|
9
5
|
class MissingIdAttributeError < RuntimeError; end
|
10
6
|
class ParamError < RuntimeError; end
|
11
|
-
class InvalidRequest < RuntimeError; end
|
12
|
-
class InvalidResponse < RuntimeError
|
13
|
-
attr_reader :response
|
14
7
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
end
|
19
|
-
end
|
8
|
+
def initialize(site_id, api_key, options = {})
|
9
|
+
options[:region] = Customerio::Regions::US if options[:region].nil?
|
10
|
+
raise "region must be an instance of Customerio::Regions::Region" unless options[:region].is_a?(Customerio::Regions::Region)
|
20
11
|
|
21
|
-
|
22
|
-
@
|
23
|
-
@password = secret_key
|
24
|
-
@json = options.has_key?(:json) ? options[:json] : true
|
25
|
-
@base_uri = options[:base_uri] || DEFAULT_BASE_URI
|
26
|
-
@timeout = options[:timeout] || DEFAULT_TIMEOUT
|
12
|
+
options[:url] = options[:region].track_url if options[:url].nil? || options[:url].empty?
|
13
|
+
@client = Customerio::BaseClient.new({ site_id: site_id, api_key: api_key }, options)
|
27
14
|
end
|
28
15
|
|
29
16
|
def identify(attributes)
|
@@ -31,33 +18,36 @@ module Customerio
|
|
31
18
|
end
|
32
19
|
|
33
20
|
def delete(customer_id)
|
34
|
-
|
21
|
+
raise ParamError.new("customer_id must be a non-empty string") if is_empty?(customer_id)
|
22
|
+
@client.request_and_verify_response(:delete, customer_path(customer_id))
|
23
|
+
end
|
24
|
+
|
25
|
+
def suppress(customer_id)
|
26
|
+
raise ParamError.new("customer_id must be a non-empty string") if is_empty?(customer_id)
|
27
|
+
@client.request_and_verify_response(:post, suppress_path(customer_id))
|
35
28
|
end
|
36
29
|
|
37
|
-
def
|
38
|
-
|
30
|
+
def unsuppress(customer_id)
|
31
|
+
raise ParamError.new("customer_id must be a non-empty string") if is_empty?(customer_id)
|
32
|
+
@client.request_and_verify_response(:post, unsuppress_path(customer_id))
|
33
|
+
end
|
39
34
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
create_anonymous_event(event_name, attributes)
|
44
|
-
else
|
45
|
-
# Passed in a customer id and an event name.
|
46
|
-
# Track the event for the given customer
|
47
|
-
customer_id, event_name = args
|
35
|
+
def track(customer_id, event_name, attributes = {})
|
36
|
+
raise ParamError.new("customer_id must be a non-empty string") if is_empty?(customer_id)
|
37
|
+
raise ParamError.new("event_name must be a non-empty string") if is_empty?(event_name)
|
48
38
|
|
49
|
-
|
50
|
-
end
|
39
|
+
create_customer_event(customer_id, event_name, attributes)
|
51
40
|
end
|
52
41
|
|
53
42
|
def anonymous_track(event_name, attributes = {})
|
43
|
+
raise ParamError.new("event_name must be a non-empty string") if is_empty?(event_name)
|
54
44
|
create_anonymous_event(event_name, attributes)
|
55
45
|
end
|
56
46
|
|
57
47
|
def add_device(customer_id, device_id, platform, data={})
|
58
|
-
raise ParamError.new("customer_id must be a non-empty string")
|
59
|
-
raise ParamError.new("device_id must be a non-empty string")
|
60
|
-
raise ParamError.new("platform must be a non-empty string")
|
48
|
+
raise ParamError.new("customer_id must be a non-empty string") if is_empty?(customer_id)
|
49
|
+
raise ParamError.new("device_id must be a non-empty string") if is_empty?(device_id)
|
50
|
+
raise ParamError.new("platform must be a non-empty string") if is_empty?(platform)
|
61
51
|
|
62
52
|
if data.nil?
|
63
53
|
data = {}
|
@@ -65,39 +55,54 @@ module Customerio
|
|
65
55
|
|
66
56
|
raise ParamError.new("data parameter must be a hash") unless data.is_a?(Hash)
|
67
57
|
|
68
|
-
|
58
|
+
@client.request_and_verify_response(:put, device_path(customer_id), {
|
69
59
|
:device => data.update({
|
70
60
|
:id => device_id,
|
71
61
|
:platform => platform,
|
72
62
|
})
|
73
|
-
})
|
63
|
+
})
|
74
64
|
end
|
75
65
|
|
76
66
|
def delete_device(customer_id, device_id)
|
77
|
-
raise ParamError.new("customer_id must be a non-empty string")
|
78
|
-
raise ParamError.new("device_id must be a non-empty string")
|
79
|
-
|
80
|
-
|
67
|
+
raise ParamError.new("customer_id must be a non-empty string") if is_empty?(customer_id)
|
68
|
+
raise ParamError.new("device_id must be a non-empty string") if is_empty?(device_id)
|
69
|
+
|
70
|
+
@client.request_and_verify_response(:delete, device_id_path(customer_id, device_id))
|
81
71
|
end
|
82
72
|
|
83
73
|
private
|
84
74
|
|
75
|
+
def escape(val)
|
76
|
+
# CGI.escape is recommended for escaping, however, it doesn't correctly escape spaces.
|
77
|
+
Addressable::URI.encode_component(val.to_s, Addressable::URI::CharacterClasses::UNRESERVED)
|
78
|
+
end
|
79
|
+
|
85
80
|
def device_path(customer_id)
|
86
|
-
"/api/v1/customers/#{customer_id}/devices"
|
81
|
+
"/api/v1/customers/#{escape(customer_id)}/devices"
|
87
82
|
end
|
88
83
|
|
89
84
|
def device_id_path(customer_id, device_id)
|
90
|
-
"/api/v1/customers/#{customer_id}/devices/#{device_id}"
|
85
|
+
"/api/v1/customers/#{escape(customer_id)}/devices/#{escape(device_id)}"
|
86
|
+
end
|
87
|
+
|
88
|
+
def customer_path(id)
|
89
|
+
"/api/v1/customers/#{escape(id)}"
|
90
|
+
end
|
91
|
+
|
92
|
+
def suppress_path(customer_id)
|
93
|
+
"/api/v1/customers/#{escape(customer_id)}/suppress"
|
94
|
+
end
|
95
|
+
|
96
|
+
def unsuppress_path(customer_id)
|
97
|
+
"/api/v1/customers/#{escape(customer_id)}/unsuppress"
|
91
98
|
end
|
92
99
|
|
93
100
|
def create_or_update(attributes = {})
|
94
101
|
attributes = Hash[attributes.map { |(k,v)| [ k.to_sym, v ] }]
|
95
|
-
|
96
|
-
raise MissingIdAttributeError.new("Must provide a customer id") unless attributes[:id]
|
102
|
+
raise MissingIdAttributeError.new("Must provide a customer id") if is_empty?(attributes[:id])
|
97
103
|
|
98
104
|
url = customer_path(attributes[:id])
|
99
|
-
|
100
|
-
verify_response(request(:put, url, attributes))
|
105
|
+
@client.request_and_verify_response(:put, url, attributes)
|
101
106
|
end
|
102
107
|
|
103
108
|
def create_customer_event(customer_id, event_name, attributes = {})
|
@@ -111,71 +116,15 @@ module Customerio
|
|
111
116
|
def create_event(url, event_name, attributes = {})
|
112
117
|
body = { :name => event_name, :data => attributes }
|
113
118
|
body[:timestamp] = attributes[:timestamp] if valid_timestamp?(attributes[:timestamp])
|
114
|
-
|
115
|
-
end
|
116
|
-
|
117
|
-
def customer_path(id)
|
118
|
-
"/api/v1/customers/#{id}"
|
119
|
+
@client.request_and_verify_response(:post, url, body)
|
119
120
|
end
|
120
121
|
|
121
122
|
def valid_timestamp?(timestamp)
|
122
123
|
timestamp && timestamp.is_a?(Integer) && timestamp > 999999999 && timestamp < 100000000000
|
123
124
|
end
|
124
125
|
|
125
|
-
|
126
|
-
|
127
|
-
if response.code.to_i >= 200 && response.code.to_i < 300
|
128
|
-
response
|
129
|
-
else
|
130
|
-
raise InvalidResponse.new("Customer.io API returned an invalid response: #{response.code}", response)
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
def extract_attributes(args)
|
135
|
-
hash = args.last.is_a?(Hash) ? args.pop : {}
|
136
|
-
hash.inject({}){ |hash, (k,v)| hash[k.to_sym] = v; hash }
|
137
|
-
end
|
138
|
-
|
139
|
-
def request(method, path, body = nil, headers = {})
|
140
|
-
uri = URI.join(@base_uri, path)
|
141
|
-
|
142
|
-
session = Net::HTTP.new(uri.host, uri.port)
|
143
|
-
session.use_ssl = (uri.scheme == 'https')
|
144
|
-
session.open_timeout = @timeout
|
145
|
-
session.read_timeout = @timeout
|
146
|
-
|
147
|
-
req = request_class(method).new(uri.path)
|
148
|
-
req.initialize_http_header(headers)
|
149
|
-
req.basic_auth @username, @password
|
150
|
-
|
151
|
-
add_request_body(req, body) unless body.nil?
|
152
|
-
|
153
|
-
session.start do |http|
|
154
|
-
http.request(req)
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
def request_class(method)
|
159
|
-
case method
|
160
|
-
when :post
|
161
|
-
Net::HTTP::Post
|
162
|
-
when :put
|
163
|
-
Net::HTTP::Put
|
164
|
-
when :delete
|
165
|
-
Net::HTTP::Delete
|
166
|
-
else
|
167
|
-
raise InvalidRequest.new("Invalid request method #{method.inspect}")
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
def add_request_body(req, body)
|
172
|
-
if @json
|
173
|
-
req.add_field('Content-Type', 'application/json')
|
174
|
-
req.body = MultiJson.dump(body)
|
175
|
-
else
|
176
|
-
req.add_field('Content-Type', 'application/x-www-form-urlencoded')
|
177
|
-
req.body = ParamEncoder.to_params(body)
|
178
|
-
end
|
126
|
+
def is_empty?(val)
|
127
|
+
val.nil? || (val.is_a?(String) && val.strip == "")
|
179
128
|
end
|
180
129
|
end
|
181
130
|
end
|
@@ -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
|
data/lib/customerio/version.rb
CHANGED
@@ -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
|