ably 0.8.15 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -60,9 +60,10 @@ describe Ably::Realtime::Connection, :event_machine do
60
60
 
61
61
  context 'for renewable tokens' do
62
62
  context 'that are valid for the duration of the test' do
63
- context 'with valid pre authorised token expiring in the future' do
63
+ context 'with valid pre authorized token expiring in the future' do
64
+ let(:client_options) { default_options.merge(use_token_auth: true) }
64
65
  it 'uses the existing token created by Auth' do
65
- client.auth.authorise(ttl: 300)
66
+ client.auth.authorize(ttl: 300)
66
67
  expect(client.auth).to_not receive(:request_token)
67
68
  connection.once(:connected) do
68
69
  stop_reactor
@@ -101,8 +102,8 @@ describe Ably::Realtime::Connection, :event_machine do
101
102
  context 'opening a new connection' do
102
103
  context 'with almost expired tokens' do
103
104
  before do
104
- # Authorise synchronously to ensure token has been issued
105
- client.auth.authorise_sync(ttl: ttl)
105
+ # Authorize synchronously to ensure token has been issued
106
+ client.auth.authorize_sync(ttl: ttl)
106
107
  end
107
108
 
108
109
  let(:ttl) { 2 }
@@ -137,7 +138,7 @@ describe Ably::Realtime::Connection, :event_machine do
137
138
  end
138
139
  let(:client_options) { default_options.merge(auth_callback: token_callback) }
139
140
 
140
- it 'renews the token on connect, and makes one immediate subsequent attempt to obtain a new token' do
141
+ it 'renews the token on connect, and makes one immediate subsequent attempt to obtain a new token (#RSA4b)' do
141
142
  started_at = Time.now.to_f
142
143
  connection.once(:disconnected) do
143
144
  connection.once(:disconnected) do |connection_state_change|
@@ -150,25 +151,33 @@ describe Ably::Realtime::Connection, :event_machine do
150
151
  end
151
152
 
152
153
  context 'when disconnected_retry_timeout is 0.5 seconds' do
153
- let(:client_options) { default_options.merge(disconnected_retry_timeout: 0.5, auth_callback: token_callback, log_level: :error) }
154
+ let(:client_options) { default_options.merge(disconnected_retry_timeout: 0.5, auth_callback: token_callback) }
154
155
 
155
156
  it 'renews the token on connect, and continues to attempt renew based on the retry schedule' do
156
- started_at = Time.now.to_f
157
157
  disconnect_count = 0
158
+ first_disconnected_at = nil
158
159
  connection.on(:disconnected) do |connection_state_change|
160
+ first_disconnected_at ||= begin
161
+ Time.now.to_f
162
+ end
159
163
  expect(connection_state_change.reason.code).to eql(40142) # token expired
160
- disconnect_count += 1
161
- if disconnect_count == 6
162
- expect(Time.now.to_f - started_at).to be > 4 * 0.5 # at least 4 0.5 second pauses should have happened
163
- expect(Time.now.to_f - started_at).to be < 9 # allow 1.5 seconds for each authentication cycle
164
+ if disconnect_count == 4 # 3 attempts to reconnect after initial
165
+ # First disconnect reattempts immediately as part of connect sequence
166
+ # Second disconnect reattempt immediately as part of disconnected retry sequence
167
+ # Following two take 0.5 second each
168
+ # Not convinced two immediate retries is necessary, but not worth engineering effort to fix given
169
+ # it's only one extra retry
170
+ expect(Time.now.to_f - first_disconnected_at).to be > 2 * 0.5
171
+ expect(Time.now.to_f - first_disconnected_at).to be < 9 # allow 1.5 seconds for each authentication cycle
164
172
  stop_reactor
165
173
  end
174
+ disconnect_count += 1
166
175
  end
167
176
  end
168
177
  end
169
178
 
170
179
  context 'using implicit token auth' do
171
- let(:client_options) { default_options.merge(use_token_auth: true, token_params: { ttl: ttl }) }
180
+ let(:client_options) { default_options.merge(use_token_auth: true, default_token_params: { ttl: ttl }) }
172
181
 
173
182
  before do
174
183
  stub_const 'Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER', -10 # ensure client lib thinks token is still valid
@@ -178,14 +187,18 @@ describe Ably::Realtime::Connection, :event_machine do
178
187
  connection.once(:disconnected) do
179
188
  expect(client.rest_client.connection).to receive(:post).
180
189
  with(/requestToken$/, anything).
181
- exactly(:once).
190
+ exactly(:twice). # it retries an expired token request immediately
182
191
  and_call_original
183
192
 
184
193
  expect(client.rest_client).to_not receive(:fallback_connection)
185
194
  expect(client).to_not receive(:fallback_endpoint)
186
195
 
196
+ # Connection will go into :disconnected, then back to
197
+ # :connecting, then :disconnected again
187
198
  connection.once(:disconnected) do
188
- stop_reactor
199
+ connection.once(:disconnected) do
200
+ stop_reactor
201
+ end
189
202
  end
190
203
  end
191
204
  end
@@ -198,7 +211,7 @@ describe Ably::Realtime::Connection, :event_machine do
198
211
  let(:ttl) { 5 }
199
212
  let(:channel_name) { random_str }
200
213
  let(:channel) { client.channel(channel_name) }
201
- let(:client_options) { default_options.merge(use_token_auth: true, token_params: { ttl: ttl }) }
214
+ let(:client_options) { default_options.merge(use_token_auth: true, default_token_params: { ttl: ttl }) }
202
215
 
203
216
  context 'the server' do
204
217
  it 'disconnects the client, and the client automatically renews the token and then reconnects', em_timeout: 15 do
@@ -229,6 +242,7 @@ describe Ably::Realtime::Connection, :event_machine do
229
242
  end
230
243
 
231
244
  context 'connection state' do
245
+ let(:publish_count) { 10 }
232
246
  let(:ttl) { 4 }
233
247
  let(:auth_requests) { [] }
234
248
  let(:token_callback) do
@@ -243,13 +257,15 @@ describe Ably::Realtime::Connection, :event_machine do
243
257
  let(:publishing_channel) { publishing_client.channels.get(channel_name) }
244
258
  let(:messages_received) { [] }
245
259
 
246
- def publish_and_check_first_disconnect
247
- 10.times.each { |index| publishing_channel.publish('event', index.to_s) }
260
+ def publish_and_check_disconnect(options = {})
261
+ iteration = options.fetch(:iteration) { 1 }
262
+ total_expected = publish_count * iteration
263
+ publish_count.times.each { |index| publishing_channel.publish('event', (total_expected - publish_count + index).to_s) }
248
264
  channel.subscribe('event') do |message|
249
265
  messages_received << message.data.to_i
250
- if messages_received.count == 10
251
- expect(messages_received).to match(10.times)
252
- expect(auth_requests.count).to eql(2)
266
+ if messages_received.count == total_expected
267
+ expect(messages_received).to match(total_expected.times)
268
+ expect(auth_requests.count).to eql(iteration + 1)
253
269
  EventMachine.add_timer(1) do
254
270
  channel.unsubscribe 'event'
255
271
  yield
@@ -258,25 +274,19 @@ describe Ably::Realtime::Connection, :event_machine do
258
274
  end
259
275
  end
260
276
 
261
- def publish_and_check_second_disconnect
262
- 10.times.each { |index| publishing_channel.publish('event', (index + 10).to_s) }
263
- channel.subscribe('event') do |message|
264
- messages_received << message.data.to_i
265
- if messages_received.count == 20
266
- expect(messages_received).to match(20.times)
267
- expect(auth_requests.count).to eql(3)
268
- stop_reactor
269
- end
270
- end
271
- end
272
-
273
- it 'retains messages published when disconnected twice during authentication', em_timeout: 20 do
277
+ it 'retains messages published when disconnected three times during authentication', em_timeout: 30 do
274
278
  publishing_channel.attach do
275
279
  channel.attach do
276
280
  connection.once(:disconnected) do
277
- publish_and_check_first_disconnect do
281
+ publish_and_check_disconnect(iteration: 1) do
278
282
  connection.once(:disconnected) do
279
- publish_and_check_second_disconnect
283
+ publish_and_check_disconnect(iteration: 2) do
284
+ connection.once(:disconnected) do
285
+ publish_and_check_disconnect(iteration: 3) do
286
+ stop_reactor
287
+ end
288
+ end
289
+ end
280
290
  end
281
291
  end
282
292
  end
@@ -322,13 +332,14 @@ describe Ably::Realtime::Connection, :event_machine do
322
332
  let!(:expired_token_details) do
323
333
  # Request a token synchronously
324
334
  token_client = auto_close Ably::Realtime::Client.new(default_options)
325
- token_client.auth.request_token_sync(ttl: 0.01)
335
+ token_client.auth.request_token_sync(ttl: ttl)
326
336
  end
327
337
 
328
338
  context 'opening a new connection' do
329
339
  let(:client_options) { default_options.merge(key: nil, token: expired_token_details.token, log_level: :none) }
340
+ let(:ttl) { 0.01 }
330
341
 
331
- it 'transitions state to failed', em_timeout: 10 do
342
+ it 'transitions state to failed (#RSA4a)', em_timeout: 10 do
332
343
  EventMachine.add_timer(1) do # wait for token to expire
333
344
  expect(expired_token_details).to be_expired
334
345
  connection.once(:connected) { raise 'Connection should never connect as token has expired' }
@@ -341,7 +352,17 @@ describe Ably::Realtime::Connection, :event_machine do
341
352
  end
342
353
 
343
354
  context 'when connected' do
344
- skip 'transitions state to failed'
355
+ let(:client_options) { default_options.merge(key: nil, token: expired_token_details.token, log_level: :none) }
356
+ let(:ttl) { 4 }
357
+
358
+ it 'transitions state to failed (#RSA4a)' do
359
+ connection.once(:connected) do
360
+ connection.once(:failed) do
361
+ expect(client.connection.error_reason.code).to eql(40142)
362
+ stop_reactor
363
+ end
364
+ end
365
+ end
345
366
  end
346
367
  end
347
368
  end
@@ -579,11 +600,13 @@ describe Ably::Realtime::Connection, :event_machine do
579
600
  end
580
601
 
581
602
  context 'when closing' do
582
- it 'raises an exception before the connection is closed' do
603
+ it 'fails the deferrable before the connection is closed' do
583
604
  connection.connect do
584
605
  connection.once(:closing) do
585
- expect { connection.connect }.to raise_error Ably::Exceptions::InvalidStateChange
586
- stop_reactor
606
+ connection.connect.errback do |error|
607
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
608
+ stop_reactor
609
+ end
587
610
  end
588
611
  connection.close
589
612
  end
@@ -605,10 +628,12 @@ describe Ably::Realtime::Connection, :event_machine do
605
628
  it 'the sent message msgSerial is 0 but the connection serial remains at -1' do
606
629
  channel.attach do
607
630
  connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
608
- connection.__outgoing_protocol_msgbus__.unsubscribe
609
- expect(protocol_message['msgSerial']).to eql(0)
610
- expect(connection.serial).to eql(-1)
611
- stop_reactor
631
+ if protocol_message.action == :message
632
+ connection.__outgoing_protocol_msgbus__.unsubscribe
633
+ expect(protocol_message['msgSerial']).to eql(0)
634
+ expect(connection.serial).to eql(-1)
635
+ stop_reactor
636
+ end
612
637
  end
613
638
  channel.publish('event', 'data')
614
639
  end
@@ -678,7 +703,6 @@ describe Ably::Realtime::Connection, :event_machine do
678
703
 
679
704
  def log_connection_changes
680
705
  connection.on(:closing) { events[:closing_emitted] = true }
681
- connection.on(:error) { events[:error_emitted] = true }
682
706
 
683
707
  connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
684
708
  events[:closed_message_from_server_received] = true if protocol_message.action == :closed
@@ -691,7 +715,6 @@ describe Ably::Realtime::Connection, :event_machine do
691
715
  expect(connection.state).to eq(:closed)
692
716
 
693
717
  EventMachine.add_timer(1) do # allow for all subscribers on incoming message bes
694
- expect(events[:error_emitted]).to_not eql(true)
695
718
  expect(events[:closed_message_from_server_received]).to_not eql(true)
696
719
  expect(events[:closing_emitted]).to eql(true)
697
720
  stop_reactor
@@ -708,7 +731,6 @@ describe Ably::Realtime::Connection, :event_machine do
708
731
  connection.on(:connected) do
709
732
  connection.on(:closed) do
710
733
  EventMachine.add_timer(1) do # allow for all subscribers on incoming message bus
711
- expect(events[:error_emitted]).to_not eql(true)
712
734
  expect(events[:closed_message_from_server_received]).to eql(true)
713
735
  expect(events[:closing_emitted]).to eql(true)
714
736
  stop_reactor
@@ -739,14 +761,13 @@ describe Ably::Realtime::Connection, :event_machine do
739
761
  connection.on(:closed) do
740
762
  expect(Time.now - close_requested_at).to be >= custom_timeout
741
763
  expect(connection.state).to eq(:closed)
742
- expect(events[:error_emitted]).to_not eql(true)
743
764
  expect(events[:closed_message_from_server_received]).to_not eql(true)
744
765
  expect(events[:closing_emitted]).to eql(true)
745
766
  stop_reactor
746
767
  end
747
768
 
748
769
  log_connection_changes
749
- connection.close
770
+ EventMachine.next_tick { connection.close }
750
771
  end
751
772
  end
752
773
  end
@@ -755,26 +776,117 @@ describe Ably::Realtime::Connection, :event_machine do
755
776
  end
756
777
 
757
778
  context '#ping' do
758
- it 'echoes a heart beat' do
779
+ it 'echoes a heart beat (#RTN13a)' do
759
780
  connection.on(:connected) do
760
781
  connection.ping do |time_elapsed|
761
782
  expect(time_elapsed).to be > 0
783
+ expect(time_elapsed).to be < 3
762
784
  stop_reactor
763
785
  end
764
786
  end
765
787
  end
766
788
 
767
- context 'when not connected' do
768
- it 'raises an exception' do
769
- expect { connection.ping }.to raise_error RuntimeError, /Cannot send a ping when connection/
770
- stop_reactor
789
+ it 'sends a unique ID in each protocol message (#RTN13e)' do
790
+ connection.on(:connected) do
791
+ heartbeat_ids = []
792
+ pings_complete = []
793
+ connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
794
+ if protocol_message.action == :heartbeat
795
+ heartbeat_ids << protocol_message.id
796
+ end
797
+ end
798
+
799
+ ping_block = Proc.new do
800
+ pings_complete << true
801
+ if pings_complete.length == 3
802
+ expect(heartbeat_ids.uniq.length).to eql(3)
803
+ stop_reactor
804
+ end
805
+ end
806
+
807
+ connection.ping(&ping_block)
808
+ connection.ping(&ping_block)
809
+ connection.ping(&ping_block)
810
+ end
811
+ end
812
+
813
+ it 'waits until the connection becomes CONNECTED when in the CONNETING state' do
814
+ connection.once(:connecting) do
815
+ connection.ping do |time_elapsed|
816
+ expect(connection.state).to eq(:connected)
817
+ stop_reactor
818
+ end
819
+ end
820
+ end
821
+
822
+ context 'with incompatible states' do
823
+ let(:client_options) { default_options.merge(log_level: :none) }
824
+
825
+ context 'when not connected' do
826
+ it 'fails the deferrable (#RTN13b)' do
827
+ connection.ping.errback do |error|
828
+ expect(error.message).to match(/Cannot send a ping when.*initialized/i)
829
+ stop_reactor
830
+ end
831
+ end
832
+ end
833
+
834
+ context 'when suspended' do
835
+ it 'fails the deferrable (#RTN13b)' do
836
+ connection.once(:connected) do
837
+ connection.transition_state_machine! :suspended
838
+ connection.ping.errback do |error|
839
+ expect(error.message).to match(/Cannot send a ping when.*suspended/i)
840
+ stop_reactor
841
+ end
842
+ end
843
+ end
844
+ end
845
+
846
+ context 'when failed' do
847
+ it 'fails the deferrable (#RTN13b)' do
848
+ connection.once(:connected) do
849
+ connection.transition_state_machine! :failed
850
+ connection.ping.errback do |error|
851
+ expect(error.message).to match(/Cannot send a ping when.*failed/i)
852
+ stop_reactor
853
+ end
854
+ end
855
+ end
856
+ end
857
+
858
+ context 'when closed' do
859
+ it 'fails the deferrable (#RTN13b)' do
860
+ connection.once(:connected) do
861
+ connection.close
862
+ connection.once(:closed) do
863
+ connection.ping.errback do |error|
864
+ expect(error.message).to match(/Cannot send a ping when.*closed/i)
865
+ stop_reactor
866
+ end
867
+ end
868
+ end
869
+ end
870
+ end
871
+
872
+ context 'when it becomes closed' do
873
+ it 'fails the deferrable (#RTN13b)' do
874
+ connection.once(:connected) do
875
+ connection.ping.errback do |error|
876
+ expect(error.message).to match(/Ping failed as connection has changed state to.*closing/i)
877
+ stop_reactor
878
+ end
879
+ connection.close
880
+ end
881
+ end
771
882
  end
772
883
  end
773
884
 
774
885
  context 'with a success block that raises an exception' do
775
886
  it 'catches the exception and logs the error' do
776
887
  connection.on(:connected) do
777
- expect(connection.logger).to receive(:error).with(/Forced exception/) do
888
+ expect(connection.logger).to receive(:error) do |*args, &block|
889
+ expect(args.concat([block ? block.call : nil]).join(',')).to match(/Forced exception/)
778
890
  stop_reactor
779
891
  end
780
892
  connection.ping { raise 'Forced exception' }
@@ -785,13 +897,22 @@ describe Ably::Realtime::Connection, :event_machine do
785
897
  context 'when ping times out' do
786
898
  let(:client_options) { default_options.merge(log_level: :error) }
787
899
 
788
- it 'logs a warning' do
900
+ it 'fails the deferrable logs a warning (#RTN13a, #RTN13c)' do
901
+ message_logged = false
789
902
  connection.once(:connected) do
790
903
  allow(connection).to receive(:defaults).and_return(connection.defaults.merge(realtime_request_timeout: 0.0001))
791
- expect(connection.logger).to receive(:warn).with(/Ping timed out/) do
792
- stop_reactor
904
+ expect(connection.logger).to receive(:warn) do |*args, &block|
905
+ expect(block.call).to match(/Ping timed out/)
906
+ message_logged = true
907
+ end
908
+ connection.ping.errback do |error|
909
+ EventMachine.add_timer(0.1) do
910
+ expect(error.message).to match(/Ping timed out/)
911
+ expect(error.code).to eql(50003)
912
+ expect(message_logged).to be_truthy
913
+ stop_reactor
914
+ end
793
915
  end
794
- connection.ping
795
916
  end
796
917
  end
797
918
 
@@ -807,6 +928,107 @@ describe Ably::Realtime::Connection, :event_machine do
807
928
  end
808
929
  end
809
930
 
931
+ context 'Heartbeats (#RTN23)' do
932
+ context 'heartbeat interval' do
933
+ context 'when reduced artificially' do
934
+ let(:protocol_message_attributes) do
935
+ {
936
+ action: Ably::Models::ProtocolMessage::ACTION.Connected.to_i,
937
+ connection_serial: 55,
938
+ connection_details: {
939
+ max_idle_interval: 2 * 1000
940
+ }
941
+ }
942
+ end
943
+ let(:client_options) { default_options.merge(realtime_request_timeout: 3) }
944
+ let(:expected_heartbeat_interval) { 5 }
945
+
946
+ it 'is the sum of the max_idle_interval and realtime_request_timeout (#RTN23a)' do
947
+ connection.once(:connected) do
948
+ connection.__incoming_protocol_msgbus__.publish :protocol_message, Ably::Models::ProtocolMessage.new(protocol_message_attributes)
949
+ EventMachine.next_tick do
950
+ expect(connection.heartbeat_interval).to eql(expected_heartbeat_interval)
951
+ stop_reactor
952
+ end
953
+ end
954
+ end
955
+
956
+ it 'disconnects the transport if no heartbeat received since connected (#RTN23a)' do
957
+ connection.once(:connected) do
958
+ connection.__incoming_protocol_msgbus__.publish :protocol_message, Ably::Models::ProtocolMessage.new(protocol_message_attributes)
959
+ last_received_at = Time.now
960
+ connection.once(:disconnected) do
961
+ expect(Time.now.to_i - last_received_at.to_i).to be_within(2).of(expected_heartbeat_interval)
962
+ stop_reactor
963
+ end
964
+ end
965
+ end
966
+
967
+ it 'disconnects the transport if no heartbeat received since last event received (#RTN23a)' do
968
+ connection.once(:connected) do
969
+ connection.__incoming_protocol_msgbus__.publish :protocol_message, Ably::Models::ProtocolMessage.new(protocol_message_attributes)
970
+ last_received_at = Time.now
971
+ EventMachine.add_timer(3) { client.channels.get('foo').attach }
972
+ connection.once(:disconnected) do
973
+ expect(Time.now.to_i - last_received_at.to_i).to be_within(2).of(expected_heartbeat_interval + 3)
974
+ stop_reactor
975
+ end
976
+ end
977
+ end
978
+ end
979
+ end
980
+
981
+ context 'transport-level heartbeats are supported in the websocket transport' do
982
+ it 'provides the heartbeats argument in the websocket connection params (#RTN23b)' do
983
+ expect(EventMachine).to receive(:connect) do |host, port, transport, object, url|
984
+ uri = URI.parse(url)
985
+ expect(CGI::parse(uri.query)['heartbeats'][0]).to eql('false')
986
+ stop_reactor
987
+ end
988
+ client
989
+ end
990
+
991
+ it 'receives websocket heartbeat messages (#RTN23b) [slow test as need to wait for heartbeat]', em_timeout: 45 do
992
+ skip "Heartbeats param is missing from realtime implementation, see https://github.com/ably/realtime/issues/656"
993
+
994
+ connection.once(:connected) do
995
+ connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
996
+ if protocol_message.action == :heartbeat
997
+ expect(protocol_message.attributes[:source]).to eql('websocket')
998
+ expect(connection.time_since_connection_confirmed_alive?).to be_within(1).of(0)
999
+ stop_reactor
1000
+ end
1001
+ end
1002
+ end
1003
+ end
1004
+ end
1005
+
1006
+ context 'with websocket heartbeats disabled (undocumented)' do
1007
+ let(:client_options) { default_options.merge(websocket_heartbeats_disabled: true) }
1008
+
1009
+ it 'does not provide the heartbeats argument in the websocket connection params (#RTN23b)' do
1010
+ expect(EventMachine).to receive(:connect) do |host, port, transport, object, url|
1011
+ uri = URI.parse(url)
1012
+ expect(CGI::parse(uri.query)['heartbeats'][0]).to be_nil
1013
+ stop_reactor
1014
+ end
1015
+ client
1016
+ end
1017
+
1018
+ it 'receives websocket protocol messages (#RTN23b) [slow test as need to wait for heartbeat]', em_timeout: 45 do
1019
+ connection.once(:connected) do
1020
+ connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
1021
+ if protocol_message.action == :heartbeat
1022
+ expect(protocol_message.attributes[:source]).to_not eql('websocket')
1023
+ expect(connection.time_since_connection_confirmed_alive?).to be_within(1).of(0)
1024
+ stop_reactor
1025
+ end
1026
+ end
1027
+ end
1028
+ end
1029
+ end
1030
+ end
1031
+
810
1032
  context '#details' do
811
1033
  let(:connection) { client.connection }
812
1034
 
@@ -817,7 +1039,7 @@ describe Ably::Realtime::Connection, :event_machine do
817
1039
  end
818
1040
  end
819
1041
 
820
- it 'contains the ConnectionDetails object once connected' do
1042
+ it 'contains the ConnectionDetails object once connected (#RTN21)' do
821
1043
  connection.on(:connected) do
822
1044
  expect(connection.details).to be_a(Ably::Models::ConnectionDetails)
823
1045
  expect(connection.details.connection_key).to_not be_nil
@@ -826,7 +1048,7 @@ describe Ably::Realtime::Connection, :event_machine do
826
1048
  end
827
1049
  end
828
1050
 
829
- it 'contains the new ConnectionDetails object once a subsequent connection is created' do
1051
+ it 'contains the new ConnectionDetails object once a subsequent connection is created (#RTN21)' do
830
1052
  connection.once(:connected) do
831
1053
  expect(connection.details.connection_key).to_not be_nil
832
1054
  old_key = connection.details.connection_key
@@ -841,13 +1063,13 @@ describe Ably::Realtime::Connection, :event_machine do
841
1063
  end
842
1064
  end
843
1065
 
844
- context 'with a different connection_state_ttl' do
1066
+ context 'with a different default connection_state_ttl' do
845
1067
  before do
846
1068
  old_defaults = Ably::Realtime::Connection::DEFAULTS
847
1069
  stub_const 'Ably::Realtime::Connection::DEFAULTS', old_defaults.merge(connection_state_ttl: 15)
848
1070
  end
849
1071
 
850
- it 'updates the private Connection#connection_state_ttl' do
1072
+ it 'updates the private Connection#connection_state_ttl when received from Ably in ConnectionDetails' do
851
1073
  expect(connection.connection_state_ttl).to eql(15)
852
1074
 
853
1075
  connection.once(:connected) do
@@ -954,7 +1176,7 @@ describe Ably::Realtime::Connection, :event_machine do
954
1176
  context "opening a new connection using a recently disconnected connection's #recovery_key" do
955
1177
  context 'connection#id and connection#key after recovery' do
956
1178
  it 'remains the same for id and party for key' do
957
- connection_key_consistent_part_regex = /.*?!(\w{5,})-/
1179
+ connection_key_consistent_part_regex = /.*?!([\w-]{5,})-\w+/
958
1180
  previous_connection_id = nil
959
1181
  previous_connection_key = nil
960
1182
 
@@ -1036,13 +1258,13 @@ describe Ably::Realtime::Connection, :event_machine do
1036
1258
  context 'with invalid formatted value sent to server' do
1037
1259
  let(:client_options) { default_options.merge(recover: 'not-a-valid-connection-key:1', log_level: :none) }
1038
1260
 
1039
- it 'emits a fatal error on the connection object, sets the #error_reason and disconnects' do
1040
- connection.once(:error) do |error|
1261
+ it 'sets the #error_reason and moves the connection to FAILED' do
1262
+ connection.once(:failed) do |state_change|
1041
1263
  expect(connection.state).to eq(:failed)
1042
- expect(error.message).to match(/Invalid connectionKey/i)
1264
+ expect(state_change.reason.message).to match(/Invalid connectionKey/i)
1043
1265
  expect(connection.error_reason.message).to match(/Invalid connectionKey/i)
1044
1266
  expect(connection.error_reason.code).to eql(80018)
1045
- expect(connection.error_reason).to eql(error)
1267
+ expect(connection.error_reason).to eql(state_change.reason)
1046
1268
  stop_reactor
1047
1269
  end
1048
1270
  end
@@ -1051,13 +1273,13 @@ describe Ably::Realtime::Connection, :event_machine do
1051
1273
  context 'with expired (missing) value sent to server' do
1052
1274
  let(:client_options) { default_options.merge(recover: 'wVIsgTHAB1UvXh7z-1991d8586:0', log_level: :fatal) }
1053
1275
 
1054
- it 'emits an error on the connection object, sets the #error_reason, yet will connect anyway' do
1055
- connection.once(:error) do |error|
1276
+ it 'connects but sets the error reason and includes the reason in the state change' do
1277
+ connection.once(:connected) do |state_change|
1056
1278
  expect(connection.state).to eq(:connected)
1057
- expect(error.message).to match(/Unable to recover connection/i)
1279
+ expect(state_change.reason.message).to match(/Unable to recover connection/i)
1058
1280
  expect(connection.error_reason.message).to match(/Unable to recover connection/i)
1059
1281
  expect(connection.error_reason.code).to eql(80008)
1060
- expect(connection.error_reason).to eql(error)
1282
+ expect(connection.error_reason).to eql(state_change.reason)
1061
1283
  stop_reactor
1062
1284
  end
1063
1285
  end
@@ -1088,22 +1310,22 @@ describe Ably::Realtime::Connection, :event_machine do
1088
1310
  end
1089
1311
 
1090
1312
  context 'when a state transition is unsupported' do
1091
- let(:client_options) { default_options.merge(log_level: :none) } # silence FATAL errors
1313
+ let(:client_options) { default_options.merge(log_level: :fatal) } # silence FATAL errors
1092
1314
 
1093
- it 'emits a InvalidStateChange' do
1315
+ it 'logs the invalid state change as fatal' do
1094
1316
  connection.connect do
1095
1317
  connection.transition_state_machine :initialized
1318
+ EventMachine.add_timer(1) { stop_reactor }
1096
1319
  end
1097
1320
 
1098
- connection.on(:error) do |error|
1099
- expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
1100
- stop_reactor
1321
+ expect(client.logger).to receive(:fatal).at_least(:once) do |*args, &block|
1322
+ expect(args.concat([block ? block.call : nil]).join(',')).to match(/Unable to transition/)
1101
1323
  end
1102
1324
  end
1103
1325
  end
1104
1326
 
1105
1327
  context 'protocol failure' do
1106
- let(:client_options) { default_options.merge(protocol: :json) }
1328
+ let(:client_options) { default_options.merge(protocol: :json, log_level: :none) }
1107
1329
 
1108
1330
  context 'receiving an invalid ProtocolMessage' do
1109
1331
  it 'emits an error on the connection and logs a fatal error message' do
@@ -1111,9 +1333,11 @@ describe Ably::Realtime::Connection, :event_machine do
1111
1333
  connection.transport.send(:driver).emit 'message', OpenStruct.new(data: { action: 500 }.to_json)
1112
1334
  end
1113
1335
 
1114
- expect(client.logger).to receive(:fatal).with(/Invalid Protocol Message/)
1115
- connection.on(:error) do |error|
1116
- expect(error.message).to match(/Invalid Protocol Message/)
1336
+ expect(client.logger).to receive(:fatal).at_least(:once) do |*args, &block|
1337
+ expect(args.concat([block ? block.call : nil]).join(',')).to match(/Invalid Protocol Message/)
1338
+ end
1339
+ connection.on(:failed) do |state_change|
1340
+ expect(state_change.reason.message).to match(/Invalid Protocol Message/)
1117
1341
  stop_reactor
1118
1342
  end
1119
1343
  end
@@ -1250,11 +1474,13 @@ describe Ably::Realtime::Connection, :event_machine do
1250
1474
  )
1251
1475
  end
1252
1476
 
1253
- it 'detaches the channels and prevents publishing of messages on those channels' do
1477
+ it 'moves the channels into the suspended state and prevents publishing of messages on those channels' do
1254
1478
  channel.attach do
1255
- channel.once(:detached) do
1256
- expect { channel.publish 'test' }.to raise_error(Ably::Exceptions::ChannelInactive)
1257
- stop_reactor
1479
+ channel.once(:suspended) do
1480
+ channel.publish('test').errback do |error|
1481
+ expect(error).to be_a(Ably::Exceptions::MessageQueueingDisabled)
1482
+ stop_reactor
1483
+ end
1258
1484
  end
1259
1485
 
1260
1486
  close_connection_proc = Proc.new do
@@ -1282,8 +1508,10 @@ describe Ably::Realtime::Connection, :event_machine do
1282
1508
  it 'sets all channels to failed and prevents publishing of messages on those channels' do
1283
1509
  channel.attach
1284
1510
  channel.once(:failed) do
1285
- expect { channel.publish 'test' }.to raise_error(Ably::Exceptions::ChannelInactive)
1286
- stop_reactor
1511
+ channel.publish('test').errback do |error|
1512
+ expect(error).to be_a(Ably::Exceptions::ChannelInactive)
1513
+ stop_reactor
1514
+ end
1287
1515
  end
1288
1516
  end
1289
1517
  end
@@ -1314,16 +1542,9 @@ describe Ably::Realtime::Connection, :event_machine do
1314
1542
  end
1315
1543
 
1316
1544
  context 'ConnectionStateChange object' do
1317
- def unbind
1318
- if connection.transport
1319
- connection.transport.unbind
1320
- else
1321
- EventMachine.add_timer(0.005) { unbind }
1322
- end
1323
- end
1324
-
1325
1545
  it 'has current state' do
1326
1546
  connection.on(:connected) do |connection_state_change|
1547
+ expect(connection_state_change.current).to be_a(Ably::Realtime::Connection::STATE)
1327
1548
  expect(connection_state_change.current).to eq(:connected)
1328
1549
  stop_reactor
1329
1550
  end
@@ -1331,11 +1552,20 @@ describe Ably::Realtime::Connection, :event_machine do
1331
1552
 
1332
1553
  it 'has a previous state' do
1333
1554
  connection.on(:connected) do |connection_state_change|
1555
+ expect(connection_state_change.previous).to be_a(Ably::Realtime::Connection::STATE)
1334
1556
  expect(connection_state_change.previous).to eq(:connecting)
1335
1557
  stop_reactor
1336
1558
  end
1337
1559
  end
1338
1560
 
1561
+ it 'has the event that generated the state change (#TH5)' do
1562
+ connection.on(:connected) do |connection_state_change|
1563
+ expect(connection_state_change.event).to be_a(Ably::Realtime::Connection::EVENT)
1564
+ expect(connection_state_change.event).to eq(:connected)
1565
+ stop_reactor
1566
+ end
1567
+ end
1568
+
1339
1569
  it 'contains a private API protocol_message attribute that is used for special state change events', :api_private do
1340
1570
  connection.on(:connected) do |connection_state_change|
1341
1571
  expect(connection_state_change.protocol_message).to be_a(Ably::Models::ProtocolMessage)
@@ -1385,7 +1615,7 @@ describe Ably::Realtime::Connection, :event_machine do
1385
1615
  expect(connection_state_change.retry_in).to eql(0)
1386
1616
  stop_reactor
1387
1617
  end
1388
- unbind
1618
+ EventMachine.add_timer(0.005) { connection.transport.unbind }
1389
1619
  end
1390
1620
  end
1391
1621
 
@@ -1406,36 +1636,125 @@ describe Ably::Realtime::Connection, :event_machine do
1406
1636
  expect(connection_state_change.retry_in).to be > 0
1407
1637
  stop_reactor
1408
1638
  end
1409
- unbind
1639
+ EventMachine.add_timer(0.005) { connection.transport.unbind }
1410
1640
  end
1411
1641
  connection.transport.unbind
1412
1642
  end
1413
1643
  end
1414
1644
  end
1415
1645
  end
1646
+
1647
+ context 'whilst CONNECTED' do
1648
+ context 'when a CONNECTED message is received (#RTN24)' do
1649
+ let(:connection_key) { random_str(32) }
1650
+ let(:protocol_message_attributes) do
1651
+ {
1652
+ action: Ably::Models::ProtocolMessage::ACTION.Connected.to_i,
1653
+ connection_serial: 55,
1654
+ connection_details: {
1655
+ client_id: 'bob',
1656
+ connection_key: connection_key,
1657
+ connection_state_ttl: 33 * 1000,
1658
+ max_frame_size: 555,
1659
+ max_inbound_rate: 999,
1660
+ max_message_size: 1310,
1661
+ server_id: 'us-east-1-a.foo.com',
1662
+ max_idle_interval: 4 * 1000
1663
+ }
1664
+ }
1665
+ end
1666
+
1667
+ it 'emits an UPDATE event' do
1668
+ connection.once(:connected) do
1669
+ connection.once(:update) do |connection_state_change|
1670
+ expect(connection_state_change.current).to eq(:connected)
1671
+ expect(connection_state_change.previous).to eq(:connected)
1672
+ expect(connection_state_change.retry_in).to be_nil
1673
+ expect(connection_state_change.reason).to be_nil
1674
+ expect(connection.state).to eq(:connected)
1675
+ stop_reactor
1676
+ end
1677
+
1678
+ connection.__incoming_protocol_msgbus__.publish :protocol_message, Ably::Models::ProtocolMessage.new(protocol_message_attributes)
1679
+ end
1680
+ end
1681
+
1682
+ it 'updates the ConnectionDetail and Connection attributes (#RTC8a1)' do
1683
+ connection.once(:connected) do
1684
+ expect(client.auth.client_id).to eql('*')
1685
+
1686
+ connection.once(:update) do |connection_state_change|
1687
+ expect(client.auth.client_id).to eql('bob')
1688
+ expect(connection.key).to eql(connection_key)
1689
+ expect(connection.serial).to eql(55)
1690
+ expect(connection.connection_state_ttl).to eql(33)
1691
+
1692
+ expect(connection.details.client_id).to eql('bob')
1693
+ expect(connection.details.connection_key).to eql(connection_key)
1694
+ expect(connection.details.connection_state_ttl).to eql(33)
1695
+ expect(connection.details.max_frame_size).to eql(555)
1696
+ expect(connection.details.max_inbound_rate).to eql(999)
1697
+ expect(connection.details.max_message_size).to eql(1310)
1698
+ expect(connection.details.server_id).to eql('us-east-1-a.foo.com')
1699
+ expect(connection.details.max_idle_interval).to eql(4)
1700
+ stop_reactor
1701
+ end
1702
+
1703
+ connection.__incoming_protocol_msgbus__.publish :protocol_message, Ably::Models::ProtocolMessage.new(protocol_message_attributes)
1704
+ end
1705
+ end
1706
+ end
1707
+
1708
+ context 'when a CONNECTED message with an error is received' do
1709
+ let(:protocol_message_attributes) do
1710
+ {
1711
+ action: Ably::Models::ProtocolMessage::ACTION.Connected.to_i,
1712
+ connection_serial: 22,
1713
+ error: { code: 50000, message: 'Internal failure' },
1714
+ }
1715
+ end
1716
+
1717
+ it 'emits an UPDATE event' do
1718
+ connection.once(:connected) do
1719
+ connection.on(:update) do |connection_state_change|
1720
+ expect(connection_state_change.current).to eq(:connected)
1721
+ expect(connection_state_change.previous).to eq(:connected)
1722
+ expect(connection_state_change.retry_in).to be_nil
1723
+ expect(connection_state_change.reason).to be_a(Ably::Models::ErrorInfo)
1724
+ expect(connection_state_change.reason.code).to eql(50000)
1725
+ expect(connection_state_change.reason.message).to match(/Internal failure/)
1726
+ expect(connection.state).to eq(:connected)
1727
+ stop_reactor
1728
+ end
1729
+
1730
+ connection.__incoming_protocol_msgbus__.publish :protocol_message, Ably::Models::ProtocolMessage.new(protocol_message_attributes)
1731
+ end
1732
+ end
1733
+ end
1734
+ end
1416
1735
  end
1417
1736
 
1418
1737
  context 'version params' do
1419
- it 'sends the protocol version param v' do
1738
+ it 'sends the protocol version param v (#G4, #RTN2f)' do
1420
1739
  expect(EventMachine).to receive(:connect) do |host, port, transport, object, url|
1421
1740
  uri = URI.parse(url)
1422
- expect(CGI::parse(uri.query)['v'][0]).to eql(Ably::PROTOCOL_VERSION)
1741
+ expect(CGI::parse(uri.query)['v'][0]).to eql('1.0')
1423
1742
  stop_reactor
1424
1743
  end
1425
1744
  client
1426
1745
  end
1427
1746
 
1428
- it 'sends the lib version param lib' do
1747
+ it 'sends the lib version param lib (#RTN2g)' do
1429
1748
  expect(EventMachine).to receive(:connect) do |host, port, transport, object, url|
1430
1749
  uri = URI.parse(url)
1431
- expect(CGI::parse(uri.query)['lib'][0]).to eql("ruby-#{Ably::VERSION}")
1750
+ expect(CGI::parse(uri.query)['lib'][0]).to match(/^ruby-1\.0\.\d+(-[\w\.]+)?+$/)
1432
1751
  stop_reactor
1433
1752
  end
1434
1753
  client
1435
1754
  end
1436
1755
 
1437
1756
  context 'with variant' do
1438
- let(:variant) { 'foo ' }
1757
+ let(:variant) { 'foo' }
1439
1758
 
1440
1759
  before do
1441
1760
  Ably.lib_variant = variant
@@ -1445,10 +1764,10 @@ describe Ably::Realtime::Connection, :event_machine do
1445
1764
  Ably.lib_variant = nil
1446
1765
  end
1447
1766
 
1448
- it 'sends the lib version param lib with the variant' do
1767
+ it 'sends the lib version param lib with the variant (#RTN2g + #RSC7b)' do
1449
1768
  expect(EventMachine).to receive(:connect) do |host, port, transport, object, url|
1450
1769
  uri = URI.parse(url)
1451
- expect(CGI::parse(uri.query)['lib'][0]).to eql("ruby-#{variant}-#{Ably::VERSION}")
1770
+ expect(CGI::parse(uri.query)['lib'][0]).to match(/^ruby-#{variant}-1\.0\.\d+(-[\w\.]+)?$/)
1452
1771
  stop_reactor
1453
1772
  end
1454
1773
  client