restforce 0.1.1 → 0.1.2
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/README.md +20 -2
- data/lib/restforce.rb +0 -1
- data/lib/restforce/client.rb +13 -10
- data/lib/restforce/middleware/authentication.rb +16 -21
- data/lib/restforce/middleware/authentication/password.rb +6 -13
- data/lib/restforce/middleware/authentication/token.rb +5 -12
- data/lib/restforce/middleware/instance_url.rb +4 -13
- data/lib/restforce/sobject.rb +6 -3
- data/lib/restforce/version.rb +1 -1
- data/spec/lib/client_spec.rb +59 -0
- data/spec/lib/middleware/authentication_spec.rb +2 -32
- data/spec/lib/middleware/instance_url_spec.rb +9 -26
- metadata +2 -2
data/README.md
CHANGED
@@ -197,8 +197,6 @@ _See also: http://www.salesforce.com/us/developer/docs/api_rest/Content/dome_ups
|
|
197
197
|
|
198
198
|
### destroy(sobject, id)
|
199
199
|
|
200
|
-
_Alias: delete_
|
201
|
-
|
202
200
|
Takes an sobject name and an Id and deletes the record. Returns true if the
|
203
201
|
record was successfully deleted.
|
204
202
|
|
@@ -231,6 +229,26 @@ _See also: http://www.salesforce.com/us/developer/docs/api_rest/Content/dome_des
|
|
231
229
|
|
232
230
|
* * *
|
233
231
|
|
232
|
+
### authenticate!
|
233
|
+
|
234
|
+
Performs an authentication and returns the response. In general, calling this
|
235
|
+
directly shouldn't be required, since the client will handle authentication for
|
236
|
+
you automatically. This should only be used if you want to force
|
237
|
+
an authentication before using the streaming api, or you want to get some
|
238
|
+
information about the user.
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
response = client.authenticate!
|
242
|
+
# => #<Restforce::Mash access_token="..." id="https://login.salesforce.com/id/00DE0000000cOGcMAM/005E0000001eM4LIAU" instance_url="https://na9.salesforce.com" issued_at="1348465359751" scope="api refresh_token" signature="3fW0pC/TEY2cjK5FCBFOZdjRtCfAuEbK1U74H/eF+Ho=">
|
243
|
+
|
244
|
+
# Get the user information
|
245
|
+
info = client.get(response.id).body
|
246
|
+
info.user_id
|
247
|
+
# => '005E0000001eM4LIAU'
|
248
|
+
```
|
249
|
+
|
250
|
+
* * *
|
251
|
+
|
234
252
|
### File Uploads
|
235
253
|
|
236
254
|
Using the new [Blob Data](http://www.salesforce.com/us/developer/docs/api_rest/Content/dome_sobject_insert_update_blob.htm) api feature (500mb limit):
|
data/lib/restforce.rb
CHANGED
data/lib/restforce/client.rb
CHANGED
@@ -243,7 +243,6 @@ module Restforce
|
|
243
243
|
rescue *exceptions
|
244
244
|
false
|
245
245
|
end
|
246
|
-
alias_method :delete, :destroy
|
247
246
|
|
248
247
|
# See .destroy
|
249
248
|
#
|
@@ -253,7 +252,6 @@ module Restforce
|
|
253
252
|
api_delete "sobjects/#{sobject}/#{id}"
|
254
253
|
true
|
255
254
|
end
|
256
|
-
alias_method :delete!, :destroy!
|
257
255
|
|
258
256
|
# Public: Runs the block with caching disabled.
|
259
257
|
#
|
@@ -279,10 +277,9 @@ module Restforce
|
|
279
277
|
|
280
278
|
# Public: Force an authentication
|
281
279
|
def authenticate!
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
connection.headers.delete('X-ForceAuthenticate')
|
280
|
+
raise 'No authentication middleware present' unless authentication_middleware
|
281
|
+
middleware = authentication_middleware.new nil, self, @options
|
282
|
+
middleware.authenticate!
|
286
283
|
end
|
287
284
|
|
288
285
|
# Public: Decodes a signed request received from Force.com Canvas.
|
@@ -322,11 +319,16 @@ module Restforce
|
|
322
319
|
# Returns the Faraday::Response.
|
323
320
|
[:get, :post, :put, :delete, :patch].each do |method|
|
324
321
|
define_method method do |*args|
|
322
|
+
retries = @options[:authentication_retries]
|
325
323
|
begin
|
326
324
|
connection.send(method, *args)
|
327
|
-
rescue Restforce::
|
328
|
-
|
329
|
-
|
325
|
+
rescue Restforce::UnauthorizedError
|
326
|
+
if retries > 0
|
327
|
+
retries -= 1
|
328
|
+
connection.url_prefix = @options[:instance_url]
|
329
|
+
retry
|
330
|
+
end
|
331
|
+
raise
|
330
332
|
end
|
331
333
|
end
|
332
334
|
|
@@ -350,7 +352,7 @@ module Restforce
|
|
350
352
|
|
351
353
|
# Internal: Internal faraday connection where all requests go through
|
352
354
|
def connection
|
353
|
-
@connection ||= Faraday.new do |builder|
|
355
|
+
@connection ||= Faraday.new(@options[:instance_url]) do |builder|
|
354
356
|
builder.use Restforce::Middleware::Mashify, self, @options
|
355
357
|
builder.use Restforce::Middleware::Multipart
|
356
358
|
builder.request :json
|
@@ -360,6 +362,7 @@ module Restforce
|
|
360
362
|
builder.use Restforce::Middleware::RaiseError
|
361
363
|
builder.response :json
|
362
364
|
builder.use Restforce::Middleware::Caching, cache, @options if cache
|
365
|
+
builder.use FaradayMiddleware::FollowRedirects
|
363
366
|
builder.use Restforce::Middleware::Logger, Restforce.configuration.logger, @options if Restforce.log?
|
364
367
|
builder.adapter Faraday.default_adapter
|
365
368
|
end
|
@@ -7,40 +7,35 @@ module Restforce
|
|
7
7
|
class Middleware::Authentication < Restforce::Middleware
|
8
8
|
|
9
9
|
def call(env)
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
return authenticate! if force_authenticate?(env)
|
15
|
-
@app.call(env)
|
16
|
-
rescue Restforce::UnauthorizedError
|
17
|
-
if retries > 0
|
18
|
-
authenticate!
|
19
|
-
env[:body] = request_body
|
20
|
-
env[:request] = request
|
21
|
-
retries -= 1
|
22
|
-
retry
|
23
|
-
end
|
24
|
-
raise
|
25
|
-
end
|
10
|
+
@app.call(env)
|
11
|
+
rescue Restforce::UnauthorizedError
|
12
|
+
authenticate!
|
13
|
+
raise
|
26
14
|
end
|
27
15
|
|
28
16
|
def authenticate!
|
29
|
-
|
17
|
+
response = connection.post '/services/oauth2/token' do |req|
|
18
|
+
req.body = URI.encode_www_form params
|
19
|
+
end
|
20
|
+
raise Restforce::AuthenticationError, error_message(response) if response.status != 200
|
21
|
+
@options[:instance_url] = response.body['instance_url']
|
22
|
+
@options[:oauth_token] = response.body['access_token']
|
23
|
+
response.body
|
24
|
+
end
|
25
|
+
|
26
|
+
def params
|
27
|
+
raise 'not implemented'
|
30
28
|
end
|
31
29
|
|
32
30
|
def connection
|
33
31
|
@connection ||= Faraday.new(:url => "https://#{@options[:host]}") do |builder|
|
32
|
+
builder.use Restforce::Middleware::Mashify, nil, @options
|
34
33
|
builder.response :json
|
35
34
|
builder.use Restforce::Middleware::Logger, Restforce.configuration.logger, @options if Restforce.log?
|
36
35
|
builder.adapter Faraday.default_adapter
|
37
36
|
end
|
38
37
|
end
|
39
38
|
|
40
|
-
def force_authenticate?(env)
|
41
|
-
env[:request_headers] && env[:request_headers]['X-ForceAuthenticate']
|
42
|
-
end
|
43
|
-
|
44
39
|
def error_message(response)
|
45
40
|
"#{response.body['error']}: #{response.body['error_description']}"
|
46
41
|
end
|
@@ -3,19 +3,12 @@ module Restforce
|
|
3
3
|
# Authentication middleware used if username and password flow is used
|
4
4
|
class Middleware::Authentication::Password < Restforce::Middleware::Authentication
|
5
5
|
|
6
|
-
def
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
:username => @options[:username],
|
13
|
-
:password => password
|
14
|
-
)
|
15
|
-
end
|
16
|
-
raise Restforce::AuthenticationError, error_message(response) if response.status != 200
|
17
|
-
@options[:instance_url] = response.body['instance_url']
|
18
|
-
@options[:oauth_token] = response.body['access_token']
|
6
|
+
def params
|
7
|
+
{ :grant_type => 'password',
|
8
|
+
:client_id => @options[:client_id],
|
9
|
+
:client_secret => @options[:client_secret],
|
10
|
+
:username => @options[:username],
|
11
|
+
:password => password }
|
19
12
|
end
|
20
13
|
|
21
14
|
def password
|
@@ -3,18 +3,11 @@ module Restforce
|
|
3
3
|
# Authentication middleware used if oauth_token and refresh_token are set
|
4
4
|
class Middleware::Authentication::Token < Restforce::Middleware::Authentication
|
5
5
|
|
6
|
-
def
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
:client_id => @options[:client_id],
|
12
|
-
:client_secret => @options[:client_secret]
|
13
|
-
)
|
14
|
-
end
|
15
|
-
raise Restforce::AuthenticationError, error_message(response) if response.status != 200
|
16
|
-
@options[:instance_url] = response.body['instance_url']
|
17
|
-
@options[:oauth_token] = response.body['access_token']
|
6
|
+
def params
|
7
|
+
{ :grant_type => 'refresh_token',
|
8
|
+
:refresh_token => @options[:refresh_token],
|
9
|
+
:client_id => @options[:client_id],
|
10
|
+
:client_secret => @options[:client_secret] }
|
18
11
|
end
|
19
12
|
|
20
13
|
end
|
@@ -4,22 +4,13 @@ module Restforce
|
|
4
4
|
class Middleware::InstanceURL < Restforce::Middleware
|
5
5
|
|
6
6
|
def call(env)
|
7
|
-
# If the
|
8
|
-
|
9
|
-
raise Restforce::UnauthorizedError, 'instance url not set' unless @options[:instance_url]
|
10
|
-
|
11
|
-
# If the url_prefix for the connection doesn't match the instance_url
|
12
|
-
# set in the options, we raise an error which gets caught outside of
|
13
|
-
# middleware, where the url_prefix is then set before retrying the
|
14
|
-
# request. It would be ideal if this could all be handled in
|
15
|
-
# middleware...
|
16
|
-
raise Restforce::InstanceURLError unless connection.url_prefix == instance_url
|
17
|
-
|
7
|
+
# If the connection url_prefix isn't set, we must not be authenticated.
|
8
|
+
raise Restforce::UnauthorizedError, 'Connection prefix not set' unless url_prefix_set?
|
18
9
|
@app.call(env)
|
19
10
|
end
|
20
11
|
|
21
|
-
def
|
22
|
-
|
12
|
+
def url_prefix_set?
|
13
|
+
!!(connection.url_prefix && connection.url_prefix.host)
|
23
14
|
end
|
24
15
|
|
25
16
|
end
|
data/lib/restforce/sobject.rb
CHANGED
@@ -18,10 +18,7 @@ module Restforce
|
|
18
18
|
# account.Name = 'Foobar'
|
19
19
|
# account.save
|
20
20
|
def save
|
21
|
-
# Remove 'attributes' and parent/child relationships. We only want to
|
22
|
-
# persist the attributes on the sobject.
|
23
21
|
ensure_id
|
24
|
-
attrs = self.to_hash.reject { |key, _| key =~ /.*__r/ || key =~ /^attributes$/ }
|
25
22
|
@client.update(sobject_type, attrs)
|
26
23
|
end
|
27
24
|
|
@@ -36,6 +33,12 @@ module Restforce
|
|
36
33
|
@client.destroy(sobject_type, self.Id)
|
37
34
|
end
|
38
35
|
|
36
|
+
# Public: Returns a hash representation of this object with the attributes
|
37
|
+
# key and parent/child relationships removed.
|
38
|
+
def attrs
|
39
|
+
self.to_hash.reject { |key, _| key =~ /.*__r/ || key =~ /^attributes$/ }
|
40
|
+
end
|
41
|
+
|
39
42
|
private
|
40
43
|
|
41
44
|
def ensure_id
|
data/lib/restforce/version.rb
CHANGED
data/spec/lib/client_spec.rb
CHANGED
@@ -330,6 +330,7 @@ shared_examples_for 'methods' do
|
|
330
330
|
|
331
331
|
subject { client.authenticate! }
|
332
332
|
specify { expect { subject }.to_not raise_error }
|
333
|
+
it { should be_a Hash }
|
333
334
|
end
|
334
335
|
|
335
336
|
describe '.cache' do
|
@@ -395,6 +396,64 @@ shared_examples_for 'methods' do
|
|
395
396
|
specify { expect { subject }.to_not raise_error }
|
396
397
|
end
|
397
398
|
end
|
399
|
+
|
400
|
+
describe 'authentication retries' do
|
401
|
+
context 'when retries reaches 0' do
|
402
|
+
before do
|
403
|
+
@auth_request = stub_api_request('query\?q=SELECT%20some,%20fields%20FROM%20object', status: 401, with: 'expired_session_response')
|
404
|
+
@query_request = stub_request(:post, "https://login.salesforce.com/services/oauth2/token")
|
405
|
+
.with(:body => "grant_type=password&client_id=client_id&client_secret=" \
|
406
|
+
"client_secret&username=foo&password=barsecurity_token")
|
407
|
+
.to_return(:status => 200, :body => fixture(:auth_success_response))
|
408
|
+
end
|
409
|
+
|
410
|
+
subject { client.query('SELECT some, fields FROM object') }
|
411
|
+
specify { expect { subject }.to raise_error Restforce::UnauthorizedError }
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
describe '.query with caching' do
|
416
|
+
class MockCache
|
417
|
+
def initialize
|
418
|
+
@storage = {}
|
419
|
+
end
|
420
|
+
|
421
|
+
def fetch(key, &block)
|
422
|
+
@storage[key] || begin
|
423
|
+
block.call.tap do |result|
|
424
|
+
@storage[key] = result
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
let(:cache) { MockCache.new }
|
431
|
+
let(:oauth_token) { 'foobar' }
|
432
|
+
|
433
|
+
before do
|
434
|
+
@requests = [].tap do |requests|
|
435
|
+
requests << stub_request(:get, /query\?q=SELECT%20some,%20fields%20FROM%20object/)
|
436
|
+
.with(headers: { 'Authorization' => "OAuth #{oauth_token}" })
|
437
|
+
.to_return(status: 401, body: fixture('expired_session_response'))
|
438
|
+
|
439
|
+
requests << stub_request(:post, "https://login.salesforce.com/services/oauth2/token")
|
440
|
+
.with(:body => "grant_type=password&client_id=client_id&client_secret=" \
|
441
|
+
"client_secret&username=foo&password=barsecurity_token")
|
442
|
+
.to_return(status: 200, body: fixture(:auth_success_response))
|
443
|
+
|
444
|
+
requests << stub_request(:get, /query\?q=SELECT%20some,%20fields%20FROM%20object/)
|
445
|
+
.with(headers: { 'Authorization' => "OAuth 00Dx0000000BV7z!AR8AQAxo9UfVkh8AlV0Gomt9Czx9LjHnSSpwBMmbRcgKFmxOtvxjTrKW19ye6PE3Ds1eQz3z8jr3W7_VbWmEu4Q8TVGSTHxs" })
|
446
|
+
.to_return(status: 200, body: fixture('sobject/query_success_response'))
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
after do
|
451
|
+
@requests.each { |request| request.should have_been_requested }
|
452
|
+
end
|
453
|
+
|
454
|
+
subject { client.query('SELECT some, fields FROM object') }
|
455
|
+
it { should be_an Array }
|
456
|
+
end
|
398
457
|
end
|
399
458
|
|
400
459
|
describe 'with mashify middleware' do
|
@@ -11,7 +11,7 @@ describe Restforce::Middleware::Authentication do
|
|
11
11
|
it 'raises an error' do
|
12
12
|
expect {
|
13
13
|
middleware.authenticate!
|
14
|
-
}.to raise_error RuntimeError, '
|
14
|
+
}.to raise_error RuntimeError, 'not implemented'
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
@@ -33,21 +33,8 @@ describe Restforce::Middleware::Authentication do
|
|
33
33
|
end
|
34
34
|
|
35
35
|
it 'attempts to authenticate' do
|
36
|
-
app.should_receive(:call).once
|
36
|
+
app.should_receive(:call).once.and_raise(Restforce::UnauthorizedError.new('something bad'))
|
37
37
|
middleware.should_receive(:authenticate!)
|
38
|
-
app.should_receive(:call).with(body: 'foo', request: { proxy: nil }).once
|
39
|
-
middleware.call(env)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
context 'when the retry limit is reached' do
|
44
|
-
before do
|
45
|
-
app.should_receive(:call).twice.and_raise(Restforce::UnauthorizedError)
|
46
|
-
middleware.should_receive(:authenticate!)
|
47
|
-
end
|
48
|
-
|
49
|
-
let(:retries) { 1 }
|
50
|
-
it 'should raise an exception' do
|
51
38
|
expect { middleware.call(env) }.to raise_error Restforce::UnauthorizedError
|
52
39
|
end
|
53
40
|
end
|
@@ -79,21 +66,4 @@ describe Restforce::Middleware::Authentication do
|
|
79
66
|
end
|
80
67
|
end
|
81
68
|
end
|
82
|
-
|
83
|
-
describe '.force_authenticate?' do
|
84
|
-
subject { middleware.force_authenticate?(env) }
|
85
|
-
|
86
|
-
context 'without X-ForceAuthenticate header set' do
|
87
|
-
it { should be_false }
|
88
|
-
end
|
89
|
-
|
90
|
-
context 'with X-ForceAuthenticate header set' do
|
91
|
-
before do
|
92
|
-
env[:request_headers] = {}
|
93
|
-
env[:request_headers]['X-ForceAuthenticate'] = true
|
94
|
-
end
|
95
|
-
|
96
|
-
it { should be_true }
|
97
|
-
end
|
98
|
-
end
|
99
69
|
end
|
@@ -3,13 +3,15 @@ require 'spec_helper'
|
|
3
3
|
describe Restforce::Middleware::InstanceURL do
|
4
4
|
let(:app) { double('app') }
|
5
5
|
let(:client) { double('client') }
|
6
|
-
let(:options) { {
|
6
|
+
let(:options) { { } }
|
7
7
|
let(:middleware) { described_class.new app, client, options }
|
8
8
|
|
9
9
|
context 'when the instance url is not set' do
|
10
|
-
|
10
|
+
before do
|
11
|
+
client.stub_chain(:connection, :url_prefix).and_return(URI.parse('http:/'))
|
12
|
+
end
|
11
13
|
|
12
|
-
it 'raises an
|
14
|
+
it 'raises an exception' do
|
13
15
|
expect {
|
14
16
|
middleware.call(nil)
|
15
17
|
}.to raise_error Restforce::UnauthorizedError
|
@@ -17,32 +19,13 @@ describe Restforce::Middleware::InstanceURL do
|
|
17
19
|
end
|
18
20
|
|
19
21
|
context 'when the instance url is set' do
|
20
|
-
let(:instance_url) { 'https://foo.bar' }
|
21
|
-
|
22
22
|
before do
|
23
|
-
client.stub_chain(:connection, :url_prefix).and_return
|
24
|
-
end
|
25
|
-
|
26
|
-
context 'and it does not match the connection url prefix' do
|
27
|
-
let(:url_prefix) { 'https://whiz.bang' }
|
28
|
-
|
29
|
-
it 'raises an error' do
|
30
|
-
expect {
|
31
|
-
middleware.call(nil)
|
32
|
-
}.to raise_error Restforce::InstanceURLError
|
33
|
-
end
|
23
|
+
client.stub_chain(:connection, :url_prefix).and_return(URI.parse('http://foobar.com/'))
|
34
24
|
end
|
35
25
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
before do
|
40
|
-
app.should_receive(:call)
|
41
|
-
end
|
42
|
-
|
43
|
-
it 'calls tne next middleware' do
|
44
|
-
middleware.call(nil)
|
45
|
-
end
|
26
|
+
it 'does not raise an exception' do
|
27
|
+
app.should_receive(:call).once
|
28
|
+
middleware.call(nil)
|
46
29
|
end
|
47
30
|
end
|
48
31
|
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.1.
|
4
|
+
version: 0.1.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-09-
|
12
|
+
date: 2012-09-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|