ably 0.7.2 → 0.7.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/LICENSE.txt +1 -1
  2. data/README.md +107 -24
  3. data/SPEC.md +531 -398
  4. data/lib/ably/auth.rb +23 -15
  5. data/lib/ably/exceptions.rb +9 -0
  6. data/lib/ably/models/message.rb +17 -9
  7. data/lib/ably/models/paginated_resource.rb +12 -8
  8. data/lib/ably/models/presence_message.rb +18 -10
  9. data/lib/ably/models/protocol_message.rb +15 -4
  10. data/lib/ably/modules/async_wrapper.rb +4 -3
  11. data/lib/ably/modules/event_emitter.rb +31 -2
  12. data/lib/ably/modules/message_emitter.rb +77 -0
  13. data/lib/ably/modules/safe_deferrable.rb +71 -0
  14. data/lib/ably/modules/safe_yield.rb +41 -0
  15. data/lib/ably/modules/state_emitter.rb +28 -8
  16. data/lib/ably/realtime.rb +0 -5
  17. data/lib/ably/realtime/channel.rb +24 -29
  18. data/lib/ably/realtime/channel/channel_manager.rb +54 -11
  19. data/lib/ably/realtime/channel/channel_state_machine.rb +21 -6
  20. data/lib/ably/realtime/client.rb +7 -2
  21. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +29 -26
  22. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +4 -4
  23. data/lib/ably/realtime/connection.rb +41 -9
  24. data/lib/ably/realtime/connection/connection_manager.rb +72 -24
  25. data/lib/ably/realtime/connection/connection_state_machine.rb +26 -4
  26. data/lib/ably/realtime/connection/websocket_transport.rb +19 -6
  27. data/lib/ably/realtime/presence.rb +74 -208
  28. data/lib/ably/realtime/presence/members_map.rb +264 -0
  29. data/lib/ably/realtime/presence/presence_manager.rb +59 -0
  30. data/lib/ably/realtime/presence/presence_state_machine.rb +64 -0
  31. data/lib/ably/rest/channel.rb +1 -1
  32. data/lib/ably/rest/client.rb +6 -2
  33. data/lib/ably/rest/presence.rb +1 -1
  34. data/lib/ably/util/pub_sub.rb +3 -1
  35. data/lib/ably/util/safe_deferrable.rb +18 -0
  36. data/lib/ably/version.rb +1 -1
  37. data/spec/acceptance/realtime/channel_history_spec.rb +2 -2
  38. data/spec/acceptance/realtime/channel_spec.rb +28 -6
  39. data/spec/acceptance/realtime/connection_failures_spec.rb +116 -46
  40. data/spec/acceptance/realtime/connection_spec.rb +55 -10
  41. data/spec/acceptance/realtime/message_spec.rb +32 -0
  42. data/spec/acceptance/realtime/presence_spec.rb +456 -96
  43. data/spec/acceptance/realtime/stats_spec.rb +2 -2
  44. data/spec/acceptance/realtime/time_spec.rb +2 -2
  45. data/spec/acceptance/rest/auth_spec.rb +75 -7
  46. data/spec/shared/client_initializer_behaviour.rb +8 -0
  47. data/spec/shared/safe_deferrable_behaviour.rb +71 -0
  48. data/spec/support/api_helper.rb +1 -1
  49. data/spec/support/event_machine_helper.rb +1 -1
  50. data/spec/support/test_app.rb +13 -7
  51. data/spec/unit/models/message_spec.rb +15 -14
  52. data/spec/unit/models/paginated_resource_spec.rb +4 -4
  53. data/spec/unit/models/presence_message_spec.rb +17 -17
  54. data/spec/unit/models/stat_spec.rb +4 -4
  55. data/spec/unit/modules/async_wrapper_spec.rb +28 -9
  56. data/spec/unit/modules/event_emitter_spec.rb +50 -0
  57. data/spec/unit/modules/state_emitter_spec.rb +76 -2
  58. data/spec/unit/realtime/channel_spec.rb +51 -20
  59. data/spec/unit/realtime/channels_spec.rb +3 -3
  60. data/spec/unit/realtime/connection_spec.rb +30 -0
  61. data/spec/unit/realtime/presence_spec.rb +52 -26
  62. data/spec/unit/realtime/safe_deferrable_spec.rb +12 -0
  63. metadata +85 -39
  64. checksums.yaml +0 -7
  65. data/.ruby-version.old +0 -1
@@ -2,6 +2,8 @@
2
2
  require 'spec_helper'
3
3
 
4
4
  describe Ably::Realtime::Presence, :event_machine do
5
+ include Ably::Modules::Conversions
6
+
5
7
  vary_by_protocol do
6
8
  let(:default_options) { { api_key: api_key, environment: environment, protocol: protocol } }
7
9
  let(:client_options) { default_options }
@@ -20,6 +22,98 @@ describe Ably::Realtime::Presence, :event_machine do
20
22
  let(:presence_client_two) { channel_client_two.presence }
21
23
  let(:data_payload) { random_str }
22
24
 
25
+ def force_connection_failure(client)
26
+ # Prevent any further SYNC messages coming in on this connection
27
+ client.connection.transport.send(:driver).remove_all_listeners('message')
28
+ client.connection.transport.unbind
29
+ end
30
+
31
+ shared_examples_for 'a public presence method' do |method_name, expected_state, args, options = {}|
32
+ def setup_test(method_name, args, options)
33
+ if options[:enter_first]
34
+ presence_client_one.public_send(method_name.to_s.gsub(/leave|update/, 'enter'), args) do
35
+ yield
36
+ end
37
+ else
38
+ yield
39
+ end
40
+ end
41
+
42
+ unless expected_state == :left
43
+ %w(detached failed).each do |state|
44
+ it "raise an exception if the channel is #{state}" do
45
+ setup_test(method_name, args, options) do
46
+ channel_client_one.attach do
47
+ channel_client_one.change_state state.to_sym
48
+ expect { presence_client_one.public_send(method_name, args) }.to raise_error Ably::Exceptions::IncompatibleStateForOperation, /Operation is not allowed when channel is in STATE.#{state}/i
49
+ stop_reactor
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
57
+ setup_test(method_name, args, options) do
58
+ expect(presence_client_one.public_send(method_name, args)).to be_a(Ably::Util::SafeDeferrable)
59
+ stop_reactor
60
+ end
61
+ end
62
+
63
+ it 'calls the Deferrable callback on success' do
64
+ setup_test(method_name, args, options) do
65
+ presence_client_one.public_send(method_name, args).callback do |presence|
66
+ expect(presence).to eql(presence_client_one)
67
+ expect(presence_client_one.state).to eq(expected_state) if expected_state
68
+ stop_reactor
69
+ end
70
+ end
71
+ end
72
+
73
+ it 'catches exceptions in the provided method block and logs them to the logger' do
74
+ setup_test(method_name, args, options) do
75
+ expect(presence_client_one.logger).to receive(:error).with(/Intentional exception/) do
76
+ stop_reactor
77
+ end
78
+ presence_client_one.public_send(method_name, args) { raise 'Intentional exception' }
79
+ end
80
+ end
81
+
82
+ context 'if connection fails before success' do
83
+ before do
84
+ # Reconfigure client library so that it makes no retry attempts and fails immediately
85
+ stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
86
+ Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
87
+ disconnected: { retry_every: 0.1, max_time_in_state: 0 },
88
+ suspended: { retry_every: 0.1, max_time_in_state: 0 }
89
+ )
90
+ end
91
+
92
+ let(:client_options) { default_options.merge(log_level: :none) }
93
+
94
+ it 'calls the Deferrable errback if channel is detached' do
95
+ setup_test(method_name, args, options) do
96
+ channel_client_one.attach do
97
+ client_one.connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
98
+ # Don't allow any messages to reach the server
99
+ client_one.connection.__outgoing_protocol_msgbus__.unsubscribe
100
+ force_connection_failure client_one
101
+ end
102
+
103
+ presence_client_one.public_send(method_name, args).tap do |deferrable|
104
+ deferrable.callback { raise 'Should not succeed' }
105
+ deferrable.errback do |presence, error|
106
+ expect(presence).to be_a(Ably::Realtime::Presence)
107
+ expect(error).to be_kind_of(Ably::Exceptions::BaseAblyException)
108
+ stop_reactor
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+
23
117
  context 'when attached (but not present) on a presence channel with an anonymous client (no client ID)' do
24
118
  it 'maintains state as other clients enter and leave the channel' do
25
119
  channel_anonymous_client.attach do
@@ -30,11 +124,11 @@ describe Ably::Realtime::Presence, :event_machine do
30
124
  expect(members.first.client_id).to eql(client_one.client_id)
31
125
  expect(members.first.action).to eq(:enter)
32
126
 
33
- presence_anonymous_client.subscribe(:leave) do |presence_message|
34
- expect(presence_message.client_id).to eql(client_one.client_id)
127
+ presence_anonymous_client.subscribe(:leave) do |leave_presence_message|
128
+ expect(leave_presence_message.client_id).to eql(client_one.client_id)
35
129
 
36
- presence_anonymous_client.get do |members|
37
- expect(members.count).to eql(0)
130
+ presence_anonymous_client.get do |members_once_left|
131
+ expect(members_once_left.count).to eql(0)
38
132
  stop_reactor
39
133
  end
40
134
  end
@@ -48,6 +142,49 @@ describe Ably::Realtime::Presence, :event_machine do
48
142
  end
49
143
  end
50
144
 
145
+ context '#members map', api_private: true do
146
+ it 'is available once the channel is created' do
147
+ expect(presence_anonymous_client.members).to_not be_nil
148
+ stop_reactor
149
+ end
150
+
151
+ it 'is not synchronised when initially created' do
152
+ expect(presence_anonymous_client.members).to_not be_sync_complete
153
+ stop_reactor
154
+ end
155
+
156
+ it 'will trigger an :in_sync event when synchronisation is complete' do
157
+ presence_client_one.enter
158
+ presence_client_two.enter
159
+
160
+ presence_anonymous_client.members.once(:in_sync) do
161
+ stop_reactor
162
+ end
163
+ end
164
+
165
+ context 'before server sync complete' do
166
+ it 'behaves like an Enumerable allowing direct access to current members' do
167
+ expect(presence_anonymous_client.members.count).to eql(0)
168
+ expect(presence_anonymous_client.members.map(&:member_key)).to eql([])
169
+ stop_reactor
170
+ end
171
+ end
172
+
173
+ context 'once server sync is complete' do
174
+ it 'behaves like an Enumerable allowing direct access to current members' do
175
+ when_all(presence_client_one.enter, presence_client_two.enter) do
176
+ presence_anonymous_client.members.once(:in_sync) do
177
+ expect(presence_anonymous_client.members.count).to eql(2)
178
+ member_ids = presence_anonymous_client.members.map(&:member_key)
179
+ expect(member_ids.count).to eql(2)
180
+ expect(member_ids.uniq.count).to eql(2)
181
+ stop_reactor
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
187
+
51
188
  context '#sync_complete?' do
52
189
  context 'when attaching to a channel without any members present' do
53
190
  it 'is true and the presence channel is considered synced immediately' do
@@ -73,33 +210,162 @@ describe Ably::Realtime::Presence, :event_machine do
73
210
  end
74
211
  end
75
212
 
76
- context 'when the SYNC of a presence channel spans multiple ProtocolMessage messages' do
77
- context 'with 250 existing (present) members' do
213
+ context '250 existing (present) members on a channel (3 SYNC pages)' do
214
+ context 'requires at least 3 SYNC ProtocolMessages' do
78
215
  let(:enter_expected_count) { 250 }
79
216
  let(:present) { [] }
80
217
  let(:entered) { [] }
218
+ let(:sync_pages_received) { [] }
219
+
220
+ def setup_members_on(presence)
221
+ enter_expected_count.times do |index|
222
+ presence.enter_client("client:#{index}") do |message|
223
+ entered << message
224
+ next unless entered.count == enter_expected_count
225
+ yield
226
+ end
227
+ end
228
+ end
81
229
 
82
- context 'when a new client attaches to the presence channel', em_timeout: 10 do
230
+ context 'when a client attaches to the presence channel', em_timeout: 10 do
83
231
  it 'emits :present for each member' do
84
- enter_expected_count.times do |index|
85
- presence_client_one.enter_client("client:#{index}") do |message|
86
- entered << message
87
- next unless entered.count == enter_expected_count
232
+ setup_members_on(presence_client_one) do
233
+ presence_anonymous_client.subscribe(:present) do |present_message|
234
+ expect(present_message.action).to eq(:present)
235
+ present << present_message
236
+ next unless present.count == enter_expected_count
237
+
238
+ expect(present.map(&:client_id).uniq.count).to eql(enter_expected_count)
239
+ stop_reactor
240
+ end
241
+ end
242
+ end
243
+
244
+ context 'and a member leaves before the SYNC operation is complete' do
245
+ it 'emits :leave immediately as the member leaves' do
246
+ all_client_ids = enter_expected_count.times.map { |id| "client:#{id}" }
247
+
248
+ setup_members_on(presence_client_one) do
249
+ leave_member = nil
88
250
 
89
251
  presence_anonymous_client.subscribe(:present) do |present_message|
90
- expect(present_message.action).to eq(:present)
91
252
  present << present_message
92
- next unless present.count == enter_expected_count
253
+ all_client_ids.delete(present_message.client_id)
254
+ end
93
255
 
94
- expect(present.map(&:client_id).uniq.count).to eql(enter_expected_count)
256
+ presence_anonymous_client.subscribe(:leave) do |leave_message|
257
+ expect(leave_message.client_id).to eql(leave_member.client_id)
258
+ expect(present.count).to be < enter_expected_count
95
259
  stop_reactor
96
260
  end
261
+
262
+ anonymous_client.connect do
263
+ anonymous_client.connection.transport.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
264
+ if protocol_message.action == :sync
265
+ sync_pages_received << protocol_message
266
+ if sync_pages_received.count == 1
267
+ leave_action = Ably::Models::PresenceMessage::ACTION.Leave
268
+ leave_member = Ably::Models::PresenceMessage.new(
269
+ 'id' => "#{client_one.connection.id}-#{all_client_ids.first}:0",
270
+ 'clientId' => all_client_ids.first,
271
+ 'connectionId' => client_one.connection.id,
272
+ 'timestamp' => as_since_epoch(Time.now),
273
+ 'action' => leave_action
274
+ )
275
+ presence_anonymous_client.__incoming_msgbus__.publish :presence, leave_member
276
+ end
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
282
+
283
+ it 'ignores presence events with timestamps prior to the current :present event in the MembersMap' do
284
+ started_at = Time.now
285
+
286
+ setup_members_on(presence_client_one) do
287
+ leave_member = nil
288
+
289
+ presence_anonymous_client.subscribe(:present) do |present_message|
290
+ present << present_message
291
+ leave_member = present_message unless leave_member
292
+
293
+ if present.count == enter_expected_count
294
+ presence_anonymous_client.get do |members|
295
+ expect(members.find { |member| member.client_id == leave_member.client_id}.action).to eq(:present)
296
+ stop_reactor
297
+ end
298
+ end
299
+ end
300
+
301
+ presence_anonymous_client.subscribe(:leave) do |leave_message|
302
+ raise 'Leave event should not have been fired because it is out of date'
303
+ end
304
+
305
+ anonymous_client.connect do
306
+ anonymous_client.connection.transport.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
307
+ if protocol_message.action == :sync
308
+ sync_pages_received << protocol_message
309
+ if sync_pages_received.count == 1
310
+ leave_action = Ably::Models::PresenceMessage::ACTION.Leave
311
+ leave_member = Ably::Models::PresenceMessage.new(
312
+ leave_member.as_json.merge('action' => leave_action, 'timestamp' => as_since_epoch(started_at))
313
+ )
314
+ presence_anonymous_client.__incoming_msgbus__.publish :presence, leave_member
315
+ end
316
+ end
317
+ end
318
+ end
319
+ end
320
+ end
321
+
322
+ 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' do
323
+ left_client = 10
324
+ left_client_id = "client:#{left_client}"
325
+
326
+ setup_members_on(presence_client_one) do
327
+ member_left_emitted = false
328
+
329
+ presence_anonymous_client.subscribe(:present) do |present_message|
330
+ if present_message.client_id == left_client_id
331
+ raise "Member #{present_message.client_id} should not have been emitted as present"
332
+ end
333
+ present << present_message.client_id
334
+ end
335
+
336
+ presence_anonymous_client.subscribe(:leave) do |leave_message|
337
+ if present.include?(leave_message.client_id)
338
+ raise "Member #{leave_message.client_id} should not have been emitted as present previously"
339
+ end
340
+ expect(leave_message.client_id).to eql(left_client_id)
341
+ member_left_emitted = true
342
+ end
343
+
344
+ presence_anonymous_client.get do |members|
345
+ expect(members.count).to eql(enter_expected_count - 1)
346
+ expect(member_left_emitted).to eql(true)
347
+ expect(members.map(&:client_id)).to_not include(left_client_id)
348
+ stop_reactor
349
+ end
350
+
351
+ channel_anonymous_client.attach do
352
+ leave_action = Ably::Models::PresenceMessage::ACTION.Leave
353
+ fake_leave_presence_message = Ably::Models::PresenceMessage.new(
354
+ 'id' => "#{client_one.connection.id}-#{left_client_id}:0",
355
+ 'clientId' => left_client_id,
356
+ 'connectionId' => client_one.connection.id,
357
+ 'timestamp' => as_since_epoch(Time.now),
358
+ 'action' => leave_action
359
+ )
360
+ # Push out a LEAVE event directly to the Presence object before it's received the :present action via the SYNC ProtocolMessage
361
+ presence_anonymous_client.__incoming_msgbus__.publish :presence, fake_leave_presence_message
362
+ end
97
363
  end
98
364
  end
99
365
  end
100
366
 
101
367
  context '#get' do
102
- it '#waits until sync is complete', event_machine: 15 do
368
+ it 'waits until sync is complete', event_machine: 15 do
103
369
  enter_expected_count.times do |index|
104
370
  presence_client_one.enter_client("client:#{index}") do |message|
105
371
  entered << message
@@ -119,7 +385,7 @@ describe Ably::Realtime::Presence, :event_machine do
119
385
  end
120
386
 
121
387
  context 'automatic attachment of channel on access to presence object' do
122
- it 'is implicit if presence state is initalized' do
388
+ it 'is implicit if presence state is initialized' do
123
389
  channel_client_one.presence
124
390
  channel_client_one.on(:attached) do
125
391
  expect(channel_client_one.state).to eq(:attached)
@@ -201,23 +467,40 @@ describe Ably::Realtime::Presence, :event_machine do
201
467
  end
202
468
  end
203
469
 
470
+ context 'message #connection_id' do
471
+ it 'matches the current client connection_id' do
472
+ channel_client_two.attach do
473
+ presence_client_one.enter
474
+ end
475
+
476
+ presence_client_two.subscribe do |presence|
477
+ expect(presence.connection_id).to eq(client_one.connection.id)
478
+ stop_reactor
479
+ end
480
+ end
481
+ end
482
+
204
483
  it 'raises an exception if client_id is not set' do
205
484
  expect { channel_anonymous_client.presence.enter }.to raise_error(Ably::Exceptions::Standard, /without a client_id/)
206
485
  stop_reactor
207
486
  end
208
487
 
209
- it 'returns a Deferrable' do
210
- expect(presence_client_one.enter).to be_a(EventMachine::Deferrable)
211
- stop_reactor
212
- end
488
+ context 'without necessary capabilities to join presence' do
489
+ let(:restricted_client) do
490
+ Ably::Realtime::Client.new(default_options.merge(api_key: restricted_api_key, log_level: :fatal))
491
+ end
492
+ let(:restricted_channel) { restricted_client.channel("cansubscribe:channel") }
493
+ let(:restricted_presence) { restricted_channel.presence }
213
494
 
214
- it 'calls the Deferrable callback on success' do
215
- presence_client_one.enter.callback do |presence|
216
- expect(presence).to eql(presence_client_one)
217
- expect(presence_client_one.state).to eq(:entered)
218
- stop_reactor
495
+ it 'calls the Deferrable errback on capabilities failure' do
496
+ restricted_presence.enter(client_id: 'clientId').tap do |deferrable|
497
+ deferrable.callback { raise "Should not succeed" }
498
+ deferrable.errback { stop_reactor }
499
+ end
219
500
  end
220
501
  end
502
+
503
+ it_should_behave_like 'a public presence method', :enter, :entered, {}
221
504
  end
222
505
 
223
506
  context '#update' do
@@ -266,22 +549,7 @@ describe Ably::Realtime::Presence, :event_machine do
266
549
  end
267
550
  end
268
551
 
269
- it 'returns a Deferrable' do
270
- presence_client_one.enter do
271
- expect(presence_client_one.update).to be_a(EventMachine::Deferrable)
272
- stop_reactor
273
- end
274
- end
275
-
276
- it 'calls the Deferrable callback on success' do
277
- presence_client_one.enter do
278
- presence_client_one.update.callback do |presence|
279
- expect(presence).to eql(presence_client_one)
280
- expect(presence_client_one.state).to eq(:entered)
281
- stop_reactor
282
- end
283
- end
284
- end
552
+ it_should_behave_like 'a public presence method', :update, :entered, {}, enter_first: true
285
553
  end
286
554
 
287
555
  context '#leave' do
@@ -334,22 +602,7 @@ describe Ably::Realtime::Presence, :event_machine do
334
602
  stop_reactor
335
603
  end
336
604
 
337
- it 'returns a Deferrable' do
338
- presence_client_one.enter do
339
- expect(presence_client_one.leave).to be_a(EventMachine::Deferrable)
340
- stop_reactor
341
- end
342
- end
343
-
344
- it 'calls the Deferrable callback on success' do
345
- presence_client_one.enter do
346
- presence_client_one.leave.callback do |presence|
347
- expect(presence).to eql(presence_client_one)
348
- expect(presence_client_one.state).to eq(:left)
349
- stop_reactor
350
- end
351
- end
352
- end
605
+ it_should_behave_like 'a public presence method', :leave, :left, {}, enter_first: true
353
606
  end
354
607
 
355
608
  context ':left event' do
@@ -415,15 +668,36 @@ describe Ably::Realtime::Presence, :event_machine do
415
668
  end
416
669
  end
417
670
 
418
- it 'returns a Deferrable' do
419
- expect(presence_client_one.enter_client('client_id')).to be_a(EventMachine::Deferrable)
420
- stop_reactor
671
+ context 'message #connection_id' do
672
+ let(:client_id) { random_str }
673
+
674
+ it 'matches the current client connection_id' do
675
+ channel_client_two.attach do
676
+ presence_client_one.enter_client(client_id)
677
+ end
678
+
679
+ presence_client_two.subscribe do |presence|
680
+ expect(presence.client_id).to eq(client_id)
681
+ expect(presence.connection_id).to eq(client_one.connection.id)
682
+ stop_reactor
683
+ end
684
+ end
421
685
  end
422
686
 
423
- it 'calls the Deferrable callback on success' do
424
- presence_client_one.enter_client('client_id').callback do |presence|
425
- expect(presence).to eql(presence_client_one)
426
- stop_reactor
687
+ it_should_behave_like 'a public presence method', :enter_client, nil, 'client_id'
688
+
689
+ context 'without necessary capabilities to enter on behalf of another client' do
690
+ let(:restricted_client) do
691
+ Ably::Realtime::Client.new(default_options.merge(api_key: restricted_api_key, log_level: :fatal))
692
+ end
693
+ let(:restricted_channel) { restricted_client.channel("cansubscribe:channel") }
694
+ let(:restricted_presence) { restricted_channel.presence }
695
+
696
+ it 'calls the Deferrable errback on capabilities failure' do
697
+ restricted_presence.enter_client('clientId').tap do |deferrable|
698
+ deferrable.callback { raise "Should not succeed" }
699
+ deferrable.errback { stop_reactor }
700
+ end
427
701
  end
428
702
  end
429
703
  end
@@ -489,17 +763,7 @@ describe Ably::Realtime::Presence, :event_machine do
489
763
  end
490
764
  end
491
765
 
492
- it 'returns a Deferrable' do
493
- expect(presence_client_one.update_client('client_id')).to be_a(EventMachine::Deferrable)
494
- stop_reactor
495
- end
496
-
497
- it 'calls the Deferrable callback on success' do
498
- presence_client_one.update_client('client_id').callback do |presence|
499
- expect(presence).to eql(presence_client_one)
500
- stop_reactor
501
- end
502
- end
766
+ it_should_behave_like 'a public presence method', :update_client, nil, 'client_id'
503
767
  end
504
768
 
505
769
  context '#leave_client' do
@@ -592,23 +856,13 @@ describe Ably::Realtime::Presence, :event_machine do
592
856
  end
593
857
  end
594
858
 
595
- it 'returns a Deferrable' do
596
- expect(presence_client_one.leave_client('client_id')).to be_a(EventMachine::Deferrable)
597
- stop_reactor
598
- end
599
-
600
- it 'calls the Deferrable callback on success' do
601
- presence_client_one.leave_client('client_id').callback do |presence|
602
- expect(presence).to eql(presence_client_one)
603
- stop_reactor
604
- end
605
- end
859
+ it_should_behave_like 'a public presence method', :leave_client, nil, 'client_id'
606
860
  end
607
861
  end
608
862
 
609
863
  context '#get' do
610
- it 'returns a Deferrable' do
611
- expect(presence_client_one.get).to be_a(EventMachine::Deferrable)
864
+ it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
865
+ expect(presence_client_one.get).to be_a(Ably::Util::SafeDeferrable)
612
866
  stop_reactor
613
867
  end
614
868
 
@@ -619,6 +873,89 @@ describe Ably::Realtime::Presence, :event_machine do
619
873
  end
620
874
  end
621
875
 
876
+ it 'catches exceptions in the provided method block' do
877
+ expect(presence_client_one.logger).to receive(:error).with(/Intentional exception/) do
878
+ stop_reactor
879
+ end
880
+ presence_client_one.get { raise 'Intentional exception' }
881
+ end
882
+
883
+ %w(detached failed).each do |state|
884
+ it "raise an exception if the channel is #{state}" do
885
+ channel_client_one.attach do
886
+ channel_client_one.change_state state.to_sym
887
+ expect { presence_client_one.get }.to raise_error Ably::Exceptions::IncompatibleStateForOperation, /Operation is not allowed when channel is in STATE.#{state}/i
888
+ stop_reactor
889
+ end
890
+ end
891
+ end
892
+
893
+ context 'during a sync' do
894
+ let(:pages) { 2 }
895
+ let(:members_per_page) { 100 }
896
+ let(:sync_pages_received) { [] }
897
+ let(:client_options) { default_options.merge(log_level: :none) }
898
+
899
+ def connect_members_deferrables
900
+ (members_per_page * pages + 1).times.map do |index|
901
+ presence_client_one.enter_client("client:#{index}")
902
+ end
903
+ end
904
+
905
+ before do
906
+ # Reconfigure client library so that it makes no retry attempts and fails immediately
907
+ stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
908
+ Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
909
+ disconnected: { retry_every: 0.1, max_time_in_state: 0 },
910
+ suspended: { retry_every: 0.1, max_time_in_state: 0 }
911
+ )
912
+ end
913
+
914
+ it 'fails if the connection fails' do
915
+ when_all(*connect_members_deferrables) do
916
+ channel_client_two.attach do
917
+ client_two.connection.transport.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
918
+ if protocol_message.action == :sync
919
+ sync_pages_received << protocol_message
920
+ force_connection_failure client_two if sync_pages_received.count == 1
921
+ end
922
+ end
923
+ end
924
+
925
+ presence_client_two.get.tap do |deferrable|
926
+ deferrable.callback { raise 'Get should not succeed' }
927
+ deferrable.errback do |error|
928
+ stop_reactor
929
+ end
930
+ end
931
+ end
932
+ end
933
+
934
+ it 'fails if the channel is detached' do
935
+ when_all(*connect_members_deferrables) do
936
+ channel_client_two.attach do
937
+ client_two.connection.transport.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
938
+ if protocol_message.action == :sync
939
+ # prevent any more SYNC messages coming through
940
+ client_two.connection.transport.__incoming_protocol_msgbus__.unsubscribe
941
+ channel_client_two.change_state :detaching
942
+ channel_client_two.change_state :detached
943
+ end
944
+ end
945
+ end
946
+
947
+ presence_client_two.get.tap do |deferrable|
948
+ deferrable.callback { raise 'Get should not succeed' }
949
+ deferrable.errback do |error|
950
+ stop_reactor
951
+ end
952
+ end
953
+ end
954
+ end
955
+ end
956
+
957
+ # skip 'it fails if the connection changes to failed state'
958
+
622
959
  it 'returns the current members on the channel' do
623
960
  presence_client_one.enter do
624
961
  presence_client_one.get do |members|
@@ -753,9 +1090,11 @@ describe Ably::Realtime::Presence, :event_machine do
753
1090
  stop_reactor
754
1091
  end
755
1092
 
756
- presence_client_one.enter
757
- presence_client_one.update
758
- presence_client_one.leave
1093
+ presence_client_one.enter do
1094
+ presence_client_one.update do
1095
+ presence_client_one.leave
1096
+ end
1097
+ end
759
1098
  end
760
1099
  end
761
1100
  end
@@ -992,10 +1331,31 @@ describe Ably::Realtime::Presence, :event_machine do
992
1331
  end
993
1332
  end
994
1333
 
995
- skip 'ensure connection_id is unique and updated on ENTER'
996
- skip 'ensure connection_id for presence member matches the messages they publish on the channel'
997
- skip 'stop a call to get when the channel has not been entered'
998
- skip 'stop a call to get when the channel has been entered but the list is not up to date'
999
- skip 'presence will resume sync if connection is dropped mid-way'
1334
+ context 'connection failure mid-way through a large member sync' do
1335
+ let(:members_count) { 400 }
1336
+ let(:sync_pages_received) { [] }
1337
+
1338
+ # Will re-enable once https://github.com/ably/realtime/issues/91 is resolved
1339
+ skip 'resumes the SYNC operation', em_timeout: 15 do
1340
+ when_all(*members_count.times.map do |index|
1341
+ presence_client_one.enter_client("client:#{index}")
1342
+ end) do
1343
+ channel_client_two.attach do
1344
+ client_two.connection.transport.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
1345
+ if protocol_message.action == :sync
1346
+ sync_pages_received << protocol_message
1347
+ force_connection_failure client_two if sync_pages_received.count == 2
1348
+ end
1349
+ end
1350
+ end
1351
+
1352
+ presence_client_two.get do |members|
1353
+ expect(members.count).to eql(members_count)
1354
+ expect(members.map(&:member_key).uniq.count).to eql(members_count)
1355
+ stop_reactor
1356
+ end
1357
+ end
1358
+ end
1359
+ end
1000
1360
  end
1001
1361
  end