ably-rest 0.7.1 → 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (148) hide show
  1. checksums.yaml +13 -5
  2. data/.gitmodules +1 -1
  3. data/.rspec +1 -0
  4. data/.travis.yml +7 -3
  5. data/SPEC.md +495 -419
  6. data/ably-rest.gemspec +19 -5
  7. data/lib/ably-rest.rb +9 -1
  8. data/lib/submodules/ably-ruby/.gitignore +6 -0
  9. data/lib/submodules/ably-ruby/.rspec +1 -0
  10. data/lib/submodules/ably-ruby/.ruby-version.old +1 -0
  11. data/lib/submodules/ably-ruby/.travis.yml +10 -0
  12. data/lib/submodules/ably-ruby/Gemfile +4 -0
  13. data/lib/submodules/ably-ruby/LICENSE.txt +22 -0
  14. data/lib/submodules/ably-ruby/README.md +122 -0
  15. data/lib/submodules/ably-ruby/Rakefile +34 -0
  16. data/lib/submodules/ably-ruby/SPEC.md +1794 -0
  17. data/lib/submodules/ably-ruby/ably.gemspec +36 -0
  18. data/lib/submodules/ably-ruby/lib/ably.rb +12 -0
  19. data/lib/submodules/ably-ruby/lib/ably/auth.rb +438 -0
  20. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +69 -0
  21. data/lib/submodules/ably-ruby/lib/ably/logger.rb +102 -0
  22. data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +37 -0
  23. data/lib/submodules/ably-ruby/lib/ably/models/idiomatic_ruby_wrapper.rb +223 -0
  24. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +132 -0
  25. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/base.rb +108 -0
  26. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/base64.rb +40 -0
  27. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/cipher.rb +83 -0
  28. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/json.rb +34 -0
  29. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/utf8.rb +26 -0
  30. data/lib/submodules/ably-ruby/lib/ably/models/nil_logger.rb +20 -0
  31. data/lib/submodules/ably-ruby/lib/ably/models/paginated_resource.rb +173 -0
  32. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +147 -0
  33. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +210 -0
  34. data/lib/submodules/ably-ruby/lib/ably/models/stat.rb +161 -0
  35. data/lib/submodules/ably-ruby/lib/ably/models/token.rb +74 -0
  36. data/lib/submodules/ably-ruby/lib/ably/modules/ably.rb +15 -0
  37. data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +62 -0
  38. data/lib/submodules/ably-ruby/lib/ably/modules/channels_collection.rb +69 -0
  39. data/lib/submodules/ably-ruby/lib/ably/modules/conversions.rb +100 -0
  40. data/lib/submodules/ably-ruby/lib/ably/modules/encodeable.rb +69 -0
  41. data/lib/submodules/ably-ruby/lib/ably/modules/enum.rb +202 -0
  42. data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +128 -0
  43. data/lib/submodules/ably-ruby/lib/ably/modules/event_machine_helpers.rb +26 -0
  44. data/lib/submodules/ably-ruby/lib/ably/modules/http_helpers.rb +41 -0
  45. data/lib/submodules/ably-ruby/lib/ably/modules/message_pack.rb +14 -0
  46. data/lib/submodules/ably-ruby/lib/ably/modules/model_common.rb +41 -0
  47. data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +153 -0
  48. data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +57 -0
  49. data/lib/submodules/ably-ruby/lib/ably/modules/statesman_monkey_patch.rb +33 -0
  50. data/lib/submodules/ably-ruby/lib/ably/modules/uses_state_machine.rb +74 -0
  51. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +64 -0
  52. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +298 -0
  53. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +92 -0
  54. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +69 -0
  55. data/lib/submodules/ably-ruby/lib/ably/realtime/channels.rb +50 -0
  56. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +184 -0
  57. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +184 -0
  58. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +70 -0
  59. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +445 -0
  60. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +368 -0
  61. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_state_machine.rb +91 -0
  62. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +188 -0
  63. data/lib/submodules/ably-ruby/lib/ably/realtime/models/nil_channel.rb +30 -0
  64. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +564 -0
  65. data/lib/submodules/ably-ruby/lib/ably/rest.rb +43 -0
  66. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +104 -0
  67. data/lib/submodules/ably-ruby/lib/ably/rest/channels.rb +44 -0
  68. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +396 -0
  69. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/encoder.rb +49 -0
  70. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/exceptions.rb +41 -0
  71. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/external_exceptions.rb +24 -0
  72. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +17 -0
  73. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/logger.rb +58 -0
  74. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_json.rb +27 -0
  75. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_message_pack.rb +27 -0
  76. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +92 -0
  77. data/lib/submodules/ably-ruby/lib/ably/util/crypto.rb +105 -0
  78. data/lib/submodules/ably-ruby/lib/ably/util/pub_sub.rb +43 -0
  79. data/lib/submodules/ably-ruby/lib/ably/version.rb +3 -0
  80. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +154 -0
  81. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +558 -0
  82. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +119 -0
  83. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +575 -0
  84. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +785 -0
  85. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +457 -0
  86. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +55 -0
  87. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +1001 -0
  88. data/lib/submodules/ably-ruby/spec/acceptance/realtime/stats_spec.rb +23 -0
  89. data/lib/submodules/ably-ruby/spec/acceptance/realtime/time_spec.rb +27 -0
  90. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +564 -0
  91. data/lib/submodules/ably-ruby/spec/acceptance/rest/base_spec.rb +165 -0
  92. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +134 -0
  93. data/lib/submodules/ably-ruby/spec/acceptance/rest/channels_spec.rb +41 -0
  94. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +273 -0
  95. data/lib/submodules/ably-ruby/spec/acceptance/rest/encoders_spec.rb +185 -0
  96. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +247 -0
  97. data/lib/submodules/ably-ruby/spec/acceptance/rest/presence_spec.rb +292 -0
  98. data/lib/submodules/ably-ruby/spec/acceptance/rest/stats_spec.rb +172 -0
  99. data/lib/submodules/ably-ruby/spec/acceptance/rest/time_spec.rb +15 -0
  100. data/lib/submodules/ably-ruby/spec/resources/crypto-data-128.json +56 -0
  101. data/lib/submodules/ably-ruby/spec/resources/crypto-data-256.json +56 -0
  102. data/lib/submodules/ably-ruby/spec/rspec_config.rb +57 -0
  103. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +212 -0
  104. data/lib/submodules/ably-ruby/spec/shared/model_behaviour.rb +86 -0
  105. data/lib/submodules/ably-ruby/spec/shared/protocol_msgbus_behaviour.rb +36 -0
  106. data/lib/submodules/ably-ruby/spec/spec_helper.rb +20 -0
  107. data/lib/submodules/ably-ruby/spec/support/api_helper.rb +60 -0
  108. data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +104 -0
  109. data/lib/submodules/ably-ruby/spec/support/markdown_spec_formatter.rb +118 -0
  110. data/lib/submodules/ably-ruby/spec/support/private_api_formatter.rb +36 -0
  111. data/lib/submodules/ably-ruby/spec/support/protocol_helper.rb +32 -0
  112. data/lib/submodules/ably-ruby/spec/support/random_helper.rb +15 -0
  113. data/lib/submodules/ably-ruby/spec/support/rest_testapp_before_retry.rb +15 -0
  114. data/lib/submodules/ably-ruby/spec/support/test_app.rb +113 -0
  115. data/lib/submodules/ably-ruby/spec/unit/auth_spec.rb +68 -0
  116. data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +146 -0
  117. data/lib/submodules/ably-ruby/spec/unit/models/error_info_spec.rb +18 -0
  118. data/lib/submodules/ably-ruby/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +349 -0
  119. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/base64_spec.rb +181 -0
  120. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/cipher_spec.rb +260 -0
  121. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/json_spec.rb +135 -0
  122. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/utf8_spec.rb +56 -0
  123. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +389 -0
  124. data/lib/submodules/ably-ruby/spec/unit/models/paginated_resource_spec.rb +288 -0
  125. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +386 -0
  126. data/lib/submodules/ably-ruby/spec/unit/models/protocol_message_spec.rb +315 -0
  127. data/lib/submodules/ably-ruby/spec/unit/models/stat_spec.rb +113 -0
  128. data/lib/submodules/ably-ruby/spec/unit/models/token_spec.rb +86 -0
  129. data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +124 -0
  130. data/lib/submodules/ably-ruby/spec/unit/modules/conversions_spec.rb +72 -0
  131. data/lib/submodules/ably-ruby/spec/unit/modules/enum_spec.rb +272 -0
  132. data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +184 -0
  133. data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +283 -0
  134. data/lib/submodules/ably-ruby/spec/unit/realtime/channel_spec.rb +206 -0
  135. data/lib/submodules/ably-ruby/spec/unit/realtime/channels_spec.rb +81 -0
  136. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +30 -0
  137. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +33 -0
  138. data/lib/submodules/ably-ruby/spec/unit/realtime/incoming_message_dispatcher_spec.rb +36 -0
  139. data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +111 -0
  140. data/lib/submodules/ably-ruby/spec/unit/realtime/realtime_spec.rb +9 -0
  141. data/lib/submodules/ably-ruby/spec/unit/realtime/websocket_transport_spec.rb +25 -0
  142. data/lib/submodules/ably-ruby/spec/unit/rest/channel_spec.rb +109 -0
  143. data/lib/submodules/ably-ruby/spec/unit/rest/channels_spec.rb +79 -0
  144. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +53 -0
  145. data/lib/submodules/ably-ruby/spec/unit/rest/rest_spec.rb +10 -0
  146. data/lib/submodules/ably-ruby/spec/unit/util/crypto_spec.rb +87 -0
  147. data/lib/submodules/ably-ruby/spec/unit/util/pub_sub_spec.rb +86 -0
  148. metadata +182 -27
@@ -0,0 +1,785 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Ably::Realtime::Connection, :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) { 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
18
+ end
19
+ end
20
+
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
27
+ end
28
+
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
46
+ end
47
+ end
48
+ end
49
+
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
55
+ end
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
79
+ end
80
+ end
81
+
82
+ context 'that expire' do
83
+ let(:client_options) { default_options.merge(log_level: :none) }
84
+
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).at_least(: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).at_least(: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).
121
+ with(/requestToken$/, anything).
122
+ at_least(:once).
123
+ and_call_original
124
+
125
+ expect(client.rest_client).to_not receive(:fallback_connection)
126
+ expect(client).to_not receive(:fallback_endpoint)
127
+
128
+ connection.once(:failed) do
129
+ connection.off
130
+ stop_reactor
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ context 'when connected with a valid non-expired token' do
139
+ context 'that then expires following the connection being opened' do
140
+ let(:ttl) { 2 }
141
+ let(:channel) { client.channel('test') }
142
+
143
+ context 'the server' do
144
+ it 'disconnects the client, and the client automatically renews the token and then reconnects', em_timeout: 10 do
145
+ original_token = client.auth.current_token
146
+ expect(original_token).to_not be_expired
147
+
148
+ connection.once(:connected) do
149
+ started_at = Time.now
150
+ connection.once(:disconnected) do |error|
151
+ EventMachine.add_timer(1) do # allow 1 second
152
+ expect(Time.now - started_at >= ttl)
153
+ expect(original_token).to be_expired
154
+ expect(error.code).to eql(40140) # token expired
155
+ connection.once(:connected) do
156
+ expect(client.auth.current_token).to_not be_expired
157
+ stop_reactor
158
+ end
159
+ end
160
+ end
161
+ end
162
+
163
+ channel.attach
164
+ end
165
+ end
166
+
167
+ skip 'retains connection state'
168
+ skip 'changes state to failed if a new token cannot be issued'
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ context 'for non-renewable tokens' do
175
+ context 'that are expired' do
176
+ let!(:expired_token) do
177
+ Ably::Realtime::Client.new(default_options).auth.request_token(ttl: 0.01)
178
+ end
179
+
180
+ context 'opening a new connection' do
181
+ let(:client_options) { default_options.merge(api_key: nil, token_id: expired_token.id, log_level: :none) }
182
+
183
+ it 'transitions state to failed', em_timeout: 10 do
184
+ EventMachine.add_timer(1) do # wait for token to expire
185
+ expect(expired_token).to be_expired
186
+ connection.once(:connected) { raise 'Connection should never connect as token has expired' }
187
+ connection.once(:failed) do
188
+ expect(client.connection.error_reason.code).to eql(40140)
189
+ stop_reactor
190
+ end
191
+ end
192
+ end
193
+ end
194
+
195
+ context 'when connected' do
196
+ skip 'transitions state to failed'
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ context 'initialization state changes' do
204
+ let(:phases) { [:connecting, :connected] }
205
+ let(:events_triggered) { [] }
206
+ let(:test_expectation) do
207
+ Proc.new do
208
+ expect(events_triggered).to eq(phases)
209
+ stop_reactor
210
+ end
211
+ end
212
+
213
+ def expect_ordered_phases
214
+ phases.each do |phase|
215
+ connection.on(phase) do
216
+ events_triggered << phase
217
+ test_expectation.call if events_triggered.length == phases.length
218
+ end
219
+ end
220
+ end
221
+
222
+ context 'with implicit #connect' do
223
+ it 'are triggered in order' do
224
+ expect_ordered_phases
225
+ end
226
+ end
227
+
228
+ context 'with explicit #connect' do
229
+ it 'are triggered in order' do
230
+ expect_ordered_phases
231
+ connection.connect
232
+ end
233
+ end
234
+ end
235
+
236
+ context '#connect' do
237
+ it 'returns a Deferrable' do
238
+ expect(connection.connect).to be_a(EventMachine::Deferrable)
239
+ stop_reactor
240
+ end
241
+
242
+ it 'calls the Deferrable callback on success' do
243
+ connection.connect.callback do |connection|
244
+ expect(connection).to be_a(Ably::Realtime::Connection)
245
+ expect(connection.state).to eq(:connected)
246
+ stop_reactor
247
+ end
248
+ end
249
+
250
+ context 'when already connected' do
251
+ it 'does nothing and no further state changes are emitted' do
252
+ connection.once(:connected) do
253
+ connection.once_state_changed { raise 'State should not have changed' }
254
+ 3.times { connection.connect }
255
+ EventMachine.add_timer(1) do
256
+ expect(connection).to be_connected
257
+ connection.off
258
+ stop_reactor
259
+ end
260
+ end
261
+ end
262
+ end
263
+
264
+ describe 'once connected' do
265
+ let(:connection2) { Ably::Realtime::Client.new(client_options).connection }
266
+
267
+ describe 'connection#id' do
268
+ it 'is a string' do
269
+ connection.connect do
270
+ expect(connection.id).to be_a(String)
271
+ stop_reactor
272
+ end
273
+ end
274
+
275
+ it 'is unique from the connection#key' do
276
+ connection.connect do
277
+ expect(connection.id).to_not eql(connection.key)
278
+ stop_reactor
279
+ end
280
+ end
281
+
282
+ it 'is unique for every connection' do
283
+ when_all(connection.connect, connection2.connect) do
284
+ expect(connection.id).to_not eql(connection2.id)
285
+ stop_reactor
286
+ end
287
+ end
288
+ end
289
+
290
+ describe 'connection#key' do
291
+ it 'is a string' do
292
+ connection.connect do
293
+ expect(connection.key).to be_a(String)
294
+ stop_reactor
295
+ end
296
+ end
297
+
298
+ it 'is unique from the connection#id' do
299
+ connection.connect do
300
+ expect(connection.key).to_not eql(connection.id)
301
+ stop_reactor
302
+ end
303
+ end
304
+
305
+ it 'is unique for every connection' do
306
+ when_all(connection.connect, connection2.connect) do
307
+ expect(connection.key).to_not eql(connection2.key)
308
+ stop_reactor
309
+ end
310
+ end
311
+ end
312
+ end
313
+
314
+ context 'following a previous connection being opened and closed' do
315
+ it 'reconnects and is provided with a new connection ID and connection key from the server' do
316
+ connection.connect do
317
+ connection_id = connection.id
318
+ connection_key = connection.key
319
+
320
+ connection.close do
321
+ connection.connect do
322
+ expect(connection.id).to_not eql(connection_id)
323
+ expect(connection.key).to_not eql(connection_key)
324
+ stop_reactor
325
+ end
326
+ end
327
+ end
328
+ end
329
+ end
330
+ end
331
+
332
+ describe '#serial connection serial' do
333
+ let(:channel) { client.channel(random_str) }
334
+
335
+ it 'is set to -1 when a new connection is opened' do
336
+ connection.connect do
337
+ expect(connection.serial).to eql(-1)
338
+ stop_reactor
339
+ end
340
+ end
341
+
342
+ context 'when a message is sent but the ACK has not yet been received' do
343
+
344
+ it 'the sent message msgSerial is 0 but the connection serial remains at -1' do
345
+ channel.attach do
346
+ connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
347
+ connection.__outgoing_protocol_msgbus__.unsubscribe
348
+ expect(protocol_message['msgSerial']).to eql(0)
349
+ expect(connection.serial).to eql(-1)
350
+ stop_reactor
351
+ end
352
+ channel.publish('event', 'data')
353
+ end
354
+ end
355
+ end
356
+
357
+ it 'is set to 0 when a message sent ACK is received' do
358
+ channel.publish('event', 'data') do
359
+ expect(connection.serial).to eql(0)
360
+ stop_reactor
361
+ end
362
+ end
363
+
364
+ it 'is set to 1 when the second message sent ACK is received' do
365
+ channel.publish('event', 'data') do
366
+ channel.publish('event', 'data') do
367
+ expect(connection.serial).to eql(1)
368
+ stop_reactor
369
+ end
370
+ end
371
+ end
372
+ end
373
+
374
+ context '#close' do
375
+ it 'returns a Deferrable' do
376
+ connection.connect do
377
+ expect(connection.close).to be_a(EventMachine::Deferrable)
378
+ stop_reactor
379
+ end
380
+ end
381
+
382
+ it 'calls the Deferrable callback on success' do
383
+ connection.connect do
384
+ connection.close.callback do |connection|
385
+ expect(connection).to be_a(Ably::Realtime::Connection)
386
+ expect(connection.state).to eq(:closed)
387
+ stop_reactor
388
+ end
389
+ end
390
+ end
391
+
392
+ context 'when already closed' do
393
+ it 'does nothing and no further state changes are emitted' do
394
+ connection.once(:connected) do
395
+ connection.close do
396
+ connection.once_state_changed { raise 'State should not have changed' }
397
+ 3.times { connection.close }
398
+ EventMachine.add_timer(1) do
399
+ expect(connection).to be_closed
400
+ connection.off
401
+ stop_reactor
402
+ end
403
+ end
404
+ end
405
+ end
406
+ end
407
+
408
+ context 'when connection state is' do
409
+ let(:events) { Hash.new }
410
+
411
+ def log_connection_changes
412
+ connection.on(:closing) { events[:closing_emitted] = true }
413
+ connection.on(:error) { events[:error_emitted] = true }
414
+
415
+ connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
416
+ events[:closed_message_from_server_received] = true if protocol_message.action == :closed
417
+ end
418
+ end
419
+
420
+ context ':initialized' do
421
+ it 'changes the connection state to :closing and then immediately :closed without sending a ProtocolMessage CLOSE' do
422
+ connection.on(:closed) do
423
+ expect(connection.state).to eq(:closed)
424
+
425
+ EventMachine.add_timer(1) do # allow for all subscribers on incoming message bes
426
+ expect(events[:error_emitted]).to_not eql(true)
427
+ expect(events[:closed_message_from_server_received]).to_not eql(true)
428
+ expect(events[:closing_emitted]).to eql(true)
429
+ stop_reactor
430
+ end
431
+ end
432
+
433
+ log_connection_changes
434
+ connection.close
435
+ end
436
+ end
437
+
438
+ context ':connected' do
439
+ it 'changes the connection state to :closing and waits for the server to confirm connection is :closed with a ProtocolMessage' do
440
+ connection.on(:connected) do
441
+ connection.on(:closed) do
442
+ EventMachine.add_timer(1) do # allow for all subscribers on incoming message bus
443
+ expect(events[:error_emitted]).to_not eql(true)
444
+ expect(events[:closed_message_from_server_received]).to eql(true)
445
+ expect(events[:closing_emitted]).to eql(true)
446
+ stop_reactor
447
+ end
448
+ end
449
+
450
+ log_connection_changes
451
+ connection.close
452
+ end
453
+ end
454
+
455
+ context 'with an unresponsive connection' do
456
+ let(:stubbed_timeout) { 2 }
457
+
458
+ before do
459
+ stub_const 'Ably::Realtime::Connection::ConnectionManager::TIMEOUTS',
460
+ Ably::Realtime::Connection::ConnectionManager::TIMEOUTS.merge(close: stubbed_timeout)
461
+
462
+ connection.on(:connected) do
463
+ # Prevent all incoming & outgoing ProtocolMessages from being processed by the client library
464
+ connection.__outgoing_protocol_msgbus__.unsubscribe
465
+ connection.__incoming_protocol_msgbus__.unsubscribe
466
+ end
467
+ end
468
+
469
+ it 'force closes the connection when a :closed ProtocolMessage response is not received' do
470
+ connection.on(:connected) do
471
+ close_requested_at = Time.now
472
+
473
+ connection.on(:closed) do
474
+ expect(Time.now - close_requested_at).to be >= stubbed_timeout
475
+ expect(connection.state).to eq(:closed)
476
+ expect(events[:error_emitted]).to_not eql(true)
477
+ expect(events[:closed_message_from_server_received]).to_not eql(true)
478
+ expect(events[:closing_emitted]).to eql(true)
479
+ stop_reactor
480
+ end
481
+
482
+ log_connection_changes
483
+ connection.close
484
+ end
485
+ end
486
+ end
487
+ end
488
+ end
489
+ end
490
+
491
+ context '#ping' do
492
+ it 'echoes a heart beat' do
493
+ connection.on(:connected) do
494
+ connection.ping do |time_elapsed|
495
+ expect(time_elapsed).to be > 0
496
+ stop_reactor
497
+ end
498
+ end
499
+ end
500
+
501
+ context 'when not connected' do
502
+ it 'raises an exception' do
503
+ expect { connection.ping }.to raise_error RuntimeError, /Cannot send a ping when connection/
504
+ stop_reactor
505
+ end
506
+ end
507
+ end
508
+
509
+ context 'recovery' do
510
+ let(:channel_name) { random_str }
511
+ let(:channel) { client.channel(channel_name) }
512
+ let(:publishing_client) do
513
+ Ably::Realtime::Client.new(client_options)
514
+ end
515
+ let(:publishing_client_channel) { publishing_client.channel(channel_name) }
516
+ let(:client_options) { default_options.merge(log_level: :fatal) }
517
+
518
+ before do
519
+ # Reconfigure client library retry periods and timeouts so that tests run quickly
520
+ stub_const 'Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG',
521
+ Ably::Realtime::Connection::ConnectionManager::CONNECT_RETRY_CONFIG.merge(
522
+ disconnected: { retry_every: 0.1, max_time_in_state: 0.2 },
523
+ suspended: { retry_every: 0.1, max_time_in_state: 0.2 },
524
+ )
525
+ end
526
+
527
+ describe '#recovery_key' do
528
+ def self.available_states
529
+ [:connecting, :connected, :disconnected, :suspended, :failed]
530
+ end
531
+ let(:available_states) { self.class.available_states}
532
+ let(:states) { Hash.new }
533
+ let(:client_options) { default_options.merge(log_level: :none) }
534
+
535
+ it 'is composed of connection id and serial that is kept up to date with each message ACK received' do
536
+ connection.on(:connected) do
537
+ expected_serial = -1
538
+ expect(connection.id).to_not be_nil
539
+ expect(connection.serial).to eql(expected_serial)
540
+
541
+ client.channel('test').attach do |channel|
542
+ channel.publish('event', 'data') do
543
+ expected_serial += 1 # attach message received
544
+ expect(connection.serial).to eql(expected_serial)
545
+
546
+ channel.publish('event', 'data') do
547
+ expected_serial += 1 # attach message received
548
+ expect(connection.serial).to eql(expected_serial)
549
+ stop_reactor
550
+ end
551
+ end
552
+ end
553
+ end
554
+ end
555
+
556
+ it "is available when connection is in one of the states: #{available_states.join(', ')}" do
557
+ connection.once(:connected) do
558
+ allow(client).to receive(:endpoint).and_return(
559
+ URI::Generic.build(
560
+ scheme: 'wss',
561
+ host: 'this.host.does.not.exist.com'
562
+ )
563
+ )
564
+
565
+ connection.transition_state_machine! :disconnected
566
+ end
567
+
568
+ available_states.each do |state|
569
+ connection.on(state) do
570
+ states[state.to_sym] = true if connection.recovery_key
571
+ end
572
+ end
573
+
574
+ connection.once(:failed) do
575
+ expect(states.keys).to match_array(available_states)
576
+ stop_reactor
577
+ end
578
+ end
579
+
580
+ it 'is nil when connection is explicitly CLOSED' do
581
+ connection.once(:connected) do
582
+ connection.close do
583
+ expect(connection.recovery_key).to be_nil
584
+ stop_reactor
585
+ end
586
+ end
587
+ end
588
+ end
589
+
590
+ context "opening a new connection using a recently disconnected connection's #recovery_key" do
591
+ context 'connection#id and connection#key after recovery' do
592
+ let(:client_options) { default_options.merge(log_level: :none) }
593
+
594
+ it 'remain the same' do
595
+ previous_connection_id = nil
596
+ previous_connection_key = nil
597
+
598
+ connection.once(:connected) do
599
+ previous_connection_id = connection.id
600
+ previous_connection_key = connection.key
601
+ connection.transition_state_machine! :failed
602
+ end
603
+
604
+ connection.once(:failed) do
605
+ recover_client = Ably::Realtime::Client.new(default_options.merge(recover: client.connection.recovery_key))
606
+ recover_client.connection.on(:connected) do
607
+ expect(recover_client.connection.key).to eql(previous_connection_key)
608
+ expect(recover_client.connection.id).to eql(previous_connection_id)
609
+ stop_reactor
610
+ end
611
+ end
612
+ end
613
+ end
614
+
615
+ context 'when messages have been sent whilst the old connection is disconnected' do
616
+ describe 'the new connection' do
617
+ let(:client_options) { default_options.merge(log_level: :none) }
618
+
619
+ it 'recovers server-side queued messages' do
620
+ channel.attach do |message|
621
+ connection.transition_state_machine! :failed
622
+ end
623
+
624
+ connection.on(:failed) do
625
+ publishing_client_channel.publish('event', 'message') do
626
+ recover_client = Ably::Realtime::Client.new(default_options.merge(recover: client.connection.recovery_key))
627
+ recover_client.channel(channel_name).attach do |recover_client_channel|
628
+ recover_client_channel.subscribe('event') do |message|
629
+ expect(message.data).to eql('message')
630
+ stop_reactor
631
+ end
632
+ end
633
+ end
634
+ end
635
+ end
636
+ end
637
+ end
638
+ end
639
+
640
+ context 'with :recover option' do
641
+ context 'with invalid syntax' do
642
+ let(:invaid_client_options) { default_options.merge(recover: 'invalid') }
643
+
644
+ it 'raises an exception' do
645
+ expect { Ably::Realtime::Client.new(invaid_client_options) }.to raise_error ArgumentError, /Recover/
646
+ stop_reactor
647
+ end
648
+ end
649
+
650
+ context 'with invalid formatted value sent to server' do
651
+ let(:client_options) { default_options.merge(recover: 'not-a-valid-connection-key:1', log_level: :none) }
652
+
653
+ it 'triggers a fatal error on the connection object, sets the #error_reason and disconnects' do
654
+ connection.once(:error) do |error|
655
+ expect(connection.state).to eq(:failed)
656
+ expect(connection.error_reason.message).to match(/Invalid connection key/)
657
+ expect(connection.error_reason.code).to eql(40006)
658
+ expect(connection.error_reason).to eql(error)
659
+ stop_reactor
660
+ end
661
+ end
662
+ end
663
+
664
+ context 'with expired (missing) value sent to server' do
665
+ let(:client_options) { default_options.merge(recover: '0123456789abcdef:0', log_level: :fatal) }
666
+
667
+ it 'triggers an error on the connection object, sets the #error_reason, yet will connect anyway' do
668
+ connection.once(:error) do |error|
669
+ expect(connection.state).to eq(:connected)
670
+ expect(connection.error_reason.message).to match(/Invalid connection key/i)
671
+ expect(connection.error_reason.code).to eql(80008)
672
+ expect(connection.error_reason).to eql(error)
673
+ stop_reactor
674
+ end
675
+ end
676
+ end
677
+ end
678
+ end
679
+
680
+ context 'with many connections simultaneously', em_timeout: 15 do
681
+ let(:connection_count) { 40 }
682
+ let(:connection_ids) { [] }
683
+ let(:connection_keys) { [] }
684
+
685
+ it 'opens each with a unique connection#id and connection#key' do
686
+ connection_count.times.map do
687
+ Ably::Realtime::Client.new(client_options)
688
+ end.each do |client|
689
+ client.connection.on(:connected) do
690
+ connection_ids << client.connection.id
691
+ connection_keys << client.connection.key
692
+ next unless connection_ids.count == connection_count
693
+
694
+ expect(connection_ids.uniq.count).to eql(connection_count)
695
+ expect(connection_keys.uniq.count).to eql(connection_count)
696
+ stop_reactor
697
+ end
698
+ end
699
+ end
700
+ end
701
+
702
+ context 'when a state transition is unsupported' do
703
+ let(:client_options) { default_options.merge(log_level: :none) } # silence FATAL errors
704
+
705
+ it 'emits a StateChangeError' do
706
+ connection.connect do
707
+ connection.transition_state_machine :initialized
708
+ end
709
+
710
+ connection.on(:error) do |error|
711
+ expect(error).to be_a(Ably::Exceptions::StateChangeError)
712
+ stop_reactor
713
+ end
714
+ end
715
+ end
716
+
717
+
718
+ context 'undocumented method' do
719
+ context '#internet_up?' do
720
+ it 'returns a Deferrable' do
721
+ expect(connection.internet_up?).to be_a(EventMachine::Deferrable)
722
+ stop_reactor
723
+ end
724
+
725
+ context 'internet up URL protocol' do
726
+ let(:http_request) { double('EventMachine::HttpRequest', get: EventMachine::DefaultDeferrable.new) }
727
+
728
+ context 'when using TLS for the connection' do
729
+ let(:client_options) { default_options.merge(tls: true) }
730
+
731
+ it 'uses TLS for the Internet check to https://internet-up.ably-realtime.com/is-the-internet-up.txt' do
732
+ expect(EventMachine::HttpRequest).to receive(:new).with('https://internet-up.ably-realtime.com/is-the-internet-up.txt').and_return(http_request)
733
+ connection.internet_up?
734
+ stop_reactor
735
+ end
736
+ end
737
+
738
+ context 'when using a non-secured connection' do
739
+ let(:client_options) { default_options.merge(tls: false, use_token_auth: true) }
740
+
741
+ it 'uses TLS for the Internet check to http://internet-up.ably-realtime.com/is-the-internet-up.txt' do
742
+ expect(EventMachine::HttpRequest).to receive(:new).with('http://internet-up.ably-realtime.com/is-the-internet-up.txt').and_return(http_request)
743
+ connection.internet_up?
744
+ stop_reactor
745
+ end
746
+ end
747
+ end
748
+
749
+ context 'when the Internet is up' do
750
+ it 'calls the block with true' do
751
+ connection.internet_up? do |result|
752
+ expect(result).to be_truthy
753
+ stop_reactor
754
+ end
755
+ end
756
+
757
+ it 'calls the success callback of the Deferrable' do
758
+ connection.internet_up?.callback do
759
+ stop_reactor
760
+ end
761
+ end
762
+ end
763
+
764
+ context 'when the Internet is down' do
765
+ before do
766
+ stub_const 'Ably::INTERNET_CHECK', { url: '//does.not.exist.com', ok_text: 'no.way.this.will.match' }
767
+ end
768
+
769
+ it 'calls the block with false' do
770
+ connection.internet_up? do |result|
771
+ expect(result).to be_falsey
772
+ stop_reactor
773
+ end
774
+ end
775
+
776
+ it 'calls the failure callback of the Deferrable' do
777
+ connection.internet_up?.errback do
778
+ stop_reactor
779
+ end
780
+ end
781
+ end
782
+ end
783
+ end
784
+ end
785
+ end