ably-rest 0.9.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/ably-rest.gemspec +2 -1
  3. data/lib/submodules/ably-ruby/.travis.yml +6 -4
  4. data/lib/submodules/ably-ruby/CHANGELOG.md +52 -61
  5. data/lib/submodules/ably-ruby/README.md +10 -0
  6. data/lib/submodules/ably-ruby/SPEC.md +1473 -852
  7. data/lib/submodules/ably-ruby/ably.gemspec +2 -1
  8. data/lib/submodules/ably-ruby/lib/ably/auth.rb +57 -25
  9. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +34 -8
  10. data/lib/submodules/ably-ruby/lib/ably/logger.rb +10 -1
  11. data/lib/submodules/ably-ruby/lib/ably/models/auth_details.rb +42 -0
  12. data/lib/submodules/ably-ruby/lib/ably/models/channel_state_change.rb +18 -4
  13. data/lib/submodules/ably-ruby/lib/ably/models/connection_details.rb +6 -3
  14. data/lib/submodules/ably-ruby/lib/ably/models/connection_state_change.rb +4 -3
  15. data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +1 -1
  16. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +12 -1
  17. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/base.rb +101 -97
  18. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +13 -1
  19. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +20 -3
  20. data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +7 -3
  21. data/lib/submodules/ably-ruby/lib/ably/modules/enum.rb +17 -7
  22. data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +29 -14
  23. data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +7 -4
  24. data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +2 -4
  25. data/lib/submodules/ably-ruby/lib/ably/modules/uses_state_machine.rb +7 -3
  26. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +2 -0
  27. data/lib/submodules/ably-ruby/lib/ably/realtime/auth.rb +79 -31
  28. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +62 -26
  29. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +154 -65
  30. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +14 -15
  31. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +16 -3
  32. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +38 -29
  33. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +6 -1
  34. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +108 -49
  35. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +165 -59
  36. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_state_machine.rb +22 -3
  37. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +19 -10
  38. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +67 -45
  39. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +198 -36
  40. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_manager.rb +30 -6
  41. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_state_machine.rb +5 -12
  42. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +3 -3
  43. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +21 -8
  44. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/exceptions.rb +1 -3
  45. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/logger.rb +2 -2
  46. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -1
  47. data/lib/submodules/ably-ruby/lib/ably/util/pub_sub.rb +1 -1
  48. data/lib/submodules/ably-ruby/lib/ably/util/safe_deferrable.rb +26 -0
  49. data/lib/submodules/ably-ruby/lib/ably/version.rb +2 -2
  50. data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +416 -99
  51. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +5 -3
  52. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +1011 -160
  53. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +2 -2
  54. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +458 -27
  55. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +436 -97
  56. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +52 -23
  57. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +5 -3
  58. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +1160 -105
  59. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +151 -22
  60. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +1 -1
  61. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +88 -27
  62. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +42 -15
  63. data/lib/submodules/ably-ruby/spec/acceptance/rest/presence_spec.rb +4 -4
  64. data/lib/submodules/ably-ruby/spec/rspec_config.rb +2 -1
  65. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +2 -2
  66. data/lib/submodules/ably-ruby/spec/shared/safe_deferrable_behaviour.rb +6 -2
  67. data/lib/submodules/ably-ruby/spec/support/debug_failure_helper.rb +20 -4
  68. data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +32 -1
  69. data/lib/submodules/ably-ruby/spec/unit/auth_spec.rb +4 -11
  70. data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +28 -2
  71. data/lib/submodules/ably-ruby/spec/unit/models/auth_details_spec.rb +49 -0
  72. data/lib/submodules/ably-ruby/spec/unit/models/channel_state_change_spec.rb +23 -3
  73. data/lib/submodules/ably-ruby/spec/unit/models/connection_details_spec.rb +12 -1
  74. data/lib/submodules/ably-ruby/spec/unit/models/connection_state_change_spec.rb +15 -4
  75. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +34 -2
  76. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +73 -2
  77. data/lib/submodules/ably-ruby/spec/unit/models/protocol_message_spec.rb +64 -6
  78. data/lib/submodules/ably-ruby/spec/unit/models/token_details_spec.rb +1 -1
  79. data/lib/submodules/ably-ruby/spec/unit/models/token_request_spec.rb +1 -1
  80. data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +2 -1
  81. data/lib/submodules/ably-ruby/spec/unit/modules/enum_spec.rb +69 -0
  82. data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +149 -22
  83. data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +9 -3
  84. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +1 -1
  85. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +8 -5
  86. data/lib/submodules/ably-ruby/spec/unit/realtime/incoming_message_dispatcher_spec.rb +1 -1
  87. data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +4 -3
  88. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +1 -1
  89. data/lib/submodules/ably-ruby/spec/unit/util/crypto_spec.rb +3 -3
  90. metadata +7 -5
@@ -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,15 +149,75 @@ 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
215
  channel.transition_state_machine :failed, reason: RuntimeError.new
96
216
  expect(channel).to be_failed
217
+ expect(channel.error_reason).to_not be_nil
97
218
  channel.attach do
98
219
  expect(channel).to be_attached
220
+ expect(channel.error_reason).to be_nil
99
221
  stop_reactor
100
222
  end
101
223
  end
@@ -103,14 +225,16 @@ describe Ably::Realtime::Channel, :event_machine do
103
225
  end
104
226
 
105
227
  context 'when state is :detaching' do
106
- it 'moves straight to attaching and skips detached' do
228
+ it 'does the attach operation after the completion of the pending request (#RTL4h)' do
107
229
  channel.once(:detaching) do
108
- channel.once(:detached) { raise 'Detach should not have been reached' }
109
-
110
- channel.once(:attaching) do
111
- channel.once(:attached) do
112
- channel.off
113
- 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
114
238
  end
115
239
  end
116
240
 
@@ -147,12 +271,21 @@ describe Ably::Realtime::Channel, :event_machine do
147
271
  end
148
272
 
149
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
150
283
  let(:restricted_client) do
151
- 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)
152
285
  end
153
- let(:restricted_channel) { restricted_client.channel("cannot_subscribe") }
286
+ let(:restricted_channel) { restricted_client.channel("cansubscribe:foo") }
154
287
 
155
- it 'emits failed event' do
288
+ it 'emits failed event (#RTL4e)' do
156
289
  restricted_channel.attach
157
290
  restricted_channel.on(:failed) do |connection_state|
158
291
  expect(restricted_channel.state).to eq(:failed)
@@ -161,7 +294,7 @@ describe Ably::Realtime::Channel, :event_machine do
161
294
  end
162
295
  end
163
296
 
164
- it 'calls the errback of the returned Deferrable' do
297
+ it 'calls the errback of the returned Deferrable (#RTL4d)' do
165
298
  restricted_channel.attach.errback do |error|
166
299
  expect(restricted_channel.state).to eq(:failed)
167
300
  expect(error.status).to eq(401)
@@ -169,15 +302,6 @@ describe Ably::Realtime::Channel, :event_machine do
169
302
  end
170
303
  end
171
304
 
172
- it 'emits an error event' do
173
- restricted_channel.attach
174
- restricted_channel.on(:error) do |error|
175
- expect(restricted_channel.state).to eq(:failed)
176
- expect(error.status).to eq(401)
177
- stop_reactor
178
- end
179
- end
180
-
181
305
  it 'updates the error_reason' do
182
306
  restricted_channel.attach
183
307
  restricted_channel.on(:failed) do
@@ -191,10 +315,8 @@ describe Ably::Realtime::Channel, :event_machine do
191
315
  restricted_channel.attach
192
316
  restricted_channel.once(:failed) do
193
317
  restricted_client.close do
194
- # A direct call to #authorize is synchronous
195
- restricted_client.auth.authorize({}, key: api_key)
196
-
197
- restricted_client.connect do
318
+ token_params = { capability: { "cansubscribe:foo" => ["subscribe"] } }
319
+ restricted_client.auth.authorize(token_params) do
198
320
  restricted_channel.once(:attached) do
199
321
  expect(restricted_channel.error_reason).to be_nil
200
322
  stop_reactor
@@ -206,98 +328,149 @@ describe Ably::Realtime::Channel, :event_machine do
206
328
  end
207
329
  end
208
330
  end
209
- end
210
331
 
211
- describe '#detach' do
212
- it 'detaches from a channel' do
213
- channel.attach do
214
- channel.detach
215
- channel.on(:detached) do
216
- 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
217
336
  stop_reactor
218
337
  end
219
338
  end
220
- end
221
339
 
222
- it 'detaches from a channel and calls the provided block' do
223
- channel.attach do
224
- expect(channel.state).to eq(:attached)
225
- channel.detach do
226
- expect(channel.state).to eq(:detached)
227
- 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
228
356
  end
229
357
  end
230
358
  end
359
+ end
231
360
 
232
- it 'emits :detaching then :detached events' do
233
- channel.once(:detaching) do
234
- channel.once(:detached) do
235
- 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
236
370
  end
237
371
  end
238
372
 
239
- channel.attach do
240
- 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
241
381
  end
242
- end
243
382
 
244
- it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
245
- channel.attach do
246
- expect(channel.detach).to be_a(Ably::Util::SafeDeferrable)
247
- 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
248
393
  end
249
- end
250
394
 
251
- it 'calls the Deferrable callback on success' do
252
- channel.attach do
253
- channel.detach.callback do
254
- expect(channel).to be_a(Ably::Realtime::Channel)
255
- 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)
256
398
  stop_reactor
257
399
  end
258
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
259
431
  end
260
432
 
261
433
  context 'when state is :failed' do
262
434
  let(:client_options) { default_options.merge(log_level: :fatal) }
263
435
 
264
- it 'raises an exception' do
436
+ it 'fails the deferrable (#RTL5b)' do
265
437
  channel.attach do
266
438
  channel.transition_state_machine :failed, reason: RuntimeError.new
267
439
  expect(channel).to be_failed
268
- expect { channel.detach }.to raise_error Ably::Exceptions::InvalidStateChange
269
- stop_reactor
440
+ channel.detach.errback do |error|
441
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
442
+ stop_reactor
443
+ end
270
444
  end
271
445
  end
272
446
  end
273
447
 
274
448
  context 'when state is :attaching' do
275
- it 'moves straight to :detaching state and skips :attached' do
276
- channel.once(:attaching) do
277
- channel.once(:attached) { raise 'Attached should never be reached' }
278
-
279
- channel.once(:detaching) do
280
- channel.once(:detached) do
281
- 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
282
457
  end
458
+ channel.detach
283
459
  end
284
-
285
- channel.detach
460
+ channel.attach
286
461
  end
287
-
288
- channel.attach
289
462
  end
290
463
  end
291
464
 
292
465
  context 'when state is :detaching' do
293
- 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
294
467
  channel.once(:detaching) do
295
- channel.detach
296
468
  channel.once(:detached) do
297
469
  channel.detach do
298
470
  stop_reactor
299
471
  end
300
472
  end
473
+ channel.detach
301
474
  end
302
475
 
303
476
  channel.attach do
@@ -306,8 +479,28 @@ describe Ably::Realtime::Channel, :event_machine do
306
479
  end
307
480
  end
308
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
+
309
502
  context 'when state is :initialized' do
310
- it 'does nothing as there is no channel to detach' do
503
+ it 'does nothing as there is no channel to detach (#RTL5a)' do
311
504
  expect(channel).to be_initialized
312
505
  channel.detach do
313
506
  expect(channel).to be_initialized
@@ -323,14 +516,210 @@ describe Ably::Realtime::Channel, :event_machine do
323
516
  end
324
517
  end
325
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
326
652
  end
327
653
 
328
- describe 'channel recovery in :attaching state' do
329
- context 'the transport is disconnected before the ATTACHED protocol message is received' do
330
- skip 'attach times out and fails if not ATTACHED protocol message received'
331
- skip 'channel is ATTACHED if ATTACHED protocol message is later received'
332
- 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)
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
333
720
  end
721
+
722
+ # skip 'sends an ATTACH protocol message in response to a channel message being received on the attaching channel'
334
723
  end
335
724
 
336
725
  context '#publish' do
@@ -381,32 +770,38 @@ describe Ably::Realtime::Channel, :event_machine do
381
770
  let(:client_options) { default_options.merge(queue_messages: false) }
382
771
 
383
772
  context 'and connection state initialized' do
384
- it 'raises an exception' do
385
- expect { channel.publish('event') }.to raise_error Ably::Exceptions::MessageQueueingDisabled
773
+ it 'fails the deferrable' do
386
774
  expect(client.connection).to be_initialized
387
- stop_reactor
775
+ channel.publish('event').errback do |error|
776
+ expect(error).to be_a(Ably::Exceptions::MessageQueueingDisabled)
777
+ stop_reactor
778
+ end
388
779
  end
389
780
  end
390
781
 
391
782
  context 'and connection state connecting' do
392
- it 'raises an exception' do
783
+ it 'fails the deferrable' do
393
784
  client.connect
394
785
  EventMachine.next_tick do
395
- expect { channel.publish('event') }.to raise_error Ably::Exceptions::MessageQueueingDisabled
396
786
  expect(client.connection).to be_connecting
397
- stop_reactor
787
+ channel.publish('event').errback do |error|
788
+ expect(error).to be_a(Ably::Exceptions::MessageQueueingDisabled)
789
+ stop_reactor
790
+ end
398
791
  end
399
792
  end
400
793
  end
401
794
 
402
795
  context 'and connection state disconnected' do
403
- let(:client_options) { default_options.merge(queue_messages: false, :log_level => :error ) }
404
- it 'raises an exception' do
796
+ let(:client_options) { default_options.merge(queue_messages: false) }
797
+ it 'fails the deferrable' do
405
798
  client.connection.once(:connected) do
406
799
  client.connection.once(:disconnected) do
407
- expect { channel.publish('event') }.to raise_error Ably::Exceptions::MessageQueueingDisabled
408
800
  expect(client.connection).to be_disconnected
409
- stop_reactor
801
+ channel.publish('event').errback do |error|
802
+ expect(error).to be_a(Ably::Exceptions::MessageQueueingDisabled)
803
+ stop_reactor
804
+ end
410
805
  end
411
806
  client.connection.transition_state_machine :disconnected
412
807
  end
@@ -447,6 +842,15 @@ describe Ably::Realtime::Channel, :event_machine do
447
842
  end
448
843
  end
449
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
450
854
  end
451
855
 
452
856
  context 'with an array of Hash objects with :name and :data attributes' do
@@ -696,6 +1100,13 @@ describe Ably::Realtime::Channel, :event_machine do
696
1100
  end
697
1101
  end
698
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
+
699
1110
  context 'with an empty client_id in the message' do
700
1111
  it 'succeeds and publishes without a client_id' do
701
1112
  channel.publish([name: 'event', client_id: nil]).tap do |deferrable|
@@ -913,7 +1324,9 @@ describe Ably::Realtime::Channel, :event_machine do
913
1324
 
914
1325
  it 'logs the error and continues' do
915
1326
  emitted_exception = false
916
- 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
917
1330
  channel.subscribe('click') do |message|
918
1331
  emitted_exception = true
919
1332
  raise exception
@@ -986,36 +1399,42 @@ describe Ably::Realtime::Channel, :event_machine do
986
1399
  client.connection.manager.error_received_from_server error
987
1400
  end
988
1401
 
989
- context 'an :attached channel' do
990
- it 'transitions state to :failed' do
991
- channel.attach do
992
- channel.on(:failed) do |connection_state_change|
993
- error = connection_state_change.reason
994
- expect(error).to be_a(Ably::Exceptions::ConnectionFailed)
995
- expect(error.code).to eql(80002)
996
- 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
997
1413
  end
998
- fake_error connection_error
1414
+ channel.attach
999
1415
  end
1000
1416
  end
1417
+ end
1001
1418
 
1002
- it 'emits an error event on the channel' do
1419
+ context 'an :attached channel' do
1420
+ it 'transitions state to :failed (#RTL3a)' do
1003
1421
  channel.attach do
1004
- channel.on(:error) do |error|
1422
+ channel.on(:failed) do |connection_state_change|
1423
+ error = connection_state_change.reason
1005
1424
  expect(error).to be_a(Ably::Exceptions::ConnectionFailed)
1006
- expect(error.code).to eql(80002)
1425
+ expect(error.code).to eql(50000)
1007
1426
  stop_reactor
1008
1427
  end
1009
1428
  fake_error connection_error
1010
1429
  end
1011
1430
  end
1012
1431
 
1013
- it 'updates the channel error_reason' do
1432
+ it 'updates the channel error_reason (#RTL3a)' do
1014
1433
  channel.attach do
1015
1434
  channel.on(:failed) do |connection_state_change|
1016
1435
  error = connection_state_change.reason
1017
1436
  expect(error).to be_a(Ably::Exceptions::ConnectionFailed)
1018
- expect(error.code).to eql(80002)
1437
+ expect(error.code).to eql(50000)
1019
1438
  stop_reactor
1020
1439
  end
1021
1440
  fake_error connection_error
@@ -1024,10 +1443,9 @@ describe Ably::Realtime::Channel, :event_machine do
1024
1443
  end
1025
1444
 
1026
1445
  context 'a :detached channel' do
1027
- it 'remains in the :detached state' do
1446
+ it 'remains in the :detached state (#RTL3a)' do
1028
1447
  channel.attach do
1029
1448
  channel.on(:failed) { raise 'Failed state should not have been reached' }
1030
- channel.on(:error) { raise 'Error should not have been emitted' }
1031
1449
 
1032
1450
  channel.detach do
1033
1451
  EventMachine.add_timer(1) do
@@ -1044,11 +1462,10 @@ describe Ably::Realtime::Channel, :event_machine do
1044
1462
  context 'a :failed channel' do
1045
1463
  let(:original_error) { RuntimeError.new }
1046
1464
 
1047
- 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
1048
1466
  channel.attach do
1049
- channel.on(:error) do
1467
+ channel.on(:failed) do
1050
1468
  channel.on(:failed) { raise 'Failed state should not have been reached' }
1051
- channel.on(:error) { raise 'Error should not have been emitted' }
1052
1469
 
1053
1470
  EventMachine.add_timer(1) do
1054
1471
  expect(channel).to be_failed
@@ -1065,11 +1482,13 @@ describe Ably::Realtime::Channel, :event_machine do
1065
1482
  end
1066
1483
 
1067
1484
  context 'a channel ATTACH request' do
1068
- it 'raises an exception' do
1485
+ it 'fails the deferrable (#RTL4b)' do
1069
1486
  client.connect do
1070
1487
  client.connection.once(:failed) do
1071
- expect { channel.attach }.to raise_error Ably::Exceptions::InvalidStateChange
1072
- stop_reactor
1488
+ channel.attach.errback do |error|
1489
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
1490
+ stop_reactor
1491
+ end
1073
1492
  end
1074
1493
  fake_error connection_error
1075
1494
  end
@@ -1079,7 +1498,7 @@ describe Ably::Realtime::Channel, :event_machine do
1079
1498
 
1080
1499
  context ':closed' do
1081
1500
  context 'an :attached channel' do
1082
- it 'transitions state to :detached' do
1501
+ it 'transitions state to :detached (#RTL3b)' do
1083
1502
  channel.attach do
1084
1503
  channel.on(:detached) do
1085
1504
  stop_reactor
@@ -1089,12 +1508,26 @@ describe Ably::Realtime::Channel, :event_machine do
1089
1508
  end
1090
1509
  end
1091
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
+
1092
1526
  context 'a :detached channel' do
1093
- it 'remains in the :detached state' do
1527
+ it 'remains in the :detached state (#RTL3b)' do
1094
1528
  channel.attach do
1095
1529
  channel.detach do
1096
1530
  channel.on(:detached) { raise 'Detached state should not have been reached' }
1097
- channel.on(:error) { raise 'Error should not have been emitted' }
1098
1531
 
1099
1532
  EventMachine.add_timer(1) do
1100
1533
  expect(channel).to be_detached
@@ -1111,11 +1544,10 @@ describe Ably::Realtime::Channel, :event_machine do
1111
1544
  let(:client_options) { default_options.merge(log_level: :fatal) }
1112
1545
  let(:original_error) { Ably::Models::ErrorInfo.new(message: 'Error') }
1113
1546
 
1114
- 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
1115
1548
  channel.attach do
1116
- channel.once(:error) do
1549
+ channel.once(:failed) do
1117
1550
  channel.on(:detached) { raise 'Detached state should not have been reached' }
1118
- channel.on(:error) { raise 'Error should not have been emitted' }
1119
1551
 
1120
1552
  EventMachine.add_timer(1) do
1121
1553
  expect(channel).to be_failed
@@ -1132,11 +1564,13 @@ describe Ably::Realtime::Channel, :event_machine do
1132
1564
  end
1133
1565
 
1134
1566
  context 'a channel ATTACH request when connection CLOSED' do
1135
- it 'raises an exception' do
1567
+ it 'fails the deferrable (#RTL4b)' do
1136
1568
  client.connect do
1137
1569
  client.connection.once(:closed) do
1138
- expect { channel.attach }.to raise_error Ably::Exceptions::InvalidStateChange
1139
- stop_reactor
1570
+ channel.attach.errback do |error|
1571
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
1572
+ stop_reactor
1573
+ end
1140
1574
  end
1141
1575
  client.close
1142
1576
  end
@@ -1144,11 +1578,13 @@ describe Ably::Realtime::Channel, :event_machine do
1144
1578
  end
1145
1579
 
1146
1580
  context 'a channel ATTACH request when connection CLOSING' do
1147
- it 'raises an exception' do
1581
+ it 'fails the deferrable (#RTL4b)' do
1148
1582
  client.connect do
1149
1583
  client.connection.once(:closing) do
1150
- expect { channel.attach }.to raise_error Ably::Exceptions::InvalidStateChange
1151
- stop_reactor
1584
+ channel.attach.errback do |error|
1585
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
1586
+ stop_reactor
1587
+ end
1152
1588
  end
1153
1589
  client.close
1154
1590
  end
@@ -1157,25 +1593,48 @@ describe Ably::Realtime::Channel, :event_machine do
1157
1593
  end
1158
1594
 
1159
1595
  context ':suspended' do
1160
- context 'an :attached channel' do
1161
- 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
1162
1609
 
1163
- it 'transitions state to :detached' do
1610
+ context 'an :attached channel' do
1611
+ it 'transitions state to :suspended (#RTL3c)' do
1164
1612
  channel.attach do
1165
- channel.on(:detached) do
1613
+ channel.on(:suspended) do
1166
1614
  stop_reactor
1167
1615
  end
1168
1616
  client.connection.transition_state_machine :suspended
1169
1617
  end
1170
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
1171
1631
  end
1172
1632
 
1173
1633
  context 'a :detached channel' do
1174
- it 'remains in the :detached state' do
1634
+ it 'remains in the :detached state (#RTL3c)' do
1175
1635
  channel.attach do
1176
1636
  channel.detach do
1177
1637
  channel.on(:detached) { raise 'Detached state should not have been reached' }
1178
- channel.on(:error) { raise 'Error should not have been emitted' }
1179
1638
 
1180
1639
  EventMachine.add_timer(1) do
1181
1640
  expect(channel).to be_detached
@@ -1192,11 +1651,10 @@ describe Ably::Realtime::Channel, :event_machine do
1192
1651
  let(:original_error) { RuntimeError.new }
1193
1652
  let(:client_options) { default_options.merge(log_level: :fatal) }
1194
1653
 
1195
- 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
1196
1655
  channel.attach do
1197
- channel.once(:error) do
1656
+ channel.once(:failed) do
1198
1657
  channel.on(:detached) { raise 'Detached state should not have been reached' }
1199
- channel.on(:error) { raise 'Error should not have been emitted' }
1200
1658
 
1201
1659
  EventMachine.add_timer(1) do
1202
1660
  expect(channel).to be_failed
@@ -1212,18 +1670,140 @@ describe Ably::Realtime::Channel, :event_machine do
1212
1670
  end
1213
1671
  end
1214
1672
 
1215
- context 'a channel ATTACH request when connection SUSPENDED' do
1673
+ context 'a channel ATTACH request when connection SUSPENDED (#RTL4b)' do
1216
1674
  let(:client_options) { default_options.merge(log_level: :fatal) }
1217
1675
 
1218
- it 'raises an exception' do
1676
+ it 'fails the deferrable' do
1219
1677
  client.connect do
1220
1678
  client.connection.once(:suspended) do
1221
- expect { channel.attach }.to raise_error Ably::Exceptions::InvalidStateChange
1222
- 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
1223
1699
  end
1224
1700
  client.connection.transition_state_machine :suspended
1225
1701
  end
1226
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
1227
1807
  end
1228
1808
  end
1229
1809
  end
@@ -1247,6 +1827,7 @@ describe Ably::Realtime::Channel, :event_machine do
1247
1827
  context 'ChannelStateChange object' do
1248
1828
  it 'has current state' do
1249
1829
  channel.on(:attached) do |channel_state_change|
1830
+ expect(channel_state_change.current).to be_a(Ably::Realtime::Channel::STATE)
1250
1831
  expect(channel_state_change.current).to eq(:attached)
1251
1832
  stop_reactor
1252
1833
  end
@@ -1255,12 +1836,22 @@ describe Ably::Realtime::Channel, :event_machine do
1255
1836
 
1256
1837
  it 'has a previous state' do
1257
1838
  channel.on(:attached) do |channel_state_change|
1839
+ expect(channel_state_change.previous).to be_a(Ably::Realtime::Channel::STATE)
1258
1840
  expect(channel_state_change.previous).to eq(:attaching)
1259
1841
  stop_reactor
1260
1842
  end
1261
1843
  channel.attach
1262
1844
  end
1263
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
+
1264
1855
  it 'contains a private API protocol_message attribute that is used for special state change events', :api_private do
1265
1856
  channel.on(:attached) do |channel_state_change|
1266
1857
  expect(channel_state_change.protocol_message).to be_a(Ably::Models::ProtocolMessage)
@@ -1294,6 +1885,266 @@ describe Ably::Realtime::Channel, :event_machine do
1294
1885
  end
1295
1886
  end
1296
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
1297
2148
  end
1298
2149
  end
1299
2150
  end