restforce 4.2.2 → 5.0.4

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 (39) 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/CHANGELOG.md +46 -0
  6. data/CONTRIBUTING.md +21 -1
  7. data/Dockerfile +31 -0
  8. data/Gemfile +0 -1
  9. data/README.md +43 -19
  10. data/UPGRADING.md +38 -0
  11. data/docker-compose.yml +7 -0
  12. data/lib/restforce.rb +4 -7
  13. data/lib/restforce/collection.rb +7 -2
  14. data/lib/restforce/concerns/api.rb +1 -1
  15. data/lib/restforce/concerns/caching.rb +7 -0
  16. data/lib/restforce/concerns/picklists.rb +1 -1
  17. data/lib/restforce/concerns/streaming.rb +1 -3
  18. data/lib/restforce/config.rb +4 -1
  19. data/lib/restforce/error_code.rb +617 -0
  20. data/lib/restforce/file_part.rb +24 -0
  21. data/lib/restforce/mash.rb +7 -2
  22. data/lib/restforce/middleware.rb +2 -0
  23. data/lib/restforce/middleware/caching.rb +1 -1
  24. data/lib/restforce/middleware/logger.rb +3 -2
  25. data/lib/restforce/middleware/raise_error.rb +3 -4
  26. data/lib/restforce/version.rb +1 -1
  27. data/restforce.gemspec +12 -12
  28. data/spec/integration/abstract_client_spec.rb +16 -4
  29. data/spec/spec_helper.rb +14 -1
  30. data/spec/support/fixture_helpers.rb +1 -3
  31. data/spec/unit/collection_spec.rb +18 -0
  32. data/spec/unit/concerns/caching_spec.rb +26 -0
  33. data/spec/unit/concerns/connection_spec.rb +2 -2
  34. data/spec/unit/error_code_spec.rb +61 -0
  35. data/spec/unit/middleware/authentication_spec.rb +11 -5
  36. data/spec/unit/middleware/raise_error_spec.rb +9 -0
  37. data/spec/unit/signed_request_spec.rb +1 -1
  38. metadata +34 -41
  39. 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
@@ -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
@@ -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)
@@ -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.2'
4
+ VERSION = '5.0.4'
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,12 +22,11 @@ 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
32
  gem.add_dependency 'hashie', '>= 1.2.0', '< 5.0'
@@ -35,7 +34,8 @@ Gem::Specification.new do |gem|
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.77.0'
39
- gem.add_development_dependency 'simplecov', '~> 0.17.1'
40
- gem.add_development_dependency 'webmock', '~> 3.7.6'
37
+
38
+ gem.add_development_dependency 'rubocop', '~> 1.8.1'
39
+ gem.add_development_dependency 'simplecov', '~> 0.21.2'
40
+ gem.add_development_dependency 'webmock', '~> 3.11.1'
41
41
  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
 
@@ -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
@@ -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
@@ -8,7 +8,9 @@ describe Restforce::Middleware::Authentication do
8
8
  proxy_uri: 'https://not-a-real-site.com',
9
9
  authentication_retries: retries,
10
10
  adapter: :net_http,
11
+ # rubocop:disable Naming/VariableNumber
11
12
  ssl: { version: :TLSv1_2 } }
13
+ # rubocop:enable Naming/VariableNumber
12
14
  end
13
15
 
14
16
  describe '.authenticate!' do
@@ -57,10 +59,10 @@ describe Restforce::Middleware::Authentication do
57
59
  end
58
60
 
59
61
  its(:handlers) {
60
- should include FaradayMiddleware::ParseJson,
61
- Faraday::Adapter::NetHttp
62
+ should include FaradayMiddleware::ParseJson
62
63
  }
63
64
  its(:handlers) { should_not include Restforce::Middleware::Logger }
65
+ its(:adapter) { should eq Faraday::Adapter::NetHttp }
64
66
  end
65
67
 
66
68
  context 'with logging enabled' do
@@ -70,8 +72,9 @@ describe Restforce::Middleware::Authentication do
70
72
 
71
73
  its(:handlers) {
72
74
  should include FaradayMiddleware::ParseJson,
73
- Restforce::Middleware::Logger, Faraday::Adapter::NetHttp
75
+ Restforce::Middleware::Logger
74
76
  }
77
+ its(:adapter) { should eq Faraday::Adapter::NetHttp }
75
78
  end
76
79
 
77
80
  context 'with specified adapter' do
@@ -80,13 +83,16 @@ describe Restforce::Middleware::Authentication do
80
83
  end
81
84
 
82
85
  its(:handlers) {
83
- should include FaradayMiddleware::ParseJson, Faraday::Adapter::Typhoeus
86
+ should include FaradayMiddleware::ParseJson
84
87
  }
88
+ its(:adapter) { should eq Faraday::Adapter::Typhoeus }
85
89
  end
86
90
  end
87
91
 
88
92
  it "should have SSL config set" do
93
+ # rubocop:disable Naming/VariableNumber
89
94
  connection.ssl[:version].should eq(:TLSv1_2)
95
+ # rubocop:enable Naming/VariableNumber
90
96
  end
91
97
  end
92
98
 
@@ -94,7 +100,7 @@ describe Restforce::Middleware::Authentication do
94
100
  context 'when response.body is present' do
95
101
  let(:response) {
96
102
  Faraday::Response.new(
97
- body: { 'error' => 'error', 'error_description' => 'description' },
103
+ response_body: { 'error' => 'error', 'error_description' => 'description' },
98
104
  status: 401
99
105
  )
100
106
  }
@@ -85,5 +85,14 @@ describe Restforce::Middleware::RaiseError do
85
85
  "(error code missing): #{body}"
86
86
  end
87
87
  end
88
+
89
+ context 'when error code is not already defined' do
90
+ let(:body) { { 'errorCode' => 'SOMETHING_UNDEFINED' } }
91
+ let(:status) { 400 }
92
+
93
+ it 'raises a generic Restforce::ResponseError' do
94
+ expect { on_complete }.to raise_error Restforce::ResponseError
95
+ end
96
+ end
88
97
  end
89
98
  end