sfdc 3.0.0 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -0
  3. data/Gemfile +11 -0
  4. data/Guardfile +11 -0
  5. data/LICENSE +22 -0
  6. data/README.md +88 -98
  7. data/Rakefile +10 -0
  8. data/lib/sfdc.rb +1 -0
  9. data/lib/sfdc/abstract_client.rb +1 -1
  10. data/lib/sfdc/attachment.rb +0 -2
  11. data/lib/sfdc/concerns/api.rb +1 -2
  12. data/lib/sfdc/concerns/authentication.rb +1 -2
  13. data/lib/sfdc/concerns/base.rb +5 -7
  14. data/lib/sfdc/concerns/caching.rb +1 -3
  15. data/lib/sfdc/concerns/canvas.rb +0 -2
  16. data/lib/sfdc/concerns/connection.rb +2 -3
  17. data/lib/sfdc/concerns/picklists.rb +1 -4
  18. data/lib/sfdc/concerns/streaming.rb +2 -2
  19. data/lib/sfdc/concerns/verbs.rb +0 -2
  20. data/lib/sfdc/config.rb +1 -0
  21. data/lib/sfdc/middleware/authentication.rb +4 -2
  22. data/lib/sfdc/middleware/authentication/password.rb +2 -5
  23. data/lib/sfdc/middleware/authorization.rb +1 -5
  24. data/lib/sfdc/middleware/caching.rb +0 -2
  25. data/lib/sfdc/middleware/instance_url.rb +0 -4
  26. data/lib/sfdc/middleware/mashify.rb +1 -3
  27. data/lib/sfdc/middleware/multipart.rb +6 -4
  28. data/lib/sfdc/signed_request.rb +1 -1
  29. data/lib/sfdc/tooling/client.rb +4 -6
  30. data/lib/sfdc/version.rb +2 -2
  31. data/sfdc.gemspec +27 -0
  32. data/spec/fixtures/auth_error_response.json +4 -0
  33. data/spec/fixtures/auth_success_response.json +7 -0
  34. data/spec/fixtures/blob.jpg +0 -0
  35. data/spec/fixtures/expired_session_response.json +6 -0
  36. data/spec/fixtures/reauth_success_response.json +7 -0
  37. data/spec/fixtures/refresh_error_response.json +4 -0
  38. data/spec/fixtures/refresh_success_response.json +7 -0
  39. data/spec/fixtures/services_data_success_response.json +12 -0
  40. data/spec/fixtures/sobject/create_success_response.json +5 -0
  41. data/spec/fixtures/sobject/delete_error_response.json +1 -0
  42. data/spec/fixtures/sobject/describe_sobjects_success_response.json +31 -0
  43. data/spec/fixtures/sobject/list_sobjects_success_response.json +31 -0
  44. data/spec/fixtures/sobject/org_query_response.json +11 -0
  45. data/spec/fixtures/sobject/query_aggregate_success_response.json +23 -0
  46. data/spec/fixtures/sobject/query_empty_response.json +5 -0
  47. data/spec/fixtures/sobject/query_error_response.json +6 -0
  48. data/spec/fixtures/sobject/query_paginated_first_page_response.json +14 -0
  49. data/spec/fixtures/sobject/query_paginated_last_page_response.json +13 -0
  50. data/spec/fixtures/sobject/query_success_response.json +38 -0
  51. data/spec/fixtures/sobject/recent_success_response.json +18 -0
  52. data/spec/fixtures/sobject/search_error_response.json +6 -0
  53. data/spec/fixtures/sobject/search_success_response.json +16 -0
  54. data/spec/fixtures/sobject/sobject_describe_error_response.json +6 -0
  55. data/spec/fixtures/sobject/sobject_describe_success_response.json +1429 -0
  56. data/spec/fixtures/sobject/sobject_find_error_response.json +6 -0
  57. data/spec/fixtures/sobject/sobject_find_success_response.json +29 -0
  58. data/spec/fixtures/sobject/upsert_created_success_response.json +5 -0
  59. data/spec/fixtures/sobject/upsert_error_response.json +6 -0
  60. data/spec/fixtures/sobject/upsert_multiple_error_response.json +4 -0
  61. data/spec/fixtures/sobject/upsert_updated_success_response.json +0 -0
  62. data/spec/fixtures/sobject/write_error_response.json +6 -0
  63. data/spec/integration/abstract_client_spec.rb +306 -0
  64. data/spec/integration/data/client_spec.rb +90 -0
  65. data/spec/spec_helper.rb +20 -0
  66. data/spec/support/client_integration.rb +45 -0
  67. data/spec/support/concerns.rb +18 -0
  68. data/spec/support/event_machine.rb +14 -0
  69. data/spec/support/fixture_helpers.rb +45 -0
  70. data/spec/support/matchers.rb +11 -0
  71. data/spec/support/middleware.rb +76 -0
  72. data/spec/support/mock_cache.rb +13 -0
  73. data/spec/unit/abstract_client_spec.rb +11 -0
  74. data/spec/unit/attachment_spec.rb +15 -0
  75. data/spec/unit/collection_spec.rb +52 -0
  76. data/spec/unit/concerns/api_spec.rb +244 -0
  77. data/spec/unit/concerns/authentication_spec.rb +98 -0
  78. data/spec/unit/concerns/base_spec.rb +42 -0
  79. data/spec/unit/concerns/caching_spec.rb +29 -0
  80. data/spec/unit/concerns/canvas_spec.rb +30 -0
  81. data/spec/unit/concerns/connection_spec.rb +22 -0
  82. data/spec/unit/config_spec.rb +99 -0
  83. data/spec/unit/data/client_spec.rb +10 -0
  84. data/spec/unit/mash_spec.rb +36 -0
  85. data/spec/unit/middleware/authentication/password_spec.rb +31 -0
  86. data/spec/unit/middleware/authentication/token_spec.rb +24 -0
  87. data/spec/unit/middleware/authentication_spec.rb +67 -0
  88. data/spec/unit/middleware/authorization_spec.rb +11 -0
  89. data/spec/unit/middleware/gzip_spec.rb +66 -0
  90. data/spec/unit/middleware/instance_url_spec.rb +24 -0
  91. data/spec/unit/middleware/logger_spec.rb +19 -0
  92. data/spec/unit/middleware/mashify_spec.rb +11 -0
  93. data/spec/unit/middleware/raise_error_spec.rb +32 -0
  94. data/spec/unit/signed_request_spec.rb +24 -0
  95. data/spec/unit/sobject_spec.rb +86 -0
  96. data/spec/unit/tooling/client_spec.rb +7 -0
  97. metadata +225 -65
@@ -0,0 +1,6 @@
1
+ [
2
+ {
3
+ "message": "Provided external ID field does not exist or is not accessible: 23foo",
4
+ "errorCode": "NOT_FOUND"
5
+ }
6
+ ]
@@ -0,0 +1,29 @@
1
+ {
2
+ "attributes": {
3
+ "type": "Whizbang",
4
+ "url": "/services/data/v20.0/sobjects/Whizbang/23foo"
5
+ },
6
+ "Id": "23foo",
7
+ "OwnerId": "owner_id",
8
+ "IsDeleted": false,
9
+ "Name": "My First Whizbang",
10
+ "CreatedById": "created_by_id",
11
+ "LastModifiedById": "last_modified_by_id",
12
+ "Auto_Number": "A-1",
13
+ "Checkbox_Label": true,
14
+ "Currency_Label": 23.0,
15
+ "Date_Label": "2010-01-01",
16
+ "DateTime_Label": "2011-07-07T00:37:00.000+0000",
17
+ "OtherDateTime_Label": null,
18
+ "Email_Label": "danny@example.com",
19
+ "Number_Label": 23.0,
20
+ "Percent_Label": 33.0,
21
+ "Phone_Label": "(415) 555-1212",
22
+ "Picklist_Label": "one",
23
+ "Picklist_Multiselect_Label": "four;six",
24
+ "Text_Label": "some text",
25
+ "TextArea_Label": "a text area",
26
+ "TextAreaLong_Label": "a loooooooooooooong text area",
27
+ "TextAreaRich_Label": "Rich <strong>text</strong>",
28
+ "URL_Label": "http://pivotallabs.com"
29
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "id": "foo",
3
+ "errors": [],
4
+ "success": true
5
+ }
@@ -0,0 +1,6 @@
1
+ [
2
+ {
3
+ "message": "Provided external ID field does not exist or is not accessible: Namez",
4
+ "errorCode": "NOT_FOUND"
5
+ }
6
+ ]
@@ -0,0 +1,4 @@
1
+ [
2
+ "/services/data/v23.0/sobjects/Whizbang/foo",
3
+ "/services/data/v23.0/sobjects/Whizbang/bar"
4
+ ]
@@ -0,0 +1,6 @@
1
+ [
2
+ {
3
+ "message": "No such column 'foo' on sobject of type Bar",
4
+ "errorCode": "INVALID_FIELD"
5
+ }
6
+ ]
@@ -0,0 +1,306 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples_for Sfdc::AbstractClient do
4
+ describe '.list_sobjects' do
5
+ requests :sobjects, :fixture => 'sobject/describe_sobjects_success_response'
6
+
7
+ subject { client.list_sobjects }
8
+ it { should be_an Array }
9
+ it { should eq ['Account'] }
10
+ end
11
+
12
+ describe '.describe' do
13
+ context 'with no arguments' do
14
+ requests :sobjects, :fixture => 'sobject/describe_sobjects_success_response'
15
+
16
+ subject { client.describe }
17
+ it { should be_an Array }
18
+ end
19
+
20
+ context 'with an argument' do
21
+ requests 'sobjects/Whizbang/describe', :fixture => 'sobject/sobject_describe_success_response'
22
+
23
+ subject { client.describe('Whizbang') }
24
+ its(['name']) { should eq 'Whizbang' }
25
+ end
26
+ end
27
+
28
+ describe '.query' do
29
+ requests 'query\?q=SELECT%20some,%20fields%20FROM%20object', :fixture => 'sobject/query_success_response'
30
+
31
+ subject { client.query('SELECT some, fields FROM object') }
32
+ it { should be_an Enumerable }
33
+ end
34
+
35
+ describe '.search' do
36
+ requests 'search\?q=FIND%20%7Bbar%7D', :fixture => 'sobject/search_success_response'
37
+
38
+ subject { client.search('FIND {bar}') }
39
+ it { should be_an Array }
40
+ its(:size) { should eq 2 }
41
+ end
42
+
43
+ describe '.org_id' do
44
+ requests 'query\?q=select%20id%20from%20Organization', :fixture => 'sobject/org_query_response'
45
+
46
+ subject { client.org_id }
47
+ it { should eq '00Dx0000000BV7z' }
48
+ end
49
+
50
+ describe '.create' do
51
+ context 'without multipart' do
52
+ requests 'sobjects/Account',
53
+ :method => :post,
54
+ :with_body => "{\"Name\":\"Foobar\"}",
55
+ :fixture => 'sobject/create_success_response'
56
+
57
+ subject { client.create('Account', :Name => 'Foobar') }
58
+ it { should eq 'some_id' }
59
+ end
60
+
61
+ context 'with multipart' do
62
+ requests 'sobjects/Account',
63
+ :method => :post,
64
+ :with_body => %r(----boundary_string\r\nContent-Disposition: form-data; name=\"entity_content\";\r\nContent-Type: application/json\r\n\r\n{\"Name\":\"Foobar\"}\r\n----boundary_string\r\nContent-Disposition: form-data; name=\"Blob\"; filename=\"blob.jpg\"\r\nContent-Length: 42171\r\nContent-Type: image/jpeg\r\nContent-Transfer-Encoding: binary),
65
+ :fixture => 'sobject/create_success_response'
66
+
67
+ subject { client.create('Account', :Name => 'Foobar', :Blob => Sfdc::UploadIO.new(File.expand_path('../../fixtures/blob.jpg', __FILE__), 'image/jpeg')) }
68
+ it { should eq 'some_id' }
69
+ end
70
+ end
71
+
72
+ describe '.update!' do
73
+ context 'with invalid Id' do
74
+ requests 'sobjects/Account/001D000000INjVe',
75
+ :method => :patch,
76
+ :with_body => "{\"Name\":\"Foobar\"}",
77
+ :status => 404,
78
+ :fixture => 'sobject/delete_error_response'
79
+
80
+ subject { lambda { client.update!('Account', :Id => '001D000000INjVe', :Name => 'Foobar') } }
81
+ it { should raise_error Faraday::Error::ResourceNotFound }
82
+ end
83
+ end
84
+
85
+ describe '.update' do
86
+ context 'with missing Id' do
87
+ subject { lambda { client.update('Account', :Name => 'Foobar') } }
88
+ it { should raise_error ArgumentError, 'Id field missing from attrs.' }
89
+ end
90
+
91
+ context 'with invalid Id' do
92
+ requests 'sobjects/Account/001D000000INjVe',
93
+ :method => :patch,
94
+ :with_body => "{\"Name\":\"Foobar\"}",
95
+ :status => 404,
96
+ :fixture => 'sobject/delete_error_response'
97
+
98
+ subject { client.update('Account', :Id => '001D000000INjVe', :Name => 'Foobar') }
99
+ it { should be_false }
100
+ end
101
+
102
+ context 'with success' do
103
+ requests 'sobjects/Account/001D000000INjVe',
104
+ :method => :patch,
105
+ :with_body => "{\"Name\":\"Foobar\"}"
106
+
107
+ [:Id, :id, 'Id', 'id'].each do |key|
108
+ context "with #{key.inspect} as the key" do
109
+ subject { client.update('Account', key => '001D000000INjVe', :Name => 'Foobar') }
110
+ it { should be_true }
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ describe '.upsert!' do
117
+ context 'when updated' do
118
+ requests 'sobjects/Account/External__c/foobar',
119
+ :method => :patch,
120
+ :with_body => "{\"Name\":\"Foobar\"}"
121
+
122
+ context 'with symbol external Id key' do
123
+ subject { client.upsert!('Account', 'External__c', :External__c => 'foobar', :Name => 'Foobar') }
124
+ it { should be_true }
125
+ end
126
+
127
+ context 'with string external Id key' do
128
+ subject { client.upsert!('Account', 'External__c', 'External__c' => 'foobar', 'Name' => 'Foobar') }
129
+ it { should be_true }
130
+ end
131
+ end
132
+
133
+ context 'when created' do
134
+ requests 'sobjects/Account/External__c/foobar',
135
+ :method => :patch,
136
+ :with_body => "{\"Name\":\"Foobar\"}",
137
+ :fixture => 'sobject/upsert_created_success_response'
138
+
139
+ [:External__c, 'External__c', :external__c, 'external__c'].each do |key|
140
+ context "with #{key.inspect} as the external id" do
141
+ subject { client.upsert!('Account', 'External__c', key => 'foobar', :Name => 'Foobar') }
142
+ it { should eq 'foo' }
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ describe '.destroy!' do
149
+ subject(:destroy!) { client.destroy!('Account', '001D000000INjVe') }
150
+
151
+ context 'with invalid Id' do
152
+ requests 'sobjects/Account/001D000000INjVe',
153
+ :fixture => 'sobject/delete_error_response',
154
+ :method => :delete,
155
+ :status => 404
156
+
157
+ subject { lambda { destroy! } }
158
+ it { should raise_error Faraday::Error::ResourceNotFound }
159
+ end
160
+
161
+ context 'with success' do
162
+ requests 'sobjects/Account/001D000000INjVe', :method => :delete
163
+
164
+ it { should be_true }
165
+ end
166
+ end
167
+
168
+ describe '.destroy' do
169
+ subject { client.destroy('Account', '001D000000INjVe') }
170
+
171
+ context 'with invalid Id' do
172
+ requests 'sobjects/Account/001D000000INjVe',
173
+ :fixture => 'sobject/delete_error_response',
174
+ :method => :delete,
175
+ :status => 404
176
+
177
+ it { should be_false }
178
+ end
179
+
180
+ context 'with success' do
181
+ requests 'sobjects/Account/001D000000INjVe', :method => :delete
182
+
183
+ it { should be_true }
184
+ end
185
+ end
186
+
187
+ describe '.find' do
188
+ context 'with no external id passed' do
189
+ requests 'sobjects/Account/001D000000INjVe',
190
+ :fixture => 'sobject/sobject_find_success_response'
191
+
192
+ subject { client.find('Account', '001D000000INjVe') }
193
+ it { should be_a Hash }
194
+ end
195
+
196
+ context 'when an external id is passed' do
197
+ requests 'sobjects/Account/External_Field__c/1234',
198
+ :fixture => 'sobject/sobject_find_success_response'
199
+
200
+ subject { client.find('Account', '1234', 'External_Field__c') }
201
+ it { should be_a Hash }
202
+ end
203
+ end
204
+
205
+ describe '.authenticate!' do
206
+ subject(:authenticate!) { client.authenticate! }
207
+
208
+ context 'when successful' do
209
+ before do
210
+ @request = stub_login_request(:with_body => "grant_type=password&client_id=client_id&client_secret=" \
211
+ "client_secret&username=foo&password=barsecurity_token").
212
+ to_return(:status => 200, :body => fixture(:auth_success_response))
213
+ end
214
+
215
+ after do
216
+ expect(@request).to have_been_requested
217
+ end
218
+
219
+ it { should be_a Hash }
220
+ end
221
+
222
+ context 'when no authentication middleware is present' do
223
+ before do
224
+ client.stub(:authentication_middleware).and_return(nil)
225
+ end
226
+
227
+ subject { lambda { authenticate! } }
228
+ it { should raise_error Sfdc::AuthenticationError, 'No authentication middleware present'}
229
+ end
230
+ end
231
+
232
+ describe '.without_caching' do
233
+ requests 'query\?q=SELECT%20some,%20fields%20FROM%20object',
234
+ :fixture => 'sobject/query_success_response'
235
+
236
+ before do
237
+ cache.should_receive(:delete).and_call_original
238
+ cache.should_receive(:fetch).and_call_original
239
+ end
240
+
241
+ let(:cache) { MockCache.new }
242
+ subject { client.without_caching { client.query('SELECT some, fields FROM object') } }
243
+ it { should be_an Enumerable }
244
+ end
245
+
246
+ describe 'authentication retries' do
247
+ context 'when retries reaches 0' do
248
+ before do
249
+ @auth_request = stub_api_request('query\?q=SELECT%20some,%20fields%20FROM%20object',
250
+ :status => 401,
251
+ :fixture => 'expired_session_response')
252
+ @query_request = stub_login_request(:with_body => "grant_type=password&client_id=client_id&client_secret=" \
253
+ "client_secret&username=foo&password=barsecurity_token").
254
+ to_return(:status => 200, :body => fixture(:auth_success_response))
255
+ end
256
+
257
+ subject { lambda { client.query('SELECT some, fields FROM object') } }
258
+ it { should raise_error Sfdc::UnauthorizedError }
259
+ end
260
+ end
261
+
262
+ describe '.query with caching' do
263
+ let(:cache) { MockCache.new }
264
+
265
+ before do
266
+ @query = stub_api_request('query\?q=SELECT%20some,%20fields%20FROM%20object').
267
+ with(:headers => { 'Authorization' => "OAuth #{oauth_token}" }).
268
+ to_return(:status => 401, :body => fixture('expired_session_response'), :headers => { 'Content-Type' => 'application/json' }).then.
269
+ to_return(:status => 200, :body => fixture('sobject/query_success_response'), :headers => { 'Content-Type' => 'application/json' })
270
+
271
+ @login = stub_login_request(:with_body => "grant_type=password&client_id=client_id&client_secret=" \
272
+ "client_secret&username=foo&password=barsecurity_token").
273
+ to_return(:status => 200, :body => fixture(:auth_success_response))
274
+ end
275
+
276
+ after do
277
+ expect(@query).to have_been_made.times(2)
278
+ expect(@login).to have_been_made
279
+ end
280
+
281
+ subject { client.query('SELECT some, fields FROM object') }
282
+ it { should be_an Enumerable }
283
+ end
284
+ end
285
+
286
+ describe Sfdc::AbstractClient do
287
+ describe 'with mashify' do
288
+ it_behaves_like Sfdc::AbstractClient
289
+
290
+ describe '.query' do
291
+ context 'with pagination' do
292
+ subject { client.query('SELECT some, fields FROM object').next_page }
293
+
294
+ requests 'query\?q', :fixture => 'sobject/query_paginated_first_page_response'
295
+ requests 'query/01gD', :fixture => 'sobject/query_paginated_last_page_response'
296
+
297
+ it { should be_a Sfdc::Collection }
298
+ its('first.Text_Label') { should eq 'Last Page'}
299
+ end
300
+ end
301
+ end
302
+
303
+ describe 'without mashify', :mashify => false do
304
+ it_behaves_like Sfdc::AbstractClient
305
+ end
306
+ end
@@ -0,0 +1,90 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples_for Sfdc::Data::Client do
4
+ describe '.picklist_values' do
5
+ requests 'sobjects/Account/describe',
6
+ :fixture => 'sobject/sobject_describe_success_response'
7
+
8
+ context 'when given a picklist field' do
9
+ subject { client.picklist_values('Account', 'Picklist_Field') }
10
+ it { should be_an Array }
11
+ its(:length) { should eq 3 }
12
+ it { should include_picklist_values ['one', 'two', 'three'] }
13
+ end
14
+
15
+ context 'when given a multipicklist field' do
16
+ subject { client.picklist_values('Account', 'Picklist_Multiselect_Field') }
17
+ it { should be_an Array }
18
+ its(:length) { should eq 3 }
19
+ it { should include_picklist_values ['four', 'five', 'six'] }
20
+ end
21
+
22
+ describe 'dependent picklists' do
23
+ context 'when given a picklist field that has a dependency' do
24
+ subject { client.picklist_values('Account', 'Dependent_Picklist_Field', :valid_for => 'one') }
25
+ it { should be_an Array }
26
+ its(:length) { should eq 2 }
27
+ it { should include_picklist_values ['seven', 'eight'] }
28
+ it { should_not include_picklist_values ['nine'] }
29
+ end
30
+
31
+ context 'when given a picklist field that does not have a dependency' do
32
+ subject { client.picklist_values('Account', 'Picklist_Field', :valid_for => 'one') }
33
+ it 'raises an exception' do
34
+ expect { subject }.to raise_error(/Picklist_Field is not a dependent picklist/)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ describe '.faye', :event_machine => true do
41
+ subject { client.faye }
42
+
43
+ context 'with missing instance url' do
44
+ let(:instance_url) { nil }
45
+ specify { expect { subject }.to raise_error RuntimeError, 'Instance URL missing. Call .authenticate! first.' }
46
+ end
47
+
48
+ context 'with oauth token and instance url' do
49
+ let(:instance_url) { 'http://google.com' }
50
+ let(:oauth_token) { 'bar' }
51
+ specify { expect { subject }.to_not raise_error }
52
+ end
53
+
54
+ context 'when the connection goes down' do
55
+ it 'should reauthenticate' do
56
+ access_token = double('access token')
57
+ access_token.stub(:access_token).and_return('token')
58
+ client.should_receive(:authenticate!).and_return(access_token)
59
+ client.faye.should_receive(:set_header).with('Authorization', "OAuth token")
60
+ client.faye.trigger('transport:down')
61
+ end
62
+ end
63
+ end
64
+
65
+ describe '.subcribe', :event_machine => true do
66
+ context 'when given a single pushtopic' do
67
+ it 'subscribes to the pushtopic' do
68
+ client.faye.should_receive(:subscribe).with(['/topic/PushTopic'])
69
+ client.subscribe('PushTopic')
70
+ end
71
+ end
72
+
73
+ context 'when given an array of pushtopics' do
74
+ it 'subscribes to each pushtopic' do
75
+ client.faye.should_receive(:subscribe).with(['/topic/PushTopic1', '/topic/PushTopic2'])
76
+ client.subscribe(['PushTopic1', 'PushTopic2'])
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ describe Sfdc::Data::Client do
83
+ describe 'with mashify' do
84
+ it_behaves_like Sfdc::Client
85
+ end
86
+
87
+ describe 'without mashify', :mashify => false do
88
+ it_behaves_like Sfdc::Client
89
+ end
90
+ end