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
@@ -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