ably 0.8.15 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -4
  3. data/CHANGELOG.md +6 -2
  4. data/README.md +5 -1
  5. data/SPEC.md +1473 -852
  6. data/ably.gemspec +11 -8
  7. data/lib/ably/auth.rb +90 -53
  8. data/lib/ably/exceptions.rb +37 -8
  9. data/lib/ably/logger.rb +10 -1
  10. data/lib/ably/models/auth_details.rb +42 -0
  11. data/lib/ably/models/channel_state_change.rb +18 -4
  12. data/lib/ably/models/connection_details.rb +6 -3
  13. data/lib/ably/models/connection_state_change.rb +4 -3
  14. data/lib/ably/models/error_info.rb +1 -1
  15. data/lib/ably/models/message.rb +17 -1
  16. data/lib/ably/models/message_encoders/base.rb +103 -82
  17. data/lib/ably/models/message_encoders/base64.rb +1 -1
  18. data/lib/ably/models/presence_message.rb +16 -1
  19. data/lib/ably/models/protocol_message.rb +20 -3
  20. data/lib/ably/models/token_details.rb +11 -1
  21. data/lib/ably/models/token_request.rb +16 -6
  22. data/lib/ably/modules/async_wrapper.rb +7 -3
  23. data/lib/ably/modules/encodeable.rb +51 -12
  24. data/lib/ably/modules/enum.rb +17 -7
  25. data/lib/ably/modules/event_emitter.rb +29 -14
  26. data/lib/ably/modules/model_common.rb +13 -21
  27. data/lib/ably/modules/state_emitter.rb +7 -4
  28. data/lib/ably/modules/state_machine.rb +2 -4
  29. data/lib/ably/modules/uses_state_machine.rb +7 -3
  30. data/lib/ably/realtime.rb +2 -0
  31. data/lib/ably/realtime/auth.rb +102 -42
  32. data/lib/ably/realtime/channel.rb +68 -26
  33. data/lib/ably/realtime/channel/channel_manager.rb +154 -65
  34. data/lib/ably/realtime/channel/channel_state_machine.rb +14 -15
  35. data/lib/ably/realtime/client.rb +18 -3
  36. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +38 -29
  37. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +6 -1
  38. data/lib/ably/realtime/connection.rb +108 -49
  39. data/lib/ably/realtime/connection/connection_manager.rb +167 -61
  40. data/lib/ably/realtime/connection/connection_state_machine.rb +22 -3
  41. data/lib/ably/realtime/connection/websocket_transport.rb +19 -10
  42. data/lib/ably/realtime/presence.rb +70 -45
  43. data/lib/ably/realtime/presence/members_map.rb +201 -36
  44. data/lib/ably/realtime/presence/presence_manager.rb +30 -6
  45. data/lib/ably/realtime/presence/presence_state_machine.rb +5 -12
  46. data/lib/ably/rest.rb +2 -2
  47. data/lib/ably/rest/channel.rb +5 -5
  48. data/lib/ably/rest/client.rb +31 -27
  49. data/lib/ably/rest/middleware/exceptions.rb +1 -3
  50. data/lib/ably/rest/middleware/logger.rb +2 -2
  51. data/lib/ably/rest/presence.rb +2 -2
  52. data/lib/ably/util/pub_sub.rb +1 -1
  53. data/lib/ably/util/safe_deferrable.rb +26 -0
  54. data/lib/ably/version.rb +2 -2
  55. data/spec/acceptance/realtime/auth_spec.rb +470 -111
  56. data/spec/acceptance/realtime/channel_history_spec.rb +5 -3
  57. data/spec/acceptance/realtime/channel_spec.rb +1017 -168
  58. data/spec/acceptance/realtime/client_spec.rb +6 -6
  59. data/spec/acceptance/realtime/connection_failures_spec.rb +458 -27
  60. data/spec/acceptance/realtime/connection_spec.rb +424 -105
  61. data/spec/acceptance/realtime/message_spec.rb +52 -23
  62. data/spec/acceptance/realtime/presence_history_spec.rb +5 -3
  63. data/spec/acceptance/realtime/presence_spec.rb +1110 -96
  64. data/spec/acceptance/rest/auth_spec.rb +222 -59
  65. data/spec/acceptance/rest/base_spec.rb +1 -1
  66. data/spec/acceptance/rest/channel_spec.rb +1 -2
  67. data/spec/acceptance/rest/client_spec.rb +104 -48
  68. data/spec/acceptance/rest/message_spec.rb +42 -15
  69. data/spec/acceptance/rest/presence_spec.rb +4 -11
  70. data/spec/rspec_config.rb +2 -1
  71. data/spec/shared/client_initializer_behaviour.rb +2 -2
  72. data/spec/shared/safe_deferrable_behaviour.rb +6 -2
  73. data/spec/spec_helper.rb +4 -2
  74. data/spec/support/debug_failure_helper.rb +20 -4
  75. data/spec/support/event_machine_helper.rb +32 -1
  76. data/spec/unit/auth_spec.rb +4 -11
  77. data/spec/unit/logger_spec.rb +28 -2
  78. data/spec/unit/models/auth_details_spec.rb +49 -0
  79. data/spec/unit/models/channel_state_change_spec.rb +23 -3
  80. data/spec/unit/models/connection_details_spec.rb +12 -1
  81. data/spec/unit/models/connection_state_change_spec.rb +15 -4
  82. data/spec/unit/models/message_encoders/base64_spec.rb +2 -1
  83. data/spec/unit/models/message_spec.rb +153 -0
  84. data/spec/unit/models/presence_message_spec.rb +192 -0
  85. data/spec/unit/models/protocol_message_spec.rb +64 -6
  86. data/spec/unit/models/token_details_spec.rb +75 -0
  87. data/spec/unit/models/token_request_spec.rb +74 -0
  88. data/spec/unit/modules/async_wrapper_spec.rb +2 -1
  89. data/spec/unit/modules/enum_spec.rb +69 -0
  90. data/spec/unit/modules/event_emitter_spec.rb +149 -22
  91. data/spec/unit/modules/state_emitter_spec.rb +9 -3
  92. data/spec/unit/realtime/client_spec.rb +1 -1
  93. data/spec/unit/realtime/connection_spec.rb +8 -5
  94. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +1 -1
  95. data/spec/unit/realtime/presence_spec.rb +4 -3
  96. data/spec/unit/rest/client_spec.rb +1 -1
  97. data/spec/unit/util/crypto_spec.rb +3 -3
  98. metadata +22 -19
@@ -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