restforce 3.0.1 → 4.2.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.
- checksums.yaml +5 -5
- data/.circleci/config.yml +9 -9
- data/.rubocop.yml +10 -12
- data/.rubocop_todo.yml +128 -81
- data/CHANGELOG.md +30 -1
- data/Gemfile +2 -1
- data/README.md +124 -12
- data/lib/restforce.rb +22 -1
- data/lib/restforce/abstract_client.rb +1 -0
- data/lib/restforce/attachment.rb +1 -0
- data/lib/restforce/concerns/api.rb +9 -6
- data/lib/restforce/concerns/authentication.rb +10 -0
- data/lib/restforce/concerns/base.rb +2 -0
- data/lib/restforce/concerns/batch_api.rb +87 -0
- data/lib/restforce/concerns/canvas.rb +1 -0
- data/lib/restforce/concerns/picklists.rb +2 -1
- data/lib/restforce/concerns/streaming.rb +75 -3
- data/lib/restforce/config.rb +4 -0
- data/lib/restforce/document.rb +1 -0
- data/lib/restforce/middleware/authentication.rb +3 -2
- data/lib/restforce/middleware/authentication/jwt_bearer.rb +38 -0
- data/lib/restforce/middleware/multipart.rb +1 -0
- data/lib/restforce/middleware/raise_error.rb +24 -8
- 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/restforce.gemspec +8 -7
- data/spec/fixtures/test_private.key +27 -0
- data/spec/integration/abstract_client_spec.rb +42 -1
- data/spec/support/fixture_helpers.rb +2 -2
- data/spec/unit/concerns/authentication_spec.rb +35 -0
- data/spec/unit/concerns/batch_api_spec.rb +107 -0
- data/spec/unit/concerns/streaming_spec.rb +144 -4
- data/spec/unit/middleware/authentication/jwt_bearer_spec.rb +62 -0
- data/spec/unit/middleware/raise_error_spec.rb +32 -11
- metadata +53 -32
@@ -6,23 +6,30 @@ module Restforce
|
|
6
6
|
@env = env
|
7
7
|
case env[:status]
|
8
8
|
when 300
|
9
|
-
raise
|
10
|
-
|
11
|
-
|
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
14
|
raise Restforce::UnauthorizedError, message
|
14
15
|
when 404
|
15
|
-
raise
|
16
|
+
raise Restforce::NotFoundError, message
|
16
17
|
when 413
|
17
|
-
raise
|
18
|
-
|
18
|
+
raise Restforce::EntityTooLargeError.new(
|
19
|
+
"413: Request Entity Too Large",
|
20
|
+
response_values
|
21
|
+
)
|
19
22
|
when 400...600
|
20
|
-
|
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,14 @@ 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
|
+
constant_name = error_code.split('_').map(&:capitalize).join.to_sym
|
60
|
+
Restforce::ErrorCode.const_get(constant_name)
|
61
|
+
end
|
46
62
|
end
|
47
63
|
end
|
data/lib/restforce/sobject.rb
CHANGED
data/lib/restforce/version.rb
CHANGED
data/restforce.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require File.expand_path('
|
3
|
+
require File.expand_path('lib/restforce/version', __dir__)
|
4
4
|
|
5
5
|
Gem::Specification.new do |gem|
|
6
6
|
gem.authors = ["Eric J. Holmes", "Tim Rogers"]
|
@@ -22,19 +22,20 @@ 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.
|
25
|
+
gem.required_ruby_version = '>= 2.4'
|
26
26
|
|
27
27
|
gem.add_dependency 'faraday', '<= 1.0', '>= 0.9.0'
|
28
28
|
gem.add_dependency 'faraday_middleware', ['>= 0.8.8', '<= 1.0']
|
29
29
|
|
30
30
|
gem.add_dependency 'json', '>= 1.7.5'
|
31
|
+
gem.add_dependency 'jwt', ['>= 1.5.6']
|
31
32
|
|
32
33
|
gem.add_dependency 'hashie', ['>= 1.2.0', '< 4.0']
|
33
34
|
|
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
35
|
gem.add_development_dependency 'faye' unless RUBY_PLATFORM == 'java'
|
36
|
+
gem.add_development_dependency 'rspec', '~> 2.14.0'
|
37
|
+
gem.add_development_dependency 'rspec_junit_formatter', '~> 0.4.1'
|
38
|
+
gem.add_development_dependency 'rubocop', '~> 0.75.0'
|
39
|
+
gem.add_development_dependency 'simplecov', '~> 0.17.1'
|
40
|
+
gem.add_development_dependency 'webmock', '~> 3.7.6'
|
40
41
|
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-----
|
@@ -96,7 +96,7 @@ shared_examples_for Restforce::AbstractClient do
|
|
96
96
|
subject do
|
97
97
|
client.create('Account', Name: 'Foobar',
|
98
98
|
Blob: Restforce::UploadIO.new(
|
99
|
-
File.expand_path('
|
99
|
+
File.expand_path('../fixtures/blob.jpg', __dir__),
|
100
100
|
'image/jpeg'
|
101
101
|
))
|
102
102
|
end
|
@@ -209,6 +209,24 @@ shared_examples_for Restforce::AbstractClient do
|
|
209
209
|
end
|
210
210
|
end
|
211
211
|
end
|
212
|
+
|
213
|
+
context 'when created with a space in the id' do
|
214
|
+
requests 'sobjects/Account/External__c/foo%20bar',
|
215
|
+
method: :patch,
|
216
|
+
with_body: "{\"Name\":\"Foobar\"}",
|
217
|
+
fixture: 'sobject/upsert_created_success_response'
|
218
|
+
|
219
|
+
[:External__c, 'External__c', :external__c, 'external__c'].each do |key|
|
220
|
+
context "with #{key.inspect} as the external id" do
|
221
|
+
subject do
|
222
|
+
client.upsert!('Account', 'External__c', key => 'foo bar',
|
223
|
+
:Name => 'Foobar')
|
224
|
+
end
|
225
|
+
|
226
|
+
it { should eq 'foo' }
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
212
230
|
end
|
213
231
|
|
214
232
|
describe '.destroy!' do
|
@@ -229,6 +247,13 @@ shared_examples_for Restforce::AbstractClient do
|
|
229
247
|
|
230
248
|
it { should be_true }
|
231
249
|
end
|
250
|
+
|
251
|
+
context 'with a space in the id' do
|
252
|
+
subject(:destroy!) { client.destroy!('Account', '001D000000 INjVe') }
|
253
|
+
requests 'sobjects/Account/001D000000%20INjVe', method: :delete
|
254
|
+
|
255
|
+
it { should be_true }
|
256
|
+
end
|
232
257
|
end
|
233
258
|
|
234
259
|
describe '.destroy' do
|
@@ -266,6 +291,14 @@ shared_examples_for Restforce::AbstractClient do
|
|
266
291
|
subject { client.find('Account', '1234', 'External_Field__c') }
|
267
292
|
it { should be_a Hash }
|
268
293
|
end
|
294
|
+
|
295
|
+
context 'with a space in an external id' do
|
296
|
+
requests 'sobjects/Account/External_Field__c/12%2034',
|
297
|
+
fixture: 'sobject/sobject_find_success_response'
|
298
|
+
|
299
|
+
subject { client.find('Account', '12 34', 'External_Field__c') }
|
300
|
+
it { should be_a Hash }
|
301
|
+
end
|
269
302
|
end
|
270
303
|
|
271
304
|
describe '.select' do
|
@@ -284,6 +317,14 @@ shared_examples_for Restforce::AbstractClient do
|
|
284
317
|
subject { client.select('Account', '1234', ['External_Field__c']) }
|
285
318
|
it { should be_a Hash }
|
286
319
|
end
|
320
|
+
|
321
|
+
context 'with a space in the id' do
|
322
|
+
requests 'sobjects/Account/12%2034',
|
323
|
+
fixture: 'sobject/sobject_select_success_response'
|
324
|
+
|
325
|
+
subject { client.select('Account', '12 34', nil, nil) }
|
326
|
+
it { should be_a Hash }
|
327
|
+
end
|
287
328
|
end
|
288
329
|
|
289
330
|
context 'when an external id is specified' do
|
@@ -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
|
@@ -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
|
@@ -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, &subscribe_block)
|
16
17
|
client.stub faye: faye_double
|
17
18
|
|
18
|
-
client.
|
19
|
+
client.subscription(channels, &subscribe_block)
|
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 Restforce::Concerns::Streaming::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
|