restforce 4.2.0 → 5.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +9 -9
  3. data/.github/ISSUE_TEMPLATE/unhandled-salesforce-error.md +17 -0
  4. data/.rubocop.yml +4 -3
  5. data/.rubocop_todo.yml +2 -2
  6. data/CHANGELOG.md +42 -0
  7. data/CONTRIBUTING.md +21 -1
  8. data/Dockerfile +31 -0
  9. data/Gemfile +0 -1
  10. data/README.md +43 -19
  11. data/UPGRADING.md +38 -0
  12. data/docker-compose.yml +7 -0
  13. data/lib/restforce.rb +10 -13
  14. data/lib/restforce/collection.rb +7 -2
  15. data/lib/restforce/concerns/api.rb +1 -1
  16. data/lib/restforce/concerns/caching.rb +7 -0
  17. data/lib/restforce/concerns/connection.rb +3 -3
  18. data/lib/restforce/concerns/picklists.rb +1 -1
  19. data/lib/restforce/config.rb +4 -1
  20. data/lib/restforce/error_code.rb +416 -0
  21. data/lib/restforce/file_part.rb +24 -0
  22. data/lib/restforce/mash.rb +8 -3
  23. data/lib/restforce/middleware.rb +2 -0
  24. data/lib/restforce/middleware/authentication.rb +4 -1
  25. data/lib/restforce/middleware/caching.rb +1 -1
  26. data/lib/restforce/middleware/instance_url.rb +1 -1
  27. data/lib/restforce/middleware/logger.rb +3 -2
  28. data/lib/restforce/middleware/raise_error.rb +3 -4
  29. data/lib/restforce/version.rb +1 -1
  30. data/restforce.gemspec +12 -13
  31. data/spec/integration/abstract_client_spec.rb +18 -6
  32. data/spec/spec_helper.rb +14 -1
  33. data/spec/support/fixture_helpers.rb +1 -3
  34. data/spec/unit/collection_spec.rb +18 -0
  35. data/spec/unit/concerns/api_spec.rb +1 -1
  36. data/spec/unit/concerns/caching_spec.rb +26 -0
  37. data/spec/unit/concerns/connection_spec.rb +2 -2
  38. data/spec/unit/error_code_spec.rb +61 -0
  39. data/spec/unit/mash_spec.rb +5 -0
  40. data/spec/unit/middleware/authentication_spec.rb +27 -4
  41. data/spec/unit/middleware/raise_error_spec.rb +19 -10
  42. data/spec/unit/signed_request_spec.rb +1 -1
  43. data/spec/unit/sobject_spec.rb +2 -5
  44. metadata +33 -40
  45. data/lib/restforce/upload_io.rb +0 -9
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'faraday/file_part'
5
+ rescue LoadError
6
+ require 'faraday/upload_io'
7
+ end
8
+
9
+ module Restforce
10
+ if defined?(::Faraday::FilePart)
11
+ FilePart = Faraday::FilePart
12
+
13
+ # Deprecated
14
+ UploadIO = Faraday::FilePart
15
+ else
16
+ # Handle pre-1.0 versions of faraday
17
+ FilePart = Faraday::UploadIO
18
+ UploadIO = Faraday::UploadIO
19
+ end
20
+ end
21
+
22
+ # This patch is only needed with multipart-post < 2.0.0
23
+ # 2.0.0 was released in 2013.
24
+ require 'restforce/patches/parts' unless Parts::Part.method(:new).arity.abs == 4
@@ -11,9 +11,10 @@ module Restforce
11
11
  # appropriate Restforce::Collection, Restforce::SObject and
12
12
  # Restforce::Mash objects.
13
13
  def build(val, client)
14
- if val.is_a?(Array)
14
+ case val
15
+ when Array
15
16
  val.collect { |a_val| self.build(a_val, client) }
16
- elsif val.is_a?(Hash)
17
+ when Hash
17
18
  self.klass(val).new(val, client)
18
19
  else
19
20
  val
@@ -28,7 +29,7 @@ module Restforce
28
29
  # of sobject records.
29
30
  Restforce::Collection
30
31
  elsif val.key? 'attributes'
31
- case (val['attributes']['type'])
32
+ case val.dig('attributes', 'type')
32
33
  when "Attachment"
33
34
  Restforce::Attachment
34
35
  when "Document"
@@ -55,6 +56,9 @@ module Restforce
55
56
  self.class.new(self, @client, self.default)
56
57
  end
57
58
 
59
+ # The #convert_value method and its signature are part of Hashie::Mash's API, so we
60
+ # can't unilaterally decide to change `duping` to be a keyword argument
61
+ # rubocop:disable Style/OptionalBooleanParameter
58
62
  def convert_value(val, duping = false)
59
63
  case val
60
64
  when self.class
@@ -68,5 +72,6 @@ module Restforce
68
72
  val
69
73
  end
70
74
  end
75
+ # rubocop:enable Style/OptionalBooleanParameter
71
76
  end
72
77
  end
@@ -16,6 +16,8 @@ module Restforce
16
16
  autoload :CustomHeaders, 'restforce/middleware/custom_headers'
17
17
 
18
18
  def initialize(app, client, options)
19
+ super(app)
20
+
19
21
  @app = app
20
22
  @client = client
21
23
  @options = options
@@ -63,7 +63,10 @@ module Restforce
63
63
 
64
64
  # Internal: The parsed error response.
65
65
  def error_message(response)
66
- "#{response.body['error']}: #{response.body['error_description']}"
66
+ return response.status.to_s unless response.body
67
+
68
+ "#{response.body['error']}: #{response.body['error_description']} " \
69
+ "(#{response.status})"
67
70
  end
68
71
 
69
72
  # Featured detect form encoding.
@@ -18,7 +18,7 @@ module Restforce
18
18
  end
19
19
 
20
20
  def use_cache?
21
- @options.fetch(:use_cache, true)
21
+ @options[:use_cache]
22
22
  end
23
23
 
24
24
  def hashed_auth_header(env)
@@ -14,7 +14,7 @@ module Restforce
14
14
  end
15
15
 
16
16
  def url_prefix_set?
17
- !!(connection.url_prefix&.host)
17
+ !!connection.url_prefix&.host
18
18
  end
19
19
  end
20
20
  end
@@ -11,7 +11,7 @@ module Restforce
11
11
  @options = options
12
12
  @logger = logger || begin
13
13
  require 'logger'
14
- ::Logger.new(STDOUT)
14
+ ::Logger.new($stdout)
15
15
  end
16
16
  end
17
17
 
@@ -36,7 +36,8 @@ module Restforce
36
36
  end
37
37
 
38
38
  def dump(hash)
39
- "\n" + hash.map { |k, v| " #{k}: #{v.inspect}" }.join("\n")
39
+ dumped_pairs = hash.map { |k, v| " #{k}: #{v.inspect}" }.join("\n")
40
+ "\n#{dumped_pairs}"
40
41
  end
41
42
  end
42
43
  end
@@ -11,9 +11,9 @@ module Restforce
11
11
  response_values
12
12
  )
13
13
  when 401
14
- raise Restforce::UnauthorizedError, message
14
+ raise Restforce::UnauthorizedError.new(message, response_values)
15
15
  when 404
16
- raise Restforce::NotFoundError, message
16
+ raise Restforce::NotFoundError.new(message, response_values)
17
17
  when 413
18
18
  raise Restforce::EntityTooLargeError.new(
19
19
  "413: Request Entity Too Large",
@@ -56,8 +56,7 @@ module Restforce
56
56
  def exception_class_for_error_code(error_code)
57
57
  return Restforce::ResponseError unless ERROR_CODE_MATCHER.match?(error_code)
58
58
 
59
- constant_name = error_code.split('_').map(&:capitalize).join.to_sym
60
- Restforce::ErrorCode.const_get(constant_name)
59
+ Restforce::ErrorCode.get_exception_class(error_code)
61
60
  end
62
61
  end
63
62
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Restforce
4
- VERSION = '4.2.0'
4
+ VERSION = '5.0.2'
5
5
  end
@@ -3,11 +3,11 @@
3
3
  require File.expand_path('lib/restforce/version', __dir__)
4
4
 
5
5
  Gem::Specification.new do |gem|
6
- gem.authors = ["Eric J. Holmes", "Tim Rogers"]
7
- gem.email = ["eric@ejholmes.net", "tim@gocardless.com"]
8
- gem.description = 'A lightweight ruby client for the Salesforce REST API.'
9
- gem.summary = 'A lightweight ruby client for the Salesforce REST API.'
10
- gem.homepage = "http://restforce.org/"
6
+ gem.authors = ["Tim Rogers", "Eric J. Holmes"]
7
+ gem.email = ["me@timrogers.co.uk", "eric@ejholmes.net"]
8
+ gem.description = 'A lightweight Ruby client for the Salesforce REST API'
9
+ gem.summary = 'A lightweight Ruby client for the Salesforce REST API'
10
+ gem.homepage = "https://restforce.github.io/"
11
11
  gem.license = "MIT"
12
12
 
13
13
  gem.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
@@ -22,20 +22,19 @@ Gem::Specification.new do |gem|
22
22
  'changelog_uri' => 'https://github.com/restforce/restforce/blob/master/CHANGELOG.md'
23
23
  }
24
24
 
25
- gem.required_ruby_version = '>= 2.4'
25
+ gem.required_ruby_version = '>= 2.5'
26
26
 
27
- gem.add_dependency 'faraday', '<= 1.0', '>= 0.9.0'
28
- gem.add_dependency 'faraday_middleware', ['>= 0.8.8', '<= 1.0']
27
+ gem.add_dependency 'faraday', '<= 2.0', '>= 0.9.0'
28
+ gem.add_dependency 'faraday_middleware', ['>= 0.8.8', '<= 2.0']
29
29
 
30
- gem.add_dependency 'json', '>= 1.7.5'
31
30
  gem.add_dependency 'jwt', ['>= 1.5.6']
32
31
 
33
- gem.add_dependency 'hashie', ['>= 1.2.0', '< 4.0']
32
+ gem.add_dependency 'hashie', '>= 1.2.0', '< 5.0'
34
33
 
35
34
  gem.add_development_dependency 'faye' unless RUBY_PLATFORM == 'java'
36
35
  gem.add_development_dependency 'rspec', '~> 2.14.0'
37
36
  gem.add_development_dependency 'rspec_junit_formatter', '~> 0.4.1'
38
- gem.add_development_dependency 'rubocop', '~> 0.75.0'
39
- gem.add_development_dependency 'simplecov', '~> 0.17.1'
40
- gem.add_development_dependency 'webmock', '~> 3.7.6'
37
+ gem.add_development_dependency 'rubocop', '~> 0.90.0'
38
+ gem.add_development_dependency 'simplecov', '~> 0.19.0'
39
+ gem.add_development_dependency 'webmock', '~> 3.8.3'
41
40
  end
@@ -86,22 +86,34 @@ shared_examples_for Restforce::AbstractClient do
86
86
  end
87
87
 
88
88
  context 'with multipart' do
89
- # rubocop:disable Metrics/LineLength
89
+ # rubocop:disable Layout/LineLength
90
90
  requests 'sobjects/Account',
91
91
  method: :post,
92
- 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),
92
+ 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),
93
93
  fixture: 'sobject/create_success_response'
94
- # rubocop:enable Metrics/LineLength
94
+ # rubocop:enable Layout/LineLength
95
95
 
96
96
  subject do
97
97
  client.create('Account', Name: 'Foobar',
98
- Blob: Restforce::UploadIO.new(
98
+ Blob: Restforce::FilePart.new(
99
99
  File.expand_path('../fixtures/blob.jpg', __dir__),
100
100
  'image/jpeg'
101
101
  ))
102
102
  end
103
103
 
104
104
  it { should eq 'some_id' }
105
+
106
+ context 'with deprecated UploadIO' do
107
+ subject do
108
+ client.create('Account', Name: 'Foobar',
109
+ Blob: Restforce::UploadIO.new(
110
+ File.expand_path('../fixtures/blob.jpg', __dir__),
111
+ 'image/jpeg'
112
+ ))
113
+ end
114
+
115
+ it { should eq 'some_id' }
116
+ end
105
117
  end
106
118
  end
107
119
 
@@ -125,7 +137,7 @@ shared_examples_for Restforce::AbstractClient do
125
137
 
126
138
  it {
127
139
  should raise_error(
128
- Faraday::Error::ResourceNotFound,
140
+ Faraday::ResourceNotFound,
129
141
  "#{error.first['errorCode']}: #{error.first['message']}"
130
142
  )
131
143
  }
@@ -239,7 +251,7 @@ shared_examples_for Restforce::AbstractClient do
239
251
  status: 404
240
252
 
241
253
  subject { lambda { destroy! } }
242
- it { should raise_error Faraday::Error::ResourceNotFound }
254
+ it { should raise_error Faraday::ResourceNotFound }
243
255
  end
244
256
 
245
257
  context 'with success' do
@@ -15,8 +15,21 @@ RSpec.configure do |config|
15
15
  config.order = 'random'
16
16
  config.filter_run focus: true
17
17
  config.run_all_when_everything_filtered = true
18
+
19
+ original_stderr = $stderr
20
+ original_stdout = $stdout
21
+ config.before(:all) do
22
+ # Redirect stderr and stdout
23
+ $stderr = File.open(File::NULL, "w")
24
+ $stdout = File.open(File::NULL, "w")
25
+ end
26
+ config.after(:all) do
27
+ $stderr = original_stderr
28
+ $stdout = original_stdout
29
+ end
18
30
  end
19
31
 
20
32
  # Requires supporting ruby files with custom matchers and macros, etc,
21
33
  # in spec/support/ and its subdirectories.
22
- Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each { |f| require f }
34
+ paths = Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")]
35
+ paths.sort.each { |f| require f }
@@ -22,9 +22,7 @@ module FixtureHelpers
22
22
  end
23
23
 
24
24
  def stub_login_request(*)
25
- stub = stub_request(:post, "https://login.salesforce.com/services/oauth2/token")
26
-
27
- stub
25
+ stub_request(:post, "https://login.salesforce.com/services/oauth2/token")
28
26
  end
29
27
 
30
28
  def fixture(filename)
@@ -62,4 +62,22 @@ describe Restforce::Collection do
62
62
  end
63
63
  end
64
64
  end
65
+
66
+ describe '#empty?' do
67
+ subject(:empty?) do
68
+ described_class.new(JSON.parse(fixture(sobject_fixture)), client).empty?
69
+ end
70
+
71
+ context 'with size 1' do
72
+ let(:sobject_fixture) { 'sobject/query_success_response' }
73
+
74
+ it { should be_false }
75
+ end
76
+
77
+ context 'with size 0' do
78
+ let(:sobject_fixture) { 'sobject/query_empty_response' }
79
+
80
+ it { should be_true }
81
+ end
82
+ end
65
83
  end
@@ -274,7 +274,7 @@ describe Restforce::Concerns::API do
274
274
  end
275
275
 
276
276
  it 'rescues exceptions' do
277
- [Faraday::Error::ClientError].each do |exception_klass|
277
+ [Faraday::ClientError].each do |exception_klass|
278
278
  client.should_receive(:"#{method}!").
279
279
  with(*args).
280
280
  and_raise(exception_klass.new(nil))
@@ -28,4 +28,30 @@ describe Restforce::Concerns::Caching do
28
28
  end
29
29
  end
30
30
  end
31
+
32
+ describe '.with_caching' do
33
+ let(:options) { double('Options') }
34
+
35
+ before do
36
+ client.stub options: options
37
+ end
38
+
39
+ it 'runs the block with caching enabled' do
40
+ options.should_receive(:[]=).with(:use_cache, true)
41
+ options.should_receive(:[]=).with(:use_cache, false)
42
+ expect { |b| client.with_caching(&b) }.to yield_control
43
+ end
44
+
45
+ context 'when an exception is raised' do
46
+ it 'ensures the :use_cache is set to false' do
47
+ options.should_receive(:[]=).with(:use_cache, true)
48
+ options.should_receive(:[]=).with(:use_cache, false)
49
+ expect {
50
+ client.with_caching do
51
+ raise 'Foo'
52
+ end
53
+ }.to raise_error 'Foo'
54
+ end
55
+ end
56
+ end
31
57
  end
@@ -73,9 +73,9 @@ describe Restforce::Concerns::Connection do
73
73
  Restforce.stub(log?: true)
74
74
  end
75
75
 
76
- it "must always be used last before the Faraday Adapter" do
76
+ it "must always be used as the last handler" do
77
77
  client.middleware.handlers.reverse.index(Restforce::Middleware::Logger).
78
- should eq 1
78
+ should eq 0
79
79
  end
80
80
  end
81
81
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Restforce::ErrorCode do
6
+ describe "mapping of error codes to classes" do
7
+ subject(:error_exception_classes) { described_class::ERROR_EXCEPTION_CLASSES }
8
+
9
+ let(:exception_classes) do
10
+ described_class.constants.
11
+ map { |constant_name| described_class.const_get(constant_name) }.
12
+ select { |constant| constant.is_a?(Class) }
13
+ end
14
+
15
+ it "maps all defined exception classes to an error code" do
16
+ exception_classes.each do |exception_class|
17
+ expect(error_exception_classes.values).to include(exception_class)
18
+ end
19
+ end
20
+
21
+ it "maps all error codes to a defined exception class" do
22
+ error_exception_classes.each_value do |mapped_exception_class|
23
+ expect(exception_classes).to include(mapped_exception_class)
24
+ end
25
+ end
26
+ end
27
+
28
+ describe '.get_exception_class' do
29
+ context 'when a non-existent error code is looked up' do
30
+ let(:new_error_code) { 'ANOTHER_NEW_ERROR_CODE' }
31
+ subject { described_class.get_exception_class(new_error_code) }
32
+
33
+ it { should be Restforce::ResponseError }
34
+
35
+ it 'outputs a warning' do
36
+ expect(Warning).to receive(:warn)
37
+ subject
38
+ end
39
+ end
40
+
41
+ context 'when a known error code is looked up' do
42
+ let(:existing_error_code) { "ALL_OR_NONE_OPERATION_ROLLED_BACK" }
43
+ let(:existing_error) { described_class::AllOrNoneOperationRolledBack }
44
+
45
+ subject do
46
+ described_class.get_exception_class(existing_error_code)
47
+ end
48
+
49
+ it { should < Restforce::ResponseError }
50
+
51
+ it 'returns existing error' do
52
+ should be(existing_error)
53
+ end
54
+
55
+ it 'does not output a warning' do
56
+ expect(Warning).to_not receive(:warn)
57
+ subject
58
+ end
59
+ end
60
+ end
61
+ end
@@ -33,6 +33,11 @@ describe Restforce::Mash do
33
33
  let(:input) { { 'attributes' => { 'type' => 'Document' } } }
34
34
  it { should eq Restforce::Document }
35
35
  end
36
+
37
+ context 'when the attributes value is nil' do
38
+ let(:input) { { 'attributes' => nil } }
39
+ it { should eq Restforce::SObject }
40
+ end
36
41
  end
37
42
 
38
43
  context 'else' do
@@ -57,10 +57,10 @@ describe Restforce::Middleware::Authentication do
57
57
  end
58
58
 
59
59
  its(:handlers) {
60
- should include FaradayMiddleware::ParseJson,
61
- Faraday::Adapter::NetHttp
60
+ should include FaradayMiddleware::ParseJson
62
61
  }
63
62
  its(:handlers) { should_not include Restforce::Middleware::Logger }
63
+ its(:adapter) { should eq Faraday::Adapter::NetHttp }
64
64
  end
65
65
 
66
66
  context 'with logging enabled' do
@@ -70,8 +70,9 @@ describe Restforce::Middleware::Authentication do
70
70
 
71
71
  its(:handlers) {
72
72
  should include FaradayMiddleware::ParseJson,
73
- Restforce::Middleware::Logger, Faraday::Adapter::NetHttp
73
+ Restforce::Middleware::Logger
74
74
  }
75
+ its(:adapter) { should eq Faraday::Adapter::NetHttp }
75
76
  end
76
77
 
77
78
  context 'with specified adapter' do
@@ -80,8 +81,9 @@ describe Restforce::Middleware::Authentication do
80
81
  end
81
82
 
82
83
  its(:handlers) {
83
- should include FaradayMiddleware::ParseJson, Faraday::Adapter::Typhoeus
84
+ should include FaradayMiddleware::ParseJson
84
85
  }
86
+ its(:adapter) { should eq Faraday::Adapter::Typhoeus }
85
87
  end
86
88
  end
87
89
 
@@ -89,4 +91,25 @@ describe Restforce::Middleware::Authentication do
89
91
  connection.ssl[:version].should eq(:TLSv1_2)
90
92
  end
91
93
  end
94
+
95
+ describe '.error_message' do
96
+ context 'when response.body is present' do
97
+ let(:response) {
98
+ Faraday::Response.new(
99
+ response_body: { 'error' => 'error', 'error_description' => 'description' },
100
+ status: 401
101
+ )
102
+ }
103
+
104
+ subject { middleware.error_message(response) }
105
+ it { should eq "error: description (401)" }
106
+ end
107
+
108
+ context 'when response.body is nil' do
109
+ let(:response) { Faraday::Response.new(status: 401) }
110
+
111
+ subject { middleware.error_message(response) }
112
+ it { should eq "401" }
113
+ end
114
+ end
92
115
  end