restforce 0.1.10 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of restforce might be problematic. Click here for more details.
- data/Gemfile +1 -0
- data/README.md +23 -4
- data/lib/restforce/client/api.rb +13 -1
- data/lib/restforce/client/connection.rb +13 -0
- data/lib/restforce/client/streaming.rb +4 -4
- data/lib/restforce/config.rb +1 -1
- data/lib/restforce/middleware/multipart.rb +8 -8
- data/lib/restforce/version.rb +1 -1
- data/restforce.gemspec +2 -3
- data/spec/fixtures/sobject/query_paginated_first_page_response.json +1 -1
- data/spec/lib/client_spec.rb +121 -208
- data/spec/lib/collection_spec.rb +3 -6
- data/spec/lib/config_spec.rb +1 -1
- data/spec/lib/sobject_spec.rb +19 -37
- data/spec/spec_helper.rb +7 -1
- data/spec/support/fixture_helpers.rb +36 -22
- metadata +8 -24
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
# Restforce
|
1
|
+
# Restforce
|
2
|
+
|
3
|
+
[![travis-ci](https://secure.travis-ci.org/ejholmes/restforce.png)](https://secure.travis-ci.org/ejholmes/restforce) [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/ejholmes/restforce) [![Dependency Status](https://gemnasium.com/ejholmes/restforce.png)](https://gemnasium.com/ejholmes/restforce)
|
2
4
|
|
3
5
|
Restforce is a ruby gem for the [Salesforce REST api](http://www.salesforce.com/us/developer/docs/api_rest/index.htm).
|
4
6
|
It's meant to be a lighter weight alternative to the [databasedotcom gem](https://github.com/heroku/databasedotcom) that offers
|
@@ -10,7 +12,6 @@ Features include:
|
|
10
12
|
* Support for interacting with multiple users from different orgs.
|
11
13
|
* Support for parent-to-child relationships.
|
12
14
|
* Support for aggregate queries.
|
13
|
-
* Remove the need to materialize constants.
|
14
15
|
* Support for the [Streaming API](#streaming)
|
15
16
|
* Support for blob data types.
|
16
17
|
* Support for GZIP compression.
|
@@ -140,6 +141,25 @@ _See also: http://www.salesforce.com/us/developer/docs/api_rest/Content/dome_que
|
|
140
141
|
|
141
142
|
* * *
|
142
143
|
|
144
|
+
### find(sobject, id, field=nil)
|
145
|
+
|
146
|
+
Finds the record with the specified id and the specified sobject type and
|
147
|
+
returns all fields for the sobject. An external id field can be used instead
|
148
|
+
of the default Id field by specifiying the name of the external id field as the
|
149
|
+
last parameter.
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
client.find('Account', '001D000000INjVe')
|
153
|
+
# => #<Restforce::SObject Id="001D000000INjVe" Name="Test" LastModifiedBy="005G0000002f8FHIAY" ... >
|
154
|
+
|
155
|
+
client.find('Account', '1234', 'Some_External_Id_Field__c')
|
156
|
+
# => #<Restforce::SObject Id="001D000000INjVe" Name="Test" LastModifiedBy="005G0000002f8FHIAY" ... >
|
157
|
+
```
|
158
|
+
|
159
|
+
_See also: http://www.salesforce.com/us/developer/docs/api_rest/Content/resources_sobject_upsert.htm_
|
160
|
+
|
161
|
+
* * *
|
162
|
+
|
143
163
|
### search(sosl)
|
144
164
|
|
145
165
|
Performs a sosl query and returns the result. The result will be a
|
@@ -319,8 +339,7 @@ Restforce supports the [Streaming API](http://wiki.developerforce.com/page/Getti
|
|
319
339
|
pub/sub with Salesforce a trivial task:
|
320
340
|
|
321
341
|
```ruby
|
322
|
-
# Restforce uses faye as the underlying implementation for CometD.
|
323
|
-
# using faye 0.8.3.
|
342
|
+
# Restforce uses faye as the underlying implementation for CometD.
|
324
343
|
require 'faye'
|
325
344
|
|
326
345
|
# Initialize a client with your username/password/oauth token/etc.
|
data/lib/restforce/client/api.rb
CHANGED
@@ -191,7 +191,7 @@ module Restforce
|
|
191
191
|
# Returns the Id of the newly created record if the record was created.
|
192
192
|
# Raises an error if something bad happens.
|
193
193
|
def upsert!(sobject, field, attrs)
|
194
|
-
external_id = attrs.
|
194
|
+
external_id = attrs.delete(attrs.keys.find { |k| k.to_s.downcase == field.to_s.downcase })
|
195
195
|
response = api_patch "sobjects/#{sobject}/#{field.to_s}/#{external_id}", attrs
|
196
196
|
(response.body && response.body['id']) ? response.body['id'] : true
|
197
197
|
end
|
@@ -219,6 +219,18 @@ module Restforce
|
|
219
219
|
true
|
220
220
|
end
|
221
221
|
|
222
|
+
# Public: Finds a single record and returns all fields.
|
223
|
+
#
|
224
|
+
# sobject - The String name of the sobject.
|
225
|
+
# id - The id of the record. If field is specified, id should be the id
|
226
|
+
# of the external field.
|
227
|
+
# field - External ID field to use (default: nil).
|
228
|
+
#
|
229
|
+
# Returns the Restforce::SObject sobject record.
|
230
|
+
def find(sobject, id, field=nil)
|
231
|
+
api_get(field ? "sobjects/#{sobject}/#{field}/#{id}" : "sobjects/#{sobject}/#{id}").body
|
232
|
+
end
|
233
|
+
|
222
234
|
private
|
223
235
|
|
224
236
|
# Internal: Returns a path to an api endpoint
|
@@ -20,18 +20,31 @@ module Restforce
|
|
20
20
|
# Internal: Internal faraday connection where all requests go through
|
21
21
|
def connection
|
22
22
|
@connection ||= Faraday.new(@options[:instance_url]) do |builder|
|
23
|
+
# Parses JSON into Hashie::Mash structures.
|
23
24
|
builder.use Restforce::Middleware::Mashify, self, @options
|
25
|
+
# Handles multipart file uploads for blobs.
|
24
26
|
builder.use Restforce::Middleware::Multipart
|
27
|
+
# Converts the request into JSON.
|
25
28
|
builder.request :json
|
29
|
+
# Handles reauthentication for 403 responses.
|
26
30
|
builder.use authentication_middleware, self, @options if authentication_middleware
|
31
|
+
# Sets the oauth token in the headers.
|
27
32
|
builder.use Restforce::Middleware::Authorization, self, @options
|
33
|
+
# Ensures the instance url is set.
|
28
34
|
builder.use Restforce::Middleware::InstanceURL, self, @options
|
35
|
+
# Parses returned JSON response into a hash.
|
29
36
|
builder.response :json
|
37
|
+
# Caches GET requests.
|
30
38
|
builder.use Restforce::Middleware::Caching, cache, @options if cache
|
39
|
+
# Follows 30x redirects.
|
31
40
|
builder.use FaradayMiddleware::FollowRedirects
|
41
|
+
# Raises errors for 40x responses.
|
32
42
|
builder.use Restforce::Middleware::RaiseError
|
43
|
+
# Log request/responses
|
33
44
|
builder.use Restforce::Middleware::Logger, Restforce.configuration.logger, @options if Restforce.log?
|
45
|
+
# Compress/Decompress the request/response
|
34
46
|
builder.use Restforce::Middleware::Gzip, self, @options
|
47
|
+
|
35
48
|
builder.adapter Faraday.default_adapter
|
36
49
|
end
|
37
50
|
end
|
@@ -4,12 +4,12 @@ module Restforce
|
|
4
4
|
|
5
5
|
# Public: Subscribe to a PushTopic
|
6
6
|
#
|
7
|
-
#
|
8
|
-
# block
|
7
|
+
# channels - The name of the PushTopic channel(s) to subscribe to.
|
8
|
+
# block - A block to run when a new message is received.
|
9
9
|
#
|
10
10
|
# Returns a Faye::Subscription
|
11
|
-
def subscribe(
|
12
|
-
faye.subscribe "/topic/#{channel}", &block
|
11
|
+
def subscribe(channels, &block)
|
12
|
+
faye.subscribe Array(channels).map { |channel| "/topic/#{channel}" }, &block
|
13
13
|
end
|
14
14
|
|
15
15
|
# Public: Faye client to use for subscribing to PushTopics
|
data/lib/restforce/config.rb
CHANGED
@@ -33,16 +33,16 @@ module Restforce
|
|
33
33
|
|
34
34
|
def create_multipart(env, params)
|
35
35
|
boundary = env[:request][:boundary]
|
36
|
-
|
37
36
|
parts = []
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
37
|
+
|
38
|
+
# Fields
|
39
|
+
parts << Faraday::Parts::Part.new(boundary, 'entity_content', params.reject { |k,v| v.respond_to? :content_type }.to_json)
|
40
|
+
|
41
|
+
# Files
|
42
|
+
params.each do |k,v|
|
43
|
+
parts << Faraday::Parts::Part.new(boundary, k.to_s, v) if v.respond_to? :content_type
|
43
44
|
end
|
44
|
-
|
45
|
-
parts.reverse!
|
45
|
+
|
46
46
|
parts << Faraday::Parts::EpiloguePart.new(boundary)
|
47
47
|
|
48
48
|
body = Faraday::CompositeReadIO.new(parts)
|
data/lib/restforce/version.rb
CHANGED
data/restforce.gemspec
CHANGED
@@ -15,14 +15,13 @@ Gem::Specification.new do |gem|
|
|
15
15
|
gem.require_paths = ["lib"]
|
16
16
|
gem.version = Restforce::VERSION
|
17
17
|
|
18
|
-
gem.add_dependency 'rake'
|
19
18
|
gem.add_dependency 'faraday', '~> 0.8.4'
|
20
|
-
gem.add_dependency 'faraday_middleware', '
|
19
|
+
gem.add_dependency 'faraday_middleware', '>= 0.8.8'
|
21
20
|
gem.add_dependency 'json', '~> 1.7.5'
|
22
21
|
gem.add_dependency 'hashie', '~> 1.2.0'
|
23
22
|
|
24
23
|
gem.add_development_dependency 'rspec', '~> 2.12.0'
|
25
24
|
gem.add_development_dependency 'webmock'
|
26
25
|
gem.add_development_dependency 'simplecov'
|
27
|
-
gem.add_development_dependency 'faye'
|
26
|
+
gem.add_development_dependency 'faye' unless RUBY_PLATFORM == 'java'
|
28
27
|
end
|
data/spec/lib/client_spec.rb
CHANGED
@@ -50,31 +50,22 @@ shared_examples_for 'methods' do
|
|
50
50
|
|
51
51
|
context 'without required options for authentication middleware to be provided' do
|
52
52
|
let(:client_options) { {} }
|
53
|
-
|
54
53
|
it { should be_nil }
|
55
54
|
end
|
56
55
|
|
57
56
|
context 'with username, password, security token, client id and client secret provided' do
|
58
57
|
let(:client_options) { password_options }
|
59
|
-
|
60
58
|
it { should eq Restforce::Middleware::Authentication::Password }
|
61
59
|
end
|
62
60
|
|
63
61
|
context 'with refresh token, client id and client secret provided' do
|
64
62
|
let(:client_options) { oauth_options }
|
65
|
-
|
66
63
|
it { should eq Restforce::Middleware::Authentication::Token }
|
67
64
|
end
|
68
65
|
end
|
69
66
|
|
70
67
|
describe '.list_sobjects' do
|
71
|
-
|
72
|
-
@request = stub_api_request :sobjects, :with => 'sobject/describe_sobjects_success_response'
|
73
|
-
end
|
74
|
-
|
75
|
-
after do
|
76
|
-
expect(@request).to have_been_requested
|
77
|
-
end
|
68
|
+
requests :sobjects, :fixture => 'sobject/describe_sobjects_success_response'
|
78
69
|
|
79
70
|
subject { client.list_sobjects }
|
80
71
|
it { should be_an Array }
|
@@ -83,28 +74,14 @@ shared_examples_for 'methods' do
|
|
83
74
|
|
84
75
|
describe '.describe' do
|
85
76
|
context 'with no arguments' do
|
86
|
-
|
87
|
-
@request = stub_api_request :sobjects,
|
88
|
-
:with => 'sobject/describe_sobjects_success_response'
|
89
|
-
end
|
90
|
-
|
91
|
-
after do
|
92
|
-
expect(@request).to have_been_requested
|
93
|
-
end
|
77
|
+
requests :sobjects, :fixture => 'sobject/describe_sobjects_success_response'
|
94
78
|
|
95
79
|
subject { client.describe }
|
96
80
|
it { should be_an Array }
|
97
81
|
end
|
98
82
|
|
99
83
|
context 'with an argument' do
|
100
|
-
|
101
|
-
@request = stub_api_request 'sobjects/Whizbang/describe',
|
102
|
-
:with => 'sobject/sobject_describe_success_response'
|
103
|
-
end
|
104
|
-
|
105
|
-
after do
|
106
|
-
expect(@request).to have_been_requested
|
107
|
-
end
|
84
|
+
requests 'sobjects/Whizbang/describe', :fixture => 'sobject/sobject_describe_success_response'
|
108
85
|
|
109
86
|
subject { client.describe('Whizbang') }
|
110
87
|
its(['name']) { should eq 'Whizbang' }
|
@@ -112,28 +89,14 @@ shared_examples_for 'methods' do
|
|
112
89
|
end
|
113
90
|
|
114
91
|
describe '.query' do
|
115
|
-
|
116
|
-
@request = stub_api_request 'query\?q=SELECT%20some,%20fields%20FROM%20object',
|
117
|
-
:with => 'sobject/query_success_response'
|
118
|
-
end
|
119
|
-
|
120
|
-
after do
|
121
|
-
expect(@request).to have_been_requested
|
122
|
-
end
|
92
|
+
requests 'query\?q=SELECT%20some,%20fields%20FROM%20object', :fixture => 'sobject/query_success_response'
|
123
93
|
|
124
94
|
subject { client.query('SELECT some, fields FROM object') }
|
125
95
|
it { should be_an Array }
|
126
96
|
end
|
127
97
|
|
128
98
|
describe '.search' do
|
129
|
-
|
130
|
-
@request = stub_api_request 'search\?q=FIND%20%7Bbar%7D',
|
131
|
-
:with => 'sobject/search_success_response'
|
132
|
-
end
|
133
|
-
|
134
|
-
after do
|
135
|
-
expect(@request).to have_been_requested
|
136
|
-
end
|
99
|
+
requests 'search\?q=FIND%20%7Bbar%7D', :fixture => 'sobject/search_success_response'
|
137
100
|
|
138
101
|
subject { client.search('FIND {bar}') }
|
139
102
|
it { should be_an Array }
|
@@ -141,14 +104,7 @@ shared_examples_for 'methods' do
|
|
141
104
|
end
|
142
105
|
|
143
106
|
describe '.org_id' do
|
144
|
-
|
145
|
-
@request = stub_api_request 'query\?q=select%20id%20from%20Organization',
|
146
|
-
:with => 'sobject/org_query_response'
|
147
|
-
end
|
148
|
-
|
149
|
-
after do
|
150
|
-
expect(@request).to have_been_requested
|
151
|
-
end
|
107
|
+
requests 'query\?q=select%20id%20from%20Organization', :fixture => 'sobject/org_query_response'
|
152
108
|
|
153
109
|
subject { client.org_id }
|
154
110
|
it { should eq '00Dx0000000BV7z' }
|
@@ -156,32 +112,20 @@ shared_examples_for 'methods' do
|
|
156
112
|
|
157
113
|
describe '.create' do
|
158
114
|
context 'without multipart' do
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
:body => "{\"Name\":\"Foobar\"}"
|
164
|
-
end
|
165
|
-
|
166
|
-
after do
|
167
|
-
expect(@request).to have_been_requested
|
168
|
-
end
|
115
|
+
requests 'sobjects/Account',
|
116
|
+
:method => :post,
|
117
|
+
:with_body => "{\"Name\":\"Foobar\"}",
|
118
|
+
:fixture => 'sobject/create_success_response'
|
169
119
|
|
170
120
|
subject { client.create('Account', :Name => 'Foobar') }
|
171
121
|
it { should eq 'some_id' }
|
172
122
|
end
|
173
123
|
|
174
124
|
context 'with multipart' do
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
: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)
|
180
|
-
end
|
181
|
-
|
182
|
-
after do
|
183
|
-
expect(@request).to have_been_requested
|
184
|
-
end
|
125
|
+
requests 'sobjects/Account',
|
126
|
+
:method => :post,
|
127
|
+
: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),
|
128
|
+
:fixture => 'sobject/create_success_response'
|
185
129
|
|
186
130
|
subject { client.create('Account', :Name => 'Foobar', :Blob => Restforce::UploadIO.new(File.expand_path('../../fixtures/blob.jpg', __FILE__), 'image/jpeg')) }
|
187
131
|
it { should eq 'some_id' }
|
@@ -190,17 +134,11 @@ shared_examples_for 'methods' do
|
|
190
134
|
|
191
135
|
describe '.update!' do
|
192
136
|
context 'with invalid Id' do
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
:status => 404
|
199
|
-
end
|
200
|
-
|
201
|
-
after do
|
202
|
-
expect(@request).to have_been_requested
|
203
|
-
end
|
137
|
+
requests 'sobjects/Account/001D000000INjVe',
|
138
|
+
:method => :patch,
|
139
|
+
:with_body => "{\"Name\":\"Foobar\"}",
|
140
|
+
:status => 404,
|
141
|
+
:fixture => 'sobject/delete_error_response'
|
204
142
|
|
205
143
|
subject { client.update!('Account', :Id => '001D000000INjVe', :Name => 'Foobar') }
|
206
144
|
specify { expect { subject }.to raise_error Faraday::Error::ResourceNotFound }
|
@@ -214,61 +152,35 @@ shared_examples_for 'methods' do
|
|
214
152
|
end
|
215
153
|
|
216
154
|
context 'with invalid Id' do
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
:status => 404
|
223
|
-
end
|
224
|
-
|
225
|
-
after do
|
226
|
-
expect(@request).to have_been_requested
|
227
|
-
end
|
155
|
+
requests 'sobjects/Account/001D000000INjVe',
|
156
|
+
:method => :patch,
|
157
|
+
:with_body => "{\"Name\":\"Foobar\"}",
|
158
|
+
:status => 404,
|
159
|
+
:fixture => 'sobject/delete_error_response'
|
228
160
|
|
229
161
|
subject { client.update('Account', :Id => '001D000000INjVe', :Name => 'Foobar') }
|
230
162
|
it { should be_false }
|
231
163
|
end
|
232
164
|
|
233
165
|
context 'with success' do
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
context 'with symbol Id key' do
|
245
|
-
subject { client.update('Account', :Id => '001D000000INjVe', :Name => 'Foobar') }
|
246
|
-
it { should be_true }
|
247
|
-
end
|
248
|
-
|
249
|
-
context 'with string Id key' do
|
250
|
-
subject { client.update('Account', 'Id' => '001D000000INjVe', 'Name' => 'Foobar') }
|
251
|
-
it { should be_true }
|
252
|
-
end
|
253
|
-
|
254
|
-
context 'with a lower case id' do
|
255
|
-
subject { client.update('Account', 'id' => '001D000000INjVe', 'Name' => 'Foobar') }
|
256
|
-
it { should be_true }
|
166
|
+
requests 'sobjects/Account/001D000000INjVe',
|
167
|
+
:method => :patch,
|
168
|
+
:with_body => "{\"Name\":\"Foobar\"}"
|
169
|
+
|
170
|
+
[:Id, :id, 'Id', 'id'].each do |key|
|
171
|
+
context "with #{key.inspect} as the key" do
|
172
|
+
subject { client.update('Account', key => '001D000000INjVe', :Name => 'Foobar') }
|
173
|
+
it { should be_true }
|
174
|
+
end
|
257
175
|
end
|
258
176
|
end
|
259
177
|
end
|
260
178
|
|
261
179
|
describe '.upsert!' do
|
262
180
|
context 'when updated' do
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
:body => "{\"Name\":\"Foobar\"}"
|
267
|
-
end
|
268
|
-
|
269
|
-
after do
|
270
|
-
expect(@request).to have_been_requested
|
271
|
-
end
|
181
|
+
requests 'sobjects/Account/External__c/foobar',
|
182
|
+
:method => :patch,
|
183
|
+
:with_body => "{\"Name\":\"Foobar\"}"
|
272
184
|
|
273
185
|
context 'with symbol external Id key' do
|
274
186
|
subject { client.upsert!('Account', 'External__c', :External__c => 'foobar', :Name => 'Foobar') }
|
@@ -282,25 +194,16 @@ shared_examples_for 'methods' do
|
|
282
194
|
end
|
283
195
|
|
284
196
|
context 'when created' do
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
context 'with symbol external Id key' do
|
297
|
-
subject { client.upsert!('Account', 'External__c', :External__c => 'foobar', :Name => 'Foobar') }
|
298
|
-
it { should eq 'foo' }
|
299
|
-
end
|
300
|
-
|
301
|
-
context 'with string external Id key' do
|
302
|
-
subject { client.upsert!('Account', 'External__c', 'External__c' => 'foobar', 'Name' => 'Foobar') }
|
303
|
-
it { should eq 'foo' }
|
197
|
+
requests 'sobjects/Account/External__c/foobar',
|
198
|
+
:method => :patch,
|
199
|
+
:with_body => "{\"Name\":\"Foobar\"}",
|
200
|
+
:fixture => 'sobject/upsert_created_success_response'
|
201
|
+
|
202
|
+
[:External__c, 'External__c', :external__c, 'external__c'].each do |key|
|
203
|
+
context "with #{key.inspect} as the external id" do
|
204
|
+
subject { client.upsert!('Account', 'External__c', key => 'foobar', :Name => 'Foobar') }
|
205
|
+
it { should eq 'foo' }
|
206
|
+
end
|
304
207
|
end
|
305
208
|
end
|
306
209
|
end
|
@@ -309,28 +212,16 @@ shared_examples_for 'methods' do
|
|
309
212
|
subject { client.destroy!('Account', '001D000000INjVe') }
|
310
213
|
|
311
214
|
context 'with invalid Id' do
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
:status => 404
|
317
|
-
end
|
318
|
-
|
319
|
-
after do
|
320
|
-
expect(@request).to have_been_requested
|
321
|
-
end
|
215
|
+
requests 'sobjects/Account/001D000000INjVe',
|
216
|
+
:fixture => 'sobject/delete_error_response',
|
217
|
+
:method => :delete,
|
218
|
+
:status => 404
|
322
219
|
|
323
220
|
specify { expect { subject }.to raise_error Faraday::Error::ResourceNotFound }
|
324
221
|
end
|
325
222
|
|
326
223
|
context 'with success' do
|
327
|
-
|
328
|
-
@request = stub_api_request 'sobjects/Account/001D000000INjVe', :method => :delete
|
329
|
-
end
|
330
|
-
|
331
|
-
after do
|
332
|
-
expect(@request).to have_been_requested
|
333
|
-
end
|
224
|
+
requests 'sobjects/Account/001D000000INjVe', :method => :delete
|
334
225
|
|
335
226
|
it { should be_true }
|
336
227
|
end
|
@@ -340,36 +231,42 @@ shared_examples_for 'methods' do
|
|
340
231
|
subject { client.destroy('Account', '001D000000INjVe') }
|
341
232
|
|
342
233
|
context 'with invalid Id' do
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
:status => 404
|
348
|
-
end
|
349
|
-
|
350
|
-
after do
|
351
|
-
expect(@request).to have_been_requested
|
352
|
-
end
|
234
|
+
requests 'sobjects/Account/001D000000INjVe',
|
235
|
+
:fixture => 'sobject/delete_error_response',
|
236
|
+
:method => :delete,
|
237
|
+
:status => 404
|
353
238
|
|
354
239
|
it { should be_false }
|
355
240
|
end
|
356
241
|
|
357
242
|
context 'with success' do
|
358
|
-
|
359
|
-
@request = stub_api_request 'sobjects/Account/001D000000INjVe', :method => :delete
|
360
|
-
end
|
361
|
-
|
362
|
-
after do
|
363
|
-
expect(@request).to have_been_requested
|
364
|
-
end
|
243
|
+
requests 'sobjects/Account/001D000000INjVe', :method => :delete
|
365
244
|
|
366
245
|
it { should be_true }
|
367
246
|
end
|
368
247
|
end
|
369
248
|
|
249
|
+
describe '.find' do
|
250
|
+
context 'with no external id passed' do
|
251
|
+
requests 'sobjects/Account/001D000000INjVe',
|
252
|
+
:fixture => 'sobject/sobject_find_success_response'
|
253
|
+
|
254
|
+
subject { client.find('Account', '001D000000INjVe') }
|
255
|
+
it { should be_a Hash }
|
256
|
+
end
|
257
|
+
|
258
|
+
context 'when an external id is passed' do
|
259
|
+
requests 'sobjects/Account/External_Field__c/1234',
|
260
|
+
:fixture => 'sobject/sobject_find_success_response'
|
261
|
+
|
262
|
+
subject { client.find('Account', '1234', 'External_Field__c') }
|
263
|
+
it { should be_a Hash }
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
370
267
|
describe '.authenticate!' do
|
371
268
|
before do
|
372
|
-
@request = stub_login_request(:
|
269
|
+
@request = stub_login_request(:with_body => "grant_type=password&client_id=client_id&client_secret=" \
|
373
270
|
"client_secret&username=foo&password=barsecurity_token").
|
374
271
|
to_return(:status => 200, :body => fixture(:auth_success_response))
|
375
272
|
end
|
@@ -424,45 +321,69 @@ shared_examples_for 'methods' do
|
|
424
321
|
end
|
425
322
|
|
426
323
|
describe '.without_caching' do
|
427
|
-
|
324
|
+
requests 'query\?q=SELECT%20some,%20fields%20FROM%20object',
|
325
|
+
:fixture => 'sobject/query_success_response'
|
428
326
|
|
429
327
|
before do
|
430
|
-
@request = stub_api_request 'query\?q=SELECT%20some,%20fields%20FROM%20object',
|
431
|
-
:with => 'sobject/query_success_response'
|
432
328
|
cache.should_receive(:delete).and_call_original
|
433
329
|
cache.should_receive(:fetch).and_call_original
|
434
330
|
end
|
435
331
|
|
436
|
-
|
437
|
-
expect(@request).to have_been_requested
|
438
|
-
end
|
439
|
-
|
332
|
+
let(:cache) { MockCache.new }
|
440
333
|
subject { client.without_caching { client.query('SELECT some, fields FROM object') } }
|
441
334
|
it { should be_an Array }
|
442
335
|
end
|
443
336
|
|
444
|
-
|
445
|
-
|
337
|
+
unless RUBY_PLATFORM == 'java'
|
338
|
+
describe '.faye', :eventmachine => true do
|
339
|
+
subject { client.faye }
|
446
340
|
|
447
|
-
|
448
|
-
|
449
|
-
|
341
|
+
context 'with missing instance url' do
|
342
|
+
let(:instance_url) { nil }
|
343
|
+
specify { expect { subject }.to raise_error RuntimeError, 'Instance URL missing. Call .authenticate! first.' }
|
344
|
+
end
|
345
|
+
|
346
|
+
context 'with oauth token and instance url' do
|
347
|
+
let(:instance_url) { 'http://google.com' }
|
348
|
+
let(:oauth_token) { 'bar' }
|
349
|
+
specify { expect { subject }.to_not raise_error }
|
350
|
+
end
|
351
|
+
|
352
|
+
context 'when the connection goes down' do
|
353
|
+
it 'should reauthenticate' do
|
354
|
+
access_token = double('access token')
|
355
|
+
access_token.stub(:access_token).and_return('token')
|
356
|
+
client.should_receive(:authenticate!).and_return(access_token)
|
357
|
+
client.faye.should_receive(:set_header).with('Authorization', "OAuth token")
|
358
|
+
client.faye.trigger('transport:down')
|
359
|
+
end
|
360
|
+
end
|
450
361
|
end
|
451
362
|
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
363
|
+
describe '.subcribe', :eventmachine => true do
|
364
|
+
context 'when given a single pushtopic' do
|
365
|
+
it 'subscribes to the pushtopic' do
|
366
|
+
client.faye.should_receive(:subscribe).with(['/topic/PushTopic'])
|
367
|
+
client.subscribe('PushTopic')
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
context 'when given an array of pushtopics' do
|
372
|
+
it 'subscribes to each pushtopic' do
|
373
|
+
client.faye.should_receive(:subscribe).with(['/topic/PushTopic1', '/topic/PushTopic2'])
|
374
|
+
client.subscribe(['PushTopic1', 'PushTopic2'])
|
375
|
+
end
|
376
|
+
end
|
456
377
|
end
|
457
|
-
end
|
378
|
+
end
|
458
379
|
|
459
380
|
describe 'authentication retries' do
|
460
381
|
context 'when retries reaches 0' do
|
461
382
|
before do
|
462
383
|
@auth_request = stub_api_request('query\?q=SELECT%20some,%20fields%20FROM%20object',
|
463
384
|
:status => 401,
|
464
|
-
:
|
465
|
-
@query_request = stub_login_request(:
|
385
|
+
:fixture => 'expired_session_response')
|
386
|
+
@query_request = stub_login_request(:with_body => "grant_type=password&client_id=client_id&client_secret=" \
|
466
387
|
"client_secret&username=foo&password=barsecurity_token").
|
467
388
|
to_return(:status => 200, :body => fixture(:auth_success_response))
|
468
389
|
end
|
@@ -481,7 +402,7 @@ shared_examples_for 'methods' do
|
|
481
402
|
to_return(:status => 401, :body => fixture('expired_session_response')).then.
|
482
403
|
to_return(:status => 200, :body => fixture('sobject/query_success_response'))
|
483
404
|
|
484
|
-
@login = stub_login_request(:
|
405
|
+
@login = stub_login_request(:with_body => "grant_type=password&client_id=client_id&client_secret=" \
|
485
406
|
"client_secret&username=foo&password=barsecurity_token").
|
486
407
|
to_return(:status => 200, :body => fixture(:auth_success_response))
|
487
408
|
end
|
@@ -509,16 +430,8 @@ describe 'with mashify middleware' do
|
|
509
430
|
|
510
431
|
describe '.query' do
|
511
432
|
context 'with pagination' do
|
512
|
-
|
513
|
-
|
514
|
-
requests << stub_api_request('query\?q', :with => 'sobject/query_paginated_first_page_response')
|
515
|
-
requests << stub_api_request('query/01gD', :with => 'sobject/query_paginated_last_page_response')
|
516
|
-
end
|
517
|
-
end
|
518
|
-
|
519
|
-
after do
|
520
|
-
@requests.each { |request| expect(request).to have_been_requested }
|
521
|
-
end
|
433
|
+
requests 'query\?q', :fixture => 'sobject/query_paginated_first_page_response'
|
434
|
+
requests 'query/01gD', :fixture => 'sobject/query_paginated_last_page_response'
|
522
435
|
|
523
436
|
subject { client.query('SELECT some, fields FROM object').next_page }
|
524
437
|
it { should be_a Restforce::Collection }
|
data/spec/lib/collection_spec.rb
CHANGED
@@ -18,11 +18,8 @@ describe Restforce::Collection do
|
|
18
18
|
specify { expect(subject.instance_variable_get(:@client)).to eq client }
|
19
19
|
|
20
20
|
describe 'each record' do
|
21
|
-
|
22
|
-
|
23
|
-
expect(record).to be_a Restforce::SObject
|
24
|
-
end
|
25
|
-
end
|
21
|
+
subject { records }
|
22
|
+
it { should be_all { |record| expect(record).to be_a Restforce::SObject } }
|
26
23
|
end
|
27
24
|
end
|
28
25
|
|
@@ -34,7 +31,7 @@ describe Restforce::Collection do
|
|
34
31
|
it { should respond_to :each }
|
35
32
|
its(:size) { should eq 1 }
|
36
33
|
its(:total_size) { should eq 2 }
|
37
|
-
its(:next_page_url) { should eq
|
34
|
+
its(:next_page_url) { should eq "/services/data/v#{Restforce.configuration.api_version}/query/01gD" }
|
38
35
|
specify { expect(subject.instance_variable_get(:@client)).to eq client }
|
39
36
|
|
40
37
|
describe '.next_page' do
|
data/spec/lib/config_spec.rb
CHANGED
@@ -11,7 +11,7 @@ describe Restforce do
|
|
11
11
|
it { should be_a Restforce::Configuration }
|
12
12
|
|
13
13
|
context 'by default' do
|
14
|
-
its(:api_version) { should eq '
|
14
|
+
its(:api_version) { should eq '26.0' }
|
15
15
|
its(:host) { should eq 'login.salesforce.com' }
|
16
16
|
its(:authentication_retries) { should eq 3 }
|
17
17
|
[:username, :password, :security_token, :client_id, :client_secret,
|
data/spec/lib/sobject_spec.rb
CHANGED
@@ -53,16 +53,13 @@ describe Restforce::SObject do
|
|
53
53
|
end
|
54
54
|
|
55
55
|
context 'when an Id is present' do
|
56
|
+
requests 'sobjects/Whizbang/001D000000INjVe',
|
57
|
+
:method => :patch,
|
58
|
+
:with_body => "{\"Checkbox_Label\":false,\"Text_Label\":\"Hi there!\",\"Date_Label\":\"2010-01-01\"," +
|
59
|
+
"\"DateTime_Label\":\"2011-07-07T00:37:00.000+0000\",\"Picklist_Multiselect_Label\":\"four;six\"}"
|
60
|
+
|
56
61
|
before do
|
57
62
|
hash.merge!(:Id => '001D000000INjVe')
|
58
|
-
@request = stub_api_request 'sobjects/Whizbang/001D000000INjVe',
|
59
|
-
:method => :patch,
|
60
|
-
:body => "{\"Checkbox_Label\":false,\"Text_Label\":\"Hi there!\",\"Date_Label\":\"2010-01-01\"," +
|
61
|
-
"\"DateTime_Label\":\"2011-07-07T00:37:00.000+0000\",\"Picklist_Multiselect_Label\":\"four;six\"}"
|
62
|
-
end
|
63
|
-
|
64
|
-
after do
|
65
|
-
expect(@request).to have_been_requested
|
66
63
|
end
|
67
64
|
|
68
65
|
specify { expect { subject }.to_not raise_error }
|
@@ -73,16 +70,13 @@ describe Restforce::SObject do
|
|
73
70
|
subject { sobject.save! }
|
74
71
|
|
75
72
|
context 'when an exception is raised' do
|
73
|
+
requests 'sobjects/Whizbang/001D000000INjVe',
|
74
|
+
:fixture => 'sobject/delete_error_response',
|
75
|
+
:method => :patch,
|
76
|
+
:status => 404
|
77
|
+
|
76
78
|
before do
|
77
79
|
hash.merge!(:Id => '001D000000INjVe')
|
78
|
-
@request = stub_api_request 'sobjects/Whizbang/001D000000INjVe',
|
79
|
-
:with => 'sobject/delete_error_response',
|
80
|
-
:method => :patch,
|
81
|
-
:status => 404
|
82
|
-
end
|
83
|
-
|
84
|
-
after do
|
85
|
-
expect(@request).to have_been_requested
|
86
80
|
end
|
87
81
|
|
88
82
|
specify { expect { subject }.to raise_error Faraday::Error::ResourceNotFound }
|
@@ -97,13 +91,10 @@ describe Restforce::SObject do
|
|
97
91
|
end
|
98
92
|
|
99
93
|
context 'when an Id is present' do
|
94
|
+
requests 'sobjects/Whizbang/001D000000INjVe', :method => :delete
|
95
|
+
|
100
96
|
before do
|
101
97
|
hash.merge!(:Id => '001D000000INjVe')
|
102
|
-
@request = stub_api_request 'sobjects/Whizbang/001D000000INjVe', :method => :delete
|
103
|
-
end
|
104
|
-
|
105
|
-
after do
|
106
|
-
expect(@request).to have_been_requested
|
107
98
|
end
|
108
99
|
|
109
100
|
specify { expect { subject }.to_not raise_error }
|
@@ -114,16 +105,13 @@ describe Restforce::SObject do
|
|
114
105
|
subject { sobject.destroy! }
|
115
106
|
|
116
107
|
context 'when an exception is raised' do
|
108
|
+
requests 'sobjects/Whizbang/001D000000INjVe',
|
109
|
+
:fixture => 'sobject/delete_error_response',
|
110
|
+
:method => :delete,
|
111
|
+
:status => 404
|
112
|
+
|
117
113
|
before do
|
118
114
|
hash.merge!(:Id => '001D000000INjVe')
|
119
|
-
@request = stub_api_request 'sobjects/Whizbang/001D000000INjVe',
|
120
|
-
:with => 'sobject/delete_error_response',
|
121
|
-
:method => :delete,
|
122
|
-
:status => 404
|
123
|
-
end
|
124
|
-
|
125
|
-
after do
|
126
|
-
expect(@request).to have_been_requested
|
127
115
|
end
|
128
116
|
|
129
117
|
specify { expect { subject }.to raise_error Faraday::Error::ResourceNotFound }
|
@@ -131,14 +119,8 @@ describe Restforce::SObject do
|
|
131
119
|
end
|
132
120
|
|
133
121
|
describe '.describe' do
|
134
|
-
|
135
|
-
|
136
|
-
:with => 'sobject/sobject_describe_success_response'
|
137
|
-
end
|
138
|
-
|
139
|
-
after do
|
140
|
-
expect(@request).to have_been_requested
|
141
|
-
end
|
122
|
+
requests 'sobjects/Whizbang/describe',
|
123
|
+
:fixture => 'sobject/sobject_describe_success_response'
|
142
124
|
|
143
125
|
subject { sobject.describe }
|
144
126
|
it { should be_a Hash }
|
data/spec/spec_helper.rb
CHANGED
@@ -14,5 +14,11 @@ WebMock.disable_net_connect!
|
|
14
14
|
Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each {|f| require f}
|
15
15
|
|
16
16
|
RSpec.configure do |config|
|
17
|
-
config.
|
17
|
+
config.around :eventmachine => true do |example|
|
18
|
+
EM.run {
|
19
|
+
example.run
|
20
|
+
EM.stop
|
21
|
+
}
|
22
|
+
end
|
18
23
|
end
|
24
|
+
|
@@ -1,31 +1,45 @@
|
|
1
1
|
module FixtureHelpers
|
2
|
+
module InstanceMethods
|
2
3
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
4
|
+
def stub_api_request(endpoint, options={})
|
5
|
+
options = {
|
6
|
+
:method => :get,
|
7
|
+
:status => 200,
|
8
|
+
:api_version => Restforce.configuration.api_version
|
9
|
+
}.merge(options)
|
10
|
+
|
11
|
+
stub = stub_request(options[:method], %r{/services/data/v#{options[:api_version]}/#{endpoint}})
|
12
|
+
stub = stub.with(:body => options[:with_body]) if options[:with_body] && !RUBY_VERSION.match(/^1.8/)
|
13
|
+
stub = stub.to_return(:status => options[:status], :body => fixture(options[:fixture])) if options[:fixture]
|
14
|
+
stub
|
15
|
+
end
|
16
|
+
|
17
|
+
def stub_login_request(options={})
|
18
|
+
stub = stub_request(:post, "https://login.salesforce.com/services/oauth2/token")
|
19
|
+
stub = stub.with(:body => options[:with_body]) if options[:with_body] && !RUBY_VERSION.match(/^1.8/)
|
20
|
+
stub
|
21
|
+
end
|
16
22
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
}.merge(options)
|
23
|
+
def fixture(f)
|
24
|
+
File.read(File.expand_path("../../fixtures/#{f}.json", __FILE__))
|
25
|
+
end
|
21
26
|
|
22
|
-
stub = stub_request(:post, "https://login.salesforce.com/services/oauth2/token")
|
23
|
-
stub = stub.with(:body => options[:body]) if options[:body] && !RUBY_VERSION.match(/^1.8/)
|
24
|
-
stub
|
25
27
|
end
|
26
28
|
|
27
|
-
|
28
|
-
|
29
|
+
module ClassMethods
|
30
|
+
def requests(endpoint, options={})
|
31
|
+
before do
|
32
|
+
(@requests ||= []) << stub_api_request(endpoint, options)
|
33
|
+
end
|
34
|
+
|
35
|
+
after do
|
36
|
+
@requests.each { |request| expect(request).to have_been_requested }
|
37
|
+
end
|
38
|
+
end
|
29
39
|
end
|
40
|
+
end
|
30
41
|
|
42
|
+
RSpec.configure do |config|
|
43
|
+
config.include FixtureHelpers::InstanceMethods
|
44
|
+
config.extend FixtureHelpers::ClassMethods
|
31
45
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: restforce
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,24 +9,8 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-12-
|
12
|
+
date: 2012-12-23 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
-
- !ruby/object:Gem::Dependency
|
15
|
-
name: rake
|
16
|
-
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
|
-
requirements:
|
19
|
-
- - ! '>='
|
20
|
-
- !ruby/object:Gem::Version
|
21
|
-
version: '0'
|
22
|
-
type: :runtime
|
23
|
-
prerelease: false
|
24
|
-
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
|
-
requirements:
|
27
|
-
- - ! '>='
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
version: '0'
|
30
14
|
- !ruby/object:Gem::Dependency
|
31
15
|
name: faraday
|
32
16
|
requirement: !ruby/object:Gem::Requirement
|
@@ -48,7 +32,7 @@ dependencies:
|
|
48
32
|
requirement: !ruby/object:Gem::Requirement
|
49
33
|
none: false
|
50
34
|
requirements:
|
51
|
-
- -
|
35
|
+
- - ! '>='
|
52
36
|
- !ruby/object:Gem::Version
|
53
37
|
version: 0.8.8
|
54
38
|
type: :runtime
|
@@ -56,7 +40,7 @@ dependencies:
|
|
56
40
|
version_requirements: !ruby/object:Gem::Requirement
|
57
41
|
none: false
|
58
42
|
requirements:
|
59
|
-
- -
|
43
|
+
- - ! '>='
|
60
44
|
- !ruby/object:Gem::Version
|
61
45
|
version: 0.8.8
|
62
46
|
- !ruby/object:Gem::Dependency
|
@@ -144,17 +128,17 @@ dependencies:
|
|
144
128
|
requirement: !ruby/object:Gem::Requirement
|
145
129
|
none: false
|
146
130
|
requirements:
|
147
|
-
- - '
|
131
|
+
- - ! '>='
|
148
132
|
- !ruby/object:Gem::Version
|
149
|
-
version: 0
|
133
|
+
version: '0'
|
150
134
|
type: :development
|
151
135
|
prerelease: false
|
152
136
|
version_requirements: !ruby/object:Gem::Requirement
|
153
137
|
none: false
|
154
138
|
requirements:
|
155
|
-
- - '
|
139
|
+
- - ! '>='
|
156
140
|
- !ruby/object:Gem::Version
|
157
|
-
version: 0
|
141
|
+
version: '0'
|
158
142
|
description: A lightweight ruby client for the Salesforce REST api.
|
159
143
|
email:
|
160
144
|
- eric@ejholmes.net
|