ably-rest 0.9.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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