restforce 3.0.1 → 5.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.circleci/config.yml +9 -9
- data/.github/ISSUE_TEMPLATE/unhandled-salesforce-error.md +17 -0
- data/.github/dependabot.yml +19 -0
- data/.rubocop.yml +13 -14
- data/.rubocop_todo.yml +128 -81
- data/CHANGELOG.md +107 -1
- data/CONTRIBUTING.md +21 -1
- data/Dockerfile +31 -0
- data/Gemfile +10 -6
- data/README.md +168 -31
- data/UPGRADING.md +38 -0
- data/docker-compose.yml +7 -0
- data/lib/restforce/abstract_client.rb +1 -0
- data/lib/restforce/attachment.rb +1 -0
- data/lib/restforce/collection.rb +7 -2
- data/lib/restforce/concerns/api.rb +10 -7
- data/lib/restforce/concerns/authentication.rb +10 -0
- data/lib/restforce/concerns/base.rb +4 -2
- data/lib/restforce/concerns/batch_api.rb +87 -0
- data/lib/restforce/concerns/caching.rb +7 -0
- data/lib/restforce/concerns/canvas.rb +1 -0
- data/lib/restforce/concerns/connection.rb +3 -3
- data/lib/restforce/concerns/picklists.rb +4 -3
- data/lib/restforce/concerns/streaming.rb +73 -3
- data/lib/restforce/config.rb +8 -1
- data/lib/restforce/document.rb +1 -0
- data/lib/restforce/error_code.rb +638 -0
- data/lib/restforce/file_part.rb +24 -0
- data/lib/restforce/mash.rb +8 -3
- data/lib/restforce/middleware/authentication/jwt_bearer.rb +38 -0
- data/lib/restforce/middleware/authentication.rb +7 -3
- data/lib/restforce/middleware/caching.rb +1 -1
- data/lib/restforce/middleware/instance_url.rb +1 -1
- data/lib/restforce/middleware/logger.rb +8 -7
- data/lib/restforce/middleware/multipart.rb +1 -0
- data/lib/restforce/middleware/raise_error.rb +24 -9
- data/lib/restforce/middleware.rb +2 -0
- data/lib/restforce/signed_request.rb +1 -0
- data/lib/restforce/sobject.rb +1 -0
- data/lib/restforce/tooling/client.rb +3 -3
- data/lib/restforce/version.rb +1 -1
- data/lib/restforce.rb +21 -3
- data/restforce.gemspec +11 -20
- data/spec/fixtures/test_private.key +27 -0
- data/spec/integration/abstract_client_spec.rb +83 -33
- data/spec/integration/data/client_spec.rb +6 -2
- data/spec/spec_helper.rb +24 -1
- data/spec/support/client_integration.rb +7 -7
- data/spec/support/concerns.rb +1 -1
- data/spec/support/fixture_helpers.rb +3 -5
- data/spec/support/middleware.rb +1 -2
- data/spec/unit/collection_spec.rb +20 -2
- data/spec/unit/concerns/api_spec.rb +12 -12
- data/spec/unit/concerns/authentication_spec.rb +39 -4
- data/spec/unit/concerns/batch_api_spec.rb +107 -0
- data/spec/unit/concerns/caching_spec.rb +26 -0
- data/spec/unit/concerns/connection_spec.rb +2 -2
- data/spec/unit/concerns/streaming_spec.rb +144 -4
- data/spec/unit/config_spec.rb +1 -1
- data/spec/unit/error_code_spec.rb +61 -0
- data/spec/unit/mash_spec.rb +5 -0
- data/spec/unit/middleware/authentication/jwt_bearer_spec.rb +62 -0
- data/spec/unit/middleware/authentication/password_spec.rb +2 -2
- data/spec/unit/middleware/authentication/token_spec.rb +2 -2
- data/spec/unit/middleware/authentication_spec.rb +31 -4
- data/spec/unit/middleware/gzip_spec.rb +2 -2
- data/spec/unit/middleware/raise_error_spec.rb +57 -17
- data/spec/unit/signed_request_spec.rb +1 -1
- data/spec/unit/sobject_spec.rb +2 -5
- metadata +39 -108
- 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
|
-
|
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
|
-
|
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")]
|
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
|
-
|
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
|
-
|
44
|
-
|
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)
|
data/spec/support/concerns.rb
CHANGED
@@ -22,13 +22,11 @@ module FixtureHelpers
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def stub_login_request(*)
|
25
|
-
|
26
|
-
|
27
|
-
stub
|
25
|
+
stub_request(:post, "https://login.salesforce.com/services/oauth2/token")
|
28
26
|
end
|
29
27
|
|
30
|
-
def fixture(
|
31
|
-
File.read(File.expand_path("../../fixtures/#{
|
28
|
+
def fixture(filename)
|
29
|
+
File.read(File.expand_path("../../fixtures/#{filename}.json", __FILE__))
|
32
30
|
end
|
33
31
|
end
|
34
32
|
|
data/spec/support/middleware.rb
CHANGED
@@ -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
|
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
|
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(
|
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::
|
277
|
+
[Faraday::ClientError].each do |exception_klass|
|
278
278
|
client.should_receive(:"#{method}!").
|
279
|
-
with(
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
106
|
+
it { should be_truthy }
|
97
107
|
end
|
98
108
|
|
99
109
|
context 'when oauth options are not provided' do
|
100
|
-
it { should_not
|
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
|
76
|
+
it "must always be used as the last handler" do
|
77
77
|
client.middleware.handlers.reverse.index(Restforce::Middleware::Logger).
|
78
|
-
should eq
|
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)
|
8
|
-
|
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(
|
16
|
+
with(channels)
|
16
17
|
client.stub faye: faye_double
|
17
18
|
|
18
|
-
client.
|
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
|