restforce 3.0.1 → 5.1.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -98,17 +98,21 @@ shared_examples_for Restforce::Data::Client do
98
98
  end
99
99
 
100
100
  describe '.subscribe', event_machine: true do
101
+ let(:faye_double) { double('Faye') }
102
+
101
103
  context 'when given a single pushtopic' do
102
104
  it 'subscribes to the pushtopic' do
103
- client.faye.should_receive(:subscribe).with(['/topic/PushTopic'])
105
+ faye_double.should_receive(:subscribe).with(['/topic/PushTopic'])
106
+ client.stub faye: faye_double
104
107
  client.subscribe('PushTopic')
105
108
  end
106
109
  end
107
110
 
108
111
  context 'when given an array of pushtopics' do
109
112
  it 'subscribes to each pushtopic' do
110
- client.faye.should_receive(:subscribe).with(['/topic/PushTopic1',
113
+ faye_double.should_receive(:subscribe).with(['/topic/PushTopic1',
111
114
  '/topic/PushTopic2'])
115
+ client.stub faye: faye_double
112
116
  client.subscribe(%w[PushTopic1 PushTopic2])
113
117
  end
114
118
  end
data/spec/spec_helper.rb CHANGED
@@ -8,6 +8,8 @@ Bundler.require :default, :test
8
8
  require 'faye' unless RUBY_PLATFORM == 'java'
9
9
 
10
10
  require 'webmock/rspec'
11
+ require 'rspec/collection_matchers'
12
+ require 'rspec/its'
11
13
 
12
14
  WebMock.disable_net_connect!
13
15
 
@@ -15,8 +17,29 @@ RSpec.configure do |config|
15
17
  config.order = 'random'
16
18
  config.filter_run focus: true
17
19
  config.run_all_when_everything_filtered = true
20
+
21
+ original_stderr = $stderr
22
+ original_stdout = $stdout
23
+ config.before(:all) do
24
+ # Redirect stderr and stdout
25
+ $stderr = File.open(File::NULL, "w")
26
+ $stdout = File.open(File::NULL, "w")
27
+ end
28
+ config.after(:all) do
29
+ $stderr = original_stderr
30
+ $stdout = original_stdout
31
+ end
32
+
33
+ config.expect_with :rspec do |expectations|
34
+ expectations.syntax = %i[expect should]
35
+ end
36
+
37
+ config.mock_with :rspec do |mocks|
38
+ mocks.syntax = %i[expect should]
39
+ end
18
40
  end
19
41
 
20
42
  # Requires supporting ruby files with custom matchers and macros, etc,
21
43
  # in spec/support/ and its subdirectories.
22
- Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each { |f| require f }
44
+ paths = Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")]
45
+ paths.sort.each { |f| require f }
@@ -5,7 +5,7 @@ module ClientIntegrationExampleGroup
5
5
  base.class_eval do
6
6
  let(:oauth_token) do
7
7
  '00Dx0000000BV7z!AR8AQAxo9UfVkh8AlV0Gomt9Czx9LjHnSSpwBMmbRcgKFmxOtvxjTrKW19ye6P' \
8
- 'E3Ds1eQz3z8jr3W7_VbWmEu4Q8TVGSTHxs'
8
+ 'E3Ds1eQz3z8jr3W7_VbWmEu4Q8TVGSTHxs'
9
9
  end
10
10
 
11
11
  let(:refresh_token) { 'refresh' }
@@ -39,13 +39,13 @@ module ClientIntegrationExampleGroup
39
39
  end
40
40
 
41
41
  RSpec.configure do |config|
42
+ describes = lambda do |described|
43
+ described <= Restforce::AbstractClient
44
+ end
45
+
42
46
  config.include self,
43
- example_group: {
44
- describes: lambda do |described|
45
- described <= Restforce::AbstractClient
46
- end,
47
- file_path: %r{spec/integration}
48
- }
47
+ file_path: %r{spec/integration},
48
+ describes: describes
49
49
 
50
50
  config.before mashify: false do
51
51
  base_options.merge!(mashify: false)
@@ -15,6 +15,6 @@ module ConcernsExampleGroup
15
15
  end
16
16
 
17
17
  RSpec.configure do |config|
18
- config.include self, example_group: { file_path: %r{spec/unit/concerns} }
18
+ config.include self, file_path: %r{spec/unit/concerns}
19
19
  end
20
20
  end
@@ -22,13 +22,11 @@ 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
- def fixture(f)
31
- File.read(File.expand_path("../../fixtures/#{f}.json", __FILE__))
28
+ def fixture(filename)
29
+ File.read(File.expand_path("../../fixtures/#{filename}.json", __FILE__))
32
30
  end
33
31
  end
34
32
 
@@ -19,8 +19,7 @@ module MiddlewareExampleGroup
19
19
  end
20
20
 
21
21
  RSpec.configure do |config|
22
- config.include self,
23
- example_group: { file_path: %r{spec/unit/middleware} }
22
+ config.include self, file_path: %r{spec/unit/middleware}
24
23
  end
25
24
  end
26
25
 
@@ -13,7 +13,7 @@ describe Restforce::Collection do
13
13
 
14
14
  it { should respond_to :each }
15
15
  its(:size) { should eq 1 }
16
- its(:has_next_page?) { should be_false }
16
+ its(:has_next_page?) { should be false }
17
17
  it { should have_client client }
18
18
  its(:page_size) { should eq 1 }
19
19
 
@@ -56,10 +56,28 @@ describe Restforce::Collection do
56
56
  should(be_all { |page| expect(page).to be_a Restforce::Collection })
57
57
  end
58
58
 
59
- its(:has_next_page?) { should be_true }
59
+ its(:has_next_page?) { should be true }
60
60
  it { should(be_all { |record| expect(record).to be_a Restforce::SObject }) }
61
61
  its(:next_page) { should be_a Restforce::Collection }
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
@@ -11,7 +11,7 @@ describe Restforce::Concerns::API do
11
11
  it 'returns the user info from identity url' do
12
12
  identity_url = double('identity_url')
13
13
  response.body.stub(:identity).and_return(identity_url)
14
- client.should_receive(:api_get).with.and_return(response)
14
+ client.should_receive(:api_get).with(no_args).and_return(response)
15
15
 
16
16
  identity = double('identity')
17
17
  identity.stub(:body).and_return(identity)
@@ -268,15 +268,15 @@ describe Restforce::Concerns::API do
268
268
 
269
269
  it "delegates to :#{method}!" do
270
270
  client.should_receive(:"#{method}!").
271
- with(*args).
271
+ with(no_args).
272
272
  and_return(response)
273
273
  expect(result).to eq response
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
- with(*args).
279
+ with(no_args).
280
280
  and_raise(exception_klass.new(nil))
281
281
  expect(result).to eq false
282
282
  end
@@ -314,7 +314,7 @@ describe Restforce::Concerns::API do
314
314
  it 'sends an HTTP PATCH, and returns true' do
315
315
  client.should_receive(:api_patch).
316
316
  with('sobjects/Whizbang/1234', StageName: "Call Scheduled")
317
- expect(result).to be_true
317
+ expect(result).to be true
318
318
  end
319
319
  end
320
320
 
@@ -324,7 +324,7 @@ describe Restforce::Concerns::API do
324
324
  it 'sends an HTTP PATCH, and encodes the ID' do
325
325
  client.should_receive(:api_patch).
326
326
  with('sobjects/Whizbang/1234%2F%3Fabc', StageName: "Call Scheduled")
327
- expect(result).to be_true
327
+ expect(result).to be true
328
328
  end
329
329
  end
330
330
 
@@ -348,7 +348,7 @@ describe Restforce::Concerns::API do
348
348
  client.should_receive(:api_patch).
349
349
  with('sobjects/Whizbang/External_ID__c/1234', {}).
350
350
  and_return(response)
351
- expect(result).to be_true
351
+ expect(result).to be true
352
352
  end
353
353
 
354
354
  context 'and the response body is a string' do
@@ -357,7 +357,7 @@ describe Restforce::Concerns::API do
357
357
  client.should_receive(:api_patch).
358
358
  with('sobjects/Whizbang/External_ID__c/1234', {}).
359
359
  and_return(response)
360
- expect(result).to be_true
360
+ expect(result).to be true
361
361
  end
362
362
  end
363
363
  end
@@ -431,7 +431,7 @@ describe Restforce::Concerns::API do
431
431
  client.should_receive(:api_patch).
432
432
  with('sobjects/Whizbang/External_ID__c/%E3%81%82', {}).
433
433
  and_return(response)
434
- expect(result).to be_true
434
+ expect(result).to be true
435
435
  end
436
436
  end
437
437
  end
@@ -448,7 +448,7 @@ describe Restforce::Concerns::API do
448
448
  client.should_receive(:api_patch).
449
449
  with('sobjects/Whizbang/External_ID__c/1234', {}).
450
450
  and_return(response)
451
- expect(result).to be_true
451
+ expect(result).to be true
452
452
  end
453
453
  end
454
454
  end
@@ -462,7 +462,7 @@ describe Restforce::Concerns::API do
462
462
  it 'sends and HTTP delete, and returns true' do
463
463
  client.should_receive(:api_delete).
464
464
  with('sobjects/Whizbang/1234')
465
- expect(result).to be_true
465
+ expect(result).to be true
466
466
  end
467
467
 
468
468
  context 'when the id field contains special characters' do
@@ -471,7 +471,7 @@ describe Restforce::Concerns::API do
471
471
  it 'sends an HTTP delete, and encodes the ID' do
472
472
  client.should_receive(:api_delete).
473
473
  with('sobjects/Whizbang/1234%2F%3Fabc')
474
- expect(result).to be_true
474
+ expect(result).to be true
475
475
  end
476
476
  end
477
477
  end
@@ -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
@@ -70,11 +80,11 @@ describe Restforce::Concerns::Authentication do
70
80
  client_secret: 'secret' }
71
81
  end
72
82
 
73
- it { should be_true }
83
+ it { should be_truthy }
74
84
  end
75
85
 
76
86
  context 'when username and password options are not provided' do
77
- it { should_not be_true }
87
+ it { should_not be_truthy }
78
88
  end
79
89
  end
80
90
 
@@ -93,11 +103,36 @@ describe Restforce::Concerns::Authentication do
93
103
  client_secret: 'secret' }
94
104
  end
95
105
 
96
- it { should be_true }
106
+ it { should be_truthy }
97
107
  end
98
108
 
99
109
  context 'when oauth options are not provided' do
100
- it { should_not be_true }
110
+ it { should_not be true }
111
+ end
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_truthy }
132
+ end
133
+
134
+ context 'when jwt options are not provided' do
135
+ it { should_not be true }
101
136
  end
102
137
  end
103
138
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Restforce::Concerns::BatchAPI do
6
+ let(:endpoint) { 'composite/batch' }
7
+
8
+ before do
9
+ client.should_receive(:options).and_return(api_version: 34.0)
10
+ end
11
+
12
+ shared_examples_for 'batched requests' do
13
+ it '#create' do
14
+ client.
15
+ should_receive(:api_post).
16
+ with(endpoint, { batchRequests: [
17
+ { method: 'POST', url: 'v34.0/sobjects/Object', richInput: { name: 'test' } }
18
+ ], haltOnError: halt_on_error }.to_json).
19
+ and_return(response)
20
+
21
+ client.send(method) do |subrequests|
22
+ subrequests.create('Object', name: 'test')
23
+ end
24
+ end
25
+
26
+ it '#update' do
27
+ client.
28
+ should_receive(:api_post).
29
+ with(endpoint, { batchRequests: [
30
+ { method: 'PATCH', url: "v34.0/sobjects/Object/123", richInput: {
31
+ name: 'test'
32
+ } }
33
+ ], haltOnError: halt_on_error }.to_json).
34
+ and_return(response)
35
+
36
+ client.send(method) do |subrequests|
37
+ subrequests.update('Object', id: '123', name: 'test')
38
+ end
39
+ end
40
+
41
+ it '#destroy' do
42
+ client.
43
+ should_receive(:api_post).
44
+ with(endpoint, { batchRequests: [
45
+ { method: 'DELETE', url: "v34.0/sobjects/Object/123" }
46
+ ], haltOnError: halt_on_error }.to_json).
47
+ and_return(response)
48
+
49
+ client.send(method) do |subrequests|
50
+ subrequests.destroy('Object', '123')
51
+ end
52
+ end
53
+
54
+ it '#upsert' do
55
+ client.
56
+ should_receive(:api_post).
57
+ with(endpoint, { batchRequests: [
58
+ { method: 'PATCH', url: 'v34.0/sobjects/Object/extIdField__c/456', richInput: {
59
+ name: 'test'
60
+ } }
61
+ ], haltOnError: halt_on_error }.to_json).
62
+ and_return(response)
63
+
64
+ client.send(method) do |subrequests|
65
+ subrequests.upsert('Object', 'extIdField__c',
66
+ extIdField__c: '456', name: 'test')
67
+ end
68
+ end
69
+
70
+ it 'multiple subrequests' do
71
+ client.
72
+ should_receive(:api_post).
73
+ with(endpoint, { batchRequests: [
74
+ { method: 'POST', url: 'v34.0/sobjects/Object', richInput: {
75
+ name: 'test'
76
+ } },
77
+ { method: 'PATCH', url: "v34.0/sobjects/Object/123", richInput: {
78
+ name: 'test'
79
+ } },
80
+ { method: 'DELETE', url: "v34.0/sobjects/Object/123" }
81
+ ], haltOnError: halt_on_error }.to_json).
82
+ and_return(response)
83
+
84
+ client.send(method) do |subrequests|
85
+ subrequests.create('Object', name: 'test')
86
+ subrequests.update('Object', id: '123', name: 'test')
87
+ subrequests.destroy('Object', '123')
88
+ end
89
+ end
90
+ end
91
+
92
+ describe '#batch' do
93
+ let(:method) { :batch }
94
+ let(:halt_on_error) { false }
95
+ let(:response) { double('Faraday::Response', body: { 'results' => [] }) }
96
+ it_behaves_like 'batched requests'
97
+ end
98
+
99
+ describe '#batch!' do
100
+ let(:method) { :batch! }
101
+ let(:halt_on_error) { true }
102
+ let(:response) {
103
+ double('Faraday::Response', body: { 'hasErrors' => false, 'results' => [] })
104
+ }
105
+ it_behaves_like 'batched requests'
106
+ end
107
+ 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,73 @@ 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)
16
17
  client.stub faye: faye_double
17
18
 
18
- client.subscribe(channels, &subscribe_block)
19
+ client.subscription(channels)
20
+ end
21
+
22
+ context "replay_handlers" do
23
+ before {
24
+ faye_double.should_receive(:subscribe).at_least(1)
25
+ client.stub faye: faye_double
26
+ }
27
+
28
+ it 'registers nil handlers when no replay option is given' do
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
+ )
35
+ end
36
+
37
+ it 'registers a replay_handler for each channel given' do
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
+ )
44
+ end
45
+
46
+ it 'replaces earlier handlers in subsequent calls' do
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
+
58
+ client.replay_handlers.should eq(
59
+ '/topic/channel1' => 2,
60
+ '/topic/channel2' => 3,
61
+ '/topic/channel3' => 3
62
+ )
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
19
74
  end
20
75
  end
21
76
 
@@ -42,6 +97,8 @@ describe Restforce::Concerns::Streaming, event_machine: true do
42
97
  faye_double.should_receive(:set_header).with('Authorization', 'OAuth secret2')
43
98
  faye_double.should_receive(:bind).with('transport:down').and_yield
44
99
  faye_double.should_receive(:bind).with('transport:up').and_yield
100
+ faye_double.should_receive(:add_extension).with \
101
+ kind_of(Restforce::Concerns::Streaming::ReplayExtension)
45
102
  subject
46
103
  end
47
104
  end
@@ -52,4 +109,87 @@ describe Restforce::Concerns::Streaming, event_machine: true do
52
109
  end
53
110
  end
54
111
  end
112
+
113
+ describe "ReplayExtension" do
114
+ let(:handlers) { {} }
115
+ let(:extension) { Restforce::Concerns::Streaming::ReplayExtension.new(handlers) }
116
+
117
+ it 'sends nil without a specified handler' do
118
+ output = subscribe(extension, to: "/topic/channel1")
119
+ read_replay(output).should eq('/topic/channel1' => nil)
120
+ end
121
+
122
+ it 'with a scalar replay id' do
123
+ handlers['/topic/channel1'] = -2
124
+ output = subscribe(extension, to: "/topic/channel1")
125
+ read_replay(output).should eq('/topic/channel1' => -2)
126
+ end
127
+
128
+ it 'with a hash' do
129
+ hash_handler = { '/topic/channel1' => -1, '/topic/channel2' => -2 }
130
+
131
+ handlers['/topic/channel1'] = hash_handler
132
+ handlers['/topic/channel2'] = hash_handler
133
+
134
+ output = subscribe(extension, to: "/topic/channel1")
135
+ read_replay(output).should eq('/topic/channel1' => -1)
136
+
137
+ output = subscribe(extension, to: "/topic/channel2")
138
+ read_replay(output).should eq('/topic/channel2' => -2)
139
+ end
140
+
141
+ it 'with an object' do
142
+ custom_handler = double('custom_handler')
143
+ custom_handler.should_receive(:[]).and_return(123)
144
+ handlers['/topic/channel1'] = custom_handler
145
+
146
+ output = subscribe(extension, to: "/topic/channel1")
147
+ read_replay(output).should eq('/topic/channel1' => 123)
148
+ end
149
+
150
+ it 'remembers the last replayId' do
151
+ handler = { '/topic/channel1' => 41 }
152
+ handlers['/topic/channel1'] = handler
153
+ message = {
154
+ 'channel' => '/topic/channel1',
155
+ 'data' => {
156
+ 'event' => { 'replayId' => 42 }
157
+ }
158
+ }
159
+
160
+ extension.incoming(message, ->(m) {})
161
+ handler.should eq('/topic/channel1' => 42)
162
+ end
163
+
164
+ it 'when an incoming message has no replayId' do
165
+ handler = { '/topic/channel1' => 41 }
166
+ handlers['/topic/channel1'] = handler
167
+
168
+ message = {
169
+ 'channel' => '/topic/channel1',
170
+ 'data' => {}
171
+ }
172
+
173
+ extension.incoming(message, ->(m) {})
174
+ handler.should eq('/topic/channel1' => 41)
175
+ end
176
+
177
+ private
178
+
179
+ def subscribe(extension, options = {})
180
+ output = nil
181
+ message = {
182
+ 'channel' => '/meta/subscribe',
183
+ 'subscription' => options[:to]
184
+ }
185
+ extension.outgoing(message, ->(m) {
186
+ output = m
187
+ })
188
+ output
189
+ end
190
+
191
+ def read_replay(message)
192
+ message.fetch('ext', {})['replay']
193
+ end
194
+ end
55
195
  end
@@ -76,7 +76,7 @@ describe Restforce do
76
76
  subject { Restforce.log? }
77
77
 
78
78
  context 'by default' do
79
- it { should be_false }
79
+ it { should be false }
80
80
  end
81
81
  end
82
82