ably 0.8.5 → 0.8.6

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/CHANGELOG.md +42 -48
  4. data/SPEC.md +1099 -640
  5. data/ably.gemspec +10 -4
  6. data/lib/ably/auth.rb +155 -47
  7. data/lib/ably/exceptions.rb +2 -0
  8. data/lib/ably/models/channel_state_change.rb +2 -3
  9. data/lib/ably/models/connection_details.rb +54 -0
  10. data/lib/ably/models/protocol_message.rb +14 -4
  11. data/lib/ably/models/token_details.rb +13 -7
  12. data/lib/ably/models/token_request.rb +1 -2
  13. data/lib/ably/modules/ably.rb +3 -2
  14. data/lib/ably/modules/message_emitter.rb +1 -3
  15. data/lib/ably/modules/state_emitter.rb +2 -2
  16. data/lib/ably/realtime/auth.rb +6 -0
  17. data/lib/ably/realtime/channel/channel_manager.rb +2 -0
  18. data/lib/ably/realtime/channel.rb +15 -4
  19. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +11 -1
  20. data/lib/ably/realtime/client.rb +10 -3
  21. data/lib/ably/realtime/connection/connection_manager.rb +58 -54
  22. data/lib/ably/realtime/connection.rb +62 -6
  23. data/lib/ably/realtime/presence.rb +18 -5
  24. data/lib/ably/rest/channel.rb +9 -1
  25. data/lib/ably/rest/client.rb +32 -14
  26. data/lib/ably/rest/presence.rb +1 -1
  27. data/lib/ably/version.rb +1 -1
  28. data/lib/ably.rb +2 -0
  29. data/spec/acceptance/realtime/auth_spec.rb +251 -11
  30. data/spec/acceptance/realtime/channel_history_spec.rb +12 -2
  31. data/spec/acceptance/realtime/channel_spec.rb +316 -24
  32. data/spec/acceptance/realtime/client_spec.rb +93 -1
  33. data/spec/acceptance/realtime/connection_failures_spec.rb +177 -86
  34. data/spec/acceptance/realtime/connection_spec.rb +284 -60
  35. data/spec/acceptance/realtime/message_spec.rb +45 -6
  36. data/spec/acceptance/realtime/presence_history_spec.rb +4 -0
  37. data/spec/acceptance/realtime/presence_spec.rb +181 -49
  38. data/spec/acceptance/realtime/time_spec.rb +13 -0
  39. data/spec/acceptance/rest/auth_spec.rb +222 -4
  40. data/spec/acceptance/rest/channel_spec.rb +132 -1
  41. data/spec/acceptance/rest/client_spec.rb +129 -28
  42. data/spec/acceptance/rest/presence_spec.rb +7 -7
  43. data/spec/acceptance/rest/time_spec.rb +10 -0
  44. data/spec/shared/client_initializer_behaviour.rb +41 -17
  45. data/spec/spec_helper.rb +1 -0
  46. data/spec/support/debug_failure_helper.rb +16 -0
  47. data/spec/unit/models/connection_details_spec.rb +60 -0
  48. data/spec/unit/models/protocol_message_spec.rb +45 -0
  49. data/spec/unit/modules/event_emitter_spec.rb +3 -1
  50. data/spec/unit/realtime/channel_spec.rb +6 -5
  51. data/spec/unit/realtime/client_spec.rb +5 -1
  52. data/spec/unit/realtime/connection_spec.rb +5 -1
  53. data/spec/unit/realtime/realtime_spec.rb +5 -1
  54. metadata +54 -7
@@ -21,12 +21,12 @@ module Ably
21
21
  # Default Ably domain for REST
22
22
  DOMAIN = 'rest.ably.io'
23
23
 
24
- # Configuration for connection retry attempts
25
- CONNECTION_RETRY = {
26
- single_request_open_timeout: 4,
27
- single_request_timeout: 15,
28
- cumulative_request_open_timeout: 10,
29
- max_retry_attempts: 3
24
+ # Configuration for HTTP timeouts and HTTP request reattempts to fallback hosts
25
+ HTTP_DEFAULTS = {
26
+ open_timeout: 4,
27
+ request_timeout: 15,
28
+ max_retry_duration: 10,
29
+ max_retry_count: 3
30
30
  }.freeze
31
31
 
32
32
  def_delegators :auth, :client_id, :auth_options
@@ -51,18 +51,23 @@ module Ably
51
51
  # @return [Logger::Severity]
52
52
  attr_reader :log_level
53
53
 
54
- # The custom host that is being used if it was provided with the option `:rest_host` when the {Client} was created
54
+ # The custom host that is being used if it was provided with the option +:rest_host+ when the {Client} was created
55
55
  # @return [String,Nil]
56
56
  attr_reader :custom_host
57
57
 
58
- # The custom port for non-TLS requests if it was provided with the option `:port` when the {Client} was created
58
+ # The custom port for non-TLS requests if it was provided with the option +:port+ when the {Client} was created
59
59
  # @return [Integer,Nil]
60
60
  attr_reader :custom_port
61
61
 
62
- # The custom TLS port for TLS requests if it was provided with the option `:tls_port` when the {Client} was created
62
+ # The custom TLS port for TLS requests if it was provided with the option +:tls_port+ when the {Client} was created
63
63
  # @return [Integer,Nil]
64
64
  attr_reader :custom_tls_port
65
65
 
66
+ # The immutable configured HTTP defaults for this client.
67
+ # See {#initialize} for the configurable HTTP defaults prefixed with +http_+
68
+ # @return [Hash]
69
+ attr_reader :http_defaults
70
+
66
71
  # The registered encoders that are used to encode and decode message payloads
67
72
  # @return [Array<Ably::Models::MessageEncoder::Base>]
68
73
  # @api private
@@ -96,6 +101,11 @@ module Ably
96
101
  # @option options [Boolean] :query_time when true will query the {https://www.ably.io Ably} system for the current time instead of using the local time
97
102
  # @option options [Hash] :token_params convenience to pass in +token_params+ that will be used as a default for all token requests. See {Auth#create_token_request}
98
103
  #
104
+ # @option options [Integer] :http_open_timeout (4 seconds) timeout in seconds for opening an HTTP connection for all HTTP requests
105
+ # @option options [Integer] :http_request_timeout (15 seconds) timeout in seconds for any single complete HTTP request and response
106
+ # @option options [Integer] :http_max_retry_count (3) maximum number of fallback host retries for HTTP requests that fail due to network issues or server problems
107
+ # @option options [Integer] :http_max_retry_duration (10 seconds) maximum elapsed time in which fallback host retries for HTTP requests will be attempted i.e. if the first default host attempt takes 5s, and then the subsequent fallback retry attempt takes 7s, no further fallback host attempts will be made as the total elapsed time of 12s exceeds the default 10s limit
108
+ #
99
109
  # @return [Ably::Rest::Client]
100
110
  #
101
111
  # @example
@@ -127,6 +137,14 @@ module Ably
127
137
  @custom_port = options.delete(:port)
128
138
  @custom_tls_port = options.delete(:tls_port)
129
139
 
140
+ @http_defaults = HTTP_DEFAULTS.dup
141
+ options.each do |key, val|
142
+ if http_key = key[/^http_(.+)/, 1]
143
+ @http_defaults[http_key.to_sym] = val if val && @http_defaults.has_key?(http_key.to_sym)
144
+ end
145
+ end
146
+ @http_defaults.freeze
147
+
130
148
  if @log_level == :none
131
149
  @custom_logger = Ably::Models::NilLogger.new
132
150
  else
@@ -322,8 +340,8 @@ module Ably
322
340
  # Sends HTTP request to connection end point
323
341
  # Connection failures will automatically be reattempted until thresholds are met
324
342
  def send_request(method, path, params, options)
325
- max_retry_attempts = CONNECTION_RETRY.fetch(:max_retry_attempts)
326
- cumulative_timeout = CONNECTION_RETRY.fetch(:cumulative_request_open_timeout)
343
+ max_retry_count = http_defaults.fetch(:max_retry_count)
344
+ max_retry_duration = http_defaults.fetch(:max_retry_duration)
327
345
  requested_at = Time.now
328
346
  retry_count = 0
329
347
 
@@ -338,7 +356,7 @@ module Ably
338
356
 
339
357
  rescue Faraday::TimeoutError, Faraday::ClientError, Ably::Exceptions::ServerError => error
340
358
  time_passed = Time.now - requested_at
341
- if can_fallback_to_alternate_ably_host? && retry_count < max_retry_attempts && time_passed <= cumulative_timeout
359
+ if can_fallback_to_alternate_ably_host? && retry_count < max_retry_count && time_passed <= max_retry_duration
342
360
  retry_count += 1
343
361
  retry
344
362
  end
@@ -395,8 +413,8 @@ module Ably
395
413
  user_agent: user_agent
396
414
  },
397
415
  request: {
398
- open_timeout: CONNECTION_RETRY.fetch(:single_request_open_timeout),
399
- timeout: CONNECTION_RETRY.fetch(:single_request_timeout)
416
+ open_timeout: http_defaults.fetch(:open_timeout),
417
+ timeout: http_defaults.fetch(:request_timeout)
400
418
  }
401
419
  }
402
420
  end
@@ -82,7 +82,7 @@ module Ably
82
82
 
83
83
  private
84
84
  def base_path
85
- "/channels/#{CGI.escape(channel.name)}/presence"
85
+ "/channels/#{Addressable::URI.encode(channel.name)}/presence"
86
86
  end
87
87
 
88
88
  def decode_message(presence_message)
data/lib/ably/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Ably
2
- VERSION = '0.8.5'
2
+ VERSION = '0.8.6'
3
3
  end
data/lib/ably.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'addressable/uri'
2
+
1
3
  %w(modules util).each do |namespace|
2
4
  Dir.glob(File.expand_path("ably/#{namespace}/*.rb", File.dirname(__FILE__))).sort.each do |file|
3
5
  require file
@@ -70,15 +70,6 @@ describe Ably::Realtime::Auth, :event_machine do
70
70
  end
71
71
  end
72
72
 
73
- context '#token' do
74
- let(:client_options) { default_options.merge(token: random_str) }
75
-
76
- it 'contains the current token after auth' do
77
- expect(auth.token).to_not be_nil
78
- stop_reactor
79
- end
80
- end
81
-
82
73
  context '#current_token_details' do
83
74
  it 'contains the current token after auth' do
84
75
  expect(auth.current_token_details).to be_nil
@@ -99,6 +90,7 @@ describe Ably::Realtime::Auth, :event_machine do
99
90
  context '#options (auth_options)' do
100
91
  let(:auth_url) { "https://echo.ably.io/?type=text" }
101
92
  let(:auth_params) { { :body => random_str } }
93
+ let(:client_options) { default_options.merge(auto_connect: false) }
102
94
 
103
95
  it 'contains the configured auth options' do
104
96
  auth.authorise({}, auth_url: auth_url, auth_params: auth_params) do
@@ -193,6 +185,75 @@ describe Ably::Realtime::Auth, :event_machine do
193
185
  stop_reactor
194
186
  end
195
187
  end
188
+
189
+ context 'when implicitly called, with an explicit ClientOptions client_id' do
190
+ let(:client_id) { random_str }
191
+ let(:client_options) { default_options.merge(auth_callback: Proc.new { auth_token_object }, client_id: client_id, log_level: :none) }
192
+ let(:rest_auth_client) { Ably::Rest::Client.new(default_options.merge(key: api_key, client_id: 'invalid')) }
193
+
194
+ context 'and an incompatible client_id in a TokenDetails object passed to the auth callback' do
195
+ let(:auth_token_object) { rest_auth_client.auth.request_token }
196
+
197
+ it 'rejects a TokenDetails object with an incompatible client_id and raises an exception' do
198
+ client.connect
199
+ client.connection.on(:error) do |error|
200
+ expect(error).to be_a(Ably::Exceptions::IncompatibleClientId)
201
+ EventMachine.add_timer(0.1) do
202
+ expect(client.connection).to be_failed
203
+ stop_reactor
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ context 'and an incompatible client_id in a TokenRequest object passed to the auth callback and raises an exception' do
210
+ let(:auth_token_object) { rest_auth_client.auth.create_token_request }
211
+
212
+ it 'rejects a TokenRequests object with an incompatible client_id and raises an exception' do
213
+ client.connect
214
+ client.connection.on(:error) do |error|
215
+ expect(error).to be_a(Ably::Exceptions::IncompatibleClientId)
216
+ EventMachine.add_timer(0.1) do
217
+ expect(client.connection).to be_failed
218
+ stop_reactor
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
224
+
225
+ context 'when explicitly called, with an explicit ClientOptions client_id' do
226
+ let(:auth_proc) do
227
+ Proc.new do
228
+ if !@requested
229
+ @requested = true
230
+ valid_auth_token
231
+ else
232
+ invalid_auth_token
233
+ end
234
+ end
235
+ end
236
+
237
+ let(:client_id) { random_str }
238
+ let(:client_options) { default_options.merge(auth_callback: auth_proc, client_id: client_id, log_level: :none) }
239
+ let(:valid_auth_token) { Ably::Rest::Client.new(default_options.merge(key: api_key, client_id: client_id)).auth.request_token }
240
+ let(:invalid_auth_token) { Ably::Rest::Client.new(default_options.merge(key: api_key, client_id: 'invalid')).auth.request_token }
241
+
242
+ context 'and an incompatible client_id in a TokenDetails object passed to the auth callback' do
243
+ it 'rejects a TokenDetails object with an incompatible client_id and raises an exception' do
244
+ client.connection.once(:connected) do
245
+ client.auth.authorise({}, force: true)
246
+ client.connection.on(:error) do |error|
247
+ expect(error).to be_a(Ably::Exceptions::IncompatibleClientId)
248
+ EventMachine.add_timer(0.1) do
249
+ expect(client.connection).to be_failed
250
+ stop_reactor
251
+ end
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
196
257
  end
197
258
 
198
259
  context '#authorise_async' do
@@ -216,7 +277,7 @@ describe Ably::Realtime::Auth, :event_machine do
216
277
  end
217
278
  end
218
279
 
219
- context '#auth_params' do
280
+ context '#auth_params_sync' do
220
281
  it 'returns the auth params synchronously' do
221
282
  expect(auth.auth_params_sync).to be_a(Hash)
222
283
  stop_reactor
@@ -232,11 +293,190 @@ describe Ably::Realtime::Auth, :event_machine do
232
293
  end
233
294
  end
234
295
 
235
- context '#auth_header' do
296
+ context '#auth_header_sync' do
236
297
  it 'returns an auth header synchronously' do
237
298
  expect(auth.auth_header_sync).to be_a(String)
238
299
  stop_reactor
239
300
  end
240
301
  end
302
+
303
+ describe '#client_id_validated?' do
304
+ let(:auth) { Ably::Rest::Client.new(default_options.merge(key: api_key)).auth }
305
+
306
+ context 'when using basic auth' do
307
+ let(:client_options) { default_options.merge(key: api_key) }
308
+
309
+ context 'before connected' do
310
+ it 'is false as basic auth users do not have an identity' do
311
+ expect(client.auth).to_not be_client_id_validated
312
+ stop_reactor
313
+ end
314
+ end
315
+
316
+ context 'once connected' do
317
+ it 'is true' do
318
+ client.connection.once(:connected) do
319
+ expect(client.auth).to be_client_id_validated
320
+ stop_reactor
321
+ end
322
+ end
323
+
324
+ it 'contains a validated wildcard client_id' do
325
+ client.connection.once(:connected) do
326
+ expect(client.auth.client_id).to eql('*')
327
+ stop_reactor
328
+ end
329
+ end
330
+ end
331
+ end
332
+
333
+ context 'when using a token string' do
334
+ context 'with a valid client_id' do
335
+ let(:client_options) { default_options.merge(token: auth.request_token(client_id: 'present').token) }
336
+
337
+ context 'before connected' do
338
+ it 'is false as identification is not possible from an opaque token string' do
339
+ expect(client.auth).to_not be_client_id_validated
340
+ stop_reactor
341
+ end
342
+
343
+ specify '#client_id is nil' do
344
+ expect(client.auth.client_id).to be_nil
345
+ stop_reactor
346
+ end
347
+ end
348
+
349
+ context 'once connected' do
350
+ it 'is true' do
351
+ client.connection.once(:connected) do
352
+ expect(client.auth).to be_client_id_validated
353
+ stop_reactor
354
+ end
355
+ end
356
+
357
+ specify '#client_id is populated' do
358
+ client.connection.once(:connected) do
359
+ expect(client.auth.client_id).to eql('present')
360
+ stop_reactor
361
+ end
362
+ end
363
+ end
364
+ end
365
+
366
+ context 'with no client_id (anonymous)' do
367
+ let(:client_options) { default_options.merge(token: auth.request_token(client_id: nil).token) }
368
+
369
+ context 'before connected' do
370
+ it 'is false as identification is not possible from an opaque token string' do
371
+ expect(client.auth).to_not be_client_id_validated
372
+ stop_reactor
373
+ end
374
+ end
375
+
376
+ context 'once connected' do
377
+ it 'is true' do
378
+ client.connection.once(:connected) do
379
+ expect(client.auth).to be_client_id_validated
380
+ stop_reactor
381
+ end
382
+ end
383
+ end
384
+ end
385
+
386
+ context 'with a wildcard client_id (anonymous)' do
387
+ let(:client_options) { default_options.merge(token: auth.request_token(client_id: '*').token) }
388
+
389
+ context 'before connected' do
390
+ it 'is false as identification is not possible from an opaque token string' do
391
+ expect(client.auth).to_not be_client_id_validated
392
+ stop_reactor
393
+ end
394
+ end
395
+
396
+ context 'once connected' do
397
+ it 'is true' do
398
+ client.connection.once(:connected) do
399
+ expect(client.auth).to be_client_id_validated
400
+ stop_reactor
401
+ end
402
+ end
403
+ end
404
+ end
405
+ end
406
+
407
+ context 'when using a token' do
408
+ context 'with a client_id' do
409
+ let(:client_options) { default_options.merge(token: auth.request_token(client_id: 'present')) }
410
+
411
+ it 'is true' do
412
+ expect(client.auth).to be_client_id_validated
413
+ stop_reactor
414
+ end
415
+
416
+ context 'once connected' do
417
+ it 'is true' do
418
+ client.connection.once(:connected) do
419
+ expect(client.auth).to be_client_id_validated
420
+ stop_reactor
421
+ end
422
+ end
423
+ end
424
+ end
425
+
426
+ context 'with no client_id (anonymous)' do
427
+ let(:client_options) { default_options.merge(token: auth.request_token(client_id: nil)) }
428
+
429
+ it 'is true' do
430
+ expect(client.auth).to be_client_id_validated
431
+ stop_reactor
432
+ end
433
+
434
+ context 'once connected' do
435
+ it 'is true' do
436
+ client.connection.once(:connected) do
437
+ expect(client.auth).to be_client_id_validated
438
+ stop_reactor
439
+ end
440
+ end
441
+ end
442
+ end
443
+
444
+ context 'with a wildcard client_id (anonymous)' do
445
+ let(:client_options) { default_options.merge(token: auth.request_token(client_id: '*')) }
446
+
447
+ it 'is true' do
448
+ expect(client.auth).to be_client_id_validated
449
+ stop_reactor
450
+ end
451
+
452
+ context 'once connected' do
453
+ it 'is true' do
454
+ client.connection.once(:connected) do
455
+ expect(client.auth).to be_client_id_validated
456
+ stop_reactor
457
+ end
458
+ end
459
+ end
460
+ end
461
+ end
462
+
463
+ context 'when using a token request with a client_id' do
464
+ let(:client_options) { default_options.merge(token: auth.create_token_request(client_id: 'present')) }
465
+
466
+ it 'is not true as identification is not confirmed until authenticated' do
467
+ expect(client.auth).to_not be_client_id_validated
468
+ stop_reactor
469
+ end
470
+
471
+ context 'once connected' do
472
+ it 'is true as identification is completed following CONNECTED ProtocolMessage' do
473
+ client.channel('test').publish('a') do
474
+ expect(client.auth).to be_client_id_validated
475
+ stop_reactor
476
+ end
477
+ end
478
+ end
479
+ end
480
+ end
241
481
  end
242
482
  end
@@ -186,9 +186,19 @@ describe Ably::Realtime::Channel, '#history', :event_machine do
186
186
  messages.next do |next_page_messages|
187
187
  expect(next_page_messages.items.count).to eql(5)
188
188
  expect(next_page_messages.items.map(&:data).uniq.first).to eql(message_before_attach)
189
- expect(next_page_messages).to be_last
190
189
 
191
- stop_reactor
190
+ if next_page_messages.last?
191
+ expect(next_page_messages).to be_last
192
+ stop_reactor
193
+ else
194
+ # If previous page said there is another page it is plausible and correct that
195
+ # the next page is empty and then the last, if the limit was satisfied
196
+ next_page_messages.next do |empty_page|
197
+ expect(empty_page.items.count).to eql(0)
198
+ expect(empty_page).to be_last
199
+ stop_reactor
200
+ end
201
+ end
192
202
  end
193
203
  end
194
204
  end