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
@@ -75,6 +75,40 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
75
75
  end
76
76
  end
77
77
 
78
+ context 'with supported extra payload content type (#RTL6h, #RSL6a2)' do
79
+ def publish_and_check_extras(extras)
80
+ channel.attach
81
+ channel.publish 'event', {}, extras: extras
82
+ channel.subscribe do |message|
83
+ expect(message.extras).to eql(extras)
84
+ stop_reactor
85
+ end
86
+ end
87
+
88
+ context 'JSON Object (Hash)' do
89
+ let(:data) { { 'push' => { 'title' => 'Testing' } } }
90
+
91
+ it 'is encoded and decoded to the same hash' do
92
+ publish_and_check_extras data
93
+ end
94
+ end
95
+
96
+ context 'JSON Array' do
97
+ let(:data) { { 'push' => [ nil, true, false, 55, 'string', { 'Hash' => true }, ['array'] ] } }
98
+
99
+ it 'is encoded and decoded to the same Array' do
100
+ publish_and_check_extras data
101
+ end
102
+ end
103
+
104
+ context 'nil' do
105
+ it 'is encoded and decoded to the same Array' do
106
+ channel.publish 'event', {}, extras: nil
107
+ publish_and_check_extras nil
108
+ end
109
+ end
110
+ end
111
+
78
112
  context 'with unsupported data payload content type' do
79
113
  context 'Integer' do
80
114
  let(:data) { 1 }
@@ -327,8 +361,9 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
327
361
  end
328
362
  2.times { |i| EventMachine.add_timer(i.to_f / 5) { channel.publish('event', 'data') } }
329
363
 
330
- channel.on(:error) do |error|
331
- expect(error.message).to match(/duplicate/)
364
+ expect(client.logger).to receive(:error) do |*args, &block|
365
+ expect(args.concat([block ? block.call : nil]).join(',')).to match(/duplicate/)
366
+
332
367
  EventMachine.add_timer(0.5) do
333
368
  expect(messages_received.count).to eql(2)
334
369
  stop_reactor
@@ -374,7 +409,7 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
374
409
 
375
410
  let(:encrypted_channel) { client.channel(channel_name, cipher: cipher_options) }
376
411
 
377
- it 'encrypts message automatically before they are pushed to the server' do
412
+ it 'encrypts message automatically before they are pushed to the server (#RTL7d)' do
378
413
  encrypted_channel.__incoming_msgbus__.unsubscribe # remove all subscribe callbacks that could decrypt the message
379
414
 
380
415
  encrypted_channel.__incoming_msgbus__.subscribe(:message) do |message|
@@ -392,7 +427,7 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
392
427
  encrypted_channel.publish 'example', encoded_data_decoded
393
428
  end
394
429
 
395
- it 'sends and receives messages that are encrypted & decrypted by the Ably library' do
430
+ it 'sends and receives messages that are encrypted & decrypted by the Ably library (#RTL7d)' do
396
431
  encrypted_channel.publish 'example', encoded_data_decoded
397
432
  encrypted_channel.subscribe do |message|
398
433
  expect(message.data).to eql(encoded_data_decoded)
@@ -413,12 +448,12 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
413
448
  end
414
449
  end
415
450
 
416
- context 'with AES-128-CBC using crypto-data-128.json fixtures' do
451
+ context 'with AES-128-CBC using crypto-data-128.json fixtures (#RTL7d)' do
417
452
  data = JSON.parse(File.read(File.join(resources_root, 'crypto-data-128.json')))
418
453
  add_tests_for_data data
419
454
  end
420
455
 
421
- context 'with AES-256-CBC using crypto-data-256.json fixtures' do
456
+ context 'with AES-256-CBC using crypto-data-256.json fixtures (#RTL7d)' do
422
457
  data = JSON.parse(File.read(File.join(resources_root, 'crypto-data-256.json')))
423
458
  add_tests_for_data data
424
459
  end
@@ -521,7 +556,7 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
521
556
 
522
557
  let(:payload) { MessagePack.pack({ 'key' => random_str }) }
523
558
 
524
- it 'delivers the message but still encrypted with a value in the #encoding attribute' do
559
+ it 'delivers the message but still encrypted with a value in the #encoding attribute (#RTL7e)' do
525
560
  unencrypted_channel_client2.attach do
526
561
  encrypted_channel_client1.publish 'example', payload
527
562
  unencrypted_channel_client2.subscribe do |message|
@@ -532,15 +567,13 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
532
567
  end
533
568
  end
534
569
 
535
- it 'emits a Cipher error on the channel' do
570
+ it 'logs a Cipher error (#RTL7e)' do
536
571
  unencrypted_channel_client2.attach do
537
- encrypted_channel_client1.publish 'example', payload
538
- unencrypted_channel_client2.on(:error) do |error|
539
- expect(error).to be_a(Ably::Exceptions::CipherError)
540
- expect(error.code).to eql(92001)
541
- expect(error.message).to match(/Message cannot be decrypted/)
572
+ expect(other_client.logger).to receive(:error) do |*args, &block|
573
+ expect(args.concat([block ? block.call : nil]).join(',')).to match(/Message cannot be decrypted/)
542
574
  stop_reactor
543
575
  end
576
+ encrypted_channel_client1.publish 'example', payload
544
577
  end
545
578
  end
546
579
  end
@@ -554,7 +587,7 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
554
587
 
555
588
  let(:payload) { MessagePack.pack({ 'key' => random_str }) }
556
589
 
557
- it 'delivers the message but still encrypted with the cipher detials in the #encoding attribute' do
590
+ it 'delivers the message but still encrypted with the cipher detials in the #encoding attribute (#RTL7e)' do
558
591
  encrypted_channel_client1.publish 'example', payload
559
592
  encrypted_channel_client2.subscribe do |message|
560
593
  expect(message.data).to_not eql(payload)
@@ -563,13 +596,11 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
563
596
  end
564
597
  end
565
598
 
566
- it 'emits a Cipher error on the channel' do
599
+ it 'emits a Cipher error on the channel (#RTL7e)' do
567
600
  encrypted_channel_client2.attach do
568
601
  encrypted_channel_client1.publish 'example', payload
569
- encrypted_channel_client2.on(:error) do |error|
570
- expect(error).to be_a(Ably::Exceptions::CipherError)
571
- expect(error.code).to eql(92002)
572
- expect(error.message).to match(/Cipher algorithm [\w-]+ does not match/)
602
+ expect(other_client.logger).to receive(:error) do |*args, &block|
603
+ expect(args.concat([block ? block.call : nil]).join(',')).to match(/Cipher algorithm [\w-]+ does not match/)
573
604
  stop_reactor
574
605
  end
575
606
  end
@@ -599,10 +630,8 @@ describe 'Ably::Realtime::Channel Message', :event_machine do
599
630
  it 'emits a Cipher error on the channel' do
600
631
  encrypted_channel_client2.attach do
601
632
  encrypted_channel_client1.publish 'example', payload
602
- encrypted_channel_client2.on(:error) do |error|
603
- expect(error).to be_a(Ably::Exceptions::CipherError)
604
- expect(error.code).to eql(92003)
605
- expect(error.message).to match(/CipherError decrypting data/)
633
+ expect(other_client.logger).to receive(:error) do |*args, &block|
634
+ expect(args.concat([block ? block.call : nil]).join(',')).to match(/CipherError decrypting data/)
606
635
  stop_reactor
607
636
  end
608
637
  end
@@ -94,9 +94,11 @@ describe Ably::Realtime::Presence, 'history', :event_machine do
94
94
  end
95
95
  end
96
96
 
97
- it 'raises an exception unless state is attached' do
98
- expect { presence_client_one.history(until_attach: true) }.to raise_error(ArgumentError, /not attached/)
99
- stop_reactor
97
+ it 'fails with an exception unless state is attached' do
98
+ presence_client_one.history(until_attach: true).errback do |error|
99
+ expect(error.message).to match(/not attached/)
100
+ stop_reactor
101
+ end
100
102
  end
101
103
  end
102
104
  end
@@ -42,11 +42,18 @@ describe Ably::Realtime::Presence, :event_machine do
42
42
 
43
43
  def setup_test(method_name, args, options)
44
44
  if options[:enter_first]
45
- presence_client_one.subscribe do
45
+ acked = false
46
+ received = false
47
+ presence_client_one.public_send(method_name.to_s.gsub(/leave|update/, 'enter'), args) do
48
+ acked = true
49
+ yield if acked & received
50
+ end
51
+ presence_client_one.subscribe do |presence_message|
52
+ expect(presence_message.action).to eq(:enter)
46
53
  presence_client_one.unsubscribe
47
- yield
54
+ received = true
55
+ yield if acked & received
48
56
  end
49
- presence_client_one.public_send(method_name.to_s.gsub(/leave|update/, 'enter'), args)
50
57
  else
51
58
  yield
52
59
  end
@@ -58,8 +65,30 @@ describe Ably::Realtime::Presence, :event_machine do
58
65
  channel_client_one.attach do
59
66
  channel_client_one.transition_state_machine :detaching
60
67
  channel_client_one.once(:detached) do
61
- expect { presence_client_one.public_send(method_name, args) }.to raise_error Ably::Exceptions::InvalidStateChange, /Operation is not allowed when channel is in STATE.detached/i
62
- stop_reactor
68
+ presence_client_one.public_send(method_name, args).tap do |deferrable|
69
+ deferrable.callback { raise 'Get should not succeed' }
70
+ deferrable.errback do |error|
71
+ expect(error).to be_a(Ably::Exceptions::InvalidState)
72
+ expect(error.message).to match(/Operation is not allowed when channel is in STATE.Detached/)
73
+ stop_reactor
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ it 'raise an exception if the channel becomes detached' do
82
+ setup_test(method_name, args, options) do
83
+ channel_client_one.attach do
84
+ channel_client_one.transition_state_machine :detaching
85
+ presence_client_one.public_send(method_name, args).tap do |deferrable|
86
+ deferrable.callback { raise 'Get should not succeed' }
87
+ deferrable.errback do |error|
88
+ expect(error).to be_a(Ably::Exceptions::InvalidState)
89
+ expect(error.message).to match(/Operation failed as channel transitioned to STATE.Detached/)
90
+ stop_reactor
91
+ end
63
92
  end
64
93
  end
65
94
  end
@@ -70,8 +99,30 @@ describe Ably::Realtime::Presence, :event_machine do
70
99
  channel_client_one.attach do
71
100
  channel_client_one.transition_state_machine :failed
72
101
  expect(channel_client_one.state).to eq(:failed)
73
- expect { presence_client_one.public_send(method_name, args) }.to raise_error Ably::Exceptions::InvalidStateChange, /Operation is not allowed when channel is in STATE.failed/i
74
- stop_reactor
102
+ presence_client_one.public_send(method_name, args).tap do |deferrable|
103
+ deferrable.callback { raise 'Get should not succeed' }
104
+ deferrable.errback do |error|
105
+ expect(error).to be_a(Ably::Exceptions::InvalidState)
106
+ expect(error.message).to match(/Operation is not allowed when channel is in STATE.Failed/)
107
+ stop_reactor
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ it 'raise an exception if the channel becomes failed' do
115
+ setup_test(method_name, args, options) do
116
+ channel_client_one.attach do
117
+ presence_client_one.public_send(method_name, args).tap do |deferrable|
118
+ deferrable.callback { raise 'Get should not succeed' }
119
+ deferrable.errback do |error|
120
+ expect(error).to be_a(Ably::Exceptions::MessageDeliveryFailed)
121
+ stop_reactor
122
+ end
123
+ end
124
+ channel_client_one.transition_state_machine :failed
125
+ expect(channel_client_one.state).to eq(:failed)
75
126
  end
76
127
  end
77
128
  end
@@ -88,20 +139,24 @@ describe Ably::Realtime::Presence, :event_machine do
88
139
  let(:client_one) { auto_close Ably::Realtime::Client.new(default_options.merge(queue_messages: false, client_id: client_id)) }
89
140
 
90
141
  context 'and connection state initialized' do
91
- it 'raises an exception' do
92
- expect { presence_client_one.public_send(method_name, args) }.to raise_error Ably::Exceptions::MessageQueueingDisabled
142
+ it 'fails the deferrable' do
143
+ presence_client_one.public_send(method_name, args).errback do |error|
144
+ expect(error).to be_a(Ably::Exceptions::MessageQueueingDisabled)
145
+ stop_reactor
146
+ end
93
147
  expect(client_one.connection).to be_initialized
94
- stop_reactor
95
148
  end
96
149
  end
97
150
 
98
151
  context 'and connection state connecting' do
99
- it 'raises an exception' do
152
+ it 'fails the deferrable' do
100
153
  client_one.connect
101
154
  EventMachine.next_tick do
102
- expect { presence_client_one.public_send(method_name, args) }.to raise_error Ably::Exceptions::MessageQueueingDisabled
155
+ presence_client_one.public_send(method_name, args).errback do |error|
156
+ expect(error).to be_a(Ably::Exceptions::MessageQueueingDisabled)
157
+ stop_reactor
158
+ end
103
159
  expect(client_one.connection).to be_connecting
104
- stop_reactor
105
160
  end
106
161
  end
107
162
  end
@@ -109,12 +164,14 @@ describe Ably::Realtime::Presence, :event_machine do
109
164
  context 'and connection state disconnected' do
110
165
  let(:client_one) { auto_close Ably::Realtime::Client.new(default_options.merge(queue_messages: false, client_id: client_id, :log_level => :error)) }
111
166
 
112
- it 'raises an exception' do
167
+ it 'fails the deferrable' do
113
168
  client_one.connection.once(:connected) do
114
169
  client_one.connection.once(:disconnected) do
115
- expect { presence_client_one.public_send(method_name, args) }.to raise_error Ably::Exceptions::MessageQueueingDisabled
170
+ presence_client_one.public_send(method_name, args).errback do |error|
171
+ expect(error).to be_a(Ably::Exceptions::MessageQueueingDisabled)
172
+ stop_reactor
173
+ end
116
174
  expect(client_one.connection).to be_disconnected
117
- stop_reactor
118
175
  end
119
176
  client_one.connection.transition_state_machine :disconnected
120
177
  end
@@ -260,7 +317,8 @@ describe Ably::Realtime::Presence, :event_machine do
260
317
 
261
318
  it 'catches exceptions in the provided method block and logs them to the logger' do
262
319
  setup_test(method_name, args, options) do
263
- expect(presence_client_one.logger).to receive(:error).with(/Intentional exception/) do
320
+ expect(presence_client_one.logger).to receive(:error) do |*args, &block|
321
+ expect(args.concat([block ? block.call : nil]).join(',')).to match(/Intentional exception/)
264
322
  stop_reactor
265
323
  end
266
324
  presence_client_one.public_send(method_name, args) { raise 'Intentional exception' }
@@ -323,6 +381,13 @@ describe Ably::Realtime::Presence, :event_machine do
323
381
  stop_reactor
324
382
  end
325
383
  end
384
+
385
+ context 'and a client_id that is not a string type' do
386
+ it 'throws an exception' do
387
+ expect { presence_channel.public_send(method_name, 1) }.to raise_error Ably::Exceptions::IncompatibleClientId
388
+ stop_reactor
389
+ end
390
+ end
326
391
  end
327
392
 
328
393
  context ":#{method_name} when authenticated with a valid client_id" do
@@ -411,14 +476,14 @@ describe Ably::Realtime::Presence, :event_machine do
411
476
  end
412
477
 
413
478
  context 'when attached (but not present) on a presence channel with an anonymous client (no client ID)' do
414
- it 'maintains state as other clients enter and leave the channel' do
479
+ it 'maintains state as other clients enter and leave the channel (#RTP2e)' do
415
480
  channel_anonymous_client.attach do
416
481
  presence_anonymous_client.subscribe(:enter) do |presence_message|
417
482
  expect(presence_message.client_id).to eql(client_one.client_id)
418
483
 
419
484
  presence_anonymous_client.get do |members|
420
485
  expect(members.first.client_id).to eql(client_one.client_id)
421
- expect(members.first.action).to eq(:enter)
486
+ expect(members.first.action).to eq(:present)
422
487
 
423
488
  presence_anonymous_client.subscribe(:leave) do |leave_presence_message|
424
489
  expect(leave_presence_message.client_id).to eql(client_one.client_id)
@@ -438,7 +503,7 @@ describe Ably::Realtime::Presence, :event_machine do
438
503
  end
439
504
  end
440
505
 
441
- context '#members map', api_private: true do
506
+ context '#members map / PresenceMap (#RTP2)', api_private: true do
442
507
  it 'is available once the channel is created' do
443
508
  expect(presence_anonymous_client.members).to_not be_nil
444
509
  stop_reactor
@@ -490,29 +555,199 @@ describe Ably::Realtime::Presence, :event_machine do
490
555
  end
491
556
  end
492
557
  end
558
+
559
+ context 'the map is based on the member_key (connection_id & client_id)' do
560
+ # 2 unqiue client_id with client_id "b" being on two connections
561
+ let(:enter_action) { 2 }
562
+ let(:presence_data) do
563
+ [
564
+ { client_id: 'a', connection_id: 'one', id: 'one:0:0', action: enter_action },
565
+ { client_id: 'b', connection_id: 'one', id: 'one:0:1', action: enter_action },
566
+ { client_id: 'a', connection_id: 'one', id: 'one:0:2', action: enter_action },
567
+ { client_id: 'b', connection_id: 'one', id: 'one:0:3', action: enter_action },
568
+ { client_id: 'b', connection_id: 'two', id: 'two:0:4', action: enter_action }
569
+ ]
570
+ end
571
+
572
+ it 'ensures uniqueness from this member_key (#RTP2a)' do
573
+ channel_anonymous_client.attach do
574
+ presence_anonymous_client.get do |members|
575
+ expect(members.length).to eql(0)
576
+
577
+ ## Fabricate members
578
+ action = Ably::Models::ProtocolMessage::ACTION.Presence
579
+ presence_msg = Ably::Models::ProtocolMessage.new(
580
+ action: action,
581
+ connection_serial: 20,
582
+ channel: channel_name,
583
+ presence: presence_data,
584
+ timestamp: Time.now.to_i * 1000
585
+ )
586
+ anonymous_client.connection.__incoming_protocol_msgbus__.publish :protocol_message, presence_msg
587
+
588
+ EventMachine.add_timer(0.5) do
589
+ presence_anonymous_client.get do |members|
590
+ expect(members.length).to eql(3)
591
+ expect(members.map { |member| member.client_id }.uniq).to contain_exactly('a', 'b')
592
+ stop_reactor
593
+ end
594
+ end
595
+ end
596
+ end
597
+ end
598
+ end
599
+
600
+ context 'newness is compared based on PresenceMessage#id unless synthesized' do
601
+ let(:page_size) { 100 }
602
+ let(:enter_expected_count) { page_size + 1 } # 100 per page, this ensures we have more than one page so that we can test newness during sync
603
+ let(:enter_action) { 2 }
604
+ let(:leave_action) { 3 }
605
+ let(:now) { Time.now.to_i * 1000 }
606
+ let(:entered) { [] }
607
+ let(:client_one) { auto_close Ably::Realtime::Client.new(default_options.merge(auth_callback: wildcard_token)) }
608
+
609
+ def setup_members_on(presence)
610
+ enter_expected_count.times do |indx|
611
+ # 10 messages per second max rate on simulation accounts
612
+ rate = indx.to_f / 10
613
+ EventMachine.add_timer(rate) do
614
+ presence.enter_client("client:#{indx}") do |message|
615
+ entered << message
616
+ next unless entered.count == enter_expected_count
617
+ yield
618
+ end
619
+ end
620
+ end
621
+ end
622
+
623
+ def allow_sync_fabricate_data_final_sync_and_assert_members
624
+ setup_members_on(presence_client_one) do
625
+ sync_pages_received = []
626
+ anonymous_client.connection.once(:connected) do
627
+ anonymous_client.connection.transport.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
628
+ if protocol_message.action == :sync
629
+ sync_pages_received << protocol_message
630
+ if sync_pages_received.count == 1
631
+ action = Ably::Models::ProtocolMessage::ACTION.Presence
632
+ presence_msg = Ably::Models::ProtocolMessage.new(
633
+ action: action,
634
+ connection_serial: anonymous_client.connection.serial + 1,
635
+ channel: channel_name,
636
+ presence: presence_data,
637
+ timestamp: Time.now.to_i * 1000
638
+ )
639
+ anonymous_client.connection.__incoming_protocol_msgbus__.publish :protocol_message, presence_msg
640
+
641
+ # Now simulate an end to the sync
642
+ action = Ably::Models::ProtocolMessage::ACTION.Sync
643
+ sync_msg = Ably::Models::ProtocolMessage.new(
644
+ action: action,
645
+ connection_serial: anonymous_client.connection.serial + 2,
646
+ channel: channel_name,
647
+ channel_serial: 'validserialprefix:', # with no part after the `:` this indicates the end to the SYNC
648
+ presence: [],
649
+ timestamp: Time.now.to_i * 1000
650
+ )
651
+ anonymous_client.connection.__incoming_protocol_msgbus__.publish :protocol_message, sync_msg
652
+
653
+ # Stop the next SYNC arriving
654
+ anonymous_client.connection.__incoming_protocol_msgbus__.unsubscribe
655
+ end
656
+ end
657
+ end
658
+
659
+ presence_anonymous_client.get do |members|
660
+ expect(members.length).to eql(page_size + 2)
661
+ expect(members.find { |member| member.client_id == 'a' }).to be_nil
662
+ expect(members.find { |member| member.client_id == 'b' }.timestamp.to_i).to eql(now / 1000)
663
+ expect(members.find { |member| member.client_id == 'c' }.timestamp.to_i).to eql(now / 1000)
664
+ stop_reactor
665
+ end
666
+ end
667
+ end
668
+ end
669
+
670
+ context 'when presence messages are synthesized' do
671
+ let(:presence_data) do
672
+ [
673
+ { client_id: 'a', connection_id: 'one', id: 'one:0:0', action: enter_action, timestamp: now }, # first messages from client, second fabricated
674
+ { client_id: 'a', connection_id: 'one', id: 'fabricated:0:1', action: leave_action, timestamp: now + 1 }, # leave after enter based on timestamp
675
+ { client_id: 'b', connection_id: 'one', id: 'one:0:2', action: enter_action, timestamp: now }, # first messages from client, second fabricated
676
+ { client_id: 'b', connection_id: 'one', id: 'fabricated:0:3', action: leave_action, timestamp: now - 1 }, # leave before enter based on timestamp
677
+ { client_id: 'c', connection_id: 'one', id: 'fabricated:0:4', action: enter_action, timestamp: now }, # both messages fabricated
678
+ { client_id: 'c', connection_id: 'one', id: 'fabricated:0:5', action: leave_action, timestamp: now - 1 } # leave before enter based on timestamp
679
+ ]
680
+ end
681
+
682
+ it 'compares based on timestamp if either has a connectionId not part of the presence message id (#RTP2b1)' do
683
+ allow_sync_fabricate_data_final_sync_and_assert_members
684
+ end
685
+ end
686
+
687
+ context 'when presence messages are not synthesized (events sent from clients)' do
688
+ let(:presence_data) do
689
+ [
690
+ { client_id: 'a', connection_id: 'one', id: 'one:0:0', action: enter_action, timestamp: now }, # first messages from client
691
+ { client_id: 'a', connection_id: 'one', id: 'one:1:0', action: leave_action, timestamp: now - 1 }, # leave after enter based on msgSerial part of ID
692
+ { client_id: 'b', connection_id: 'one', id: 'one:2:2', action: enter_action, timestamp: now }, # first messages from client
693
+ { client_id: 'b', connection_id: 'one', id: 'one:2:1', action: leave_action, timestamp: now + 1 }, # leave before enter based on index part of ID
694
+ { client_id: 'c', connection_id: 'one', id: 'one:4:4', action: enter_action, timestamp: now }, # first messages from client
695
+ { client_id: 'c', connection_id: 'one', id: 'one:3:5', action: leave_action, timestamp: now + 1 } # leave before enter based on msgSerial part of ID
696
+ ]
697
+ end
698
+
699
+ it 'compares based on timestamp if either has a connectionId not part of the presence message id (#RTP2b2)' do
700
+ allow_sync_fabricate_data_final_sync_and_assert_members
701
+ end
702
+ end
703
+ end
493
704
  end
494
705
 
495
- context '#sync_complete?' do
706
+ context '#sync_complete? and SYNC flags (#RTP1)' do
496
707
  context 'when attaching to a channel without any members present' do
497
- it 'is true and the presence channel is considered synced immediately' do
708
+ it 'sync_complete? is true, there is no presence flag, and the presence channel is considered synced immediately (#RTP1)' do
709
+ flag_checked = false
710
+
711
+ anonymous_client.connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
712
+ if protocol_message.action == :attached
713
+ flag_checked = true
714
+ expect(protocol_message.has_presence_flag?).to eql(false)
715
+ end
716
+ end
717
+
498
718
  channel_anonymous_client.attach do
499
719
  expect(channel_anonymous_client.presence).to be_sync_complete
500
- stop_reactor
720
+ EventMachine.next_tick do
721
+ expect(flag_checked).to eql(true)
722
+ stop_reactor
723
+ end
501
724
  end
502
725
  end
503
726
  end
504
727
 
505
728
  context 'when attaching to a channel with members present' do
506
- it 'is false and the presence channel will subsequently be synced' do
729
+ it 'sync_complete? is false, there is a presence flag, and the presence channel is subsequently synced (#RTP1)' do
730
+ flag_checked = false
731
+
732
+ anonymous_client.connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
733
+ if protocol_message.action == :attached
734
+ flag_checked = true
735
+ expect(protocol_message.has_presence_flag?).to eql(true)
736
+ end
737
+ end
738
+
507
739
  presence_client_one.enter
508
740
  presence_client_one.subscribe(:enter) do
509
741
  presence_client_one.unsubscribe :enter
510
742
 
511
743
  channel_anonymous_client.attach do
512
744
  expect(channel_anonymous_client.presence).to_not be_sync_complete
513
- channel_anonymous_client.presence.get(wait_for_sync: true) do
745
+ channel_anonymous_client.presence.get do
514
746
  expect(channel_anonymous_client.presence).to be_sync_complete
515
- stop_reactor
747
+ EventMachine.next_tick do
748
+ expect(flag_checked).to eql(true)
749
+ stop_reactor
750
+ end
516
751
  end
517
752
  end
518
753
  end
@@ -520,19 +755,20 @@ describe Ably::Realtime::Presence, :event_machine do
520
755
  end
521
756
  end
522
757
 
523
- context '250 existing (present) members on a channel (3 SYNC pages)' do
524
- context 'requires at least 3 SYNC ProtocolMessages', em_timeout: 30 do
525
- let(:enter_expected_count) { 250 }
758
+ context '101 existing (present) members on a channel (2 SYNC pages)' do
759
+ context 'requiring at least 2 SYNC ProtocolMessages', em_timeout: 40 do
760
+ let(:enter_expected_count) { 101 }
526
761
  let(:present) { [] }
527
762
  let(:entered) { [] }
528
763
  let(:sync_pages_received) { [] }
529
764
  let(:client_one) { auto_close Ably::Realtime::Client.new(client_options.merge(auth_callback: wildcard_token)) }
530
765
 
531
766
  def setup_members_on(presence)
532
- enter_expected_count.times do |index|
767
+ enter_expected_count.times do |indx|
533
768
  # 10 messages per second max rate on simulation accounts
534
- EventMachine.add_timer(index / 10) do
535
- presence.enter_client("client:#{index}") do |message|
769
+ rate = indx.to_f / 10
770
+ EventMachine.add_timer(rate) do
771
+ presence.enter_client("client:#{indx}") do |message|
536
772
  entered << message
537
773
  next unless entered.count == enter_expected_count
538
774
  yield
@@ -555,8 +791,47 @@ describe Ably::Realtime::Presence, :event_machine do
555
791
  end
556
792
  end
557
793
 
794
+ context 'and a member enters before the SYNC operation is complete' do
795
+ let(:enter_client_id) { random_str }
796
+
797
+ it 'emits a :enter immediately and the member is :present once the sync is complete (#RTP2g)' do
798
+ setup_members_on(presence_client_one) do
799
+ member_entered = false
800
+
801
+ anonymous_client.connect do
802
+ presence_anonymous_client.subscribe(:enter) do |member|
803
+ expect(member.client_id).to eql(enter_client_id)
804
+ member_entered = true
805
+ end
806
+
807
+ presence_anonymous_client.get do |members|
808
+ expect(members.find { |member| member.client_id == enter_client_id }.action).to eq(:present)
809
+ stop_reactor
810
+ end
811
+
812
+ anonymous_client.connection.transport.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
813
+ if protocol_message.action == :sync
814
+ sync_pages_received << protocol_message
815
+ if sync_pages_received.count == 1
816
+ enter_action = Ably::Models::PresenceMessage::ACTION.Enter
817
+ enter_member = Ably::Models::PresenceMessage.new(
818
+ 'id' => "#{client_one.connection.id}:#{random_str}:0",
819
+ 'clientId' => enter_client_id,
820
+ 'connectionId' => client_one.connection.id,
821
+ 'timestamp' => as_since_epoch(Time.now),
822
+ 'action' => enter_action
823
+ )
824
+ presence_anonymous_client.__incoming_msgbus__.publish :presence, enter_member
825
+ end
826
+ end
827
+ end
828
+ end
829
+ end
830
+ end
831
+ end
832
+
558
833
  context 'and a member leaves before the SYNC operation is complete' do
559
- it 'emits :leave immediately as the member leaves' do
834
+ it 'emits :leave immediately as the member leaves and cleans up the ABSENT member after (#RTP2f, #RTP2g)' do
560
835
  all_client_ids = enter_expected_count.times.map { |id| "client:#{id}" }
561
836
 
562
837
  setup_members_on(presence_client_one) do
@@ -570,7 +845,14 @@ describe Ably::Realtime::Presence, :event_machine do
570
845
  presence_anonymous_client.subscribe(:leave) do |leave_message|
571
846
  expect(leave_message.client_id).to eql(leave_member.client_id)
572
847
  expect(present.count).to be < enter_expected_count
573
- EventMachine.add_timer(1) do
848
+
849
+ # Hacky accessing a private method, but absent members are intentionally not exposed to any public APIs
850
+ expect(presence_anonymous_client.members.send(:absent_members).length).to eql(1)
851
+
852
+ presence_anonymous_client.members.once(:in_sync) do
853
+ # Check that members count is exact indicating the members with LEAVE action after sync are removed
854
+ expect(presence_anonymous_client).to be_sync_complete
855
+ expect(presence_anonymous_client.members.length).to eql(enter_expected_count - 1)
574
856
  presence_anonymous_client.unsubscribe
575
857
  stop_reactor
576
858
  end
@@ -583,7 +865,7 @@ describe Ably::Realtime::Presence, :event_machine do
583
865
  if sync_pages_received.count == 1
584
866
  leave_action = Ably::Models::PresenceMessage::ACTION.Leave
585
867
  leave_member = Ably::Models::PresenceMessage.new(
586
- 'id' => "#{client_one.connection.id}-#{all_client_ids.first}:0",
868
+ 'id' => "#{client_one.connection.id}:#{all_client_ids.first}:0",
587
869
  'clientId' => all_client_ids.first,
588
870
  'connectionId' => client_one.connection.id,
589
871
  'timestamp' => as_since_epoch(Time.now),
@@ -597,7 +879,7 @@ describe Ably::Realtime::Presence, :event_machine do
597
879
  end
598
880
  end
599
881
 
600
- it 'ignores presence events with timestamps prior to the current :present event in the MembersMap' do
882
+ it 'ignores presence events with timestamps / identifiers prior to the current :present event in the MembersMap (#RTP2c)' do
601
883
  started_at = Time.now
602
884
 
603
885
  setup_members_on(presence_client_one) do
@@ -605,11 +887,12 @@ describe Ably::Realtime::Presence, :event_machine do
605
887
 
606
888
  presence_anonymous_client.subscribe(:present) do |present_message|
607
889
  present << present_message
608
- leave_member = present_message unless leave_member
609
890
 
610
891
  if present.count == enter_expected_count
611
892
  presence_anonymous_client.get do |members|
612
- expect(members.find { |member| member.client_id == leave_member.client_id}.action).to eq(:present)
893
+ member = members.find { |member| member.client_id == leave_member.client_id}
894
+ expect(member).to_not be_nil
895
+ expect(member.action).to eq(:present)
613
896
  EventMachine.add_timer(1) do
614
897
  presence_anonymous_client.unsubscribe
615
898
  stop_reactor
@@ -619,7 +902,7 @@ describe Ably::Realtime::Presence, :event_machine do
619
902
  end
620
903
 
621
904
  presence_anonymous_client.subscribe(:leave) do |leave_message|
622
- raise 'Leave event should not have been fired because it is out of date'
905
+ raise "Leave event for #{leave_message} should not have been fired because it is out of date"
623
906
  end
624
907
 
625
908
  anonymous_client.connect do
@@ -627,10 +910,12 @@ describe Ably::Realtime::Presence, :event_machine do
627
910
  if protocol_message.action == :sync
628
911
  sync_pages_received << protocol_message
629
912
  if sync_pages_received.count == 1
913
+ first_member = protocol_message.presence[0] # get the first member in the SYNC set
630
914
  leave_action = Ably::Models::PresenceMessage::ACTION.Leave
631
915
  leave_member = Ably::Models::PresenceMessage.new(
632
- leave_member.as_json.merge('action' => leave_action, 'timestamp' => as_since_epoch(started_at))
916
+ first_member.as_json.merge('action' => leave_action, 'timestamp' => as_since_epoch(started_at))
633
917
  )
918
+ # After the SYNC has started, no inject that member has having left with a timestamp before the sync
634
919
  presence_anonymous_client.__incoming_msgbus__.publish :presence, leave_member
635
920
  end
636
921
  end
@@ -639,7 +924,7 @@ describe Ably::Realtime::Presence, :event_machine do
639
924
  end
640
925
  end
641
926
 
642
- it 'does not emit :present after the :leave event has been emitted, and that member is not included in the list of members via #get with :wait_for_sync' do
927
+ it 'does not emit :present after the :leave event has been emitted, and that member is not included in the list of members via #get (#RTP2f)' do
643
928
  left_client = 10
644
929
  left_client_id = "client:#{left_client}"
645
930
 
@@ -661,7 +946,7 @@ describe Ably::Realtime::Presence, :event_machine do
661
946
  member_left_emitted = true
662
947
  end
663
948
 
664
- presence_anonymous_client.get(wait_for_sync: true) do |members|
949
+ presence_anonymous_client.get do |members|
665
950
  expect(members.count).to eql(enter_expected_count - 1)
666
951
  expect(member_left_emitted).to eql(true)
667
952
  expect(members.map(&:client_id)).to_not include(left_client_id)
@@ -674,7 +959,7 @@ describe Ably::Realtime::Presence, :event_machine do
674
959
  channel_anonymous_client.attach do
675
960
  leave_action = Ably::Models::PresenceMessage::ACTION.Leave
676
961
  fake_leave_presence_message = Ably::Models::PresenceMessage.new(
677
- 'id' => "#{client_one.connection.id}-#{left_client_id}:0",
962
+ 'id' => "#{client_one.connection.id}:#{left_client_id}:0",
678
963
  'clientId' => left_client_id,
679
964
  'connectionId' => client_one.connection.id,
680
965
  'timestamp' => as_since_epoch(Time.now),
@@ -688,11 +973,11 @@ describe Ably::Realtime::Presence, :event_machine do
688
973
  end
689
974
 
690
975
  context '#get' do
691
- context 'with :wait_for_sync option set to true' do
692
- it 'waits until sync is complete', em_timeout: 30 do # allow for slow connections and lots of messages
693
- enter_expected_count.times do |index|
694
- EventMachine.add_timer(index / 10) do
695
- presence_client_one.enter_client("client:#{index}")
976
+ context 'by default' do
977
+ it 'waits until sync is complete (#RTP11c1)', em_timeout: 30 do # allow for slow connections and lots of messages
978
+ enter_expected_count.times do |indx|
979
+ EventMachine.add_timer(indx / 10) do
980
+ presence_client_one.enter_client "client:#{indx}"
696
981
  end
697
982
  end
698
983
 
@@ -700,7 +985,7 @@ describe Ably::Realtime::Presence, :event_machine do
700
985
  entered << message
701
986
  next unless entered.count == enter_expected_count
702
987
 
703
- presence_anonymous_client.get(wait_for_sync: true) do |members|
988
+ presence_anonymous_client.get do |members|
704
989
  expect(members.map(&:client_id).uniq.count).to eql(enter_expected_count)
705
990
  expect(members.count).to eql(enter_expected_count)
706
991
  stop_reactor
@@ -709,23 +994,22 @@ describe Ably::Realtime::Presence, :event_machine do
709
994
  end
710
995
  end
711
996
 
712
- context 'by default' do
997
+ context 'with :wait_for_sync option set to false (#RTP11c1)' do
713
998
  it 'it does not wait for sync', em_timeout: 30 do # allow for slow connections and lots of messages
714
999
  enter_expected_count.times do |indx|
715
1000
  EventMachine.add_timer(indx / 10) do
716
1001
  presence_client_one.enter_client "client:#{indx}"
717
- end
718
- end
719
-
720
- presence_client_one.subscribe(:enter) do |message|
721
- entered << message
722
- next unless entered.count == enter_expected_count
723
-
724
- channel_anonymous_client.attach do
725
- presence_anonymous_client.get do |members|
726
- expect(presence_anonymous_client.members).to_not be_in_sync
727
- expect(members.count).to eql(0)
728
- stop_reactor
1002
+ presence_client_one.subscribe(:enter) do |message|
1003
+ entered << message
1004
+ next unless entered.count == enter_expected_count
1005
+
1006
+ channel_anonymous_client.attach do
1007
+ presence_anonymous_client.get(wait_for_sync: false) do |members|
1008
+ expect(presence_anonymous_client.members).to_not be_in_sync
1009
+ expect(members.count).to eql(0)
1010
+ stop_reactor
1011
+ end
1012
+ end
729
1013
  end
730
1014
  end
731
1015
  end
@@ -937,9 +1221,16 @@ describe Ably::Realtime::Presence, :event_machine do
937
1221
  end
938
1222
  end
939
1223
 
940
- it 'raises an exception if not entered' do
941
- expect { channel_client_one.presence.leave }.to raise_error(Ably::Exceptions::Standard, /Unable to leave presence channel that is not entered/)
942
- stop_reactor
1224
+ it 'succeeds and does not emit an event (#RTP10d)' do
1225
+ channel_client_one.presence.leave do
1226
+ # allow enough time for leave event to (not) fire
1227
+ EventMachine.add_timer(2) do
1228
+ stop_reactor
1229
+ end
1230
+ end
1231
+ channel_client_one.subscribe(:leave) do
1232
+ raise "No leave event should fire"
1233
+ end
943
1234
  end
944
1235
 
945
1236
  it_should_behave_like 'a public presence method', :leave, :left, {}, enter_first: true
@@ -1219,28 +1510,75 @@ describe Ably::Realtime::Presence, :event_machine do
1219
1510
  end
1220
1511
 
1221
1512
  it 'catches exceptions in the provided method block' do
1222
- expect(presence_client_one.logger).to receive(:error).with(/Intentional exception/) do
1513
+ expect(presence_client_one.logger).to receive(:error) do |*args, &block|
1514
+ expect(args.concat([block ? block.call : nil]).join(',')).to match(/Intentional exception/)
1223
1515
  stop_reactor
1224
1516
  end
1225
1517
  presence_client_one.get { raise 'Intentional exception' }
1226
1518
  end
1227
1519
 
1228
- it 'raise an exception if the channel is detached' do
1520
+ it 'implicitly attaches the channel (#RTP11b)' do
1521
+ expect(channel_client_one).to be_initialized
1522
+ presence_client_one.get do |members|
1523
+ expect(channel_client_one).to be_attached
1524
+ stop_reactor
1525
+ end
1526
+ end
1527
+
1528
+ context 'when the channel is SUSPENDED' do
1529
+ context 'with wait_for_sync: true' do
1530
+ it 'results in an error with @code@ @91005@ and a @message@ stating that the presence state is out of sync (#RTP11d)' do
1531
+ presence_client_one.enter do
1532
+ channel_client_one.transition_state_machine! :suspended
1533
+ presence_client_one.get(wait_for_sync: true).errback do |error|
1534
+ expect(error.code).to eql(91005)
1535
+ expect(error.message).to match(/presence state is out of sync/i)
1536
+ stop_reactor
1537
+ end
1538
+ end
1539
+ end
1540
+ end
1541
+
1542
+ context 'with wait_for_sync: false' do
1543
+ it 'returns the current PresenceMap and does not wait for the channel to change to the ATTACHED state (#RTP11d)' do
1544
+ presence_client_one.enter do
1545
+ channel_client_one.transition_state_machine! :suspended
1546
+ presence_client_one.get(wait_for_sync: false) do |members|
1547
+ expect(channel_client_one).to be_suspended
1548
+ stop_reactor
1549
+ end
1550
+ end
1551
+ end
1552
+ end
1553
+ end
1554
+
1555
+ it 'fails if the connection is DETACHED (#RTP11b)' do
1229
1556
  channel_client_one.attach do
1230
- channel_client_one.transition_state_machine :detaching
1231
- channel_client_one.once(:detached) do
1232
- expect { presence_client_one.get }.to raise_error Ably::Exceptions::InvalidStateChange, /Operation is not allowed when channel is in STATE.detached/i
1233
- stop_reactor
1557
+ channel_client_one.detach do
1558
+ presence_client_one.get.tap do |deferrable|
1559
+ deferrable.callback { raise 'Get should not succeed' }
1560
+ deferrable.errback do |error|
1561
+ expect(error).to be_a(Ably::Exceptions::InvalidState)
1562
+ expect(error.message).to match(/Operation is not allowed when channel is in STATE.Detached/)
1563
+ stop_reactor
1564
+ end
1565
+ end
1234
1566
  end
1235
1567
  end
1236
1568
  end
1237
1569
 
1238
- it 'raise an exception if the channel is failed' do
1570
+ it 'fails if the connection is FAILED (#RTP11b)' do
1239
1571
  channel_client_one.attach do
1240
1572
  channel_client_one.transition_state_machine :failed
1241
1573
  expect(channel_client_one.state).to eq(:failed)
1242
- expect { presence_client_one.get }.to raise_error Ably::Exceptions::InvalidStateChange, /Operation is not allowed when channel is in STATE.failed/i
1243
- stop_reactor
1574
+ presence_client_one.get.tap do |deferrable|
1575
+ deferrable.callback { raise 'Get should not succeed' }
1576
+ deferrable.errback do |error|
1577
+ expect(error).to be_a(Ably::Exceptions::InvalidState)
1578
+ expect(error.message).to match(/Operation is not allowed when channel is in STATE.Failed/)
1579
+ stop_reactor
1580
+ end
1581
+ end
1244
1582
  end
1245
1583
  end
1246
1584
 
@@ -1252,11 +1590,11 @@ describe Ably::Realtime::Presence, :event_machine do
1252
1590
  let(:client_options) { default_options.merge(log_level: :none) }
1253
1591
 
1254
1592
  def connect_members_deferrables
1255
- (members_per_page * pages + 1).times.map do |index|
1593
+ (members_per_page * pages + 1).times.map do |mem_index|
1256
1594
  # rate limit to 10 per second
1257
1595
  EventMachine::DefaultDeferrable.new.tap do |deferrable|
1258
- EventMachine.add_timer(index / 10) do
1259
- presence_client_one.enter_client("client:#{index}").tap do |enter_deferrable|
1596
+ EventMachine.add_timer(mem_index/10) do
1597
+ presence_client_one.enter_client("client:#{mem_index}").tap do |enter_deferrable|
1260
1598
  enter_deferrable.callback { |*args| deferrable.succeed *args }
1261
1599
  enter_deferrable.errback { |*args| deferrable.fail *args }
1262
1600
  end
@@ -1266,7 +1604,7 @@ describe Ably::Realtime::Presence, :event_machine do
1266
1604
  end
1267
1605
 
1268
1606
  context 'when :wait_for_sync is true' do
1269
- it 'fails if the connection fails' do
1607
+ it 'fails if the connection becomes FAILED (#RTP11b)' do
1270
1608
  when_all(*connect_members_deferrables) do
1271
1609
  channel_client_two.attach do
1272
1610
  client_two.connection.transport.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
@@ -1289,7 +1627,7 @@ describe Ably::Realtime::Presence, :event_machine do
1289
1627
  end
1290
1628
  end
1291
1629
 
1292
- it 'fails if the channel is detached' do
1630
+ it 'fails if the channel becomes detached (#RTP11b)' do
1293
1631
  when_all(*connect_members_deferrables) do
1294
1632
  channel_client_two.attach do
1295
1633
  client_two.connection.transport.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
@@ -1313,9 +1651,7 @@ describe Ably::Realtime::Presence, :event_machine do
1313
1651
  end
1314
1652
  end
1315
1653
 
1316
- # skip 'it fails if the connection changes to failed state'
1317
-
1318
- it 'returns the current members on the channel' do
1654
+ it 'returns the current members on the channel (#RTP11a)' do
1319
1655
  presence_client_one.enter
1320
1656
  presence_client_one.subscribe(:enter) do
1321
1657
  presence_client_one.unsubscribe :enter
@@ -1332,7 +1668,7 @@ describe Ably::Realtime::Presence, :event_machine do
1332
1668
  end
1333
1669
  end
1334
1670
 
1335
- it 'filters by connection_id option if provided' do
1671
+ it 'filters by connection_id option if provided (#RTP11c3)' do
1336
1672
  presence_client_one.enter do
1337
1673
  presence_client_two.enter
1338
1674
  end
@@ -1354,7 +1690,7 @@ describe Ably::Realtime::Presence, :event_machine do
1354
1690
  end
1355
1691
  end
1356
1692
 
1357
- it 'filters by client_id option if provided' do
1693
+ it 'filters by client_id option if provided (#RTP11c2)' do
1358
1694
  presence_client_one.enter do
1359
1695
  presence_client_two.enter
1360
1696
  end
@@ -1378,7 +1714,7 @@ describe Ably::Realtime::Presence, :event_machine do
1378
1714
  end
1379
1715
  end
1380
1716
 
1381
- it 'does not wait for SYNC to complete if :wait_for_sync option is false' do
1717
+ it 'does not wait for SYNC to complete if :wait_for_sync option is false (#RTP11c1)' do
1382
1718
  presence_client_one.enter
1383
1719
  presence_client_one.subscribe(:enter) do
1384
1720
  presence_client_one.unsubscribe :enter
@@ -1390,6 +1726,18 @@ describe Ably::Realtime::Presence, :event_machine do
1390
1726
  end
1391
1727
  end
1392
1728
 
1729
+ it 'returns the list of members and waits for SYNC to complete by default (#RTP11a)' do
1730
+ presence_client_one.enter
1731
+ presence_client_one.subscribe(:enter) do
1732
+ presence_client_one.unsubscribe :enter
1733
+
1734
+ presence_client_two.get do |members|
1735
+ expect(members.count).to eql(1)
1736
+ stop_reactor
1737
+ end
1738
+ end
1739
+ end
1740
+
1393
1741
  context 'when a member enters and then leaves' do
1394
1742
  it 'has no members' do
1395
1743
  presence_client_one.enter do
@@ -1405,6 +1753,21 @@ describe Ably::Realtime::Presence, :event_machine do
1405
1753
  end
1406
1754
  end
1407
1755
 
1756
+ context 'when a member enters and the presence map is updated' do
1757
+ it 'adds the member as being :present (#RTP2d)' do
1758
+ presence_client_one.enter
1759
+ presence_client_one.subscribe(:enter) do
1760
+ presence_client_one.unsubscribe :enter
1761
+
1762
+ presence_client_one.get do |members|
1763
+ expect(members.count).to eq(1)
1764
+ expect(members.first.action).to eq(:present)
1765
+ stop_reactor
1766
+ end
1767
+ end
1768
+ end
1769
+ end
1770
+
1408
1771
  context 'with lots of members on different clients' do
1409
1772
  let(:client_one) { auto_close Ably::Realtime::Client.new(client_options.merge(auth_callback: wildcard_token)) }
1410
1773
  let(:client_two) { auto_close Ably::Realtime::Client.new(client_options.merge(auth_callback: wildcard_token)) }
@@ -1413,9 +1776,9 @@ describe Ably::Realtime::Presence, :event_machine do
1413
1776
  let(:total_members) { members_per_client * 2 }
1414
1777
 
1415
1778
  it 'returns a complete list of members on all clients' do
1416
- members_per_client.times do |index|
1417
- presence_client_one.enter_client("client_1:#{index}")
1418
- presence_client_two.enter_client("client_2:#{index}")
1779
+ members_per_client.times do |indx|
1780
+ presence_client_one.enter_client("client_1:#{indx}")
1781
+ presence_client_two.enter_client("client_2:#{indx}")
1419
1782
  end
1420
1783
 
1421
1784
  presence_client_one.subscribe(:enter) do
@@ -1502,7 +1865,9 @@ describe Ably::Realtime::Presence, :event_machine do
1502
1865
 
1503
1866
  it 'logs the error and continues' do
1504
1867
  emitted_exception = false
1505
- expect(client_one.logger).to receive(:error).with(/#{exception.message}/)
1868
+ expect(client_one.logger).to receive(:error) do |*args, &block|
1869
+ expect(args.concat([block ? block.call : nil]).join(',')).to match(/#{exception.message}/)
1870
+ end
1506
1871
  presence_client_one.subscribe do |presence_message|
1507
1872
  emitted_exception = true
1508
1873
  raise exception
@@ -1737,9 +2102,8 @@ describe Ably::Realtime::Presence, :event_machine do
1737
2102
 
1738
2103
  it 'emits an error when cipher does not match and presence data cannot be decoded' do
1739
2104
  incompatible_encrypted_channel.attach do
1740
- incompatible_encrypted_channel.on(:error) do |error|
1741
- expect(error).to be_a(Ably::Exceptions::CipherError)
1742
- expect(error.message).to match(/Cipher algorithm AES-128-CBC does not match/)
2105
+ expect(client_two.logger).to receive(:error) do |*args, &block|
2106
+ expect(args.concat([block ? block.call : nil]).join(',')).to match(/Cipher algorithm AES-128-CBC does not match/)
1743
2107
  stop_reactor
1744
2108
  end
1745
2109
 
@@ -1777,13 +2141,13 @@ describe Ably::Realtime::Presence, :event_machine do
1777
2141
  end
1778
2142
 
1779
2143
  context 'connection failure mid-way through a large member sync' do
1780
- let(:members_count) { 250 }
2144
+ let(:members_count) { 201 }
1781
2145
  let(:sync_pages_received) { [] }
1782
2146
  let(:client_options) { default_options.merge(log_level: :fatal) }
1783
2147
 
1784
- it 'resumes the SYNC operation', em_timeout: 15 do
1785
- when_all(*members_count.times.map do |index|
1786
- presence_anonymous_client.enter_client("client:#{index}")
2148
+ it 'resumes the SYNC operation (#RTP3)', em_timeout: 15 do
2149
+ when_all(*members_count.times.map do |indx|
2150
+ presence_anonymous_client.enter_client("client:#{indx}")
1787
2151
  end) do
1788
2152
  channel_client_two.attach do
1789
2153
  client_two.connection.transport.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
@@ -1802,5 +2166,655 @@ describe Ably::Realtime::Presence, :event_machine do
1802
2166
  end
1803
2167
  end
1804
2168
  end
2169
+
2170
+ context 'server-initiated sync' do
2171
+ context 'with multiple SYNC pages' do
2172
+ let(:present_action) { 1 }
2173
+ let(:leave_action) { 3 }
2174
+ let(:presence_sync_1) do
2175
+ [
2176
+ { client_id: 'a', connection_id: 'one', id: 'one:0:0', action: present_action },
2177
+ { client_id: 'b', connection_id: 'one', id: 'one:0:1', action: present_action }
2178
+ ]
2179
+ end
2180
+ let(:presence_sync_2) do
2181
+ [
2182
+ { client_id: 'a', connection_id: 'one', id: 'one:1:0', action: leave_action }
2183
+ ]
2184
+ end
2185
+
2186
+ it 'is initiated with a SYNC message and completed with a later SYNC message with no cursor value part of the channelSerial (#RTP18a, #RTP18b) ', em_timeout: 15 do
2187
+ presence_anonymous_client.get do |members|
2188
+ expect(members.length).to eql(0)
2189
+ expect(presence_anonymous_client).to be_sync_complete
2190
+
2191
+ presence_anonymous_client.subscribe(:present) do
2192
+ expect(presence_anonymous_client).to_not be_sync_complete
2193
+ presence_anonymous_client.get do |members|
2194
+ expect(presence_anonymous_client).to be_sync_complete
2195
+ expect(members.length).to eql(1)
2196
+ expect(members.first.client_id).to eql('b')
2197
+ stop_reactor
2198
+ end
2199
+ end
2200
+
2201
+ ## Fabricate server-initiated SYNC in two parts
2202
+ action = Ably::Models::ProtocolMessage::ACTION.Sync
2203
+ sync_message = Ably::Models::ProtocolMessage.new(
2204
+ action: action,
2205
+ connection_serial: 10,
2206
+ channel_serial: 'sequenceid:cursor',
2207
+ channel: channel_name,
2208
+ presence: presence_sync_1,
2209
+ timestamp: Time.now.to_i * 1000
2210
+ )
2211
+ anonymous_client.connection.__incoming_protocol_msgbus__.publish :protocol_message, sync_message
2212
+
2213
+ sync_message = Ably::Models::ProtocolMessage.new(
2214
+ action: action,
2215
+ connection_serial: 11,
2216
+ channel_serial: 'sequenceid:', # indicates SYNC is complete
2217
+ channel: channel_name,
2218
+ presence: presence_sync_2,
2219
+ timestamp: Time.now.to_i * 1000
2220
+ )
2221
+ anonymous_client.connection.__incoming_protocol_msgbus__.publish :protocol_message, sync_message
2222
+ end
2223
+ end
2224
+ end
2225
+
2226
+ context 'with a single SYNC page' do
2227
+ let(:present_action) { 1 }
2228
+ let(:leave_action) { 3 }
2229
+ let(:presence_sync) do
2230
+ [
2231
+ { client_id: 'a', connection_id: 'one', id: 'one:0:0', action: present_action },
2232
+ { client_id: 'b', connection_id: 'one', id: 'one:0:1', action: present_action },
2233
+ { client_id: 'a', connection_id: 'one', id: 'one:1:0', action: leave_action }
2234
+ ]
2235
+ end
2236
+
2237
+ it 'is initiated and completed with a single SYNC message (and no channelSerial) (#RTP18a, #RTP18c) ', em_timeout: 15 do
2238
+ presence_anonymous_client.get do |members|
2239
+ expect(members.length).to eql(0)
2240
+ expect(presence_anonymous_client).to be_sync_complete
2241
+
2242
+ presence_anonymous_client.subscribe(:present) do
2243
+ expect(presence_anonymous_client).to_not be_sync_complete
2244
+ presence_anonymous_client.get do |members|
2245
+ expect(presence_anonymous_client).to be_sync_complete
2246
+ expect(members.length).to eql(1)
2247
+ expect(members.first.client_id).to eql('b')
2248
+ stop_reactor
2249
+ end
2250
+ end
2251
+
2252
+ ## Fabricate server-initiated SYNC in two parts
2253
+ action = Ably::Models::ProtocolMessage::ACTION.Sync
2254
+ sync_message = Ably::Models::ProtocolMessage.new(
2255
+ action: action,
2256
+ connection_serial: 10,
2257
+ channel: channel_name,
2258
+ presence: presence_sync,
2259
+ timestamp: Time.now.to_i * 1000
2260
+ )
2261
+ anonymous_client.connection.__incoming_protocol_msgbus__.publish :protocol_message, sync_message
2262
+ end
2263
+ end
2264
+ end
2265
+
2266
+ context 'when members exist in the PresenceMap before a SYNC completes' do
2267
+ let(:enter_action) { Ably::Models::PresenceMessage::ACTION.Enter.to_i }
2268
+ let(:present_action) { Ably::Models::PresenceMessage::ACTION.Present.to_i }
2269
+ let(:presence_sync_protocol_message) do
2270
+ [
2271
+ { client_id: 'a', connection_id: 'one', id: 'one:0:0', action: present_action },
2272
+ { client_id: 'b', connection_id: 'one', id: 'one:0:1', action: present_action }
2273
+ ]
2274
+ end
2275
+ let(:presence_enter_message) do
2276
+ Ably::Models::PresenceMessage.new(
2277
+ 'id' => "#{random_str}:#{random_str}:0",
2278
+ 'clientId' => random_str,
2279
+ 'connectionId' => random_str,
2280
+ 'timestamp' => as_since_epoch(Time.now),
2281
+ 'action' => enter_action
2282
+ )
2283
+ end
2284
+
2285
+ it 'removes the members that are no longer present (#RTP19)', em_timeout: 15 do
2286
+ presence_anonymous_client.get do |members|
2287
+ expect(members.length).to eql(0)
2288
+
2289
+ # Now inject a fake member into the PresenceMap by faking the receive of a Presence message from Ably into the Presence object
2290
+ presence_anonymous_client.__incoming_msgbus__.publish :presence, presence_enter_message
2291
+
2292
+ EventMachine.next_tick do
2293
+ presence_anonymous_client.get do |members|
2294
+ expect(members.length).to eql(1)
2295
+ expect(members.first.client_id).to eql(presence_enter_message.client_id)
2296
+
2297
+ presence_events = []
2298
+ presence_anonymous_client.subscribe do |presence_message|
2299
+ presence_events << [presence_message.client_id, presence_message.action.to_sym]
2300
+ if presence_message.action == :leave
2301
+ expect(presence_message.id).to be_nil
2302
+ expect(presence_message.timestamp.to_f * 1000).to be_within(20).of(Time.now.to_f * 1000)
2303
+ end
2304
+ end
2305
+
2306
+ ## Fabricate server-initiated SYNC in two parts
2307
+ action = Ably::Models::ProtocolMessage::ACTION.Sync
2308
+ sync_message = Ably::Models::ProtocolMessage.new(
2309
+ action: action,
2310
+ connection_serial: 10,
2311
+ channel: channel_name,
2312
+ presence: presence_sync_protocol_message,
2313
+ timestamp: Time.now.to_i * 1000
2314
+ )
2315
+ anonymous_client.connection.__incoming_protocol_msgbus__.publish :protocol_message, sync_message
2316
+
2317
+ EventMachine.next_tick do
2318
+ presence_anonymous_client.get do |members|
2319
+ expect(members.length).to eql(2)
2320
+ expect(members.find { |member| member.client_id == presence_enter_message.client_id}).to be_nil
2321
+ expect(presence_events).to contain_exactly(
2322
+ ['a', :present],
2323
+ ['b', :present],
2324
+ [presence_enter_message.client_id, :leave]
2325
+ )
2326
+ stop_reactor
2327
+ end
2328
+ end
2329
+ end
2330
+ end
2331
+ end
2332
+ end
2333
+ end
2334
+ end
2335
+
2336
+ context 'when the client does not have presence subscribe privileges but is present on the channel' do
2337
+ let(:present_only_capability) do
2338
+ { channel_name => ["presence"] }
2339
+ end
2340
+ let(:present_only_callback) { Proc.new { Ably::Rest::Client.new(client_options).auth.request_token(client_id: '*', capability: present_only_capability) } }
2341
+ let(:client_one) { auto_close Ably::Realtime::Client.new(client_options.merge(auth_callback: present_only_callback)) }
2342
+
2343
+ it 'receives presence updates for all presence events generated by the current connection and the presence map is kept up to date (#RTP17a)' do
2344
+ skip 'This functionality is not yet in sandbox, see https://github.com/ably/realtime/issues/656'
2345
+
2346
+ enter_client_ids = []
2347
+ presence_client_one.subscribe(:enter) do |presence_message|
2348
+ enter_client_ids << presence_message.client_id
2349
+ end
2350
+
2351
+ leave_client_ids = []
2352
+ presence_client_one.subscribe(:leave) do
2353
+ leave_client_ids << presence_message.client_id
2354
+ end
2355
+
2356
+ presence_client_one.enter_client 'bob' do
2357
+ presence_client_one.enter_client 'sarah'
2358
+ end
2359
+
2360
+ entered_count = 0
2361
+ presence_client_one.subscribe(:enter) do
2362
+ entered_count += 1
2363
+ next unless entered_count == 2
2364
+
2365
+ presence_client_one.unsubscribe :enter
2366
+ presence_client_one.get do |members|
2367
+ EventMachine.add_timer(1) do
2368
+ expect(members.map(&:client_id)).to contain_exactly('bob', 'sarah')
2369
+ expect(enter_client_ids).to contain_exactly('bob', 'sarah')
2370
+
2371
+ presence_client_one.leave_client 'bob' do
2372
+ presence_client_one.leave_client 'sarah'
2373
+ end
2374
+
2375
+ leave_count = 0
2376
+ presence_client_one.subscribe(:leave) do
2377
+ leave_count += 1
2378
+ next unless leave_count == 2
2379
+
2380
+ presence_client_one.get do |members|
2381
+ expect(members.length).to eql(0)
2382
+ expect(leave_client_ids).to contain_exactly('bob', 'sarah')
2383
+ stop_reactor
2384
+ end
2385
+ end
2386
+ end
2387
+ end
2388
+ end
2389
+ end
2390
+ end
2391
+
2392
+ context "local PresenceMap for presence members entered by this client" do
2393
+ it "maintains a copy of the member map for any member that shares this connection's connection ID (#RTP17)" do
2394
+ presence_client_one.enter do
2395
+ presence_client_two.enter
2396
+ end
2397
+
2398
+ entered_count = 0
2399
+ presence_client_one.subscribe(:enter) do
2400
+ entered_count += 1
2401
+ next unless entered_count == 2
2402
+ channel_anonymous_client.attach do
2403
+ channel_anonymous_client.presence.get do |members|
2404
+ expect(channel_anonymous_client.presence.members.local_members).to be_empty
2405
+ expect(presence_client_one.members.local_members.length).to eql(1)
2406
+ expect(presence_client_one.members.local_members.values.first.connection_id).to eql(client_one.connection.id)
2407
+ expect(presence_client_two.members.local_members.values.first.connection_id).to eql(client_two.connection.id)
2408
+ presence_client_two.leave
2409
+ presence_client_two.subscribe(:leave) do
2410
+ expect(presence_client_two.members.local_members).to be_empty
2411
+ stop_reactor
2412
+ end
2413
+ end
2414
+ end
2415
+ end
2416
+ end
2417
+
2418
+ context 'when a channel becomes attached again' do
2419
+ let(:attached_action) { Ably::Models::ProtocolMessage::ACTION.Attached.to_i }
2420
+ let(:sync_action) { Ably::Models::ProtocolMessage::ACTION.Sync.to_i }
2421
+ let(:presence_action) { Ably::Models::ProtocolMessage::ACTION.Presence.to_i }
2422
+ let(:present_action) { Ably::Models::PresenceMessage::ACTION.Present.to_i }
2423
+ let(:resume_flag) { 4 }
2424
+ let(:presence_flag) { 1 }
2425
+
2426
+ def fabricate_incoming_protocol_message(protocol_message)
2427
+ client_one.connection.__incoming_protocol_msgbus__.publish :protocol_message, protocol_message
2428
+ end
2429
+
2430
+ # Prevents any messages from the WebSocket transport being sent / received
2431
+ # Connection protocol message subscriptions are still active, but nothing reaches or comes from the WebSocket transport
2432
+ def cripple_websocket_transport
2433
+ client_one.connection.transport.__incoming_protocol_msgbus__.unsubscribe
2434
+ client_one.connection.transport.__outgoing_protocol_msgbus__.unsubscribe
2435
+ end
2436
+
2437
+ context 'and the resume flag is true' do
2438
+ context 'and the presence flag is false' do
2439
+ it 'does not send any presence events as the PresenceMap is in sync (#RTP5c1)' do
2440
+ presence_client_one.enter
2441
+ presence_client_one.subscribe(:enter) do
2442
+ presence_client_one.unsubscribe :enter
2443
+
2444
+ client_one.connection.transport.__outgoing_protocol_msgbus__.subscribe do |message|
2445
+ raise "No presence state updates to Ably are expected. Message sent: #{message.to_json}" if client_one.connection.connected?
2446
+ end
2447
+
2448
+ cripple_websocket_transport
2449
+
2450
+ fabricate_incoming_protocol_message Ably::Models::ProtocolMessage.new(
2451
+ action: attached_action,
2452
+ channel: channel_name,
2453
+ flags: resume_flag
2454
+ )
2455
+
2456
+ EventMachine.add_timer(1) do
2457
+ presence_client_one.get do |members|
2458
+ expect(members.length).to eql(1)
2459
+ expect(presence_client_one.members.local_members.length).to eql(1)
2460
+ stop_reactor
2461
+ end
2462
+ end
2463
+ end
2464
+ end
2465
+ end
2466
+
2467
+ context 'and the presence flag is true' do
2468
+ context 'and following the SYNC all local MemberMap members are present in the PresenceMap' do
2469
+ it 'does nothing as MemberMap is in sync (#RTP5c2)' do
2470
+ presence_client_one.enter
2471
+ presence_client_one.subscribe(:enter) do
2472
+ presence_client_one.unsubscribe :enter
2473
+
2474
+ expect(presence_client_one.members.length).to eql(1)
2475
+ expect(presence_client_one.members.local_members.length).to eql(1)
2476
+
2477
+ presence_client_one.members.once(:in_sync) do
2478
+ presence_client_one.get do |members|
2479
+ expect(members.length).to eql(1)
2480
+ expect(presence_client_one.members.local_members.length).to eql(1)
2481
+ stop_reactor
2482
+ end
2483
+ end
2484
+
2485
+ client_one.connection.transport.__outgoing_protocol_msgbus__.subscribe do |message|
2486
+ raise "No presence state updates to Ably are expected. Message sent: #{message.to_json}" if client_one.connection.connected?
2487
+ end
2488
+
2489
+ cripple_websocket_transport
2490
+
2491
+ fabricate_incoming_protocol_message Ably::Models::ProtocolMessage.new(
2492
+ action: attached_action,
2493
+ channel: channel_name,
2494
+ flags: resume_flag + presence_flag
2495
+ )
2496
+
2497
+ fabricate_incoming_protocol_message Ably::Models::ProtocolMessage.new(
2498
+ action: sync_action,
2499
+ channel: channel_name,
2500
+ presence: presence_client_one.members.map(&:shallow_clone).map(&:as_json),
2501
+ channelSerial: nil # no further SYNC messages expected
2502
+ )
2503
+ end
2504
+ end
2505
+ end
2506
+
2507
+ context 'and following the SYNC a local MemberMap member is not present in the PresenceMap' do
2508
+ it 're-enters the missing members automatically (#RTP5c2)' do
2509
+ sync_check_completed = false
2510
+
2511
+ presence_client_one.enter
2512
+ presence_client_one.subscribe(:enter) do
2513
+ presence_client_one.unsubscribe :enter
2514
+
2515
+ expect(presence_client_one.members.length).to eql(1)
2516
+ expect(presence_client_one.members.local_members.length).to eql(1)
2517
+
2518
+ client_one.connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |message|
2519
+ next if message.action == :close # ignore finalization of connection
2520
+
2521
+ expect(message.action).to eq(:presence)
2522
+ presence_message = message.presence.first
2523
+ expect(presence_message.action).to eq(:enter)
2524
+ expect(presence_message.client_id).to eq(client_one.auth.client_id)
2525
+
2526
+ presence_client_one.subscribe(:enter) do |message|
2527
+ expect(message.connection_id).to eql(client_one.connection.id)
2528
+ expect(message.client_id).to eq(client_one.auth.client_id)
2529
+
2530
+ EventMachine.next_tick do
2531
+ expect(presence_client_one.members.length).to eql(2)
2532
+ expect(presence_client_one.members.local_members.length).to eql(1)
2533
+ expect(sync_check_completed).to be_truthy
2534
+ stop_reactor
2535
+ end
2536
+ end
2537
+
2538
+ # Fabricate Ably sending back the Enter PresenceMessage to the client a short while after
2539
+ # ensuring the PresenceMap for a short period does not have this member as to be expected in reality
2540
+ EventMachine.add_timer(0.2) do
2541
+ connection_id = random_str
2542
+ fabricate_incoming_protocol_message Ably::Models::ProtocolMessage.new(
2543
+ action: presence_action,
2544
+ channel: channel_name,
2545
+ connectionId: client_one.connection.id,
2546
+ connectionSerial: 50,
2547
+ timestamp: as_since_epoch(Time.now),
2548
+ presence: [presence_message.shallow_clone(id: "#{client_one.connection.id}:0:0", timestamp: as_since_epoch(Time.now)).as_json]
2549
+ )
2550
+ end
2551
+ end
2552
+
2553
+ presence_client_one.members.once(:in_sync) do
2554
+ # For a brief period, the client will have re-entered the missing members from the local_members
2555
+ # but the enter from Ably will have not been received, so at this point the local_members will be empty
2556
+ presence_client_one.get do |members|
2557
+ expect(members.length).to eql(1)
2558
+ expect(members.first.connection_id).to_not eql(client_one.connection.id)
2559
+ expect(presence_client_one.members.local_members.length).to eql(0)
2560
+ sync_check_completed = true
2561
+ end
2562
+ end
2563
+
2564
+ cripple_websocket_transport
2565
+
2566
+ fabricate_incoming_protocol_message Ably::Models::ProtocolMessage.new(
2567
+ action: attached_action,
2568
+ channel: channel_name,
2569
+ flags: resume_flag + presence_flag
2570
+ )
2571
+
2572
+ # Complete the SYNC but without the member who was entered by this client
2573
+ connection_id = random_str
2574
+ fabricate_incoming_protocol_message Ably::Models::ProtocolMessage.new(
2575
+ action: sync_action,
2576
+ channel: channel_name,
2577
+ timestamp: as_since_epoch(Time.now),
2578
+ presence: [{ id: "#{connection_id}:0:0", action: present_action, connection_id: connection_id, client_id: random_str }],
2579
+ chanenlSerial: nil # no further SYNC messages expected
2580
+ )
2581
+ end
2582
+ end
2583
+ end
2584
+ end
2585
+ end
2586
+
2587
+ context 'and the resume flag is false' do
2588
+ context 'and the presence flag is false' do
2589
+ let(:member_data) { random_str }
2590
+
2591
+ it 'immediately resends all local presence members (#RTP5c2, #RTP19a)' do
2592
+ in_sync_confirmed_no_local_members = false
2593
+ local_member_leave_event_fired = false
2594
+
2595
+ presence_client_one.enter(member_data)
2596
+ presence_client_one.subscribe(:enter) do
2597
+ presence_client_one.unsubscribe :enter
2598
+
2599
+ presence_client_one.subscribe(:leave) do |message|
2600
+ # The local member will leave the PresenceMap due to the ATTACHED without Presence
2601
+ local_member_leave_event_fired = true
2602
+ end
2603
+
2604
+ # Local members re-entered automatically appear as updates due to the
2605
+ # fabricated ATTACHED message sent and the members already being present
2606
+ presence_client_one.subscribe(:update) do |message|
2607
+ expect(local_member_leave_event_fired).to be_truthy
2608
+ expect(message.data).to eq(member_data)
2609
+ expect(message.client_id).to eq(client_one.auth.client_id)
2610
+ EventMachine.next_tick do
2611
+ expect(presence_client_one.members.length).to eql(1)
2612
+ expect(presence_client_one.members.local_members.length).to eql(1)
2613
+ expect(in_sync_confirmed_no_local_members).to be_truthy
2614
+ stop_reactor
2615
+ end
2616
+ end
2617
+
2618
+ presence_client_one.members.once(:in_sync) do
2619
+ # Immediately after SYNC (no sync actually occurred, but this event fires immediately after a channel SYNCs or is not expecting to SYNC)
2620
+ expect(presence_client_one.members.length).to eql(0)
2621
+ expect(presence_client_one.members.local_members.length).to eql(0)
2622
+ in_sync_confirmed_no_local_members = true
2623
+ end
2624
+
2625
+ # ATTACHED ProtocolMessage with no presence flag will clear the presence set immediately, #RTP19a
2626
+ fabricate_incoming_protocol_message Ably::Models::ProtocolMessage.new(
2627
+ action: attached_action,
2628
+ channel: channel_name,
2629
+ flags: 0 # no resume or presence flag
2630
+ )
2631
+ end
2632
+ end
2633
+ end
2634
+ end
2635
+
2636
+ context 'when re-entering a client automatically, if the re-enter fails for any reason' do
2637
+ let(:client_one_options) do
2638
+ client_options.merge(client_id: client_one_id, log_level: :error)
2639
+ end
2640
+ let(:client_one) { auto_close Ably::Realtime::Client.new(client_one_options) }
2641
+
2642
+ it 'should emit an ErrorInfo with error code 91004 (#RTP5c3)' do
2643
+ presence_client_one.enter
2644
+
2645
+ # Wait for client to be entered
2646
+ presence_client_one.subscribe(:enter) do
2647
+ # Local member should not be re-entered as the request to re-enter will timeout
2648
+ presence_client_one.subscribe(:update) do |message|
2649
+ raise "Unexpected update, this should not happen as the re-enter fails"
2650
+ end
2651
+
2652
+ channel_client_one.on(:update) do |channel_state_change|
2653
+ next if channel_state_change.reason.nil? # first update is generated by the fabricated ATTACHED
2654
+
2655
+ expect(channel_state_change.reason.code).to eql(91004)
2656
+ expect(channel_state_change.reason.message).to match(/#{client_one_id}/)
2657
+ expect(channel_state_change.reason.message).to match(/Fabricated/) # fabricated message
2658
+ expect(channel_state_change.reason.message).to match(/2345/) # fabricated error code
2659
+ stop_reactor
2660
+ end
2661
+
2662
+ cripple_websocket_transport
2663
+
2664
+ client_one.connection.__outgoing_protocol_msgbus__.subscribe do |protocol_message|
2665
+ if protocol_message.action == :presence
2666
+ # Fabricate a NACK for the re-enter message
2667
+ EventMachine.add_timer(0.1) do
2668
+ fabricate_incoming_protocol_message Ably::Models::ProtocolMessage.new(
2669
+ action: Ably::Models::ProtocolMessage::ACTION.Nack.to_i ,
2670
+ channel: channel_name,
2671
+ count: 1,
2672
+ msg_serial: protocol_message.message_serial,
2673
+ error: {
2674
+ message: 'Fabricated',
2675
+ code: 2345
2676
+ }
2677
+ )
2678
+ end
2679
+ end
2680
+ end
2681
+
2682
+ # ATTACHED ProtocolMessage with no presence flag will clear the presence set immediately, #RTP19a
2683
+ fabricate_incoming_protocol_message Ably::Models::ProtocolMessage.new(
2684
+ action: attached_action,
2685
+ channel: channel_name,
2686
+ flags: 0 # no resume or presence flag
2687
+ )
2688
+ end
2689
+ end
2690
+ end
2691
+ end
2692
+ end
2693
+
2694
+ context 'channel state side effects' do
2695
+ context 'channel transitions to the FAILED state' do
2696
+ let(:anonymous_client) { auto_close Ably::Realtime::Client.new(client_options.merge(log_level: :fatal)) }
2697
+ let(:client_one) { auto_close Ably::Realtime::Client.new(client_options.merge(client_id: client_one_id, log_level: :fatal)) }
2698
+
2699
+ it 'clears the PresenceMap and local member map copy and does not emit any presence events (#RTP5a)' do
2700
+ presence_client_one.enter
2701
+ presence_client_one.subscribe(:enter) do
2702
+ presence_client_one.unsubscribe :enter
2703
+
2704
+ channel_anonymous_client.attach do
2705
+ presence_anonymous_client.get do |members|
2706
+ expect(members.count).to eq(1)
2707
+
2708
+ presence_anonymous_client.subscribe { raise 'No presence events should be emitted' }
2709
+ channel_anonymous_client.transition_state_machine! :failed, reason: RuntimeError.new
2710
+ expect(presence_anonymous_client.members.length).to eq(0)
2711
+ expect(channel_anonymous_client).to be_failed
2712
+ presence_anonymous_client.unsubscribe # prevent events being sent to the channel from Ably as it is unaware it's FAILED
2713
+
2714
+ expect(presence_client_one.members.local_members.count).to eq(1)
2715
+ channel_client_one.transition_state_machine! :failed
2716
+ expect(channel_client_one).to be_failed
2717
+ expect(presence_client_one.members.local_members.count).to eq(0)
2718
+ stop_reactor
2719
+ end
2720
+ end
2721
+ end
2722
+ end
2723
+ end
2724
+
2725
+ context 'channel transitions to the DETACHED state' do
2726
+ it 'clears the PresenceMap and local member map copy and does not emit any presence events (#RTP5a)' do
2727
+ presence_client_one.enter
2728
+ presence_client_one.subscribe(:enter) do
2729
+ presence_client_one.unsubscribe :enter
2730
+
2731
+ channel_anonymous_client.attach do
2732
+ presence_anonymous_client.get do |members|
2733
+ expect(members.count).to eq(1)
2734
+
2735
+ presence_anonymous_client.subscribe { raise 'No presence events should be emitted' }
2736
+ channel_anonymous_client.detach do
2737
+ expect(presence_anonymous_client.members.length).to eq(0)
2738
+ expect(channel_anonymous_client).to be_detached
2739
+
2740
+ expect(presence_client_one.members.local_members.count).to eq(1)
2741
+ channel_client_one.detach do
2742
+ expect(presence_client_one.members.local_members.count).to eq(0)
2743
+ stop_reactor
2744
+ end
2745
+ end
2746
+ end
2747
+ end
2748
+ end
2749
+ end
2750
+ end
2751
+
2752
+ context 'channel transitions to the SUSPENDED state' do
2753
+ let(:auth_callback) do
2754
+ Proc.new do
2755
+ # Pause to allow presence updates to occur whilst disconnected
2756
+ sleep 1
2757
+ Ably::Rest::Client.new(client_options).auth.request_token
2758
+ end
2759
+ end
2760
+ let(:anonymous_client) { auto_close Ably::Realtime::Client.new(client_options.merge(auth_callback: auth_callback)) }
2761
+
2762
+ it 'maintains the PresenceMap and only publishes presence event changes since the last attached state (#RTP5f)' do
2763
+ presence_client_one.enter do
2764
+ presence_client_two.enter
2765
+ end
2766
+
2767
+ entered_count = 0
2768
+ presence_client_one.subscribe(:enter) do
2769
+ entered_count += 1
2770
+ next unless entered_count == 2
2771
+
2772
+ presence_client_one.unsubscribe :enter
2773
+ channel_anonymous_client.attach do
2774
+ presence_anonymous_client.get do |members|
2775
+ expect(members.count).to eq(2)
2776
+
2777
+ received_events = []
2778
+ presence_anonymous_client.subscribe do |presence_message|
2779
+ expect(presence_message.action).to eq(:leave)
2780
+ expect(presence_message.client_id).to eql(client_one_id)
2781
+ received_events << [:leave, presence_message.client_id]
2782
+ end
2783
+
2784
+ # Kill the connection triggering an automatic reconnect and reattach of the channel that is about to put into the suspended state
2785
+ anonymous_client.connection.transport.close_connection_after_writing
2786
+
2787
+ # Prevent the same connection resuming, we want a new connection and the channel SYNC to be sent
2788
+ anonymous_client.connection.reset_resume_info
2789
+
2790
+ anonymous_client.connection.once(:disconnected) do
2791
+ # Move to the SUSPENDED state and check presence map intact
2792
+ channel_anonymous_client.transition_state_machine! :suspended
2793
+
2794
+ # Change the presence map state on that channel by getting one member to leave whilst the connection for anonymous client is diconnected
2795
+ presence_client_one.leave
2796
+
2797
+ # Whilst SUSPENDED and DISCONNECTED, a get of the PresenceMap should still reveal two members
2798
+ presence_anonymous_client.get(wait_for_sync: false) do |members|
2799
+ expect(members.count).to eq(2)
2800
+
2801
+ channel_anonymous_client.once(:attached) do
2802
+ presence_anonymous_client.get do |members|
2803
+ expect(members.count).to eq(1)
2804
+ EventMachine.add_timer(0.5) do
2805
+ expect(received_events).to contain_exactly([:leave, client_one_id])
2806
+ presence_anonymous_client.unsubscribe
2807
+ stop_reactor
2808
+ end
2809
+ end
2810
+ end
2811
+ end
2812
+ end
2813
+ end
2814
+ end
2815
+ end
2816
+ end
2817
+ end
2818
+ end
1805
2819
  end
1806
2820
  end