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.
Files changed (37) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +9 -9
  3. data/.rubocop.yml +10 -12
  4. data/.rubocop_todo.yml +128 -81
  5. data/CHANGELOG.md +30 -1
  6. data/Gemfile +2 -1
  7. data/README.md +124 -12
  8. data/lib/restforce.rb +22 -1
  9. data/lib/restforce/abstract_client.rb +1 -0
  10. data/lib/restforce/attachment.rb +1 -0
  11. data/lib/restforce/concerns/api.rb +9 -6
  12. data/lib/restforce/concerns/authentication.rb +10 -0
  13. data/lib/restforce/concerns/base.rb +2 -0
  14. data/lib/restforce/concerns/batch_api.rb +87 -0
  15. data/lib/restforce/concerns/canvas.rb +1 -0
  16. data/lib/restforce/concerns/picklists.rb +2 -1
  17. data/lib/restforce/concerns/streaming.rb +75 -3
  18. data/lib/restforce/config.rb +4 -0
  19. data/lib/restforce/document.rb +1 -0
  20. data/lib/restforce/middleware/authentication.rb +3 -2
  21. data/lib/restforce/middleware/authentication/jwt_bearer.rb +38 -0
  22. data/lib/restforce/middleware/multipart.rb +1 -0
  23. data/lib/restforce/middleware/raise_error.rb +24 -8
  24. data/lib/restforce/signed_request.rb +1 -0
  25. data/lib/restforce/sobject.rb +1 -0
  26. data/lib/restforce/tooling/client.rb +3 -3
  27. data/lib/restforce/version.rb +1 -1
  28. data/restforce.gemspec +8 -7
  29. data/spec/fixtures/test_private.key +27 -0
  30. data/spec/integration/abstract_client_spec.rb +42 -1
  31. data/spec/support/fixture_helpers.rb +2 -2
  32. data/spec/unit/concerns/authentication_spec.rb +35 -0
  33. data/spec/unit/concerns/batch_api_spec.rb +107 -0
  34. data/spec/unit/concerns/streaming_spec.rb +144 -4
  35. data/spec/unit/middleware/authentication/jwt_bearer_spec.rb +62 -0
  36. data/spec/unit/middleware/raise_error_spec.rb +32 -11
  37. metadata +53 -32
@@ -6,23 +6,30 @@ module Restforce
6
6
  @env = env
7
7
  case env[:status]
8
8
  when 300
9
- raise Faraday::Error::ClientError.new("300: The external ID provided matches " \
10
- "more than one record",
11
- response_values)
9
+ raise Restforce::MatchesMultipleError.new(
10
+ "300: The external ID provided matches more than one record",
11
+ response_values
12
+ )
12
13
  when 401
13
14
  raise Restforce::UnauthorizedError, message
14
15
  when 404
15
- raise Faraday::Error::ResourceNotFound, message
16
+ raise Restforce::NotFoundError, message
16
17
  when 413
17
- raise Faraday::Error::ClientError.new("413: Request Entity Too Large",
18
- response_values)
18
+ raise Restforce::EntityTooLargeError.new(
19
+ "413: Request Entity Too Large",
20
+ response_values
21
+ )
19
22
  when 400...600
20
- raise Faraday::Error::ClientError.new(message, response_values)
23
+ klass = exception_class_for_error_code(body['errorCode'])
24
+ raise klass.new(message, response_values)
21
25
  end
22
26
  end
23
27
 
24
28
  def message
25
- "#{body['errorCode']}: #{body['message']}"
29
+ message = "#{body['errorCode']}: #{body['message']}"
30
+ message << "\nRESPONSE: #{JSON.dump(@env[:body])}"
31
+ rescue StandardError
32
+ message # if JSON.dump fails, return message without extra detail
26
33
  end
27
34
 
28
35
  def body
@@ -43,5 +50,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
@@ -27,6 +27,7 @@ module Restforce
27
27
  # Returns nil if the signed request is invalid.
28
28
  def decode
29
29
  return nil if signature != hmac
30
+
30
31
  JSON.parse(Base64.decode64(payload))
31
32
  end
32
33
 
@@ -63,6 +63,7 @@ module Restforce
63
63
 
64
64
  def ensure_id
65
65
  return true if self.Id?
66
+
66
67
  raise ArgumentError, 'You need to query the Id for the record first.'
67
68
  end
68
69
  end
@@ -5,9 +5,9 @@ module Restforce
5
5
  class Client < AbstractClient
6
6
  private
7
7
 
8
- def api_path(path)
9
- super("tooling/#{path}")
10
- end
8
+ def api_path(path)
9
+ super("tooling/#{path}")
10
+ end
11
11
  end
12
12
  end
13
13
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Restforce
4
- VERSION = '3.0.1'
4
+ VERSION = '4.2.0'
5
5
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require File.expand_path('../lib/restforce/version', __FILE__)
3
+ require File.expand_path('lib/restforce/version', __dir__)
4
4
 
5
5
  Gem::Specification.new do |gem|
6
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.3'
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('../../fixtures/blob.jpg', __FILE__),
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
@@ -27,8 +27,8 @@ module FixtureHelpers
27
27
  stub
28
28
  end
29
29
 
30
- def fixture(f)
31
- File.read(File.expand_path("../../fixtures/#{f}.json", __FILE__))
30
+ def fixture(filename)
31
+ File.read(File.expand_path("../../fixtures/#{filename}.json", __FILE__))
32
32
  end
33
33
  end
34
34
 
@@ -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) { %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)
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