ably 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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