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
@@ -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