ably-rest 0.7.1 → 0.7.3

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