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
@@ -0,0 +1,119 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Ably::Realtime::Client, :event_machine do
5
+ vary_by_protocol do
6
+ let(:default_options) do
7
+ { api_key: api_key, environment: environment, protocol: protocol }
8
+ end
9
+
10
+ let(:client_options) { default_options }
11
+ let(:connection) { subject.connection }
12
+ let(:auth_params) { subject.auth.auth_params }
13
+
14
+ subject { Ably::Realtime::Client.new(client_options) }
15
+
16
+ context 'initialization' do
17
+ context 'basic auth' do
18
+ it 'is enabled by default with a provided :api_key option' do
19
+ connection.on(:connected) do
20
+ expect(auth_params[:key_id]).to_not be_nil
21
+ expect(auth_params[:access_token]).to be_nil
22
+ expect(subject.auth.current_token).to be_nil
23
+ stop_reactor
24
+ end
25
+ end
26
+
27
+ context ':tls option' do
28
+ context 'set to false to forec a plain-text connection' do
29
+ let(:client_options) { default_options.merge(tls: false, log_level: :none) }
30
+
31
+ it 'fails to connect because a private key cannot be sent over a non-secure connection' do
32
+ connection.on(:connected) { raise 'Should not have connected' }
33
+
34
+ connection.on(:failed) do |error|
35
+ expect(error).to be_a(Ably::Exceptions::InsecureRequestError)
36
+ stop_reactor
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ context 'token auth' do
44
+ [true, false].each do |tls_enabled|
45
+ context "with TLS #{tls_enabled ? 'enabled' : 'disabled'}" do
46
+ let(:capability) { { :foo => ["publish"] } }
47
+ let(:token) { Ably::Realtime::Client.new(default_options).auth.request_token(capability: capability) }
48
+ let(:client_options) { default_options.merge(token_id: token.id) }
49
+
50
+ context 'and a pre-generated Token provided with the :token_id option' do
51
+ it 'connects using token auth' do
52
+ connection.on(:connected) do
53
+ expect(auth_params[:access_token]).to_not be_nil
54
+ expect(auth_params[:key_id]).to be_nil
55
+ expect(subject.auth.current_token).to be_nil
56
+ stop_reactor
57
+ end
58
+ end
59
+ end
60
+
61
+ context 'with valid :api_key and :use_token_auth option set to true' do
62
+ let(:client_options) { default_options.merge(use_token_auth: true) }
63
+
64
+ it 'automatically authorises on connect and generates a token' do
65
+ connection.on(:connected) do
66
+ expect(subject.auth.current_token).to_not be_nil
67
+ expect(auth_params[:access_token]).to_not be_nil
68
+ stop_reactor
69
+ end
70
+ end
71
+ end
72
+
73
+ context 'with client_id' do
74
+ let(:client_options) do
75
+ default_options.merge(client_id: random_str)
76
+ end
77
+ it 'connects using token auth' do
78
+ run_reactor do
79
+ connection.on(:connected) do
80
+ expect(connection.state).to eq(:connected)
81
+ expect(auth_params[:access_token]).to_not be_nil
82
+ expect(auth_params[:key_id]).to be_nil
83
+ stop_reactor
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ context 'with token_request_block' do
92
+ let(:client_id) { random_str }
93
+ let(:auth) { subject.auth }
94
+
95
+ subject do
96
+ Ably::Realtime::Client.new(client_options) do
97
+ @block_called = true
98
+ auth.create_token_request(client_id: client_id)
99
+ end
100
+ end
101
+
102
+ it 'calls the block' do
103
+ connection.on(:connected) do
104
+ expect(@block_called).to eql(true)
105
+ stop_reactor
106
+ end
107
+ end
108
+
109
+ it 'uses the token request when requesting a new token' do
110
+ connection.on(:connected) do
111
+ expect(auth.current_token.client_id).to eql(client_id)
112
+ stop_reactor
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,499 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Ably::Realtime::Connection, 'failures', :event_machine do
5
+ let(:connection) { client.connection }
6
+
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) do
14
+ Ably::Realtime::Client.new(client_options)
15
+ end
16
+
17
+ context 'authentication failure' do
18
+ let(:client_options) do
19
+ default_options.merge(api_key: invalid_key, log_level: :none)
20
+ end
21
+
22
+ context 'when API key is invalid' do
23
+ context 'with invalid app part of the key' do
24
+ let(:invalid_key) { 'not_an_app.invalid_key_id:invalid_key_value' }
25
+
26
+ it 'enters the failed state and returns a not found error' do
27
+ connection.on(:failed) do |error|
28
+ expect(connection.state).to eq(:failed)
29
+ # TODO: Check error type is an InvalidToken exception
30
+ expect(error.status).to eq(404)
31
+ expect(error.code).to eq(40400) # not found
32
+ stop_reactor
33
+ end
34
+ end
35
+ end
36
+
37
+ context 'with invalid key ID part of the key' do
38
+ let(:invalid_key) { "#{app_id}.invalid_key_id:invalid_key_value" }
39
+
40
+ it 'enters the failed state and returns an authorization error' do
41
+ connection.on(:failed) do |error|
42
+ expect(connection.state).to eq(:failed)
43
+ # TODO: Check error type is a TokenNotFOund exception
44
+ expect(error.status).to eq(401)
45
+ expect(error.code).to eq(40400) # not found
46
+ stop_reactor
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ context 'automatic connection retry' do
54
+ let(:client_failure_options) { default_options.merge(log_level: :none) }
55
+
56
+ context 'with invalid WebSocket host' do
57
+ let(:retry_every_for_tests) { 0.2 }
58
+ let(:max_time_in_state_for_tests) { 0.6 }
59
+
60
+ before do
61
+ # Reconfigure client library retry periods and timeouts so that tests run quickly
62
+ stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
63
+ Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
64
+ disconnected: { retry_every: retry_every_for_tests, max_time_in_state: max_time_in_state_for_tests },
65
+ suspended: { retry_every: retry_every_for_tests, max_time_in_state: max_time_in_state_for_tests },
66
+ )
67
+ end
68
+
69
+ let(:expected_retry_attempts) { (max_time_in_state_for_tests / retry_every_for_tests).round }
70
+ let(:state_changes) { Hash.new { |hash, key| hash[key] = 0 } }
71
+ let(:timer) { Hash.new }
72
+
73
+ let(:client_options) do
74
+ client_failure_options.merge(realtime_host: 'non.existent.host')
75
+ end
76
+
77
+ def count_state_changes
78
+ EventMachine.next_tick do
79
+ %w(connecting disconnected failed suspended).each do |state|
80
+ connection.on(state.to_sym) { state_changes[state.to_sym] += 1 }
81
+ end
82
+ end
83
+ end
84
+
85
+ def start_timer
86
+ timer[:start] = Time.now
87
+ end
88
+
89
+ def time_passed
90
+ Time.now.to_f - timer[:start].to_f
91
+ end
92
+
93
+ context 'when disconnected' do
94
+ it 'enters the suspended state after multiple attempts to connect' do
95
+ connection.on(:failed) { raise 'Connection should not have reached :failed state yet' }
96
+
97
+ count_state_changes && start_timer
98
+
99
+ connection.once(:suspended) do
100
+ expect(connection.state).to eq(:suspended)
101
+
102
+ expect(state_changes[:connecting]).to eql(expected_retry_attempts)
103
+ expect(state_changes[:disconnected]).to eql(expected_retry_attempts)
104
+
105
+ expect(time_passed).to be > max_time_in_state_for_tests
106
+ stop_reactor
107
+ end
108
+ end
109
+
110
+ describe '#close' do
111
+ it 'transitions connection state to :closed' do
112
+ connection.on(:connected) { raise 'Connection should not have reached :connected state' }
113
+ connection.on(:failed) { raise 'Connection should not have reached :failed state yet' }
114
+
115
+ connection.once(:disconnected) do
116
+ expect(connection.state).to eq(:disconnected)
117
+
118
+ connection.on(:closed) do
119
+ expect(connection.state).to eq(:closed)
120
+ stop_reactor
121
+ end
122
+
123
+ connection.close
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ context 'when connection state is :suspended' do
130
+ it 'enters the failed state after multiple attempts' do
131
+ connection.on(:connected) { raise 'Connection should not have reached :connected state' }
132
+
133
+ connection.once(:suspended) do
134
+ count_state_changes && start_timer
135
+
136
+ connection.on(:failed) do
137
+ expect(connection.state).to eq(:failed)
138
+
139
+ expect(state_changes[:connecting]).to eql(expected_retry_attempts)
140
+ expect(state_changes[:suspended]).to eql(expected_retry_attempts)
141
+ expect(state_changes[:disconnected]).to eql(0)
142
+
143
+ expect(time_passed).to be > max_time_in_state_for_tests
144
+ stop_reactor
145
+ end
146
+ end
147
+ end
148
+
149
+ describe '#close' do
150
+ it 'transitions connection state to :closed' do
151
+ connection.on(:connected) { raise 'Connection should not have reached :connected state' }
152
+
153
+ connection.once(:suspended) do
154
+ expect(connection.state).to eq(:suspended)
155
+
156
+ connection.on(:closed) do
157
+ expect(connection.state).to eq(:closed)
158
+ stop_reactor
159
+ end
160
+
161
+ connection.close
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ context 'when connection state is :failed' do
168
+ describe '#close' do
169
+ it 'will not transition state to :close and raises a StateChangeError exception' do
170
+ connection.on(:connected) { raise 'Connection should not have reached :connected state' }
171
+
172
+ connection.once(:failed) do
173
+ expect(connection.state).to eq(:failed)
174
+ expect { connection.close }.to raise_error Ably::Exceptions::StateChangeError, /Unable to transition from failed => closing/
175
+ stop_reactor
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ context '#error_reason' do
182
+ [:disconnected, :suspended, :failed].each do |state|
183
+ it "contains the error when state is #{state}" do
184
+ connection.on(state) do |error|
185
+ expect(connection.error_reason).to eq(error)
186
+ expect(connection.error_reason.code).to eql(80000)
187
+ stop_reactor
188
+ end
189
+ end
190
+ end
191
+
192
+ it 'is reset to nil when :connected' do
193
+ connection.once(:disconnected) do |error|
194
+ # stub the host so that the connection connects
195
+ allow(connection).to receive(:host).and_return(TestApp.instance.host)
196
+ connection.once(:connected) do
197
+ expect(connection.error_reason).to be_nil
198
+ stop_reactor
199
+ end
200
+ end
201
+ end
202
+
203
+ it 'is reset to nil when :closed' do
204
+ connection.once(:disconnected) do |error|
205
+ connection.close do
206
+ expect(connection.error_reason).to be_nil
207
+ stop_reactor
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+
214
+ describe '#connect' do
215
+ let(:timeouts) { Ably::Realtime::Connection::ConnectionManager::TIMEOUTS }
216
+
217
+ before do
218
+ stub_const 'Ably::Realtime::Connection::ConnectionManager::TIMEOUTS',
219
+ Ably::Realtime::Connection::ConnectionManager::TIMEOUTS.merge(open: 1.5)
220
+
221
+ connection.on(:connected) { raise "Connection should not open in this test as CONNECTED ProtocolMessage is never received" }
222
+
223
+ connection.once(:connecting) do
224
+ # don't process any incoming ProtocolMessages so the connection never opens
225
+ connection.__incoming_protocol_msgbus__.unsubscribe
226
+ end
227
+ end
228
+
229
+ context 'connection opening times out' do
230
+ it 'attempts to reconnect' do
231
+ started_at = Time.now
232
+
233
+ connection.once(:disconnected) do
234
+ expect(Time.now.to_f - started_at.to_f).to be > timeouts.fetch(:open)
235
+ connection.once(:connecting) do
236
+ stop_reactor
237
+ end
238
+ end
239
+
240
+ connection.connect
241
+ end
242
+
243
+ it 'calls the errback of the returned Deferrable object when first connection attempt fails' do
244
+ connection.connect.errback do |error|
245
+ expect(connection.state).to eq(:disconnected)
246
+ stop_reactor
247
+ end
248
+ end
249
+
250
+ context 'when retry intervals are stubbed to attempt reconnection quickly' do
251
+ let(:client_options) { client_failure_options }
252
+
253
+ before do
254
+ # Reconfigure client library retry periods and timeouts so that tests run quickly
255
+ stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
256
+ Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
257
+ disconnected: { retry_every: 0.1, max_time_in_state: 0.2 },
258
+ suspended: { retry_every: 0.1, max_time_in_state: 0.2 },
259
+ )
260
+ end
261
+
262
+ it 'never calls the provided success block', em_timeout: 10 do
263
+ connection.connect do
264
+ raise 'success block should not have been called'
265
+ end
266
+
267
+ connection.once(:failed) do
268
+ stop_reactor
269
+ end
270
+ end
271
+ end
272
+ end
273
+ end
274
+ end
275
+
276
+ context 'connection resume' do
277
+ let(:channel_name) { random_str }
278
+ let(:channel) { client.channel(channel_name) }
279
+ let(:publishing_client) do
280
+ Ably::Realtime::Client.new(client_options)
281
+ end
282
+ let(:publishing_client_channel) { publishing_client.channel(channel_name) }
283
+ let(:client_options) { default_options.merge(log_level: :none) }
284
+
285
+ def fail_if_suspended_or_failed
286
+ connection.on(:suspended) { raise 'Connection should not have reached :suspended state' }
287
+ connection.on(:failed) { raise 'Connection should not have reached :failed state' }
288
+ end
289
+
290
+ context 'when DISCONNECTED ProtocolMessage received from the server' do
291
+ it 'reconnects automatically' do
292
+ fail_if_suspended_or_failed
293
+
294
+ connection.once(:connected) do
295
+ connection.once(:disconnected) do
296
+ connection.once(:connected) do
297
+ state_history = connection.state_history.map { |transition| transition[:state].to_sym }
298
+ expect(state_history).to eql([:connecting, :connected, :disconnected, :connecting, :connected])
299
+ stop_reactor
300
+ end
301
+ end
302
+ protocol_message = Ably::Models::ProtocolMessage.new(action: Ably::Models::ProtocolMessage::ACTION.Disconnected.to_i)
303
+ connection.__incoming_protocol_msgbus__.publish :protocol_message, protocol_message
304
+ end
305
+ end
306
+ end
307
+
308
+ context 'when websocket transport is closed' do
309
+ it 'reconnects automatically' do
310
+ fail_if_suspended_or_failed
311
+
312
+ connection.once(:connected) do
313
+ connection.once(:disconnected) do
314
+ connection.once(:connected) do
315
+ state_history = connection.state_history.map { |transition| transition[:state].to_sym }
316
+ expect(state_history).to eql([:connecting, :connected, :disconnected, :connecting, :connected])
317
+ stop_reactor
318
+ end
319
+ end
320
+ connection.transport.close_connection_after_writing
321
+ end
322
+ end
323
+ end
324
+
325
+ context 'after successfully reconnecting and resuming' do
326
+ it 'retains connection_id and connection_key' do
327
+ previous_connection_id = nil
328
+ previous_connection_key = nil
329
+
330
+ connection.once(:connected) do
331
+ previous_connection_id = connection.id
332
+ previous_connection_key = connection.key
333
+ connection.transport.close_connection_after_writing
334
+
335
+ connection.once(:connected) do
336
+ expect(connection.key).to eql(previous_connection_key)
337
+ expect(connection.id).to eql(previous_connection_id)
338
+ stop_reactor
339
+ end
340
+ end
341
+ end
342
+
343
+ it 'retains channel subscription state' do
344
+ messages_received = false
345
+
346
+ channel.subscribe('event') do |message|
347
+ expect(message.data).to eql('message')
348
+ stop_reactor
349
+ end
350
+
351
+ channel.attach do
352
+ publishing_client_channel.attach do
353
+ connection.transport.close_connection_after_writing
354
+
355
+ connection.once(:connected) do
356
+ publishing_client_channel.publish 'event', 'message'
357
+ end
358
+ end
359
+ end
360
+ end
361
+
362
+ context 'when messages were published whilst the client was disconnected' do
363
+ it 'receives the messages published whilst offline' do
364
+ messages_received = false
365
+
366
+ channel.subscribe('event') do |message|
367
+ expect(message.data).to eql('message')
368
+ messages_received = true
369
+ end
370
+
371
+ channel.attach do
372
+ publishing_client_channel.attach do
373
+ connection.transport.off # remove all event handlers that detect socket connection state has changed
374
+ connection.transport.close_connection_after_writing
375
+
376
+ publishing_client_channel.publish('event', 'message') do
377
+ EventMachine.add_timer(1) do
378
+ expect(messages_received).to eql(false)
379
+ # simulate connection dropped to re-establish web socket
380
+ connection.transition_state_machine :disconnected
381
+ end
382
+ end
383
+
384
+ # subsequent connection will receive message sent whilst disconnected
385
+ connection.once(:connected) do
386
+ EventMachine.add_timer(1) do
387
+ expect(messages_received).to eql(true)
388
+ stop_reactor
389
+ end
390
+ end
391
+ end
392
+ end
393
+ end
394
+ end
395
+ end
396
+ end
397
+
398
+ describe 'fallback host feature' do
399
+ let(:retry_every_for_tests) { 0.1 }
400
+ let(:max_time_in_state_for_tests) { 0.3 }
401
+
402
+ before do
403
+ # Reconfigure client library retry periods and timeouts so that tests run quickly
404
+ stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
405
+ Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
406
+ disconnected: { retry_every: retry_every_for_tests, max_time_in_state: max_time_in_state_for_tests },
407
+ suspended: { retry_every: retry_every_for_tests, max_time_in_state: max_time_in_state_for_tests },
408
+ )
409
+ end
410
+
411
+ let(:expected_retry_attempts) { (max_time_in_state_for_tests / retry_every_for_tests).round }
412
+ let(:retry_count_for_one_state) { 1 + expected_retry_attempts } # initial connect then disconnected
413
+ let(:retry_count_for_all_states) { 1 + expected_retry_attempts * 2 } # initial connection, disconnected & then suspended
414
+
415
+ context 'with custom realtime websocket host option' do
416
+ let(:expected_host) { 'this.host.does.not.exist' }
417
+ let(:client_options) { default_options.merge(realtime_host: expected_host, log_level: :none) }
418
+
419
+ it 'never uses a fallback host' do
420
+ expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
421
+ expect(host).to eql(expected_host)
422
+ raise EventMachine::ConnectionError
423
+ end
424
+
425
+ connection.on(:failed) do
426
+ stop_reactor
427
+ end
428
+ end
429
+ end
430
+
431
+ context 'with non-production environment' do
432
+ let(:environment) { 'sandbox' }
433
+ let(:expected_host) { "#{environment}-#{Ably::Realtime::Client::DOMAIN}" }
434
+ let(:client_options) { default_options.merge(environment: environment, log_level: :none) }
435
+
436
+ it 'never uses a fallback host' do
437
+ expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
438
+ expect(host).to eql(expected_host)
439
+ raise EventMachine::ConnectionError
440
+ end
441
+
442
+ connection.on(:failed) do
443
+ stop_reactor
444
+ end
445
+ end
446
+ end
447
+
448
+ context 'with production environment' do
449
+ let(:custom_hosts) { %w(A.ably-realtime.com B.ably-realtime.com) }
450
+ before do
451
+ stub_const 'Ably::FALLBACK_HOSTS', custom_hosts
452
+ end
453
+
454
+ let(:expected_host) { Ably::Realtime::Client::DOMAIN }
455
+ let(:client_options) { default_options.merge(environment: nil, log_level: :none) }
456
+
457
+ let(:fallback_hosts_used) { Array.new }
458
+
459
+ it 'uses a fallback host on every subsequent disconnected attempt until suspended' do
460
+ request = 0
461
+ expect(EventMachine).to receive(:connect).exactly(retry_count_for_one_state).times do |host|
462
+ if request == 0
463
+ expect(host).to eql(expected_host)
464
+ else
465
+ expect(custom_hosts).to include(host)
466
+ fallback_hosts_used << host
467
+ end
468
+ request += 1
469
+ raise EventMachine::ConnectionError
470
+ end
471
+
472
+ connection.on(:suspended) do
473
+ expect(fallback_hosts_used.uniq).to match_array(custom_hosts)
474
+ stop_reactor
475
+ end
476
+ end
477
+
478
+ it 'uses the primary host when suspended, and a fallback host on every subsequent suspended attempt' do
479
+ request = 0
480
+ expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
481
+ if request == 0 || request == expected_retry_attempts + 1
482
+ expect(host).to eql(expected_host)
483
+ else
484
+ expect(custom_hosts).to include(host)
485
+ fallback_hosts_used << host
486
+ end
487
+ request += 1
488
+ raise EventMachine::ConnectionError
489
+ end
490
+
491
+ connection.on(:failed) do
492
+ expect(fallback_hosts_used.uniq).to match_array(custom_hosts)
493
+ stop_reactor
494
+ end
495
+ end
496
+ end
497
+ end
498
+ end
499
+ end