ably-rest 0.9.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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