ably 0.8.3 → 0.8.4

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -3
  3. data/lib/ably/auth.rb +1 -1
  4. data/lib/ably/exceptions.rb +3 -0
  5. data/lib/ably/models/channel_state_change.rb +41 -0
  6. data/lib/ably/models/connection_state_change.rb +43 -0
  7. data/lib/ably/models/message.rb +1 -1
  8. data/lib/ably/models/presence_message.rb +1 -1
  9. data/lib/ably/models/protocol_message.rb +2 -1
  10. data/lib/ably/modules/state_emitter.rb +4 -1
  11. data/lib/ably/modules/uses_state_machine.rb +28 -4
  12. data/lib/ably/realtime/channel.rb +11 -3
  13. data/lib/ably/realtime/channel/channel_manager.rb +24 -4
  14. data/lib/ably/realtime/channel/channel_state_machine.rb +20 -11
  15. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +4 -4
  16. data/lib/ably/realtime/connection.rb +1 -0
  17. data/lib/ably/realtime/connection/connection_manager.rb +33 -21
  18. data/lib/ably/realtime/connection/connection_state_machine.rb +24 -16
  19. data/lib/ably/rest/channel.rb +3 -2
  20. data/lib/ably/util/crypto.rb +15 -0
  21. data/lib/ably/version.rb +1 -1
  22. data/spec/acceptance/realtime/channel_spec.rb +155 -9
  23. data/spec/acceptance/realtime/client_spec.rb +2 -2
  24. data/spec/acceptance/realtime/connection_failures_spec.rb +8 -4
  25. data/spec/acceptance/realtime/connection_spec.rb +122 -11
  26. data/spec/acceptance/realtime/message_spec.rb +119 -3
  27. data/spec/acceptance/realtime/presence_spec.rb +34 -13
  28. data/spec/acceptance/rest/channel_spec.rb +9 -0
  29. data/spec/acceptance/rest/client_spec.rb +10 -0
  30. data/spec/unit/models/channel_state_change_spec.rb +44 -0
  31. data/spec/unit/models/connection_state_change_spec.rb +54 -0
  32. data/spec/unit/util/crypto_spec.rb +18 -0
  33. metadata +8 -2
@@ -55,6 +55,7 @@ module Ably
55
55
  )
56
56
  include Ably::Modules::StateEmitter
57
57
  include Ably::Modules::UsesStateMachine
58
+ ensure_state_machine_emits 'Ably::Models::ConnectionStateChange'
58
59
 
59
60
  # Expected format for a connection recover key
60
61
  RECOVER_REGEX = /^(?<recover>[\w-]+):(?<connection_serial>\-?\w+)$/
@@ -53,7 +53,7 @@ module Ably::Realtime
53
53
  end
54
54
 
55
55
  unless client.auth.authentication_security_requirements_met?
56
- connection.transition_state_machine :failed, Ably::Exceptions::InsecureRequest.new('Cannot use Basic Auth over non-TLS connections', 401, 40103)
56
+ connection.transition_state_machine :failed, reason: Ably::Exceptions::InsecureRequest.new('Cannot use Basic Auth over non-TLS connections', 401, 40103)
57
57
  return
58
58
  end
59
59
 
@@ -80,7 +80,8 @@ module Ably::Realtime
80
80
  # @api private
81
81
  def connection_opening_failed(error)
82
82
  logger.warn "ConnectionManager: Connection to #{connection.current_host}:#{connection.port} failed; #{error.message}"
83
- connection.transition_state_machine next_retry_state, Ably::Exceptions::ConnectionError.new("Connection failed: #{error.message}", nil, 80000)
83
+ next_state = get_next_retry_state_info
84
+ connection.transition_state_machine next_state.fetch(:state), retry_in: next_state.fetch(:pause), reason: Ably::Exceptions::ConnectionError.new("Connection failed: #{error.message}", nil, 80000)
84
85
  end
85
86
 
86
87
  # Called whenever a new connection is made
@@ -155,11 +156,10 @@ module Ably::Realtime
155
156
  # When a connection is disconnected whilst connecting, attempt reconnect and/or set state to :suspended or :failed
156
157
  #
157
158
  # @api private
158
- def respond_to_transport_disconnected_when_connecting(current_transition)
159
+ def respond_to_transport_disconnected_when_connecting(error)
159
160
  return unless connection.disconnected? || connection.suspended? # do nothing if state has changed through an explicit request
160
161
  return unless retry_connection? # do not always reattempt connection or change state as client may be re-authorising
161
162
 
162
- error = current_transition.metadata
163
163
  if error.kind_of?(Ably::Models::ErrorInfo)
164
164
  renew_token_and_reconnect error if error.code == RESOLVABLE_ERROR_CODES.fetch(:token_expired)
165
165
  return
@@ -172,23 +172,22 @@ module Ably::Realtime
172
172
  return if connection_retry_for(:suspended)
173
173
 
174
174
  # Fallback if no other criteria met
175
- connection.transition_state_machine :failed, current_transition.metadata
175
+ connection.transition_state_machine :failed, reason: error
176
176
  end
177
177
 
178
178
  # When a connection is disconnected after connecting, attempt reconnect and/or set state to :suspended or :failed
179
179
  #
180
180
  # @api private
181
- def respond_to_transport_disconnected_whilst_connected(current_transition)
181
+ def respond_to_transport_disconnected_whilst_connected(error)
182
182
  logger.warn "ConnectionManager: Connection to #{connection.transport.url} was disconnected unexpectedly"
183
183
 
184
- error = current_transition.metadata
185
184
  if error.kind_of?(Ably::Models::ErrorInfo) && error.code != RESOLVABLE_ERROR_CODES.fetch(:token_expired)
186
185
  connection.emit :error, error
187
186
  logger.error "ConnectionManager: Error in Disconnected ProtocolMessage received from the server - #{error}"
188
187
  end
189
188
 
190
189
  destroy_transport
191
- respond_to_transport_disconnected_when_connecting current_transition
190
+ respond_to_transport_disconnected_when_connecting error
192
191
  end
193
192
 
194
193
  # {Ably::Models::ProtocolMessage ProtocolMessage Error} received from server.
@@ -198,13 +197,13 @@ module Ably::Realtime
198
197
  def error_received_from_server(error)
199
198
  case error.code
200
199
  when RESOLVABLE_ERROR_CODES.fetch(:token_expired)
201
- connection.transition_state_machine :disconnected
200
+ connection.transition_state_machine :disconnected, retry_in: 0
202
201
  connection.unsafe_once_or_if(:disconnected) do
203
202
  renew_token_and_reconnect error
204
203
  end
205
204
  else
206
205
  logger.error "ConnectionManager: Error #{error.class.name} code #{error.code} received from server '#{error.message}', transitioning to failed state"
207
- connection.transition_state_machine :failed, error
206
+ connection.transition_state_machine :failed, reason: error
208
207
  end
209
208
  end
210
209
 
@@ -249,12 +248,26 @@ module Ably::Realtime
249
248
  timers.fetch(key, []).each(&:cancel)
250
249
  end
251
250
 
252
- def next_retry_state
253
- if connection_retry_from_suspended_state? || time_passed_since_disconnected > CONNECT_RETRY_CONFIG.fetch(:disconnected).fetch(:max_time_in_state)
251
+ def get_next_retry_state_info
252
+ retry_state = if connection_retry_from_suspended_state? || !can_reattempt_connect_for_state?(:disconnected)
254
253
  :suspended
255
254
  else
256
255
  :disconnected
257
256
  end
257
+ {
258
+ state: retry_state,
259
+ pause: next_retry_pause(retry_state)
260
+ }
261
+ end
262
+
263
+ def next_retry_pause(retry_state)
264
+ return nil unless CONNECT_RETRY_CONFIG.fetch(retry_state)
265
+
266
+ if retries_for_state(retry_state, ignore_states: [:connecting]).empty?
267
+ 0
268
+ else
269
+ CONNECT_RETRY_CONFIG.fetch(retry_state).fetch(:retry_every)
270
+ end
258
271
  end
259
272
 
260
273
  def connection_retry_from_suspended_state?
@@ -348,13 +361,12 @@ module Ably::Realtime
348
361
  connection.transition_state_machine :closed
349
362
  elsif !connection.closed? && !connection.disconnected?
350
363
  exception = if reason
351
- Ably::Exceptions::ConnectionClosed.new(reason)
352
- end
353
- if connection_retry_from_suspended_state? || !can_reattempt_connect_for_state?(:disconnected)
354
- connection.transition_state_machine :suspended, exception
364
+ Ably::Exceptions::TransportClosed.new(reason, nil, 80003)
355
365
  else
356
- connection.transition_state_machine :disconnected, exception
366
+ Ably::Exceptions::TransportClosed.new('Transport disconnected unexpectedly', nil, 80003)
357
367
  end
368
+ next_state = get_next_retry_state_info
369
+ connection.transition_state_machine next_state.fetch(:state), retry_in: next_state.fetch(:pause), reason: exception
358
370
  end
359
371
  end
360
372
  end
@@ -362,7 +374,7 @@ module Ably::Realtime
362
374
  def renew_token_and_reconnect(error)
363
375
  if client.auth.token_renewable?
364
376
  if @renewing_token
365
- connection.transition_state_machine :failed, error
377
+ connection.transition_state_machine :failed, reason: error
366
378
  return
367
379
  end
368
380
 
@@ -383,18 +395,18 @@ module Ably::Realtime
383
395
  if token_details && !token_details.expired?
384
396
  connection.connect
385
397
  else
386
- connection.transition_state_machine :failed, error unless connection.failed?
398
+ connection.transition_state_machine :failed, reason: error unless connection.failed?
387
399
  end
388
400
  end
389
401
 
390
402
  authorise_deferrable.errback do |auth_error|
391
403
  logger.error "ConnectionManager: Error authorising following token expiry: #{auth_error}"
392
- connection.transition_state_machine :failed, auth_error
404
+ connection.transition_state_machine :failed, reason: auth_error
393
405
  end
394
406
  end
395
407
  else
396
408
  logger.error "ConnectionManager: Token has expired and is not renewable - #{error}"
397
- connection.transition_state_machine :failed, error
409
+ connection.transition_state_machine :failed, reason: error
398
410
  end
399
411
  end
400
412
 
@@ -41,23 +41,25 @@ module Ably::Realtime
41
41
  end
42
42
 
43
43
  before_transition(to: [:connected]) do |connection, current_transition|
44
- connection.manager.connected current_transition.metadata
44
+ connection.manager.connected current_transition.metadata.protocol_message
45
45
  end
46
46
 
47
47
  after_transition(to: [:connected]) do |connection, current_transition|
48
- protocol_message = current_transition.metadata
49
- if is_error_type?(protocol_message.error)
50
- connection.logger.warn "ConnectionManager: Connected with error - #{protocol_message.error.message}"
51
- connection.emit :error, protocol_message.error
48
+ error = current_transition.metadata.reason
49
+ if is_error_type?(error)
50
+ connection.logger.warn "ConnectionManager: Connected with error - #{error.message}"
51
+ connection.emit :error, error
52
52
  end
53
53
  end
54
54
 
55
55
  after_transition(to: [:disconnected, :suspended], from: [:connecting]) do |connection, current_transition|
56
- connection.manager.respond_to_transport_disconnected_when_connecting current_transition
56
+ err = error_from_state_change(current_transition)
57
+ connection.manager.respond_to_transport_disconnected_when_connecting err
57
58
  end
58
59
 
59
60
  after_transition(to: [:disconnected], from: [:connected]) do |connection, current_transition|
60
- connection.manager.respond_to_transport_disconnected_whilst_connected current_transition
61
+ err = error_from_state_change(current_transition)
62
+ connection.manager.respond_to_transport_disconnected_whilst_connected err
61
63
  end
62
64
 
63
65
  after_transition(to: [:disconnected, :suspended]) do |connection|
@@ -65,7 +67,8 @@ module Ably::Realtime
65
67
  end
66
68
 
67
69
  before_transition(to: [:failed]) do |connection, current_transition|
68
- connection.manager.fail current_transition.metadata
70
+ err = error_from_state_change(current_transition)
71
+ connection.manager.fail err
69
72
  end
70
73
 
71
74
  after_transition(to: [:closing], from: [:initialized, :disconnected, :suspended]) do |connection|
@@ -82,24 +85,29 @@ module Ably::Realtime
82
85
 
83
86
  # Transitions responsible for updating connection#error_reason
84
87
  before_transition(to: [:disconnected, :suspended, :failed]) do |connection, current_transition|
85
- connection.set_failed_connection_error_reason current_transition.metadata
88
+ err = error_from_state_change(current_transition)
89
+ connection.set_failed_connection_error_reason err
86
90
  end
87
91
 
88
92
  before_transition(to: [:connected, :closed]) do |connection, current_transition|
89
- error = if current_transition.metadata.kind_of?(Ably::Models::ProtocolMessage)
90
- current_transition.metadata.error
91
- else
92
- current_transition.metadata
93
- end
93
+ err = error_from_state_change(current_transition)
94
94
 
95
- if is_error_type?(error)
96
- connection.set_failed_connection_error_reason error
95
+ if err
96
+ connection.set_failed_connection_error_reason err
97
97
  else
98
98
  # Connected & Closed are "healthy" final states so reset the error reason
99
99
  connection.clear_error_reason
100
100
  end
101
101
  end
102
102
 
103
+ def self.error_from_state_change(current_transition)
104
+ # ConnectionStateChange object is always passed in current_transition metadata object
105
+ connection_state_change = current_transition.metadata
106
+ # Reason attribute contains errors
107
+ err = connection_state_change && connection_state_change.reason
108
+ err if is_error_type?(err)
109
+ end
110
+
103
111
  private
104
112
  def connection
105
113
  object
@@ -34,6 +34,7 @@ module Ably
34
34
  #
35
35
  # @param name [String, Array<Ably::Models::Message|Hash>, nil] The event name of the message to publish, or an Array of [Ably::Model::Message] objects or [Hash] objects with +:name+ and +:data+ pairs
36
36
  # @param data [String, ByteArray, nil] The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument
37
+ # @param attributes [Hash, nil] Optional additional message attributes such as :client_id or :connection_id, applied when name attribute is nil or a string
37
38
  # @return [Boolean] true if the message was published, otherwise false
38
39
  #
39
40
  # @example
@@ -54,13 +55,13 @@ module Ably
54
55
  # ]
55
56
  # channel.publish messages
56
57
  #
57
- def publish(name, data = nil)
58
+ def publish(name, data = nil, attributes = {})
58
59
  messages = if name.kind_of?(Enumerable)
59
60
  name
60
61
  else
61
62
  ensure_utf_8 :name, name, allow_nil: true
62
63
  ensure_supported_payload data
63
- [{ name: name, data: data }]
64
+ [{ name: name, data: data }.merge(attributes)]
64
65
  end
65
66
 
66
67
  payload = messages.map do |message|
@@ -37,6 +37,21 @@ module Ably::Util
37
37
  @options = DEFAULTS.merge(options).freeze
38
38
  end
39
39
 
40
+ # Obtain a default CipherParams. This uses default algorithm, mode and
41
+ # padding and key length. A key and IV are generated using the default
42
+ # system SecureRandom; the key may be obtained from the returned CipherParams
43
+ # for out-of-band distribution to other clients.
44
+ #
45
+ # @return [Hash] CipherParam options Hash with attributes :key, :algorithn, :mode, :key_length
46
+ #
47
+ def self.get_default_params(key = nil)
48
+ params = DEFAULTS.merge(key: key)
49
+ params[:key_length] = key.unpack('b*').first.length if params[:key]
50
+ cipher_type = "#{params[:algorithm]}-#{params[:key_length]}-#{params[:mode]}"
51
+ params[:key] = OpenSSL::Cipher.new(cipher_type.upcase).random_key unless params[:key]
52
+ params
53
+ end
54
+
40
55
  # Encrypt payload using configured Cipher
41
56
  #
42
57
  # @param [String] payload the payload to be encrypted
@@ -1,3 +1,3 @@
1
1
  module Ably
2
- VERSION = '0.8.3'
2
+ VERSION = '0.8.4'
3
3
  end
@@ -92,7 +92,7 @@ describe Ably::Realtime::Channel, :event_machine do
92
92
 
93
93
  it 'reattaches' do
94
94
  channel.attach do
95
- channel.transition_state_machine :failed, RuntimeError.new
95
+ channel.transition_state_machine :failed, reason: RuntimeError.new
96
96
  expect(channel).to be_failed
97
97
  channel.attach do
98
98
  expect(channel).to be_attached
@@ -154,9 +154,9 @@ describe Ably::Realtime::Channel, :event_machine do
154
154
 
155
155
  it 'emits failed event' do
156
156
  restricted_channel.attach
157
- restricted_channel.on(:failed) do |error|
157
+ restricted_channel.on(:failed) do |connection_state|
158
158
  expect(restricted_channel.state).to eq(:failed)
159
- expect(error.status).to eq(401)
159
+ expect(connection_state.reason.status).to eq(401)
160
160
  stop_reactor
161
161
  end
162
162
  end
@@ -262,7 +262,7 @@ describe Ably::Realtime::Channel, :event_machine do
262
262
 
263
263
  it 'raises an exception' do
264
264
  channel.attach do
265
- channel.transition_state_machine :failed, RuntimeError.new
265
+ channel.transition_state_machine :failed, reason: RuntimeError.new
266
266
  expect(channel).to be_failed
267
267
  expect { channel.detach }.to raise_error Ably::Exceptions::InvalidStateChange
268
268
  stop_reactor
@@ -433,6 +433,19 @@ describe Ably::Realtime::Channel, :event_machine do
433
433
  end
434
434
  end
435
435
  end
436
+
437
+ context 'and additional attributes' do
438
+ let(:client_id) { random_str }
439
+
440
+ it 'publishes the message with the attributes and return true indicating success' do
441
+ channel.publish(name, data, client_id: client_id) do
442
+ channel.history do |page|
443
+ expect(page.items.first.client_id).to eql(client_id)
444
+ stop_reactor
445
+ end
446
+ end
447
+ end
448
+ end
436
449
  end
437
450
 
438
451
  context 'with an array of Hash objects with :name and :data attributes' do
@@ -666,7 +679,8 @@ describe Ably::Realtime::Channel, :event_machine do
666
679
  context 'an :attached channel' do
667
680
  it 'transitions state to :failed' do
668
681
  channel.attach do
669
- channel.on(:failed) do |error|
682
+ channel.on(:failed) do |connection_state_change|
683
+ error = connection_state_change.reason
670
684
  expect(error).to be_a(Ably::Exceptions::ConnectionFailed)
671
685
  expect(error.code).to eql(80002)
672
686
  stop_reactor
@@ -688,7 +702,8 @@ describe Ably::Realtime::Channel, :event_machine do
688
702
 
689
703
  it 'updates the channel error_reason' do
690
704
  channel.attach do
691
- channel.on(:failed) do |error|
705
+ channel.on(:failed) do |connection_state_change|
706
+ error = connection_state_change.reason
692
707
  expect(error).to be_a(Ably::Exceptions::ConnectionFailed)
693
708
  expect(error.code).to eql(80002)
694
709
  stop_reactor
@@ -734,7 +749,7 @@ describe Ably::Realtime::Channel, :event_machine do
734
749
  fake_error connection_error
735
750
  end
736
751
 
737
- channel.transition_state_machine :failed, original_error
752
+ channel.transition_state_machine :failed, reason: original_error
738
753
  end
739
754
  end
740
755
  end
@@ -783,8 +798,8 @@ describe Ably::Realtime::Channel, :event_machine do
783
798
  end
784
799
 
785
800
  context 'a :failed channel' do
786
- let(:original_error) { RuntimeError.new }
787
801
  let(:client_options) { default_options.merge(log_level: :fatal) }
802
+ let(:original_error) { Ably::Models::ErrorInfo.new(message: 'Error') }
788
803
 
789
804
  it 'remains in the :failed state and retains the error_reason' do
790
805
  channel.attach do
@@ -801,7 +816,7 @@ describe Ably::Realtime::Channel, :event_machine do
801
816
  client.connection.close
802
817
  end
803
818
 
804
- channel.transition_state_machine :failed, original_error
819
+ channel.transition_state_machine :failed, reason: original_error
805
820
  end
806
821
  end
807
822
  end
@@ -830,6 +845,75 @@ describe Ably::Realtime::Channel, :event_machine do
830
845
  end
831
846
  end
832
847
  end
848
+
849
+ context ':suspended' do
850
+ context 'an :attached channel' do
851
+ let(:client_options) { default_options.merge(log_level: :fatal) }
852
+
853
+ it 'transitions state to :detached' do
854
+ channel.attach do
855
+ channel.on(:detached) do
856
+ stop_reactor
857
+ end
858
+ client.connection.transition_state_machine :suspended
859
+ end
860
+ end
861
+ end
862
+
863
+ context 'a :detached channel' do
864
+ it 'remains in the :detached state' do
865
+ channel.attach do
866
+ channel.detach do
867
+ channel.on(:detached) { raise 'Detached state should not have been reached' }
868
+ channel.on(:error) { raise 'Error should not have been emitted' }
869
+
870
+ EventMachine.add_timer(1) do
871
+ expect(channel).to be_detached
872
+ stop_reactor
873
+ end
874
+
875
+ client.connection.transition_state_machine :suspended
876
+ end
877
+ end
878
+ end
879
+ end
880
+
881
+ context 'a :failed channel' do
882
+ let(:original_error) { RuntimeError.new }
883
+ let(:client_options) { default_options.merge(log_level: :fatal) }
884
+
885
+ it 'remains in the :failed state and retains the error_reason' do
886
+ channel.attach do
887
+ channel.once(:error) do
888
+ channel.on(:detached) { raise 'Detached state should not have been reached' }
889
+ channel.on(:error) { raise 'Error should not have been emitted' }
890
+
891
+ EventMachine.add_timer(1) do
892
+ expect(channel).to be_failed
893
+ expect(channel.error_reason).to eql(original_error)
894
+ stop_reactor
895
+ end
896
+
897
+ client.connection.transition_state_machine :suspended
898
+ end
899
+
900
+ channel.transition_state_machine :failed, reason: original_error
901
+ end
902
+ end
903
+ end
904
+
905
+ context 'a channel ATTACH request when connection SUSPENDED' do
906
+ it 'raises an exception' do
907
+ client.connect do
908
+ client.connection.once(:suspended) do
909
+ expect { channel.attach }.to raise_error Ably::Exceptions::InvalidStateChange
910
+ stop_reactor
911
+ end
912
+ client.connection.transition_state_machine :suspended
913
+ end
914
+ end
915
+ end
916
+ end
833
917
  end
834
918
 
835
919
  describe '#presence' do
@@ -838,5 +922,67 @@ describe Ably::Realtime::Channel, :event_machine do
838
922
  stop_reactor
839
923
  end
840
924
  end
925
+
926
+ context 'channel state change' do
927
+ it 'emits a ChannelStateChange object' do
928
+ channel.on(:attached) do |channel_state_change|
929
+ expect(channel_state_change).to be_a(Ably::Models::ChannelStateChange)
930
+ stop_reactor
931
+ end
932
+ channel.attach
933
+ end
934
+
935
+ context 'ChannelStateChange object' do
936
+ it 'has current state' do
937
+ channel.on(:attached) do |channel_state_change|
938
+ expect(channel_state_change.current).to eq(:attached)
939
+ stop_reactor
940
+ end
941
+ channel.attach
942
+ end
943
+
944
+ it 'has a previous state' do
945
+ channel.on(:attached) do |channel_state_change|
946
+ expect(channel_state_change.previous).to eq(:attaching)
947
+ stop_reactor
948
+ end
949
+ channel.attach
950
+ end
951
+
952
+ it 'contains a private API protocol_message attribute that is used for special state change events', :api_private do
953
+ channel.on(:attached) do |channel_state_change|
954
+ expect(channel_state_change.protocol_message).to be_a(Ably::Models::ProtocolMessage)
955
+ expect(channel_state_change.reason).to be_nil
956
+ stop_reactor
957
+ end
958
+ channel.attach
959
+ end
960
+
961
+ it 'has an empty reason when there is no error' do
962
+ channel.on(:detached) do |channel_state_change|
963
+ expect(channel_state_change.reason).to be_nil
964
+ stop_reactor
965
+ end
966
+ channel.attach do
967
+ channel.detach
968
+ end
969
+ end
970
+
971
+ context 'on failure' do
972
+ let(:client_options) { default_options.merge(log_level: :none) }
973
+
974
+ it 'has a reason Error object when there is an error on the channel' do
975
+ channel.on(:failed) do |channel_state_change|
976
+ expect(channel_state_change.reason).to be_a(Ably::Exceptions::BaseAblyException)
977
+ stop_reactor
978
+ end
979
+ channel.attach do
980
+ error = Ably::Exceptions::ConnectionFailed.new('forced failure', 500, 50000)
981
+ client.connection.manager.error_received_from_server error
982
+ end
983
+ end
984
+ end
985
+ end
986
+ end
841
987
  end
842
988
  end