ably-rest 0.7.3 → 0.7.5

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 (69) hide show
  1. checksums.yaml +8 -8
  2. data/.travis.yml +1 -0
  3. data/SPEC.md +480 -472
  4. data/lib/ably-rest.rb +1 -1
  5. data/lib/submodules/ably-ruby/LICENSE.txt +1 -1
  6. data/lib/submodules/ably-ruby/README.md +107 -24
  7. data/lib/submodules/ably-ruby/SPEC.md +531 -398
  8. data/lib/submodules/ably-ruby/lib/ably/auth.rb +24 -16
  9. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +9 -0
  10. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +17 -9
  11. data/lib/submodules/ably-ruby/lib/ably/models/paginated_resource.rb +12 -8
  12. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +18 -10
  13. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +15 -4
  14. data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +4 -3
  15. data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +31 -2
  16. data/lib/submodules/ably-ruby/lib/ably/modules/message_emitter.rb +77 -0
  17. data/lib/submodules/ably-ruby/lib/ably/modules/safe_deferrable.rb +71 -0
  18. data/lib/submodules/ably-ruby/lib/ably/modules/safe_yield.rb +41 -0
  19. data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +28 -8
  20. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +0 -5
  21. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +24 -29
  22. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +54 -11
  23. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +21 -6
  24. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +7 -2
  25. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +29 -26
  26. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +4 -4
  27. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +41 -9
  28. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +72 -24
  29. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_state_machine.rb +26 -4
  30. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +19 -6
  31. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +74 -208
  32. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +264 -0
  33. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_manager.rb +59 -0
  34. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_state_machine.rb +64 -0
  35. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +1 -1
  36. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +6 -2
  37. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -1
  38. data/lib/submodules/ably-ruby/lib/ably/util/pub_sub.rb +3 -1
  39. data/lib/submodules/ably-ruby/lib/ably/util/safe_deferrable.rb +18 -0
  40. data/lib/submodules/ably-ruby/lib/ably/version.rb +1 -1
  41. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +2 -2
  42. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +28 -6
  43. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +116 -46
  44. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +55 -10
  45. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +32 -0
  46. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +456 -96
  47. data/lib/submodules/ably-ruby/spec/acceptance/realtime/stats_spec.rb +2 -2
  48. data/lib/submodules/ably-ruby/spec/acceptance/realtime/time_spec.rb +2 -2
  49. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +96 -7
  50. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +8 -0
  51. data/lib/submodules/ably-ruby/spec/shared/safe_deferrable_behaviour.rb +71 -0
  52. data/lib/submodules/ably-ruby/spec/support/api_helper.rb +1 -1
  53. data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +1 -1
  54. data/lib/submodules/ably-ruby/spec/support/test_app.rb +13 -7
  55. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +15 -14
  56. data/lib/submodules/ably-ruby/spec/unit/models/paginated_resource_spec.rb +4 -4
  57. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +17 -17
  58. data/lib/submodules/ably-ruby/spec/unit/models/stat_spec.rb +4 -4
  59. data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +28 -9
  60. data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +50 -0
  61. data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +76 -2
  62. data/lib/submodules/ably-ruby/spec/unit/realtime/channel_spec.rb +51 -20
  63. data/lib/submodules/ably-ruby/spec/unit/realtime/channels_spec.rb +3 -3
  64. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +30 -0
  65. data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +52 -26
  66. data/lib/submodules/ably-ruby/spec/unit/realtime/safe_deferrable_spec.rb +12 -0
  67. data/spec/spec_helper.rb +5 -0
  68. metadata +12 -4
  69. data/lib/submodules/ably-ruby/.ruby-version.old +0 -1
@@ -1,3 +1,3 @@
1
1
  module Ably
2
- VERSION = '0.7.3'
2
+ VERSION = '0.7.5'
3
3
  end
@@ -17,10 +17,10 @@ describe Ably::Realtime::Channel, '#history', :event_machine do
17
17
 
18
18
  let(:options) { { :protocol => :json } }
19
19
 
20
- it 'returns a Deferrable' do
20
+ it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
21
21
  channel.publish('event', payload) do |message|
22
22
  history = channel.history
23
- expect(history).to be_a(EventMachine::Deferrable)
23
+ expect(history).to be_a(Ably::Util::SafeDeferrable)
24
24
  history.callback do |messages|
25
25
  expect(messages.count).to eql(1)
26
26
  expect(messages).to be_a(Ably::Models::PaginatedResource)
@@ -84,12 +84,12 @@ describe Ably::Realtime::Channel, :event_machine do
84
84
  end
85
85
  end
86
86
 
87
- it 'returns a Deferrable' do
88
- expect(channel.attach).to be_a(EventMachine::Deferrable)
87
+ it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
88
+ expect(channel.attach).to be_a(Ably::Util::SafeDeferrable)
89
89
  stop_reactor
90
90
  end
91
91
 
92
- it 'calls the Deferrable callback on success' do
92
+ it 'calls the SafeDeferrable callback on success' do
93
93
  channel.attach.callback do |channel|
94
94
  expect(channel).to be_a(Ably::Realtime::Channel)
95
95
  expect(channel.state).to eq(:attached)
@@ -195,6 +195,26 @@ describe Ably::Realtime::Channel, :event_machine do
195
195
  stop_reactor
196
196
  end
197
197
  end
198
+
199
+ context 'and subsequent authorisation with suitable permissions' do
200
+ it 'attaches to the channel successfully and resets the channel error_reason' do
201
+ restricted_channel.attach
202
+ restricted_channel.once(:failed) do
203
+ restricted_client.close do
204
+ # A direct call to #authorise is synchronous
205
+ restricted_client.auth.authorise(api_key: api_key)
206
+
207
+ restricted_client.connect do
208
+ restricted_channel.once(:attached) do
209
+ expect(restricted_channel.error_reason).to be_nil
210
+ stop_reactor
211
+ end
212
+ restricted_channel.attach
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
198
218
  end
199
219
  end
200
220
 
@@ -230,9 +250,11 @@ describe Ably::Realtime::Channel, :event_machine do
230
250
  end
231
251
  end
232
252
 
233
- it 'returns a Deferrable' do
234
- expect(channel.attach).to be_a(EventMachine::Deferrable)
235
- stop_reactor
253
+ it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
254
+ channel.attach do
255
+ expect(channel.detach).to be_a(Ably::Util::SafeDeferrable)
256
+ stop_reactor
257
+ end
236
258
  end
237
259
 
238
260
  it 'calls the Deferrable callback on success' do
@@ -66,7 +66,8 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
66
66
  )
67
67
  end
68
68
 
69
- let(:expected_retry_attempts) { (max_time_in_state_for_tests / retry_every_for_tests).round }
69
+ # retry immediately after failure, then one retry every :retry_every_for_tests
70
+ let(:expected_retry_attempts) { 1 + (max_time_in_state_for_tests / retry_every_for_tests).round }
70
71
  let(:state_changes) { Hash.new { |hash, key| hash[key] = 0 } }
71
72
  let(:timer) { Hash.new }
72
73
 
@@ -288,21 +289,76 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
288
289
  end
289
290
 
290
291
  context 'when DISCONNECTED ProtocolMessage received from the server' do
291
- it 'reconnects automatically' do
292
+ it 'reconnects automatically and immediately' do
292
293
  fail_if_suspended_or_failed
293
294
 
294
295
  connection.once(:connected) do
295
296
  connection.once(:disconnected) do
296
- connection.once(:connected) do
297
- state_history = connection.state_history.map { |transition| transition[:state].to_sym }
298
- expect(state_history).to eql([:connecting, :connected, :disconnected, :connecting, :connected])
299
- stop_reactor
297
+ disconnected_at = Time.now.to_f
298
+ connection.once(:connecting) do
299
+ expect(Time.now.to_f).to be_within(0.25).of(disconnected_at)
300
+ connection.once(:connected) do
301
+ state_history = connection.state_history.map { |transition| transition[:state].to_sym }
302
+ expect(state_history).to eql([:connecting, :connected, :disconnected, :connecting, :connected])
303
+ stop_reactor
304
+ end
300
305
  end
301
306
  end
302
307
  protocol_message = Ably::Models::ProtocolMessage.new(action: Ably::Models::ProtocolMessage::ACTION.Disconnected.to_i)
303
308
  connection.__incoming_protocol_msgbus__.publish :protocol_message, protocol_message
304
309
  end
305
310
  end
311
+
312
+ context 'and subsequently fails to reconnect' do
313
+ let(:retry_every) { 1.5 }
314
+
315
+ before do
316
+ # Reconfigure client library retry periods and timeouts so that tests run quickly
317
+ stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
318
+ Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
319
+ disconnected: { retry_every: retry_every, max_time_in_state: 60 })
320
+ end
321
+
322
+ it "retries every CONNECT_RETRY_CONFIG[:disconnected][:retry_every] seconds" do
323
+ fail_if_suspended_or_failed
324
+
325
+ stubbed_first_attempt = false
326
+
327
+ connection.once(:connected) do
328
+ connection.once(:disconnected) do
329
+ connection.once(:connecting) do
330
+ connection.once(:disconnected) do
331
+ disconnected_at = Time.now.to_f
332
+ connection.once(:connecting) do
333
+ expect(Time.now.to_f - disconnected_at).to be > retry_every
334
+ state_history = connection.state_history.map { |transition| transition[:state].to_sym }
335
+ expect(state_history).to eql([:connecting, :connected, :disconnected, :connecting, :disconnected, :connecting])
336
+
337
+ # allow one more recoonect when reactor stopped
338
+ expect(connection.manager).to receive(:reconnect_transport)
339
+ stop_reactor
340
+ end
341
+ end
342
+
343
+ # When reconnect called simply open the transport and close immediately
344
+ expect(connection.manager).to receive(:reconnect_transport) do
345
+ next if stubbed_first_attempt
346
+
347
+ connection.manager.setup_transport do
348
+ EventMachine.next_tick do
349
+ connection.transport.unbind
350
+ stubbed_first_attempt = true
351
+ end
352
+ end
353
+ end
354
+ end
355
+ end
356
+
357
+ protocol_message = Ably::Models::ProtocolMessage.new(action: Ably::Models::ProtocolMessage::ACTION.Disconnected.to_i)
358
+ connection.__incoming_protocol_msgbus__.publish :protocol_message, protocol_message
359
+ end
360
+ end
361
+ end
306
362
  end
307
363
 
308
364
  context 'when websocket transport is closed' do
@@ -359,6 +415,16 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
359
415
  end
360
416
  end
361
417
 
418
+ it 'triggers the resume callback', api_private: true do
419
+ channel.attach do
420
+ connection.transport.close_connection_after_writing
421
+ connection.on_resume do
422
+ expect(connection).to be_connected
423
+ stop_reactor
424
+ end
425
+ end
426
+ end
427
+
362
428
  context 'when messages were published whilst the client was disconnected' do
363
429
  it 'receives the messages published whilst offline' do
364
430
  messages_received = false
@@ -394,55 +460,57 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
394
460
  end
395
461
  end
396
462
 
397
- context 'when failing to resume because the connection_key is not or no longer valid' do
398
- def kill_connection_transport_and_prevent_valid_resume
399
- connection.transport.close_connection_after_writing
400
- connection.configure_new '0123456789abcdef', '0123456789abcdef', -1 # force the resume connection key to be invalid
401
- end
402
-
403
- it 'updates the connection_id and connection_key' do
404
- connection.once(:connected) do
405
- previous_connection_id = connection.id
406
- previous_connection_key = connection.key
463
+ context 'when failing to resume' do
464
+ context 'because the connection_key is not or no longer valid' do
465
+ def kill_connection_transport_and_prevent_valid_resume
466
+ connection.transport.close_connection_after_writing
467
+ connection.configure_new '0123456789abcdef', '0123456789abcdef', -1 # force the resume connection key to be invalid
468
+ end
407
469
 
470
+ it 'updates the connection_id and connection_key' do
408
471
  connection.once(:connected) do
409
- expect(connection.key).to_not eql(previous_connection_key)
410
- expect(connection.id).to_not eql(previous_connection_id)
411
- stop_reactor
412
- end
413
-
414
- kill_connection_transport_and_prevent_valid_resume
415
- end
416
- end
472
+ previous_connection_id = connection.id
473
+ previous_connection_key = connection.key
417
474
 
418
- it 'detaches all channels' do
419
- channel_count = 10
420
- channels = channel_count.times.map { |index| client.channel("channel-#{index}") }
421
- when_all(*channels.map(&:attach)) do
422
- detached_channels = []
423
- channels.each do |channel|
424
- channel.on(:detached) do
425
- detached_channels << channel
426
- next unless detached_channels.count == channel_count
427
- expect(detached_channels.count).to eql(channel_count)
475
+ connection.once(:connected) do
476
+ expect(connection.key).to_not eql(previous_connection_key)
477
+ expect(connection.id).to_not eql(previous_connection_id)
428
478
  stop_reactor
429
479
  end
430
- end
431
480
 
432
- kill_connection_transport_and_prevent_valid_resume
481
+ kill_connection_transport_and_prevent_valid_resume
482
+ end
433
483
  end
434
- end
435
484
 
436
- it 'emits an error on the channel and sets the error reason' do
437
- client.channel(random_str).attach do |channel|
438
- channel.on(:error) do |error|
439
- expect(error.message).to match(/Invalid connection key/i)
440
- expect(error.code).to eql(80008)
441
- expect(channel.error_reason).to eql(error)
442
- stop_reactor
485
+ it 'detaches all channels' do
486
+ channel_count = 10
487
+ channels = channel_count.times.map { |index| client.channel("channel-#{index}") }
488
+ when_all(*channels.map(&:attach)) do
489
+ detached_channels = []
490
+ channels.each do |channel|
491
+ channel.on(:detached) do
492
+ detached_channels << channel
493
+ next unless detached_channels.count == channel_count
494
+ expect(detached_channels.count).to eql(channel_count)
495
+ stop_reactor
496
+ end
497
+ end
498
+
499
+ kill_connection_transport_and_prevent_valid_resume
443
500
  end
501
+ end
444
502
 
445
- kill_connection_transport_and_prevent_valid_resume
503
+ it 'emits an error on the channel and sets the error reason' do
504
+ client.channel(random_str).attach do |channel|
505
+ channel.on(:error) do |error|
506
+ expect(error.message).to match(/Invalid connection key/i)
507
+ expect(error.code).to eql(80008)
508
+ expect(channel.error_reason).to eql(error)
509
+ stop_reactor
510
+ end
511
+
512
+ kill_connection_transport_and_prevent_valid_resume
513
+ end
446
514
  end
447
515
  end
448
516
  end
@@ -461,7 +529,9 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
461
529
  )
462
530
  end
463
531
 
464
- let(:expected_retry_attempts) { (max_time_in_state_for_tests / retry_every_for_tests).round }
532
+ # Retry immediately and then wait retry_every before every subsequent attempt
533
+ let(:expected_retry_attempts) { 1 + (max_time_in_state_for_tests / retry_every_for_tests).round }
534
+
465
535
  let(:retry_count_for_one_state) { 1 + expected_retry_attempts } # initial connect then disconnected
466
536
  let(:retry_count_for_all_states) { 1 + expected_retry_attempts * 2 } # initial connection, disconnected & then suspended
467
537
 
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
2
  require 'spec_helper'
3
+ require 'ostruct'
3
4
 
4
5
  describe Ably::Realtime::Connection, :event_machine do
5
6
  let(:connection) { client.connection }
@@ -148,14 +149,12 @@ describe Ably::Realtime::Connection, :event_machine do
148
149
  connection.once(:connected) do
149
150
  started_at = Time.now
150
151
  connection.once(:disconnected) do |error|
151
- EventMachine.add_timer(1) do # allow 1 second
152
+ connection.once(:connected) do
153
+ expect(client.auth.current_token).to_not be_expired
152
154
  expect(Time.now - started_at >= ttl)
153
155
  expect(original_token).to be_expired
154
156
  expect(error.code).to eql(40140) # token expired
155
- connection.once(:connected) do
156
- expect(client.auth.current_token).to_not be_expired
157
- stop_reactor
158
- end
157
+ stop_reactor
159
158
  end
160
159
  end
161
160
  end
@@ -234,8 +233,8 @@ describe Ably::Realtime::Connection, :event_machine do
234
233
  end
235
234
 
236
235
  context '#connect' do
237
- it 'returns a Deferrable' do
238
- expect(connection.connect).to be_a(EventMachine::Deferrable)
236
+ it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
237
+ expect(connection.connect).to be_a(Ably::Util::SafeDeferrable)
239
238
  stop_reactor
240
239
  end
241
240
 
@@ -372,9 +371,9 @@ describe Ably::Realtime::Connection, :event_machine do
372
371
  end
373
372
 
374
373
  context '#close' do
375
- it 'returns a Deferrable' do
374
+ it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
376
375
  connection.connect do
377
- expect(connection.close).to be_a(EventMachine::Deferrable)
376
+ expect(connection.close).to be_a(Ably::Util::SafeDeferrable)
378
377
  stop_reactor
379
378
  end
380
379
  end
@@ -504,6 +503,17 @@ describe Ably::Realtime::Connection, :event_machine do
504
503
  stop_reactor
505
504
  end
506
505
  end
506
+
507
+ context 'with a success block that raises an exception' do
508
+ it 'catches the exception and logs the error' do
509
+ connection.on(:connected) do
510
+ expect(connection.logger).to receive(:error).with(/Forced exception/) do
511
+ stop_reactor
512
+ end
513
+ connection.ping { raise 'Forced exception' }
514
+ end
515
+ end
516
+ end
507
517
  end
508
518
 
509
519
  context 'recovery' do
@@ -591,7 +601,7 @@ describe Ably::Realtime::Connection, :event_machine do
591
601
  context 'connection#id and connection#key after recovery' do
592
602
  let(:client_options) { default_options.merge(log_level: :none) }
593
603
 
594
- it 'remain the same' do
604
+ it 'remains the same' do
595
605
  previous_connection_id = nil
596
606
  previous_connection_key = nil
597
607
 
@@ -610,6 +620,22 @@ describe Ably::Realtime::Connection, :event_machine do
610
620
  end
611
621
  end
612
622
  end
623
+
624
+ it 'does not trigger a resume callback', api_private: true do
625
+ connection.once(:connected) do
626
+ connection.transition_state_machine! :failed
627
+ end
628
+
629
+ connection.once(:failed) do
630
+ recover_client = Ably::Realtime::Client.new(default_options.merge(recover: client.connection.recovery_key))
631
+ recover_client.connection.on_resume do
632
+ raise 'Should not trigger resume callback'
633
+ end
634
+ recover_client.connection.on(:connected) do
635
+ EventMachine.add_timer(0.5) { stop_reactor }
636
+ end
637
+ end
638
+ end
613
639
  end
614
640
 
615
641
  context 'when messages have been sent whilst the old connection is disconnected' do
@@ -653,6 +679,7 @@ describe Ably::Realtime::Connection, :event_machine do
653
679
  it 'triggers a fatal error on the connection object, sets the #error_reason and disconnects' do
654
680
  connection.once(:error) do |error|
655
681
  expect(connection.state).to eq(:failed)
682
+ expect(error.message).to match(/Invalid connection key/)
656
683
  expect(connection.error_reason.message).to match(/Invalid connection key/)
657
684
  expect(connection.error_reason.code).to eql(40006)
658
685
  expect(connection.error_reason).to eql(error)
@@ -667,6 +694,7 @@ describe Ably::Realtime::Connection, :event_machine do
667
694
  it 'triggers an error on the connection object, sets the #error_reason, yet will connect anyway' do
668
695
  connection.once(:error) do |error|
669
696
  expect(connection.state).to eq(:connected)
697
+ expect(error.message).to match(/Invalid connection key/i)
670
698
  expect(connection.error_reason.message).to match(/Invalid connection key/i)
671
699
  expect(connection.error_reason.code).to eql(80008)
672
700
  expect(connection.error_reason).to eql(error)
@@ -714,6 +742,23 @@ describe Ably::Realtime::Connection, :event_machine do
714
742
  end
715
743
  end
716
744
 
745
+ context 'protocol failure' do
746
+ let(:client_options) { default_options.merge(protocol: :json) }
747
+
748
+ context 'receiving an invalid ProtocolMessage' do
749
+ it 'emits an error on the connection and logs a fatal error message' do
750
+ connection.connect do
751
+ connection.transport.send(:driver).emit 'message', OpenStruct.new(data: { action: 500 }.to_json)
752
+ end
753
+
754
+ expect(client.logger).to receive(:fatal).with(/Invalid Protocol Message/)
755
+ connection.on(:error) do |error|
756
+ expect(error.message).to match(/Invalid Protocol Message/)
757
+ stop_reactor
758
+ end
759
+ end
760
+ end
761
+ end
717
762
 
718
763
  context 'undocumented method' do
719
764
  context '#internet_up?' do
@@ -195,6 +195,38 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
195
195
  end
196
196
  end
197
197
 
198
+ context 'server incorrectly resends a message that was already received by the client library' do
199
+ let(:messages_received) { [] }
200
+ let(:connection) { client.connection }
201
+ let(:client_options) { default_options.merge(log_level: :fatal) }
202
+
203
+ it 'discards the message and logs it as an error to the channel' do
204
+ first_message_protocol_message = nil
205
+ connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
206
+ first_message_protocol_message ||= protocol_message unless protocol_message.messages.empty?
207
+ end
208
+
209
+ channel.subscribe do |message|
210
+ messages_received << message
211
+ if messages_received.count == 2
212
+ # simulate a duplicate protocol message being received
213
+ EventMachine.next_tick do
214
+ connection.__incoming_protocol_msgbus__.publish :protocol_message, first_message_protocol_message
215
+ end
216
+ end
217
+ end
218
+ 2.times { |i| EventMachine.add_timer(i.to_f / 5) { channel.publish('event', 'data') } }
219
+
220
+ channel.on(:error) do |error|
221
+ expect(error.message).to match(/duplicate/)
222
+ EventMachine.add_timer(0.5) do
223
+ expect(messages_received.count).to eql(2)
224
+ stop_reactor
225
+ end
226
+ end
227
+ end
228
+ end
229
+
198
230
  context 'encoding and decoding encrypted messages' do
199
231
  shared_examples 'an Ably encrypter and decrypter' do |item, data|
200
232
  let(:algorithm) { data['algorithm'].upcase }