ably 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.ruby-version.old +1 -0
  4. data/.travis.yml +0 -2
  5. data/Rakefile +22 -4
  6. data/SPEC.md +1676 -0
  7. data/ably.gemspec +1 -1
  8. data/lib/ably.rb +0 -8
  9. data/lib/ably/auth.rb +54 -46
  10. data/lib/ably/exceptions.rb +19 -5
  11. data/lib/ably/logger.rb +1 -1
  12. data/lib/ably/models/error_info.rb +1 -1
  13. data/lib/ably/models/idiomatic_ruby_wrapper.rb +11 -9
  14. data/lib/ably/models/message.rb +15 -12
  15. data/lib/ably/models/message_encoders/base.rb +6 -5
  16. data/lib/ably/models/message_encoders/base64.rb +1 -0
  17. data/lib/ably/models/message_encoders/cipher.rb +6 -3
  18. data/lib/ably/models/message_encoders/json.rb +1 -0
  19. data/lib/ably/models/message_encoders/utf8.rb +2 -9
  20. data/lib/ably/models/nil_logger.rb +20 -0
  21. data/lib/ably/models/paginated_resource.rb +5 -2
  22. data/lib/ably/models/presence_message.rb +21 -12
  23. data/lib/ably/models/protocol_message.rb +22 -6
  24. data/lib/ably/modules/ably.rb +11 -0
  25. data/lib/ably/modules/async_wrapper.rb +2 -0
  26. data/lib/ably/modules/conversions.rb +23 -3
  27. data/lib/ably/modules/encodeable.rb +2 -1
  28. data/lib/ably/modules/enum.rb +2 -0
  29. data/lib/ably/modules/event_emitter.rb +7 -1
  30. data/lib/ably/modules/event_machine_helpers.rb +2 -0
  31. data/lib/ably/modules/http_helpers.rb +2 -0
  32. data/lib/ably/modules/model_common.rb +12 -2
  33. data/lib/ably/modules/state_emitter.rb +76 -0
  34. data/lib/ably/modules/state_machine.rb +53 -0
  35. data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
  36. data/lib/ably/modules/uses_state_machine.rb +74 -0
  37. data/lib/ably/realtime.rb +4 -2
  38. data/lib/ably/realtime/channel.rb +51 -58
  39. data/lib/ably/realtime/channel/channel_manager.rb +91 -0
  40. data/lib/ably/realtime/channel/channel_state_machine.rb +68 -0
  41. data/lib/ably/realtime/client.rb +70 -26
  42. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +31 -13
  43. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  44. data/lib/ably/realtime/connection.rb +135 -92
  45. data/lib/ably/realtime/connection/connection_manager.rb +216 -33
  46. data/lib/ably/realtime/connection/connection_state_machine.rb +30 -73
  47. data/lib/ably/realtime/models/nil_channel.rb +10 -1
  48. data/lib/ably/realtime/presence.rb +336 -92
  49. data/lib/ably/rest.rb +2 -2
  50. data/lib/ably/rest/channel.rb +13 -4
  51. data/lib/ably/rest/client.rb +138 -38
  52. data/lib/ably/rest/middleware/logger.rb +24 -3
  53. data/lib/ably/rest/presence.rb +12 -7
  54. data/lib/ably/version.rb +1 -1
  55. data/spec/acceptance/realtime/channel_history_spec.rb +101 -85
  56. data/spec/acceptance/realtime/channel_spec.rb +461 -120
  57. data/spec/acceptance/realtime/client_spec.rb +119 -0
  58. data/spec/acceptance/realtime/connection_failures_spec.rb +499 -0
  59. data/spec/acceptance/realtime/connection_spec.rb +571 -97
  60. data/spec/acceptance/realtime/message_spec.rb +347 -333
  61. data/spec/acceptance/realtime/presence_history_spec.rb +35 -40
  62. data/spec/acceptance/realtime/presence_spec.rb +769 -239
  63. data/spec/acceptance/realtime/stats_spec.rb +14 -22
  64. data/spec/acceptance/realtime/time_spec.rb +16 -20
  65. data/spec/acceptance/rest/auth_spec.rb +425 -364
  66. data/spec/acceptance/rest/base_spec.rb +108 -176
  67. data/spec/acceptance/rest/channel_spec.rb +89 -89
  68. data/spec/acceptance/rest/channels_spec.rb +30 -32
  69. data/spec/acceptance/rest/client_spec.rb +273 -0
  70. data/spec/acceptance/rest/encoders_spec.rb +185 -0
  71. data/spec/acceptance/rest/message_spec.rb +186 -163
  72. data/spec/acceptance/rest/presence_spec.rb +150 -111
  73. data/spec/acceptance/rest/stats_spec.rb +45 -40
  74. data/spec/acceptance/rest/time_spec.rb +8 -10
  75. data/spec/rspec_config.rb +10 -1
  76. data/spec/shared/client_initializer_behaviour.rb +212 -0
  77. data/spec/{support/model_helper.rb → shared/model_behaviour.rb} +6 -6
  78. data/spec/{support/protocol_msgbus_helper.rb → shared/protocol_msgbus_behaviour.rb} +1 -1
  79. data/spec/spec_helper.rb +9 -0
  80. data/spec/support/api_helper.rb +11 -0
  81. data/spec/support/event_machine_helper.rb +101 -3
  82. data/spec/support/markdown_spec_formatter.rb +90 -0
  83. data/spec/support/private_api_formatter.rb +36 -0
  84. data/spec/support/protocol_helper.rb +32 -0
  85. data/spec/support/random_helper.rb +15 -0
  86. data/spec/support/test_app.rb +4 -0
  87. data/spec/unit/auth_spec.rb +68 -0
  88. data/spec/unit/logger_spec.rb +77 -66
  89. data/spec/unit/models/error_info_spec.rb +1 -1
  90. data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +2 -3
  91. data/spec/unit/models/message_encoders/base64_spec.rb +2 -2
  92. data/spec/unit/models/message_encoders/cipher_spec.rb +2 -2
  93. data/spec/unit/models/message_encoders/utf8_spec.rb +2 -46
  94. data/spec/unit/models/message_spec.rb +160 -15
  95. data/spec/unit/models/paginated_resource_spec.rb +29 -27
  96. data/spec/unit/models/presence_message_spec.rb +163 -20
  97. data/spec/unit/models/protocol_message_spec.rb +43 -8
  98. data/spec/unit/modules/async_wrapper_spec.rb +2 -3
  99. data/spec/unit/modules/conversions_spec.rb +1 -1
  100. data/spec/unit/modules/enum_spec.rb +2 -3
  101. data/spec/unit/modules/event_emitter_spec.rb +62 -5
  102. data/spec/unit/modules/state_emitter_spec.rb +283 -0
  103. data/spec/unit/realtime/channel_spec.rb +107 -2
  104. data/spec/unit/realtime/channels_spec.rb +1 -0
  105. data/spec/unit/realtime/client_spec.rb +8 -48
  106. data/spec/unit/realtime/connection_spec.rb +3 -3
  107. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +2 -2
  108. data/spec/unit/realtime/presence_spec.rb +13 -4
  109. data/spec/unit/realtime/realtime_spec.rb +0 -11
  110. data/spec/unit/realtime/websocket_transport_spec.rb +2 -2
  111. data/spec/unit/rest/channel_spec.rb +109 -0
  112. data/spec/unit/rest/channels_spec.rb +4 -3
  113. data/spec/unit/rest/client_spec.rb +30 -125
  114. data/spec/unit/rest/rest_spec.rb +10 -0
  115. data/spec/unit/util/crypto_spec.rb +10 -5
  116. data/spec/unit/util/pub_sub_spec.rb +5 -5
  117. metadata +44 -12
  118. data/spec/integration/modules/state_emitter_spec.rb +0 -80
  119. data/spec/integration/rest/auth.rb +0 -9
@@ -1,211 +1,554 @@
1
+ # encoding: utf-8
1
2
  require 'spec_helper'
2
- require 'securerandom'
3
3
 
4
- describe Ably::Realtime::Channel do
5
- include RSpec::EventMachine
4
+ describe Ably::Realtime::Channel, :event_machine do
5
+ vary_by_protocol do
6
+ let(:default_options) { { api_key: api_key, environment: environment, protocol: protocol } }
7
+ let(:client_options) { default_options }
8
+
9
+ let(:client) { Ably::Realtime::Client.new(client_options) }
10
+ let(:channel_name) { random_str }
11
+ let(:payload) { random_str }
12
+ let(:channel) { client.channel(channel_name) }
13
+ let(:messages) { [] }
14
+
15
+ describe 'initialization' do
16
+ context 'with :connect_automatically option set to false on connection' do
17
+ let(:client) do
18
+ Ably::Realtime::Client.new(default_options.merge(connect_automatically: false))
19
+ end
6
20
 
7
- [:msgpack, :json].each do |protocol|
8
- context "over #{protocol}" do
9
- let(:default_options) { { api_key: api_key, environment: environment, protocol: protocol } }
21
+ it 'remains initialized when accessing a channel' do
22
+ client.channel('test')
23
+ EventMachine.add_timer(2) do
24
+ expect(client.connection).to be_initialized
25
+ stop_reactor
26
+ end
27
+ end
10
28
 
11
- let(:client) { Ably::Realtime::Client.new(default_options) }
12
- let(:channel_name) { SecureRandom.hex(2) }
13
- let(:payload) { SecureRandom.hex(4) }
14
- let(:channel) { client.channel(channel_name) }
15
- let(:messages) { [] }
29
+ it 'opens a connection implicitly on #attach' do
30
+ client.channel('test').attach do
31
+ expect(client.connection).to be_connected
32
+ stop_reactor
33
+ end
34
+ end
16
35
 
17
- it 'attaches to a channel' do
18
- run_reactor do
36
+ it 'opens a connection implicitly when accessing #presence' do
37
+ client.channel('test').tap do |channel|
38
+ channel.on(:attached) do
39
+ expect(client.connection).to be_connected
40
+ stop_reactor
41
+ end
42
+ channel.presence
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ describe '#attach' do
49
+ it 'emits attaching then attached events' do
50
+ channel.once(:attaching) do
51
+ channel.once(:attached) do
52
+ stop_reactor
53
+ end
54
+ end
55
+
56
+ channel.attach
57
+ end
58
+
59
+ it 'ignores subsequent #attach calls but calls the success callback if provided' do
60
+ channel.once(:attaching) do
19
61
  channel.attach
20
- channel.on(:attached) do
21
- expect(channel.state).to eq(:attached)
62
+ channel.once(:attached) do
63
+ channel.attach do
64
+ stop_reactor
65
+ end
66
+ end
67
+ end
68
+
69
+ channel.attach
70
+ end
71
+
72
+ it 'attaches to a channel' do
73
+ channel.attach
74
+ channel.on(:attached) do
75
+ expect(channel.state).to eq(:attached)
76
+ stop_reactor
77
+ end
78
+ end
79
+
80
+ it 'attaches to a channel and calls the provided block' do
81
+ channel.attach do
82
+ expect(channel.state).to eq(:attached)
83
+ stop_reactor
84
+ end
85
+ end
86
+
87
+ it 'returns a Deferrable' do
88
+ expect(channel.attach).to be_a(EventMachine::Deferrable)
89
+ stop_reactor
90
+ end
91
+
92
+ it 'calls the Deferrable callback on success' do
93
+ channel.attach.callback do |channel|
94
+ expect(channel).to be_a(Ably::Realtime::Channel)
95
+ expect(channel.state).to eq(:attached)
96
+ stop_reactor
97
+ end
98
+ end
99
+
100
+ context 'when state is :failed' do
101
+ let(:client_options) { default_options.merge(log_level: :fatal) }
102
+
103
+ it 'reattaches' do
104
+ channel.attach do
105
+ channel.transition_state_machine :failed, RuntimeError.new
106
+ expect(channel).to be_failed
107
+ channel.attach do
108
+ expect(channel).to be_attached
109
+ stop_reactor
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ context 'when state is :detaching' do
116
+ it 'moves straight to attaching and skips detached' do
117
+ channel.once(:detaching) do
118
+ channel.once(:detached) { raise 'Detach should not have been reached' }
119
+
120
+ channel.once(:attaching) do
121
+ channel.once(:attached) do
122
+ channel.off
123
+ stop_reactor
124
+ end
125
+ end
126
+
127
+ channel.attach
128
+ end
129
+
130
+ channel.attach do
131
+ channel.detach
132
+ end
133
+ end
134
+ end
135
+
136
+ context 'with many connections and many channels on each simultaneously' do
137
+ let(:connection_count) { 30 }
138
+ let(:channel_count) { 10 }
139
+ let(:permutation_count) { connection_count * channel_count }
140
+ let(:channel_connection_ids) { [] }
141
+
142
+ it 'attaches all channels', em_timeout: 15 do
143
+ connection_count.times.map do
144
+ Ably::Realtime::Client.new(default_options)
145
+ end.each do |client|
146
+ channel_count.times.map do |index|
147
+ client.channel("channel-#{index}").attach do
148
+ channel_connection_ids << "#{client.connection.id}:#{index}"
149
+ next unless channel_connection_ids.count == permutation_count
150
+
151
+ expect(channel_connection_ids.uniq.count).to eql(permutation_count)
152
+ stop_reactor
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ context 'failure as a result of insufficient key permissions' do
160
+ let(:restricted_client) do
161
+ Ably::Realtime::Client.new(default_options.merge(api_key: restricted_api_key, log_level: :fatal))
162
+ end
163
+ let(:restricted_channel) { restricted_client.channel("cannot_subscribe") }
164
+
165
+ it 'triggers failed event' do
166
+ restricted_channel.attach
167
+ restricted_channel.on(:failed) do |error|
168
+ expect(restricted_channel.state).to eq(:failed)
169
+ expect(error.status).to eq(401)
170
+ stop_reactor
171
+ end
172
+ end
173
+
174
+ it 'calls the errback of the returned Deferrable' do
175
+ restricted_channel.attach.errback do |channel, error|
176
+ expect(restricted_channel.state).to eq(:failed)
177
+ expect(error.status).to eq(401)
178
+ stop_reactor
179
+ end
180
+ end
181
+
182
+ it 'triggers an error event' do
183
+ restricted_channel.attach
184
+ restricted_channel.on(:error) do |error|
185
+ expect(restricted_channel.state).to eq(:failed)
186
+ expect(error.status).to eq(401)
187
+ stop_reactor
188
+ end
189
+ end
190
+
191
+ it 'updates the error_reason' do
192
+ restricted_channel.attach
193
+ restricted_channel.on(:failed) do
194
+ expect(restricted_channel.error_reason.status).to eq(401)
195
+ stop_reactor
196
+ end
197
+ end
198
+ end
199
+ end
200
+
201
+ describe '#detach' do
202
+ it 'detaches from a channel' do
203
+ channel.attach do
204
+ channel.detach
205
+ channel.on(:detached) do
206
+ expect(channel.state).to eq(:detached)
207
+ stop_reactor
208
+ end
209
+ end
210
+ end
211
+
212
+ it 'detaches from a channel and calls the provided block' do
213
+ channel.attach do |chan|
214
+ chan.detach do |detached_chan|
215
+ expect(detached_chan.state).to eq(:detached)
216
+ stop_reactor
217
+ end
218
+ end
219
+ end
220
+
221
+ it 'emits :detaching then :detached events' do
222
+ channel.once(:detaching) do
223
+ channel.once(:detached) do
224
+ stop_reactor
225
+ end
226
+ end
227
+
228
+ channel.attach do
229
+ channel.detach
230
+ end
231
+ end
232
+
233
+ it 'returns a Deferrable' do
234
+ expect(channel.attach).to be_a(EventMachine::Deferrable)
235
+ stop_reactor
236
+ end
237
+
238
+ it 'calls the Deferrable callback on success' do
239
+ channel.attach do
240
+ channel.detach.callback do |channel|
241
+ expect(channel).to be_a(Ably::Realtime::Channel)
242
+ expect(channel.state).to eq(:detached)
22
243
  stop_reactor
23
244
  end
24
245
  end
25
246
  end
26
247
 
27
- it 'attaches to a channel with a block' do
28
- run_reactor do
248
+ context 'when state is :failed' do
249
+ let(:client_options) { default_options.merge(log_level: :fatal) }
250
+
251
+ it 'raises an exception' do
29
252
  channel.attach do
30
- expect(channel.state).to eq(:attached)
253
+ channel.transition_state_machine :failed, RuntimeError.new
254
+ expect(channel).to be_failed
255
+ expect { channel.detach }.to raise_error Ably::Exceptions::StateChangeError
31
256
  stop_reactor
32
257
  end
33
258
  end
34
259
  end
35
260
 
36
- it 'detaches from a channel with a block' do
37
- run_reactor do
38
- channel.attach do |chan|
39
- chan.detach do |detached_chan|
40
- expect(detached_chan.state).to eq(:detached)
41
- stop_reactor
261
+ context 'when state is :attaching' do
262
+ it 'moves straight to :detaching state and skips :attached' do
263
+ channel.once(:attaching) do
264
+ channel.once(:attached) { raise 'Attached should never be reached' }
265
+
266
+ channel.once(:detaching) do
267
+ channel.once(:detached) do
268
+ stop_reactor
269
+ end
42
270
  end
271
+
272
+ channel.detach
43
273
  end
274
+
275
+ channel.attach
44
276
  end
45
277
  end
46
278
 
47
- it 'publishes 3 messages once attached' do
48
- run_reactor do
49
- channel.attach do
50
- 3.times { channel.publish('event', payload) }
279
+ context 'when state is :detaching' do
280
+ it 'ignores subsequent #detach calls but calls the callback if provided' do
281
+ channel.once(:detaching) do
282
+ channel.detach
283
+ channel.once(:detached) do
284
+ channel.detach do
285
+ stop_reactor
286
+ end
287
+ end
51
288
  end
52
- channel.subscribe do |message|
53
- messages << message if message.data == payload
54
- stop_reactor if messages.length == 3
289
+
290
+ channel.attach do
291
+ channel.detach
55
292
  end
56
293
  end
294
+ end
295
+ end
57
296
 
58
- expect(messages.count).to eql(3)
297
+ describe 'channel recovery in :attaching state' do
298
+ context 'the transport is disconnected before the ATTACHED protocol message is received' do
299
+ skip 'attach times out and fails if not ATTACHED protocol message received'
300
+ skip 'channel is ATTACHED if ATTACHED protocol message is later received'
301
+ skip 'sends an ATTACH protocol message in response to a channel message being received on the attaching channel'
59
302
  end
303
+ end
60
304
 
61
- it 'publishes 3 messages from queue before attached' do
62
- run_reactor do
63
- 3.times { channel.publish('event', SecureRandom.hex) }
305
+ context '#publish' do
306
+ context 'when attached' do
307
+ it 'publishes messages' do
308
+ channel.attach do
309
+ 3.times { channel.publish('event', payload) }
310
+ end
64
311
  channel.subscribe do |message|
65
- messages << message if message.name == 'event'
66
- stop_reactor if messages.length == 3
312
+ messages << message if message.data == payload
313
+ stop_reactor if messages.count == 3
67
314
  end
68
315
  end
69
-
70
- expect(messages.count).to eql(3)
71
316
  end
72
317
 
73
- it 'publishes 3 messages from queue before attached in a single protocol message' do
74
- run_reactor do
75
- 3.times { channel.publish('event', SecureRandom.hex) }
318
+ context 'when not yet attached' do
319
+ it 'publishes queued messages once attached' do
320
+ 3.times { channel.publish('event', random_str) }
76
321
  channel.subscribe do |message|
77
322
  messages << message if message.name == 'event'
78
- stop_reactor if messages.length == 3
323
+ stop_reactor if messages.count == 3
79
324
  end
80
325
  end
81
326
 
82
- # All 3 messages should be batched into a single Protocol Message by the client library
83
- # message.id = "{protocol_message.id}:{protocol_message_index}"
327
+ it 'publishes queued messages within a single protocol message' do
328
+ 3.times { channel.publish('event', random_str) }
329
+ channel.subscribe do |message|
330
+ messages << message if message.name == 'event'
331
+ next unless messages.length == 3
84
332
 
85
- # Check that all messages share the same message_serial
86
- message_id = messages.map { |msg| msg.id.split(':')[0] }
87
- expect(message_id.uniq.count).to eql(1)
333
+ # All 3 messages should be batched into a single Protocol Message by the client library
334
+ # message.id = "{protocol_message.id}:{protocol_message_index}"
335
+ # Check that all messages share the same protocol_message.id
336
+ message_id = messages.map { |msg| msg.id.split(':')[0] }
337
+ expect(message_id.uniq.count).to eql(1)
88
338
 
89
- # Check that all messages use message index 0,1,2
90
- message_indexes = messages.map { |msg| msg.id.split(':')[1] }
91
- expect(message_indexes).to include("0", "1", "2")
339
+ # Check that messages use index 0,1,2 in the ID
340
+ message_indexes = messages.map { |msg| msg.id.split(':')[1] }
341
+ expect(message_indexes).to include("0", "1", "2")
342
+ stop_reactor
343
+ end
344
+ end
92
345
  end
346
+ end
93
347
 
94
- it 'subscribes and unsubscribes' do
95
- run_reactor do
348
+ describe '#subscribe' do
349
+ context 'with an event argument' do
350
+ it 'subscribes for a single event' do
96
351
  channel.subscribe('click') do |message|
97
- messages << message
352
+ expect(message.data).to eql('data')
353
+ stop_reactor
98
354
  end
99
- channel.attach do
100
- channel.unsubscribe('click')
101
- channel.publish('click', 'data')
102
- EventMachine.add_timer(2) do
103
- stop_reactor
104
- expect(messages.length).to eql(0)
105
- end
355
+ channel.publish('click', 'data')
356
+ end
357
+ end
358
+
359
+ context 'with no event argument' do
360
+ it 'subscribes for all events' do
361
+ channel.subscribe do |message|
362
+ expect(message.data).to eql('data')
363
+ stop_reactor
106
364
  end
365
+ channel.publish('click', 'data')
107
366
  end
108
367
  end
109
368
 
110
- it 'subscribes and unsubscribes from multiple channels' do
111
- run_reactor do
112
- click_callback = -> (message) { messages << message }
369
+ context 'many times with different event names' do
370
+ it 'filters events accordingly to each callback' do
371
+ click_callback = proc { |message| messages << message }
113
372
 
114
373
  channel.subscribe('click', &click_callback)
115
374
  channel.subscribe('move', &click_callback)
116
375
  channel.subscribe('press', &click_callback)
117
376
 
118
377
  channel.attach do
119
- channel.unsubscribe('click')
120
- channel.unsubscribe('move', &click_callback)
121
- channel.unsubscribe('press') { this_callback_is_not_subscribed_so_ignored }
122
-
123
378
  channel.publish('click', 'data')
124
379
  channel.publish('move', 'data')
125
- channel.publish('press', 'data')
380
+ channel.publish('press', 'data') do
381
+ EventMachine.add_timer(2) do
382
+ expect(messages.count).to eql(3)
383
+ stop_reactor
384
+ end
385
+ end
386
+ end
387
+ end
388
+ end
389
+ end
390
+
391
+ describe '#unsubscribe' do
392
+ context 'with an event argument' do
393
+ it 'unsubscribes for a single event' do
394
+ channel.subscribe('click') { raise 'Should not have been called' }
395
+ channel.unsubscribe('click')
126
396
 
127
- EventMachine.add_timer(2) do
397
+ channel.publish('click', 'data') do
398
+ EventMachine.add_timer(1) do
128
399
  stop_reactor
129
- # Only the press subscribe callback should still be subscribed
130
- expect(messages.length).to eql(1)
131
400
  end
132
401
  end
133
402
  end
134
403
  end
135
404
 
136
- it 'opens many connections and then many channels simultaneously' do
137
- run_reactor(15) do
138
- count, connected_ids = 25, []
405
+ context 'with no event argument' do
406
+ it 'unsubscribes for a single event' do
407
+ channel.subscribe { raise 'Should not have been called' }
408
+ channel.unsubscribe
139
409
 
140
- clients = count.times.map do
141
- Ably::Realtime::Client.new(default_options)
410
+ channel.publish('click', 'data') do
411
+ EventMachine.add_timer(1) do
412
+ stop_reactor
413
+ end
142
414
  end
415
+ end
416
+ end
417
+ end
143
418
 
144
- channels_opened = 0
145
- open_channels_on_clients = Proc.new do
146
- 5.times.each do |channel|
147
- clients.each do |client|
148
- client.channel("channel-#{channel}").attach do
149
- channels_opened += 1
150
- if channels_opened == clients.count * 5
151
- expect(channels_opened).to eql(clients.count * 5)
152
- stop_reactor
153
- end
154
- end
419
+ context 'when connection state changes to' do
420
+ context ':failed' do
421
+ let(:connection_error) { Ably::Exceptions::ConnectionError.new('forced failure', 500, 50000) }
422
+ let(:client_options) { default_options.merge(log_level: :none) }
423
+
424
+ def fake_error(error)
425
+ client.connection.manager.error_received_from_server error
426
+ end
427
+
428
+ context 'an :attached channel' do
429
+ it 'transitions state to :failed' do
430
+ channel.attach do
431
+ channel.on(:failed) do |error|
432
+ expect(error).to eql(connection_error)
433
+ stop_reactor
155
434
  end
435
+ fake_error connection_error
156
436
  end
157
437
  end
158
438
 
159
- clients.each do |client|
160
- client.connection.on(:connected) do
161
- connected_ids << client.connection.id
439
+ it 'triggers an error event on the channel' do
440
+ channel.attach do
441
+ channel.on(:error) do |error|
442
+ expect(error).to eql(connection_error)
443
+ stop_reactor
444
+ end
445
+ fake_error connection_error
446
+ end
447
+ end
162
448
 
163
- if connected_ids.count == 25
164
- expect(connected_ids.uniq.count).to eql(25)
165
- open_channels_on_clients.call
449
+ it 'updates the channel error_reason' do
450
+ channel.attach do
451
+ channel.on(:failed) do |error|
452
+ expect(channel.error_reason).to eql(connection_error)
453
+ stop_reactor
166
454
  end
455
+ fake_error connection_error
167
456
  end
168
457
  end
169
458
  end
170
- end
171
459
 
172
- it 'opens many connections and attaches to channels before connected' do
173
- run_reactor(15) do
174
- count, connected_ids = 25, []
460
+ context 'a :detached channel' do
461
+ it 'remains in the :detached state' do
462
+ channel.attach do
463
+ channel.on(:failed) { raise 'Failed state should not have been reached' }
464
+ channel.on(:error) { raise 'Error should not have been emitted' }
175
465
 
176
- clients = count.times.map do
177
- Ably::Realtime::Client.new(default_options)
466
+ channel.detach do
467
+ EventMachine.add_timer(1) do
468
+ expect(channel).to be_detached
469
+ stop_reactor
470
+ end
471
+
472
+ fake_error connection_error
473
+ end
474
+ end
178
475
  end
476
+ end
179
477
 
180
- channels_opened = 0
478
+ context 'a :failed channel' do
479
+ let(:original_error) { RuntimeError.new }
181
480
 
182
- clients.each do |client|
183
- 5.times.each do |channel|
184
- client.channel("channel-#{channel}").attach do
185
- channels_opened += 1
186
- if channels_opened == clients.count * 5
187
- expect(channels_opened).to eql(clients.count * 5)
481
+ it 'remains in the :failed state and ignores the failure error' do
482
+ channel.attach do
483
+ channel.on(:error) do
484
+ channel.on(:failed) { raise 'Failed state should not have been reached' }
485
+ channel.on(:error) { raise 'Error should not have been emitted' }
486
+
487
+ EventMachine.add_timer(1) do
488
+ expect(channel).to be_failed
489
+ expect(channel.error_reason).to eql(original_error)
188
490
  stop_reactor
189
491
  end
492
+
493
+ fake_error connection_error
190
494
  end
495
+
496
+ channel.transition_state_machine :failed, original_error
191
497
  end
192
498
  end
193
499
  end
194
500
  end
195
501
 
196
- context 'attach failure' do
197
- let(:restricted_client) do
198
- Ably::Realtime::Client.new(default_options.merge(api_key: restricted_api_key))
502
+ context ':closed' do
503
+ context 'an :attached channel' do
504
+ it 'transitions state to :detached' do
505
+ channel.attach do
506
+ channel.on(:detached) do
507
+ stop_reactor
508
+ end
509
+ client.connection.close
510
+ end
511
+ end
199
512
  end
200
- let(:restricted_channel) { restricted_client.channel("cannot_subscribe") }
201
513
 
202
- it 'triggers failed event' do
203
- run_reactor do
204
- restricted_channel.attach
205
- restricted_channel.on(:failed) do |error|
206
- expect(restricted_channel.state).to eq(:failed)
207
- expect(error.status).to eq(401)
208
- stop_reactor
514
+ context 'a :detached channel' do
515
+ it 'remains in the :detached state' do
516
+ channel.attach do
517
+ channel.detach do
518
+ channel.on(:detached) { raise 'Detached state should not have been reached' }
519
+ channel.on(:error) { raise 'Error should not have been emitted' }
520
+
521
+ EventMachine.add_timer(1) do
522
+ expect(channel).to be_detached
523
+ stop_reactor
524
+ end
525
+
526
+ client.connection.close
527
+ end
528
+ end
529
+ end
530
+ end
531
+
532
+ context 'a :failed channel' do
533
+ let(:original_error) { RuntimeError.new }
534
+ let(:client_options) { default_options.merge(log_level: :fatal) }
535
+
536
+ it 'remains in the :failed state and retains the error_reason' do
537
+ channel.attach do
538
+ channel.once(:error) do
539
+ channel.on(:detached) { raise 'Detached state should not have been reached' }
540
+ channel.on(:error) { raise 'Error should not have been emitted' }
541
+
542
+ EventMachine.add_timer(1) do
543
+ expect(channel).to be_failed
544
+ expect(channel.error_reason).to eql(original_error)
545
+ stop_reactor
546
+ end
547
+
548
+ client.connection.close
549
+ end
550
+
551
+ channel.transition_state_machine :failed, original_error
209
552
  end
210
553
  end
211
554
  end
@@ -213,5 +556,3 @@ describe Ably::Realtime::Channel do
213
556
  end
214
557
  end
215
558
  end
216
-
217
-