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,181 +1,655 @@
1
+ # encoding: utf-8
1
2
  require 'spec_helper'
2
3
 
3
- describe Ably::Realtime::Connection do
4
- include RSpec::EventMachine
5
-
4
+ describe Ably::Realtime::Connection, :event_machine do
6
5
  let(:connection) { client.connection }
7
6
 
8
- [:json, :msgpack].each do |protocol|
9
- context "over #{protocol}" do
10
- let(:default_options) do
11
- { api_key: api_key, environment: environment, protocol: protocol }
7
+ vary_by_protocol do
8
+ let(:default_options) do
9
+ { api_key: api_key, environment: environment, protocol: protocol }
10
+ end
11
+
12
+ let(:client_options) { default_options }
13
+ let(:client) { Ably::Realtime::Client.new(client_options) }
14
+
15
+ before(:example) do
16
+ EventMachine.add_shutdown_hook do
17
+ connection.off # minimise side effects of callbacks from finished test calling stop_reactor
12
18
  end
19
+ end
13
20
 
14
- let(:client) do
15
- Ably::Realtime::Client.new(default_options)
21
+ context 'intialization' do
22
+ it 'connects automatically' do
23
+ connection.on(:connected) do
24
+ expect(connection.state).to eq(:connected)
25
+ stop_reactor
26
+ end
16
27
  end
17
28
 
18
- context 'with API key' do
19
- it 'connects automatically' do
20
- run_reactor do
21
- connection.on(:connected) do
22
- expect(connection.state).to eq(:connected)
23
- expect(client.auth.auth_params[:key_id]).to_not be_nil
24
- expect(client.auth.auth_params[:access_token]).to be_nil
25
- stop_reactor
26
- end
29
+ context 'with :connect_automatically option set to false' do
30
+ let(:client) do
31
+ Ably::Realtime::Client.new(default_options.merge(connect_automatically: false))
32
+ end
33
+
34
+ it 'does not connect automatically' do
35
+ EventMachine.add_timer(1) do
36
+ expect(connection).to be_initialized
37
+ stop_reactor
38
+ end
39
+ client
40
+ end
41
+
42
+ it 'connects when method #connect is called' do
43
+ connection.connect do
44
+ expect(connection).to be_connected
45
+ stop_reactor
27
46
  end
28
47
  end
29
48
  end
30
49
 
31
- context 'with client_id resulting in token auth' do
32
- let(:default_options) do
33
- { api_key: api_key, environment: environment, protocol: protocol, client_id: SecureRandom.hex, log_level: :debug }
50
+ context 'with token auth' do
51
+ before do
52
+ # Reduce token expiry buffer to zero so that a token expired? predicate is exact
53
+ # Normally there is a buffer so that a token expiring soon is considered expired
54
+ stub_const 'Ably::Models::Token::TOKEN_EXPIRY_BUFFER', 0
34
55
  end
35
- it 'connects automatically' do
36
- run_reactor do
37
- connection.on(:connected) do
38
- expect(connection.state).to eq(:connected)
39
- expect(client.auth.auth_params[:access_token]).to_not be_nil
40
- expect(client.auth.auth_params[:key_id]).to be_nil
41
- stop_reactor
56
+
57
+ context 'for renewable tokens' do
58
+ context 'that are valid for the duration of the test' do
59
+ context 'with valid pre authorised token expiring in the future' do
60
+ it 'uses the existing token created by Auth' do
61
+ client.auth.authorise(ttl: 300)
62
+ expect(client.auth).to_not receive(:request_token)
63
+ connection.once(:connected) do
64
+ stop_reactor
65
+ end
66
+ end
67
+ end
68
+
69
+ context 'with implicit authorisation' do
70
+ let(:client_options) { default_options.merge(client_id: 'force_token_auth') }
71
+
72
+ it 'uses the token created by the implicit authorisation' do
73
+ expect(client.auth).to receive(:request_token).once.and_call_original
74
+
75
+ connection.once(:connected) do
76
+ stop_reactor
77
+ end
78
+ end
42
79
  end
43
80
  end
44
- end
45
- end
46
81
 
47
- context 'initialization phases' do
48
- let(:phases) { [:initialized, :connecting, :connected] }
49
- let(:events_triggered) { [] }
82
+ context 'that expire' do
83
+ let(:client_options) { default_options.merge(log_level: :none) }
50
84
 
51
- it 'are triggered in order' do
52
- test_expectation = Proc.new do
53
- expect(events_triggered).to eq(phases)
54
- stop_reactor
85
+ before do
86
+ client.auth.authorise(ttl: ttl)
87
+ end
88
+
89
+ context 'opening a new connection' do
90
+ context 'with recently expired token' do
91
+ let(:ttl) { 2 }
92
+
93
+ it 'renews the token on connect' do
94
+ sleep ttl + 0.1
95
+ expect(client.auth.current_token).to be_expired
96
+ expect(client.auth).to receive(:authorise).once.and_call_original
97
+ connection.once(:connected) do
98
+ expect(client.auth.current_token).to_not be_expired
99
+ stop_reactor
100
+ end
101
+ end
102
+ end
103
+
104
+ context 'with immediately expiring token' do
105
+ let(:ttl) { 0.01 }
106
+
107
+ it 'renews the token on connect, and only makes one subsequent attempt to obtain a new token' do
108
+ expect(client.auth).to receive(:authorise).twice.and_call_original
109
+ connection.once(:disconnected) do
110
+ connection.once(:failed) do |error|
111
+ expect(error.code).to eql(40140) # token expired
112
+ stop_reactor
113
+ end
114
+ end
115
+ end
116
+
117
+ it 'uses the primary host for subsequent connection and auth requests' do
118
+ EventMachine.add_timer(1) do # wait for token to expire
119
+ connection.once(:disconnected) do
120
+ expect(client.rest_client.connection).to receive(:post).with(/requestToken$/, anything).and_call_original
121
+
122
+ expect(client.rest_client).to_not receive(:fallback_connection)
123
+ expect(client).to_not receive(:fallback_endpoint)
124
+
125
+ connection.once(:failed) do
126
+ connection.off
127
+ stop_reactor
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ context 'when connected with a valid non-expired token' do
136
+ context 'that then expires following the connection being opened' do
137
+ let(:ttl) { 2 }
138
+ let(:channel) { client.channel('test') }
139
+
140
+ context 'the server' do
141
+ it 'disconnects the client, and the client automatically renews the token and then reconnects', em_timeout: 10 do
142
+ expect(client.auth.current_token).to_not be_expired
143
+
144
+ channel.attach
145
+ original_token = client.auth.current_token
146
+
147
+ connection.once(:connected) do
148
+ started_at = Time.now
149
+ connection.once(:disconnected) do |error|
150
+ expect(Time.now - started_at >= ttl)
151
+ expect(original_token).to be_expired
152
+ expect(error.code).to eql(40140) # token expired
153
+ connection.once(:connected) do
154
+ expect(client.auth.current_token).to_not be_expired
155
+ stop_reactor
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ skip 'retains connection state'
163
+ skip 'changes state to failed if a new token cannot be issued'
164
+ end
165
+ end
55
166
  end
167
+ end
168
+
169
+ context 'for non-renewable tokens' do
170
+ context 'that are expired' do
171
+ let!(:expired_token) do
172
+ Ably::Realtime::Client.new(default_options).auth.request_token(ttl: 0.01)
173
+ end
56
174
 
57
- run_reactor do
58
- phases.each do |phase|
59
- connection.on(phase) do
60
- events_triggered << phase
61
- test_expectation.call if events_triggered.length == phases.length
175
+ context 'opening a new connection' do
176
+ let(:client_options) { default_options.merge(api_key: nil, token_id: expired_token.id, log_level: :none) }
177
+
178
+ it 'transitions state to failed', em_timeout: 10 do
179
+ EventMachine.add_timer(1) do # wait for token to expire
180
+ expect(expired_token).to be_expired
181
+ connection.once(:connected) { raise 'Connection should never connect as token has expired' }
182
+ connection.once(:failed) do
183
+ expect(client.connection.error_reason.code).to eql(40140)
184
+ stop_reactor
185
+ end
186
+ end
62
187
  end
63
188
  end
189
+
190
+ context 'when connected' do
191
+ skip 'transitions state to failed'
192
+ end
64
193
  end
65
194
  end
66
195
  end
67
196
 
68
- skip '#close disconnects, closes the connection immediately and changes the connection state to closed'
197
+ end
69
198
 
70
- specify '#close(graceful: true) gracefully waits for the server to close the connection' do
71
- run_reactor(8) do
72
- connection.close
73
- connection.on(:closed) do
74
- expect(connection.state).to eq(:closed)
75
- stop_reactor
199
+ context 'initialization state changes' do
200
+ let(:phases) { [:connecting, :connected] }
201
+ let(:events_triggered) { [] }
202
+ let(:test_expectation) do
203
+ Proc.new do
204
+ expect(events_triggered).to eq(phases)
205
+ stop_reactor
206
+ end
207
+ end
208
+
209
+ def expect_ordered_phases
210
+ phases.each do |phase|
211
+ connection.on(phase) do
212
+ events_triggered << phase
213
+ test_expectation.call if events_triggered.length == phases.length
76
214
  end
77
215
  end
78
216
  end
79
217
 
80
- it 'echoes a heart beat with #ping' do
81
- run_reactor do
82
- connection.on(:connected) do
83
- connection.ping do |time_elapsed|
84
- expect(time_elapsed).to be > 0
218
+ context 'with implicit #connect' do
219
+ it 'are triggered in order' do
220
+ expect_ordered_phases
221
+ end
222
+ end
223
+
224
+ context 'with explicit #connect' do
225
+ it 'are triggered in order' do
226
+ expect_ordered_phases
227
+ connection.connect
228
+ end
229
+ end
230
+ end
231
+
232
+ context '#connect' do
233
+ it 'returns a Deferrable' do
234
+ expect(connection.connect).to be_a(EventMachine::Deferrable)
235
+ stop_reactor
236
+ end
237
+
238
+ it 'calls the Deferrable callback on success' do
239
+ connection.connect.callback do |connection|
240
+ expect(connection).to be_a(Ably::Realtime::Connection)
241
+ expect(connection.state).to eq(:connected)
242
+ stop_reactor
243
+ end
244
+ end
245
+
246
+ context 'when already connected' do
247
+ it 'does nothing and no further state changes are emitted' do
248
+ connection.once(:connected) do
249
+ connection.once_state_changed { raise 'State should not have changed' }
250
+ 3.times { connection.connect }
251
+ EventMachine.add_timer(1) do
252
+ expect(connection).to be_connected
253
+ connection.off
85
254
  stop_reactor
86
255
  end
87
256
  end
88
257
  end
89
258
  end
90
259
 
91
- skip 'connects, closes gracefully and reconnects on #connect'
260
+ describe 'once connected' do
261
+ let(:connection2) { Ably::Realtime::Client.new(client_options).connection }
262
+
263
+ describe 'connection#id' do
264
+ it 'is a string' do
265
+ connection.connect do
266
+ expect(connection.id).to be_a(String)
267
+ stop_reactor
268
+ end
269
+ end
270
+
271
+ it 'is unique from the connection#key' do
272
+ connection.connect do
273
+ expect(connection.id).to_not eql(connection.key)
274
+ stop_reactor
275
+ end
276
+ end
277
+
278
+ it 'is unique for every connection' do
279
+ when_all(connection.connect, connection2.connect) do
280
+ expect(connection.id).to_not eql(connection2.id)
281
+ stop_reactor
282
+ end
283
+ end
284
+ end
285
+
286
+ describe 'connection#key' do
287
+ it 'is a string' do
288
+ connection.connect do
289
+ expect(connection.key).to be_a(String)
290
+ stop_reactor
291
+ end
292
+ end
293
+
294
+ it 'is unique from the connection#id' do
295
+ connection.connect do
296
+ expect(connection.key).to_not eql(connection.id)
297
+ stop_reactor
298
+ end
299
+ end
92
300
 
93
- it 'connects, closes the connection, and then reconnects with a new connection ID' do
94
- run_reactor(15) do
301
+ it 'is unique for every connection' do
302
+ when_all(connection.connect, connection2.connect) do
303
+ expect(connection.key).to_not eql(connection2.key)
304
+ stop_reactor
305
+ end
306
+ end
307
+ end
308
+ end
309
+
310
+ context 'following a previous connection being opened and closed' do
311
+ it 'reconnects and is provided with a new connection ID and connection key from the server' do
95
312
  connection.connect do
96
- connection_id = connection.id
313
+ connection_id = connection.id
314
+ connection_key = connection.key
315
+
97
316
  connection.close do
98
317
  connection.connect do
99
318
  expect(connection.id).to_not eql(connection_id)
319
+ expect(connection.key).to_not eql(connection_key)
100
320
  stop_reactor
101
321
  end
102
322
  end
103
323
  end
104
324
  end
105
325
  end
326
+ end
327
+
328
+ context '#close' do
329
+ it 'returns a Deferrable' do
330
+ connection.connect do
331
+ expect(connection.close).to be_a(EventMachine::Deferrable)
332
+ stop_reactor
333
+ end
334
+ end
106
335
 
107
- context 'failures' do
108
- context 'with invalid app part of the key' do
109
- let(:missing_key) { 'not_an_app.invalid_key_id:invalid_key_value' }
110
- let(:client) do
111
- Ably::Realtime::Client.new(default_options.merge(api_key: missing_key))
336
+ it 'calls the Deferrable callback on success' do
337
+ connection.connect do
338
+ connection.close.callback do |connection|
339
+ expect(connection).to be_a(Ably::Realtime::Connection)
340
+ expect(connection.state).to eq(:closed)
341
+ stop_reactor
112
342
  end
343
+ end
344
+ end
113
345
 
114
- it 'enters the failed state and returns a not found error' do
115
- run_reactor do
116
- connection.on(:failed) do |error|
117
- expect(connection.state).to eq(:failed)
118
- expect(error.status).to eq(404)
346
+ context 'when already closed' do
347
+ it 'does nothing and no further state changes are emitted' do
348
+ connection.once(:connected) do
349
+ connection.close do
350
+ connection.once_state_changed { raise 'State should not have changed' }
351
+ 3.times { connection.close }
352
+ EventMachine.add_timer(1) do
353
+ expect(connection).to be_closed
354
+ connection.off
119
355
  stop_reactor
120
356
  end
121
357
  end
122
358
  end
123
359
  end
360
+ end
124
361
 
125
- context 'with invalid key ID part of the key' do
126
- let(:invalid_key) { "#{app_id}.invalid_key_id:invalid_key_value" }
127
- let(:client) do
128
- Ably::Realtime::Client.new(default_options.merge(api_key: invalid_key))
362
+ context 'when connection state is' do
363
+ let(:events) { Hash.new }
364
+
365
+ def log_connection_changes
366
+ connection.on(:closing) { events[:closing_emitted] = true }
367
+ connection.on(:error) { events[:error_emitted] = true }
368
+
369
+ connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
370
+ events[:closed_message_from_server_received] = true if protocol_message.action == :closed
129
371
  end
372
+ end
130
373
 
131
- it 'enters the failed state and returns an authorization error' do
132
- run_reactor do
133
- connection.on(:failed) do |error|
134
- expect(connection.state).to eq(:failed)
135
- expect(error.status).to eq(401)
374
+ context ':initialized' do
375
+ it 'changes the connection state to :closing and then immediately :closed without sending a ProtocolMessage CLOSE' do
376
+ connection.on(:closed) do
377
+ expect(connection.state).to eq(:closed)
378
+
379
+ EventMachine.add_timer(1) do # allow for all subscribers on incoming message bes
380
+ expect(events[:error_emitted]).to_not eql(true)
381
+ expect(events[:closed_message_from_server_received]).to_not eql(true)
382
+ expect(events[:closing_emitted]).to eql(true)
136
383
  stop_reactor
137
384
  end
138
385
  end
386
+
387
+ log_connection_changes
388
+ connection.close
139
389
  end
140
390
  end
141
391
 
142
- context 'with invalid WebSocket host' do
143
- let(:client) do
144
- Ably::Realtime::Client.new(default_options.merge(ws_host: 'non.existent.host'))
392
+ context ':connected' do
393
+ it 'changes the connection state to :closing and waits for the server to confirm connection is :closed with a ProtocolMessage' do
394
+ connection.on(:connected) do
395
+ connection.on(:closed) do
396
+ EventMachine.add_timer(1) do # allow for all subscribers on incoming message bus
397
+ expect(events[:error_emitted]).to_not eql(true)
398
+ expect(events[:closed_message_from_server_received]).to eql(true)
399
+ expect(events[:closing_emitted]).to eql(true)
400
+ stop_reactor
401
+ end
402
+ end
403
+
404
+ log_connection_changes
405
+ connection.close
406
+ end
145
407
  end
146
408
 
147
- it 'enters the failed state and returns an authorization error' do
148
- run_reactor do
149
- connection.on(:failed) do |error|
150
- expect(connection.state).to eq(:failed)
151
- expect(error.code).to eq(80000)
152
- expect(error.status).to be_nil
153
- stop_reactor
409
+ context 'with an unresponsive connection' do
410
+ let(:stubbed_timeout) { 2 }
411
+
412
+ before do
413
+ stub_const 'Ably::Realtime::Connection::ConnectionManager::TIMEOUTS',
414
+ Ably::Realtime::Connection::ConnectionManager::TIMEOUTS.merge(close: stubbed_timeout)
415
+
416
+ connection.on(:connected) do
417
+ # Prevent all incoming & outgoing ProtocolMessages from being processed by the client library
418
+ connection.__outgoing_protocol_msgbus__.unsubscribe
419
+ connection.__incoming_protocol_msgbus__.unsubscribe
420
+ end
421
+ end
422
+
423
+ it 'force closes the connection when a :closed ProtocolMessage response is not received' do
424
+ connection.on(:connected) do
425
+ close_requested_at = Time.now
426
+
427
+ connection.on(:closed) do
428
+ expect(Time.now - close_requested_at).to be >= stubbed_timeout
429
+ expect(connection.state).to eq(:closed)
430
+ expect(events[:error_emitted]).to_not eql(true)
431
+ expect(events[:closed_message_from_server_received]).to_not eql(true)
432
+ expect(events[:closing_emitted]).to eql(true)
433
+ stop_reactor
434
+ end
435
+
436
+ log_connection_changes
437
+ connection.close
154
438
  end
155
439
  end
156
440
  end
157
441
  end
158
442
  end
443
+ end
444
+
445
+ context '#ping' do
446
+ it 'echoes a heart beat' do
447
+ connection.on(:connected) do
448
+ connection.ping do |time_elapsed|
449
+ expect(time_elapsed).to be > 0
450
+ stop_reactor
451
+ end
452
+ end
453
+ end
454
+
455
+ context 'when not connected' do
456
+ it 'raises an exception' do
457
+ expect { connection.ping }.to raise_error RuntimeError, /Cannot send a ping when connection/
458
+ stop_reactor
459
+ end
460
+ end
461
+ end
462
+
463
+ context 'recovery' do
464
+ let(:channel_name) { random_str }
465
+ let(:channel) { client.channel(channel_name) }
466
+ let(:publishing_client) do
467
+ Ably::Realtime::Client.new(client_options)
468
+ end
469
+ let(:publishing_client_channel) { publishing_client.channel(channel_name) }
470
+ let(:client_options) { default_options.merge(log_level: :fatal) }
471
+
472
+ before do
473
+ # Reconfigure client library retry periods and timeouts so that tests run quickly
474
+ stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
475
+ Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
476
+ disconnected: { retry_every: 0.1, max_time_in_state: 0.2 },
477
+ suspended: { retry_every: 0.1, max_time_in_state: 0.2 },
478
+ )
479
+ end
480
+
481
+ describe '#recovery_key' do
482
+ def self.available_states
483
+ [:connecting, :connected, :disconnected, :suspended, :failed]
484
+ end
485
+ let(:available_states) { self.class.available_states}
486
+ let(:states) { Hash.new }
487
+ let(:client_options) { default_options.merge(log_level: :none) }
488
+
489
+ it 'is composed of connection id and serial that is kept up to date with each message sent' do
490
+ connection.on(:connected) do
491
+ expected_serial = -1
492
+ expect(connection.id).to_not be_nil
493
+ expect(connection.serial).to eql(expected_serial)
494
+
495
+ client.channel('test').attach do |channel|
496
+ channel.publish('event', 'data') do
497
+ expected_serial += 1 # attach message received
498
+ expect(connection.serial).to eql(expected_serial)
499
+
500
+ channel.publish('event', 'data') do
501
+ expected_serial += 1 # attach message received
502
+ expect(connection.serial).to eql(expected_serial)
503
+ stop_reactor
504
+ end
505
+ end
506
+ end
507
+ end
508
+ end
509
+
510
+ it "is available when connection is in one of the states: #{available_states.join(', ')}" do
511
+ connection.once(:connected) do
512
+ allow(client).to receive(:endpoint).and_return(
513
+ URI::Generic.build(
514
+ scheme: 'wss',
515
+ host: 'this.host.does.not.exist.com'
516
+ )
517
+ )
159
518
 
160
- it 'opens many connections simultaneously' do
161
- run_reactor(15) do
162
- count, connected_ids = 25, []
519
+ connection.transition_state_machine! :disconnected
520
+ end
163
521
 
164
- clients = count.times.map do
165
- Ably::Realtime::Client.new(default_options)
522
+ available_states.each do |state|
523
+ connection.on(state) do
524
+ states[state.to_sym] = true if connection.recovery_key
525
+ end
166
526
  end
167
527
 
168
- clients.each do |client|
169
- client.connection.on(:connected) do
170
- connected_ids << client.connection.id
528
+ connection.once(:failed) do
529
+ expect(states.keys).to match_array(available_states)
530
+ stop_reactor
531
+ end
532
+ end
533
+
534
+ it 'is nil when connection is explicitly CLOSED' do
535
+ connection.once(:connected) do
536
+ connection.close do
537
+ expect(connection.recovery_key).to be_nil
538
+ stop_reactor
539
+ end
540
+ end
541
+ end
542
+ end
543
+
544
+ context "opening a new connection using a recently disconnected connection's #recovery_key" do
545
+ context 'connection#id and connection#key after recovery' do
546
+ let(:client_options) { default_options.merge(log_level: :none) }
171
547
 
172
- if connected_ids.count == 25
173
- expect(connected_ids.uniq.count).to eql(25)
548
+ it 'remain the same' do
549
+ previous_connection_id = nil
550
+ previous_connection_key = nil
551
+
552
+ connection.once(:connected) do
553
+ previous_connection_id = connection.id
554
+ previous_connection_key = connection.key
555
+ connection.transition_state_machine! :failed
556
+ end
557
+
558
+ connection.once(:failed) do
559
+ recover_client = Ably::Realtime::Client.new(default_options.merge(recover: client.connection.recovery_key))
560
+ recover_client.connection.on(:connected) do
561
+ expect(recover_client.connection.key).to eql(previous_connection_key)
562
+ expect(recover_client.connection.id).to eql(previous_connection_id)
174
563
  stop_reactor
175
564
  end
176
565
  end
177
566
  end
178
567
  end
568
+
569
+ context 'when messages have been sent whilst the old connection is disconnected' do
570
+ describe 'the new connection' do
571
+ let(:client_options) { default_options.merge(log_level: :none) }
572
+
573
+ it 'recovers server-side queued messages' do
574
+ channel.attach do |message|
575
+ connection.transition_state_machine! :failed
576
+ end
577
+
578
+ connection.on(:failed) do
579
+ publishing_client_channel.publish('event', 'message') do
580
+ recover_client = Ably::Realtime::Client.new(default_options.merge(recover: client.connection.recovery_key))
581
+ recover_client.channel(channel_name).attach do |recover_client_channel|
582
+ recover_client_channel.subscribe('event') do |message|
583
+ expect(message.data).to eql('message')
584
+ stop_reactor
585
+ end
586
+ end
587
+ end
588
+ end
589
+ end
590
+ end
591
+ end
592
+ end
593
+
594
+ context 'with :recover option' do
595
+ context 'with invalid syntax' do
596
+ let(:invaid_client_options) { default_options.merge(recover: 'invalid') }
597
+
598
+ it 'raises an exception' do
599
+ expect { Ably::Realtime::Client.new(invaid_client_options) }.to raise_error ArgumentError, /Recover/
600
+ stop_reactor
601
+ end
602
+ end
603
+
604
+ context 'with invalid value' do
605
+ let(:client_options) { default_options.merge(recover: 'invalid:key', log_level: :fatal) }
606
+
607
+ skip 'triggers an error on the connection object, sets the #error_reason and connects anyway' do
608
+ connection.on(:error) do |error|
609
+ expect(connection.state).to eq(:connected)
610
+ expect(connection.error_reason.message).to match(/Recover/)
611
+ expect(connection.error_reason).to eql(error)
612
+ stop_reactor
613
+ end
614
+ end
615
+ end
616
+ end
617
+ end
618
+
619
+ context 'with many connections simultaneously', em_timeout: 15 do
620
+ let(:connection_count) { 40 }
621
+ let(:connection_ids) { [] }
622
+ let(:connection_keys) { [] }
623
+
624
+ it 'opens each with a unique connection#id and connection#key' do
625
+ connection_count.times.map do
626
+ Ably::Realtime::Client.new(client_options)
627
+ end.each do |client|
628
+ client.connection.on(:connected) do
629
+ connection_ids << client.connection.id
630
+ connection_keys << client.connection.key
631
+ next unless connection_ids.count == connection_count
632
+
633
+ expect(connection_ids.uniq.count).to eql(connection_count)
634
+ expect(connection_keys.uniq.count).to eql(connection_count)
635
+ stop_reactor
636
+ end
637
+ end
638
+ end
639
+ end
640
+
641
+ context 'when a state transition is unsupported' do
642
+ let(:client_options) { default_options.merge(log_level: :none) } # silence FATAL errors
643
+
644
+ it 'emits a StateChangeError' do
645
+ connection.connect do
646
+ connection.transition_state_machine :initialized
647
+ end
648
+
649
+ connection.on(:error) do |error|
650
+ expect(error).to be_a(Ably::Exceptions::StateChangeError)
651
+ stop_reactor
652
+ end
179
653
  end
180
654
  end
181
655
  end