restforce 0.0.8 → 0.1.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/README.md CHANGED
@@ -12,6 +12,7 @@ It attempts to solve a couple of key issues that the databasedotcom gem has been
12
12
  * Support for the Streaming API
13
13
  * Support for blob data types.
14
14
  * A clean and modular architecture using [Faraday middleware](https://github.com/technoweenie/faraday)
15
+ * Support for decoding [Force.com Canvas](http://www.salesforce.com/us/developer/docs/platform_connectpre/canvas_framework.pdf) signed requests. (NEW!)
15
16
 
16
17
  [Documentation](http://rubydoc.info/gems/restforce/frames)
17
18
 
@@ -110,6 +111,9 @@ accounts = client.query("select Id, Something__c from Account where Id = 'someid
110
111
  account = records.first
111
112
  # => #<Restforce::SObject >
112
113
 
114
+ account.sobject_type
115
+ # => 'Account'
116
+
113
117
  account.Id
114
118
  # => "someid"
115
119
 
@@ -204,6 +208,25 @@ _See also: http://www.salesforce.com/us/developer/docs/api_rest/Content/dome_del
204
208
 
205
209
  * * *
206
210
 
211
+ ### describe(sobject)
212
+
213
+ If no parameter is given, it will return the global describe. If the name of an
214
+ sobject is given, it will return the describe for that sobject.
215
+
216
+ ```ruby
217
+ # get the global describe for all sobjects
218
+ client.describe
219
+ # => { ... }
220
+
221
+ # get the describe for the Account object
222
+ client.describe('Account')
223
+ # => { ... }
224
+ ```
225
+
226
+ _See also: http://www.salesforce.com/us/developer/docs/api_rest/Content/dome_describeGlobal.htm, http://www.salesforce.com/us/developer/docs/api_rest/Content/dome_sobject_describe.htm_
227
+
228
+ * * *
229
+
207
230
  ### File Uploads
208
231
 
209
232
  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):
@@ -269,6 +292,15 @@ Restforce.configure do |config|
269
292
  end
270
293
  ```
271
294
 
295
+ If you enable caching, you can disable caching on a per-request basis by using
296
+ .without_caching:
297
+
298
+ ```ruby
299
+ client.without_caching do
300
+ client.query('select Id from Account')
301
+ end
302
+ ```
303
+
272
304
  * * *
273
305
 
274
306
  ### Logging/Debugging
@@ -279,25 +311,29 @@ You can easily inspect what Restforce is sending/receiving by setting
279
311
  ```ruby
280
312
  Restforce.log = true
281
313
  client = Restforce.new.query('select Id, Name from Account')
314
+ ```
282
315
 
283
- # => I, [2012-09-11T21:54:00.488991 #24032] INFO -- : post https://login.salesforce.com/services/oauth2/token
284
- # => D, [2012-09-11T21:54:00.489078 #24032] DEBUG -- request:
285
- # => I, [2012-09-11T21:54:00.997295 #24032] INFO -- Status: 200
286
- # => D, [2012-09-11T21:54:00.997391 #24032] DEBUG -- response headers: server: ""
287
- # => content-type: "application/json; charset=UTF-8"
288
- # => transfer-encoding: "chunked"
289
- # => date: "Wed, 12 Sep 2012 04:53:59 GMT"
290
- # => connection: "close"
291
- # => D, [2012-09-11T21:54:00.997431 #24032] DEBUG -- response body: { ... }
292
- # => I, [2012-09-11T21:54:00.998985 #24032] INFO -- : get https://na9.salesforce.com/services/data/v24.0/query?q=select+Id%2C+Name+from+Account
293
- # => D, [2012-09-11T21:54:00.999040 #24032] DEBUG -- request: Authorization: "OAuth token"
294
- # => I, [2012-09-11T21:54:01.622874 #24032] INFO -- Status: 200
295
- # => D, [2012-09-11T21:54:01.623001 #24032] DEBUG -- response headers: server: ""
296
- # => content-type: "application/json; charset=UTF-8"
297
- # => transfer-encoding: "chunked"
298
- # => date: "Wed, 12 Sep 2012 04:54:00 GMT"
299
- # => connection: "close"
300
- # => D, [2012-09-11T21:54:01.623058 #24032] DEBUG -- response body: { ... }
316
+ **Log Output**
317
+
318
+ ```
319
+ I, [2012-09-11T21:54:00.488991 #24032] INFO -- : post https://login.salesforce.com/services/oauth2/token
320
+ D, [2012-09-11T21:54:00.489078 #24032] DEBUG -- request:
321
+ I, [2012-09-11T21:54:00.997295 #24032] INFO -- Status: 200
322
+ D, [2012-09-11T21:54:00.997391 #24032] DEBUG -- response headers: server: ""
323
+ content-type: "application/json; charset=UTF-8"
324
+ transfer-encoding: "chunked"
325
+ date: "Wed, 12 Sep 2012 04:53:59 GMT"
326
+ connection: "close"
327
+ D, [2012-09-11T21:54:00.997431 #24032] DEBUG -- response body: { ... }
328
+ I, [2012-09-11T21:54:00.998985 #24032] INFO -- : get https://na9.salesforce.com/services/data/v24.0/query?q=select+Id%2C+Name+from+Account
329
+ D, [2012-09-11T21:54:00.999040 #24032] DEBUG -- request: Authorization: "OAuth token"
330
+ I, [2012-09-11T21:54:01.622874 #24032] INFO -- Status: 200
331
+ D, [2012-09-11T21:54:01.623001 #24032] DEBUG -- response headers: server: ""
332
+ content-type: "application/json; charset=UTF-8"
333
+ transfer-encoding: "chunked"
334
+ date: "Wed, 12 Sep 2012 04:54:00 GMT"
335
+ connection: "close"
336
+ D, [2012-09-11T21:54:01.623058 #24032] DEBUG -- response body: { ... }
301
337
  ```
302
338
 
303
339
  ## Contributing
data/lib/restforce.rb CHANGED
@@ -3,6 +3,9 @@ require 'faraday_middleware'
3
3
  require 'json'
4
4
  require 'faye'
5
5
 
6
+ require 'openssl'
7
+ require 'base64'
8
+
6
9
  require 'restforce/version'
7
10
  require 'restforce/config'
8
11
  require 'restforce/mash'
@@ -52,21 +52,13 @@ module Restforce
52
52
  raise 'Please specify a hash of options' unless options.is_a?(Hash)
53
53
  @options = {}.tap do |options|
54
54
  [:username, :password, :security_token, :client_id, :client_secret, :host,
55
- :api_version, :oauth_token, :refresh_token, :instance_url, :cache].each do |option|
55
+ :api_version, :oauth_token, :refresh_token, :instance_url, :cache, :authentication_retries].each do |option|
56
56
  options[option] = Restforce.configuration.send option
57
57
  end
58
58
  end
59
59
  @options.merge!(options)
60
60
  end
61
61
 
62
- # Public: Get the global describe for all sobjects.
63
- #
64
- # Returns the Hash representation of the describe call.
65
- def describe_sobjects
66
- response = api_get 'sobjects'
67
- response.body['sobjects']
68
- end
69
-
70
62
  # Public: Get the names of all sobjects on the org.
71
63
  #
72
64
  # Examples
@@ -77,7 +69,7 @@ module Restforce
77
69
  #
78
70
  # Returns an Array of String names for each SObject.
79
71
  def list_sobjects
80
- describe_sobjects.collect { |sobject| sobject['name'] }
72
+ describe.collect { |sobject| sobject['name'] }
81
73
  end
82
74
 
83
75
  # Public: Returns a detailed describe result for the specified sobject
@@ -86,14 +78,23 @@ module Restforce
86
78
  #
87
79
  # Examples
88
80
  #
81
+ # # get the global describe for all sobjects
82
+ # client.describe
83
+ # # => { ... }
84
+ #
89
85
  # # get the describe for the Account object
90
86
  # client.describe('Account')
91
87
  # # => { ... }
92
88
  #
93
89
  # Returns the Hash representation of the describe call.
94
- def describe(sobject)
95
- response = api_get "sobjects/#{sobject.to_s}/describe"
96
- response.body
90
+ def describe(sobject=nil)
91
+ if sobject
92
+ response = api_get "sobjects/#{sobject.to_s}/describe"
93
+ response.body
94
+ else
95
+ response = api_get 'sobjects'
96
+ response.body['sobjects']
97
+ end
97
98
  end
98
99
 
99
100
  # Public: Get the current organization's Id.
@@ -249,6 +250,18 @@ module Restforce
249
250
  true
250
251
  end
251
252
 
253
+ # Public: Runs the block with caching disabled.
254
+ #
255
+ # block - A query/describe/etc.
256
+ #
257
+ # Returns the result of the block
258
+ def without_caching(&block)
259
+ @options[:perform_caching] = false
260
+ block.call
261
+ ensure
262
+ @options.delete(:perform_caching)
263
+ end
264
+
252
265
  # Public: Subscribe to a PushTopic
253
266
  #
254
267
  # channel - The name of the PushTopic channel to subscribe to.
@@ -267,6 +280,19 @@ module Restforce
267
280
  connection.headers.delete('X-ForceAuthenticate')
268
281
  end
269
282
 
283
+ # Public: Decodes a signed request received from Force.com Canvas.
284
+ #
285
+ # message - The POST message containing the signed request from Salesforce.
286
+ #
287
+ # Returns the Hash context if the message is valid.
288
+ def decode_signed_request(message)
289
+ raise 'client_secret not set' unless @options[:client_secret]
290
+ encryped_secret, payload = message.split('.')
291
+ digest = OpenSSL::Digest::Digest.new('sha256')
292
+ signature = Base64.encode64(OpenSSL::HMAC.hexdigest(digest, @options[:client_secret], payload))
293
+ JSON.parse(Base64.decode64(payload)) if encryped_secret == signature
294
+ end
295
+
270
296
  # Public: Helper methods for performing arbitrary actions against the API using
271
297
  # various HTTP verbs.
272
298
  #
@@ -328,7 +354,7 @@ module Restforce
328
354
  builder.use Restforce::Middleware::InstanceURL, self, @options
329
355
  builder.use Restforce::Middleware::RaiseError
330
356
  builder.response :json
331
- builder.use Restforce::Middleware::Caching, cache if cache
357
+ builder.use Restforce::Middleware::Caching, cache, @options if cache
332
358
  builder.use Restforce::Middleware::Logger, Restforce.configuration.logger if Restforce.log?
333
359
  builder.adapter Faraday.default_adapter
334
360
  end
@@ -60,9 +60,13 @@ module Restforce
60
60
  # requests will be cached.
61
61
  attr_accessor :cache
62
62
 
63
+ # The number of times reauthentication should be tried before failing.
64
+ attr_accessor :authentication_retries
65
+
63
66
  def initialize
64
- @api_version ||= '24.0'
65
- @host ||= 'login.salesforce.com'
67
+ @api_version ||= '24.0'
68
+ @host ||= 'login.salesforce.com'
69
+ @authentication_retries ||= 3
66
70
  end
67
71
 
68
72
  def logger
@@ -7,16 +7,21 @@ module Restforce
7
7
  class Middleware::Authentication < Restforce::Middleware
8
8
 
9
9
  def call(env)
10
+ retries = @options[:authentication_retries]
10
11
  request_body = env[:body]
11
12
  request = env[:request]
12
13
  begin
13
14
  return authenticate! if force_authenticate?(env)
14
15
  @app.call(env)
15
16
  rescue Restforce::UnauthorizedError
16
- authenticate!
17
- env[:body] = request_body
18
- env[:request] = request
19
- @app.call(env)
17
+ if retries > 0
18
+ authenticate!
19
+ env[:body] = request_body
20
+ env[:request] = request
21
+ retries -= 1
22
+ retry
23
+ end
24
+ raise
20
25
  end
21
26
  end
22
27
 
@@ -1,11 +1,19 @@
1
1
  module Restforce
2
2
  class Middleware::Caching < FaradayMiddleware::Caching
3
3
 
4
+ def call(env)
5
+ perform_caching? ? super : @app.call(env)
6
+ end
7
+
4
8
  # We don't want to cache requests for different clients, so append the
5
9
  # oauth token to the cache key.
6
10
  def cache_key(env)
7
11
  super(env) + env[:request_headers][Restforce::Middleware::Authorization::AUTH_HEADER].gsub(/\s/, '')
8
12
  end
9
13
 
14
+ def perform_caching?
15
+ !@options.has_key?(:perform_caching) || @options[:perform_caching]
16
+ end
17
+
10
18
  end
11
19
  end
@@ -1,3 +1,3 @@
1
1
  module Restforce
2
- VERSION = "0.0.8"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -52,19 +52,6 @@ shared_examples_for 'methods' do
52
52
  it { should eq Restforce::Middleware::Authentication::Token }
53
53
  end
54
54
  end
55
-
56
- describe '.describe_sobjects' do
57
- before do
58
- @request = stub_api_request :sobjects, with: 'sobject/describe_sobjects_success_response'
59
- end
60
-
61
- after do
62
- @request.should have_been_requested
63
- end
64
-
65
- subject { client.describe_sobjects }
66
- it { should be_an Array }
67
- end
68
55
 
69
56
  describe '.list_sobjects' do
70
57
  before do
@@ -81,16 +68,31 @@ shared_examples_for 'methods' do
81
68
  end
82
69
 
83
70
  describe '.describe' do
84
- before do
85
- @request = stub_api_request 'sobjects/Whizbang/describe', with: 'sobject/sobject_describe_success_response'
86
- end
71
+ context 'with no arguments' do
72
+ before do
73
+ @request = stub_api_request :sobjects, with: 'sobject/describe_sobjects_success_response'
74
+ end
87
75
 
88
- after do
89
- @request.should have_been_requested
76
+ after do
77
+ @request.should have_been_requested
78
+ end
79
+
80
+ subject { client.describe }
81
+ it { should be_an Array }
90
82
  end
91
83
 
92
- subject { client.describe('Whizbang') }
93
- its(['name']) { should eq 'Whizbang' }
84
+ context 'with an argument' do
85
+ before do
86
+ @request = stub_api_request 'sobjects/Whizbang/describe', with: 'sobject/sobject_describe_success_response'
87
+ end
88
+
89
+ after do
90
+ @request.should have_been_requested
91
+ end
92
+
93
+ subject { client.describe('Whizbang') }
94
+ its(['name']) { should eq 'Whizbang' }
95
+ end
94
96
  end
95
97
 
96
98
  describe '.query' do
@@ -333,6 +335,42 @@ shared_examples_for 'methods' do
333
335
  subject { client.send :cache }
334
336
  it { should eq cache }
335
337
  end
338
+
339
+ describe '.decode_signed_request' do
340
+ subject { client.decode_signed_request(message) }
341
+
342
+ context 'when the message is valid' do
343
+ let(:data) { Base64.encode64('{ "key": "value" }') }
344
+ let(:message) do
345
+ digest = OpenSSL::Digest::Digest.new('sha256')
346
+ signature = Base64.encode64(OpenSSL::HMAC.hexdigest(digest, client_secret, data))
347
+ "#{signature}.#{data}"
348
+ end
349
+
350
+ it { should eq('key' => 'value') }
351
+ end
352
+
353
+ context 'when the message is invalid' do
354
+ let(:message) { 'foobar.awdkjkj' }
355
+ it { should be_nil }
356
+ end
357
+ end
358
+
359
+ describe '.without_caching' do
360
+ let(:cache) { double('cache') }
361
+
362
+ before do
363
+ @request = stub_api_request 'query\?q=SELECT%20some,%20fields%20FROM%20object', with: 'sobject/query_success_response'
364
+ cache.should_receive(:fetch).never
365
+ end
366
+
367
+ after do
368
+ @request.should have_been_requested
369
+ end
370
+
371
+ subject { client.without_caching { client.query('SELECT some, fields FROM object') } }
372
+ it { should be_an Array }
373
+ end
336
374
  end
337
375
 
338
376
  describe 'with mashify middleware' do
@@ -11,8 +11,9 @@ 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 '24.0' }
15
- its(:host) { should eq 'login.salesforce.com' }
14
+ its(:api_version) { should eq '24.0' }
15
+ its(:host) { should eq 'login.salesforce.com' }
16
+ its(:authentication_retries) { should eq 3 }
16
17
  [:username, :password, :security_token, :client_id, :client_secret,
17
18
  :oauth_token, :refresh_token, :instance_url].each do |attr|
18
19
  its(attr) { should be_nil }
@@ -3,7 +3,8 @@ require 'spec_helper'
3
3
  describe Restforce::Middleware::Authentication do
4
4
  let(:app) { double('app') }
5
5
  let(:env) { { } }
6
- let(:options) { { host: 'login.salesforce.com' } }
6
+ let(:retries) { 3 }
7
+ let(:options) { { host: 'login.salesforce.com', authentication_retries: retries } }
7
8
  let(:middleware) { described_class.new app, nil, options }
8
9
 
9
10
  describe '.authenticate!' do
@@ -38,6 +39,18 @@ describe Restforce::Middleware::Authentication do
38
39
  middleware.call(env)
39
40
  end
40
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
+ expect { middleware.call(env) }.to raise_error Restforce::UnauthorizedError
52
+ end
53
+ end
41
54
  end
42
55
 
43
56
  describe '.connection' do
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.0.8
4
+ version: 0.1.0
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-19 00:00:00.000000000 Z
12
+ date: 2012-09-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake