restforce 4.0.0 → 5.0.0

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 (48) 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 +36 -0
  7. data/CONTRIBUTING.md +1 -1
  8. data/Gemfile +1 -1
  9. data/README.md +71 -33
  10. data/UPGRADING.md +38 -0
  11. data/lib/restforce.rb +11 -13
  12. data/lib/restforce/collection.rb +5 -0
  13. data/lib/restforce/concerns/api.rb +1 -1
  14. data/lib/restforce/concerns/authentication.rb +10 -0
  15. data/lib/restforce/concerns/base.rb +2 -0
  16. data/lib/restforce/concerns/caching.rb +7 -0
  17. data/lib/restforce/concerns/connection.rb +3 -3
  18. data/lib/restforce/concerns/streaming.rb +22 -7
  19. data/lib/restforce/config.rb +6 -0
  20. data/lib/restforce/error_code.rb +406 -0
  21. data/lib/restforce/file_part.rb +24 -0
  22. data/lib/restforce/mash.rb +1 -1
  23. data/lib/restforce/middleware/authentication.rb +7 -3
  24. data/lib/restforce/middleware/authentication/jwt_bearer.rb +38 -0
  25. data/lib/restforce/middleware/caching.rb +1 -1
  26. data/lib/restforce/middleware/instance_url.rb +1 -1
  27. data/lib/restforce/middleware/raise_error.rb +3 -4
  28. data/lib/restforce/version.rb +1 -1
  29. data/restforce.gemspec +14 -14
  30. data/spec/fixtures/test_private.key +27 -0
  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/authentication_spec.rb +35 -0
  37. data/spec/unit/concerns/caching_spec.rb +26 -0
  38. data/spec/unit/concerns/connection_spec.rb +2 -2
  39. data/spec/unit/concerns/streaming_spec.rb +58 -30
  40. data/spec/unit/error_code_spec.rb +61 -0
  41. data/spec/unit/mash_spec.rb +5 -0
  42. data/spec/unit/middleware/authentication/jwt_bearer_spec.rb +62 -0
  43. data/spec/unit/middleware/authentication_spec.rb +27 -4
  44. data/spec/unit/middleware/raise_error_spec.rb +19 -10
  45. data/spec/unit/signed_request_spec.rb +1 -1
  46. data/spec/unit/sobject_spec.rb +2 -5
  47. metadata +41 -31
  48. 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
@@ -28,7 +28,7 @@ module Restforce
28
28
  # of sobject records.
29
29
  Restforce::Collection
30
30
  elsif val.key? 'attributes'
31
- case (val['attributes']['type'])
31
+ case val.dig('attributes', 'type')
32
32
  when "Attachment"
33
33
  Restforce::Attachment
34
34
  when "Document"
@@ -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.
@@ -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
@@ -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,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.0.0'
4
+ VERSION = '5.0.0'
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,19 +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'
30
+ gem.add_dependency 'jwt', ['>= 1.5.6']
31
31
 
32
- gem.add_dependency 'hashie', ['>= 1.2.0', '< 4.0']
32
+ gem.add_dependency 'hashie', '>= 1.2.0', '< 5.0'
33
33
 
34
34
  gem.add_development_dependency 'faye' unless RUBY_PLATFORM == 'java'
35
35
  gem.add_development_dependency 'rspec', '~> 2.14.0'
36
- gem.add_development_dependency 'rspec_junit_formatter', '~> 0.3.0'
37
- gem.add_development_dependency 'rubocop', '~> 0.75.0'
38
- gem.add_development_dependency 'simplecov', '~> 0.17.1'
39
- gem.add_development_dependency 'webmock', '~> 3.7.6'
36
+ gem.add_development_dependency 'rspec_junit_formatter', '~> 0.4.1'
37
+ gem.add_development_dependency 'rubocop', '~> 0.87.1'
38
+ gem.add_development_dependency 'simplecov', '~> 0.18.2'
39
+ gem.add_development_dependency 'webmock', '~> 3.8.3'
40
40
  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(
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))
@@ -52,6 +52,16 @@ describe Restforce::Concerns::Authentication do
52
52
 
53
53
  it { should eq Restforce::Middleware::Authentication::Token }
54
54
  end
55
+
56
+ context 'when jwt option is provided' do
57
+ before do
58
+ client.stub username_password?: false
59
+ client.stub oauth_refresh?: false
60
+ client.stub jwt?: true
61
+ end
62
+
63
+ it { should eq Restforce::Middleware::Authentication::JWTBearer }
64
+ end
55
65
  end
56
66
 
57
67
  describe '.username_password?' do
@@ -100,4 +110,29 @@ describe Restforce::Concerns::Authentication do
100
110
  it { should_not be_true }
101
111
  end
102
112
  end
113
+
114
+ describe '.jwt?' do
115
+ subject { client.jwt? }
116
+ let(:options) do
117
+ {}
118
+ end
119
+
120
+ before do
121
+ client.stub options: options
122
+ end
123
+
124
+ context 'when jwt options are provided' do
125
+ let(:options) do
126
+ { jwt_key: 'JWT_PRIVATE_KEY',
127
+ username: 'foo',
128
+ client_id: 'client' }
129
+ end
130
+
131
+ it { should be_true }
132
+ end
133
+
134
+ context 'when jwt options are not provided' do
135
+ it { should_not be_true }
136
+ end
137
+ end
103
138
  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
@@ -4,18 +4,19 @@ require 'spec_helper'
4
4
 
5
5
  describe Restforce::Concerns::Streaming, event_machine: true do
6
6
  describe '.subscribe' do
7
- let(:channels) { %w[channel1 channel2] }
8
- let(:topics) { channels.map { |c| "/topic/#{c}" } }
7
+ let(:channels) do
8
+ ['/topic/topic1', '/event/MyCustomEvent__e', '/data/ChangeEvents']
9
+ end
9
10
  let(:subscribe_block) { lambda { 'subscribe' } }
10
11
  let(:faye_double) { double('Faye') }
11
12
 
12
13
  it 'subscribes to the topics with faye' do
13
14
  faye_double.
14
15
  should_receive(:subscribe).
15
- with(topics, &subscribe_block)
16
+ with(channels, &subscribe_block)
16
17
  client.stub faye: faye_double
17
18
 
18
- client.subscribe(channels, &subscribe_block)
19
+ client.subscription(channels, &subscribe_block)
19
20
  end
20
21
 
21
22
  context "replay_handlers" do
@@ -25,24 +26,51 @@ describe Restforce::Concerns::Streaming, event_machine: true do
25
26
  }
26
27
 
27
28
  it 'registers nil handlers when no replay option is given' do
28
- client.subscribe(channels, &subscribe_block)
29
- client.replay_handlers.should eq('channel1' => nil, 'channel2' => nil)
29
+ client.subscription(channels, &subscribe_block)
30
+ client.replay_handlers.should eq(
31
+ '/topic/topic1' => nil,
32
+ '/event/MyCustomEvent__e' => nil,
33
+ '/data/ChangeEvents' => nil
34
+ )
30
35
  end
31
36
 
32
37
  it 'registers a replay_handler for each channel given' do
33
- client.subscribe(channels, replay: -2, &subscribe_block)
34
- client.replay_handlers.should eq('channel1' => -2, 'channel2' => -2)
38
+ client.subscription(channels, replay: -2, &subscribe_block)
39
+ client.replay_handlers.should eq(
40
+ '/topic/topic1' => -2,
41
+ '/event/MyCustomEvent__e' => -2,
42
+ '/data/ChangeEvents' => -2
43
+ )
35
44
  end
36
45
 
37
46
  it 'replaces earlier handlers in subsequent calls' do
38
- client.subscribe(%w[channel1 channel2], replay: 2, &subscribe_block)
39
- client.subscribe(%w[channel2 channel3], replay: 3, &subscribe_block)
47
+ client.subscription(
48
+ ['/topic/channel1', '/topic/channel2'],
49
+ replay: 2,
50
+ &subscribe_block
51
+ )
52
+ client.subscription(
53
+ ['/topic/channel2', '/topic/channel3'],
54
+ replay: 3,
55
+ &subscribe_block
56
+ )
57
+
40
58
  client.replay_handlers.should eq(
41
- 'channel1' => 2,
42
- 'channel2' => 3,
43
- 'channel3' => 3
59
+ '/topic/channel1' => 2,
60
+ '/topic/channel2' => 3,
61
+ '/topic/channel3' => 3
44
62
  )
45
63
  end
64
+
65
+ context 'backwards compatibility' do
66
+ it 'it assumes channels are push topics' do
67
+ client.subscribe(%w[channel1 channel2], replay: -2, &subscribe_block)
68
+ client.replay_handlers.should eq(
69
+ '/topic/channel1' => -2,
70
+ '/topic/channel2' => -2
71
+ )
72
+ end
73
+ end
46
74
  end
47
75
  end
48
76
 
@@ -87,41 +115,41 @@ describe Restforce::Concerns::Streaming, event_machine: true do
87
115
  let(:extension) { Restforce::Concerns::Streaming::ReplayExtension.new(handlers) }
88
116
 
89
117
  it 'sends nil without a specified handler' do
90
- output = subscribe(extension, to: "channel1")
118
+ output = subscribe(extension, to: "/topic/channel1")
91
119
  read_replay(output).should eq('/topic/channel1' => nil)
92
120
  end
93
121
 
94
122
  it 'with a scalar replay id' do
95
- handlers['channel1'] = -2
96
- output = subscribe(extension, to: "channel1")
123
+ handlers['/topic/channel1'] = -2
124
+ output = subscribe(extension, to: "/topic/channel1")
97
125
  read_replay(output).should eq('/topic/channel1' => -2)
98
126
  end
99
127
 
100
128
  it 'with a hash' do
101
- hash_handler = { 'channel1' => -1, 'channel2' => -2 }
129
+ hash_handler = { '/topic/channel1' => -1, '/topic/channel2' => -2 }
102
130
 
103
- handlers['channel1'] = hash_handler
104
- handlers['channel2'] = hash_handler
131
+ handlers['/topic/channel1'] = hash_handler
132
+ handlers['/topic/channel2'] = hash_handler
105
133
 
106
- output = subscribe(extension, to: "channel1")
134
+ output = subscribe(extension, to: "/topic/channel1")
107
135
  read_replay(output).should eq('/topic/channel1' => -1)
108
136
 
109
- output = subscribe(extension, to: "channel2")
137
+ output = subscribe(extension, to: "/topic/channel2")
110
138
  read_replay(output).should eq('/topic/channel2' => -2)
111
139
  end
112
140
 
113
141
  it 'with an object' do
114
142
  custom_handler = double('custom_handler')
115
143
  custom_handler.should_receive(:[]).and_return(123)
116
- handlers['channel1'] = custom_handler
144
+ handlers['/topic/channel1'] = custom_handler
117
145
 
118
- output = subscribe(extension, to: "channel1")
146
+ output = subscribe(extension, to: "/topic/channel1")
119
147
  read_replay(output).should eq('/topic/channel1' => 123)
120
148
  end
121
149
 
122
150
  it 'remembers the last replayId' do
123
- handler = { 'channel1' => 41 }
124
- handlers['channel1'] = handler
151
+ handler = { '/topic/channel1' => 41 }
152
+ handlers['/topic/channel1'] = handler
125
153
  message = {
126
154
  'channel' => '/topic/channel1',
127
155
  'data' => {
@@ -130,12 +158,12 @@ describe Restforce::Concerns::Streaming, event_machine: true do
130
158
  }
131
159
 
132
160
  extension.incoming(message, ->(m) {})
133
- handler.should eq('channel1' => 42)
161
+ handler.should eq('/topic/channel1' => 42)
134
162
  end
135
163
 
136
164
  it 'when an incoming message has no replayId' do
137
- handler = { 'channel1' => 41 }
138
- handlers['channel1'] = handler
165
+ handler = { '/topic/channel1' => 41 }
166
+ handlers['/topic/channel1'] = handler
139
167
 
140
168
  message = {
141
169
  'channel' => '/topic/channel1',
@@ -143,7 +171,7 @@ describe Restforce::Concerns::Streaming, event_machine: true do
143
171
  }
144
172
 
145
173
  extension.incoming(message, ->(m) {})
146
- handler.should eq('channel1' => 41)
174
+ handler.should eq('/topic/channel1' => 41)
147
175
  end
148
176
 
149
177
  private
@@ -152,7 +180,7 @@ describe Restforce::Concerns::Streaming, event_machine: true do
152
180
  output = nil
153
181
  message = {
154
182
  'channel' => '/meta/subscribe',
155
- 'subscription' => "/topic/#{options[:to]}"
183
+ 'subscription' => options[:to]
156
184
  }
157
185
  extension.outgoing(message, ->(m) {
158
186
  output = m