ably 0.8.15 → 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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -4
  3. data/CHANGELOG.md +6 -2
  4. data/README.md +5 -1
  5. data/SPEC.md +1473 -852
  6. data/ably.gemspec +11 -8
  7. data/lib/ably/auth.rb +90 -53
  8. data/lib/ably/exceptions.rb +37 -8
  9. data/lib/ably/logger.rb +10 -1
  10. data/lib/ably/models/auth_details.rb +42 -0
  11. data/lib/ably/models/channel_state_change.rb +18 -4
  12. data/lib/ably/models/connection_details.rb +6 -3
  13. data/lib/ably/models/connection_state_change.rb +4 -3
  14. data/lib/ably/models/error_info.rb +1 -1
  15. data/lib/ably/models/message.rb +17 -1
  16. data/lib/ably/models/message_encoders/base.rb +103 -82
  17. data/lib/ably/models/message_encoders/base64.rb +1 -1
  18. data/lib/ably/models/presence_message.rb +16 -1
  19. data/lib/ably/models/protocol_message.rb +20 -3
  20. data/lib/ably/models/token_details.rb +11 -1
  21. data/lib/ably/models/token_request.rb +16 -6
  22. data/lib/ably/modules/async_wrapper.rb +7 -3
  23. data/lib/ably/modules/encodeable.rb +51 -12
  24. data/lib/ably/modules/enum.rb +17 -7
  25. data/lib/ably/modules/event_emitter.rb +29 -14
  26. data/lib/ably/modules/model_common.rb +13 -21
  27. data/lib/ably/modules/state_emitter.rb +7 -4
  28. data/lib/ably/modules/state_machine.rb +2 -4
  29. data/lib/ably/modules/uses_state_machine.rb +7 -3
  30. data/lib/ably/realtime.rb +2 -0
  31. data/lib/ably/realtime/auth.rb +102 -42
  32. data/lib/ably/realtime/channel.rb +68 -26
  33. data/lib/ably/realtime/channel/channel_manager.rb +154 -65
  34. data/lib/ably/realtime/channel/channel_state_machine.rb +14 -15
  35. data/lib/ably/realtime/client.rb +18 -3
  36. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +38 -29
  37. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +6 -1
  38. data/lib/ably/realtime/connection.rb +108 -49
  39. data/lib/ably/realtime/connection/connection_manager.rb +167 -61
  40. data/lib/ably/realtime/connection/connection_state_machine.rb +22 -3
  41. data/lib/ably/realtime/connection/websocket_transport.rb +19 -10
  42. data/lib/ably/realtime/presence.rb +70 -45
  43. data/lib/ably/realtime/presence/members_map.rb +201 -36
  44. data/lib/ably/realtime/presence/presence_manager.rb +30 -6
  45. data/lib/ably/realtime/presence/presence_state_machine.rb +5 -12
  46. data/lib/ably/rest.rb +2 -2
  47. data/lib/ably/rest/channel.rb +5 -5
  48. data/lib/ably/rest/client.rb +31 -27
  49. data/lib/ably/rest/middleware/exceptions.rb +1 -3
  50. data/lib/ably/rest/middleware/logger.rb +2 -2
  51. data/lib/ably/rest/presence.rb +2 -2
  52. data/lib/ably/util/pub_sub.rb +1 -1
  53. data/lib/ably/util/safe_deferrable.rb +26 -0
  54. data/lib/ably/version.rb +2 -2
  55. data/spec/acceptance/realtime/auth_spec.rb +470 -111
  56. data/spec/acceptance/realtime/channel_history_spec.rb +5 -3
  57. data/spec/acceptance/realtime/channel_spec.rb +1017 -168
  58. data/spec/acceptance/realtime/client_spec.rb +6 -6
  59. data/spec/acceptance/realtime/connection_failures_spec.rb +458 -27
  60. data/spec/acceptance/realtime/connection_spec.rb +424 -105
  61. data/spec/acceptance/realtime/message_spec.rb +52 -23
  62. data/spec/acceptance/realtime/presence_history_spec.rb +5 -3
  63. data/spec/acceptance/realtime/presence_spec.rb +1110 -96
  64. data/spec/acceptance/rest/auth_spec.rb +222 -59
  65. data/spec/acceptance/rest/base_spec.rb +1 -1
  66. data/spec/acceptance/rest/channel_spec.rb +1 -2
  67. data/spec/acceptance/rest/client_spec.rb +104 -48
  68. data/spec/acceptance/rest/message_spec.rb +42 -15
  69. data/spec/acceptance/rest/presence_spec.rb +4 -11
  70. data/spec/rspec_config.rb +2 -1
  71. data/spec/shared/client_initializer_behaviour.rb +2 -2
  72. data/spec/shared/safe_deferrable_behaviour.rb +6 -2
  73. data/spec/spec_helper.rb +4 -2
  74. data/spec/support/debug_failure_helper.rb +20 -4
  75. data/spec/support/event_machine_helper.rb +32 -1
  76. data/spec/unit/auth_spec.rb +4 -11
  77. data/spec/unit/logger_spec.rb +28 -2
  78. data/spec/unit/models/auth_details_spec.rb +49 -0
  79. data/spec/unit/models/channel_state_change_spec.rb +23 -3
  80. data/spec/unit/models/connection_details_spec.rb +12 -1
  81. data/spec/unit/models/connection_state_change_spec.rb +15 -4
  82. data/spec/unit/models/message_encoders/base64_spec.rb +2 -1
  83. data/spec/unit/models/message_spec.rb +153 -0
  84. data/spec/unit/models/presence_message_spec.rb +192 -0
  85. data/spec/unit/models/protocol_message_spec.rb +64 -6
  86. data/spec/unit/models/token_details_spec.rb +75 -0
  87. data/spec/unit/models/token_request_spec.rb +74 -0
  88. data/spec/unit/modules/async_wrapper_spec.rb +2 -1
  89. data/spec/unit/modules/enum_spec.rb +69 -0
  90. data/spec/unit/modules/event_emitter_spec.rb +149 -22
  91. data/spec/unit/modules/state_emitter_spec.rb +9 -3
  92. data/spec/unit/realtime/client_spec.rb +1 -1
  93. data/spec/unit/realtime/connection_spec.rb +8 -5
  94. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +1 -1
  95. data/spec/unit/realtime/presence_spec.rb +4 -3
  96. data/spec/unit/rest/client_spec.rb +1 -1
  97. data/spec/unit/util/crypto_spec.rb +3 -3
  98. metadata +22 -19
@@ -206,9 +206,11 @@ describe Ably::Realtime::Channel, '#history', :event_machine do
206
206
  end
207
207
  end
208
208
 
209
- it 'raises an exception unless state is attached' do
210
- expect { channel.history(until_attach: true) }.to raise_error(ArgumentError, /not attached/)
211
- stop_reactor
209
+ it 'fails the deferrable unless the state is attached' do
210
+ channel.history(until_attach: true).errback do |error|
211
+ expect(error.message).to match(/not attached/)
212
+ stop_reactor
213
+ end
212
214
  end
213
215
  end
214
216
  end
@@ -7,11 +7,16 @@ describe Ably::Realtime::Channel, :event_machine do
7
7
  let(:client_options) { default_options }
8
8
 
9
9
  let(:client) { auto_close Ably::Realtime::Client.new(client_options) }
10
+ let(:connection) { client.connection }
10
11
  let(:channel_name) { random_str }
11
12
  let(:payload) { random_str }
12
13
  let(:channel) { client.channel(channel_name) }
13
14
  let(:messages) { [] }
14
15
 
16
+ def disconnect_transport
17
+ connection.transport.unbind
18
+ end
19
+
15
20
  describe 'initialization' do
16
21
  context 'with :auto_connect option set to false on connection' do
17
22
  let(:client) do
@@ -36,41 +41,98 @@ describe Ably::Realtime::Channel, :event_machine do
36
41
  end
37
42
 
38
43
  describe '#attach' do
39
- it 'emits attaching then attached events' do
40
- channel.once(:attaching) do
41
- channel.once(:attached) do
42
- stop_reactor
44
+ context 'when initialized' do
45
+ it 'emits attaching then attached events' do
46
+ channel.once(:attaching) do
47
+ channel.once(:attached) do
48
+ stop_reactor
49
+ end
43
50
  end
51
+
52
+ channel.attach
44
53
  end
45
54
 
46
- channel.attach
47
- end
55
+ it 'ignores subsequent #attach calls but calls the success callback if provided' do
56
+ channel.once(:attaching) do
57
+ channel.attach
58
+ channel.once(:attached) do
59
+ channel.attach do
60
+ stop_reactor
61
+ end
62
+ end
63
+ end
48
64
 
49
- it 'ignores subsequent #attach calls but calls the success callback if provided' do
50
- channel.once(:attaching) do
51
65
  channel.attach
52
- channel.once(:attached) do
66
+ end
67
+
68
+ it 'attaches to a channel' do
69
+ channel.attach
70
+ channel.on(:attached) do
71
+ expect(channel.state).to eq(:attached)
72
+ stop_reactor
73
+ end
74
+ end
75
+
76
+ it 'attaches to a channel and calls the provided block (#RTL4d)' do
77
+ channel.attach do
78
+ expect(channel.state).to eq(:attached)
79
+ stop_reactor
80
+ end
81
+ end
82
+
83
+ it 'sends an ATTACH and waits for an ATTACHED (#RTL4c)' do
84
+ connection.once(:connected) do
85
+ attach_count = 0
86
+ attached_count = 0
87
+ test_complete = false
88
+ client.connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
89
+ next if test_complete
90
+ attached_count += 1 if protocol_message.action == :attached
91
+ end
92
+ client.connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
93
+ next if test_complete
94
+ attach_count += 1 if protocol_message.action == :attach
95
+ end
53
96
  channel.attach do
54
- stop_reactor
97
+ EventMachine.add_timer(1) do
98
+ test_complete = true
99
+ expect(attach_count).to eql(1)
100
+ expect(attached_count).to eql(1)
101
+ stop_reactor
102
+ end
55
103
  end
56
104
  end
57
105
  end
58
106
 
59
- channel.attach
60
- end
61
-
62
- it 'attaches to a channel' do
63
- channel.attach
64
- channel.on(:attached) do
65
- expect(channel.state).to eq(:attached)
66
- stop_reactor
107
+ it 'implicitly attaches the channel (#RTL7c)' do
108
+ expect(channel).to be_initialized
109
+ channel.subscribe { |message| }
110
+ channel.once(:attached) do
111
+ stop_reactor
112
+ end
67
113
  end
68
- end
69
114
 
70
- it 'attaches to a channel and calls the provided block' do
71
- channel.attach do
72
- expect(channel.state).to eq(:attached)
73
- stop_reactor
115
+ context 'when the implicit channel attach fails' do
116
+ let(:allowed_params) do
117
+ { capability: { "*" => ["*"] } }
118
+ end
119
+ let(:not_allowed_params) do
120
+ { capability: { "only_this_channel" => ["*"] } }
121
+ end
122
+ let(:client_options) { default_options.merge(default_token_params: not_allowed_params, use_token_auth: true, log_level: :fatal) }
123
+
124
+ it 'registers the listener anyway (#RTL7c)' do
125
+ channel.subscribe do |message|
126
+ stop_reactor
127
+ end
128
+ channel.once(:failed) do
129
+ client.auth.authorize(allowed_params) do
130
+ channel.attach do
131
+ channel.publish 'foo'
132
+ end
133
+ end
134
+ end
135
+ end
74
136
  end
75
137
  end
76
138
 
@@ -79,7 +141,7 @@ describe Ably::Realtime::Channel, :event_machine do
79
141
  stop_reactor
80
142
  end
81
143
 
82
- it 'calls the SafeDeferrable callback on success' do
144
+ it 'calls the SafeDeferrable callback on success (#RTL4d)' do
83
145
  channel.attach.callback do
84
146
  expect(channel).to be_a(Ably::Realtime::Channel)
85
147
  expect(channel.state).to eq(:attached)
@@ -87,32 +149,92 @@ describe Ably::Realtime::Channel, :event_machine do
87
149
  end
88
150
  end
89
151
 
152
+ context 'when an ATTACHED acknowledge is not received on the current connection' do
153
+ # As soon as the client sends the ATTACH on a CONNECTED connection
154
+ # simulate a transport failure that triggers the DISCONNECTED state twice
155
+ it 'sends another ATTACH each time the connection becomes connected' do
156
+ attached_messages = []
157
+ client.connection.__outgoing_protocol_msgbus__.on(:protocol_message) do |protocol_message|
158
+ if protocol_message.action == :attach
159
+ attached_messages << protocol_message
160
+ if attached_messages.count < 3
161
+ EventMachine.next_tick do
162
+ disconnect_transport
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ connection.once(:connected) do
169
+ connection.once(:disconnected) do
170
+ expect(attached_messages.count).to eql(1)
171
+ connection.once(:disconnected) do
172
+ expect(attached_messages.count).to eql(2)
173
+ connection.once(:connected) do
174
+ EventMachine.add_timer(0.1) do
175
+ expect(attached_messages.count).to eql(3)
176
+ end
177
+ end
178
+ end
179
+ end
180
+ channel.attach
181
+ end
182
+
183
+ channel.once(:attached) do
184
+ EventMachine.add_timer(1) do
185
+ expect(attached_messages.count).to eql(3)
186
+ stop_reactor
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ context 'when state is :attached' do
193
+ it 'does nothing (#RTL4a)' do
194
+ channel.attach do
195
+ stopping = false
196
+ client.connection.__outgoing_protocol_msgbus__.once(:protocol_message) do |protocol_message|
197
+ raise "No outgoing messages should be sent as already ATTACHED" unless stopping
198
+ end
199
+ 5.times do |index|
200
+ EventMachine.add_timer(0.2 * index) { channel.attach }
201
+ end
202
+ EventMachine.add_timer(1.5) do
203
+ stopping = true
204
+ stop_reactor
205
+ end
206
+ end
207
+ end
208
+ end
209
+
90
210
  context 'when state is :failed' do
91
211
  let(:client_options) { default_options.merge(log_level: :fatal) }
92
212
 
93
- it 'reattaches' do
213
+ it 'reattaches and sets the errorReason to nil (#RTL4g)' do
94
214
  channel.attach do
95
- channel.once(:failed) do
96
- expect(channel).to be_failed
97
- channel.attach do
98
- expect(channel).to be_attached
99
- stop_reactor
100
- end
101
- end
102
215
  channel.transition_state_machine :failed, reason: RuntimeError.new
216
+ expect(channel).to be_failed
217
+ expect(channel.error_reason).to_not be_nil
218
+ channel.attach do
219
+ expect(channel).to be_attached
220
+ expect(channel.error_reason).to be_nil
221
+ stop_reactor
222
+ end
103
223
  end
104
224
  end
105
225
  end
106
226
 
107
227
  context 'when state is :detaching' do
108
- it 'moves straight to attaching and skips detached' do
228
+ it 'does the attach operation after the completion of the pending request (#RTL4h)' do
109
229
  channel.once(:detaching) do
110
- channel.once(:detached) { raise 'Detach should not have been reached' }
111
-
112
- channel.once(:attaching) do
113
- channel.once(:attached) do
114
- channel.off
115
- stop_reactor
230
+ channel.once(:detached) do
231
+ channel.once(:attaching) do
232
+ channel.once(:attached) do
233
+ EventMachine.add_timer(1) do
234
+ expect(channel).to be_attached
235
+ stop_reactor
236
+ end
237
+ end
116
238
  end
117
239
  end
118
240
 
@@ -149,12 +271,21 @@ describe Ably::Realtime::Channel, :event_machine do
149
271
  end
150
272
 
151
273
  context 'failure as a result of insufficient key permissions' do
274
+ let(:auth_options) do
275
+ default_options.merge(
276
+ key: restricted_api_key,
277
+ log_level: :fatal,
278
+ use_token_auth: true,
279
+ # TODO: Use wildcard / default when intersection issue resolved, realtime#780
280
+ default_token_params: { capability: { "canpublish:foo" => ["publish"] } }
281
+ )
282
+ end
152
283
  let(:restricted_client) do
153
- auto_close Ably::Realtime::Client.new(default_options.merge(key: restricted_api_key, log_level: :fatal))
284
+ auto_close Ably::Realtime::Client.new(auth_options)
154
285
  end
155
- let(:restricted_channel) { restricted_client.channel("cannot_subscribe") }
286
+ let(:restricted_channel) { restricted_client.channel("cansubscribe:foo") }
156
287
 
157
- it 'emits failed event' do
288
+ it 'emits failed event (#RTL4e)' do
158
289
  restricted_channel.attach
159
290
  restricted_channel.on(:failed) do |connection_state|
160
291
  expect(restricted_channel.state).to eq(:failed)
@@ -163,7 +294,7 @@ describe Ably::Realtime::Channel, :event_machine do
163
294
  end
164
295
  end
165
296
 
166
- it 'calls the errback of the returned Deferrable' do
297
+ it 'calls the errback of the returned Deferrable (#RTL4d)' do
167
298
  restricted_channel.attach.errback do |error|
168
299
  expect(restricted_channel.state).to eq(:failed)
169
300
  expect(error.status).to eq(401)
@@ -171,15 +302,6 @@ describe Ably::Realtime::Channel, :event_machine do
171
302
  end
172
303
  end
173
304
 
174
- it 'emits an error event' do
175
- restricted_channel.attach
176
- restricted_channel.on(:error) do |error|
177
- expect(restricted_channel.state).to eq(:failed)
178
- expect(error.status).to eq(401)
179
- stop_reactor
180
- end
181
- end
182
-
183
305
  it 'updates the error_reason' do
184
306
  restricted_channel.attach
185
307
  restricted_channel.on(:failed) do
@@ -193,10 +315,8 @@ describe Ably::Realtime::Channel, :event_machine do
193
315
  restricted_channel.attach
194
316
  restricted_channel.once(:failed) do
195
317
  restricted_client.close do
196
- # A direct call to #authorise is synchronous
197
- restricted_client.auth.authorise({}, key: api_key)
198
-
199
- restricted_client.connect do
318
+ token_params = { capability: { "cansubscribe:foo" => ["subscribe"] } }
319
+ restricted_client.auth.authorize(token_params) do
200
320
  restricted_channel.once(:attached) do
201
321
  expect(restricted_channel.error_reason).to be_nil
202
322
  stop_reactor
@@ -208,98 +328,149 @@ describe Ably::Realtime::Channel, :event_machine do
208
328
  end
209
329
  end
210
330
  end
211
- end
212
331
 
213
- describe '#detach' do
214
- it 'detaches from a channel' do
215
- channel.attach do
216
- channel.detach
217
- channel.on(:detached) do
218
- expect(channel.state).to eq(:detached)
332
+ context 'with connection state' do
333
+ it 'is initialized (#RTL4i)' do
334
+ expect(connection).to be_initialized
335
+ channel.attach do
219
336
  stop_reactor
220
337
  end
221
338
  end
222
- end
223
339
 
224
- it 'detaches from a channel and calls the provided block' do
225
- channel.attach do
226
- expect(channel.state).to eq(:attached)
227
- channel.detach do
228
- expect(channel.state).to eq(:detached)
229
- stop_reactor
340
+ it 'is connecting (#RTL4i)' do
341
+ connection.once(:connecting) do
342
+ channel.attach do
343
+ stop_reactor
344
+ end
345
+ end
346
+ end
347
+
348
+ it 'is disconnected (#RTL4i)' do
349
+ connection.once(:connected) do
350
+ connection.once(:disconnected) do
351
+ channel.attach do
352
+ stop_reactor
353
+ end
354
+ end
355
+ disconnect_transport
230
356
  end
231
357
  end
232
358
  end
359
+ end
233
360
 
234
- it 'emits :detaching then :detached events' do
235
- channel.once(:detaching) do
236
- channel.once(:detached) do
237
- stop_reactor
361
+ describe '#detach' do
362
+ context 'when state is :attached' do
363
+ it 'it detaches from a channel (#RTL5d)' do
364
+ channel.attach do
365
+ channel.detach
366
+ channel.on(:detached) do
367
+ expect(channel.state).to eq(:detached)
368
+ stop_reactor
369
+ end
238
370
  end
239
371
  end
240
372
 
241
- channel.attach do
242
- channel.detach
373
+ it 'detaches from a channel and calls the provided block (#RTL5d, #RTL5e)' do
374
+ channel.attach do
375
+ expect(channel.state).to eq(:attached)
376
+ channel.detach do
377
+ expect(channel.state).to eq(:detached)
378
+ stop_reactor
379
+ end
380
+ end
243
381
  end
244
- end
245
382
 
246
- it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
247
- channel.attach do
248
- expect(channel.detach).to be_a(Ably::Util::SafeDeferrable)
249
- stop_reactor
383
+ it 'emits :detaching then :detached events' do
384
+ channel.once(:detaching) do
385
+ channel.once(:detached) do
386
+ stop_reactor
387
+ end
388
+ end
389
+
390
+ channel.attach do
391
+ channel.detach
392
+ end
250
393
  end
251
- end
252
394
 
253
- it 'calls the Deferrable callback on success' do
254
- channel.attach do
255
- channel.detach.callback do
256
- expect(channel).to be_a(Ably::Realtime::Channel)
257
- expect(channel.state).to eq(:detached)
395
+ it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
396
+ channel.attach do
397
+ expect(channel.detach).to be_a(Ably::Util::SafeDeferrable)
258
398
  stop_reactor
259
399
  end
260
400
  end
401
+
402
+ it 'calls the Deferrable callback on success' do
403
+ channel.attach do
404
+ channel.detach.callback do
405
+ expect(channel).to be_a(Ably::Realtime::Channel)
406
+ expect(channel.state).to eq(:detached)
407
+ stop_reactor
408
+ end
409
+ end
410
+ end
411
+
412
+ context 'and DETACHED message is not received within realtime request timeout' do
413
+ let(:request_timeout) { 2 }
414
+ let(:client_options) { default_options.merge(realtime_request_timeout: request_timeout) }
415
+
416
+ it 'fails the deferrable and returns to the previous state (#RTL5f, #RTL5e)' do
417
+ channel.attach do
418
+ # don't process any incoming ProtocolMessages so the channel never becomes detached
419
+ connection.__incoming_protocol_msgbus__.unsubscribe
420
+ detached_requested_at = Time.now.to_i
421
+ channel.detach do
422
+ raise "The detach should not succeed if no incoming protocol messages are processed"
423
+ end.errback do
424
+ expect(channel).to be_attached
425
+ expect(Time.now.to_i - detached_requested_at).to be_within(1).of(request_timeout)
426
+ stop_reactor
427
+ end
428
+ end
429
+ end
430
+ end
261
431
  end
262
432
 
263
433
  context 'when state is :failed' do
264
434
  let(:client_options) { default_options.merge(log_level: :fatal) }
265
435
 
266
- it 'raises an exception' do
436
+ it 'fails the deferrable (#RTL5b)' do
267
437
  channel.attach do
268
438
  channel.transition_state_machine :failed, reason: RuntimeError.new
269
439
  expect(channel).to be_failed
270
- expect { channel.detach }.to raise_error Ably::Exceptions::InvalidStateChange
271
- stop_reactor
440
+ channel.detach.errback do |error|
441
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
442
+ stop_reactor
443
+ end
272
444
  end
273
445
  end
274
446
  end
275
447
 
276
448
  context 'when state is :attaching' do
277
- it 'moves straight to :detaching state and skips :attached' do
278
- channel.once(:attaching) do
279
- channel.once(:attached) { raise 'Attached should never be reached' }
280
-
281
- channel.once(:detaching) do
282
- channel.once(:detached) do
283
- stop_reactor
449
+ it 'waits for the attach to complete and then moves to detached' do
450
+ connection.once(:connected) do
451
+ channel.once(:attaching) do
452
+ reached_attached = false
453
+ channel.once(:attached) do
454
+ channel.once(:detached) do
455
+ stop_reactor
456
+ end
284
457
  end
458
+ channel.detach
285
459
  end
286
-
287
- channel.detach
460
+ channel.attach
288
461
  end
289
-
290
- channel.attach
291
462
  end
292
463
  end
293
464
 
294
465
  context 'when state is :detaching' do
295
- it 'ignores subsequent #detach calls but calls the callback if provided' do
466
+ it 'ignores subsequent #detach calls but calls the callback if provided (#RTL5i)' do
296
467
  channel.once(:detaching) do
297
- channel.detach
298
468
  channel.once(:detached) do
299
469
  channel.detach do
300
470
  stop_reactor
301
471
  end
302
472
  end
473
+ channel.detach
303
474
  end
304
475
 
305
476
  channel.attach do
@@ -308,8 +479,28 @@ describe Ably::Realtime::Channel, :event_machine do
308
479
  end
309
480
  end
310
481
 
482
+ context 'when state is :suspended' do
483
+ it 'moves the channel state immediately to DETACHED state (#RTL5j)' do
484
+ channel.attach do
485
+ channel.once(:suspended) do
486
+ channel.on do |channel_state_change|
487
+ expect(channel_state_change.current).to eq(:detached)
488
+ expect(channel.state).to eq(:detached)
489
+ EventMachine.add_timer(1) do
490
+ stop_reactor
491
+ end
492
+ end
493
+ EventMachine.next_tick do
494
+ channel.detach
495
+ end
496
+ end
497
+ channel.transition_state_machine :suspended
498
+ end
499
+ end
500
+ end
501
+
311
502
  context 'when state is :initialized' do
312
- it 'does nothing as there is no channel to detach' do
503
+ it 'does nothing as there is no channel to detach (#RTL5a)' do
313
504
  expect(channel).to be_initialized
314
505
  channel.detach do
315
506
  expect(channel).to be_initialized
@@ -325,14 +516,210 @@ describe Ably::Realtime::Channel, :event_machine do
325
516
  end
326
517
  end
327
518
  end
519
+
520
+ context 'when state is :detached' do
521
+ it 'does nothing as the channel is detached (#RTL5a)' do
522
+ channel.attach do
523
+ channel.detach do
524
+ expect(channel).to be_detached
525
+ channel.on do
526
+ raise "Channel state should not change when calling detached if already detached"
527
+ end
528
+ channel.detach do
529
+ EventMachine.add_timer(1) { stop_reactor }
530
+ end
531
+ end
532
+ end
533
+ end
534
+ end
535
+
536
+ context 'when connection state is' do
537
+ context 'closing' do
538
+ it 'fails the deferrable (#RTL5b)' do
539
+ connection.once(:connected) do
540
+ channel.attach do
541
+ connection.once(:closing) do
542
+ channel.detach.errback do |error|
543
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
544
+ stop_reactor
545
+ end
546
+ end
547
+ connection.close
548
+ end
549
+ end
550
+ end
551
+ end
552
+
553
+ context 'failed and channel is failed' do
554
+ let(:client_options) do
555
+ default_options.merge(log_level: :none)
556
+ end
557
+
558
+ it 'fails the deferrable (#RTL5b)' do
559
+ connection.once(:connected) do
560
+ channel.attach do
561
+ connection.once(:failed) do
562
+ expect(channel).to be_failed
563
+ channel.detach.errback do |error|
564
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
565
+ stop_reactor
566
+ end
567
+ end
568
+ error = Ably::Exceptions::ConnectionFailed.new('forced failure', 500, 50000)
569
+ client.connection.manager.error_received_from_server error
570
+ end
571
+ end
572
+ end
573
+ end
574
+
575
+ context 'failed and channel is detached' do
576
+ let(:client_options) do
577
+ default_options.merge(log_level: :none)
578
+ end
579
+
580
+ it 'fails the deferrable (#RTL5b)' do
581
+ connection.once(:connected) do
582
+ channel.attach do
583
+ channel.detach do
584
+ connection.once(:failed) do
585
+ expect(channel).to be_detached
586
+ channel.detach.errback do |error|
587
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
588
+ stop_reactor
589
+ end
590
+ end
591
+ error = Ably::Exceptions::ConnectionFailed.new('forced failure', 500, 50000)
592
+ client.connection.manager.error_received_from_server error
593
+ end
594
+ end
595
+ end
596
+ end
597
+ end
598
+
599
+ context 'initialized' do
600
+ it 'does the detach operation once the connection state is connected (#RTL5h)' do
601
+ expect(connection).to be_initialized
602
+ channel.attach
603
+ channel.detach
604
+ connection.once(:connected) do
605
+ channel.once(:attached) do
606
+ channel.once(:detached) do
607
+ stop_reactor
608
+ end
609
+ end
610
+ end
611
+ end
612
+ end
613
+
614
+ context 'connecting' do
615
+ it 'does the detach operation once the connection state is connected (#RTL5h)' do
616
+ connection.once(:connecting) do
617
+ channel.attach
618
+ channel.detach
619
+ connection.once(:connected) do
620
+ channel.once(:attached) do
621
+ channel.once(:detached) do
622
+ stop_reactor
623
+ end
624
+ end
625
+ end
626
+ end
627
+ end
628
+ end
629
+
630
+ context 'disconnected' do
631
+ let(:client_options) do
632
+ default_options.merge(log_level: :fatal)
633
+ end
634
+ it 'does the detach operation once the connection state is connected (#RTL5h)' do
635
+ connection.once(:connected) do
636
+ connection.once(:disconnected) do
637
+ channel.attach
638
+ channel.detach
639
+ connection.once(:connected) do
640
+ channel.once(:attached) do
641
+ channel.once(:detached) do
642
+ stop_reactor
643
+ end
644
+ end
645
+ end
646
+ end
647
+ disconnect_transport
648
+ end
649
+ end
650
+ end
651
+ end
328
652
  end
329
653
 
330
- describe 'channel recovery in :attaching state' do
331
- context 'the transport is disconnected before the ATTACHED protocol message is received' do
332
- skip 'attach times out and fails if not ATTACHED protocol message received'
333
- skip 'channel is ATTACHED if ATTACHED protocol message is later received'
334
- skip 'sends an ATTACH protocol message in response to a channel message being received on the attaching channel'
654
+ describe 'automatic channel recovery' do
655
+ let(:realtime_request_timeout) { 2 }
656
+ let(:client_options) do
657
+ default_options.merge(realtime_request_timeout: 2, log_level: :fatal)
335
658
  end
659
+
660
+ context 'when an ATTACH request times out' do
661
+ it 'moves to the SUSPENDED state (#RTL4f)' do
662
+ connection.once(:connected) do
663
+ attach_request_sent_at = Time.now
664
+ channel.attach
665
+ client.connection.__incoming_protocol_msgbus__.unsubscribe
666
+ channel.once(:suspended) do
667
+ expect(attach_request_sent_at.to_i).to be_within(realtime_request_timeout + 1).of(Time.now.to_i)
668
+ stop_reactor
669
+ end
670
+ end
671
+ end
672
+ end
673
+
674
+ context 'if a subsequent ATTACHED is received on an ATTACHED channel' do
675
+ it 'ignores the additional ATTACHED if resumed is true (#RTL12)' do
676
+ channel.attach do
677
+ channel.once do |obj|
678
+ fail "No state change expected: #{obj}"
679
+ end
680
+ attached_message = Ably::Models::ProtocolMessage.new(action: 11, channel: channel_name, flags: 4) # ATTACHED with resumed flag
681
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, attached_message
682
+ EventMachine.add_timer(1) do
683
+ channel.off
684
+ stop_reactor
685
+ end
686
+ end
687
+ end
688
+
689
+ it 'emits an UPDATE only when resumed is true (#RTL12)' do
690
+ channel.attach do
691
+ expect(channel.error_reason).to be_nil
692
+ channel.on(:update) do |state_change|
693
+ expect(state_change.current).to eq(:attached)
694
+ expect(state_change.previous).to eq(:attached)
695
+ expect(state_change.resumed).to be_falsey
696
+ expect(state_change.reason).to be_nil
697
+ expect(channel.error_reason).to be_nil
698
+ stop_reactor
699
+ end
700
+ attached_message = Ably::Models::ProtocolMessage.new(action: 11, channel: channel_name, flags: 0) # No resumed flag
701
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, attached_message
702
+ end
703
+ end
704
+
705
+ it 'emits an UPDATE when resumed is true and includes the reason error from the ProtocolMessage (#RTL12)' do
706
+ channel.attach do
707
+ expect(channel.error_reason).to be_nil
708
+ channel.on(:update) do |state_change|
709
+ expect(state_change.current).to eq(:attached)
710
+ expect(state_change.previous).to eq(:attached)
711
+ expect(state_change.resumed).to be_falsey
712
+ expect(state_change.reason.code).to eql(50505)
713
+ expect(channel.error_reason.code).to eql(50505)
714
+ stop_reactor
715
+ end
716
+ attached_message = Ably::Models::ProtocolMessage.new(action: 11, channel: channel_name, error: { code: 50505 }, flags: 0) # No resumed flag with error
717
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, attached_message
718
+ end
719
+ end
720
+ end
721
+
722
+ # skip 'sends an ATTACH protocol message in response to a channel message being received on the attaching channel'
336
723
  end
337
724
 
338
725
  context '#publish' do
@@ -369,7 +756,7 @@ describe Ably::Realtime::Channel, :event_machine do
369
756
  # All 3 messages should be batched into a single Protocol Message by the client library
370
757
  # message.id = "{protocol_message.id}:{protocol_message_index}"
371
758
  # Check that all messages share the same protocol_message.id
372
- message_id = messages.map { |msg| msg.id.split(':')[0] }
759
+ message_id = messages.map { |msg| msg.id.split(':')[0...-1].join(':') }
373
760
  expect(message_id.uniq.count).to eql(1)
374
761
 
375
762
  # Check that messages use index 0,1,2 in the ID
@@ -383,32 +770,38 @@ describe Ably::Realtime::Channel, :event_machine do
383
770
  let(:client_options) { default_options.merge(queue_messages: false) }
384
771
 
385
772
  context 'and connection state initialized' do
386
- it 'raises an exception' do
387
- expect { channel.publish('event') }.to raise_error Ably::Exceptions::MessageQueueingDisabled
773
+ it 'fails the deferrable' do
388
774
  expect(client.connection).to be_initialized
389
- stop_reactor
775
+ channel.publish('event').errback do |error|
776
+ expect(error).to be_a(Ably::Exceptions::MessageQueueingDisabled)
777
+ stop_reactor
778
+ end
390
779
  end
391
780
  end
392
781
 
393
782
  context 'and connection state connecting' do
394
- it 'raises an exception' do
783
+ it 'fails the deferrable' do
395
784
  client.connect
396
785
  EventMachine.next_tick do
397
- expect { channel.publish('event') }.to raise_error Ably::Exceptions::MessageQueueingDisabled
398
786
  expect(client.connection).to be_connecting
399
- stop_reactor
787
+ channel.publish('event').errback do |error|
788
+ expect(error).to be_a(Ably::Exceptions::MessageQueueingDisabled)
789
+ stop_reactor
790
+ end
400
791
  end
401
792
  end
402
793
  end
403
794
 
404
795
  context 'and connection state disconnected' do
405
- let(:client_options) { default_options.merge(queue_messages: false, :log_level => :error ) }
406
- it 'raises an exception' do
796
+ let(:client_options) { default_options.merge(queue_messages: false) }
797
+ it 'fails the deferrable' do
407
798
  client.connection.once(:connected) do
408
799
  client.connection.once(:disconnected) do
409
- expect { channel.publish('event') }.to raise_error Ably::Exceptions::MessageQueueingDisabled
410
800
  expect(client.connection).to be_disconnected
411
- stop_reactor
801
+ channel.publish('event').errback do |error|
802
+ expect(error).to be_a(Ably::Exceptions::MessageQueueingDisabled)
803
+ stop_reactor
804
+ end
412
805
  end
413
806
  client.connection.transition_state_machine :disconnected
414
807
  end
@@ -449,6 +842,15 @@ describe Ably::Realtime::Channel, :event_machine do
449
842
  end
450
843
  end
451
844
  end
845
+
846
+ context 'and additional invalid attributes' do
847
+ let(:client_id) { 1 }
848
+
849
+ it 'throws an exception' do
850
+ expect { channel.publish([name: 'event', client_id: 1]) }.to raise_error ArgumentError, /client_id must be a String/
851
+ stop_reactor
852
+ end
853
+ end
452
854
  end
453
855
 
454
856
  context 'with an array of Hash objects with :name and :data attributes' do
@@ -698,6 +1100,13 @@ describe Ably::Realtime::Channel, :event_machine do
698
1100
  end
699
1101
  end
700
1102
 
1103
+ context 'with a non-String client_id in the message' do
1104
+ it 'throws an exception' do
1105
+ expect { channel.publish([name: 'event', client_id: 1]) }.to raise_error ArgumentError, /client_id must be a String/
1106
+ stop_reactor
1107
+ end
1108
+ end
1109
+
701
1110
  context 'with an empty client_id in the message' do
702
1111
  it 'succeeds and publishes without a client_id' do
703
1112
  channel.publish([name: 'event', client_id: nil]).tap do |deferrable|
@@ -915,7 +1324,9 @@ describe Ably::Realtime::Channel, :event_machine do
915
1324
 
916
1325
  it 'logs the error and continues' do
917
1326
  emitted_exception = false
918
- expect(client.logger).to receive(:error).with(/#{exception.message}/)
1327
+ expect(client.logger).to receive(:error) do |*args, &block|
1328
+ expect(args.concat([block ? block.call : nil]).join(',')).to match(/#{exception.message}/)
1329
+ end
919
1330
  channel.subscribe('click') do |message|
920
1331
  emitted_exception = true
921
1332
  raise exception
@@ -988,36 +1399,42 @@ describe Ably::Realtime::Channel, :event_machine do
988
1399
  client.connection.manager.error_received_from_server error
989
1400
  end
990
1401
 
991
- context 'an :attached channel' do
992
- it 'transitions state to :failed' do
993
- channel.attach do
994
- channel.on(:failed) do |connection_state_change|
995
- error = connection_state_change.reason
996
- expect(error).to be_a(Ably::Exceptions::ConnectionFailed)
997
- expect(error.code).to eql(80002)
998
- stop_reactor
1402
+ context 'an :attaching channel' do
1403
+ it 'transitions state to :failed (#RTL3a)' do
1404
+ connection.once(:connected) do
1405
+ channel.once(:attaching) do
1406
+ channel.on(:failed) do |connection_state_change|
1407
+ error = connection_state_change.reason
1408
+ expect(error).to be_a(Ably::Exceptions::ConnectionFailed)
1409
+ expect(error.code).to eql(50000)
1410
+ stop_reactor
1411
+ end
1412
+ fake_error connection_error
999
1413
  end
1000
- fake_error connection_error
1414
+ channel.attach
1001
1415
  end
1002
1416
  end
1417
+ end
1003
1418
 
1004
- it 'emits an error event on the channel' do
1419
+ context 'an :attached channel' do
1420
+ it 'transitions state to :failed (#RTL3a)' do
1005
1421
  channel.attach do
1006
- channel.on(:error) do |error|
1422
+ channel.on(:failed) do |connection_state_change|
1423
+ error = connection_state_change.reason
1007
1424
  expect(error).to be_a(Ably::Exceptions::ConnectionFailed)
1008
- expect(error.code).to eql(80002)
1425
+ expect(error.code).to eql(50000)
1009
1426
  stop_reactor
1010
1427
  end
1011
1428
  fake_error connection_error
1012
1429
  end
1013
1430
  end
1014
1431
 
1015
- it 'updates the channel error_reason' do
1432
+ it 'updates the channel error_reason (#RTL3a)' do
1016
1433
  channel.attach do
1017
1434
  channel.on(:failed) do |connection_state_change|
1018
1435
  error = connection_state_change.reason
1019
1436
  expect(error).to be_a(Ably::Exceptions::ConnectionFailed)
1020
- expect(error.code).to eql(80002)
1437
+ expect(error.code).to eql(50000)
1021
1438
  stop_reactor
1022
1439
  end
1023
1440
  fake_error connection_error
@@ -1026,10 +1443,9 @@ describe Ably::Realtime::Channel, :event_machine do
1026
1443
  end
1027
1444
 
1028
1445
  context 'a :detached channel' do
1029
- it 'remains in the :detached state' do
1446
+ it 'remains in the :detached state (#RTL3a)' do
1030
1447
  channel.attach do
1031
1448
  channel.on(:failed) { raise 'Failed state should not have been reached' }
1032
- channel.on(:error) { raise 'Error should not have been emitted' }
1033
1449
 
1034
1450
  channel.detach do
1035
1451
  EventMachine.add_timer(1) do
@@ -1046,11 +1462,10 @@ describe Ably::Realtime::Channel, :event_machine do
1046
1462
  context 'a :failed channel' do
1047
1463
  let(:original_error) { RuntimeError.new }
1048
1464
 
1049
- it 'remains in the :failed state and ignores the failure error' do
1465
+ it 'remains in the :failed state and ignores the failure error (#RTL3a)' do
1050
1466
  channel.attach do
1051
- channel.on(:error) do
1467
+ channel.on(:failed) do
1052
1468
  channel.on(:failed) { raise 'Failed state should not have been reached' }
1053
- channel.on(:error) { raise 'Error should not have been emitted' }
1054
1469
 
1055
1470
  EventMachine.add_timer(1) do
1056
1471
  expect(channel).to be_failed
@@ -1067,11 +1482,13 @@ describe Ably::Realtime::Channel, :event_machine do
1067
1482
  end
1068
1483
 
1069
1484
  context 'a channel ATTACH request' do
1070
- it 'raises an exception' do
1485
+ it 'fails the deferrable (#RTL4b)' do
1071
1486
  client.connect do
1072
1487
  client.connection.once(:failed) do
1073
- expect { channel.attach }.to raise_error Ably::Exceptions::InvalidStateChange
1074
- stop_reactor
1488
+ channel.attach.errback do |error|
1489
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
1490
+ stop_reactor
1491
+ end
1075
1492
  end
1076
1493
  fake_error connection_error
1077
1494
  end
@@ -1081,7 +1498,7 @@ describe Ably::Realtime::Channel, :event_machine do
1081
1498
 
1082
1499
  context ':closed' do
1083
1500
  context 'an :attached channel' do
1084
- it 'transitions state to :detached' do
1501
+ it 'transitions state to :detached (#RTL3b)' do
1085
1502
  channel.attach do
1086
1503
  channel.on(:detached) do
1087
1504
  stop_reactor
@@ -1091,12 +1508,26 @@ describe Ably::Realtime::Channel, :event_machine do
1091
1508
  end
1092
1509
  end
1093
1510
 
1511
+ context 'an :attaching channel (#RTL3b)' do
1512
+ it 'transitions state to :detached' do
1513
+ channel.on(:attaching) do
1514
+ channel.on(:detached) do
1515
+ stop_reactor
1516
+ end
1517
+ client.connection.__incoming_protocol_msgbus__.unsubscribe
1518
+ client.connection.close
1519
+ closed_message = Ably::Models::ProtocolMessage.new(action: 8) # CLOSED
1520
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, closed_message
1521
+ end
1522
+ channel.attach
1523
+ end
1524
+ end
1525
+
1094
1526
  context 'a :detached channel' do
1095
- it 'remains in the :detached state' do
1527
+ it 'remains in the :detached state (#RTL3b)' do
1096
1528
  channel.attach do
1097
1529
  channel.detach do
1098
1530
  channel.on(:detached) { raise 'Detached state should not have been reached' }
1099
- channel.on(:error) { raise 'Error should not have been emitted' }
1100
1531
 
1101
1532
  EventMachine.add_timer(1) do
1102
1533
  expect(channel).to be_detached
@@ -1113,11 +1544,10 @@ describe Ably::Realtime::Channel, :event_machine do
1113
1544
  let(:client_options) { default_options.merge(log_level: :fatal) }
1114
1545
  let(:original_error) { Ably::Models::ErrorInfo.new(message: 'Error') }
1115
1546
 
1116
- it 'remains in the :failed state and retains the error_reason' do
1547
+ it 'remains in the :failed state and retains the error_reason (#RTL3b)' do
1117
1548
  channel.attach do
1118
- channel.once(:error) do
1549
+ channel.once(:failed) do
1119
1550
  channel.on(:detached) { raise 'Detached state should not have been reached' }
1120
- channel.on(:error) { raise 'Error should not have been emitted' }
1121
1551
 
1122
1552
  EventMachine.add_timer(1) do
1123
1553
  expect(channel).to be_failed
@@ -1134,11 +1564,13 @@ describe Ably::Realtime::Channel, :event_machine do
1134
1564
  end
1135
1565
 
1136
1566
  context 'a channel ATTACH request when connection CLOSED' do
1137
- it 'raises an exception' do
1567
+ it 'fails the deferrable (#RTL4b)' do
1138
1568
  client.connect do
1139
1569
  client.connection.once(:closed) do
1140
- expect { channel.attach }.to raise_error Ably::Exceptions::InvalidStateChange
1141
- stop_reactor
1570
+ channel.attach.errback do |error|
1571
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
1572
+ stop_reactor
1573
+ end
1142
1574
  end
1143
1575
  client.close
1144
1576
  end
@@ -1146,11 +1578,13 @@ describe Ably::Realtime::Channel, :event_machine do
1146
1578
  end
1147
1579
 
1148
1580
  context 'a channel ATTACH request when connection CLOSING' do
1149
- it 'raises an exception' do
1581
+ it 'fails the deferrable (#RTL4b)' do
1150
1582
  client.connect do
1151
1583
  client.connection.once(:closing) do
1152
- expect { channel.attach }.to raise_error Ably::Exceptions::InvalidStateChange
1153
- stop_reactor
1584
+ channel.attach.errback do |error|
1585
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
1586
+ stop_reactor
1587
+ end
1154
1588
  end
1155
1589
  client.close
1156
1590
  end
@@ -1159,25 +1593,48 @@ describe Ably::Realtime::Channel, :event_machine do
1159
1593
  end
1160
1594
 
1161
1595
  context ':suspended' do
1162
- context 'an :attached channel' do
1163
- let(:client_options) { default_options.merge(log_level: :fatal) }
1596
+ context 'an :attaching channel' do
1597
+ it 'transitions state to :suspended (#RTL3c)' do
1598
+ channel.on(:attaching) do
1599
+ channel.on(:suspended) do
1600
+ stop_reactor
1601
+ end
1602
+ client.connection.once_or_if(:connecting) do
1603
+ client.connection.transition_state_machine :suspended
1604
+ end
1605
+ end
1606
+ channel.attach
1607
+ end
1608
+ end
1164
1609
 
1165
- it 'transitions state to :detached' do
1610
+ context 'an :attached channel' do
1611
+ it 'transitions state to :suspended (#RTL3c)' do
1166
1612
  channel.attach do
1167
- channel.on(:detached) do
1613
+ channel.on(:suspended) do
1168
1614
  stop_reactor
1169
1615
  end
1170
1616
  client.connection.transition_state_machine :suspended
1171
1617
  end
1172
1618
  end
1619
+
1620
+ it 'transitions state automatically to :attaching once the connection is re-established (#RTN15c3)' do
1621
+ channel.attach do
1622
+ channel.on(:suspended) do
1623
+ client.connection.connect
1624
+ channel.once(:attached) do
1625
+ stop_reactor
1626
+ end
1627
+ end
1628
+ client.connection.transition_state_machine :suspended
1629
+ end
1630
+ end
1173
1631
  end
1174
1632
 
1175
1633
  context 'a :detached channel' do
1176
- it 'remains in the :detached state' do
1634
+ it 'remains in the :detached state (#RTL3c)' do
1177
1635
  channel.attach do
1178
1636
  channel.detach do
1179
1637
  channel.on(:detached) { raise 'Detached state should not have been reached' }
1180
- channel.on(:error) { raise 'Error should not have been emitted' }
1181
1638
 
1182
1639
  EventMachine.add_timer(1) do
1183
1640
  expect(channel).to be_detached
@@ -1194,11 +1651,10 @@ describe Ably::Realtime::Channel, :event_machine do
1194
1651
  let(:original_error) { RuntimeError.new }
1195
1652
  let(:client_options) { default_options.merge(log_level: :fatal) }
1196
1653
 
1197
- it 'remains in the :failed state and retains the error_reason' do
1654
+ it 'remains in the :failed state and retains the error_reason (#RTL3c)' do
1198
1655
  channel.attach do
1199
- channel.once(:error) do
1656
+ channel.once(:failed) do
1200
1657
  channel.on(:detached) { raise 'Detached state should not have been reached' }
1201
- channel.on(:error) { raise 'Error should not have been emitted' }
1202
1658
 
1203
1659
  EventMachine.add_timer(1) do
1204
1660
  expect(channel).to be_failed
@@ -1214,18 +1670,140 @@ describe Ably::Realtime::Channel, :event_machine do
1214
1670
  end
1215
1671
  end
1216
1672
 
1217
- context 'a channel ATTACH request when connection SUSPENDED' do
1673
+ context 'a channel ATTACH request when connection SUSPENDED (#RTL4b)' do
1218
1674
  let(:client_options) { default_options.merge(log_level: :fatal) }
1219
1675
 
1220
- it 'raises an exception' do
1676
+ it 'fails the deferrable' do
1221
1677
  client.connect do
1222
1678
  client.connection.once(:suspended) do
1223
- expect { channel.attach }.to raise_error Ably::Exceptions::InvalidStateChange
1224
- stop_reactor
1679
+ channel.attach.errback do |error|
1680
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
1681
+ stop_reactor
1682
+ end
1683
+ end
1684
+ client.connection.transition_state_machine :suspended
1685
+ end
1686
+ end
1687
+ end
1688
+ end
1689
+
1690
+ context ':connected' do
1691
+ context 'a :suspended channel' do
1692
+ it 'is automatically reattached (#RTL3d)' do
1693
+ channel.attach do
1694
+ channel.once(:suspended) do
1695
+ client.connection.connect
1696
+ channel.once(:attached) do
1697
+ stop_reactor
1698
+ end
1225
1699
  end
1226
1700
  client.connection.transition_state_machine :suspended
1227
1701
  end
1228
1702
  end
1703
+
1704
+ context 'when re-attach attempt fails' do
1705
+ let(:client_options) do
1706
+ default_options.merge(realtime_request_timeout: 2, log_level: :fatal)
1707
+ end
1708
+
1709
+ it 'returns to a suspended state (#RTL3d)' do
1710
+ channel.attach do
1711
+ channel.once(:attached) do
1712
+ fail "Channel should not have become attached"
1713
+ end
1714
+
1715
+ channel.once(:suspended) do
1716
+ client.connection.connect
1717
+ channel.once(:attaching) do
1718
+ # don't process any incoming ProtocolMessages so the connection never opens
1719
+ client.connection.__incoming_protocol_msgbus__.unsubscribe
1720
+ channel.once(:suspended) do |state_change|
1721
+ expect(state_change.reason.code).to eql(90007)
1722
+ stop_reactor
1723
+ end
1724
+ end
1725
+ end
1726
+ client.connection.transition_state_machine :suspended
1727
+ end
1728
+ end
1729
+ end
1730
+ end
1731
+ end
1732
+
1733
+ context ':disconnected' do
1734
+ context 'with an initialized channel' do
1735
+ it 'has no effect on the channel states (#RTL3e)' do
1736
+ connection.once(:connected) do
1737
+ expect(channel).to be_initialized
1738
+ connection.once(:disconnected) do
1739
+ expect(channel).to be_initialized
1740
+ stop_reactor
1741
+ end
1742
+ disconnect_transport
1743
+ end
1744
+ end
1745
+ end
1746
+
1747
+ context 'with an attaching channel' do
1748
+ it 'has no effect on the channel states (#RTL3e)' do
1749
+ connection.once(:connected) do
1750
+ channel.once(:attaching) do
1751
+ connection.once(:disconnected) do
1752
+ expect(channel).to be_attaching
1753
+ stop_reactor
1754
+ end
1755
+ disconnect_transport
1756
+ end
1757
+ channel.attach
1758
+ end
1759
+ end
1760
+ end
1761
+
1762
+ context 'with an attached channel' do
1763
+ it 'has no effect on the channel states (#RTL3e)' do
1764
+ channel.attach do
1765
+ connection.once(:disconnected) do
1766
+ expect(channel).to be_attached
1767
+ stop_reactor
1768
+ end
1769
+ disconnect_transport
1770
+ end
1771
+ end
1772
+ end
1773
+
1774
+ context 'with a detached channel' do
1775
+ it 'has no effect on the channel states (#RTL3e)' do
1776
+ channel.attach do
1777
+ channel.detach do
1778
+ connection.once(:disconnected) do
1779
+ expect(channel).to be_detached
1780
+ stop_reactor
1781
+ end
1782
+ disconnect_transport
1783
+ end
1784
+ end
1785
+ end
1786
+ end
1787
+
1788
+ context 'with a failed channel' do
1789
+ let(:client_options) do
1790
+ default_options.merge(
1791
+ default_token_params: { capability: { "foo" =>["*"] } },
1792
+ use_token_auth: true,
1793
+ log_level: :fatal
1794
+ )
1795
+ end
1796
+
1797
+ it 'has no effect on the channel states (#RTL3e)' do
1798
+ channel.once(:failed) do
1799
+ connection.once(:disconnected) do
1800
+ expect(channel).to be_failed
1801
+ stop_reactor
1802
+ end
1803
+ disconnect_transport
1804
+ end
1805
+ channel.attach
1806
+ end
1229
1807
  end
1230
1808
  end
1231
1809
  end
@@ -1249,6 +1827,7 @@ describe Ably::Realtime::Channel, :event_machine do
1249
1827
  context 'ChannelStateChange object' do
1250
1828
  it 'has current state' do
1251
1829
  channel.on(:attached) do |channel_state_change|
1830
+ expect(channel_state_change.current).to be_a(Ably::Realtime::Channel::STATE)
1252
1831
  expect(channel_state_change.current).to eq(:attached)
1253
1832
  stop_reactor
1254
1833
  end
@@ -1257,12 +1836,22 @@ describe Ably::Realtime::Channel, :event_machine do
1257
1836
 
1258
1837
  it 'has a previous state' do
1259
1838
  channel.on(:attached) do |channel_state_change|
1839
+ expect(channel_state_change.previous).to be_a(Ably::Realtime::Channel::STATE)
1260
1840
  expect(channel_state_change.previous).to eq(:attaching)
1261
1841
  stop_reactor
1262
1842
  end
1263
1843
  channel.attach
1264
1844
  end
1265
1845
 
1846
+ it 'has the event that generated the state change (#TA5)' do
1847
+ channel.on(:attached) do |channel_state_change|
1848
+ expect(channel_state_change.event).to be_a(Ably::Realtime::Channel::EVENT)
1849
+ expect(channel_state_change.event).to eq(:attached)
1850
+ stop_reactor
1851
+ end
1852
+ channel.attach
1853
+ end
1854
+
1266
1855
  it 'contains a private API protocol_message attribute that is used for special state change events', :api_private do
1267
1856
  channel.on(:attached) do |channel_state_change|
1268
1857
  expect(channel_state_change.protocol_message).to be_a(Ably::Models::ProtocolMessage)
@@ -1296,6 +1885,266 @@ describe Ably::Realtime::Channel, :event_machine do
1296
1885
  end
1297
1886
  end
1298
1887
  end
1888
+
1889
+ context '#resume (#RTL2f)' do
1890
+ it 'is false when a channel first attaches' do
1891
+ channel.attach
1892
+ channel.on(:attached) do |channel_state_change|
1893
+ expect(channel_state_change.resumed).to be_falsey
1894
+ stop_reactor
1895
+ end
1896
+ end
1897
+
1898
+ it 'is true when a connection is recovered and the channel is attached' do
1899
+ channel.attach
1900
+ channel.once(:attached) do |channel_state_change|
1901
+ connection_id = client.connection.id
1902
+ expect(channel_state_change.resumed).to be_falsey
1903
+
1904
+ recover_client = auto_close Ably::Realtime::Client.new(client_options.merge(recover: client.connection.recovery_key))
1905
+ recover_client.connection.once(:connected) do
1906
+ expect(recover_client.connection.id).to eql(connection_id)
1907
+ recover_channel = recover_client.channels.get(channel_name)
1908
+ recover_channel.attach
1909
+ recover_channel.once(:attached) do |recover_channel_state_change|
1910
+ expect(recover_channel_state_change.resumed).to be_truthy
1911
+ stop_reactor
1912
+ end
1913
+ end
1914
+ end
1915
+ end
1916
+
1917
+ it 'is false when a connection fails to recover and the channel is attached' do
1918
+ client.connection.once(:connected) do
1919
+ recovery_key = client.connection.recovery_key
1920
+ client.connection.once(:closed) do
1921
+ recover_client = auto_close Ably::Realtime::Client.new(client_options.merge(recover: recovery_key, log_level: :error))
1922
+ recover_client.connection.once(:connected) do
1923
+ recover_channel = recover_client.channels.get(channel_name)
1924
+ recover_channel.attach
1925
+ recover_channel.once(:attached) do |recover_channel_state_change|
1926
+ expect(recover_channel_state_change.resumed).to be_falsey
1927
+ stop_reactor
1928
+ end
1929
+ end
1930
+ end
1931
+
1932
+ client.close
1933
+ end
1934
+ end
1935
+
1936
+ context 'when a resume fails' do
1937
+ let(:client_options) { default_options.merge(log_level: :error) }
1938
+
1939
+ it 'is false when a resume fails to recover and the channel is automatically re-attached' do
1940
+ channel.attach do
1941
+ connection_id = client.connection.id
1942
+ channel.once(:attached) do |channel_state_change|
1943
+ expect(client.connection.id).to_not eql(connection_id)
1944
+ expect(channel_state_change.resumed).to be_falsey
1945
+ stop_reactor
1946
+ end
1947
+ client.connection.transport.close_connection_after_writing
1948
+ client.connection.configure_new '0123456789abcdef', 'wVIsgTHAB1UvXh7z-1991d8586', -1 # force the resume connection key to be invalid
1949
+ end
1950
+ end
1951
+ end
1952
+ end
1953
+ end
1954
+
1955
+ context 'moves to' do
1956
+ %w(suspended detached failed).each do |channel_state|
1957
+ context(channel_state) do
1958
+ specify 'all queued messages fail with NACK (#RTL11)' do
1959
+ channel.attach do
1960
+ # Move to disconnected
1961
+ disconnect_transport_proc = Proc.new do
1962
+ if connection.transport
1963
+ connection.transport.close_connection_after_writing
1964
+ else
1965
+ EventMachine.next_tick { disconnect_transport_proc.call }
1966
+ end
1967
+ end
1968
+ disconnect_transport_proc.call
1969
+
1970
+ connection.on(:connecting) { disconnect_transport_proc.call }
1971
+
1972
+ connection.once(:disconnected) do
1973
+ channel.publish("foo").errback do |error|
1974
+ stop_reactor
1975
+ end
1976
+ channel.transition_state_machine channel_state.to_sym
1977
+ end
1978
+ end
1979
+ end
1980
+
1981
+ specify 'all published messages awaiting an ACK do nothing (#RTL11a)' do
1982
+ connection_been_disconnected = false
1983
+
1984
+ channel.attach do
1985
+ deferrable = channel.publish("foo")
1986
+ deferrable.errback do |error|
1987
+ fail "Message publish should not fail"
1988
+ end
1989
+ deferrable.callback do |error|
1990
+ EventMachine.add_timer(0.5) do
1991
+ expect(connection_been_disconnected).to be_truthy
1992
+ stop_reactor
1993
+ end
1994
+ end
1995
+
1996
+ # Allow 5ms for message to be sent into the socket TCP/IP stack
1997
+ EventMachine.add_timer(0.005) do
1998
+ connection.transport.close_connection_after_writing
1999
+ connection.once(:disconnected) do
2000
+ connection_been_disconnected = true
2001
+ channel.transition_state_machine channel_state.to_sym
2002
+ end
2003
+ end
2004
+ end
2005
+ end
2006
+ end
2007
+ end
2008
+ end
2009
+ end
2010
+
2011
+ context 'when it receives a server-initiated DETACHED (#RTL13)' do
2012
+ let(:detached_action) { 13 }
2013
+
2014
+ context 'and channel is initialized (#RTL13)' do
2015
+ it 'does nothing' do
2016
+ connection.once(:connected) do
2017
+ channel.on { raise 'Channel state should not change' }
2018
+
2019
+ detach_message = Ably::Models::ProtocolMessage.new(action: detached_action, channel: channel_name)
2020
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, detach_message
2021
+
2022
+ EventMachine.add_timer(1) { stop_reactor }
2023
+ end
2024
+ end
2025
+ end
2026
+
2027
+ context 'and channel is failed' do
2028
+ let(:client_options) {
2029
+ default_options.merge(
2030
+ use_token_auth: true,
2031
+ default_token_params: { capability: { "foo" => ["publish"] } },
2032
+ log_level: :fatal
2033
+ )
2034
+ }
2035
+
2036
+ it 'does nothing (#RTL13)' do
2037
+ connection.once(:connected) do
2038
+ channel.attach
2039
+ channel.once(:failed) do
2040
+ channel.on { raise 'Channel state should not change' }
2041
+
2042
+ detach_message = Ably::Models::ProtocolMessage.new(action: detached_action, channel: channel_name)
2043
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, detach_message
2044
+
2045
+ EventMachine.add_timer(1) { stop_reactor }
2046
+ end
2047
+ end
2048
+ end
2049
+ end
2050
+
2051
+ context 'and channel is attached' do
2052
+ it 'reattaches immediately (#RTL13a)' do
2053
+ channel.attach do
2054
+ channel.once(:attaching) do |state_change|
2055
+ expect(state_change.reason.code).to eql(50505)
2056
+ channel.once(:attached) do
2057
+ stop_reactor
2058
+ end
2059
+ end
2060
+
2061
+ detach_message = Ably::Models::ProtocolMessage.new(action: detached_action, channel: channel_name, error: { code: 50505 })
2062
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, detach_message
2063
+ end
2064
+ end
2065
+ end
2066
+
2067
+ context 'and channel is suspended' do
2068
+ it 'reattaches immediately (#RTL13a)' do
2069
+ channel.attach do
2070
+ channel.once(:suspended) do
2071
+ channel.once(:attaching) do |state_change|
2072
+ expect(state_change.reason.code).to eql(50505)
2073
+ channel.once(:attached) do
2074
+ stop_reactor
2075
+ end
2076
+ end
2077
+
2078
+ detach_message = Ably::Models::ProtocolMessage.new(action: detached_action, channel: channel_name, error: { code: 50505 })
2079
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, detach_message
2080
+ end
2081
+
2082
+ channel.transition_state_machine! :suspended
2083
+ end
2084
+ end
2085
+ end
2086
+
2087
+ context 'and channel is attaching' do
2088
+ let(:client_options) { default_options.merge(channel_retry_timeout: 2, realtime_request_timeout: 1, log_level: :fatal) }
2089
+
2090
+ it 'will move to the SUSPENDED state and then attempt to ATTACH with the ATTACHING state (#RTL13b)' do
2091
+ connection.once(:connected) do
2092
+ # Prevent any incoming or outgoing ATTACH/ATTACHED message from Ably
2093
+ prevent_protocol_messages_proc = Proc.new do
2094
+ if client.connection.transport
2095
+ client.connection.transport.__incoming_protocol_msgbus__.unsubscribe
2096
+ client.connection.transport.__outgoing_protocol_msgbus__.unsubscribe
2097
+ else
2098
+ EventMachine.next_tick { prevent_protocol_messages_proc.call }
2099
+ end
2100
+ end
2101
+ prevent_protocol_messages_proc.call
2102
+ end
2103
+
2104
+ channel.once(:attaching) do
2105
+ attaching_at = Time.now
2106
+ # First attaching fails during server-initiated ATTACHED received
2107
+ channel.once(:suspended) do |state_change|
2108
+ expect(Time.now.to_i - attaching_at.to_i).to be_within(1).of(1)
2109
+
2110
+ suspended_at = Time.now
2111
+ # Automatic attach happens at channel_retry_timeout
2112
+ channel.once(:attaching) do
2113
+ expect(Time.now.to_i - attaching_at.to_i).to be_within(1).of(2)
2114
+ channel.once(:suspended) do
2115
+ channel.once(:attaching) do
2116
+ channel.once(:attached) do
2117
+ stop_reactor
2118
+ end
2119
+ # Simulate ATTACHED from Ably
2120
+ attached_message = Ably::Models::ProtocolMessage.new(action: 11, channel: channel_name) # ATTACHED
2121
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, attached_message
2122
+ end
2123
+ end
2124
+ end
2125
+ end
2126
+
2127
+ detach_message = Ably::Models::ProtocolMessage.new(action: detached_action, channel: channel_name)
2128
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, detach_message
2129
+ end
2130
+ channel.attach
2131
+ end
2132
+ end
2133
+ end
2134
+
2135
+ context 'when it receives an ERROR ProtocolMessage' do
2136
+ let(:client_options) { default_options.merge(log_level: :fatal) }
2137
+
2138
+ it 'should transition to the failed state and the error_reason should be set (#RTL14)' do
2139
+ channel.attach do
2140
+ channel.once(:failed) do |state_change|
2141
+ expect(state_change.reason.code).to eql(50505)
2142
+ expect(channel.error_reason.code).to eql(50505)
2143
+ stop_reactor
2144
+ end
2145
+ error_message = Ably::Models::ProtocolMessage.new(action: 9, channel: channel_name, error: { code: 50505 }) # ProtocolMessage ERROR type
2146
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, error_message
2147
+ end
1299
2148
  end
1300
2149
  end
1301
2150
  end