ably 0.8.15 → 1.0.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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -4
  3. data/CHANGELOG.md +6 -2
  4. data/README.md +5 -1
  5. data/SPEC.md +1473 -852
  6. data/ably.gemspec +11 -8
  7. data/lib/ably/auth.rb +90 -53
  8. data/lib/ably/exceptions.rb +37 -8
  9. data/lib/ably/logger.rb +10 -1
  10. data/lib/ably/models/auth_details.rb +42 -0
  11. data/lib/ably/models/channel_state_change.rb +18 -4
  12. data/lib/ably/models/connection_details.rb +6 -3
  13. data/lib/ably/models/connection_state_change.rb +4 -3
  14. data/lib/ably/models/error_info.rb +1 -1
  15. data/lib/ably/models/message.rb +17 -1
  16. data/lib/ably/models/message_encoders/base.rb +103 -82
  17. data/lib/ably/models/message_encoders/base64.rb +1 -1
  18. data/lib/ably/models/presence_message.rb +16 -1
  19. data/lib/ably/models/protocol_message.rb +20 -3
  20. data/lib/ably/models/token_details.rb +11 -1
  21. data/lib/ably/models/token_request.rb +16 -6
  22. data/lib/ably/modules/async_wrapper.rb +7 -3
  23. data/lib/ably/modules/encodeable.rb +51 -12
  24. data/lib/ably/modules/enum.rb +17 -7
  25. data/lib/ably/modules/event_emitter.rb +29 -14
  26. data/lib/ably/modules/model_common.rb +13 -21
  27. data/lib/ably/modules/state_emitter.rb +7 -4
  28. data/lib/ably/modules/state_machine.rb +2 -4
  29. data/lib/ably/modules/uses_state_machine.rb +7 -3
  30. data/lib/ably/realtime.rb +2 -0
  31. data/lib/ably/realtime/auth.rb +102 -42
  32. data/lib/ably/realtime/channel.rb +68 -26
  33. data/lib/ably/realtime/channel/channel_manager.rb +154 -65
  34. data/lib/ably/realtime/channel/channel_state_machine.rb +14 -15
  35. data/lib/ably/realtime/client.rb +18 -3
  36. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +38 -29
  37. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +6 -1
  38. data/lib/ably/realtime/connection.rb +108 -49
  39. data/lib/ably/realtime/connection/connection_manager.rb +167 -61
  40. data/lib/ably/realtime/connection/connection_state_machine.rb +22 -3
  41. data/lib/ably/realtime/connection/websocket_transport.rb +19 -10
  42. data/lib/ably/realtime/presence.rb +70 -45
  43. data/lib/ably/realtime/presence/members_map.rb +201 -36
  44. data/lib/ably/realtime/presence/presence_manager.rb +30 -6
  45. data/lib/ably/realtime/presence/presence_state_machine.rb +5 -12
  46. data/lib/ably/rest.rb +2 -2
  47. data/lib/ably/rest/channel.rb +5 -5
  48. data/lib/ably/rest/client.rb +31 -27
  49. data/lib/ably/rest/middleware/exceptions.rb +1 -3
  50. data/lib/ably/rest/middleware/logger.rb +2 -2
  51. data/lib/ably/rest/presence.rb +2 -2
  52. data/lib/ably/util/pub_sub.rb +1 -1
  53. data/lib/ably/util/safe_deferrable.rb +26 -0
  54. data/lib/ably/version.rb +2 -2
  55. data/spec/acceptance/realtime/auth_spec.rb +470 -111
  56. data/spec/acceptance/realtime/channel_history_spec.rb +5 -3
  57. data/spec/acceptance/realtime/channel_spec.rb +1017 -168
  58. data/spec/acceptance/realtime/client_spec.rb +6 -6
  59. data/spec/acceptance/realtime/connection_failures_spec.rb +458 -27
  60. data/spec/acceptance/realtime/connection_spec.rb +424 -105
  61. data/spec/acceptance/realtime/message_spec.rb +52 -23
  62. data/spec/acceptance/realtime/presence_history_spec.rb +5 -3
  63. data/spec/acceptance/realtime/presence_spec.rb +1110 -96
  64. data/spec/acceptance/rest/auth_spec.rb +222 -59
  65. data/spec/acceptance/rest/base_spec.rb +1 -1
  66. data/spec/acceptance/rest/channel_spec.rb +1 -2
  67. data/spec/acceptance/rest/client_spec.rb +104 -48
  68. data/spec/acceptance/rest/message_spec.rb +42 -15
  69. data/spec/acceptance/rest/presence_spec.rb +4 -11
  70. data/spec/rspec_config.rb +2 -1
  71. data/spec/shared/client_initializer_behaviour.rb +2 -2
  72. data/spec/shared/safe_deferrable_behaviour.rb +6 -2
  73. data/spec/spec_helper.rb +4 -2
  74. data/spec/support/debug_failure_helper.rb +20 -4
  75. data/spec/support/event_machine_helper.rb +32 -1
  76. data/spec/unit/auth_spec.rb +4 -11
  77. data/spec/unit/logger_spec.rb +28 -2
  78. data/spec/unit/models/auth_details_spec.rb +49 -0
  79. data/spec/unit/models/channel_state_change_spec.rb +23 -3
  80. data/spec/unit/models/connection_details_spec.rb +12 -1
  81. data/spec/unit/models/connection_state_change_spec.rb +15 -4
  82. data/spec/unit/models/message_encoders/base64_spec.rb +2 -1
  83. data/spec/unit/models/message_spec.rb +153 -0
  84. data/spec/unit/models/presence_message_spec.rb +192 -0
  85. data/spec/unit/models/protocol_message_spec.rb +64 -6
  86. data/spec/unit/models/token_details_spec.rb +75 -0
  87. data/spec/unit/models/token_request_spec.rb +74 -0
  88. data/spec/unit/modules/async_wrapper_spec.rb +2 -1
  89. data/spec/unit/modules/enum_spec.rb +69 -0
  90. data/spec/unit/modules/event_emitter_spec.rb +149 -22
  91. data/spec/unit/modules/state_emitter_spec.rb +9 -3
  92. data/spec/unit/realtime/client_spec.rb +1 -1
  93. data/spec/unit/realtime/connection_spec.rb +8 -5
  94. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +1 -1
  95. data/spec/unit/realtime/presence_spec.rb +4 -3
  96. data/spec/unit/rest/client_spec.rb +1 -1
  97. data/spec/unit/util/crypto_spec.rb +3 -3
  98. metadata +22 -19
@@ -62,7 +62,7 @@ describe Ably::Realtime::Client, :event_machine do
62
62
  context 'with valid :key and :use_token_auth option set to true' do
63
63
  let(:client_options) { default_options.merge(use_token_auth: true) }
64
64
 
65
- it 'automatically authorises on connect and generates a token' do
65
+ it 'automatically authorizes on connect and generates a token' do
66
66
  connection.on(:connected) do
67
67
  expect(subject.auth.current_token_details).to_not be_nil
68
68
  expect(auth_params[:access_token]).to_not be_nil
@@ -116,16 +116,16 @@ describe Ably::Realtime::Client, :event_machine do
116
116
 
117
117
  context 'when the returned token has a client_id' do
118
118
  it "sets Auth#client_id to the new token's client_id immediately when connecting" do
119
- subject.auth.authorise do
120
- expect(subject.connection).to be_connecting
119
+ subject.auth.authorize do
120
+ expect(subject.connection).to be_connected
121
121
  expect(subject.auth.client_id).to eql(client_id)
122
122
  stop_reactor
123
123
  end
124
124
  end
125
125
 
126
126
  it "sets Client#client_id to the new token's client_id immediately when connecting" do
127
- subject.auth.authorise do
128
- expect(subject.connection).to be_connecting
127
+ subject.auth.authorize do
128
+ expect(subject.connection).to be_connected
129
129
  expect(subject.client_id).to eql(client_id)
130
130
  stop_reactor
131
131
  end
@@ -259,7 +259,7 @@ describe Ably::Realtime::Client, :event_machine do
259
259
 
260
260
  it 'provides paging' do
261
261
  10.times do
262
- subject.rest_client.request(:post, "/channels/#{channel_name}/publish", {}, { 'name': 'test' })
262
+ subject.rest_client.request(:post, "/channels/#{channel_name}/publish", {}, { 'name' => 'test' })
263
263
  end
264
264
 
265
265
  subject.request(:get, "/channels/#{channel_name}/messages", { limit: 2 }).callback do |response|
@@ -13,6 +13,9 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
13
13
  let(:client) do
14
14
  auto_close Ably::Realtime::Client.new(client_options)
15
15
  end
16
+ let(:rest_client) do
17
+ Ably::Rest::Client.new(default_options)
18
+ end
16
19
 
17
20
  context 'authentication failure' do
18
21
  let(:client_options) do
@@ -50,6 +53,152 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
50
53
  end
51
54
  end
52
55
  end
56
+
57
+ context 'with auth_url' do
58
+ context 'opening a new connection' do
59
+ context 'request fails due to network failure' do
60
+ let(:client_options) { default_options.reject { |k, v| k == :key }.merge(auth_url: "http://#{random_str}.domain.will.never.resolve.to/path", log_level: :fatal) }
61
+
62
+ specify 'the connection moves to the disconnected state and tries again, returning again to the disconnected state (#RSA4c, #RSA4c1, #RSA4c2)' do
63
+ states = Hash.new { |hash, key| hash[key] = [] }
64
+
65
+ connection.once(:connected) { raise "Connection can never move to connected because of auth failures" }
66
+
67
+ connection.on do |connection_state|
68
+ states[connection_state.current.to_sym] << Time.now
69
+ if states[:disconnected].count == 2 && connection_state.current == :disconnected
70
+ expect(connection.error_reason).to be_a(Ably::Exceptions::ConnectionError)
71
+ expect(connection.error_reason.message).to match(/auth_url/)
72
+ EventMachine.add_timer(2) do
73
+ expect(states.keys).to include(:connecting, :disconnected)
74
+ expect(states[:connecting].count).to eql(2)
75
+ expect(states[:connected].count).to eql(0)
76
+ stop_reactor
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ context 'request fails due to invalid content', :webmock do
84
+ let(:auth_endpoint) { "http://#{random_str}.domain.will.never.resolve.to/authenticate" }
85
+ let(:client_options) { default_options.reject { |k, v| k == :key }.merge(auth_url: auth_endpoint, log_level: :fatal) }
86
+
87
+ before do
88
+ stub_request(:get, auth_endpoint).
89
+ to_return(:status => 200, :body => "", :headers => { "Content-type" => "text/html" })
90
+ end
91
+
92
+ specify 'the connection moves to the disconnected state and tries again, returning again to the disconnected state (#RSA4c, #RSA4c1, #RSA4c2)' do
93
+ states = Hash.new { |hash, key| hash[key] = [] }
94
+
95
+ connection.once(:connected) { raise "Connection can never move to connected because of auth failures" }
96
+
97
+ connection.on do |connection_state|
98
+ states[connection_state.current.to_sym] << Time.now
99
+ if states[:disconnected].count == 2 && connection_state.current == :disconnected
100
+ expect(connection.error_reason).to be_a(Ably::Exceptions::ConnectionError)
101
+ expect(connection.error_reason.message).to match(/auth_url/)
102
+ expect(connection.error_reason.message).to match(/Content Type.*not supported/)
103
+ EventMachine.add_timer(2) do
104
+ expect(states.keys).to include(:connecting, :disconnected)
105
+ expect(states[:connecting].count).to eql(2)
106
+ expect(states[:connected].count).to eql(0)
107
+ stop_reactor
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ context 'existing CONNECTED connection' do
116
+ context 'authorize request failure leaves connection in existing condition' do
117
+ let(:auth_options) { { auth_url: "http://#{random_str}.domain.will.never.resolve.to/path" } }
118
+ let(:client_options) { default_options.merge(use_token_auth: true, log_level: :fatal) }
119
+
120
+ specify 'the connection remains in the CONNECTED state and authorize fails (#RSA4c, #RSA4c1, #RSA4c3)' do
121
+ connection.once(:connected) do
122
+ connection.on { raise "State should not change and should stay connected" }
123
+
124
+ client.auth.authorize(nil, auth_options).tap do |deferrable|
125
+ deferrable.callback { raise "Authorize should not succeed" }
126
+ deferrable.errback do |err|
127
+ expect(err).to be_a(Ably::Exceptions::ConnectionError)
128
+ expect(err.message).to match(/auth_url/)
129
+
130
+ EventMachine.add_timer(1) do
131
+ expect(connection).to be_connected
132
+ connection.off
133
+ stop_reactor
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ context 'with auth_callback' do
144
+ context 'opening a new connection' do
145
+ context 'when callback fails due to an exception' do
146
+ let(:client_options) { default_options.reject { |k, v| k == :key }.merge(auth_callback: Proc.new { raise "Cannot issue token" }, log_level: :fatal) }
147
+
148
+ it 'the connection moves to the disconnected state and tries again, returning again to the disconnected state (#RSA4c, #RSA4c1, #RSA4c2)' do
149
+ states = Hash.new { |hash, key| hash[key] = [] }
150
+
151
+ connection.once(:connected) { raise "Connection can never move to connected because of auth failures" }
152
+
153
+ connection.on do |connection_state|
154
+ states[connection_state.current.to_sym] << Time.now
155
+ if states[:disconnected].count == 2 && connection_state.current == :disconnected
156
+ expect(connection.error_reason).to be_a(Ably::Exceptions::ConnectionError)
157
+ expect(connection.error_reason.message).to match(/auth_callback/)
158
+ EventMachine.add_timer(2) do
159
+ expect(states.keys).to include(:connecting, :disconnected)
160
+ expect(states[:connecting].count).to eql(2)
161
+ expect(states[:connected].count).to eql(0)
162
+ stop_reactor
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ context 'existing CONNECTED connection' do
170
+ context 'when callback fails due to the request taking longer than realtime_request_timeout' do
171
+ let(:request_timeout) { 3 }
172
+ let(:client_options) { default_options.merge(
173
+ realtime_request_timeout: request_timeout,
174
+ use_token_auth: true,
175
+ log_level: :fatal)
176
+ }
177
+ let(:auth_options) { { auth_callback: Proc.new { sleep 10 }, } }
178
+
179
+ it 'the authorization request fails as configured in the realtime_request_timeout (#RSA4c, #RSA4c1, #RSA4c3)' do
180
+ connection.once(:connected) do
181
+ connection.on { raise "State should not change and should stay connected" }
182
+
183
+ client.auth.authorize(nil, auth_options).tap do |deferrable|
184
+ deferrable.callback { raise "Authorize should not succeed" }
185
+ deferrable.errback do |err|
186
+ expect(err).to be_a(Ably::Exceptions::ConnectionError)
187
+ expect(err.message).to match(/auth_callback/)
188
+
189
+ EventMachine.add_timer(1) do
190
+ expect(connection).to be_connected
191
+ connection.off
192
+ stop_reactor
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
53
202
  end
54
203
 
55
204
  context 'automatic connection retry' do
@@ -100,7 +249,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
100
249
  connection.once(:suspended) do
101
250
  expect(connection.state).to eq(:suspended)
102
251
 
103
- expect(state_changes[:connecting]).to eql(expected_retry_attempts)
252
+ expect(state_changes[:connecting]).to eql(expected_retry_attempts + 1) # allow for initial connecting attempt
104
253
  expect(state_changes[:disconnected]).to eql(expected_retry_attempts)
105
254
 
106
255
  expect(time_passed).to be > max_time_in_state_for_tests
@@ -213,7 +362,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
213
362
 
214
363
  context 'when connection state is :failed' do
215
364
  describe '#close' do
216
- it 'will not transition state to :close and raises a InvalidStateChange exception' do
365
+ it 'will not transition state to :close and fails with an InvalidStateChange exception' do
217
366
  connection.on(:connected) { raise 'Connection should not have reached :connected state' }
218
367
 
219
368
  connection.once(:suspended) do
@@ -222,8 +371,11 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
222
371
 
223
372
  connection.once(:failed) do
224
373
  expect(connection.state).to eq(:failed)
225
- expect { connection.close }.to raise_error Ably::Exceptions::InvalidStateChange, /Unable to transition from failed => closing/
226
- stop_reactor
374
+ connection.close.errback do |error|
375
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
376
+ expect(error.message).to match(/Unable to transition from failed => closing/)
377
+ stop_reactor
378
+ end
227
379
  end
228
380
  end
229
381
  end
@@ -457,7 +609,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
457
609
  end
458
610
 
459
611
  context 'after successfully reconnecting and resuming' do
460
- it 'retains connection_id and updates the connection_key' do
612
+ it 'retains connection_id and updates the connection_key (#RTN15e, #RTN16d)' do
461
613
  connection.once(:connected) do
462
614
  previous_connection_id = connection.id
463
615
  connection.transport.close_connection_after_writing
@@ -472,8 +624,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
472
624
  end
473
625
  end
474
626
 
475
- it 'emits any error received from Ably but leaves the channels attached' do
476
- emitted_error = nil
627
+ it 'includes the error received in the connection state change from Ably but leaves the channels attached' do
477
628
  channel.attach do
478
629
  connection.transport.close_connection_after_writing
479
630
 
@@ -487,19 +638,15 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
487
638
  Ably::Realtime::Client::IncomingMessageDispatcher.new(client, connection)
488
639
  end
489
640
 
490
- connection.once(:connected) do
641
+ connection.once(:connected) do |connection_state_change|
491
642
  EM.add_timer(0.5) do
492
- expect(emitted_error).to be_a(Ably::Exceptions::Standard)
493
- expect(emitted_error.message).to match(/Injected error/)
643
+ expect(connection_state_change.reason).to be_a(Ably::Exceptions::Standard)
644
+ expect(connection_state_change.reason.message).to match(/Injected error/)
494
645
  expect(connection.error_reason).to be_a(Ably::Exceptions::Standard)
495
646
  expect(channel).to be_attached
496
647
  stop_reactor
497
648
  end
498
649
  end
499
-
500
- connection.once(:error) do |error|
501
- emitted_error = error
502
- end
503
650
  end
504
651
  end
505
652
 
@@ -541,7 +688,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
541
688
 
542
689
  channel.attach do
543
690
  publishing_client_channel.attach do
544
- connection.transport.off # remove all event handlers that detect socket connection state has changed
691
+ connection.transport.unsafe_off # remove all event handlers that detect socket connection state has changed
545
692
  connection.transport.close_connection_after_writing
546
693
 
547
694
  publishing_client_channel.publish('event', 'message') do
@@ -563,6 +710,36 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
563
710
  end
564
711
  end
565
712
  end
713
+
714
+ it 'retains the client_serial (#RTN15c2, #RTN15c3)' do
715
+ last_message = nil
716
+ channel = client.channels.get("foo")
717
+
718
+ connection.once(:connected) do
719
+ connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
720
+ if protocol_message.action == :message
721
+ last_message = protocol_message
722
+ end
723
+ end
724
+
725
+ channel.publish("first") do
726
+ expect(last_message.message_serial).to eql(0)
727
+ channel.publish("second") do
728
+ expect(last_message.message_serial).to eql(1)
729
+ connection.once(:connected) do
730
+ channel.publish("first on resumed connection") do
731
+ # Message serial reset after failed resume
732
+ expect(last_message.message_serial).to eql(2)
733
+ stop_reactor
734
+ end
735
+ end
736
+
737
+ # simulate connection dropped to re-establish web socket
738
+ connection.transition_state_machine :disconnected
739
+ end
740
+ end
741
+ end
742
+ end
566
743
  end
567
744
 
568
745
  context 'when failing to resume' do
@@ -589,36 +766,159 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
589
766
  end
590
767
  end
591
768
 
592
- it 'detaches all channels' do
769
+ it 'issue a reattach for all attached channels and fail all message awaiting an ACK (#RTN15c3)' do
593
770
  channel_count = 10
594
771
  channels = channel_count.times.map { |index| client.channel("channel-#{index}") }
595
772
  when_all(*channels.map(&:attach)) do
596
- detached_channels = []
773
+ attached_channels = []
774
+ reattaching_channels = []
775
+ attach_protocol_messages = []
776
+ failed_messages = []
777
+
597
778
  channels.each do |channel|
598
- channel.on(:detached) do |channel_state_change|
779
+ channel.publish("foo").errback do
780
+ failed_messages << channel
781
+ end
782
+ channel.on(:attaching) do |channel_state_change|
599
783
  error = channel_state_change.reason
600
784
  expect(error.message).to match(/Unable to recover connection/i)
601
- detached_channels << channel
602
- next unless detached_channels.count == channel_count
603
- expect(detached_channels.count).to eql(channel_count)
785
+ reattaching_channels << channel
786
+ end
787
+ channel.on(:attached) do
788
+ attached_channels << channel
789
+ next unless attached_channels.count == channel_count
790
+ expect(reattaching_channels.count).to eql(channel_count)
791
+ expect(failed_messages.count).to eql(channel_count)
792
+ expect(attach_protocol_messages.uniq).to match(channels.map(&:name))
793
+ stop_reactor
794
+ end
795
+ end
796
+
797
+ connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
798
+ if protocol_message.action == :attach
799
+ attach_protocol_messages << protocol_message.channel
800
+ end
801
+ end
802
+
803
+ kill_connection_transport_and_prevent_valid_resume
804
+ end
805
+ end
806
+
807
+ it 'issue a reattach for all attaching channels and fail all queued messages (#RTN15c3)' do
808
+ channel_count = 10
809
+ channels = channel_count.times.map { |index| client.channel("channel-#{index}") }
810
+
811
+ channels.map(&:attach)
812
+
813
+ attached_channels = []
814
+ attach_protocol_messages = []
815
+ failed_messages = []
816
+
817
+ channels.each do |channel|
818
+ channel.publish("foo").errback do
819
+ failed_messages << channel
820
+ end
821
+
822
+ channel.on(:attached) do |state_change|
823
+ attached_channels << channel
824
+ expect(state_change).to_not be_resumed
825
+ next unless attached_channels.count == channel_count
826
+ expect(failed_messages.count).to eql(channel_count)
827
+ expect(attach_protocol_messages.uniq).to match(channels.map(&:name))
828
+ stop_reactor
829
+ end
830
+ end
831
+
832
+ connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
833
+ if protocol_message.action == :attach
834
+ attach_protocol_messages << protocol_message.channel
835
+ end
836
+ end
837
+
838
+ client.connection.once(:connected) do
839
+ kill_connection_transport_and_prevent_valid_resume
840
+ end
841
+ end
842
+
843
+ it 'issue a attach for all suspended channels (#RTN15c3)' do
844
+ channel_count = 10
845
+ channels = channel_count.times.map { |index| client.channel("channel-#{index}") }
846
+
847
+ when_all(*channels.map(&:attach)) do
848
+ # Force all channels into a suspended state
849
+ channels.map do |channel|
850
+ channel.transition_state_machine! :suspended
851
+ expect(channel).to be_suspended
852
+ end
853
+
854
+ attached_channels = []
855
+ reattaching_channels = []
856
+ attach_protocol_messages = []
857
+
858
+ channels.each do |channel|
859
+ channel.on(:attaching) do
860
+ reattaching_channels << channel
861
+ end
862
+ channel.on(:attached) do
863
+ attached_channels << channel
864
+ next unless attached_channels.count == channel_count
865
+ expect(reattaching_channels.count).to eql(channel_count)
866
+ expect(attach_protocol_messages.uniq).to match(channels.map(&:name))
604
867
  stop_reactor
605
868
  end
606
869
  end
607
870
 
871
+ connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
872
+ if protocol_message.action == :attach
873
+ attach_protocol_messages << protocol_message.channel
874
+ end
875
+ end
876
+
608
877
  kill_connection_transport_and_prevent_valid_resume
609
878
  end
610
879
  end
611
880
 
612
- it 'emits an error on the channel and sets the error reason' do
881
+ it 'sets the error reason on each channel' do
613
882
  channel.attach do
883
+ channel.on(:attaching) do |state_change|
884
+ expect(state_change.reason.message).to match(/Unable to recover connection/i)
885
+ expect(state_change.reason.code).to eql(80008)
886
+ expect(channel.error_reason.code).to eql(80008)
887
+
888
+ channel.on(:attached) do |state_change|
889
+ stop_reactor
890
+ end
891
+ end
614
892
  kill_connection_transport_and_prevent_valid_resume
615
893
  end
894
+ end
616
895
 
617
- channel.on(:error) do |error|
618
- expect(error.message).to match(/Unable to recover connection/i)
619
- expect(error.code).to eql(80008)
620
- expect(channel.error_reason).to eql(error)
621
- stop_reactor
896
+ it 'resets the client_serial (#RTN15c3)' do
897
+ last_message = nil
898
+ channel = client.channels.get("foo")
899
+
900
+ connection.once(:connected) do
901
+ connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
902
+ if protocol_message.action == :message
903
+ last_message = protocol_message
904
+ end
905
+ end
906
+
907
+ channel.publish("first") do
908
+ expect(last_message.message_serial).to eql(0)
909
+ channel.publish("second") do
910
+ expect(last_message.message_serial).to eql(1)
911
+ connection.once(:connected) do
912
+ channel.publish("first on new connection") do
913
+ # Message serial reset after failed resume
914
+ expect(last_message.message_serial).to eql(0)
915
+ stop_reactor
916
+ end
917
+ end
918
+
919
+ kill_connection_transport_and_prevent_valid_resume
920
+ end
921
+ end
622
922
  end
623
923
  end
624
924
  end
@@ -649,6 +949,137 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
649
949
  end
650
950
  end
651
951
  end
952
+
953
+ context 'when an ERROR protocol message is received' do
954
+ %w(connecting connected).each do |state|
955
+ state = state.to_sym
956
+ context "whilst #{state}" do
957
+ context 'with a token error code in the range 40140 <= code < 40150 (#RTN14b)' do
958
+ let(:client_options) { default_options.merge(use_token_auth: true) }
959
+
960
+ it 'triggers a re-authentication' do
961
+ connection.once(state) do
962
+ current_token = client.auth.current_token_details
963
+
964
+ error_message = Ably::Models::ProtocolMessage.new(action: Ably::Models::ProtocolMessage::ACTION.Error.to_i, error: { code: 40140 })
965
+ connection.__incoming_protocol_msgbus__.publish :protocol_message, error_message
966
+
967
+ connection.once(:connected) do
968
+ expect(client.auth.current_token_details).to_not eql(current_token)
969
+ stop_reactor
970
+ end
971
+ end
972
+ end
973
+ end
974
+
975
+ context 'with an error code indicating an error other than a token failure (#RTN14g, #RTN15i)' do
976
+ it 'causes the connection to fail' do
977
+ connection.once(state) do
978
+ connection.once(:failed) do
979
+ stop_reactor
980
+ end
981
+
982
+ error_message = Ably::Models::ProtocolMessage.new(action: Ably::Models::ProtocolMessage::ACTION.Error.to_i, error: { code: 50000 })
983
+ connection.__incoming_protocol_msgbus__.publish :protocol_message, error_message
984
+ end
985
+ end
986
+ end
987
+
988
+ context 'with no error code indicating an error other than a token failure (#RTN14g, #RTN15i)' do
989
+ it 'causes the connection to fail' do
990
+ connection.once(state) do
991
+ connection.once(:failed) do
992
+ stop_reactor
993
+ end
994
+
995
+ error_message = Ably::Models::ProtocolMessage.new(action: Ably::Models::ProtocolMessage::ACTION.Error.to_i)
996
+ connection.__incoming_protocol_msgbus__.publish :protocol_message, error_message
997
+ end
998
+ end
999
+ end
1000
+ end
1001
+ end
1002
+ end
1003
+
1004
+ context "whilst resuming" do
1005
+ context "with a token error code in the region 40140 <= code < 40150 (#{}RTN15c5)" do
1006
+ before do
1007
+ stub_const 'Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER', 0 # allow token to be used even if about to expire
1008
+ stub_const 'Ably::Auth::TOKEN_DEFAULTS', Ably::Auth::TOKEN_DEFAULTS.merge(renew_token_buffer: 0) # Ensure tokens issued expire immediately after issue
1009
+ end
1010
+
1011
+ let!(:four_second_token) {
1012
+ rest_client.auth.request_token(ttl: 4).token
1013
+ }
1014
+
1015
+ let!(:normal_token) {
1016
+ rest_client.auth.request_token.token
1017
+ }
1018
+
1019
+ let(:client_options) do
1020
+ default_options.merge(auth_callback: Proc.new do
1021
+ @auth_requests ||= 0
1022
+ @auth_requests += 1
1023
+
1024
+ case @auth_requests
1025
+ when 1
1026
+ four_second_token
1027
+ when 2
1028
+ normal_token
1029
+ end
1030
+ end)
1031
+ end
1032
+
1033
+ it 'triggers a re-authentication and then resumes the connection' do
1034
+ connection.once(:connected) do
1035
+ connection_id = connection.id
1036
+
1037
+ connecting_attempts = 0
1038
+ connection.on(:connecting) { connecting_attempts += 1 }
1039
+
1040
+ connection.once(:connected) do
1041
+ expect(@auth_requests).to eql(2) # initial + reconnect fails due to expiry & then obtains new token
1042
+ expect(connecting_attempts).to eql(2) # reconnect with failed token, then reconnect with successful token
1043
+ expect(connection.id).to eql(connection_id)
1044
+ stop_reactor
1045
+ end
1046
+
1047
+ # Prevent token expired DISCONNECTED arriving on the transport
1048
+ # Instead we want to let the client lib catch a transport closed event
1049
+ # Then attempt to reconnect with an expired token
1050
+ connection.transport.__incoming_protocol_msgbus__.unsubscribe
1051
+
1052
+ EventMachine.next_tick do
1053
+ # Lock the EventMachine for 4 seconds until the token has expired
1054
+ sleep 5
1055
+
1056
+ # Simulate an abrupt disconnection which will in turn resume but with an expired token
1057
+ connection.transport.close_connection_after_writing
1058
+ end
1059
+ end
1060
+ end
1061
+ end
1062
+ end
1063
+
1064
+ context 'with any other error (#RTN15c4)' do
1065
+ it 'moves the connection to the failed state' do
1066
+ channel = client.channels.get("foo")
1067
+ channel.attach do
1068
+ connection.once(:failed) do |state_change|
1069
+ expect(state_change.reason.code).to eql(40400)
1070
+ expect(connection.error_reason.code).to eql(40400)
1071
+ expect(channel).to be_failed
1072
+ expect(channel.error_reason.code).to eql(40400)
1073
+ stop_reactor
1074
+ end
1075
+
1076
+ allow(client.rest_client.auth).to receive(:key).and_return("invalid.key:secret")
1077
+
1078
+ # Simulate an abrupt disconnection which will in turn resume with an invalid key
1079
+ connection.transport.close_connection_after_writing
1080
+ end
1081
+ end
1082
+ end
652
1083
  end
653
1084
 
654
1085
  describe 'fallback host feature' do