ably 0.8.15 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -4
  3. data/CHANGELOG.md +6 -2
  4. data/README.md +5 -1
  5. data/SPEC.md +1473 -852
  6. data/ably.gemspec +11 -8
  7. data/lib/ably/auth.rb +90 -53
  8. data/lib/ably/exceptions.rb +37 -8
  9. data/lib/ably/logger.rb +10 -1
  10. data/lib/ably/models/auth_details.rb +42 -0
  11. data/lib/ably/models/channel_state_change.rb +18 -4
  12. data/lib/ably/models/connection_details.rb +6 -3
  13. data/lib/ably/models/connection_state_change.rb +4 -3
  14. data/lib/ably/models/error_info.rb +1 -1
  15. data/lib/ably/models/message.rb +17 -1
  16. data/lib/ably/models/message_encoders/base.rb +103 -82
  17. data/lib/ably/models/message_encoders/base64.rb +1 -1
  18. data/lib/ably/models/presence_message.rb +16 -1
  19. data/lib/ably/models/protocol_message.rb +20 -3
  20. data/lib/ably/models/token_details.rb +11 -1
  21. data/lib/ably/models/token_request.rb +16 -6
  22. data/lib/ably/modules/async_wrapper.rb +7 -3
  23. data/lib/ably/modules/encodeable.rb +51 -12
  24. data/lib/ably/modules/enum.rb +17 -7
  25. data/lib/ably/modules/event_emitter.rb +29 -14
  26. data/lib/ably/modules/model_common.rb +13 -21
  27. data/lib/ably/modules/state_emitter.rb +7 -4
  28. data/lib/ably/modules/state_machine.rb +2 -4
  29. data/lib/ably/modules/uses_state_machine.rb +7 -3
  30. data/lib/ably/realtime.rb +2 -0
  31. data/lib/ably/realtime/auth.rb +102 -42
  32. data/lib/ably/realtime/channel.rb +68 -26
  33. data/lib/ably/realtime/channel/channel_manager.rb +154 -65
  34. data/lib/ably/realtime/channel/channel_state_machine.rb +14 -15
  35. data/lib/ably/realtime/client.rb +18 -3
  36. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +38 -29
  37. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +6 -1
  38. data/lib/ably/realtime/connection.rb +108 -49
  39. data/lib/ably/realtime/connection/connection_manager.rb +167 -61
  40. data/lib/ably/realtime/connection/connection_state_machine.rb +22 -3
  41. data/lib/ably/realtime/connection/websocket_transport.rb +19 -10
  42. data/lib/ably/realtime/presence.rb +70 -45
  43. data/lib/ably/realtime/presence/members_map.rb +201 -36
  44. data/lib/ably/realtime/presence/presence_manager.rb +30 -6
  45. data/lib/ably/realtime/presence/presence_state_machine.rb +5 -12
  46. data/lib/ably/rest.rb +2 -2
  47. data/lib/ably/rest/channel.rb +5 -5
  48. data/lib/ably/rest/client.rb +31 -27
  49. data/lib/ably/rest/middleware/exceptions.rb +1 -3
  50. data/lib/ably/rest/middleware/logger.rb +2 -2
  51. data/lib/ably/rest/presence.rb +2 -2
  52. data/lib/ably/util/pub_sub.rb +1 -1
  53. data/lib/ably/util/safe_deferrable.rb +26 -0
  54. data/lib/ably/version.rb +2 -2
  55. data/spec/acceptance/realtime/auth_spec.rb +470 -111
  56. data/spec/acceptance/realtime/channel_history_spec.rb +5 -3
  57. data/spec/acceptance/realtime/channel_spec.rb +1017 -168
  58. data/spec/acceptance/realtime/client_spec.rb +6 -6
  59. data/spec/acceptance/realtime/connection_failures_spec.rb +458 -27
  60. data/spec/acceptance/realtime/connection_spec.rb +424 -105
  61. data/spec/acceptance/realtime/message_spec.rb +52 -23
  62. data/spec/acceptance/realtime/presence_history_spec.rb +5 -3
  63. data/spec/acceptance/realtime/presence_spec.rb +1110 -96
  64. data/spec/acceptance/rest/auth_spec.rb +222 -59
  65. data/spec/acceptance/rest/base_spec.rb +1 -1
  66. data/spec/acceptance/rest/channel_spec.rb +1 -2
  67. data/spec/acceptance/rest/client_spec.rb +104 -48
  68. data/spec/acceptance/rest/message_spec.rb +42 -15
  69. data/spec/acceptance/rest/presence_spec.rb +4 -11
  70. data/spec/rspec_config.rb +2 -1
  71. data/spec/shared/client_initializer_behaviour.rb +2 -2
  72. data/spec/shared/safe_deferrable_behaviour.rb +6 -2
  73. data/spec/spec_helper.rb +4 -2
  74. data/spec/support/debug_failure_helper.rb +20 -4
  75. data/spec/support/event_machine_helper.rb +32 -1
  76. data/spec/unit/auth_spec.rb +4 -11
  77. data/spec/unit/logger_spec.rb +28 -2
  78. data/spec/unit/models/auth_details_spec.rb +49 -0
  79. data/spec/unit/models/channel_state_change_spec.rb +23 -3
  80. data/spec/unit/models/connection_details_spec.rb +12 -1
  81. data/spec/unit/models/connection_state_change_spec.rb +15 -4
  82. data/spec/unit/models/message_encoders/base64_spec.rb +2 -1
  83. data/spec/unit/models/message_spec.rb +153 -0
  84. data/spec/unit/models/presence_message_spec.rb +192 -0
  85. data/spec/unit/models/protocol_message_spec.rb +64 -6
  86. data/spec/unit/models/token_details_spec.rb +75 -0
  87. data/spec/unit/models/token_request_spec.rb +74 -0
  88. data/spec/unit/modules/async_wrapper_spec.rb +2 -1
  89. data/spec/unit/modules/enum_spec.rb +69 -0
  90. data/spec/unit/modules/event_emitter_spec.rb +149 -22
  91. data/spec/unit/modules/state_emitter_spec.rb +9 -3
  92. data/spec/unit/realtime/client_spec.rb +1 -1
  93. data/spec/unit/realtime/connection_spec.rb +8 -5
  94. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +1 -1
  95. data/spec/unit/realtime/presence_spec.rb +4 -3
  96. data/spec/unit/rest/client_spec.rb +1 -1
  97. data/spec/unit/util/crypto_spec.rb +3 -3
  98. metadata +22 -19
@@ -28,6 +28,25 @@ module Ably::Realtime
28
28
  presence.members.change_state :sync_starting
29
29
  end
30
30
 
31
+ # Process presence messages from SYNC messages. Sync can be server-initiated or triggered following ATTACH
32
+ #
33
+ # @return [void]
34
+ #
35
+ # @api private
36
+ def sync_process_messages(serial, presence_messages)
37
+ unless presence.members.sync_starting?
38
+ presence.members.change_state :sync_starting
39
+ end
40
+
41
+ presence.members.update_sync_serial serial
42
+
43
+ presence_messages.each do |presence_message|
44
+ presence.__incoming_msgbus__.publish :sync, presence_message
45
+ end
46
+
47
+ presence.members.change_state :finalizing_sync if presence.members.sync_serial_cursor_at_end?
48
+ end
49
+
31
50
  # There server has indicated that there are no SYNC ProtocolMessages to come because
32
51
  # there are no members on this channel
33
52
  #
@@ -35,7 +54,8 @@ module Ably::Realtime
35
54
  #
36
55
  # @api private
37
56
  def sync_not_expected
38
- presence.members.change_state :in_sync
57
+ logger.debug { "#{self.class.name}: Emitting leave events for all members as a SYNC is not expected and thus there are no members on the channel" }
58
+ presence.members.change_state :sync_none
39
59
  end
40
60
 
41
61
  private
@@ -43,16 +63,20 @@ module Ably::Realtime
43
63
 
44
64
  def setup_channel_event_handlers
45
65
  channel.unsafe_on(:detached) do
46
- presence.transition_state_machine :left if presence.can_transition_to?(:left)
66
+ if !presence.initialized?
67
+ presence.transition_state_machine :left if presence.can_transition_to?(:left)
68
+ end
47
69
  end
48
70
 
49
71
  channel.unsafe_on(:failed) do |metadata|
50
- presence.transition_state_machine :failed, metadata if presence.can_transition_to?(:failed)
72
+ if !presence.initialized?
73
+ presence.transition_state_machine :left, metadata if presence.can_transition_to?(:left)
74
+ end
51
75
  end
76
+ end
52
77
 
53
- presence.unsafe_on(:entered) do |message|
54
- presence.set_connection_id message.connection_id
55
- end
78
+ def logger
79
+ presence.channel.client.logger
56
80
  end
57
81
  end
58
82
  end
@@ -15,19 +15,16 @@ module Ably::Realtime
15
15
  # :entered
16
16
  # :leaving
17
17
  # :left
18
- # :failed
19
18
  Presence::STATE.each_with_index do |state_enum, index|
20
19
  state state_enum.to_sym, initial: index == 0
21
20
  end
22
21
 
23
22
  # Entering or entered states can skip leaving and go straight to left if a channel is detached
24
23
  # A channel that detaches very quickly will also go straight to :left from :initialized
25
- # Failed states only occur when present and the channel fails or presence fails
26
24
  transition :from => :initialized, :to => [:entering, :left]
27
- transition :from => :entering, :to => [:entered, :leaving, :left, :failed]
28
- transition :from => :entered, :to => [:leaving, :left, :failed]
29
- transition :from => :leaving, :to => [:left, :entering, :failed]
30
- transition :from => :failed, :to => [:entering]
25
+ transition :from => :entering, :to => [:entered, :leaving, :left]
26
+ transition :from => :entered, :to => [:leaving, :left]
27
+ transition :from => :leaving, :to => [:left, :entering]
31
28
 
32
29
  after_transition do |presence, transition|
33
30
  presence.synchronize_state_with_statemachine
@@ -41,13 +38,9 @@ module Ably::Realtime
41
38
  presence.manager.leave current_transition.metadata
42
39
  end
43
40
 
44
- after_transition(to: [:failed]) do |presence, current_transition|
45
- presence.manager.emit_error current_transition.metadata
46
- end
47
-
48
41
  # Transitions responsible for updating channel#error_reason
49
- before_transition(to: [:left, :failed]) do |presence, current_transition|
50
- presence.channel.set_failed_channel_error_reason current_transition.metadata if is_error_type?(current_transition.metadata)
42
+ before_transition(to: [:left]) do |presence, current_transition|
43
+ presence.channel.set_channel_error_reason current_transition.metadata if is_error_type?(current_transition.metadata)
51
44
  end
52
45
 
53
46
  private
data/lib/ably/rest.rb CHANGED
@@ -3,12 +3,12 @@ require 'ably/rest/channels'
3
3
  require 'ably/rest/client'
4
4
  require 'ably/rest/presence'
5
5
 
6
+ require 'ably/models/message_encoders/base'
7
+
6
8
  Dir.glob(File.expand_path("models/*.rb", File.dirname(__FILE__))).each do |file|
7
9
  require file
8
10
  end
9
11
 
10
- require 'ably/models/message_encoders/base'
11
-
12
12
  module Ably
13
13
  # Rest provides the top-level class to be instanced for the Ably Rest library
14
14
  #
@@ -65,14 +65,14 @@ module Ably
65
65
 
66
66
  payload = messages.map do |message|
67
67
  Ably::Models::Message(message.dup).tap do |msg|
68
- msg.encode self
68
+ msg.encode client.encoders, options
69
69
 
70
70
  next if msg.client_id.nil?
71
71
  if msg.client_id == '*'
72
- raise Ably::Exceptions::IncompatibleClientId.new('Wildcard client_id is reserved and cannot be used when publishing messages', 400, 40012)
72
+ raise Ably::Exceptions::IncompatibleClientId.new('Wildcard client_id is reserved and cannot be used when publishing messages')
73
73
  end
74
74
  unless client.auth.can_assume_client_id?(msg.client_id)
75
- raise Ably::Exceptions::IncompatibleClientId.new("Cannot publish with client_id '#{msg.client_id}' as it is incompatible with the current configured client_id '#{client.client_id}'", 400, 40012)
75
+ raise Ably::Exceptions::IncompatibleClientId.new("Cannot publish with client_id '#{msg.client_id}' as it is incompatible with the current configured client_id '#{client.client_id}'")
76
76
  end
77
77
  end.as_json
78
78
  end
@@ -134,9 +134,9 @@ module Ably
134
134
  end
135
135
 
136
136
  def decode_message(message)
137
- message.decode self
137
+ message.decode client.encoders, options
138
138
  rescue Ably::Exceptions::CipherError, Ably::Exceptions::EncoderError => e
139
- client.logger.error "Decoding Error on channel '#{name}', message event name '#{message.name}'. #{e.class.name}: #{e.message}"
139
+ client.logger.error { "Decoding Error on channel '#{name}', message event name '#{message.name}'. #{e.class.name}: #{e.message}" }
140
140
  end
141
141
  end
142
142
  end
@@ -1,6 +1,7 @@
1
1
  require 'faraday'
2
2
  require 'json'
3
3
  require 'logger'
4
+ require 'uri'
4
5
 
5
6
  require 'ably/rest/middleware/exceptions'
6
7
 
@@ -24,8 +25,8 @@ module Ably
24
25
  # Configuration for HTTP timeouts and HTTP request reattempts to fallback hosts
25
26
  HTTP_DEFAULTS = {
26
27
  open_timeout: 4,
27
- request_timeout: 15,
28
- max_retry_duration: 10,
28
+ request_timeout: 10,
29
+ max_retry_duration: 15,
29
30
  max_retry_count: 3
30
31
  }.freeze
31
32
 
@@ -103,12 +104,12 @@ module Ably
103
104
  # @option options [Proc] :auth_callback when provided, the Proc will be called with the token params hash as the first argument, whenever a new token is required.
104
105
  # The Proc should return a token string, {Ably::Models::TokenDetails} or JSON equivalent, {Ably::Models::TokenRequest} or JSON equivalent
105
106
  # @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
106
- # @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}
107
+ # @option options [Hash] :default_token_params convenience to pass in +token_params+ that will be used as a default for all token requests. See {Auth#create_token_request}
107
108
  #
108
109
  # @option options [Integer] :http_open_timeout (4 seconds) timeout in seconds for opening an HTTP connection for all HTTP requests
109
- # @option options [Integer] :http_request_timeout (15 seconds) timeout in seconds for any single complete HTTP request and response
110
+ # @option options [Integer] :http_request_timeout (10 seconds) timeout in seconds for any single complete HTTP request and response
110
111
  # @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
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
112
+ # @option options [Integer] :http_max_retry_duration (15 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
112
113
  #
113
114
  # @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
115
  # @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
@@ -127,13 +128,14 @@ module Ably
127
128
 
128
129
  options = options.clone
129
130
  if options.kind_of?(String)
130
- options = if options.match(/^[\w-]{2,}\.[\w-]{2,}:[\w-]{2,}$/)
131
+ options = if options.match(Auth::API_KEY_REGEX)
131
132
  { key: options }
132
133
  else
133
134
  { token: options }
134
135
  end
135
136
  end
136
137
 
138
+ @realtime_client = options.delete(:realtime_client)
137
139
  @tls = options.delete(:tls) == false ? false : true
138
140
  @environment = options.delete(:environment) # nil is production
139
141
  @environment = nil if [:production, 'production'].include?(@environment)
@@ -182,7 +184,7 @@ module Ably
182
184
  end
183
185
  raise ArgumentError, 'Protocol is invalid. Must be either :msgpack or :json' unless [:msgpack, :json].include?(@protocol)
184
186
 
185
- token_params = options.delete(:token_params) || {}
187
+ token_params = options.delete(:default_token_params) || {}
186
188
  @options = options
187
189
  @auth = Auth.new(self, token_params, options)
188
190
  @channels = Ably::Rest::Channels.new(self)
@@ -285,14 +287,14 @@ module Ably
285
287
 
286
288
  response = case method.to_sym
287
289
  when :get
288
- reauthorise_on_authorisation_failure do
290
+ reauthorize_on_authorisation_failure do
289
291
  send_request(method, path, params, headers: headers)
290
292
  end
291
293
  when :post
292
294
  path_with_params = Addressable::URI.new
293
295
  path_with_params.query_values = params || {}
294
296
  query = path_with_params.query
295
- reauthorise_on_authorisation_failure do
297
+ reauthorize_on_authorisation_failure do
296
298
  send_request(method, "#{path}#{"?#{query}" unless query.nil? || query.empty?}", body, headers: headers)
297
299
  end
298
300
  end
@@ -346,18 +348,8 @@ module Ably
346
348
  # @return [void]
347
349
  #
348
350
  # @api private
349
- def register_encoder(encoder)
350
- encoder_klass = if encoder.kind_of?(String)
351
- encoder.split('::').inject(Kernel) do |base, klass_name|
352
- base.public_send(:const_get, klass_name)
353
- end
354
- else
355
- encoder
356
- end
357
-
358
- raise "Encoder must inherit from `Ably::Models::MessageEncoders::Base`" unless encoder_klass.ancestors.include?(Ably::Models::MessageEncoders::Base)
359
-
360
- encoders << encoder_klass.new(self)
351
+ def register_encoder(encoder, options = {})
352
+ encoders << Ably::Models::MessageEncoders.encoder_from(encoder, options)
361
353
  end
362
354
 
363
355
  # @!attribute [r] protocol_binary?
@@ -411,13 +403,25 @@ module Ably
411
403
  ].compact.join('-')
412
404
  end
413
405
 
406
+ # Allowable duration for an external auth request
407
+ # For REST client this defaults to request_timeout
408
+ # For Realtime clients this defaults to realtime_request_timeout
409
+ # @api private
410
+ def auth_request_timeout
411
+ if @realtime_client
412
+ @realtime_client.connection.defaults.fetch(:realtime_request_timeout)
413
+ else
414
+ http_defaults.fetch(:request_timeout)
415
+ end
416
+ end
417
+
414
418
  private
415
419
  def raw_request(method, path, params = {}, options = {})
416
420
  options = options.clone
417
- if options.delete(:disable_automatic_reauthorise) == true
421
+ if options.delete(:disable_automatic_reauthorize) == true
418
422
  send_request(method, path, params, options)
419
423
  else
420
- reauthorise_on_authorisation_failure do
424
+ reauthorize_on_authorisation_failure do
421
425
  send_request(method, path, params, options)
422
426
  end
423
427
  end
@@ -449,7 +453,7 @@ module Ably
449
453
  time_passed = Time.now - requested_at
450
454
  if can_fallback_to_alternate_ably_host? && retry_count < max_retry_count && time_passed <= max_retry_duration
451
455
  retry_count += 1
452
- logger.warn "Ably::Rest::Client - Retry #{retry_count} for #{method} #{path} #{params} as initial attempt failed: #{error}"
456
+ logger.warn { "Ably::Rest::Client - Retry #{retry_count} for #{method} #{path} #{params} as initial attempt failed: #{error}" }
453
457
  retry
454
458
  end
455
459
 
@@ -464,11 +468,11 @@ module Ably
464
468
  end
465
469
  end
466
470
 
467
- def reauthorise_on_authorisation_failure
471
+ def reauthorize_on_authorisation_failure
468
472
  yield
469
473
  rescue Ably::Exceptions::TokenExpired => e
470
474
  if auth.token_renewable?
471
- auth.authorise(nil, force: true)
475
+ auth.authorize
472
476
  yield
473
477
  else
474
478
  raise e
@@ -535,7 +539,7 @@ module Ably
535
539
  end
536
540
 
537
541
  def initialize_default_encoders
538
- Ably::Models::MessageEncoders.register_default_encoders self
542
+ Ably::Models::MessageEncoders.register_default_encoders self, binary_protocol: protocol == :msgpack
539
543
  end
540
544
  end
541
545
  end
@@ -7,8 +7,6 @@ module Ably
7
7
  # HTTP exceptions raised by Ably due to an error status code
8
8
  # Ably returns JSON/Msgpack error codes and messages so include this if possible in the exception messages
9
9
  class Exceptions < Faraday::Response::Middleware
10
- TOKEN_EXPIRED_CODE = 40140..40149
11
-
12
10
  def on_complete(env)
13
11
  if env.status >= 400
14
12
  error_status_code = env.status
@@ -35,7 +33,7 @@ module Ably
35
33
  if env.status >= 500
36
34
  raise Ably::Exceptions::ServerError.new(*exception_args)
37
35
  elsif env.status == 401
38
- if TOKEN_EXPIRED_CODE.include?(error_code)
36
+ if Ably::Exceptions::TOKEN_EXPIRED_CODE.include?(error_code)
39
37
  raise Ably::Exceptions::TokenExpired.new(*exception_args)
40
38
  else
41
39
  raise Ably::Exceptions::UnauthorizedRequest.new(*exception_args)
@@ -17,8 +17,8 @@ module Ably
17
17
  def_delegators :@logger, :debug, :info, :warn, :error, :fatal
18
18
 
19
19
  def call(env)
20
- debug "=> URL: #{env.method} #{env.url}, Headers: #{dump_headers env.request_headers}"
21
- debug "=> Body: #{body_for(env)}"
20
+ debug { "=> URL: #{env.method} #{env.url}, Headers: #{dump_headers env.request_headers}" }
21
+ debug { "=> Body: #{body_for(env)}" }
22
22
  super
23
23
  end
24
24
 
@@ -88,9 +88,9 @@ module Ably
88
88
  end
89
89
 
90
90
  def decode_message(presence_message)
91
- presence_message.decode channel
91
+ presence_message.decode client.encoders, channel.options
92
92
  rescue Ably::Exceptions::CipherError, Ably::Exceptions::EncoderError => e
93
- client.logger.error "Decoding Error on presence channel '#{channel.name}', presence message client_id '#{presence_message.client_id}'. #{e.class.name}: #{e.message}"
93
+ client.logger.error { "Decoding Error on presence channel '#{channel.name}', presence message client_id '#{presence_message.client_id}'. #{e.class.name}: #{e.message}" }
94
94
  end
95
95
  end
96
96
  end
@@ -38,7 +38,7 @@ module Ably::Util
38
38
 
39
39
  alias_method :subscribe, :unsafe_on
40
40
  alias_method :publish, :emit
41
- alias_method :unsubscribe, :off
41
+ alias_method :unsubscribe, :unsafe_off
42
42
  end
43
43
  end
44
44
  end
@@ -14,5 +14,31 @@ module Ably::Util
14
14
  def initialize(logger)
15
15
  @logger = logger
16
16
  end
17
+
18
+ # Create a new {SafeDeferrable} and fail immediately with the provided error in the next eventloop cycle
19
+ #
20
+ # @param error [Ably::Exceptions::BaseAblyException, Ably::Models::ErrorInfo] The error used to fail the newly created {SafeDeferrable}
21
+ #
22
+ # @return [SafeDeferrable]
23
+ #
24
+ def self.new_and_fail_immediately(logger, error)
25
+ new(logger).tap do |deferrable|
26
+ EventMachine.next_tick do
27
+ deferrable.fail error
28
+ end
29
+ end
30
+ end
31
+
32
+ # Create a new {SafeDeferrable} and succeed immediately with the provided arguments in the next eventloop cycle
33
+ #
34
+ # @return [SafeDeferrable]
35
+ #
36
+ def self.new_and_succeed_immediately(logger, *args)
37
+ new(logger).tap do |deferrable|
38
+ EventMachine.next_tick do
39
+ deferrable.succeed *args
40
+ end
41
+ end
42
+ end
17
43
  end
18
44
  end
data/lib/ably/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Ably
2
- VERSION = '0.8.15'
3
- PROTOCOL_VERSION = '0.8'
2
+ VERSION = '1.0.0'
3
+ PROTOCOL_VERSION = '1.0'
4
4
 
5
5
  # Allow a variant to be configured for all instances of this client library
6
6
  # such as ruby-rest-[VERSION]
@@ -5,6 +5,14 @@ require 'spec_helper'
5
5
  # wrapper around the Ably::Auth object
6
6
  #
7
7
  describe Ably::Realtime::Auth, :event_machine do
8
+ def disconnect_transport(connection)
9
+ if connection.transport
10
+ connection.transport.close_connection_after_writing
11
+ else
12
+ EventMachine.next_tick { disconnect_transport connection }
13
+ end
14
+ end
15
+
8
16
  vary_by_protocol do
9
17
  let(:default_options) { { key: api_key, environment: environment, protocol: protocol } }
10
18
  let(:client_options) { default_options }
@@ -73,7 +81,7 @@ describe Ably::Realtime::Auth, :event_machine do
73
81
  context '#current_token_details' do
74
82
  it 'contains the current token after auth' do
75
83
  expect(auth.current_token_details).to be_nil
76
- auth.authorise do
84
+ auth.authorize do
77
85
  expect(auth.current_token_details).to be_a(Ably::Models::TokenDetails)
78
86
  stop_reactor
79
87
  end
@@ -88,12 +96,13 @@ describe Ably::Realtime::Auth, :event_machine do
88
96
  end
89
97
 
90
98
  context '#options (auth_options)' do
99
+ let(:token_str) { auth.request_token_sync.token }
91
100
  let(:auth_url) { "https://echo.ably.io/?type=text" }
92
- let(:auth_params) { { :body => random_str } }
101
+ let(:auth_params) { { :body => token_str } }
93
102
  let(:client_options) { default_options.merge(auto_connect: false) }
94
103
 
95
104
  it 'contains the configured auth options' do
96
- auth.authorise({}, auth_url: auth_url, auth_params: auth_params) do
105
+ auth.authorize({}, auth_url: auth_url, auth_params: auth_params) do
97
106
  expect(auth.options[:auth_url]).to eql(auth_url)
98
107
  stop_reactor
99
108
  end
@@ -104,7 +113,7 @@ describe Ably::Realtime::Auth, :event_machine do
104
113
  let(:custom_ttl) { 33 }
105
114
 
106
115
  it 'contains the configured auth options' do
107
- auth.authorise(ttl: custom_ttl) do
116
+ auth.authorize(ttl: custom_ttl) do
108
117
  expect(auth.token_params[:ttl]).to eql(custom_ttl)
109
118
  stop_reactor
110
119
  end
@@ -113,7 +122,7 @@ describe Ably::Realtime::Auth, :event_machine do
113
122
 
114
123
  context '#using_basic_auth?' do
115
124
  it 'is false when using Token Auth' do
116
- auth.authorise do
125
+ auth.authorize do
117
126
  expect(auth).to_not be_using_basic_auth
118
127
  stop_reactor
119
128
  end
@@ -122,7 +131,7 @@ describe Ably::Realtime::Auth, :event_machine do
122
131
 
123
132
  context '#using_token_auth?' do
124
133
  it 'is true when using Token Auth' do
125
- auth.authorise do
134
+ auth.authorize do
126
135
  expect(auth).to be_using_token_auth
127
136
  stop_reactor
128
137
  end
@@ -130,7 +139,7 @@ describe Ably::Realtime::Auth, :event_machine do
130
139
  end
131
140
  end
132
141
 
133
- context do
142
+ context 'methods' do
134
143
  let(:custom_ttl) { 33 }
135
144
  let(:custom_client_id) { random_str }
136
145
 
@@ -176,13 +185,17 @@ describe Ably::Realtime::Auth, :event_machine do
176
185
  end
177
186
  end
178
187
 
179
- context '#authorise' do
180
- it 'returns a token asynchronously' do
181
- auth.authorise(ttl: custom_ttl, client_id: custom_client_id) do |token_details|
182
- expect(token_details).to be_a(Ably::Models::TokenDetails)
183
- expect(token_details.expires.to_i).to be_within(3).of(Time.now.to_i + custom_ttl)
184
- expect(token_details.client_id).to eql(custom_client_id)
185
- stop_reactor
188
+ context '#authorize' do
189
+ context 'with token auth' do
190
+ let(:client_options) { default_options.merge(use_token_auth: true) }
191
+
192
+ it 'returns a token asynchronously' do
193
+ auth.authorize(ttl: custom_ttl, client_id: custom_client_id) do |token_details|
194
+ expect(token_details).to be_a(Ably::Models::TokenDetails)
195
+ expect(token_details.expires.to_i).to be_within(3).of(Time.now.to_i + custom_ttl)
196
+ expect(token_details.client_id).to eql(custom_client_id)
197
+ stop_reactor
198
+ end
186
199
  end
187
200
  end
188
201
 
@@ -223,10 +236,11 @@ describe Ably::Realtime::Auth, :event_machine do
223
236
  context 'and an incompatible client_id in a TokenDetails object passed to the auth callback' do
224
237
  let(:auth_token_object) { rest_auth_client.auth.request_token }
225
238
 
226
- it 'rejects a TokenDetails object with an incompatible client_id and raises an exception' do
239
+ it 'rejects a TokenDetails object with an incompatible client_id and fails with an exception' do
227
240
  client.connect
228
- client.connection.on(:error) do |error|
229
- expect(error).to be_a(Ably::Exceptions::IncompatibleClientId)
241
+ client.connection.on(:failed) do |state_change|
242
+ expect(state_change.reason).to be_a(Ably::Exceptions::AuthenticationFailed)
243
+ expect(state_change.reason.code).to eql(40012)
230
244
  EventMachine.add_timer(0.1) do
231
245
  expect(client.connection).to be_failed
232
246
  stop_reactor
@@ -235,13 +249,14 @@ describe Ably::Realtime::Auth, :event_machine do
235
249
  end
236
250
  end
237
251
 
238
- context 'and an incompatible client_id in a TokenRequest object passed to the auth callback and raises an exception' do
252
+ context 'and an incompatible client_id in a TokenRequest object passed to the auth callback and fails with an exception' do
239
253
  let(:auth_token_object) { rest_auth_client.auth.create_token_request }
240
254
 
241
- it 'rejects a TokenRequests object with an incompatible client_id and raises an exception' do
255
+ it 'rejects a TokenRequests object with an incompatible client_id and fails with an exception' do
242
256
  client.connect
243
- client.connection.on(:error) do |error|
244
- expect(error).to be_a(Ably::Exceptions::IncompatibleClientId)
257
+ client.connection.on(:failed) do |state_change|
258
+ expect(state_change.reason).to be_a(Ably::Exceptions::AuthenticationFailed)
259
+ expect(state_change.reason.code).to eql(40012)
245
260
  EventMachine.add_timer(0.1) do
246
261
  expect(client.connection).to be_failed
247
262
  stop_reactor
@@ -269,11 +284,12 @@ describe Ably::Realtime::Auth, :event_machine do
269
284
  let(:invalid_auth_token) { Ably::Rest::Client.new(default_options.merge(key: api_key, client_id: 'invalid')).auth.request_token }
270
285
 
271
286
  context 'and an incompatible client_id in a TokenDetails object passed to the auth callback' do
272
- it 'rejects a TokenDetails object with an incompatible client_id and raises an exception' do
287
+ it 'rejects a TokenDetails object with an incompatible client_id and fails with an exception' do
273
288
  client.connection.once(:connected) do
274
- client.auth.authorise({}, force: true)
275
- client.connection.on(:error) do |error|
276
- expect(error).to be_a(Ably::Exceptions::IncompatibleClientId)
289
+ client.auth.authorize({})
290
+ client.connection.on(:failed) do |state_change|
291
+ expect(state_change.reason).to be_a(Ably::Exceptions::IncompatibleClientId)
292
+ expect(state_change.reason.code).to eql(40012)
277
293
  EventMachine.add_timer(0.1) do
278
294
  expect(client.connection).to be_failed
279
295
  stop_reactor
@@ -284,7 +300,7 @@ describe Ably::Realtime::Auth, :event_machine do
284
300
  end
285
301
  end
286
302
 
287
- context 'with force: true to trigger an authentication upgrade' do
303
+ context 'when already authenticated with a valid token' do
288
304
  let(:rest_client) { Ably::Rest::Client.new(default_options) }
289
305
  let(:client_publisher) { auto_close Ably::Realtime::Client.new(default_options) }
290
306
  let(:basic_capability) { JSON.dump("foo" => ["subscribe"]) }
@@ -304,29 +320,268 @@ describe Ably::Realtime::Auth, :event_machine do
304
320
  end }
305
321
 
306
322
  let(:client_options) { default_options.merge(auth_callback: basic_token_cb) }
323
+ let(:connection) { client.connection }
324
+
325
+ context 'when INITIALIZED' do
326
+ let(:client_options) { default_options.merge(auth_callback: basic_token_cb, auto_connect: false) }
327
+
328
+ it 'obtains a token and connects to Ably (#RTC8c, #RTC8b1)' do
329
+ has_connected = false
330
+ EventMachine.add_timer(0.2) do
331
+ expect(client.connection).to be_initialized
332
+ connection.once(:connected) do
333
+ expect(client.auth.client_id).to_not be_nil
334
+ has_connected = true
335
+ end
336
+ client.auth.authorize(nil, auth_callback: identified_token_cb) do |token|
337
+ expect(token.client_id).to eql('bob')
338
+ EventMachine.add_timer(0.25) do
339
+ expect(has_connected).to be_truthy
340
+ stop_reactor
341
+ end
342
+ end
343
+ end
344
+ end
345
+ end
307
346
 
308
- it 'forces the connection to disconnect and reconnect with a new token when in the CONNECTED state' do
309
- client.connection.once(:connected) do
310
- existing_token = client.auth.current_token_details
311
- client.auth.authorise(nil, force: true)
312
- client.connection.once(:disconnected) do
347
+ context 'when CONNECTING' do
348
+ let(:client_options) { default_options.merge(auth_callback: basic_token_cb) }
349
+
350
+ it 'aborts the current connection process, obtains a token, and connects to Ably again (#RTC8b)' do
351
+ connected_count = 0
352
+ connection.once(:connecting) do
353
+ connection.once(:connected) { connected_count += 1 }
354
+
355
+ client.auth.authorize(nil, auth_callback: identified_token_cb) do |token|
356
+ expect(token.client_id).to eql('bob')
357
+ EventMachine.add_timer(0.25) do
358
+ expect(connected_count).to eql(1)
359
+ stop_reactor
360
+ end
361
+ end
362
+ end
363
+ end
364
+ end
365
+
366
+ context 'when FAILED' do
367
+ let(:client_options) { default_options.merge(token: 'this.token:is.invalid', log_level: :none) }
368
+
369
+ it 'obtains a token and connects to Ably (#RTC8c, #RTC8b1)' do
370
+ has_connected = false
371
+ connection.once(:failed) do
313
372
  client.connection.once(:connected) do
314
- expect(existing_token).to_not eql(client.auth.current_token_details)
315
- stop_reactor
373
+ has_connected = true
374
+ end
375
+ client.auth.authorize(nil, auth_callback: basic_token_cb) do
376
+ EventMachine.add_timer(0.25) do
377
+ expect(has_connected).to be_truthy
378
+ stop_reactor
379
+ end
380
+ end
381
+ end
382
+ end
383
+ end
384
+
385
+ context 'when CLOSED' do
386
+ it 'obtains a token and connects to Ably (#RTC8c, #RTC8b1, #RTC8a3)' do
387
+ has_connected = false
388
+ connection.once(:connected) do
389
+ connection.once(:closed) do
390
+ client.connection.once(:connected) do
391
+ has_connected = true
392
+ end
393
+ client.auth.authorize(nil, auth_callback: basic_token_cb) do
394
+ EventMachine.add_timer(0.25) do
395
+ expect(has_connected).to be_truthy
396
+ stop_reactor
397
+ end
398
+ end
316
399
  end
400
+ connection.close
317
401
  end
318
402
  end
319
403
  end
320
404
 
321
- it 'forces the connection to disconnect and reconnect with a new token when in the CONNECTING state' do
322
- client.connection.once(:connecting) do
323
- existing_token = client.auth.current_token_details
324
- client.auth.authorise(nil, force: true)
325
- client.connection.once(:disconnected) do
405
+ context 'when in the CONNECTED state' do
406
+ context 'with a valid token in the AUTH ProtocolMessage sent' do
407
+ let(:client_options) { default_options.merge(use_token_auth: true) }
408
+
409
+ it 'obtains a new token (that upgrades from anonymous to identified) and upgrades the connection after receiving an updated CONNECTED ProtocolMessage (#RTC8a, #RTC8a3)' do
410
+ skip "This capability to upgrade from anonymous to identified is not yet implemented, see https://github.com/ably/wiki/issues/182"
411
+
326
412
  client.connection.once(:connected) do
327
- expect(existing_token).to_not eql(client.auth.current_token_details)
328
- stop_reactor
413
+ existing_token = client.auth.current_token_details
414
+ expect(client.connection.details.client_id).to be_nil
415
+ auth_sent = false
416
+ has_updated = false
417
+ new_connected_message_received = false
418
+
419
+ client.connection.once(:disconnected) { raise "Should not disconnnect during auth process"}
420
+ client.connection.once(:update) do
421
+ expect(auth_sent).to be_truthy
422
+ expect(new_connected_message_received).to be_truthy
423
+ expect(existing_token).to_not eql(client.auth.current_token_details)
424
+ expect(client.auth.client_id).to_not be_nil
425
+ expect(client.connection.details.client_id).to_not be_nil
426
+ has_updated = true
427
+ end
428
+
429
+ connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
430
+ auth_sent = true if protocol_message.action == :auth
431
+ end
432
+ connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
433
+ new_connected_message_received = true if protocol_message.action == :connected
434
+ end
435
+
436
+ client.auth.authorize(nil, auth_callback: identified_token_cb) do
437
+ EventMachine.add_timer(0.25) do
438
+ expect(has_updated).to be_truthy
439
+ stop_reactor
440
+ end
441
+ end
442
+ end
443
+ end
444
+
445
+ it 'obtains a new token (as anonymous user before & after) and upgrades the connection after receiving an updated CONNECTED ProtocolMessage (#RTC8a, #RTC8a3)' do
446
+ client.connection.once(:connected) do
447
+ existing_token = client.auth.current_token_details
448
+ auth_sent = false
449
+ has_updated = false
450
+ new_connected_message_received = false
451
+
452
+ client.connection.once(:disconnected) { raise "Should not disconnnect during auth process"}
453
+ client.connection.once(:update) do
454
+ EventMachine.next_tick do
455
+ expect(auth_sent).to be_truthy
456
+ expect(new_connected_message_received).to be_truthy
457
+ expect(existing_token).to_not eql(client.auth.current_token_details)
458
+ has_updated = true
459
+ end
460
+ end
461
+
462
+ connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
463
+ auth_sent = true if protocol_message.action == :auth
464
+ end
465
+ connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
466
+ new_connected_message_received = true if protocol_message.action == :connected
467
+ end
468
+
469
+ client.auth.authorize do
470
+ EventMachine.add_timer(0.25) do
471
+ expect(has_updated).to be_truthy
472
+ stop_reactor
473
+ end
474
+ end
475
+ end
476
+ end
477
+ end
478
+ end
479
+
480
+ context 'when DISCONNECTED' do
481
+ it 'obtains a token, upgrades from anonymous to identified, and connects to Ably immediately (#RTC8c, #RTC8b1)' do
482
+ skip "This capability to upgrade from anonymous to identified is not yet implemented, see https://github.com/ably/wiki/issues/182"
483
+
484
+ disconnected_waiting = false
485
+ has_connected = false
486
+
487
+ connection.once(:connected) do
488
+ expect(client.auth.client_id).to be_nil
489
+
490
+ connection.on(:disconnected) do |connection_state_change|
491
+ # Once we detect the connection will remain DISCONNECTED for 15s, then we can call authorize
492
+ # else we can't be sure authorize was responsible for the reconnect
493
+ if connection_state_change.retry_in > 1
494
+ disconnected_waiting = true
495
+
496
+ client.connection.once(:connected) do
497
+ expect(client.auth.client_id).to_not be_nil
498
+ has_connected = true
499
+ end
500
+
501
+ client.auth.authorize(nil, auth_callback: identified_token_cb) do
502
+ EventMachine.add_timer(0.25) do
503
+ expect(has_connected).to be_truthy
504
+ stop_reactor
505
+ end
506
+ end
507
+ end
508
+ end
509
+
510
+ connection.on(:connecting) do
511
+ disconnect_transport connection unless disconnected_waiting
512
+ end
513
+ disconnect_transport connection
514
+ end
515
+ end
516
+
517
+ it 'obtains a similar anonymous token and connects to Ably immediately (#RTC8c, #RTC8b1)' do
518
+ disconnected_waiting = false
519
+ has_connected = false
520
+
521
+ connection.once(:connected) do
522
+ expect(client.auth.client_id).to be_nil
523
+
524
+ connection.on(:disconnected) do |connection_state_change|
525
+ # Once we detect the connection will remain DISCONNECTED for 15s, then we can call authorize
526
+ # else we can't be sure authorize was responsible for the reconnect
527
+ if connection_state_change.retry_in > 1
528
+ disconnected_waiting = true
529
+
530
+ client.connection.once(:connected) do
531
+ expect(client.auth.client_id).to be_nil
532
+ has_connected = true
533
+ end
534
+
535
+ client.auth.authorize do
536
+ EventMachine.add_timer(0.25) do
537
+ expect(has_connected).to be_truthy
538
+ stop_reactor
539
+ end
540
+ end
541
+ end
542
+ end
543
+
544
+ connection.on(:connecting) do
545
+ disconnect_transport connection unless disconnected_waiting
546
+ end
547
+ disconnect_transport connection
548
+ end
549
+ end
550
+ end
551
+
552
+ context 'when SUSPENDED' do
553
+ let(:client_options) do
554
+ default_options.merge(
555
+ disconnected_retry_timeout: 0.1,
556
+ max_connection_state_ttl: 0.3,
557
+ use_token_auth: true
558
+ )
559
+ end
560
+
561
+ it 'obtains a token and connects to Ably immediately (#RTC8c, #RTC8b1)' do
562
+ been_suspended = false
563
+ has_connected = false
564
+
565
+ connection.once(:connected) do
566
+ connection.on(:suspended) do |connection_state_change|
567
+ been_suspended = true
568
+
569
+ client.connection.once(:connected) do
570
+ has_connected = true
571
+ end
572
+
573
+ client.auth.authorize do
574
+ EventMachine.add_timer(0.25) do
575
+ expect(has_connected).to be_truthy
576
+ stop_reactor
577
+ end
578
+ end
579
+ end
580
+
581
+ connection.on(:connecting) do
582
+ disconnect_transport connection unless been_suspended
329
583
  end
584
+ disconnect_transport connection
330
585
  end
331
586
  end
332
587
  end
@@ -338,47 +593,100 @@ describe Ably::Realtime::Auth, :event_machine do
338
593
  rest_client.auth.create_token_request({ client_id: 'mike', capability: basic_capability })
339
594
  end }
340
595
 
341
- it 'transisitions the connection state to FAILED if the client_id changes' do
596
+ it 'transitions the connection state to FAILED if the client_id changes (#RSA15c, #RTC8a2)' do
342
597
  client.connection.once(:connected) do
343
- client.auth.authorise(nil, auth_callback: identified_token_cb, force: true)
598
+ client.auth.authorize(nil, auth_callback: identified_token_cb)
344
599
  client.connection.once(:failed) do
345
- expect(client.connection.error_reason.message).to match(/incompatible.*client ID/)
600
+ expect(client.connection.error_reason.message).to match(/incompatible.*clientId/i)
601
+ expect(client.connection.error_reason.code).to eql(40012)
346
602
  stop_reactor
347
603
  end
348
604
  end
349
605
  end
350
606
  end
351
607
 
608
+ context 'when auth fails' do
609
+ let(:client_options) { default_options.merge(auth_callback: basic_token_cb, log_level: :none) }
610
+
611
+ it 'transitions the connection state to the FAILED state (#RSA15c, #RTC8a2, #RTC8a3)' do
612
+ connection_failed = false
613
+
614
+ client.connection.once(:connected) do
615
+ client.auth.authorize(nil, auth_callback: Proc.new { 'invalid.token:will.cause.failure' }).tap do |deferrable|
616
+ deferrable.errback do |error|
617
+ EventMachine.add_timer(0.2) do
618
+ expect(connection_failed).to eql(true)
619
+ expect(error.message).to match(/Invalid accessToken/i)
620
+ expect(error.code).to eql(40005)
621
+ stop_reactor
622
+ end
623
+ end
624
+ deferrable.callback { raise "Authorize should not succed" }
625
+ end
626
+ end
627
+
628
+ client.connection.once(:failed) do
629
+ expect(client.connection.error_reason.message).to match(/Invalid accessToken/i)
630
+ expect(client.connection.error_reason.code).to eql(40005)
631
+ connection_failed = true
632
+ end
633
+ end
634
+ end
635
+
636
+ context 'when the authCallback fails' do
637
+ let(:client_options) { default_options.merge(auth_callback: basic_token_cb, log_level: :none) }
638
+
639
+ it 'calls the error callback of authorize and leaves the connection intact (#RSA4c3)' do
640
+ client.connection.once(:connected) do
641
+ client.auth.authorize(nil, auth_callback: Proc.new { raise 'Exception raised' }).errback do |error|
642
+ EventMachine.add_timer(0.2) do
643
+ expect(connection).to be_connected
644
+ expect(error.message).to match(/Exception raised/i)
645
+ stop_reactor
646
+ end
647
+ end
648
+ client.connection.once(:failed) do
649
+ raise "Connection should not fail"
650
+ end
651
+ end
652
+ end
653
+ end
654
+
352
655
  context 'when upgrading capabilities' do
353
656
  let(:client_options) { default_options.merge(auth_callback: basic_token_cb, log_level: :error) }
354
657
 
355
- it 'is allowed' do
658
+ it 'is allowed (#RTC8a1)' do
356
659
  client.connection.once(:connected) do
660
+ client.connection.once(:disconnected) { raise 'Upgrade does not require a disconnect' }
661
+
357
662
  channel = client.channels.get('foo')
358
663
  channel.publish('not-allowed').errback do |error|
359
664
  expect(error.code).to eql(40160)
360
665
  expect(error.message).to match(/permission denied/)
361
- client.auth.authorise(nil, auth_callback: upgraded_token_cb, force: true)
362
- client.connection.once(:connected) do
666
+
667
+ client.auth.authorize(nil, auth_callback: upgraded_token_cb)
668
+ client.connection.once(:update) do
363
669
  expect(client.connection.error_reason).to be_nil
364
- channel.subscribe('allowed') do |message|
670
+ channel.subscribe('now-allowed') do |message|
365
671
  stop_reactor
366
672
  end
367
- channel.publish 'allowed'
673
+ channel.publish 'now-allowed'
368
674
  end
369
675
  end
370
676
  end
371
677
  end
372
678
  end
373
679
 
374
- context 'when downgrading capabilities' do
680
+ context 'when downgrading capabilities (#RTC8a1)' do
375
681
  let(:client_options) { default_options.merge(auth_callback: basic_token_cb, log_level: :none) }
376
682
 
377
683
  it 'is allowed and channels are detached' do
378
684
  client.connection.once(:connected) do
685
+ client.connection.once(:disconnected) { raise 'Upgrade does not require a disconnect' }
686
+
379
687
  channel = client.channels.get('foo')
380
688
  channel.attach do
381
- client.auth.authorise(nil, auth_callback: downgraded_token_cb, force: true)
689
+ client.auth.authorize(nil, auth_callback: downgraded_token_cb)
382
690
  channel.once(:failed) do
383
691
  expect(channel.error_reason.code).to eql(40160)
384
692
  expect(channel.error_reason.message).to match(/Channel denied access/)
@@ -389,11 +697,13 @@ describe Ably::Realtime::Auth, :event_machine do
389
697
  end
390
698
  end
391
699
 
392
- it 'ensures message delivery continuity whilst upgrading' do
700
+ it 'ensures message delivery continuity whilst upgrading (#RTC8a1)' do
393
701
  received_messages = []
394
702
  subscriber_channel = client.channels.get('foo')
395
703
  publisher_channel = client_publisher.channels.get('foo')
396
704
  subscriber_channel.attach do
705
+ client.connection.once(:disconnected) { raise 'Upgrade does not require a disconnect' }
706
+
397
707
  subscriber_channel.subscribe do |message|
398
708
  received_messages << message
399
709
  end
@@ -401,86 +711,93 @@ describe Ably::Realtime::Auth, :event_machine do
401
711
  publisher_channel.publish('foo') do
402
712
  EventMachine.add_timer(2) do
403
713
  expect(received_messages.length).to eql(1)
404
- client.auth.authorise(nil, force: true)
405
- client.connection.once(:disconnected) do
406
- publisher_channel.publish('bar') do
407
- expect(received_messages.length).to eql(1)
408
- end
409
- end
410
- client.connection.once(:connected) do
714
+
715
+ client.connection.once(:update) do
411
716
  EventMachine.add_timer(2) do
412
717
  expect(received_messages.length).to eql(2)
413
718
  stop_reactor
414
719
  end
415
720
  end
721
+
722
+ client.auth.authorize(nil)
723
+
724
+ publisher_channel.publish('bar')
416
725
  end
417
726
  end
418
727
  end
419
728
  end
420
729
  end
730
+ end
731
+ end
421
732
 
422
- it 'does not change the connection state if current connection state is closing' do
423
- client.connection.once(:connected) do
424
- client.connection.once(:closing) do
425
- client.auth.authorise(nil, force: true)
426
- client.connection.once(:connected) do
427
- raise "Should not reconnect following auth force: true"
428
- end
429
- EventMachine.add_timer(4) do
430
- expect(client.connection).to be_closed
431
- stop_reactor
432
- end
433
- end
434
- client.connection.close
435
- end
733
+ context '#authorize_async' do
734
+ it 'returns a token synchronously' do
735
+ auth.authorize_sync(ttl: custom_ttl, client_id: custom_client_id).tap do |token_details|
736
+ expect(auth.authorize_sync).to be_a(Ably::Models::TokenDetails)
737
+ expect(token_details.expires.to_i).to be_within(3).of(Time.now.to_i + custom_ttl)
738
+ expect(token_details.client_id).to eql(custom_client_id)
739
+ stop_reactor
436
740
  end
741
+ end
742
+ end
743
+ end
437
744
 
438
- it 'does not change the connection state if current connection state is closed' do
439
- client.connection.once(:connected) do
440
- client.connection.once(:closed) do
441
- client.auth.authorise(nil, force: true)
442
- client.connection.once(:connected) do
443
- raise "Should not reconnect following auth force: true"
444
- end
445
- EventMachine.add_timer(4) do
446
- expect(client.connection).to be_closed
447
- stop_reactor
448
- end
449
- end
450
- client.connection.close
451
- end
452
- end
745
+ context 'server initiated AUTH ProtocolMessage' do
746
+ before do
747
+ stub_const 'Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER', 0 # allow token to be used even if about to expire
748
+ stub_const 'Ably::Auth::TOKEN_DEFAULTS', Ably::Auth::TOKEN_DEFAULTS.merge(renew_token_buffer: 0) # Ensure tokens issued expire immediately after issue
749
+ end
453
750
 
454
- context 'when state is failed' do
455
- let(:client_options) { default_options.merge(auth_callback: basic_token_cb, log_level: :none) }
751
+ context 'when received' do
752
+ # Ably in all environments other than locla will send AUTH 30 seconds before expiry
753
+ # We set the TTL to 33s and wait (3s window)
754
+ # In local env, that window is 5 seconds instead of 30 seconds
755
+ let(:local_offset) { ENV['ABLY_ENV'] == 'local' ? 25 : 0 }
756
+ let(:client_options) { default_options.merge(use_token_auth: :true, default_token_params: { ttl: 33 - local_offset }) }
456
757
 
457
- it 'does not change the connection state' do
458
- client.connection.once(:connected) do
459
- client.connection.once(:failed) do
460
- client.auth.authorise(nil, force: true)
461
- client.connection.once(:connected) do
462
- raise "Should not reconnect following auth force: true"
463
- end
464
- EventMachine.add_timer(4) do
465
- expect(client.connection).to be_failed
466
- stop_reactor
467
- end
468
- end
469
- protocol_message = Ably::Models::ProtocolMessage.new(action: Ably::Models::ProtocolMessage::ACTION.Error.to_i)
470
- client.connection.__incoming_protocol_msgbus__.publish :protocol_message, protocol_message
471
- end
758
+ it 'should immediately start a new authentication process (#RTN22)' do
759
+ client.connection.once(:connected) do
760
+ original_token = auth.current_token_details
761
+ received_auth = false
762
+
763
+ client.connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
764
+ received_auth = true if protocol_message.action == :auth
765
+ end
766
+
767
+ client.connection.once(:update) do
768
+ expect(received_auth).to be_truthy
769
+ expect(original_token).to_not eql(auth.current_token_details)
770
+ stop_reactor
472
771
  end
473
772
  end
474
773
  end
475
774
  end
476
775
 
477
- context '#authorise_async' do
478
- it 'returns a token synchronously' do
479
- auth.authorise_sync(ttl: custom_ttl, client_id: custom_client_id).tap do |token_details|
480
- expect(auth.authorise_sync).to be_a(Ably::Models::TokenDetails)
481
- expect(token_details.expires.to_i).to be_within(3).of(Time.now.to_i + custom_ttl)
482
- expect(token_details.client_id).to eql(custom_client_id)
483
- stop_reactor
776
+ context 'when not received' do
777
+ # Ably in all environments other than production will send AUTH 5 seconds before expiry, so
778
+ # set TTL to 5s so that the window for Realtime to send has passed
779
+ let(:client_options) { default_options.merge(use_token_auth: :true, default_token_params: { ttl: 5 }) }
780
+
781
+ it 'should expect the connection to be disconnected by the server but should resume automatically (#RTN22a)' do
782
+ client.connection.once(:connected) do
783
+ original_token = auth.current_token_details
784
+ original_conn_id = client.connection.id
785
+ received_auth = false
786
+
787
+ client.connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
788
+ received_auth = true if protocol_message.action == :auth
789
+ end
790
+
791
+ client.connection.once(:disconnected) do |state_change|
792
+ expect(state_change.reason.code).to eql(40142)
793
+
794
+ client.connection.once(:connected) do
795
+ expect(received_auth).to be_falsey
796
+ expect(original_token).to_not eql(auth.current_token_details)
797
+ expect(original_conn_id).to eql(client.connection.id)
798
+ stop_reactor
799
+ end
800
+ end
484
801
  end
485
802
  end
486
803
  end
@@ -696,5 +1013,47 @@ describe Ably::Realtime::Auth, :event_machine do
696
1013
  end
697
1014
  end
698
1015
  end
1016
+
1017
+ context 'deprecated #authorise' do
1018
+ let(:client_options) { default_options.merge(key: api_key, logger: custom_logger_object, use_token_auth: true) }
1019
+ let(:custom_logger) do
1020
+ Class.new do
1021
+ def initialize
1022
+ @messages = []
1023
+ end
1024
+
1025
+ [:fatal, :error, :warn, :info, :debug].each do |severity|
1026
+ define_method severity do |message|
1027
+ @messages << [severity, message]
1028
+ end
1029
+ end
1030
+
1031
+ def logs
1032
+ @messages
1033
+ end
1034
+
1035
+ def level
1036
+ 1
1037
+ end
1038
+
1039
+ def level=(new_level)
1040
+ end
1041
+ end
1042
+ end
1043
+ let(:custom_logger_object) { custom_logger.new }
1044
+
1045
+ it 'logs a deprecation warning (#RSA10l)' do
1046
+ client.auth.authorise
1047
+ expect(custom_logger_object.logs.find { |severity, message| message.match(/authorise.*deprecated/i)} ).to_not be_nil
1048
+ stop_reactor
1049
+ end
1050
+
1051
+ it 'returns a valid token (#RSA10l)' do
1052
+ client.auth.authorise do |response|
1053
+ expect(response).to be_a(Ably::Models::TokenDetails)
1054
+ stop_reactor
1055
+ end
1056
+ end
1057
+ end
699
1058
  end
700
1059
  end