ably-rest 0.7.3 → 0.7.5

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 (69) hide show
  1. checksums.yaml +8 -8
  2. data/.travis.yml +1 -0
  3. data/SPEC.md +480 -472
  4. data/lib/ably-rest.rb +1 -1
  5. data/lib/submodules/ably-ruby/LICENSE.txt +1 -1
  6. data/lib/submodules/ably-ruby/README.md +107 -24
  7. data/lib/submodules/ably-ruby/SPEC.md +531 -398
  8. data/lib/submodules/ably-ruby/lib/ably/auth.rb +24 -16
  9. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +9 -0
  10. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +17 -9
  11. data/lib/submodules/ably-ruby/lib/ably/models/paginated_resource.rb +12 -8
  12. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +18 -10
  13. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +15 -4
  14. data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +4 -3
  15. data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +31 -2
  16. data/lib/submodules/ably-ruby/lib/ably/modules/message_emitter.rb +77 -0
  17. data/lib/submodules/ably-ruby/lib/ably/modules/safe_deferrable.rb +71 -0
  18. data/lib/submodules/ably-ruby/lib/ably/modules/safe_yield.rb +41 -0
  19. data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +28 -8
  20. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +0 -5
  21. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +24 -29
  22. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +54 -11
  23. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +21 -6
  24. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +7 -2
  25. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +29 -26
  26. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +4 -4
  27. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +41 -9
  28. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +72 -24
  29. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_state_machine.rb +26 -4
  30. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +19 -6
  31. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +74 -208
  32. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +264 -0
  33. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_manager.rb +59 -0
  34. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_state_machine.rb +64 -0
  35. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +1 -1
  36. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +6 -2
  37. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -1
  38. data/lib/submodules/ably-ruby/lib/ably/util/pub_sub.rb +3 -1
  39. data/lib/submodules/ably-ruby/lib/ably/util/safe_deferrable.rb +18 -0
  40. data/lib/submodules/ably-ruby/lib/ably/version.rb +1 -1
  41. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +2 -2
  42. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +28 -6
  43. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +116 -46
  44. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +55 -10
  45. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +32 -0
  46. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +456 -96
  47. data/lib/submodules/ably-ruby/spec/acceptance/realtime/stats_spec.rb +2 -2
  48. data/lib/submodules/ably-ruby/spec/acceptance/realtime/time_spec.rb +2 -2
  49. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +96 -7
  50. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +8 -0
  51. data/lib/submodules/ably-ruby/spec/shared/safe_deferrable_behaviour.rb +71 -0
  52. data/lib/submodules/ably-ruby/spec/support/api_helper.rb +1 -1
  53. data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +1 -1
  54. data/lib/submodules/ably-ruby/spec/support/test_app.rb +13 -7
  55. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +15 -14
  56. data/lib/submodules/ably-ruby/spec/unit/models/paginated_resource_spec.rb +4 -4
  57. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +17 -17
  58. data/lib/submodules/ably-ruby/spec/unit/models/stat_spec.rb +4 -4
  59. data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +28 -9
  60. data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +50 -0
  61. data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +76 -2
  62. data/lib/submodules/ably-ruby/spec/unit/realtime/channel_spec.rb +51 -20
  63. data/lib/submodules/ably-ruby/spec/unit/realtime/channels_spec.rb +3 -3
  64. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +30 -0
  65. data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +52 -26
  66. data/lib/submodules/ably-ruby/spec/unit/realtime/safe_deferrable_spec.rb +12 -0
  67. data/spec/spec_helper.rb +5 -0
  68. metadata +12 -4
  69. data/lib/submodules/ably-ruby/.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