ably 0.8.13 → 0.8.14

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7859d1961b34f4db85c4e3b4dd9148aa17b456e6
4
- data.tar.gz: df4339d846d3bb29ce58dc311b7e844d6a21fadb
3
+ metadata.gz: b7ff0fc1dc14fc84ea707c200752a84943571d53
4
+ data.tar.gz: 7ba75aa7a6bfdacd097ffa694409bb172711eddb
5
5
  SHA512:
6
- metadata.gz: 711fd1f07710b81c319378740f0b193d0166f16ab00a1528de10c22a105a796212ab77954a7813d631afff1aa82ed7bf1e75f9984a90346b72a636cf5ff74d6d
7
- data.tar.gz: 00e82298ba240966faae98bb7d33065bfa58ba118c40152ac906b92f6344f85d951130f23e40947d107dd809f4cd317e9f975b68324babb376a6ad1a36bd0edf
6
+ metadata.gz: 8eaf2f813c5003c87c64c0f9dbd33e6231ac1d9c1fbd2a1e07117f566c648b6eed4fd50eb4ec15e622bf510dc225e70cd3d6a42dfe25ca4da8ca69529d5001f4
7
+ data.tar.gz: f07522639b0068f1a1aea116d04bcfbc745d7d9862ad12ea64111921854c1da4eb785480f2c5881083c0aa30c052ce1741552ad8b8195c7fd75f4868df58a556
data/CHANGELOG.md CHANGED
@@ -1,6 +1,22 @@
1
1
  # Change Log
2
2
 
3
- ## [Unreleased](https://github.com/ably/ably-ruby/tree/v0.8.13)
3
+ ## [Unreleased](https://github.com/ably/ably-ruby/tree/v0.8.14)
4
+
5
+ [Full Changelog](https://github.com/ably/ably-ruby/compare/v0.8.13...v0.8.14)
6
+
7
+ **Fixed bugs:**
8
+
9
+ - Several problems with fallback hosts [\#95](https://github.com/ably/ably-ruby/issues/95)
10
+
11
+ **Closed issues:**
12
+
13
+ - Catch common SSL error and show docs [\#96](https://github.com/ably/ably-ruby/issues/96)
14
+
15
+ **Merged pull requests:**
16
+
17
+ - Fallback host improvements [\#97](https://github.com/ably/ably-ruby/pull/97) ([mattheworiordan](https://github.com/mattheworiordan))
18
+
19
+ ## [v0.8.13](https://github.com/ably/ably-ruby/tree/v0.8.13) (2016-09-29)
4
20
 
5
21
  [Full Changelog](https://github.com/ably/ably-ruby/compare/v0.8.12...v0.8.13)
6
22
 
data/lib/ably.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require 'addressable/uri'
2
2
 
3
+ require 'ably/version'
4
+
3
5
  %w(modules util).each do |namespace|
4
6
  Dir.glob(File.expand_path("ably/#{namespace}/*.rb", File.dirname(__FILE__))).sort.each do |file|
5
7
  require file
@@ -11,4 +13,3 @@ require 'ably/exceptions'
11
13
  require 'ably/logger'
12
14
  require 'ably/realtime'
13
15
  require 'ably/rest'
14
- require 'ably/version'
@@ -59,7 +59,12 @@ module Ably
59
59
 
60
60
  def to_s
61
61
  message = [super]
62
- message << "#{@base_error}" if @base_error
62
+ if @base_error
63
+ message << "#{@base_error}"
64
+ if @base_error.respond_to?(:message) && @base_error.message.match(/certificate verify failed/i)
65
+ message << "See https://goo.gl/eKvfcR to resolve this issue."
66
+ end
67
+ end
63
68
  message.join(' < ')
64
69
  end
65
70
  end
@@ -155,6 +155,11 @@ module Ably
155
155
  rest_client.register_encoder encoder
156
156
  end
157
157
 
158
+ # (see Ably::Rest::Client#fallback_hosts)
159
+ def fallback_hosts
160
+ rest_client.fallback_hosts
161
+ end
162
+
158
163
  # (see Ably::Rest::Client#logger)
159
164
  def logger
160
165
  @logger ||= Ably::Logger.new(self, log_level, rest_client.logger.custom_logger)
@@ -168,14 +173,15 @@ module Ably
168
173
  end
169
174
 
170
175
  # @!attribute [r] fallback_endpoint
171
- # @return [URI::Generic] Fallback endpoint used to connect to the realtime Ably service. Note, after each connection attempt, a new random {Ably::FALLBACK_HOSTS fallback host} is used
176
+ # @return [URI::Generic] Fallback endpoint used to connect to the realtime Ably service. Note, after each connection attempt, a new random {Ably::FALLBACK_HOSTS fallback host} or provided fallback hosts are used
172
177
  # @api private
173
178
  def fallback_endpoint
174
179
  unless defined?(@fallback_endpoints) && @fallback_endpoints
175
- @fallback_endpoints = Ably::FALLBACK_HOSTS.shuffle.map { |fallback_host| endpoint_for_host(fallback_host) }
180
+ @fallback_endpoints = fallback_hosts.shuffle.map { |fallback_host| endpoint_for_host(fallback_host) }
181
+ @fallback_endpoints << endpoint # Try the original host last if all fallbacks have been used
176
182
  end
177
183
 
178
- fallback_endpoint_index = connection.manager.retry_count_for_state(:disconnected) + connection.manager.retry_count_for_state(:suspended)
184
+ fallback_endpoint_index = connection.manager.retry_count_for_state(:disconnected) + connection.manager.retry_count_for_state(:suspended) - 1
179
185
 
180
186
  @fallback_endpoints[fallback_endpoint_index % @fallback_endpoints.count]
181
187
  end
@@ -64,8 +64,9 @@ module Ably
64
64
  DEFAULTS = {
65
65
  disconnected_retry_timeout: 15, # when the connection enters the DISCONNECTED state, after this delay in milliseconds, if the state is still DISCONNECTED, the client library will attempt to reconnect automatically
66
66
  suspended_retry_timeout: 30, # when the connection enters the SUSPENDED state, after this delay in milliseconds, if the state is still SUSPENDED, the client library will attempt to reconnect automatically
67
- connection_state_ttl: 60, # the duration that Ably will persist the connection state when a Realtime client is abruptly disconnected
68
- realtime_request_timeout: 10 # default timeout when establishing a connection, or sending a HEARTBEAT, CONNECT, ATTACH, DETACH or CLOSE ProtocolMessage
67
+ connection_state_ttl: 120, # the duration that Ably will persist the connection state when a Realtime client is abruptly disconnected
68
+ max_connection_state_ttl: nil, # allow a max TTL to be passed in for CI test purposes thus overiding any connection_state_ttl sent from Ably
69
+ realtime_request_timeout: 10, # default timeout when establishing a connection, or sending a HEARTBEAT, CONNECT, ATTACH, DETACH or CLOSE ProtocolMessage
69
70
  }.freeze
70
71
 
71
72
  # A unique public identifier for this connection, used to identify this member in presence events and messages
@@ -389,7 +390,7 @@ module Ably
389
390
  format: client.protocol,
390
391
  echo: client.echo_messages,
391
392
  v: Ably::PROTOCOL_VERSION,
392
- lib: Ably::LIB_VERSION_ID,
393
+ lib: client.rest_client.lib_version_id,
393
394
  )
394
395
 
395
396
  url_params['clientId'] = client.auth.client_id if client.auth.has_client_id?
@@ -407,12 +408,12 @@ module Ably
407
408
 
408
409
  url = URI(client.endpoint).tap do |endpoint|
409
410
  endpoint.query = URI.encode_www_form(url_params)
410
- end.to_s
411
+ end
411
412
 
412
413
  determine_host do |host|
413
414
  begin
414
- logger.debug "Connection: Opening socket connection to #{host}:#{port} and URL '#{url}'"
415
- @transport = EventMachine.connect(host, port, WebsocketTransport, self, url) do |websocket_transport|
415
+ logger.debug "Connection: Opening socket connection to #{host}:#{port}/#{url.path}?#{url.query}"
416
+ @transport = create_transport(host, port, url) do |websocket_transport|
416
417
  websocket_deferrable.succeed websocket_transport
417
418
  end
418
419
  rescue EventMachine::ConnectionError => error
@@ -473,6 +474,22 @@ module Ably
473
474
  ( (initialized? || connecting? || disconnected?) && client.queue_messages )
474
475
  end
475
476
 
477
+ # @api private
478
+ def create_transport(host, port, url, &block)
479
+ EventMachine.connect(host, port, WebsocketTransport, self, url.to_s, &block)
480
+ end
481
+
482
+ # @api private
483
+ def connection_state_ttl
484
+ defaults[:max_connection_state_ttl] || # undocumented max TTL configuration
485
+ (details && details.connection_state_ttl) ||
486
+ defaults.fetch(:connection_state_ttl)
487
+ end
488
+
489
+ def connection_state_ttl=(val)
490
+ @connection_state_ttl = val
491
+ end
492
+
476
493
  # As we are using a state machine, do not allow change_state to be used
477
494
  # #transition_state_machine must be used instead
478
495
  private :change_state
@@ -527,7 +544,18 @@ module Ably
527
544
  end
528
545
 
529
546
  def connection_resumable?
530
- !key.nil? && !serial.nil?
547
+ !key.nil? && !serial.nil? && connection_state_available?
548
+ end
549
+
550
+ def connection_state_available?
551
+ return true if connected?
552
+
553
+ connected_last = state_history.reverse.find { |connected| connected.fetch(:state) == :connected }
554
+ if connected_last.nil? || (connected_last.fetch(:transitioned_at) < Time.now - connection_state_ttl)
555
+ false
556
+ else
557
+ true
558
+ end
531
559
  end
532
560
 
533
561
  def connection_recoverable?
@@ -555,7 +583,7 @@ module Ably
555
583
  end
556
584
 
557
585
  def can_use_fallback_hosts?
558
- if production? && !custom_port? && !custom_host?
586
+ if client.fallback_hosts && !client.fallback_hosts.empty?
559
587
  if connecting? && previous_state
560
588
  use_fallback_if_disconnected? || use_fallback_if_suspended?
561
589
  end
@@ -320,7 +320,7 @@ module Ably::Realtime
320
320
  def can_reattempt_connect_for_state?(state)
321
321
  case state
322
322
  when :disconnected
323
- time_spent_attempting_state(:disconnected, ignore_states: [:connecting]) < connection.defaults.fetch(:connection_state_ttl)
323
+ time_spent_attempting_state(:disconnected, ignore_states: [:connecting]) < connection.connection_state_ttl
324
324
  when :suspended
325
325
  true # suspended state remains indefinitely
326
326
  else
@@ -78,10 +78,14 @@ module Ably
78
78
  # @api private
79
79
  attr_reader :options
80
80
 
81
+ # The list of fallback hosts to be used by this client
82
+ # if empty or nil then fallback host functionality is disabled
83
+ attr_reader :fallback_hosts
84
+
81
85
  # Creates a {Ably::Rest::Client Rest Client} and configures the {Ably::Auth} object for the connection.
82
86
  #
83
87
  # @param [Hash,String] options an options Hash used to configure the client and the authentication, or String with an API key or Token ID
84
- # @option options [Boolean] :tls (true) When fales, TLS is disabled. Please note Basic Auth is disallowed without TLS as secrets cannot be transmitted over unsecured connections.
88
+ # @option options [Boolean] :tls (true) When false, TLS is disabled. Please note Basic Auth is disallowed without TLS as secrets cannot be transmitted over unsecured connections.
85
89
  # @option options [String] :key API key comprising the key name and key secret in a single string
86
90
  # @option options [String] :token Token string or {Models::TokenDetails} used to authenticate requests
87
91
  # @option options [String] :token_details {Models::TokenDetails} used to authenticate requests
@@ -106,6 +110,9 @@ module Ably
106
110
  # @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
111
  # @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
112
  #
113
+ # @option options [Boolean] :fallback_hosts_use_default (false) When true, forces the user of fallback hosts even if a non-default production endpoint is being used
114
+ # @option options [Array<String>] :fallback_hosts When an array of fallback hosts are provided, these fallback hosts are always used if a request fails to the primary endpoint. If an empty array is provided, the fallback host functionality is disabled
115
+ #
109
116
  # @return [Ably::Rest::Client]
110
117
  #
111
118
  # @example
@@ -129,6 +136,7 @@ module Ably
129
136
 
130
137
  @tls = options.delete(:tls) == false ? false : true
131
138
  @environment = options.delete(:environment) # nil is production
139
+ @environment = nil if [:production, 'production'].include?(@environment)
132
140
  @protocol = options.delete(:protocol) || :msgpack
133
141
  @debug_http = options.delete(:debug_http)
134
142
  @log_level = options.delete(:log_level) || ::Logger::WARN
@@ -137,6 +145,20 @@ module Ably
137
145
  @custom_port = options.delete(:port)
138
146
  @custom_tls_port = options.delete(:tls_port)
139
147
 
148
+ if options[:fallback_hosts_use_default] && options[:fallback_jhosts]
149
+ raise ArgumentError, "fallback_hosts_use_default cannot be set to trye when fallback_jhosts is also provided"
150
+ end
151
+ @fallback_hosts = case
152
+ when options.delete(:fallback_hosts_use_default)
153
+ Ably::FALLBACK_HOSTS
154
+ when options_fallback_hosts = options.delete(:fallback_hosts)
155
+ options_fallback_hosts
156
+ when environment || custom_host || options[:realtime_host] || custom_port || custom_tls_port
157
+ []
158
+ else
159
+ Ably::FALLBACK_HOSTS
160
+ end
161
+
140
162
  @http_defaults = HTTP_DEFAULTS.dup
141
163
  options.each do |key, val|
142
164
  if http_key = key[/^http_(.+)/, 1]
@@ -295,7 +317,7 @@ module Ably
295
317
  # Connection used to make HTTP requests
296
318
  #
297
319
  # @param [Hash] options
298
- # @option options [Boolean] :use_fallback when true, one of the fallback connections is used randomly, see {Ably::FALLBACK_HOSTS}
320
+ # @option options [Boolean] :use_fallback when true, one of the fallback connections is used randomly, see the default {Ably::FALLBACK_HOSTS}
299
321
  #
300
322
  # @return [Faraday::Connection]
301
323
  #
@@ -309,14 +331,16 @@ module Ably
309
331
  end
310
332
 
311
333
  # Fallback connection used to make HTTP requests.
312
- # Note, each request uses a random and then subsequent random {Ably::FALLBACK_HOSTS fallback host}
334
+ # Note, each request uses a random and then subsequent random {Ably::FALLBACK_HOSTS fallback hosts}
335
+ # are used (unless custom fallback hosts are provided with fallback_hosts)
313
336
  #
314
337
  # @return [Faraday::Connection]
315
338
  #
316
339
  # @api private
317
340
  def fallback_connection
318
341
  unless defined?(@fallback_connections) && @fallback_connections
319
- @fallback_connections = Ably::FALLBACK_HOSTS.shuffle.map { |host| Faraday.new(endpoint_for_host(host).to_s, connection_options) }
342
+ @fallback_connections = fallback_hosts.shuffle.map { |host| Faraday.new(endpoint_for_host(host).to_s, connection_options) }
343
+ @fallback_connections << Faraday.new(endpoint.to_s, connection_options) # Try the original host last if all fallbacks have been used
320
344
  end
321
345
  @fallback_index ||= 0
322
346
 
@@ -325,6 +349,16 @@ module Ably
325
349
  end
326
350
  end
327
351
 
352
+ # Library Ably version user agent
353
+ # @api private
354
+ def lib_version_id
355
+ @lib_version_id ||= [
356
+ 'ruby',
357
+ Ably.lib_variant,
358
+ Ably::VERSION
359
+ ].compact.join('-')
360
+ end
361
+
328
362
  private
329
363
  def request(method, path, params = {}, options = {})
330
364
  options = options.clone
@@ -352,14 +386,13 @@ module Ably
352
386
  unless options[:send_auth_header] == false
353
387
  request.headers[:authorization] = auth.auth_header
354
388
  end
355
- request.headers['X-Ably-Version'] = Ably::PROTOCOL_VERSION
356
- request.headers['X-Ably-Lib'] = Ably::LIB_VERSION_ID
357
389
  end
358
390
 
359
391
  rescue Faraday::TimeoutError, Faraday::ClientError, Ably::Exceptions::ServerError => error
360
392
  time_passed = Time.now - requested_at
361
393
  if can_fallback_to_alternate_ably_host? && retry_count < max_retry_count && time_passed <= max_retry_duration
362
394
  retry_count += 1
395
+ logger.warn "Ably::Rest::Client - Retry #{retry_count} for #{method} #{path} #{params} as initial attempt failed: #{error}"
363
396
  retry
364
397
  end
365
398
 
@@ -410,9 +443,11 @@ module Ably
410
443
  @connection_options ||= {
411
444
  builder: middleware,
412
445
  headers: {
413
- content_type: mime_type,
414
- accept: mime_type,
415
- user_agent: user_agent
446
+ content_type: mime_type,
447
+ accept: mime_type,
448
+ user_agent: user_agent,
449
+ 'X-Ably-Version' => Ably::PROTOCOL_VERSION,
450
+ 'X-Ably-Lib' => lib_version_id
416
451
  },
417
452
  request: {
418
453
  open_timeout: http_defaults.fetch(:open_timeout),
@@ -439,7 +474,7 @@ module Ably
439
474
  end
440
475
 
441
476
  def can_fallback_to_alternate_ably_host?
442
- !custom_host && !environment
477
+ fallback_hosts && !fallback_hosts.empty?
443
478
  end
444
479
 
445
480
  def initialize_default_encoders
data/lib/ably/version.rb CHANGED
@@ -1,5 +1,16 @@
1
1
  module Ably
2
- VERSION = '0.8.13'
2
+ VERSION = '0.8.14'
3
3
  PROTOCOL_VERSION = '0.8'
4
- LIB_VERSION_ID = "ruby-#{Ably::VERSION}"
4
+
5
+ # Allow a variant to be configured for all instances of this client library
6
+ # such as ruby-rest-[VERSION]
7
+
8
+ # @api private
9
+ def self.lib_variant=(variant)
10
+ @lib_variant = variant
11
+ end
12
+
13
+ def self.lib_variant
14
+ @lib_variant
15
+ end
5
16
  end
@@ -62,7 +62,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
62
62
  log_level: :none,
63
63
  disconnected_retry_timeout: retry_every_for_tests,
64
64
  suspended_retry_timeout: retry_every_for_tests,
65
- connection_state_ttl: max_time_in_state_for_tests
65
+ max_connection_state_ttl: max_time_in_state_for_tests
66
66
  )
67
67
  end
68
68
 
@@ -169,7 +169,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
169
169
 
170
170
  context 'for the first time' do
171
171
  let(:client_options) do
172
- default_options.merge(suspended_retry_timeout: 2, connection_state_ttl: 0, log_level: :error)
172
+ default_options.merge(suspended_retry_timeout: 2, max_connection_state_ttl: 0, log_level: :error)
173
173
  end
174
174
 
175
175
  it 'waits suspended_retry_timeout before attempting to reconnect' do
@@ -306,7 +306,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
306
306
  log_level: :error,
307
307
  disconnected_retry_timeout: 0.1,
308
308
  suspended_retry_timeout: 0.1,
309
- connection_state_ttl: 0.2,
309
+ max_connection_state_ttl: 0.2,
310
310
  realtime_host: 'non.existent.host'
311
311
  )
312
312
  end
@@ -370,7 +370,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
370
370
  log_level: :none,
371
371
  disconnected_retry_timeout: retry_every,
372
372
  suspended_retry_timeout: retry_every,
373
- connection_state_ttl: 60
373
+ max_connection_state_ttl: 60
374
374
  )
375
375
  end
376
376
 
@@ -416,7 +416,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
416
416
  end
417
417
  end
418
418
 
419
- context 'when websocket transport is closed' do
419
+ context 'when websocket transport is abruptly disconnected' do
420
420
  it 'reconnects automatically' do
421
421
  fail_if_suspended_or_failed
422
422
 
@@ -431,6 +431,29 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
431
431
  connection.transport.close_connection_after_writing
432
432
  end
433
433
  end
434
+
435
+ context 'hosts used' do
436
+ it 'reconnects with the default host' do
437
+ fail_if_suspended_or_failed
438
+
439
+ connection.once(:connected) do
440
+ connection.once(:disconnected) do
441
+ hosts = []
442
+ expect(connection).to receive(:create_transport).once.and_wrap_original do |original_method, *args, &block|
443
+ hosts << args[0]
444
+ original_method.call(*args, &block)
445
+ end
446
+ connection.once(:connected) do
447
+ host = "#{"#{environment}-" if environment && environment.to_s != 'production'}#{Ably::Realtime::Client::DOMAIN}"
448
+ expect(hosts.first).to eql(host)
449
+ expect(hosts.length).to eql(1)
450
+ stop_reactor
451
+ end
452
+ end
453
+ connection.transport.close_connection_after_writing
454
+ end
455
+ end
456
+ end
434
457
  end
435
458
 
436
459
  context 'after successfully reconnecting and resuming' do
@@ -599,6 +622,32 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
599
622
  end
600
623
  end
601
624
  end
625
+
626
+ context 'as the DISCONNECTED window to resume has passed' do
627
+ let(:channel) { client.channel(random_str) }
628
+
629
+ def kill_connection_transport_and_prevent_valid_resume
630
+ connection.transport.close_connection_after_writing
631
+ end
632
+
633
+ it 'starts a new connection automatically and does not try and resume' do
634
+ connection.once(:connected) do
635
+ previous_connection_id = connection.id
636
+ previous_connection_key = connection.key
637
+
638
+ five_minutes_time = Time.now + 5 * 60
639
+ allow(Time).to receive(:now) { five_minutes_time }
640
+
641
+ connection.once(:connected) do
642
+ expect(connection.key).to_not eql(previous_connection_key)
643
+ expect(connection.id).to_not eql(previous_connection_id)
644
+ stop_reactor
645
+ end
646
+
647
+ kill_connection_transport_and_prevent_valid_resume
648
+ end
649
+ end
650
+ end
602
651
  end
603
652
  end
604
653
 
@@ -612,7 +661,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
612
661
  log_level: :none,
613
662
  disconnected_retry_timeout: retry_every_for_tests,
614
663
  suspended_retry_timeout: retry_every_for_tests,
615
- connection_state_ttl: max_time_in_state_for_tests
664
+ max_connection_state_ttl: max_time_in_state_for_tests
616
665
  )
617
666
  end
618
667
 
@@ -627,7 +676,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
627
676
  let(:client_options) { timeout_options.merge(realtime_host: expected_host) }
628
677
 
629
678
  it 'never uses a fallback host' do
630
- expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
679
+ expect(connection).to receive(:create_transport).exactly(retry_count_for_all_states).times do |host|
631
680
  expect(host).to eql(expected_host)
632
681
  raise EventMachine::ConnectionError
633
682
  end
@@ -645,7 +694,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
645
694
  let(:client_options) { timeout_options.merge(tls_port: custom_port) }
646
695
 
647
696
  it 'never uses a fallback host' do
648
- expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host, port|
697
+ expect(connection).to receive(:create_transport).exactly(retry_count_for_all_states).times do |host, port|
649
698
  expect(port).to eql(custom_port)
650
699
  raise EventMachine::ConnectionError
651
700
  end
@@ -663,8 +712,8 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
663
712
  let(:expected_host) { "#{environment}-#{Ably::Realtime::Client::DOMAIN}" }
664
713
  let(:client_options) { timeout_options.merge(environment: environment) }
665
714
 
666
- it 'never uses a fallback host' do
667
- expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
715
+ it 'does not use a fallback host by default' do
716
+ expect(connection).to receive(:create_transport).exactly(retry_count_for_all_states).times do |host|
668
717
  expect(host).to eql(expected_host)
669
718
  raise EventMachine::ConnectionError
670
719
  end
@@ -675,6 +724,55 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
675
724
  end
676
725
  end
677
726
  end
727
+
728
+ context ':fallback_hosts_use_default is true' do
729
+ let(:max_time_in_state_for_tests) { 4 }
730
+ let(:fallback_hosts_used) { Array.new }
731
+ let(:client_options) { timeout_options.merge(environment: environment, fallback_hosts_use_default: true) }
732
+
733
+ it 'uses a fallback host on every subsequent disconnected attempt until suspended (#RTN17b, #TO3k7)' do
734
+ request = 0
735
+ allow(connection).to receive(:create_transport) do |host|
736
+ if request == 0
737
+ expect(host).to eql(expected_host)
738
+ else
739
+ fallback_hosts_used << host
740
+ end
741
+ request += 1
742
+ raise EventMachine::ConnectionError
743
+ end
744
+
745
+ connection.once(:suspended) do
746
+ expect(fallback_hosts_used.uniq).to match_array(Ably::FALLBACK_HOSTS + [expected_host])
747
+ stop_reactor
748
+ end
749
+ end
750
+ end
751
+
752
+ context ':fallback_hosts array is provided' do
753
+ let(:max_time_in_state_for_tests) { 4 }
754
+ let(:fallback_hosts) { %w(a.foo.com b.foo.com) }
755
+ let(:fallback_hosts_used) { Array.new }
756
+ let(:client_options) { timeout_options.merge(environment: environment, fallback_hosts: fallback_hosts) }
757
+
758
+ it 'uses a fallback host on every subsequent disconnected attempt until suspended (#RTN17b, #TO3k6)' do
759
+ request = 0
760
+ allow(connection).to receive(:create_transport) do |host|
761
+ if request == 0
762
+ expect(host).to eql(expected_host)
763
+ else
764
+ fallback_hosts_used << host
765
+ end
766
+ request += 1
767
+ raise EventMachine::ConnectionError
768
+ end
769
+
770
+ connection.once(:suspended) do
771
+ expect(fallback_hosts_used.uniq).to match_array(fallback_hosts + [expected_host])
772
+ stop_reactor
773
+ end
774
+ end
775
+ end
678
776
  end
679
777
 
680
778
  context 'with production environment' do
@@ -694,7 +792,7 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
694
792
  end
695
793
 
696
794
  it 'never uses a fallback host' do
697
- expect(EventMachine).to receive(:connect).exactly(retry_count_for_all_states).times do |host|
795
+ expect(connection).to receive(:create_transport).exactly(retry_count_for_all_states).times do |host|
698
796
  expect(host).to eql(expected_host)
699
797
  raise EventMachine::ConnectionError
700
798
  end
@@ -713,43 +811,92 @@ describe Ably::Realtime::Connection, 'failures', :event_machine do
713
811
  @suspended = 0
714
812
  end
715
813
 
716
- it 'uses a fallback host on every subsequent disconnected attempt until suspended' do
717
- request = 0
718
- expect(EventMachine).to receive(:connect).exactly(retry_count_for_one_state).times do |host|
719
- if request == 0
720
- expect(host).to eql(expected_host)
721
- else
722
- fallback_hosts_used << host
814
+ context 'and default options' do
815
+ let(:max_time_in_state_for_tests) { 2 } # allow time for 3 attempts, 2 configured fallbacks + primary host
816
+
817
+ it 'uses a fallback host + the original host once on every subsequent disconnected attempt until suspended' do
818
+ request = 0
819
+ expect(connection).to receive(:create_transport).exactly(retry_count_for_one_state).times do |host|
820
+ if request == 0
821
+ expect(host).to eql(expected_host)
822
+ else
823
+ fallback_hosts_used << host
824
+ end
825
+ request += 1
826
+ raise EventMachine::ConnectionError
827
+ end
828
+
829
+ connection.once(:suspended) do
830
+ fallback_hosts_used.pop # remove suspended attempt host
831
+ expect(fallback_hosts_used.uniq).to match_array(custom_hosts + [expected_host])
832
+ stop_reactor
723
833
  end
724
- request += 1
725
- raise EventMachine::ConnectionError
726
834
  end
727
835
 
728
- connection.once(:suspended) do
729
- fallback_hosts_used.pop # remove suspended attempt host
730
- expect(fallback_hosts_used.uniq).to match_array(custom_hosts)
731
- stop_reactor
836
+ it 'uses the primary host when suspended, and then every fallback host and the primary host again on every subsequent suspended attempt' do
837
+ request = 0
838
+ expect(connection).to receive(:create_transport).at_least(:once) do |host|
839
+ if request == 0 || request == expected_retry_attempts + 1
840
+ expect(host).to eql(expected_host)
841
+ else
842
+ expect(custom_hosts + [expected_host]).to include(host)
843
+ fallback_hosts_used << host if @suspended > 0
844
+ end
845
+ request += 1
846
+ raise EventMachine::ConnectionError
847
+ end
848
+
849
+ connection.on(:suspended) do
850
+ @suspended += 1
851
+
852
+ if @suspended > 4
853
+ expect(fallback_hosts_used.uniq).to match_array(custom_hosts + [expected_host])
854
+ stop_reactor
855
+ end
856
+ end
732
857
  end
733
858
  end
734
859
 
735
- it 'uses the primary host when suspended, and a fallback host on every subsequent suspended attempt' do
736
- request = 0
737
- expect(EventMachine).to receive(:connect).at_least(:once) do |host|
738
- if request == 0 || request == expected_retry_attempts + 1
739
- expect(host).to eql(expected_host)
740
- else
741
- expect(custom_hosts).to include(host)
742
- fallback_hosts_used << host if @suspended > 0
860
+ context ':fallback_hosts array is provided by an empty array' do
861
+ let(:max_time_in_state_for_tests) { 3 }
862
+ let(:fallback_hosts) { [] }
863
+ let(:hosts_used) { Array.new }
864
+ let(:client_options) { timeout_options.merge(environment: 'production', fallback_hosts: fallback_hosts) }
865
+
866
+ it 'uses a fallback host on every subsequent disconnected attempt until suspended (#RTN17b, #TO3k6)' do
867
+ allow(connection).to receive(:create_transport) do |host|
868
+ hosts_used << host
869
+ raise EventMachine::ConnectionError
870
+ end
871
+
872
+ connection.once(:suspended) do
873
+ expect(hosts_used.uniq.length).to eql(1)
874
+ expect(hosts_used.uniq.first).to eql(expected_host)
875
+ stop_reactor
743
876
  end
744
- request += 1
745
- raise EventMachine::ConnectionError
746
877
  end
878
+ end
879
+
880
+ context ':fallback_hosts array is provided' do
881
+ let(:max_time_in_state_for_tests) { 3 }
882
+ let(:fallback_hosts) { %w(a.foo.com b.foo.com) }
883
+ let(:fallback_hosts_used) { Array.new }
884
+ let(:client_options) { timeout_options.merge(environment: 'production', fallback_hosts: fallback_hosts) }
747
885
 
748
- connection.on(:suspended) do
749
- @suspended += 1
886
+ it 'uses a fallback host on every subsequent disconnected attempt until suspended (#RTN17b, #TO3k6)' do
887
+ request = 0
888
+ allow(connection).to receive(:create_transport) do |host|
889
+ if request == 0
890
+ expect(host).to eql(expected_host)
891
+ else
892
+ fallback_hosts_used << host
893
+ end
894
+ request += 1
895
+ raise EventMachine::ConnectionError
896
+ end
750
897
 
751
- if @suspended > 3
752
- expect(fallback_hosts_used.uniq).to match_array(custom_hosts)
898
+ connection.once(:suspended) do
899
+ expect(fallback_hosts_used.uniq).to match_array(fallback_hosts + [expected_host])
753
900
  stop_reactor
754
901
  end
755
902
  end
@@ -833,6 +833,22 @@ describe Ably::Realtime::Connection, :event_machine do
833
833
  end
834
834
  end
835
835
  end
836
+
837
+ context 'with a different connection_state_ttl' do
838
+ before do
839
+ old_defaults = Ably::Realtime::Connection::DEFAULTS
840
+ stub_const 'Ably::Realtime::Connection::DEFAULTS', old_defaults.merge(connection_state_ttl: 15)
841
+ end
842
+
843
+ it 'updates the private Connection#connection_state_ttl' do
844
+ expect(connection.connection_state_ttl).to eql(15)
845
+
846
+ connection.once(:connected) do
847
+ expect(connection.connection_state_ttl).to be > 15
848
+ stop_reactor
849
+ end
850
+ end
851
+ end
836
852
  end
837
853
 
838
854
  context 'recovery' do
@@ -848,14 +864,14 @@ describe Ably::Realtime::Connection, :event_machine do
848
864
  log_level: :none,
849
865
  disconnected_retry_timeout: 0.1,
850
866
  suspended_retry_timeout: 0.1,
851
- connection_state_ttl: 0.2,
867
+ max_connection_state_ttl: 0.2,
852
868
  realtime_request_timeout: 5
853
869
  )
854
870
  end
855
871
 
856
872
  describe '#recovery_key' do
857
873
  def self.available_states
858
- [:connecting, :connected, :disconnected, :suspended, :failed]
874
+ [:connecting, :connected, :disconnected]
859
875
  end
860
876
  let(:available_states) { self.class.available_states }
861
877
  let(:states) { Hash.new }
@@ -968,19 +984,25 @@ describe Ably::Realtime::Connection, :event_machine do
968
984
  context 'when messages have been sent whilst the old connection is disconnected' do
969
985
  describe 'the new connection' do
970
986
  it 'recovers server-side queued messages' do
987
+ connection_id, recovery_key = nil, nil
988
+
971
989
  channel.attach do
972
- connection.transition_state_machine! :failed
990
+ connection_id = client.connection.id
991
+ recovery_key = client.connection.recovery_key
992
+ connection.transport.__incoming_protocol_msgbus__
993
+ publishing_client_channel.publish('event', 'message') do
994
+ connection.transition_state_machine! :failed
995
+ end
973
996
  end
974
997
 
975
998
  connection.on(:failed) do
976
- publishing_client_channel.publish('event', 'message') do
977
- recover_client = auto_close Ably::Realtime::Client.new(default_options.merge(recover: client.connection.recovery_key))
978
- recover_client_channel = recover_client.channel(channel_name)
979
- recover_client_channel.attach do
980
- recover_client_channel.subscribe('event') do |message|
981
- expect(message.data).to eql('message')
982
- stop_reactor
983
- end
999
+ recover_client = auto_close Ably::Realtime::Client.new(default_options.merge(recover: recovery_key))
1000
+ recover_client_channel = recover_client.channel(channel_name)
1001
+ recover_client_channel.attach do
1002
+ expect(recover_client.connection.id).to eql(connection_id)
1003
+ recover_client_channel.subscribe('event') do |message|
1004
+ expect(message.data).to eql('message')
1005
+ stop_reactor
984
1006
  end
985
1007
  end
986
1008
  end
@@ -1212,7 +1234,7 @@ describe Ably::Realtime::Connection, :event_machine do
1212
1234
  log_level: :fatal,
1213
1235
  disconnected_retry_timeout: 0.02,
1214
1236
  suspended_retry_timeout: 60,
1215
- connection_state_ttl: 0.05
1237
+ max_connection_state_ttl: 0.05
1216
1238
  )
1217
1239
  end
1218
1240
 
@@ -1375,6 +1397,27 @@ describe Ably::Realtime::Connection, :event_machine do
1375
1397
  end
1376
1398
  client
1377
1399
  end
1400
+
1401
+ context 'with variant' do
1402
+ let(:variant) { 'foo ' }
1403
+
1404
+ before do
1405
+ Ably.lib_variant = variant
1406
+ end
1407
+
1408
+ after do
1409
+ Ably.lib_variant = nil
1410
+ end
1411
+
1412
+ it 'sends the lib version param lib with the variant' do
1413
+ expect(EventMachine).to receive(:connect) do |host, port, transport, object, url|
1414
+ uri = URI.parse(url)
1415
+ expect(CGI::parse(uri.query)['lib'][0]).to eql("ruby-#{variant}-#{Ably::VERSION}")
1416
+ stop_reactor
1417
+ end
1418
+ client
1419
+ end
1420
+ end
1378
1421
  end
1379
1422
  end
1380
1423
  end
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
2
  require 'spec_helper'
3
+ require 'webrick'
3
4
 
4
5
  describe Ably::Rest::Client do
5
6
  vary_by_protocol do
@@ -214,7 +215,7 @@ describe Ably::Rest::Client do
214
215
 
215
216
  context 'connection transport' do
216
217
  context 'defaults' do
217
- let(:client_options) { default_options.merge(key: api_key) }
218
+ let(:client_options) { default_options.merge(key: api_key, environment: 'production') }
218
219
 
219
220
  context 'for default host' do
220
221
  it "is configured to timeout connection opening in #{http_defaults.fetch(:open_timeout)} seconds" do
@@ -240,7 +241,7 @@ describe Ably::Rest::Client do
240
241
  context 'with custom http_open_timeout and http_request_timeout options' do
241
242
  let(:http_open_timeout) { 999 }
242
243
  let(:http_request_timeout) { 666 }
243
- let(:client_options) { default_options.merge(key: api_key, http_open_timeout: http_open_timeout, http_request_timeout: http_request_timeout) }
244
+ let(:client_options) { default_options.merge(key: api_key, http_open_timeout: http_open_timeout, http_request_timeout: http_request_timeout, environment: 'production') }
244
245
 
245
246
  context 'for default host' do
246
247
  it 'is configured to use custom open timeout' do
@@ -269,7 +270,7 @@ describe Ably::Rest::Client do
269
270
  let(:publish_block) { proc { client.channel('test').publish('event', 'data') } }
270
271
 
271
272
  context 'configured' do
272
- let(:client_options) { default_options.merge(key: api_key) }
273
+ let(:client_options) { default_options.merge(key: api_key, environment: 'production') }
273
274
 
274
275
  it 'should make connection attempts to A.ably-realtime.com, B.ably-realtime.com, C.ably-realtime.com, D.ably-realtime.com, E.ably-realtime.com' do
275
276
  hosts = []
@@ -411,6 +412,279 @@ describe Ably::Rest::Client do
411
412
  end
412
413
  end
413
414
  end
415
+
416
+ context 'when environment is production and server returns a 50x error' do
417
+ let(:custom_hosts) { %w(A.foo.com B.foo.com) }
418
+ let(:max_retry_count) { 2 }
419
+ let(:max_retry_duration) { 0.5 }
420
+ let(:fallback_block) { Proc.new { raise Faraday::SSLError.new('ssl error message') } }
421
+ let(:production_options) do
422
+ default_options.merge(
423
+ environment: nil,
424
+ key: api_key,
425
+ http_max_retry_duration: max_retry_duration,
426
+ http_max_retry_count: max_retry_count
427
+ )
428
+ end
429
+
430
+ let(:status) { 502 }
431
+ let(:fallback_block) do
432
+ Proc.new do
433
+ {
434
+ headers: { 'Content-Type' => 'text/html' },
435
+ status: status
436
+ }
437
+ end
438
+ end
439
+ let!(:default_host_request_stub) do
440
+ stub_request(:post, "https://#{api_key}@#{Ably::Rest::Client::DOMAIN}#{path}").to_return(&fallback_block)
441
+ end
442
+
443
+ context 'with custom fallback hosts provided' do
444
+ let!(:first_fallback_request_stub) do
445
+ stub_request(:post, "https://#{api_key}@#{custom_hosts[0]}#{path}").to_return(&fallback_block)
446
+ end
447
+
448
+ let!(:second_fallback_request_stub) do
449
+ stub_request(:post, "https://#{api_key}@#{custom_hosts[1]}#{path}").to_return(&fallback_block)
450
+ end
451
+
452
+ let(:client_options) {
453
+ production_options.merge(fallback_hosts: custom_hosts)
454
+ }
455
+
456
+ it 'attempts the fallback hosts as this is an authentication failure (#RSC15b, #TO3k6)' do
457
+ expect { publish_block.call }.to raise_error(Ably::Exceptions::ServerError)
458
+ expect(default_host_request_stub).to have_been_requested
459
+ expect(first_fallback_request_stub).to have_been_requested
460
+ expect(second_fallback_request_stub).to have_been_requested
461
+ end
462
+ end
463
+
464
+ context 'with an empty array of fallback hosts provided (#RSC15b, #TO3k6)' do
465
+ let(:client_options) {
466
+ production_options.merge(fallback_hosts: [])
467
+ }
468
+
469
+ it 'does not attempt the fallback hosts as this is an authentication failure' do
470
+ expect { publish_block.call }.to raise_error(Ably::Exceptions::ServerError)
471
+ expect(default_host_request_stub).to have_been_requested
472
+ end
473
+ end
474
+
475
+ context 'using a local web-server', webmock: false do
476
+ let(:primary_host) { 'local-rest.ably.io' }
477
+ let(:fallbacks) { ['local.ably.io', 'localhost'] }
478
+ let(:port) { rand(10000) + 2000 }
479
+ let(:channel_name) { 'foo' }
480
+ let(:request_timeout) { 3 }
481
+
482
+ after do
483
+ @web_server.shutdown
484
+ end
485
+
486
+ context 'and timing out the primary host' do
487
+ before do
488
+ @web_server = WEBrick::HTTPServer.new(:Port => port, :SSLEnable => false)
489
+ @web_server.mount_proc "/channels/#{channel_name}/publish" do |req, res|
490
+ if req.header["host"].first.include?(primary_host)
491
+ @primary_host_requested = true
492
+ sleep request_timeout + 0.5
493
+ else
494
+ @fallback_request_count ||= 0
495
+ @fallback_request_count += 1
496
+ if @fallback_request_count <= fail_fallback_request_count
497
+ sleep request_timeout + 0.5
498
+ else
499
+ res.status = 200
500
+ res['Content-Type'] = 'application/json'
501
+ res.body = '{}'
502
+ end
503
+ end
504
+ end
505
+ Thread.new do
506
+ @web_server.start
507
+ end
508
+ end
509
+
510
+ context 'with request timeout less than max_retry_duration' do
511
+ let(:client_options) do
512
+ default_options.merge(
513
+ rest_host: primary_host,
514
+ fallback_hosts: fallbacks,
515
+ token: 'fake.token',
516
+ port: port,
517
+ tls: false,
518
+ http_request_timeout: request_timeout,
519
+ max_retry_duration: request_timeout * 3
520
+ )
521
+ end
522
+ let(:fail_fallback_request_count) { 1 }
523
+
524
+ it 'tries one of the fallback hosts' do
525
+ client.channel(channel_name).publish('event', 'data')
526
+ expect(@primary_host_requested).to be_truthy
527
+ expect(@fallback_request_count).to eql(2)
528
+ end
529
+ end
530
+
531
+ context 'with request timeout less than max_retry_duration' do
532
+ let(:client_options) do
533
+ default_options.merge(
534
+ rest_host: primary_host,
535
+ fallback_hosts: fallbacks,
536
+ token: 'fake.token',
537
+ port: port,
538
+ tls: false,
539
+ http_request_timeout: request_timeout,
540
+ max_retry_duration: request_timeout / 2
541
+ )
542
+ end
543
+ let(:fail_fallback_request_count) { 0 }
544
+
545
+ it 'tries one of the fallback hosts' do
546
+ client.channel(channel_name).publish('event', 'data')
547
+ expect(@primary_host_requested).to be_truthy
548
+ expect(@fallback_request_count).to eql(1)
549
+ end
550
+ end
551
+ end
552
+
553
+ context 'and failing the primary host' do
554
+ before do
555
+ @web_server = WEBrick::HTTPServer.new(:Port => port, :SSLEnable => false)
556
+ @web_server.mount_proc "/channels/#{channel_name}/publish" do |req, res|
557
+ if req.header["host"].first.include?(primary_host)
558
+ @primary_host_requested = true
559
+ res.status = 500
560
+ else
561
+ @fallback_request_count ||= 0
562
+ @fallback_request_count += 1
563
+ if @fallback_request_count <= fail_fallback_request_count
564
+ res.status = 500
565
+ else
566
+ res.status = 200
567
+ res['Content-Type'] = 'application/json'
568
+ res.body = '{}'
569
+ end
570
+ end
571
+ end
572
+ Thread.new do
573
+ @web_server.start
574
+ end
575
+ end
576
+
577
+ let(:client_options) do
578
+ default_options.merge(
579
+ rest_host: primary_host,
580
+ fallback_hosts: fallbacks,
581
+ token: 'fake.token',
582
+ port: port,
583
+ tls: false
584
+ )
585
+ end
586
+ let(:fail_fallback_request_count) { 1 }
587
+
588
+ it 'tries one of the fallback hosts' do
589
+ client.channel(channel_name).publish('event', 'data')
590
+ expect(@primary_host_requested).to be_truthy
591
+ expect(@fallback_request_count).to eql(2)
592
+ end
593
+ end
594
+ end
595
+ end
596
+
597
+ context 'when environment is not production and server returns a 50x error' do
598
+ let(:custom_hosts) { %w(A.foo.com B.foo.com) }
599
+ let(:max_retry_count) { 2 }
600
+ let(:max_retry_duration) { 0.5 }
601
+ let(:fallback_block) { Proc.new { raise Faraday::SSLError.new('ssl error message') } }
602
+ let(:env) { 'custom-env' }
603
+ let(:production_options) do
604
+ default_options.merge(
605
+ environment: env,
606
+ key: api_key,
607
+ http_max_retry_duration: max_retry_duration,
608
+ http_max_retry_count: max_retry_count
609
+ )
610
+ end
611
+
612
+ let(:status) { 502 }
613
+ let(:fallback_block) do
614
+ Proc.new do
615
+ {
616
+ headers: { 'Content-Type' => 'text/html' },
617
+ status: status
618
+ }
619
+ end
620
+ end
621
+ let!(:default_host_request_stub) do
622
+ stub_request(:post, "https://#{api_key}@#{env}-#{Ably::Rest::Client::DOMAIN}#{path}").to_return(&fallback_block)
623
+ end
624
+
625
+ context 'with custom fallback hosts provided (#RSC15b, #TO3k6)' do
626
+ let!(:first_fallback_request_stub) do
627
+ stub_request(:post, "https://#{api_key}@#{custom_hosts[0]}#{path}").to_return(&fallback_block)
628
+ end
629
+
630
+ let!(:second_fallback_request_stub) do
631
+ stub_request(:post, "https://#{api_key}@#{custom_hosts[1]}#{path}").to_return(&fallback_block)
632
+ end
633
+
634
+ let(:client_options) {
635
+ production_options.merge(fallback_hosts: custom_hosts)
636
+ }
637
+
638
+ it 'attempts the fallback hosts as this is an authentication failure' do
639
+ expect { publish_block.call }.to raise_error(Ably::Exceptions::ServerError)
640
+ expect(default_host_request_stub).to have_been_requested
641
+ expect(first_fallback_request_stub).to have_been_requested
642
+ expect(second_fallback_request_stub).to have_been_requested
643
+ end
644
+ end
645
+
646
+ context 'with an empty array of fallback hosts provided (#RSC15b, #TO3k6)' do
647
+ let(:client_options) {
648
+ production_options.merge(fallback_hosts: [])
649
+ }
650
+
651
+ it 'does not attempt the fallback hosts as this is an authentication failure' do
652
+ expect { publish_block.call }.to raise_error(Ably::Exceptions::ServerError)
653
+ expect(default_host_request_stub).to have_been_requested
654
+ end
655
+ end
656
+
657
+ context 'with fallback_hosts_use_default: true (#RSC15b, #TO3k7)' do
658
+ let(:custom_hosts) { Ably::FALLBACK_HOSTS[0...2] }
659
+
660
+ before do
661
+ stub_const 'Ably::FALLBACK_HOSTS', custom_hosts
662
+ end
663
+
664
+ let(:client_options) {
665
+ production_options.merge(fallback_hosts_use_default: true)
666
+ }
667
+
668
+ let!(:first_fallback_request_stub) do
669
+ stub_request(:post, "https://#{api_key}@#{Ably::FALLBACK_HOSTS[0]}#{path}").to_return(&fallback_block)
670
+ end
671
+
672
+ let!(:second_fallback_request_stub) do
673
+ stub_request(:post, "https://#{api_key}@#{Ably::FALLBACK_HOSTS[1]}#{path}").to_return(&fallback_block)
674
+ end
675
+
676
+ let(:client_options) {
677
+ production_options.merge(fallback_hosts: custom_hosts)
678
+ }
679
+
680
+ it 'attempts the default fallback hosts as this is an authentication failure' do
681
+ expect { publish_block.call }.to raise_error(Ably::Exceptions::ServerError)
682
+ expect(default_host_request_stub).to have_been_requested
683
+ expect(first_fallback_request_stub).to have_been_requested
684
+ expect(second_fallback_request_stub).to have_been_requested
685
+ end
686
+ end
687
+ end
414
688
  end
415
689
 
416
690
  context 'with a custom host' do
@@ -547,19 +821,38 @@ describe Ably::Rest::Client do
547
821
  end
548
822
 
549
823
  context 'version headers', :webmock do
550
- let(:client_options) { default_options.merge(key: api_key) }
551
- let!(:publish_message_stub) do
552
- stub_request(:post, "#{client.endpoint.to_s.gsub('://', "://#{api_key}@")}/channels/foo/publish").
553
- with(headers: {
554
- 'X-Ably-Version' => Ably::PROTOCOL_VERSION,
555
- 'X-Ably-Lib' => "ruby-#{Ably::VERSION}"
556
- }).
557
- to_return(status: 201, body: '{}', headers: { 'Content-Type' => 'application/json' })
558
- end
824
+ [nil, 'foo'].each do |variant|
825
+ context "with variant #{variant ? variant : 'none'}" do
826
+ if variant
827
+ before do
828
+ Ably.lib_variant = variant
829
+ end
559
830
 
560
- it 'sends a protocol version and lib version header' do
561
- client.channels.get('foo').publish("event")
562
- expect(publish_message_stub).to have_been_requested
831
+ after do
832
+ Ably.lib_variant = nil
833
+ end
834
+ end
835
+
836
+ let(:client_options) { default_options.merge(key: api_key) }
837
+ let!(:publish_message_stub) do
838
+ lib = ['ruby']
839
+ lib << variant if variant
840
+ lib << Ably::VERSION
841
+
842
+
843
+ stub_request(:post, "#{client.endpoint.to_s.gsub('://', "://#{api_key}@")}/channels/foo/publish").
844
+ with(headers: {
845
+ 'X-Ably-Version' => Ably::PROTOCOL_VERSION,
846
+ 'X-Ably-Lib' => lib.join('-')
847
+ }).
848
+ to_return(status: 201, body: '{}', headers: { 'Content-Type' => 'application/json' })
849
+ end
850
+
851
+ it 'sends a protocol version and lib version header' do
852
+ client.channels.get('foo').publish("event")
853
+ expect(publish_message_stub).to have_been_requested
854
+ end
855
+ end
563
856
  end
564
857
  end
565
858
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ably
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.13
4
+ version: 0.8.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lewis Marshall
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-09-29 00:00:00.000000000 Z
12
+ date: 2016-09-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: eventmachine