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.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.ruby-version.old +1 -0
- data/.travis.yml +0 -2
- data/Rakefile +22 -4
- data/SPEC.md +1676 -0
- data/ably.gemspec +1 -1
- data/lib/ably.rb +0 -8
- data/lib/ably/auth.rb +54 -46
- data/lib/ably/exceptions.rb +19 -5
- data/lib/ably/logger.rb +1 -1
- data/lib/ably/models/error_info.rb +1 -1
- data/lib/ably/models/idiomatic_ruby_wrapper.rb +11 -9
- data/lib/ably/models/message.rb +15 -12
- data/lib/ably/models/message_encoders/base.rb +6 -5
- data/lib/ably/models/message_encoders/base64.rb +1 -0
- data/lib/ably/models/message_encoders/cipher.rb +6 -3
- data/lib/ably/models/message_encoders/json.rb +1 -0
- data/lib/ably/models/message_encoders/utf8.rb +2 -9
- data/lib/ably/models/nil_logger.rb +20 -0
- data/lib/ably/models/paginated_resource.rb +5 -2
- data/lib/ably/models/presence_message.rb +21 -12
- data/lib/ably/models/protocol_message.rb +22 -6
- data/lib/ably/modules/ably.rb +11 -0
- data/lib/ably/modules/async_wrapper.rb +2 -0
- data/lib/ably/modules/conversions.rb +23 -3
- data/lib/ably/modules/encodeable.rb +2 -1
- data/lib/ably/modules/enum.rb +2 -0
- data/lib/ably/modules/event_emitter.rb +7 -1
- data/lib/ably/modules/event_machine_helpers.rb +2 -0
- data/lib/ably/modules/http_helpers.rb +2 -0
- data/lib/ably/modules/model_common.rb +12 -2
- data/lib/ably/modules/state_emitter.rb +76 -0
- data/lib/ably/modules/state_machine.rb +53 -0
- data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
- data/lib/ably/modules/uses_state_machine.rb +74 -0
- data/lib/ably/realtime.rb +4 -2
- data/lib/ably/realtime/channel.rb +51 -58
- data/lib/ably/realtime/channel/channel_manager.rb +91 -0
- data/lib/ably/realtime/channel/channel_state_machine.rb +68 -0
- data/lib/ably/realtime/client.rb +70 -26
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +31 -13
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
- data/lib/ably/realtime/connection.rb +135 -92
- data/lib/ably/realtime/connection/connection_manager.rb +216 -33
- data/lib/ably/realtime/connection/connection_state_machine.rb +30 -73
- data/lib/ably/realtime/models/nil_channel.rb +10 -1
- data/lib/ably/realtime/presence.rb +336 -92
- data/lib/ably/rest.rb +2 -2
- data/lib/ably/rest/channel.rb +13 -4
- data/lib/ably/rest/client.rb +138 -38
- data/lib/ably/rest/middleware/logger.rb +24 -3
- data/lib/ably/rest/presence.rb +12 -7
- data/lib/ably/version.rb +1 -1
- data/spec/acceptance/realtime/channel_history_spec.rb +101 -85
- data/spec/acceptance/realtime/channel_spec.rb +461 -120
- data/spec/acceptance/realtime/client_spec.rb +119 -0
- data/spec/acceptance/realtime/connection_failures_spec.rb +499 -0
- data/spec/acceptance/realtime/connection_spec.rb +571 -97
- data/spec/acceptance/realtime/message_spec.rb +347 -333
- data/spec/acceptance/realtime/presence_history_spec.rb +35 -40
- data/spec/acceptance/realtime/presence_spec.rb +769 -239
- data/spec/acceptance/realtime/stats_spec.rb +14 -22
- data/spec/acceptance/realtime/time_spec.rb +16 -20
- data/spec/acceptance/rest/auth_spec.rb +425 -364
- data/spec/acceptance/rest/base_spec.rb +108 -176
- data/spec/acceptance/rest/channel_spec.rb +89 -89
- data/spec/acceptance/rest/channels_spec.rb +30 -32
- data/spec/acceptance/rest/client_spec.rb +273 -0
- data/spec/acceptance/rest/encoders_spec.rb +185 -0
- data/spec/acceptance/rest/message_spec.rb +186 -163
- data/spec/acceptance/rest/presence_spec.rb +150 -111
- data/spec/acceptance/rest/stats_spec.rb +45 -40
- data/spec/acceptance/rest/time_spec.rb +8 -10
- data/spec/rspec_config.rb +10 -1
- data/spec/shared/client_initializer_behaviour.rb +212 -0
- data/spec/{support/model_helper.rb → shared/model_behaviour.rb} +6 -6
- data/spec/{support/protocol_msgbus_helper.rb → shared/protocol_msgbus_behaviour.rb} +1 -1
- data/spec/spec_helper.rb +9 -0
- data/spec/support/api_helper.rb +11 -0
- data/spec/support/event_machine_helper.rb +101 -3
- data/spec/support/markdown_spec_formatter.rb +90 -0
- data/spec/support/private_api_formatter.rb +36 -0
- data/spec/support/protocol_helper.rb +32 -0
- data/spec/support/random_helper.rb +15 -0
- data/spec/support/test_app.rb +4 -0
- data/spec/unit/auth_spec.rb +68 -0
- data/spec/unit/logger_spec.rb +77 -66
- data/spec/unit/models/error_info_spec.rb +1 -1
- data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +2 -3
- data/spec/unit/models/message_encoders/base64_spec.rb +2 -2
- data/spec/unit/models/message_encoders/cipher_spec.rb +2 -2
- data/spec/unit/models/message_encoders/utf8_spec.rb +2 -46
- data/spec/unit/models/message_spec.rb +160 -15
- data/spec/unit/models/paginated_resource_spec.rb +29 -27
- data/spec/unit/models/presence_message_spec.rb +163 -20
- data/spec/unit/models/protocol_message_spec.rb +43 -8
- data/spec/unit/modules/async_wrapper_spec.rb +2 -3
- data/spec/unit/modules/conversions_spec.rb +1 -1
- data/spec/unit/modules/enum_spec.rb +2 -3
- data/spec/unit/modules/event_emitter_spec.rb +62 -5
- data/spec/unit/modules/state_emitter_spec.rb +283 -0
- data/spec/unit/realtime/channel_spec.rb +107 -2
- data/spec/unit/realtime/channels_spec.rb +1 -0
- data/spec/unit/realtime/client_spec.rb +8 -48
- data/spec/unit/realtime/connection_spec.rb +3 -3
- data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +2 -2
- data/spec/unit/realtime/presence_spec.rb +13 -4
- data/spec/unit/realtime/realtime_spec.rb +0 -11
- data/spec/unit/realtime/websocket_transport_spec.rb +2 -2
- data/spec/unit/rest/channel_spec.rb +109 -0
- data/spec/unit/rest/channels_spec.rb +4 -3
- data/spec/unit/rest/client_spec.rb +30 -125
- data/spec/unit/rest/rest_spec.rb +10 -0
- data/spec/unit/util/crypto_spec.rb +10 -5
- data/spec/unit/util/pub_sub_spec.rb +5 -5
- metadata +44 -12
- data/spec/integration/modules/state_emitter_spec.rb +0 -80
- 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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
15
|
-
|
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
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
32
|
-
|
33
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
48
|
-
|
49
|
-
let(:events_triggered) { [] }
|
82
|
+
context 'that expire' do
|
83
|
+
let(:client_options) { default_options.merge(log_level: :none) }
|
50
84
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
197
|
+
end
|
69
198
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
-
|
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
|
-
|
94
|
-
|
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
|
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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
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 '
|
143
|
-
|
144
|
-
|
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
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
-
|
161
|
-
|
162
|
-
count, connected_ids = 25, []
|
519
|
+
connection.transition_state_machine! :disconnected
|
520
|
+
end
|
163
521
|
|
164
|
-
|
165
|
-
|
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
|
-
|
169
|
-
|
170
|
-
|
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
|
-
|
173
|
-
|
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
|