restforce 3.0.1 → 5.1.1

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 (72) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +9 -9
  3. data/.github/ISSUE_TEMPLATE/unhandled-salesforce-error.md +17 -0
  4. data/.github/dependabot.yml +19 -0
  5. data/.rubocop.yml +13 -14
  6. data/.rubocop_todo.yml +128 -81
  7. data/CHANGELOG.md +107 -1
  8. data/CONTRIBUTING.md +21 -1
  9. data/Dockerfile +31 -0
  10. data/Gemfile +10 -6
  11. data/README.md +168 -31
  12. data/UPGRADING.md +38 -0
  13. data/docker-compose.yml +7 -0
  14. data/lib/restforce/abstract_client.rb +1 -0
  15. data/lib/restforce/attachment.rb +1 -0
  16. data/lib/restforce/collection.rb +7 -2
  17. data/lib/restforce/concerns/api.rb +10 -7
  18. data/lib/restforce/concerns/authentication.rb +10 -0
  19. data/lib/restforce/concerns/base.rb +4 -2
  20. data/lib/restforce/concerns/batch_api.rb +87 -0
  21. data/lib/restforce/concerns/caching.rb +7 -0
  22. data/lib/restforce/concerns/canvas.rb +1 -0
  23. data/lib/restforce/concerns/connection.rb +3 -3
  24. data/lib/restforce/concerns/picklists.rb +4 -3
  25. data/lib/restforce/concerns/streaming.rb +73 -3
  26. data/lib/restforce/config.rb +8 -1
  27. data/lib/restforce/document.rb +1 -0
  28. data/lib/restforce/error_code.rb +638 -0
  29. data/lib/restforce/file_part.rb +24 -0
  30. data/lib/restforce/mash.rb +8 -3
  31. data/lib/restforce/middleware/authentication/jwt_bearer.rb +38 -0
  32. data/lib/restforce/middleware/authentication.rb +7 -3
  33. data/lib/restforce/middleware/caching.rb +1 -1
  34. data/lib/restforce/middleware/instance_url.rb +1 -1
  35. data/lib/restforce/middleware/logger.rb +8 -7
  36. data/lib/restforce/middleware/multipart.rb +1 -0
  37. data/lib/restforce/middleware/raise_error.rb +24 -9
  38. data/lib/restforce/middleware.rb +2 -0
  39. data/lib/restforce/signed_request.rb +1 -0
  40. data/lib/restforce/sobject.rb +1 -0
  41. data/lib/restforce/tooling/client.rb +3 -3
  42. data/lib/restforce/version.rb +1 -1
  43. data/lib/restforce.rb +21 -3
  44. data/restforce.gemspec +11 -20
  45. data/spec/fixtures/test_private.key +27 -0
  46. data/spec/integration/abstract_client_spec.rb +83 -33
  47. data/spec/integration/data/client_spec.rb +6 -2
  48. data/spec/spec_helper.rb +24 -1
  49. data/spec/support/client_integration.rb +7 -7
  50. data/spec/support/concerns.rb +1 -1
  51. data/spec/support/fixture_helpers.rb +3 -5
  52. data/spec/support/middleware.rb +1 -2
  53. data/spec/unit/collection_spec.rb +20 -2
  54. data/spec/unit/concerns/api_spec.rb +12 -12
  55. data/spec/unit/concerns/authentication_spec.rb +39 -4
  56. data/spec/unit/concerns/batch_api_spec.rb +107 -0
  57. data/spec/unit/concerns/caching_spec.rb +26 -0
  58. data/spec/unit/concerns/connection_spec.rb +2 -2
  59. data/spec/unit/concerns/streaming_spec.rb +144 -4
  60. data/spec/unit/config_spec.rb +1 -1
  61. data/spec/unit/error_code_spec.rb +61 -0
  62. data/spec/unit/mash_spec.rb +5 -0
  63. data/spec/unit/middleware/authentication/jwt_bearer_spec.rb +62 -0
  64. data/spec/unit/middleware/authentication/password_spec.rb +2 -2
  65. data/spec/unit/middleware/authentication/token_spec.rb +2 -2
  66. data/spec/unit/middleware/authentication_spec.rb +31 -4
  67. data/spec/unit/middleware/gzip_spec.rb +2 -2
  68. data/spec/unit/middleware/raise_error_spec.rb +57 -17
  69. data/spec/unit/signed_request_spec.rb +1 -1
  70. data/spec/unit/sobject_spec.rb +2 -5
  71. metadata +39 -108
  72. data/lib/restforce/upload_io.rb +0 -9
@@ -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
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt'
4
+
5
+ module Restforce
6
+ class Middleware
7
+ class Authentication
8
+ class JWTBearer < Restforce::Middleware::Authentication
9
+ def params
10
+ {
11
+ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
12
+ assertion: jwt_bearer_token
13
+ }
14
+ end
15
+
16
+ private
17
+
18
+ def jwt_bearer_token
19
+ JWT.encode claim_set, private_key, 'RS256'
20
+ end
21
+
22
+ def claim_set
23
+ {
24
+ iss: @options[:client_id],
25
+ sub: @options[:username],
26
+ aud: @options[:host],
27
+ iat: Time.now.utc.to_i,
28
+ exp: Time.now.utc.to_i + 180
29
+ }
30
+ end
31
+
32
+ def private_key
33
+ OpenSSL::PKey::RSA.new(@options[:jwt_key])
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -6,8 +6,9 @@ module Restforce
6
6
  # will attempt to either reauthenticate (username and password) or refresh
7
7
  # the oauth access token (if a refresh token is present).
8
8
  class Middleware::Authentication < Restforce::Middleware
9
- autoload :Password, 'restforce/middleware/authentication/password'
10
- autoload :Token, 'restforce/middleware/authentication/token'
9
+ autoload :Password, 'restforce/middleware/authentication/password'
10
+ autoload :Token, 'restforce/middleware/authentication/token'
11
+ autoload :JWTBearer, 'restforce/middleware/authentication/jwt_bearer'
11
12
 
12
13
  # Rescue from 401's, authenticate then raise the error again so the client
13
14
  # can reissue the request.
@@ -62,7 +63,10 @@ module Restforce
62
63
 
63
64
  # Internal: The parsed error response.
64
65
  def error_message(response)
65
- "#{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})"
66
70
  end
67
71
 
68
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
 
@@ -20,9 +20,9 @@ module Restforce
20
20
  def call(env)
21
21
  debug('request') do
22
22
  dump url: env[:url].to_s,
23
- method: env[:method],
24
- headers: env[:request_headers],
25
- body: env[:body]
23
+ method: env[:method],
24
+ headers: env[:request_headers],
25
+ body: env[:body]
26
26
  end
27
27
  super
28
28
  end
@@ -30,13 +30,14 @@ module Restforce
30
30
  def on_complete(env)
31
31
  debug('response') do
32
32
  dump status: env[:status].to_s,
33
- headers: env[:response_headers],
34
- body: env[:body]
33
+ headers: env[:response_headers],
34
+ body: env[:body]
35
35
  end
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
@@ -49,6 +49,7 @@ module Restforce
49
49
  # Files
50
50
  params.each do |k, v|
51
51
  next unless v.respond_to? :content_type
52
+
52
53
  parts << Faraday::Parts::Part.new(boundary,
53
54
  k.to_s,
54
55
  v)
@@ -6,23 +6,30 @@ module Restforce
6
6
  @env = env
7
7
  case env[:status]
8
8
  when 300
9
- raise Faraday::Error::ClientError.new("300: The external ID provided matches " \
10
- "more than one record",
11
- response_values)
9
+ raise Restforce::MatchesMultipleError.new(
10
+ "300: The external ID provided matches more than one record",
11
+ response_values
12
+ )
12
13
  when 401
13
- raise Restforce::UnauthorizedError, message
14
+ raise Restforce::UnauthorizedError.new(message, response_values)
14
15
  when 404
15
- raise Faraday::Error::ResourceNotFound, message
16
+ raise Restforce::NotFoundError.new(message, response_values)
16
17
  when 413
17
- raise Faraday::Error::ClientError.new("413: Request Entity Too Large",
18
- response_values)
18
+ raise Restforce::EntityTooLargeError.new(
19
+ "413: Request Entity Too Large",
20
+ response_values
21
+ )
19
22
  when 400...600
20
- raise Faraday::Error::ClientError.new(message, response_values)
23
+ klass = exception_class_for_error_code(body['errorCode'])
24
+ raise klass.new(message, response_values)
21
25
  end
22
26
  end
23
27
 
24
28
  def message
25
- "#{body['errorCode']}: #{body['message']}"
29
+ message = "#{body['errorCode']}: #{body['message']}"
30
+ message << "\nRESPONSE: #{JSON.dump(@env[:body])}"
31
+ rescue StandardError
32
+ message # if JSON.dump fails, return message without extra detail
26
33
  end
27
34
 
28
35
  def body
@@ -43,5 +50,13 @@ module Restforce
43
50
  body: @env[:body]
44
51
  }
45
52
  end
53
+
54
+ ERROR_CODE_MATCHER = /\A[A-Z_]+\z/.freeze
55
+
56
+ def exception_class_for_error_code(error_code)
57
+ return Restforce::ResponseError unless ERROR_CODE_MATCHER.match?(error_code)
58
+
59
+ Restforce::ErrorCode.get_exception_class(error_code)
60
+ end
46
61
  end
47
62
  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
@@ -27,6 +27,7 @@ module Restforce
27
27
  # Returns nil if the signed request is invalid.
28
28
  def decode
29
29
  return nil if signature != hmac
30
+
30
31
  JSON.parse(Base64.decode64(payload))
31
32
  end
32
33
 
@@ -63,6 +63,7 @@ module Restforce
63
63
 
64
64
  def ensure_id
65
65
  return true if self.Id?
66
+
66
67
  raise ArgumentError, 'You need to query the Id for the record first.'
67
68
  end
68
69
  end
@@ -5,9 +5,9 @@ module Restforce
5
5
  class Client < AbstractClient
6
6
  private
7
7
 
8
- def api_path(path)
9
- super("tooling/#{path}")
10
- end
8
+ def api_path(path)
9
+ super("tooling/#{path}")
10
+ end
11
11
  end
12
12
  end
13
13
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Restforce
4
- VERSION = '3.0.1'
4
+ VERSION = '5.1.1'
5
5
  end
data/lib/restforce.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require 'faraday'
4
4
  require 'faraday_middleware'
5
5
  require 'json'
6
+ require 'jwt'
6
7
 
7
8
  require 'restforce/version'
8
9
  require 'restforce/config'
@@ -14,7 +15,8 @@ module Restforce
14
15
  autoload :Middleware, 'restforce/middleware'
15
16
  autoload :Attachment, 'restforce/attachment'
16
17
  autoload :Document, 'restforce/document'
17
- autoload :UploadIO, 'restforce/upload_io'
18
+ autoload :FilePart, 'restforce/file_part'
19
+ autoload :UploadIO, 'restforce/file_part' # Deprecated
18
20
  autoload :SObject, 'restforce/sobject'
19
21
  autoload :Client, 'restforce/client'
20
22
  autoload :Mash, 'restforce/mash'
@@ -29,6 +31,7 @@ module Restforce
29
31
  autoload :Verbs, 'restforce/concerns/verbs'
30
32
  autoload :Base, 'restforce/concerns/base'
31
33
  autoload :API, 'restforce/concerns/api'
34
+ autoload :BatchAPI, 'restforce/concerns/batch_api'
32
35
  end
33
36
 
34
37
  module Data
@@ -42,8 +45,23 @@ module Restforce
42
45
  Error = Class.new(StandardError)
43
46
  ServerError = Class.new(Error)
44
47
  AuthenticationError = Class.new(Error)
45
- UnauthorizedError = Class.new(Error)
48
+ UnauthorizedError = Class.new(Faraday::ClientError)
46
49
  APIVersionError = Class.new(Error)
50
+ BatchAPIError = Class.new(Error)
51
+
52
+ # Inherit from Faraday::ResourceNotFound for backwards-compatibility
53
+ # Consumers of this library that rescue and handle Faraday::ResourceNotFound
54
+ # can continue to do so.
55
+ NotFoundError = Class.new(Faraday::ResourceNotFound)
56
+
57
+ # Inherit from Faraday::ClientError for backwards-compatibility
58
+ # Consumers of this library that rescue and handle Faraday::ClientError
59
+ # can continue to do so.
60
+ ResponseError = Class.new(Faraday::ClientError)
61
+ MatchesMultipleError= Class.new(ResponseError)
62
+ EntityTooLargeError = Class.new(ResponseError)
63
+
64
+ require 'restforce/error_code'
47
65
 
48
66
  class << self
49
67
  # Alias for Restforce::Data::Client.new
@@ -74,7 +92,7 @@ module Restforce
74
92
  self
75
93
  end
76
94
  end
77
- Object.send :include, Restforce::CoreExtensions unless Object.respond_to? :tap
95
+ Object.include Restforce::CoreExtensions unless Object.respond_to? :tap
78
96
  end
79
97
 
80
98
  if ENV['PROXY_URI']
data/restforce.gemspec CHANGED
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require File.expand_path('../lib/restforce/version', __FILE__)
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,19 +22,10 @@ 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.3'
25
+ gem.required_ruby_version = '>= 2.6'
26
26
 
27
- gem.add_dependency 'faraday', '<= 1.0', '>= 0.9.0'
28
- gem.add_dependency 'faraday_middleware', ['>= 0.8.8', '<= 1.0']
29
-
30
- gem.add_dependency 'json', '>= 1.7.5'
31
-
32
- gem.add_dependency 'hashie', ['>= 1.2.0', '< 4.0']
33
-
34
- gem.add_development_dependency 'rspec', '~> 2.14.0'
35
- gem.add_development_dependency 'webmock', '~> 3.4.0'
36
- gem.add_development_dependency 'simplecov', '~> 0.15.0'
37
- gem.add_development_dependency 'rubocop', '~> 0.50.0'
38
- gem.add_development_dependency 'rspec_junit_formatter', '~> 0.3.0'
39
- gem.add_development_dependency 'faye' unless RUBY_PLATFORM == 'java'
27
+ gem.add_dependency 'faraday', '<= 2.0', '>= 0.9.0'
28
+ gem.add_dependency 'faraday_middleware', ['>= 0.8.8', '<= 2.0']
29
+ gem.add_dependency 'hashie', '>= 1.2.0', '< 5.0'
30
+ gem.add_dependency 'jwt', ['>= 1.5.6']
40
31
  end
@@ -0,0 +1,27 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ MIIEpAIBAAKCAQEAy3KYqxZIgVDgFwdA+OQcKMJQu3iUTlyCSk9b3RLBOudnvk8u
3
+ n0ShtKkOKB4b4RZeedcrlKESoak/6NS+M7CDemRT0EagqUiz/ZsZxB2KUp7au+d8
4
+ 0KWX99/loBjDttuon8ITDw2WFC9X0+TZqfsXcQ0iV1/9Sf8WHShd8ZqShjJBlEvf
5
+ 7u7VdNW8dXrl+4cvpPzspVxg6jVotEpmp875jmGRvshgx0iz0jtfAyxaaKStITC6
6
+ MxufVNDgIYQDl6queh8b9noDLtt17Eq6YnropYN1hOjaLtoLBP7AN2gsXG7N3vqC
7
+ JG619W9X4zCmKztv4oGjymInrS2msC2J02dNGQIDAQABAoIBAAurTARsJ8Z7DA9m
8
+ FBzygIb59kV6eg8wkSyP9rXscHbfdPzeb88k0Z2aILy+VV0IumyEofRJdNce7RJ+
9
+ uVYfprrrbD9C/c4X5HMEZWrxQtDQWb1zXp5dESVfiz0ujnM7kCVxrUQsxFHuETyP
10
+ IMj2JPcQCMs4L0ACSJNtkE3eTs8xko5kwDHZGiLTi5jD1bLgaHl1A+9CTU8LosTy
11
+ hEIrNSZfNidDPU4QSbwoElYZxpDMSbtyHaIk1WHz7zLzWoogK3x5AIQh64wWAQVd
12
+ zzlp2j2jSM7oQ9j+k1aNiUBdDoRX53jmaIwE/1WDW/LT33qAoqRw+5qHeLRoRcfu
13
+ 3uj/WI0CgYEA6wnpIUhqqWT+febhXtCr1mAJlAJpzUUQncN6Zk0Kj/kE1V52OqgL
14
+ gtOactII7J3+0zK7KGptqseTank0ghmGNdRBQ7+1JTQhpjLrCm/huKDhl+sBk95u
15
+ opxw/ZTwMFYPwsmZlFcy4uWRjtI+QzaV+2Xk5JF57H8vUiX/+XqseQcCgYEA3Zdw
16
+ zVHpcVPlyiXCbSvwb9IYXiJaQl/Rg96Klxah3MZNyRRKe5IoKUTJwEDuQ1MAHrve
17
+ cWrNLcXhX6r/PzIXSSLe71wgwpn7UcaqWzZJqqN7OIGEeTzYWbB6tGhse7Dw7tWB
18
+ hRkQSE0LPzZqboHz5msRM02sa61qiI5+ASJvIN8CgYEAvT+IoEzv3R89ruBVPQPm
19
+ KMHBVJSw3iArJex8xJxp0c0fMDJUHhyq0BdTd/pYRzVcNm/VtNAlJ2p07zlSpyKo
20
+ JvWV61gUIjWclnbPO+MkK4YWvzzxUz+5c2NlszjWQQU6wYuUBpZDmeBg2E++5F2y
21
+ W+8KY2QjeOJbltiUCCvXbccCgYEAqARYB5aARumyZqBS16xlVqQazeWGQqWcmzx2
22
+ ITGL8XZ7LGgyQZgE06XQw/F3t5yLjsIsXBr7ECXmST/C4gv9E/tYxm04edV/dfYI
23
+ 3bhACx6CI8owxCyabwcdQwWam/8B8FX7KwxiCDBCwt9ju/7VDHVKSXgvsEWBbaF9
24
+ cSbG1EkCgYBZFztTUnD/cLMcvLUegN0K+6Qa3x3nRSrlrJ+v51mU1X8G8qNyFO67
25
+ gUq9h4xbCl4Z5ZTuFKXwPM4XaMzfYdrWNS2zl5IG14FXS077GhDKe062b9mFoxtm
26
+ aViCit4Hm8xpLTS8x9KB7yYAiF9sR/GklW1SUCIqnpL9JShkhzjfZw==
27
+ -----END RSA PRIVATE KEY-----
@@ -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(
99
- File.expand_path('../../fixtures/blob.jpg', __FILE__),
98
+ Blob: Restforce::FilePart.new(
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
 
@@ -117,18 +129,15 @@ shared_examples_for Restforce::AbstractClient do
117
129
  JSON.parse(fixture('sobject/delete_error_response'))
118
130
  end
119
131
 
120
- subject do
121
- lambda do
122
- client.update!('Account', Id: '001D000000INjVe', Name: 'Foobar')
123
- end
124
- end
132
+ it "raises Faraday::ResourceNotFound" do
133
+ expect { client.update!('Account', Id: '001D000000INjVe', Name: 'Foobar') }.
134
+ to raise_error do |exception|
135
+ expect(exception).to be_a(Faraday::ResourceNotFound)
125
136
 
126
- it {
127
- should raise_error(
128
- Faraday::Error::ResourceNotFound,
129
- "#{error.first['errorCode']}: #{error.first['message']}"
130
- )
131
- }
137
+ expect(exception.message).
138
+ to start_with("#{error.first['errorCode']}: #{error.first['message']}")
139
+ end
140
+ end
132
141
  end
133
142
  end
134
143
 
@@ -146,7 +155,7 @@ shared_examples_for Restforce::AbstractClient do
146
155
  fixture: 'sobject/delete_error_response'
147
156
 
148
157
  subject { client.update('Account', Id: '001D000000INjVe', Name: 'Foobar') }
149
- it { should be_false }
158
+ it { should be false }
150
159
  end
151
160
 
152
161
  context 'with success' do
@@ -160,7 +169,7 @@ shared_examples_for Restforce::AbstractClient do
160
169
  client.update('Account', key => '001D000000INjVe', :Name => 'Foobar')
161
170
  end
162
171
 
163
- it { should be_true }
172
+ it { should be true }
164
173
  end
165
174
  end
166
175
  end
@@ -179,7 +188,7 @@ shared_examples_for Restforce::AbstractClient do
179
188
  Name: 'Foobar')
180
189
  end
181
190
 
182
- it { should be_true }
191
+ it { should be true }
183
192
  end
184
193
 
185
194
  context 'with string external Id key' do
@@ -188,7 +197,7 @@ shared_examples_for Restforce::AbstractClient do
188
197
  'Name' => 'Foobar')
189
198
  end
190
199
 
191
- it { should be_true }
200
+ it { should be true }
192
201
  end
193
202
  end
194
203
 
@@ -209,6 +218,24 @@ shared_examples_for Restforce::AbstractClient do
209
218
  end
210
219
  end
211
220
  end
221
+
222
+ context 'when created with a space in the id' do
223
+ requests 'sobjects/Account/External__c/foo%20bar',
224
+ method: :patch,
225
+ with_body: "{\"Name\":\"Foobar\"}",
226
+ fixture: 'sobject/upsert_created_success_response'
227
+
228
+ [:External__c, 'External__c', :external__c, 'external__c'].each do |key|
229
+ context "with #{key.inspect} as the external id" do
230
+ subject do
231
+ client.upsert!('Account', 'External__c', key => 'foo bar',
232
+ :Name => 'Foobar')
233
+ end
234
+
235
+ it { should eq 'foo' }
236
+ end
237
+ end
238
+ end
212
239
  end
213
240
 
214
241
  describe '.destroy!' do
@@ -221,13 +248,20 @@ shared_examples_for Restforce::AbstractClient do
221
248
  status: 404
222
249
 
223
250
  subject { lambda { destroy! } }
224
- it { should raise_error Faraday::Error::ResourceNotFound }
251
+ it { should raise_error Faraday::ResourceNotFound }
225
252
  end
226
253
 
227
254
  context 'with success' do
228
255
  requests 'sobjects/Account/001D000000INjVe', method: :delete
229
256
 
230
- it { should be_true }
257
+ it { should be true }
258
+ end
259
+
260
+ context 'with a space in the id' do
261
+ subject(:destroy!) { client.destroy!('Account', '001D000000 INjVe') }
262
+ requests 'sobjects/Account/001D000000%20INjVe', method: :delete
263
+
264
+ it { should be true }
231
265
  end
232
266
  end
233
267
 
@@ -240,13 +274,13 @@ shared_examples_for Restforce::AbstractClient do
240
274
  method: :delete,
241
275
  status: 404
242
276
 
243
- it { should be_false }
277
+ it { should be false }
244
278
  end
245
279
 
246
280
  context 'with success' do
247
281
  requests 'sobjects/Account/001D000000INjVe', method: :delete
248
282
 
249
- it { should be_true }
283
+ it { should be true }
250
284
  end
251
285
  end
252
286
 
@@ -266,6 +300,14 @@ shared_examples_for Restforce::AbstractClient do
266
300
  subject { client.find('Account', '1234', 'External_Field__c') }
267
301
  it { should be_a Hash }
268
302
  end
303
+
304
+ context 'with a space in an external id' do
305
+ requests 'sobjects/Account/External_Field__c/12%2034',
306
+ fixture: 'sobject/sobject_find_success_response'
307
+
308
+ subject { client.find('Account', '12 34', 'External_Field__c') }
309
+ it { should be_a Hash }
310
+ end
269
311
  end
270
312
 
271
313
  describe '.select' do
@@ -284,6 +326,14 @@ shared_examples_for Restforce::AbstractClient do
284
326
  subject { client.select('Account', '1234', ['External_Field__c']) }
285
327
  it { should be_a Hash }
286
328
  end
329
+
330
+ context 'with a space in the id' do
331
+ requests 'sobjects/Account/12%2034',
332
+ fixture: 'sobject/sobject_select_success_response'
333
+
334
+ subject { client.select('Account', '12 34', nil, nil) }
335
+ it { should be_a Hash }
336
+ end
287
337
  end
288
338
 
289
339
  context 'when an external id is specified' do
@@ -315,8 +365,8 @@ shared_examples_for Restforce::AbstractClient do
315
365
  before do
316
366
  @request = stub_login_request(
317
367
  with_body: "grant_type=password&client_id=client_id" \
318
- "&client_secret=client_secret&username=foo" \
319
- "&password=barsecurity_token"
368
+ "&client_secret=client_secret&username=foo" \
369
+ "&password=barsecurity_token"
320
370
  ).to_return(status: 200, body: fixture(:auth_success_response))
321
371
  end
322
372
 
@@ -367,8 +417,8 @@ shared_examples_for Restforce::AbstractClient do
367
417
 
368
418
  @query_request = stub_login_request(
369
419
  with_body: "grant_type=password&client_id=client_id" \
370
- "&client_secret=client_secret&username=foo&" \
371
- "password=barsecurity_token"
420
+ "&client_secret=client_secret&username=foo&" \
421
+ "password=barsecurity_token"
372
422
  ).to_return(status: 200, body: fixture(:auth_success_response))
373
423
  end
374
424
 
@@ -384,15 +434,15 @@ shared_examples_for Restforce::AbstractClient do
384
434
  @query = stub_api_request('query\?q=SELECT%20some,%20fields%20FROM%20object').
385
435
  with(headers: { 'Authorization' => "OAuth #{oauth_token}" }).
386
436
  to_return(status: 401,
387
- body: fixture('expired_session_response'),
388
- headers: { 'Content-Type' => 'application/json' }).then.
437
+ body: fixture('expired_session_response'),
438
+ headers: { 'Content-Type' => 'application/json' }).then.
389
439
  to_return(status: 200,
390
- body: fixture('sobject/query_success_response'),
391
- headers: { 'Content-Type' => 'application/json' })
440
+ body: fixture('sobject/query_success_response'),
441
+ headers: { 'Content-Type' => 'application/json' })
392
442
 
393
443
  @login = stub_login_request(
394
444
  with_body: "grant_type=password&client_id=client_id&client_secret=" \
395
- "client_secret&username=foo&password=barsecurity_token"
445
+ "client_secret&username=foo&password=barsecurity_token"
396
446
  ).to_return(status: 200, body: fixture(:auth_success_response))
397
447
  end
398
448