ably-rest 0.9.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/ably-rest.gemspec +2 -1
  3. data/lib/submodules/ably-ruby/.travis.yml +6 -4
  4. data/lib/submodules/ably-ruby/CHANGELOG.md +52 -61
  5. data/lib/submodules/ably-ruby/README.md +10 -0
  6. data/lib/submodules/ably-ruby/SPEC.md +1473 -852
  7. data/lib/submodules/ably-ruby/ably.gemspec +2 -1
  8. data/lib/submodules/ably-ruby/lib/ably/auth.rb +57 -25
  9. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +34 -8
  10. data/lib/submodules/ably-ruby/lib/ably/logger.rb +10 -1
  11. data/lib/submodules/ably-ruby/lib/ably/models/auth_details.rb +42 -0
  12. data/lib/submodules/ably-ruby/lib/ably/models/channel_state_change.rb +18 -4
  13. data/lib/submodules/ably-ruby/lib/ably/models/connection_details.rb +6 -3
  14. data/lib/submodules/ably-ruby/lib/ably/models/connection_state_change.rb +4 -3
  15. data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +1 -1
  16. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +12 -1
  17. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/base.rb +101 -97
  18. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +13 -1
  19. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +20 -3
  20. data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +7 -3
  21. data/lib/submodules/ably-ruby/lib/ably/modules/enum.rb +17 -7
  22. data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +29 -14
  23. data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +7 -4
  24. data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +2 -4
  25. data/lib/submodules/ably-ruby/lib/ably/modules/uses_state_machine.rb +7 -3
  26. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +2 -0
  27. data/lib/submodules/ably-ruby/lib/ably/realtime/auth.rb +79 -31
  28. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +62 -26
  29. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +154 -65
  30. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +14 -15
  31. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +16 -3
  32. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +38 -29
  33. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +6 -1
  34. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +108 -49
  35. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +165 -59
  36. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_state_machine.rb +22 -3
  37. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +19 -10
  38. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +67 -45
  39. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +198 -36
  40. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_manager.rb +30 -6
  41. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_state_machine.rb +5 -12
  42. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +3 -3
  43. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +21 -8
  44. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/exceptions.rb +1 -3
  45. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/logger.rb +2 -2
  46. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -1
  47. data/lib/submodules/ably-ruby/lib/ably/util/pub_sub.rb +1 -1
  48. data/lib/submodules/ably-ruby/lib/ably/util/safe_deferrable.rb +26 -0
  49. data/lib/submodules/ably-ruby/lib/ably/version.rb +2 -2
  50. data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +416 -99
  51. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +5 -3
  52. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +1011 -160
  53. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +2 -2
  54. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +458 -27
  55. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +436 -97
  56. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +52 -23
  57. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +5 -3
  58. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +1160 -105
  59. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +151 -22
  60. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +1 -1
  61. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +88 -27
  62. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +42 -15
  63. data/lib/submodules/ably-ruby/spec/acceptance/rest/presence_spec.rb +4 -4
  64. data/lib/submodules/ably-ruby/spec/rspec_config.rb +2 -1
  65. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +2 -2
  66. data/lib/submodules/ably-ruby/spec/shared/safe_deferrable_behaviour.rb +6 -2
  67. data/lib/submodules/ably-ruby/spec/support/debug_failure_helper.rb +20 -4
  68. data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +32 -1
  69. data/lib/submodules/ably-ruby/spec/unit/auth_spec.rb +4 -11
  70. data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +28 -2
  71. data/lib/submodules/ably-ruby/spec/unit/models/auth_details_spec.rb +49 -0
  72. data/lib/submodules/ably-ruby/spec/unit/models/channel_state_change_spec.rb +23 -3
  73. data/lib/submodules/ably-ruby/spec/unit/models/connection_details_spec.rb +12 -1
  74. data/lib/submodules/ably-ruby/spec/unit/models/connection_state_change_spec.rb +15 -4
  75. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +34 -2
  76. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +73 -2
  77. data/lib/submodules/ably-ruby/spec/unit/models/protocol_message_spec.rb +64 -6
  78. data/lib/submodules/ably-ruby/spec/unit/models/token_details_spec.rb +1 -1
  79. data/lib/submodules/ably-ruby/spec/unit/models/token_request_spec.rb +1 -1
  80. data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +2 -1
  81. data/lib/submodules/ably-ruby/spec/unit/modules/enum_spec.rb +69 -0
  82. data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +149 -22
  83. data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +9 -3
  84. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +1 -1
  85. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +8 -5
  86. data/lib/submodules/ably-ruby/spec/unit/realtime/incoming_message_dispatcher_spec.rb +1 -1
  87. data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +4 -3
  88. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +1 -1
  89. data/lib/submodules/ably-ruby/spec/unit/util/crypto_spec.rb +3 -3
  90. metadata +7 -5
@@ -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,8 +42,17 @@ 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
+ acked = false
46
+ received = false
45
47
  presence_client_one.public_send(method_name.to_s.gsub(/leave|update/, 'enter'), args) do
46
- yield
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)
53
+ presence_client_one.unsubscribe
54
+ received = true
55
+ yield if acked & received
47
56
  end
48
57
  else
49
58
  yield
@@ -56,8 +65,30 @@ describe Ably::Realtime::Presence, :event_machine do
56
65
  channel_client_one.attach do
57
66
  channel_client_one.transition_state_machine :detaching
58
67
  channel_client_one.once(:detached) do
59
- 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
60
- 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
61
92
  end
62
93
  end
63
94
  end
@@ -68,8 +99,30 @@ describe Ably::Realtime::Presence, :event_machine do
68
99
  channel_client_one.attach do
69
100
  channel_client_one.transition_state_machine :failed
70
101
  expect(channel_client_one.state).to eq(:failed)
71
- 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
72
- 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)
73
126
  end
74
127
  end
75
128
  end
@@ -86,20 +139,24 @@ describe Ably::Realtime::Presence, :event_machine do
86
139
  let(:client_one) { auto_close Ably::Realtime::Client.new(default_options.merge(queue_messages: false, client_id: client_id)) }
87
140
 
88
141
  context 'and connection state initialized' do
89
- it 'raises an exception' do
90
- 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
91
147
  expect(client_one.connection).to be_initialized
92
- stop_reactor
93
148
  end
94
149
  end
95
150
 
96
151
  context 'and connection state connecting' do
97
- it 'raises an exception' do
152
+ it 'fails the deferrable' do
98
153
  client_one.connect
99
154
  EventMachine.next_tick do
100
- 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
101
159
  expect(client_one.connection).to be_connecting
102
- stop_reactor
103
160
  end
104
161
  end
105
162
  end
@@ -107,12 +164,14 @@ describe Ably::Realtime::Presence, :event_machine do
107
164
  context 'and connection state disconnected' do
108
165
  let(:client_one) { auto_close Ably::Realtime::Client.new(default_options.merge(queue_messages: false, client_id: client_id, :log_level => :error)) }
109
166
 
110
- it 'raises an exception' do
167
+ it 'fails the deferrable' do
111
168
  client_one.connection.once(:connected) do
112
169
  client_one.connection.once(:disconnected) do
113
- 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
114
174
  expect(client_one.connection).to be_disconnected
115
- stop_reactor
116
175
  end
117
176
  client_one.connection.transition_state_machine :disconnected
118
177
  end
@@ -258,7 +317,8 @@ describe Ably::Realtime::Presence, :event_machine do
258
317
 
259
318
  it 'catches exceptions in the provided method block and logs them to the logger' do
260
319
  setup_test(method_name, args, options) do
261
- 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/)
262
322
  stop_reactor
263
323
  end
264
324
  presence_client_one.public_send(method_name, args) { raise 'Intentional exception' }
@@ -321,6 +381,13 @@ describe Ably::Realtime::Presence, :event_machine do
321
381
  stop_reactor
322
382
  end
323
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
324
391
  end
325
392
 
326
393
  context ":#{method_name} when authenticated with a valid client_id" do
@@ -409,14 +476,14 @@ describe Ably::Realtime::Presence, :event_machine do
409
476
  end
410
477
 
411
478
  context 'when attached (but not present) on a presence channel with an anonymous client (no client ID)' do
412
- 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
413
480
  channel_anonymous_client.attach do
414
481
  presence_anonymous_client.subscribe(:enter) do |presence_message|
415
482
  expect(presence_message.client_id).to eql(client_one.client_id)
416
483
 
417
484
  presence_anonymous_client.get do |members|
418
485
  expect(members.first.client_id).to eql(client_one.client_id)
419
- expect(members.first.action).to eq(:enter)
486
+ expect(members.first.action).to eq(:present)
420
487
 
421
488
  presence_anonymous_client.subscribe(:leave) do |leave_presence_message|
422
489
  expect(leave_presence_message.client_id).to eql(client_one.client_id)
@@ -436,7 +503,7 @@ describe Ably::Realtime::Presence, :event_machine do
436
503
  end
437
504
  end
438
505
 
439
- context '#members map', api_private: true do
506
+ context '#members map / PresenceMap (#RTP2)', api_private: true do
440
507
  it 'is available once the channel is created' do
441
508
  expect(presence_anonymous_client.members).to_not be_nil
442
509
  stop_reactor
@@ -468,7 +535,14 @@ describe Ably::Realtime::Presence, :event_machine do
468
535
 
469
536
  context 'once server sync is complete' do
470
537
  it 'behaves like an Enumerable allowing direct access to current members' do
471
- when_all(presence_client_one.enter, presence_client_two.enter) do
538
+ presence_client_one.enter
539
+ presence_client_two.enter
540
+
541
+ entered = 0
542
+ presence_client_one.subscribe(:enter) do
543
+ entered += 1
544
+ next unless entered == 2
545
+
472
546
  presence_anonymous_client.members.once(:in_sync) do
473
547
  expect(presence_anonymous_client.members.count).to eql(2)
474
548
  member_ids = presence_anonymous_client.members.map(&:member_key)
@@ -481,26 +555,199 @@ describe Ably::Realtime::Presence, :event_machine do
481
555
  end
482
556
  end
483
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
484
704
  end
485
705
 
486
- context '#sync_complete?' do
706
+ context '#sync_complete? and SYNC flags (#RTP1)' do
487
707
  context 'when attaching to a channel without any members present' do
488
- 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
+
489
718
  channel_anonymous_client.attach do
490
719
  expect(channel_anonymous_client.presence).to be_sync_complete
491
- stop_reactor
720
+ EventMachine.next_tick do
721
+ expect(flag_checked).to eql(true)
722
+ stop_reactor
723
+ end
492
724
  end
493
725
  end
494
726
  end
495
727
 
496
728
  context 'when attaching to a channel with members present' do
497
- it 'is false and the presence channel will subsequently be synced' do
498
- presence_client_one.enter 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
+
739
+ presence_client_one.enter
740
+ presence_client_one.subscribe(:enter) do
741
+ presence_client_one.unsubscribe :enter
742
+
499
743
  channel_anonymous_client.attach do
500
744
  expect(channel_anonymous_client.presence).to_not be_sync_complete
501
- channel_anonymous_client.presence.get(wait_for_sync: true) do
745
+ channel_anonymous_client.presence.get do
502
746
  expect(channel_anonymous_client.presence).to be_sync_complete
503
- stop_reactor
747
+ EventMachine.next_tick do
748
+ expect(flag_checked).to eql(true)
749
+ stop_reactor
750
+ end
504
751
  end
505
752
  end
506
753
  end
@@ -508,19 +755,20 @@ describe Ably::Realtime::Presence, :event_machine do
508
755
  end
509
756
  end
510
757
 
511
- context '250 existing (present) members on a channel (3 SYNC pages)' do
512
- context 'requires at least 3 SYNC ProtocolMessages', em_timeout: 30 do
513
- 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 }
514
761
  let(:present) { [] }
515
762
  let(:entered) { [] }
516
763
  let(:sync_pages_received) { [] }
517
764
  let(:client_one) { auto_close Ably::Realtime::Client.new(client_options.merge(auth_callback: wildcard_token)) }
518
765
 
519
766
  def setup_members_on(presence)
520
- enter_expected_count.times do |index|
767
+ enter_expected_count.times do |indx|
521
768
  # 10 messages per second max rate on simulation accounts
522
- EventMachine.add_timer(index / 10) do
523
- 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|
524
772
  entered << message
525
773
  next unless entered.count == enter_expected_count
526
774
  yield
@@ -543,8 +791,47 @@ describe Ably::Realtime::Presence, :event_machine do
543
791
  end
544
792
  end
545
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
+
546
833
  context 'and a member leaves before the SYNC operation is complete' do
547
- 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
548
835
  all_client_ids = enter_expected_count.times.map { |id| "client:#{id}" }
549
836
 
550
837
  setup_members_on(presence_client_one) do
@@ -558,7 +845,14 @@ describe Ably::Realtime::Presence, :event_machine do
558
845
  presence_anonymous_client.subscribe(:leave) do |leave_message|
559
846
  expect(leave_message.client_id).to eql(leave_member.client_id)
560
847
  expect(present.count).to be < enter_expected_count
561
- 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)
562
856
  presence_anonymous_client.unsubscribe
563
857
  stop_reactor
564
858
  end
@@ -571,7 +865,7 @@ describe Ably::Realtime::Presence, :event_machine do
571
865
  if sync_pages_received.count == 1
572
866
  leave_action = Ably::Models::PresenceMessage::ACTION.Leave
573
867
  leave_member = Ably::Models::PresenceMessage.new(
574
- 'id' => "#{client_one.connection.id}-#{all_client_ids.first}:0",
868
+ 'id' => "#{client_one.connection.id}:#{all_client_ids.first}:0",
575
869
  'clientId' => all_client_ids.first,
576
870
  'connectionId' => client_one.connection.id,
577
871
  'timestamp' => as_since_epoch(Time.now),
@@ -585,7 +879,7 @@ describe Ably::Realtime::Presence, :event_machine do
585
879
  end
586
880
  end
587
881
 
588
- 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
589
883
  started_at = Time.now
590
884
 
591
885
  setup_members_on(presence_client_one) do
@@ -593,11 +887,12 @@ describe Ably::Realtime::Presence, :event_machine do
593
887
 
594
888
  presence_anonymous_client.subscribe(:present) do |present_message|
595
889
  present << present_message
596
- leave_member = present_message unless leave_member
597
890
 
598
891
  if present.count == enter_expected_count
599
892
  presence_anonymous_client.get do |members|
600
- 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)
601
896
  EventMachine.add_timer(1) do
602
897
  presence_anonymous_client.unsubscribe
603
898
  stop_reactor
@@ -607,7 +902,7 @@ describe Ably::Realtime::Presence, :event_machine do
607
902
  end
608
903
 
609
904
  presence_anonymous_client.subscribe(:leave) do |leave_message|
610
- 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"
611
906
  end
612
907
 
613
908
  anonymous_client.connect do
@@ -615,10 +910,12 @@ describe Ably::Realtime::Presence, :event_machine do
615
910
  if protocol_message.action == :sync
616
911
  sync_pages_received << protocol_message
617
912
  if sync_pages_received.count == 1
913
+ first_member = protocol_message.presence[0] # get the first member in the SYNC set
618
914
  leave_action = Ably::Models::PresenceMessage::ACTION.Leave
619
915
  leave_member = Ably::Models::PresenceMessage.new(
620
- 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))
621
917
  )
918
+ # After the SYNC has started, no inject that member has having left with a timestamp before the sync
622
919
  presence_anonymous_client.__incoming_msgbus__.publish :presence, leave_member
623
920
  end
624
921
  end
@@ -627,7 +924,7 @@ describe Ably::Realtime::Presence, :event_machine do
627
924
  end
628
925
  end
629
926
 
630
- 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
631
928
  left_client = 10
632
929
  left_client_id = "client:#{left_client}"
633
930
 
@@ -649,7 +946,7 @@ describe Ably::Realtime::Presence, :event_machine do
649
946
  member_left_emitted = true
650
947
  end
651
948
 
652
- presence_anonymous_client.get(wait_for_sync: true) do |members|
949
+ presence_anonymous_client.get do |members|
653
950
  expect(members.count).to eql(enter_expected_count - 1)
654
951
  expect(member_left_emitted).to eql(true)
655
952
  expect(members.map(&:client_id)).to_not include(left_client_id)
@@ -662,7 +959,7 @@ describe Ably::Realtime::Presence, :event_machine do
662
959
  channel_anonymous_client.attach do
663
960
  leave_action = Ably::Models::PresenceMessage::ACTION.Leave
664
961
  fake_leave_presence_message = Ably::Models::PresenceMessage.new(
665
- 'id' => "#{client_one.connection.id}-#{left_client_id}:0",
962
+ 'id' => "#{client_one.connection.id}:#{left_client_id}:0",
666
963
  'clientId' => left_client_id,
667
964
  'connectionId' => client_one.connection.id,
668
965
  'timestamp' => as_since_epoch(Time.now),
@@ -676,35 +973,38 @@ describe Ably::Realtime::Presence, :event_machine do
676
973
  end
677
974
 
678
975
  context '#get' do
679
- context 'with :wait_for_sync option set to true' do
680
- it 'waits until sync is complete', em_timeout: 30 do # allow for slow connections and lots of messages
681
- enter_expected_count.times do |index|
682
- EventMachine.add_timer(index / 10) do
683
- presence_client_one.enter_client("client:#{index}") do |message|
684
- entered << message
685
- next unless entered.count == enter_expected_count
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}"
981
+ end
982
+ end
686
983
 
687
- presence_anonymous_client.get(wait_for_sync: true) do |members|
688
- expect(members.map(&:client_id).uniq.count).to eql(enter_expected_count)
689
- expect(members.count).to eql(enter_expected_count)
690
- stop_reactor
691
- end
692
- end
984
+ presence_client_one.subscribe(:enter) do |message|
985
+ entered << message
986
+ next unless entered.count == enter_expected_count
987
+
988
+ presence_anonymous_client.get do |members|
989
+ expect(members.map(&:client_id).uniq.count).to eql(enter_expected_count)
990
+ expect(members.count).to eql(enter_expected_count)
991
+ stop_reactor
693
992
  end
694
993
  end
695
994
  end
696
995
  end
697
996
 
698
- context 'by default' do
997
+ context 'with :wait_for_sync option set to false (#RTP11c1)' do
699
998
  it 'it does not wait for sync', em_timeout: 30 do # allow for slow connections and lots of messages
700
- enter_expected_count.times do |index|
701
- EventMachine.add_timer(index / 10) do
702
- presence_client_one.enter_client("client:#{index}") do |message|
999
+ enter_expected_count.times do |indx|
1000
+ EventMachine.add_timer(indx / 10) do
1001
+ presence_client_one.enter_client "client:#{indx}"
1002
+ presence_client_one.subscribe(:enter) do |message|
703
1003
  entered << message
704
1004
  next unless entered.count == enter_expected_count
705
1005
 
706
1006
  channel_anonymous_client.attach do
707
- presence_anonymous_client.get do |members|
1007
+ presence_anonymous_client.get(wait_for_sync: false) do |members|
708
1008
  expect(presence_anonymous_client.members).to_not be_in_sync
709
1009
  expect(members.count).to eql(0)
710
1010
  stop_reactor
@@ -896,24 +1196,41 @@ describe Ably::Realtime::Presence, :event_machine do
896
1196
 
897
1197
  context 'and sync is complete' do
898
1198
  it 'does not cache members that have left' do
899
- presence_client_one.enter enter_data do
1199
+ enter_ack = false
1200
+
1201
+ presence_client_one.subscribe(:enter) do
1202
+ presence_client_one.unsubscribe :enter
1203
+
900
1204
  expect(presence_client_one.members).to be_in_sync
901
1205
  expect(presence_client_one.members.send(:members).count).to eql(1)
902
1206
  presence_client_one.leave data
903
1207
  end
904
1208
 
1209
+ presence_client_one.enter(enter_data) do
1210
+ enter_ack = true
1211
+ end
1212
+
905
1213
  presence_client_one.subscribe(:leave) do |presence_message|
1214
+ presence_client_one.unsubscribe :leave
906
1215
  expect(presence_message.data).to eql(data)
907
1216
  expect(presence_client_one.members.send(:members).count).to eql(0)
1217
+ expect(enter_ack).to eql(true)
908
1218
  stop_reactor
909
1219
  end
910
1220
  end
911
1221
  end
912
1222
  end
913
1223
 
914
- it 'raises an exception if not entered' do
915
- expect { channel_client_one.presence.leave }.to raise_error(Ably::Exceptions::Standard, /Unable to leave presence channel that is not entered/)
916
- 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
917
1234
  end
918
1235
 
919
1236
  it_should_behave_like 'a public presence method', :leave, :left, {}, enter_first: true
@@ -1193,28 +1510,75 @@ describe Ably::Realtime::Presence, :event_machine do
1193
1510
  end
1194
1511
 
1195
1512
  it 'catches exceptions in the provided method block' do
1196
- 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/)
1197
1515
  stop_reactor
1198
1516
  end
1199
1517
  presence_client_one.get { raise 'Intentional exception' }
1200
1518
  end
1201
1519
 
1202
- 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
1203
1556
  channel_client_one.attach do
1204
- channel_client_one.transition_state_machine :detaching
1205
- channel_client_one.once(:detached) do
1206
- expect { presence_client_one.get }.to raise_error Ably::Exceptions::InvalidStateChange, /Operation is not allowed when channel is in STATE.detached/i
1207
- 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
1208
1566
  end
1209
1567
  end
1210
1568
  end
1211
1569
 
1212
- it 'raise an exception if the channel is failed' do
1570
+ it 'fails if the connection is FAILED (#RTP11b)' do
1213
1571
  channel_client_one.attach do
1214
1572
  channel_client_one.transition_state_machine :failed
1215
1573
  expect(channel_client_one.state).to eq(:failed)
1216
- expect { presence_client_one.get }.to raise_error Ably::Exceptions::InvalidStateChange, /Operation is not allowed when channel is in STATE.failed/i
1217
- 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
1218
1582
  end
1219
1583
  end
1220
1584
 
@@ -1240,7 +1604,7 @@ describe Ably::Realtime::Presence, :event_machine do
1240
1604
  end
1241
1605
 
1242
1606
  context 'when :wait_for_sync is true' do
1243
- it 'fails if the connection fails' do
1607
+ it 'fails if the connection becomes FAILED (#RTP11b)' do
1244
1608
  when_all(*connect_members_deferrables) do
1245
1609
  channel_client_two.attach do
1246
1610
  client_two.connection.transport.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
@@ -1263,7 +1627,7 @@ describe Ably::Realtime::Presence, :event_machine do
1263
1627
  end
1264
1628
  end
1265
1629
 
1266
- it 'fails if the channel is detached' do
1630
+ it 'fails if the channel becomes detached (#RTP11b)' do
1267
1631
  when_all(*connect_members_deferrables) do
1268
1632
  channel_client_two.attach do
1269
1633
  client_two.connection.transport.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
@@ -1287,10 +1651,10 @@ describe Ably::Realtime::Presence, :event_machine do
1287
1651
  end
1288
1652
  end
1289
1653
 
1290
- # skip 'it fails if the connection changes to failed state'
1291
-
1292
- it 'returns the current members on the channel' do
1293
- presence_client_one.enter do
1654
+ it 'returns the current members on the channel (#RTP11a)' do
1655
+ presence_client_one.enter
1656
+ presence_client_one.subscribe(:enter) do
1657
+ presence_client_one.unsubscribe :enter
1294
1658
  presence_client_one.get do |members|
1295
1659
  expect(members.count).to eq(1)
1296
1660
 
@@ -1304,7 +1668,7 @@ describe Ably::Realtime::Presence, :event_machine do
1304
1668
  end
1305
1669
  end
1306
1670
 
1307
- it 'filters by connection_id option if provided' do
1671
+ it 'filters by connection_id option if provided (#RTP11c3)' do
1308
1672
  presence_client_one.enter do
1309
1673
  presence_client_two.enter
1310
1674
  end
@@ -1326,7 +1690,7 @@ describe Ably::Realtime::Presence, :event_machine do
1326
1690
  end
1327
1691
  end
1328
1692
 
1329
- it 'filters by client_id option if provided' do
1693
+ it 'filters by client_id option if provided (#RTP11c2)' do
1330
1694
  presence_client_one.enter do
1331
1695
  presence_client_two.enter
1332
1696
  end
@@ -1350,8 +1714,11 @@ describe Ably::Realtime::Presence, :event_machine do
1350
1714
  end
1351
1715
  end
1352
1716
 
1353
- it 'does not wait for SYNC to complete if :wait_for_sync option is false' do
1354
- presence_client_one.enter do
1717
+ it 'does not wait for SYNC to complete if :wait_for_sync option is false (#RTP11c1)' do
1718
+ presence_client_one.enter
1719
+ presence_client_one.subscribe(:enter) do
1720
+ presence_client_one.unsubscribe :enter
1721
+
1355
1722
  presence_client_two.get(wait_for_sync: false) do |members|
1356
1723
  expect(members.count).to eql(0)
1357
1724
  stop_reactor
@@ -1359,14 +1726,43 @@ describe Ably::Realtime::Presence, :event_machine do
1359
1726
  end
1360
1727
  end
1361
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
+
1362
1741
  context 'when a member enters and then leaves' do
1363
1742
  it 'has no members' do
1364
1743
  presence_client_one.enter do
1365
- presence_client_one.leave do
1366
- presence_client_one.get do |members|
1367
- expect(members.count).to eq(0)
1368
- stop_reactor
1369
- end
1744
+ presence_client_one.leave
1745
+ end
1746
+
1747
+ presence_client_one.subscribe(:leave) do
1748
+ presence_client_one.get do |members|
1749
+ expect(members.count).to eq(0)
1750
+ stop_reactor
1751
+ end
1752
+ end
1753
+ end
1754
+ end
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
1370
1766
  end
1371
1767
  end
1372
1768
  end
@@ -1380,9 +1776,9 @@ describe Ably::Realtime::Presence, :event_machine do
1380
1776
  let(:total_members) { members_per_client * 2 }
1381
1777
 
1382
1778
  it 'returns a complete list of members on all clients' do
1383
- members_per_client.times do |index|
1384
- presence_client_one.enter_client("client_1:#{index}")
1385
- 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}")
1386
1782
  end
1387
1783
 
1388
1784
  presence_client_one.subscribe(:enter) do
@@ -1469,7 +1865,9 @@ describe Ably::Realtime::Presence, :event_machine do
1469
1865
 
1470
1866
  it 'logs the error and continues' do
1471
1867
  emitted_exception = false
1472
- 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
1473
1871
  presence_client_one.subscribe do |presence_message|
1474
1872
  emitted_exception = true
1475
1873
  raise exception
@@ -1524,7 +1922,10 @@ describe Ably::Realtime::Presence, :event_machine do
1524
1922
 
1525
1923
  context 'REST #get' do
1526
1924
  it 'returns current members' do
1527
- presence_client_one.enter(data_payload) do
1925
+ presence_client_one.enter data_payload
1926
+ presence_client_one.subscribe(:enter) do
1927
+ presence_client_one.unsubscribe :enter
1928
+
1528
1929
  members_page = channel_rest_client_one.presence.get
1529
1930
  this_member = members_page.items.first
1530
1931
 
@@ -1538,7 +1939,10 @@ describe Ably::Realtime::Presence, :event_machine do
1538
1939
 
1539
1940
  it 'returns no members once left' do
1540
1941
  presence_client_one.enter(data_payload) do
1541
- presence_client_one.leave do
1942
+ presence_client_one.leave
1943
+ presence_client_one.subscribe(:leave) do
1944
+ presence_client_one.unsubscribe :leave
1945
+
1542
1946
  members_page = channel_rest_client_one.presence.get
1543
1947
  expect(members_page.items.count).to eql(0)
1544
1948
  stop_reactor
@@ -1654,7 +2058,8 @@ describe Ably::Realtime::Presence, :event_machine do
1654
2058
 
1655
2059
  context '#get' do
1656
2060
  it 'returns a list of members with decrypted data' do
1657
- encrypted_channel.presence.enter(data) do
2061
+ encrypted_channel.presence.enter(data)
2062
+ encrypted_channel.presence.subscribe(:enter) do
1658
2063
  encrypted_channel.presence.get do |members|
1659
2064
  member = members.first
1660
2065
  expect(member.encoding).to be_nil
@@ -1667,7 +2072,8 @@ describe Ably::Realtime::Presence, :event_machine do
1667
2072
 
1668
2073
  context 'REST #get' do
1669
2074
  it 'returns a list of members with decrypted data' do
1670
- encrypted_channel.presence.enter(data) do
2075
+ encrypted_channel.presence.enter(data)
2076
+ encrypted_channel.presence.subscribe(:enter) do
1671
2077
  member = channel_rest_client_one.presence.get.items.first
1672
2078
  expect(member.encoding).to be_nil
1673
2079
  expect(member.data).to eql(data)
@@ -1696,9 +2102,8 @@ describe Ably::Realtime::Presence, :event_machine do
1696
2102
 
1697
2103
  it 'emits an error when cipher does not match and presence data cannot be decoded' do
1698
2104
  incompatible_encrypted_channel.attach do
1699
- incompatible_encrypted_channel.on(:error) do |error|
1700
- expect(error).to be_a(Ably::Exceptions::CipherError)
1701
- 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/)
1702
2107
  stop_reactor
1703
2108
  end
1704
2109
 
@@ -1736,13 +2141,13 @@ describe Ably::Realtime::Presence, :event_machine do
1736
2141
  end
1737
2142
 
1738
2143
  context 'connection failure mid-way through a large member sync' do
1739
- let(:members_count) { 250 }
2144
+ let(:members_count) { 201 }
1740
2145
  let(:sync_pages_received) { [] }
1741
- let(:client_options) { default_options.merge(log_level: :error) }
2146
+ let(:client_options) { default_options.merge(log_level: :fatal) }
1742
2147
 
1743
- it 'resumes the SYNC operation', em_timeout: 15 do
1744
- when_all(*members_count.times.map do |index|
1745
- 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}")
1746
2151
  end) do
1747
2152
  channel_client_two.attach do
1748
2153
  client_two.connection.transport.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
@@ -1761,5 +2166,655 @@ describe Ably::Realtime::Presence, :event_machine do
1761
2166
  end
1762
2167
  end
1763
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
1764
2819
  end
1765
2820
  end