marketo-api-ruby 0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,115 @@
1
+ require_relative 'client_proxy'
2
+ require_relative 'mobject'
3
+
4
+ # MarketoAPI operations on Marketo objects (MObject).
5
+ class MarketoAPI::MObjects < MarketoAPI::ClientProxy
6
+
7
+ # :call-seq:
8
+ # delete(mobject, mobject, ...) -> status_list
9
+ #
10
+ # Implements
11
+ # {+deleteMObjects+}[http://developers.marketo.com/documentation/soap/deletemobjects/],
12
+ # returning the deletion success status for each object provided.
13
+ #
14
+ # Only works with Opportunity or OpportunityPersonRole MObjects.
15
+ #
16
+ # To delete an Opportunity:
17
+ #
18
+ # marketo.mobjects.delete MarketoAPI::MObject.opportunity(75)
19
+ def delete(*mobjects)
20
+ if mobjects.empty?
21
+ raise ArgumentError, "must provide one or more MObjects to delete"
22
+ end
23
+ response = call(
24
+ :delete_m_objects,
25
+ m_object_list: transform_param_list(__method__, mobjects)
26
+ )
27
+ extract_mobject_status_list(response)
28
+ end
29
+
30
+ # Implements
31
+ # {+listMObjects+}[http://developers.marketo.com/documentation/soap/listmobjects/],
32
+ # returning the type names of the available Marketo objects. The type
33
+ # names can be passed to #describe.
34
+ def list
35
+ extract_from_response(
36
+ call(:list_m_objects, nil),
37
+ :objects
38
+ )
39
+ end
40
+
41
+ # Implements
42
+ # {+describeMObject+}[http://developers.marketo.com/documentation/soap/describemobject/],
43
+ # returning the description of the Marketo object.
44
+ def describe(name)
45
+ unless MarketoAPI::MObject::DESCRIBE_TYPES.include?(name.to_sym)
46
+ raise ArgumentError, "invalid type #{name} to describe"
47
+ end
48
+
49
+ extract_from_response(
50
+ call(:describe_m_object, object_name: name),
51
+ :metadata
52
+ )
53
+ end
54
+
55
+ # Implements
56
+ # {+getMObjects+}[http://developers.marketo.com/documentation/soap/getmobjects/],
57
+ # returning one or more Marketo objects, up to 100 in a page. It also
58
+ # returns the current current stream position to continue working with the
59
+ # pages on subsequent calls to #get.
60
+ #
61
+ # See MObject#criteria and MObject#association on how to build criteria
62
+ # and association filters for #get queries.
63
+ def get(mobject)
64
+ call(:get_m_objects, transform_param(__method__, mobject)) { |list|
65
+ Get.new(list)
66
+ }
67
+ end
68
+
69
+ def sync(operation, *mobjects) #:nodoc:
70
+ # http://developers.marketo.com/documentation/soap/sync-mobjects/
71
+ raise NotImplementedError,
72
+ ":sync_m_objects is not implemented in this version."
73
+ response = call(
74
+ :sync_m_objects,
75
+ transform_param_list(__method__, mobjects)
76
+ )
77
+ extract_mobject_status_list(response)
78
+ end
79
+
80
+ # A response object to MObjects#get that includes the return count, the
81
+ # new stream position, and the list of MObject records.
82
+ class Get
83
+ # The number of MObjects returned from MObjects#get.
84
+ attr_reader :return_count
85
+ # The stream position used for paging in MObjects#get.
86
+ # This may be shared with each MObject in #mobjects.
87
+ attr_reader :stream_position
88
+ # The list of Marketo objects.
89
+ attr_reader :mobjects
90
+
91
+ def initialize(hash)
92
+ @return_count = hash[:return_count].to_i
93
+ @more = hash[:has_more]
94
+ @stream_position = hash[:new_stream_position]
95
+ objects = MarketoAPI.array(hash[:m_object_list])
96
+
97
+ @mobjects = objects.map { |object|
98
+ MarketoAPI::MObject.from_soap_hash(object[:m_object])
99
+ }
100
+ end
101
+
102
+ # Returns +true+ if there are more objects to be returned.
103
+ def more?
104
+ !!@more
105
+ end
106
+ end
107
+
108
+ private
109
+ def extract_mobject_status_list(response)
110
+ response = extract_from_response(response, :m_obj_status_list) { |resp|
111
+ resp.map { |e| e[:m_object_status].values_at(:id, :status) }
112
+ }.flatten
113
+ Hash[*response]
114
+ end
115
+ end
@@ -0,0 +1,66 @@
1
+ require File.expand_path('../spec_helper', File.dirname(__FILE__))
2
+
3
+ module Rapleaf
4
+ module Marketo
5
+ ACCESS_KEY = 'ACCESS_KEY'
6
+ SECRET_KEY = 'SECRET_KEY'
7
+
8
+ describe AuthenticationHeader do
9
+ it "should set mktowsUserId to access key" do
10
+ header = Rapleaf::Marketo::AuthenticationHeader.new(ACCESS_KEY, SECRET_KEY)
11
+ header.get_mktows_user_id.should == ACCESS_KEY
12
+ end
13
+
14
+ it "should set requestSignature" do
15
+ header = Rapleaf::Marketo::AuthenticationHeader.new(ACCESS_KEY, SECRET_KEY)
16
+ header.get_request_signature.should_not be_nil
17
+ header.get_request_signature.should_not == ''
18
+ end
19
+
20
+ it "should set requestTimestamp in correct format" do
21
+ header = Rapleaf::Marketo::AuthenticationHeader.new(ACCESS_KEY, SECRET_KEY)
22
+ time = DateTime.new(1998, 1, 17, 20, 15, 1)
23
+ header.set_time(time)
24
+ header.get_request_timestamp().should == '1998-01-17T20:15:01+00:00'
25
+ end
26
+
27
+ it "should calculate encrypted signature" do
28
+ # I got this example of the marketo API docs
29
+
30
+ access_key = 'bigcorp1_461839624B16E06BA2D663'
31
+ secret_key = '899756834129871744AAEE88DDCC77CDEEDEC1AAAD66'
32
+
33
+ header = Rapleaf::Marketo::AuthenticationHeader.new(access_key, secret_key)
34
+ header.set_time(DateTime.new(2010, 4, 9, 14, 4, 55, -7/24.0))
35
+
36
+ header.get_request_timestamp.should == '2010-04-09T14:04:54-07:00'
37
+ header.get_request_signature.should == 'ffbff4d4bef354807481e66dc7540f7890523a87'
38
+ end
39
+
40
+ it "should cope if no date is given" do
41
+ header = Rapleaf::Marketo::AuthenticationHeader.new(ACCESS_KEY, SECRET_KEY)
42
+ expected = DateTime.now
43
+ actual = DateTime.parse(header.get_request_timestamp)
44
+
45
+ expected.year.should == actual.year
46
+ expected.hour.should == actual.hour
47
+ end
48
+
49
+ it "should to_hash correctly" do
50
+ # I got this example from the marketo API docs
51
+
52
+ access_key = 'bigcorp1_461839624B16E06BA2D663'
53
+ secret_key = '899756834129871744AAEE88DDCC77CDEEDEC1AAAD66'
54
+
55
+ header = Rapleaf::Marketo::AuthenticationHeader.new(access_key, secret_key)
56
+ header.set_time(DateTime.new(2010, 4, 9, 14, 4, 55, -7/24.0))
57
+
58
+ header.to_hash.should == {
59
+ 'mktowsUserId' => header.get_mktows_user_id,
60
+ 'requestSignature' => header.get_request_signature,
61
+ 'requestTimestamp' => header.get_request_timestamp,
62
+ }
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,363 @@
1
+ require File.expand_path('../spec_helper', File.dirname(__FILE__))
2
+
3
+ module Rapleaf
4
+ module Marketo
5
+
6
+ describe Client do
7
+ EMAIL = "some@email.com"
8
+ IDNUM = 29
9
+ FIRST = 'Joe'
10
+ LAST = 'Smith'
11
+ COMPANY = 'Rapleaf'
12
+ MOBILE = '415 123 456'
13
+ API_KEY = 'API123KEY'
14
+
15
+ context 'Exception handling' do
16
+ it "should return nil if any exception is raised on get_lead request" do
17
+ savon_client = mock('savon_client').as_null_object
18
+ authentication_header = mock('authentication_header').as_null_object
19
+ client = Rapleaf::Marketo::Client.new(savon_client, authentication_header)
20
+ savon_client.should_receive(:request).and_raise Exception
21
+ client.get_lead_by_email(EMAIL).should be_nil
22
+ end
23
+
24
+ it "should return nil if any exception is raised on sync_lead request" do
25
+ savon_client = mock('savon_client').as_null_object
26
+ authentication_header = mock('authentication_header').as_null_object
27
+ client = Rapleaf::Marketo::Client.new(savon_client, authentication_header)
28
+ savon_client.should_receive(:request).and_raise Exception
29
+ client.sync_lead(EMAIL, FIRST, LAST, COMPANY, MOBILE).should be_nil
30
+ end
31
+ end
32
+
33
+ context 'Client interaction' do
34
+ it "should have the correct body format on get_lead_by_idnum" do
35
+ savon_client = mock('savon_client')
36
+ authentication_header = mock('authentication_header')
37
+ client = Rapleaf::Marketo::Client.new(savon_client, authentication_header)
38
+ response_hash = {
39
+ :success_get_lead => {
40
+ :result => {
41
+ :count => 1,
42
+ :lead_record_list => {
43
+ :lead_record => {
44
+ :email => EMAIL,
45
+ :lead_attribute_list => {
46
+ :attribute => [
47
+ {:attr_name => 'name1', :attr_type => 'string', :attr_value => 'val1'},
48
+ {:attr_name => 'name2', :attr_type => 'string', :attr_value => 'val2'},
49
+ {:attr_name => 'name3', :attr_type => 'string', :attr_value => 'val3'},
50
+ {:attr_name => 'name4', :attr_type => 'string', :attr_value => 'val4'}
51
+ ]
52
+ },
53
+ :foreign_sys_type => nil,
54
+ :foreign_sys_person_id => nil,
55
+ :id => IDNUM.to_s
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
61
+ expect_request(savon_client,
62
+ authentication_header,
63
+ equals_matcher(:lead_key => {
64
+ :key_value => IDNUM,
65
+ :key_type => LeadKeyType::IDNUM
66
+ }),
67
+ 'ns1:paramsGetLead',
68
+ response_hash)
69
+ expected_lead_record = LeadRecord.new(EMAIL, IDNUM)
70
+ expected_lead_record.set_attribute('name1', 'val1')
71
+ expected_lead_record.set_attribute('name2', 'val2')
72
+ expected_lead_record.set_attribute('name3', 'val3')
73
+ expected_lead_record.set_attribute('name4', 'val4')
74
+ client.get_lead_by_idnum(IDNUM).should == expected_lead_record
75
+ end
76
+
77
+ it "should have the correct body format on get_lead_by_email" do
78
+ savon_client = mock('savon_client')
79
+ authentication_header = mock('authentication_header')
80
+ client = Rapleaf::Marketo::Client.new(savon_client, authentication_header)
81
+ response_hash = {
82
+ :success_get_lead => {
83
+ :result => {
84
+ :count => 1,
85
+ :lead_record_list => {
86
+ :lead_record => {
87
+ :email => EMAIL,
88
+ :lead_attribute_list => {
89
+ :attribute => [
90
+ {:attr_name => 'name1', :attr_type => 'string', :attr_value => 'val1'},
91
+ {:attr_name => 'name2', :attr_type => 'string', :attr_value => 'val2'},
92
+ {:attr_name => 'name3', :attr_type => 'string', :attr_value => 'val3'},
93
+ {:attr_name => 'name4', :attr_type => 'string', :attr_value => 'val4'}
94
+ ]
95
+ },
96
+ :foreign_sys_type => nil,
97
+ :foreign_sys_person_id => nil,
98
+ :id => IDNUM.to_s
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
104
+ expect_request(savon_client,
105
+ authentication_header,
106
+ equals_matcher({:lead_key => {
107
+ :key_value => EMAIL,
108
+ :key_type => LeadKeyType::EMAIL}}),
109
+ 'ns1:paramsGetLead',
110
+ response_hash)
111
+ expected_lead_record = LeadRecord.new(EMAIL, IDNUM)
112
+ expected_lead_record.set_attribute('name1', 'val1')
113
+ expected_lead_record.set_attribute('name2', 'val2')
114
+ expected_lead_record.set_attribute('name3', 'val3')
115
+ expected_lead_record.set_attribute('name4', 'val4')
116
+ client.get_lead_by_email(EMAIL).should == expected_lead_record
117
+ end
118
+
119
+ it "should have the correct body format on sync_lead_record" do
120
+ savon_client = mock('savon_client')
121
+ authentication_header = mock('authentication_header')
122
+ client = Rapleaf::Marketo::Client.new(savon_client, authentication_header)
123
+ response_hash = {
124
+ :success_sync_lead => {
125
+ :result => {
126
+ :lead_id => IDNUM,
127
+ :sync_status => {
128
+ :error => nil,
129
+ :status => 'UPDATED',
130
+ :lead_id => IDNUM
131
+ },
132
+ :lead_record => {
133
+ :email => EMAIL,
134
+ :lead_attribute_list => {
135
+ :attribute => [
136
+ {:attr_name => 'name1', :attr_type => 'string', :attr_value => 'val1'},
137
+ {:attr_name => 'name2', :attr_type => 'string', :attr_value => 'val2'},
138
+ {:attr_name => 'name3', :attr_type => 'string', :attr_value => 'val3'},
139
+ {:attr_name => 'name4', :attr_type => 'string', :attr_value => 'val4'}
140
+ ]
141
+ },
142
+ :foreign_sys_type => nil,
143
+ :foreign_sys_person_id => nil,
144
+ :id => IDNUM.to_s
145
+ }
146
+ }
147
+ }
148
+ }
149
+ expect_request(savon_client,
150
+ authentication_header,
151
+ (Proc.new do |actual|
152
+ retval = true
153
+ retval = false unless actual[:return_lead]
154
+ retval = false unless actual[:lead_record][:email].equal?(EMAIL)
155
+ retval = false unless actual[:lead_record][:lead_attribute_list][:attribute].size == 5
156
+ retval = false unless actual[:lead_record][:lead_attribute_list][:attribute].include?({:attr_value => EMAIL, :attr_name => "Email", :attr_type => "string"})
157
+ retval = false unless actual[:lead_record][:lead_attribute_list][:attribute].include?({:attr_value => "val1", :attr_name => "name1", :attr_type => "string"})
158
+ retval = false unless actual[:lead_record][:lead_attribute_list][:attribute].include?({:attr_value => "val2", :attr_name => "name2", :attr_type => "string"})
159
+ retval = false unless actual[:lead_record][:lead_attribute_list][:attribute].include?({:attr_value => "val3", :attr_name => "name3", :attr_type => "string"})
160
+ retval = false unless actual[:lead_record][:lead_attribute_list][:attribute].include?({:attr_value => "val4", :attr_name => "name4", :attr_type => "string"})
161
+ retval.should == true
162
+ end),
163
+ 'ns1:paramsSyncLead',
164
+ response_hash)
165
+ lead_record = LeadRecord.new(EMAIL, IDNUM)
166
+ lead_record.set_attribute('name1', 'val1')
167
+ lead_record.set_attribute('name2', 'val2')
168
+ lead_record.set_attribute('name3', 'val3')
169
+ lead_record.set_attribute('name4', 'val4')
170
+
171
+ client.sync_lead_record(lead_record).should == lead_record
172
+ end
173
+
174
+ it "should have the correct body format on sync_lead" do
175
+ savon_client = mock('savon_client')
176
+ authentication_header = mock('authentication_header')
177
+ client = Rapleaf::Marketo::Client.new(savon_client, authentication_header)
178
+ response_hash = {
179
+ :success_sync_lead => {
180
+ :result => {
181
+ :lead_id => IDNUM,
182
+ :sync_status => {
183
+ :error => nil,
184
+ :status => 'UPDATED',
185
+ :lead_id => IDNUM
186
+ },
187
+ :lead_record => {
188
+ :email => EMAIL,
189
+ :lead_attribute_list => {
190
+ :attribute => [
191
+ {:attr_name => 'name1', :attr_type => 'string', :attr_value => 'val1'},
192
+ {:attr_name => 'name2', :attr_type => 'string', :attr_value => 'val2'},
193
+ {:attr_name => 'name3', :attr_type => 'string', :attr_value => 'val3'},
194
+ {:attr_name => 'name4', :attr_type => 'string', :attr_value => 'val4'}
195
+ ]
196
+ },
197
+ :foreign_sys_type => nil,
198
+ :foreign_sys_person_id => nil,
199
+ :id => IDNUM.to_s
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ expect_request(savon_client,
206
+ authentication_header,
207
+ Proc.new { |actual|
208
+ actual_attribute_list = actual[:lead_record][:lead_attribute_list][:attribute]
209
+ actual[:lead_record][:lead_attribute_list][:attribute] = nil
210
+ expected = {
211
+ :return_lead => true,
212
+ :lead_record => {
213
+ :email => "some@email.com",
214
+ :lead_attribute_list =>
215
+ {
216
+ :attribute => nil}}
217
+ }
218
+ actual.should == expected
219
+ actual_attribute_list.should =~ [
220
+ {:attr_value => FIRST,
221
+ :attr_name => "FirstName",
222
+ :attr_type => "string"},
223
+ {:attr_value => LAST,
224
+ :attr_name => "LastName",
225
+ :attr_type => "string"},
226
+ {:attr_value => EMAIL,
227
+ :attr_name =>"Email",
228
+ :attr_type => "string"},
229
+ {:attr_value => COMPANY,
230
+ :attr_name => "Company",
231
+ :attr_type => "string"},
232
+ {:attr_value => MOBILE,
233
+ :attr_name => "MobilePhone",
234
+ :attr_type => "string"}
235
+ ]
236
+ },
237
+ 'ns1:paramsSyncLead',
238
+ response_hash)
239
+ expected_lead_record = LeadRecord.new(EMAIL, IDNUM)
240
+ expected_lead_record.set_attribute('name1', 'val1')
241
+ expected_lead_record.set_attribute('name2', 'val2')
242
+ expected_lead_record.set_attribute('name3', 'val3')
243
+ expected_lead_record.set_attribute('name4', 'val4')
244
+ client.sync_lead(EMAIL, FIRST, LAST, COMPANY, MOBILE).should == expected_lead_record
245
+ end
246
+
247
+ context "list operations" do
248
+ LIST_KEY = 'awesome leads list'
249
+
250
+ before(:each) do
251
+ @savon_client = mock('savon_client')
252
+ @authentication_header = mock('authentication_header')
253
+ @client = Rapleaf::Marketo::Client.new(@savon_client, @authentication_header)
254
+ end
255
+
256
+ it "should have the correct body format on add_to_list" do
257
+ response_hash = {} # TODO
258
+ expect_request(@savon_client,
259
+ @authentication_header,
260
+ equals_matcher({
261
+ :list_operation => ListOperationType::ADD_TO,
262
+ :list_key => LIST_KEY,
263
+ :strict => 'false',
264
+ :list_member_list => {
265
+ :lead_key => [
266
+ {
267
+ :key_type => 'EMAIL',
268
+ :key_value => EMAIL
269
+ }
270
+ ]
271
+ }
272
+ }),
273
+ 'ns1:paramsListOperation',
274
+ response_hash)
275
+
276
+ @client.add_to_list(LIST_KEY, EMAIL).should == response_hash
277
+ end
278
+
279
+ it "should have the correct body format on remove_from_list" do
280
+ response_hash = {} # TODO
281
+ expect_request(@savon_client,
282
+ @authentication_header,
283
+ equals_matcher({
284
+ :list_operation => ListOperationType::REMOVE_FROM,
285
+ :list_key => LIST_KEY,
286
+ :strict => 'false',
287
+ :list_member_list => {
288
+ :lead_key => [
289
+ {
290
+ :key_type => 'EMAIL',
291
+ :key_value => EMAIL
292
+ }
293
+ ]
294
+ }
295
+ }),
296
+ 'ns1:paramsListOperation',
297
+ response_hash)
298
+
299
+ @client.remove_from_list(LIST_KEY, EMAIL).should == response_hash
300
+ end
301
+
302
+ it "should have the correct body format on is_member_of_list?" do
303
+ response_hash = {} # TODO
304
+ expect_request(@savon_client,
305
+ @authentication_header,
306
+ equals_matcher({
307
+ :list_operation => ListOperationType::IS_MEMBER_OF,
308
+ :list_key => LIST_KEY,
309
+ :strict => 'false',
310
+ :list_member_list => {
311
+ :lead_key => [
312
+ {
313
+ :key_type => 'EMAIL',
314
+ :key_value => EMAIL
315
+ }
316
+ ]
317
+ }
318
+ }),
319
+ 'ns1:paramsListOperation',
320
+ response_hash)
321
+
322
+ @client.is_member_of_list?(LIST_KEY, EMAIL).should == response_hash
323
+ end
324
+ end
325
+ end
326
+
327
+ private
328
+
329
+ def equals_matcher(expected)
330
+ Proc.new { |actual|
331
+ actual.should == expected
332
+ }
333
+ end
334
+
335
+ def expect_request(savon_client, authentication_header, expected_body_matcher, expected_namespace, response_hash)
336
+ header_hash = stub('header_hash')
337
+ soap_response = stub('soap_response')
338
+ request_namespace = mock('namespace')
339
+ request_header = mock('request_header')
340
+ soap_request = mock('soap_request')
341
+ authentication_header.should_receive(:set_time)
342
+ authentication_header.should_receive(:to_hash).and_return(header_hash)
343
+ request_namespace.should_receive(:[]=).with("xmlns:ns1", "http://www.marketo.com/mktows/")
344
+ request_header.should_receive(:[]=).with("ns1:AuthenticationHeader", header_hash)
345
+ soap_request.should_receive(:namespaces).and_return(request_namespace)
346
+ soap_request.should_receive(:header).and_return(request_header)
347
+ soap_request.should_receive(:body=) do |actual_body|
348
+ expected_body_matcher.call(actual_body)
349
+ end
350
+ soap_response.should_receive(:to_hash).and_return(response_hash)
351
+ savon_client.should_receive(:request).with(expected_namespace).and_yield(soap_request).and_return(soap_response)
352
+ end
353
+ end
354
+
355
+ describe ListOperationType do
356
+ it 'should define the correct types' do
357
+ ListOperationType::ADD_TO.should == 'ADDTOLIST'
358
+ ListOperationType::IS_MEMBER_OF.should == 'ISMEMBEROFLIST'
359
+ ListOperationType::REMOVE_FROM.should == 'REMOVEFROMLIST'
360
+ end
361
+ end
362
+ end
363
+ end