restforce 1.3.0 → 1.4.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.

Files changed (63) hide show
  1. data/.travis.yml +1 -0
  2. data/CHANGELOG.md +7 -1
  3. data/Gemfile +4 -0
  4. data/Guardfile +11 -0
  5. data/README.md +19 -8
  6. data/lib/restforce.rb +44 -14
  7. data/lib/restforce/abstract_client.rb +9 -0
  8. data/lib/restforce/client.rb +1 -95
  9. data/lib/restforce/{client → concerns}/api.rb +9 -9
  10. data/lib/restforce/{client → concerns}/authentication.rb +9 -9
  11. data/lib/restforce/concerns/base.rb +58 -0
  12. data/lib/restforce/{client → concerns}/caching.rb +4 -4
  13. data/lib/restforce/concerns/canvas.rb +12 -0
  14. data/lib/restforce/{client → concerns}/connection.rb +13 -12
  15. data/lib/restforce/{client → concerns}/picklists.rb +1 -1
  16. data/lib/restforce/{client → concerns}/streaming.rb +3 -3
  17. data/lib/restforce/{client → concerns}/verbs.rb +4 -4
  18. data/lib/restforce/config.rb +40 -10
  19. data/lib/restforce/data/client.rb +18 -0
  20. data/lib/restforce/middleware/authentication.rb +9 -2
  21. data/lib/restforce/sobject.rb +1 -1
  22. data/lib/restforce/tooling/client.rb +13 -0
  23. data/lib/restforce/version.rb +1 -1
  24. data/spec/{lib/client_spec.rb → integration/abstract_client_spec.rb} +21 -214
  25. data/spec/integration/data/client_spec.rb +90 -0
  26. data/spec/spec_helper.rb +0 -14
  27. data/spec/support/client_integration.rb +45 -0
  28. data/spec/support/concerns.rb +18 -0
  29. data/spec/support/event_machine.rb +14 -0
  30. data/spec/support/middleware.rb +18 -1
  31. data/spec/unit/abstract_client_spec.rb +11 -0
  32. data/spec/{lib → unit}/attachment_spec.rb +3 -6
  33. data/spec/unit/collection_spec.rb +50 -0
  34. data/spec/unit/concerns/api_spec.rb +222 -0
  35. data/spec/unit/concerns/authentication_spec.rb +98 -0
  36. data/spec/unit/concerns/base_spec.rb +50 -0
  37. data/spec/unit/concerns/caching_spec.rb +29 -0
  38. data/spec/unit/concerns/canvas_spec.rb +30 -0
  39. data/spec/unit/concerns/connection_spec.rb +14 -0
  40. data/spec/{lib → unit}/config_spec.rb +13 -23
  41. data/spec/unit/data/client_spec.rb +10 -0
  42. data/spec/{lib → unit}/mash_spec.rb +0 -0
  43. data/spec/{lib → unit}/middleware/authentication/password_spec.rb +0 -4
  44. data/spec/{lib → unit}/middleware/authentication/token_spec.rb +0 -4
  45. data/spec/unit/middleware/authentication_spec.rb +67 -0
  46. data/spec/unit/middleware/authorization_spec.rb +11 -0
  47. data/spec/{lib → unit}/middleware/gzip_spec.rb +15 -30
  48. data/spec/unit/middleware/instance_url_spec.rb +24 -0
  49. data/spec/{lib → unit}/middleware/logger_spec.rb +4 -7
  50. data/spec/unit/middleware/mashify_spec.rb +11 -0
  51. data/spec/{lib → unit}/middleware/raise_error_spec.rb +4 -5
  52. data/spec/{lib → unit}/signed_request_spec.rb +0 -0
  53. data/spec/unit/sobject_spec.rb +68 -0
  54. data/spec/unit/tooling/client_spec.rb +7 -0
  55. metadata +75 -46
  56. data/lib/restforce/client/canvas.rb +0 -12
  57. data/spec/lib/collection_spec.rb +0 -52
  58. data/spec/lib/middleware/authentication_spec.rb +0 -69
  59. data/spec/lib/middleware/authorization_spec.rb +0 -17
  60. data/spec/lib/middleware/instance_url_spec.rb +0 -31
  61. data/spec/lib/middleware/mashify_spec.rb +0 -28
  62. data/spec/lib/sobject_spec.rb +0 -122
  63. data/spec/support/basic_client.rb +0 -37
@@ -0,0 +1,90 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples_for Restforce::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 Restforce::Data::Client do
83
+ describe 'with mashify' do
84
+ it_behaves_like Restforce::Client
85
+ end
86
+
87
+ describe 'without mashify', :mashify => false do
88
+ it_behaves_like Restforce::Client
89
+ end
90
+ end
@@ -12,17 +12,3 @@ WebMock.disable_net_connect!
12
12
  # Requires supporting ruby files with custom matchers and macros, etc,
13
13
  # in spec/support/ and its subdirectories.
14
14
  Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each {|f| require f}
15
-
16
- RSpec.configure do |config|
17
- config.before do
18
- EventMachine.stub(:connect) if defined?(EventMachine)
19
- end
20
-
21
- config.around :eventmachine => true do |example|
22
- EM.run {
23
- example.run
24
- EM.stop
25
- }
26
- end
27
- end
28
-
@@ -0,0 +1,45 @@
1
+ module ClientIntegrationExampleGroup
2
+ def self.included(base)
3
+ base.class_eval do
4
+ let(:oauth_token) { '00Dx0000000BV7z!AR8AQAxo9UfVkh8AlV0Gomt9Czx9LjHnSSpwBMmbRcgKFmxOtvxjTrKW19ye6PE3Ds1eQz3z8jr3W7_VbWmEu4Q8TVGSTHxs' }
5
+ let(:refresh_token) { 'refresh' }
6
+ let(:instance_url) { 'https://na1.salesforce.com' }
7
+ let(:username) { 'foo' }
8
+ let(:password) { 'bar' }
9
+ let(:security_token) { 'security_token' }
10
+ let(:client_id) { 'client_id' }
11
+ let(:client_secret) { 'client_secret' }
12
+ let(:cache) { nil }
13
+
14
+ let(:base_options) do
15
+ {
16
+ :oauth_token => oauth_token,
17
+ :refresh_token => refresh_token,
18
+ :instance_url => instance_url,
19
+ :username => username,
20
+ :password => password,
21
+ :security_token => security_token,
22
+ :client_id => client_id,
23
+ :client_secret => client_secret,
24
+ :cache => cache
25
+ }
26
+ end
27
+
28
+ let(:client_options) { base_options }
29
+
30
+ subject(:client) { described_class.new client_options }
31
+ end
32
+ end
33
+
34
+ RSpec.configure do |config|
35
+ config.include self,
36
+ :example_group => {
37
+ :describes => lambda { |described| described <= Restforce::AbstractClient },
38
+ :file_path => %r{spec/integration}
39
+ }
40
+
41
+ config.before :mashify => false do
42
+ client.middleware.delete(Restforce::Middleware::Mashify)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,18 @@
1
+ module ConcernsExampleGroup
2
+ def self.included(base)
3
+ base.class_eval do
4
+ let(:klass) do
5
+ context = self
6
+ Class.new { include context.described_class }
7
+ end
8
+
9
+ let(:client) { klass.new }
10
+
11
+ subject { client }
12
+ end
13
+ end
14
+
15
+ RSpec.configure do |config|
16
+ config.include self, :example_group => { :file_path => %r{spec/unit/concerns} }
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ RSpec.configure do |config|
2
+ config.before do
3
+ EventMachine.stub(:connect) if defined?(EventMachine)
4
+ end
5
+
6
+ config.filter_run_excluding :event_machine => true if RUBY_PLATFORM == 'java'
7
+
8
+ config.around :event_machine => true do |example|
9
+ EM.run {
10
+ example.run
11
+ EM.stop
12
+ }
13
+ end
14
+ end
@@ -1,3 +1,21 @@
1
+ module MiddlewareExampleGroup
2
+ def self.included(base)
3
+ base.class_eval do
4
+ let(:app) { double('@app', :call => nil) }
5
+ let(:env) { { :request_headers => {}, :response_headers => {} } }
6
+ let(:retries) { 3 }
7
+ let(:options) { { } }
8
+ let(:client) { double(Restforce::AbstractClient) }
9
+ subject(:middleware) { described_class.new app, client, options }
10
+ end
11
+ end
12
+
13
+ RSpec.configure do |config|
14
+ config.include self,
15
+ :example_group => { :file_path => %r{spec/unit/middleware} }
16
+ end
17
+ end
18
+
1
19
  shared_examples_for 'authentication middleware' do
2
20
  describe '.authenticate!' do
3
21
  after do
@@ -28,6 +46,5 @@ shared_examples_for 'authentication middleware' do
28
46
  }.to raise_error Restforce::AuthenticationError, /^invalid_grant: .*/
29
47
  end
30
48
  end
31
-
32
49
  end
33
50
  end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe Restforce::AbstractClient do
4
+ subject { described_class }
5
+
6
+ it { should < Restforce::Concerns::Base }
7
+ it { should < Restforce::Concerns::Connection }
8
+ it { should < Restforce::Concerns::Authentication }
9
+ it { should < Restforce::Concerns::Caching }
10
+ it { should < Restforce::Concerns::API }
11
+ end
@@ -1,13 +1,10 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Restforce::Attachment do
4
- include_context 'basic client'
5
-
4
+ let(:client) { double(Restforce::AbstractClient) }
6
5
  let(:body_url) { '/services/data/v26.0/sobjects/Attachment/00PG0000006Hll5MAC/Body' }
7
- let(:hash) { { 'Id' => '1234', 'Body' => body_url } }
8
- let(:sobject) do
9
- described_class.new(hash, client)
10
- end
6
+ let(:hash) { { 'Id' => '1234', 'Body' => body_url } }
7
+ let(:sobject) { described_class.new(hash, client) }
11
8
 
12
9
  describe '.Body' do
13
10
  it 'requests the body' do
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ describe Restforce::Collection do
4
+ let(:client) { double(Restforce::AbstractClient) }
5
+
6
+ describe '#new' do
7
+ context 'without pagination' do
8
+ subject(:collection) do
9
+ described_class.new(JSON.parse(fixture('sobject/query_success_response')), client)
10
+ end
11
+
12
+ it { should respond_to :each }
13
+ its(:size) { should eq 1 }
14
+ its(:has_next_page?) { should be_false }
15
+ it { should have_client client }
16
+
17
+ describe 'each record' do
18
+ it { should be_all { |record| expect(record).to be_a Restforce::SObject } }
19
+ end
20
+ end
21
+
22
+ context 'with pagination' do
23
+ let(:first_page) { JSON.parse(fixture('sobject/query_paginated_first_page_response')) }
24
+ let(:next_page) { JSON.parse(fixture('sobject/query_paginated_last_page_response')) }
25
+ subject(:collection) { described_class.new(first_page, client) }
26
+
27
+ it { should respond_to :each }
28
+ it { should have_client client }
29
+
30
+ context 'when only values from the first page are being requested' do
31
+ before { client.should_receive(:get).never }
32
+
33
+ its(:size) { should eq 2 }
34
+ its(:first) { should be_a Restforce::SObject }
35
+ end
36
+
37
+ context 'when all of the values are being requested' do
38
+ before do
39
+ client.stub(:get).
40
+ and_return(stub :body => Restforce::Collection.new(next_page, client))
41
+ end
42
+
43
+ its(:pages) { should be_all { |page| expect(page).to be_a Restforce::Collection } }
44
+ its(:has_next_page?) { should be_true }
45
+ it { should be_all { |record| expect(record).to be_a Restforce::SObject } }
46
+ its(:next_page) { should be_a Restforce::Collection }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,222 @@
1
+ require 'spec_helper'
2
+
3
+ describe Restforce::Concerns::API do
4
+ let(:response) { double('Faraday::Response', :body => double('Body')) }
5
+
6
+ describe '.list_sobjects' do
7
+ subject { client.list_sobjects }
8
+
9
+ before do
10
+ client.stub :describe => [ { 'name' => 'foo' } ]
11
+ end
12
+
13
+ it { should eq ['foo'] }
14
+ end
15
+
16
+ describe '.describe' do
17
+ subject(:describe) { client.describe }
18
+
19
+ it 'returns the global describe' do
20
+ sobjects = double('sobjects')
21
+ response.body.stub(:[]).with('sobjects').and_return(sobjects)
22
+ client.should_receive(:api_get).
23
+ with('sobjects').
24
+ and_return(response)
25
+ expect(describe).to eq sobjects
26
+ end
27
+
28
+ context 'when given the name of an sobject' do
29
+ subject(:describe) { client.describe('Whizbang') }
30
+
31
+ it 'returns the full describe' do
32
+ client.should_receive(:api_get).
33
+ with('sobjects/Whizbang/describe').
34
+ and_return(response)
35
+ expect(describe).to eq response.body
36
+ end
37
+ end
38
+ end
39
+
40
+ describe '.org_id' do
41
+ subject(:org_id) { client.org_id }
42
+
43
+ it 'returns the organization id' do
44
+ organizations = [ { 'Id' => 'foo' } ]
45
+ client.should_receive(:query).
46
+ with('select id from Organization').
47
+ and_return(organizations)
48
+ expect(org_id).to eq 'foo'
49
+ end
50
+ end
51
+
52
+ describe '.query' do
53
+ let(:soql) { 'Select Id from Account' }
54
+ subject(:results) { client.query(soql) }
55
+
56
+ context 'with mashify middleware' do
57
+ before do
58
+ client.stub :mashify? => true
59
+ end
60
+
61
+ it 'returns the body' do
62
+ client.should_receive(:api_get).
63
+ with('query', :q => soql).
64
+ and_return(response)
65
+ expect(results).to eq response.body
66
+ end
67
+ end
68
+
69
+ context 'without mashify middleware' do
70
+ before do
71
+ client.stub :mashify? => false
72
+ end
73
+
74
+ it 'returns the records attribute of the body' do
75
+ records = double('records')
76
+ response.body.stub(:[]).
77
+ with('records').
78
+ and_return(records)
79
+ client.should_receive(:api_get).
80
+ with('query', :q => soql).
81
+ and_return(response)
82
+ expect(results).to eq records
83
+ end
84
+ end
85
+ end
86
+
87
+ describe '.search' do
88
+ let(:sosl) { 'FIND {bar}' }
89
+ subject(:results) { client.search(sosl) }
90
+
91
+ it 'performs a sosl search' do
92
+ client.should_receive(:api_get).
93
+ with('search', :q => sosl).
94
+ and_return(response)
95
+ expect(results).to eq response.body
96
+ end
97
+ end
98
+
99
+ [:create, :update, :upsert, :destroy].each do |method|
100
+ describe ".#{method}" do
101
+ let(:args) { [] }
102
+ subject(:result) { client.send(method, *args) }
103
+
104
+ it "delegates to :#{method}!" do
105
+ client.should_receive(:"#{method}!").
106
+ with(*args).
107
+ and_return(response)
108
+ expect(result).to eq response
109
+ end
110
+
111
+ it 'rescues exceptions' do
112
+ [Faraday::Error::ClientError].each do |exception_klass|
113
+ client.should_receive(:"#{method}!").
114
+ with(*args).
115
+ and_raise(exception_klass.new(nil))
116
+ expect(result).to eq false
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ describe '.create!' do
123
+ let(:sobject) { 'Whizbang' }
124
+ let(:attrs) { Hash.new }
125
+ subject(:result) { client.create!(sobject, attrs) }
126
+
127
+ it 'send an HTTP POST, and returns the id of the record' do
128
+ response.body.stub(:[]).with('id').and_return('1234')
129
+ client.should_receive(:api_post).
130
+ with('sobjects/Whizbang', attrs).
131
+ and_return(response)
132
+ expect(result).to eq '1234'
133
+ end
134
+ end
135
+
136
+ describe '.update!' do
137
+ let(:sobject) { 'Whizbang' }
138
+ let(:attrs) { Hash.new }
139
+ subject(:result) { client.update!(sobject, attrs) }
140
+
141
+ context 'when the id field is present' do
142
+ let(:attrs) { { :id => '1234' } }
143
+
144
+ it 'sends an HTTP PATCH, and returns true' do
145
+ client.should_receive(:api_patch).
146
+ with('sobjects/Whizbang/1234', attrs)
147
+ expect(result).to be_true
148
+ end
149
+ end
150
+
151
+ context 'when the id field is missing from the attrs' do
152
+ subject { lambda { result }}
153
+ it { should raise_error ArgumentError, 'Id field missing from attrs.' }
154
+ end
155
+ end
156
+
157
+ describe '.upsert!' do
158
+ let(:sobject) { 'Whizbang' }
159
+ let(:field) { :External_ID__c }
160
+ let(:attrs) { { 'External_ID__c' => '1234' } }
161
+ subject(:result) { client.upsert!(sobject, field, attrs) }
162
+
163
+ context 'when the record is found and updated' do
164
+ it 'returns true' do
165
+ response.body.stub :[]
166
+ client.should_receive(:api_patch).
167
+ with('sobjects/Whizbang/External_ID__c/1234', {}).
168
+ and_return(response)
169
+ expect(result).to be_true
170
+ end
171
+ end
172
+
173
+ context 'when the record is found and created' do
174
+ it 'returns the id of the record' do
175
+ response.body.stub(:[]).with('id').and_return('4321')
176
+ client.should_receive(:api_patch).
177
+ with('sobjects/Whizbang/External_ID__c/1234', {}).
178
+ and_return(response)
179
+ expect(result).to eq '4321'
180
+ end
181
+ end
182
+ end
183
+
184
+ describe '.destroy!' do
185
+ let(:id) { '1234' }
186
+ let(:sobject) { 'Whizbang' }
187
+ subject(:result) { client.destroy!(sobject, id) }
188
+
189
+ it 'sends and HTTP delete, and returns true' do
190
+ client.should_receive(:api_delete).
191
+ with('sobjects/Whizbang/1234')
192
+ expect(result).to be_true
193
+ end
194
+ end
195
+
196
+ describe '.find' do
197
+ let(:sobject) { 'Whizbang' }
198
+ let(:id) { '1234' }
199
+ let(:field) { nil }
200
+ subject(:result) { client.find(sobject, id, field) }
201
+
202
+ context 'when no external id is specified' do
203
+ it 'returns the full representation of the object' do
204
+ client.should_receive(:api_get).
205
+ with('sobjects/Whizbang/1234').
206
+ and_return(response)
207
+ expect(result).to eq response.body
208
+ end
209
+ end
210
+
211
+ context 'when an external id is specified' do
212
+ let(:field) { :External_ID__c }
213
+
214
+ it 'returns the full representation of the object' do
215
+ client.should_receive(:api_get).
216
+ with('sobjects/Whizbang/External_ID__c/1234').
217
+ and_return(response)
218
+ expect(result).to eq response.body
219
+ end
220
+ end
221
+ end
222
+ end