lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb in ably-rest-0.9.3 vs lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb in ably-rest-1.0.0
- old
+ new
@@ -3,10 +3,18 @@
# Very high level test coverage of the Realtime::Auth object which is just an async
# wrapper around the Ably::Auth object
#
describe Ably::Realtime::Auth, :event_machine do
+ def disconnect_transport(connection)
+ if connection.transport
+ connection.transport.close_connection_after_writing
+ else
+ EventMachine.next_tick { disconnect_transport connection }
+ end
+ end
+
vary_by_protocol do
let(:default_options) { { key: api_key, environment: environment, protocol: protocol } }
let(:client_options) { default_options }
let(:client) { auto_close Ably::Realtime::Client.new(client_options) }
let(:auth) { client.auth }
@@ -86,12 +94,13 @@
stop_reactor
end
end
context '#options (auth_options)' do
+ let(:token_str) { auth.request_token_sync.token }
let(:auth_url) { "https://echo.ably.io/?type=text" }
- let(:auth_params) { { :body => random_str } }
+ let(:auth_params) { { :body => token_str } }
let(:client_options) { default_options.merge(auto_connect: false) }
it 'contains the configured auth options' do
auth.authorize({}, auth_url: auth_url, auth_params: auth_params) do
expect(auth.options[:auth_url]).to eql(auth_url)
@@ -128,11 +137,11 @@
end
end
end
end
- context do
+ context 'methods' do
let(:custom_ttl) { 33 }
let(:custom_client_id) { random_str }
context '#create_token_request' do
it 'returns a token request asynchronously' do
@@ -175,16 +184,20 @@
end
end
end
context '#authorize' do
- it 'returns a token asynchronously' do
- auth.authorize(ttl: custom_ttl, client_id: custom_client_id) do |token_details|
- expect(token_details).to be_a(Ably::Models::TokenDetails)
- expect(token_details.expires.to_i).to be_within(3).of(Time.now.to_i + custom_ttl)
- expect(token_details.client_id).to eql(custom_client_id)
- stop_reactor
+ context 'with token auth' do
+ let(:client_options) { default_options.merge(use_token_auth: true) }
+
+ it 'returns a token asynchronously' do
+ auth.authorize(ttl: custom_ttl, client_id: custom_client_id) do |token_details|
+ expect(token_details).to be_a(Ably::Models::TokenDetails)
+ expect(token_details.expires.to_i).to be_within(3).of(Time.now.to_i + custom_ttl)
+ expect(token_details.client_id).to eql(custom_client_id)
+ stop_reactor
+ end
end
end
context 'with auth_callback blocking' do
let(:rest_auth_client) { Ably::Rest::Client.new(default_options.merge(key: api_key)) }
@@ -221,29 +234,31 @@
let(:rest_auth_client) { Ably::Rest::Client.new(default_options.merge(key: api_key, client_id: 'invalid')) }
context 'and an incompatible client_id in a TokenDetails object passed to the auth callback' do
let(:auth_token_object) { rest_auth_client.auth.request_token }
- it 'rejects a TokenDetails object with an incompatible client_id and raises an exception' do
+ it 'rejects a TokenDetails object with an incompatible client_id and fails with an exception' do
client.connect
- client.connection.on(:error) do |error|
- expect(error).to be_a(Ably::Exceptions::IncompatibleClientId)
+ client.connection.on(:failed) do |state_change|
+ expect(state_change.reason).to be_a(Ably::Exceptions::AuthenticationFailed)
+ expect(state_change.reason.code).to eql(40012)
EventMachine.add_timer(0.1) do
expect(client.connection).to be_failed
stop_reactor
end
end
end
end
- context 'and an incompatible client_id in a TokenRequest object passed to the auth callback and raises an exception' do
+ context 'and an incompatible client_id in a TokenRequest object passed to the auth callback and fails with an exception' do
let(:auth_token_object) { rest_auth_client.auth.create_token_request }
- it 'rejects a TokenRequests object with an incompatible client_id and raises an exception' do
+ it 'rejects a TokenRequests object with an incompatible client_id and fails with an exception' do
client.connect
- client.connection.on(:error) do |error|
- expect(error).to be_a(Ably::Exceptions::IncompatibleClientId)
+ client.connection.on(:failed) do |state_change|
+ expect(state_change.reason).to be_a(Ably::Exceptions::AuthenticationFailed)
+ expect(state_change.reason.code).to eql(40012)
EventMachine.add_timer(0.1) do
expect(client.connection).to be_failed
stop_reactor
end
end
@@ -267,15 +282,16 @@
let(:client_options) { default_options.merge(auth_callback: auth_proc, client_id: client_id, log_level: :none) }
let(:valid_auth_token) { Ably::Rest::Client.new(default_options.merge(key: api_key, client_id: client_id)).auth.request_token }
let(:invalid_auth_token) { Ably::Rest::Client.new(default_options.merge(key: api_key, client_id: 'invalid')).auth.request_token }
context 'and an incompatible client_id in a TokenDetails object passed to the auth callback' do
- it 'rejects a TokenDetails object with an incompatible client_id and raises an exception' do
+ it 'rejects a TokenDetails object with an incompatible client_id and fails with an exception' do
client.connection.once(:connected) do
client.auth.authorize({})
- client.connection.on(:error) do |error|
- expect(error).to be_a(Ably::Exceptions::IncompatibleClientId)
+ client.connection.on(:failed) do |state_change|
+ expect(state_change.reason).to be_a(Ably::Exceptions::IncompatibleClientId)
+ expect(state_change.reason.code).to eql(40012)
EventMachine.add_timer(0.1) do
expect(client.connection).to be_failed
stop_reactor
end
end
@@ -302,82 +318,374 @@
let(:downgraded_token_cb) { Proc.new do
rest_client.auth.create_token_request({ capability: downgraded_capability })
end }
let(:client_options) { default_options.merge(auth_callback: basic_token_cb) }
+ let(:connection) { client.connection }
- it 'forces the connection to disconnect and reconnect with a new token when in the CONNECTED state' do
- client.connection.once(:connected) do
- existing_token = client.auth.current_token_details
- client.auth.authorize(nil)
- client.connection.once(:disconnected) do
+ context 'when INITIALIZED' do
+ let(:client_options) { default_options.merge(auth_callback: basic_token_cb, auto_connect: false) }
+
+ it 'obtains a token and connects to Ably (#RTC8c, #RTC8b1)' do
+ has_connected = false
+ EventMachine.add_timer(0.2) do
+ expect(client.connection).to be_initialized
+ connection.once(:connected) do
+ expect(client.auth.client_id).to_not be_nil
+ has_connected = true
+ end
+ client.auth.authorize(nil, auth_callback: identified_token_cb) do |token|
+ expect(token.client_id).to eql('bob')
+ EventMachine.add_timer(0.25) do
+ expect(has_connected).to be_truthy
+ stop_reactor
+ end
+ end
+ end
+ end
+ end
+
+ context 'when CONNECTING' do
+ let(:client_options) { default_options.merge(auth_callback: basic_token_cb) }
+
+ it 'aborts the current connection process, obtains a token, and connects to Ably again (#RTC8b)' do
+ connected_count = 0
+ connection.once(:connecting) do
+ connection.once(:connected) { connected_count += 1 }
+
+ client.auth.authorize(nil, auth_callback: identified_token_cb) do |token|
+ expect(token.client_id).to eql('bob')
+ EventMachine.add_timer(0.25) do
+ expect(connected_count).to eql(1)
+ stop_reactor
+ end
+ end
+ end
+ end
+ end
+
+ context 'when FAILED' do
+ let(:client_options) { default_options.merge(token: 'this.token:is.invalid', log_level: :none) }
+
+ it 'obtains a token and connects to Ably (#RTC8c, #RTC8b1)' do
+ has_connected = false
+ connection.once(:failed) do
client.connection.once(:connected) do
- expect(existing_token).to_not eql(client.auth.current_token_details)
- stop_reactor
+ has_connected = true
end
+ client.auth.authorize(nil, auth_callback: basic_token_cb) do
+ EventMachine.add_timer(0.25) do
+ expect(has_connected).to be_truthy
+ stop_reactor
+ end
+ end
end
end
end
- it 'forces the connection to disconnect and reconnect with a new token when in the CONNECTING state' do
- client.connection.once(:connecting) do
- existing_token = client.auth.current_token_details
- client.auth.authorize(nil)
- client.connection.once(:disconnected) do
+ context 'when CLOSED' do
+ it 'obtains a token and connects to Ably (#RTC8c, #RTC8b1, #RTC8a3)' do
+ has_connected = false
+ connection.once(:connected) do
+ connection.once(:closed) do
+ client.connection.once(:connected) do
+ has_connected = true
+ end
+ client.auth.authorize(nil, auth_callback: basic_token_cb) do
+ EventMachine.add_timer(0.25) do
+ expect(has_connected).to be_truthy
+ stop_reactor
+ end
+ end
+ end
+ connection.close
+ end
+ end
+ end
+
+ context 'when in the CONNECTED state' do
+ context 'with a valid token in the AUTH ProtocolMessage sent' do
+ let(:client_options) { default_options.merge(use_token_auth: true) }
+
+ it 'obtains a new token (that upgrades from anonymous to identified) and upgrades the connection after receiving an updated CONNECTED ProtocolMessage (#RTC8a, #RTC8a3)' do
+ skip "This capability to upgrade from anonymous to identified is not yet implemented, see https://github.com/ably/wiki/issues/182"
+
client.connection.once(:connected) do
- expect(existing_token).to_not eql(client.auth.current_token_details)
- stop_reactor
+ existing_token = client.auth.current_token_details
+ expect(client.connection.details.client_id).to be_nil
+ auth_sent = false
+ has_updated = false
+ new_connected_message_received = false
+
+ client.connection.once(:disconnected) { raise "Should not disconnnect during auth process"}
+ client.connection.once(:update) do
+ expect(auth_sent).to be_truthy
+ expect(new_connected_message_received).to be_truthy
+ expect(existing_token).to_not eql(client.auth.current_token_details)
+ expect(client.auth.client_id).to_not be_nil
+ expect(client.connection.details.client_id).to_not be_nil
+ has_updated = true
+ end
+
+ connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
+ auth_sent = true if protocol_message.action == :auth
+ end
+ connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
+ new_connected_message_received = true if protocol_message.action == :connected
+ end
+
+ client.auth.authorize(nil, auth_callback: identified_token_cb) do
+ EventMachine.add_timer(0.25) do
+ expect(has_updated).to be_truthy
+ stop_reactor
+ end
+ end
end
end
+
+ it 'obtains a new token (as anonymous user before & after) and upgrades the connection after receiving an updated CONNECTED ProtocolMessage (#RTC8a, #RTC8a3)' do
+ client.connection.once(:connected) do
+ existing_token = client.auth.current_token_details
+ auth_sent = false
+ has_updated = false
+ new_connected_message_received = false
+
+ client.connection.once(:disconnected) { raise "Should not disconnnect during auth process"}
+ client.connection.once(:update) do
+ EventMachine.next_tick do
+ expect(auth_sent).to be_truthy
+ expect(new_connected_message_received).to be_truthy
+ expect(existing_token).to_not eql(client.auth.current_token_details)
+ has_updated = true
+ end
+ end
+
+ connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
+ auth_sent = true if protocol_message.action == :auth
+ end
+ connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
+ new_connected_message_received = true if protocol_message.action == :connected
+ end
+
+ client.auth.authorize do
+ EventMachine.add_timer(0.25) do
+ expect(has_updated).to be_truthy
+ stop_reactor
+ end
+ end
+ end
+ end
end
end
+ context 'when DISCONNECTED' do
+ it 'obtains a token, upgrades from anonymous to identified, and connects to Ably immediately (#RTC8c, #RTC8b1)' do
+ skip "This capability to upgrade from anonymous to identified is not yet implemented, see https://github.com/ably/wiki/issues/182"
+
+ disconnected_waiting = false
+ has_connected = false
+
+ connection.once(:connected) do
+ expect(client.auth.client_id).to be_nil
+
+ connection.on(:disconnected) do |connection_state_change|
+ # Once we detect the connection will remain DISCONNECTED for 15s, then we can call authorize
+ # else we can't be sure authorize was responsible for the reconnect
+ if connection_state_change.retry_in > 1
+ disconnected_waiting = true
+
+ client.connection.once(:connected) do
+ expect(client.auth.client_id).to_not be_nil
+ has_connected = true
+ end
+
+ client.auth.authorize(nil, auth_callback: identified_token_cb) do
+ EventMachine.add_timer(0.25) do
+ expect(has_connected).to be_truthy
+ stop_reactor
+ end
+ end
+ end
+ end
+
+ connection.on(:connecting) do
+ disconnect_transport connection unless disconnected_waiting
+ end
+ disconnect_transport connection
+ end
+ end
+
+ it 'obtains a similar anonymous token and connects to Ably immediately (#RTC8c, #RTC8b1)' do
+ disconnected_waiting = false
+ has_connected = false
+
+ connection.once(:connected) do
+ expect(client.auth.client_id).to be_nil
+
+ connection.on(:disconnected) do |connection_state_change|
+ # Once we detect the connection will remain DISCONNECTED for 15s, then we can call authorize
+ # else we can't be sure authorize was responsible for the reconnect
+ if connection_state_change.retry_in > 1
+ disconnected_waiting = true
+
+ client.connection.once(:connected) do
+ expect(client.auth.client_id).to be_nil
+ has_connected = true
+ end
+
+ client.auth.authorize do
+ EventMachine.add_timer(0.25) do
+ expect(has_connected).to be_truthy
+ stop_reactor
+ end
+ end
+ end
+ end
+
+ connection.on(:connecting) do
+ disconnect_transport connection unless disconnected_waiting
+ end
+ disconnect_transport connection
+ end
+ end
+ end
+
+ context 'when SUSPENDED' do
+ let(:client_options) do
+ default_options.merge(
+ disconnected_retry_timeout: 0.1,
+ max_connection_state_ttl: 0.3,
+ use_token_auth: true
+ )
+ end
+
+ it 'obtains a token and connects to Ably immediately (#RTC8c, #RTC8b1)' do
+ been_suspended = false
+ has_connected = false
+
+ connection.once(:connected) do
+ connection.on(:suspended) do |connection_state_change|
+ been_suspended = true
+
+ client.connection.once(:connected) do
+ has_connected = true
+ end
+
+ client.auth.authorize do
+ EventMachine.add_timer(0.25) do
+ expect(has_connected).to be_truthy
+ stop_reactor
+ end
+ end
+ end
+
+ connection.on(:connecting) do
+ disconnect_transport connection unless been_suspended
+ end
+ disconnect_transport connection
+ end
+ end
+ end
+
context 'when client is identified' do
let(:client_options) { default_options.merge(auth_callback: basic_token_cb, log_level: :none) }
let(:basic_token_cb) { Proc.new do
rest_client.auth.create_token_request({ client_id: 'mike', capability: basic_capability })
end }
- it 'transisitions the connection state to FAILED if the client_id changes' do
+ it 'transitions the connection state to FAILED if the client_id changes (#RSA15c, #RTC8a2)' do
client.connection.once(:connected) do
client.auth.authorize(nil, auth_callback: identified_token_cb)
client.connection.once(:failed) do
- expect(client.connection.error_reason.message).to match(/incompatible.*client ID/)
+ expect(client.connection.error_reason.message).to match(/incompatible.*clientId/i)
+ expect(client.connection.error_reason.code).to eql(40012)
stop_reactor
end
end
end
end
+ context 'when auth fails' do
+ let(:client_options) { default_options.merge(auth_callback: basic_token_cb, log_level: :none) }
+
+ it 'transitions the connection state to the FAILED state (#RSA15c, #RTC8a2, #RTC8a3)' do
+ connection_failed = false
+
+ client.connection.once(:connected) do
+ client.auth.authorize(nil, auth_callback: Proc.new { 'invalid.token:will.cause.failure' }).tap do |deferrable|
+ deferrable.errback do |error|
+ EventMachine.add_timer(0.2) do
+ expect(connection_failed).to eql(true)
+ expect(error.message).to match(/Invalid accessToken/i)
+ expect(error.code).to eql(40005)
+ stop_reactor
+ end
+ end
+ deferrable.callback { raise "Authorize should not succed" }
+ end
+ end
+
+ client.connection.once(:failed) do
+ expect(client.connection.error_reason.message).to match(/Invalid accessToken/i)
+ expect(client.connection.error_reason.code).to eql(40005)
+ connection_failed = true
+ end
+ end
+ end
+
+ context 'when the authCallback fails' do
+ let(:client_options) { default_options.merge(auth_callback: basic_token_cb, log_level: :none) }
+
+ it 'calls the error callback of authorize and leaves the connection intact (#RSA4c3)' do
+ client.connection.once(:connected) do
+ client.auth.authorize(nil, auth_callback: Proc.new { raise 'Exception raised' }).errback do |error|
+ EventMachine.add_timer(0.2) do
+ expect(connection).to be_connected
+ expect(error.message).to match(/Exception raised/i)
+ stop_reactor
+ end
+ end
+ client.connection.once(:failed) do
+ raise "Connection should not fail"
+ end
+ end
+ end
+ end
+
context 'when upgrading capabilities' do
let(:client_options) { default_options.merge(auth_callback: basic_token_cb, log_level: :error) }
- it 'is allowed' do
+ it 'is allowed (#RTC8a1)' do
client.connection.once(:connected) do
+ client.connection.once(:disconnected) { raise 'Upgrade does not require a disconnect' }
+
channel = client.channels.get('foo')
channel.publish('not-allowed').errback do |error|
expect(error.code).to eql(40160)
expect(error.message).to match(/permission denied/)
+
client.auth.authorize(nil, auth_callback: upgraded_token_cb)
- client.connection.once(:connected) do
+ client.connection.once(:update) do
expect(client.connection.error_reason).to be_nil
- channel.subscribe('allowed') do |message|
+ channel.subscribe('now-allowed') do |message|
stop_reactor
end
- channel.publish 'allowed'
+ channel.publish 'now-allowed'
end
end
end
end
end
- context 'when downgrading capabilities' do
+ context 'when downgrading capabilities (#RTC8a1)' do
let(:client_options) { default_options.merge(auth_callback: basic_token_cb, log_level: :none) }
it 'is allowed and channels are detached' do
client.connection.once(:connected) do
+ client.connection.once(:disconnected) { raise 'Upgrade does not require a disconnect' }
+
channel = client.channels.get('foo')
channel.attach do
client.auth.authorize(nil, auth_callback: downgraded_token_cb)
channel.once(:failed) do
expect(channel.error_reason.code).to eql(40160)
@@ -387,89 +695,37 @@
end
end
end
end
- it 'ensures message delivery continuity whilst upgrading' do
+ it 'ensures message delivery continuity whilst upgrading (#RTC8a1)' do
received_messages = []
subscriber_channel = client.channels.get('foo')
publisher_channel = client_publisher.channels.get('foo')
subscriber_channel.attach do
+ client.connection.once(:disconnected) { raise 'Upgrade does not require a disconnect' }
+
subscriber_channel.subscribe do |message|
received_messages << message
end
publisher_channel.attach do
publisher_channel.publish('foo') do
EventMachine.add_timer(2) do
expect(received_messages.length).to eql(1)
- client.auth.authorize(nil)
- client.connection.once(:disconnected) do
- publisher_channel.publish('bar') do
- expect(received_messages.length).to eql(1)
- end
- end
- client.connection.once(:connected) do
+
+ client.connection.once(:update) do
EventMachine.add_timer(2) do
expect(received_messages.length).to eql(2)
stop_reactor
end
end
- end
- end
- end
- end
- end
- it 'does not change the connection state if current connection state is closing' do
- client.connection.once(:connected) do
- client.connection.once(:closing) do
- client.auth.authorize(nil)
- client.connection.once(:connected) do
- raise "Should not reconnect following #authorize"
- end
- EventMachine.add_timer(4) do
- expect(client.connection).to be_closed
- stop_reactor
- end
- end
- client.connection.close
- end
- end
+ client.auth.authorize(nil)
- it 'does not change the connection state if current connection state is closed' do
- client.connection.once(:connected) do
- client.connection.once(:closed) do
- client.auth.authorize(nil)
- client.connection.once(:connected) do
- raise "Should not reconnect following #authorize"
- end
- EventMachine.add_timer(4) do
- expect(client.connection).to be_closed
- stop_reactor
- end
- end
- client.connection.close
- end
- end
-
- context 'when state is failed' do
- let(:client_options) { default_options.merge(auth_callback: basic_token_cb, log_level: :none) }
-
- it 'does not change the connection state' do
- client.connection.once(:connected) do
- client.connection.once(:failed) do
- client.auth.authorize(nil)
- client.connection.once(:connected) do
- raise "Should not reconnect following #authorize"
+ publisher_channel.publish('bar')
end
- EventMachine.add_timer(4) do
- expect(client.connection).to be_failed
- stop_reactor
- end
end
- protocol_message = Ably::Models::ProtocolMessage.new(action: Ably::Models::ProtocolMessage::ACTION.Error.to_i)
- client.connection.__incoming_protocol_msgbus__.publish :protocol_message, protocol_message
end
end
end
end
end
@@ -484,10 +740,71 @@
end
end
end
end
+ context 'server initiated AUTH ProtocolMessage' do
+ before do
+ stub_const 'Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER', 0 # allow token to be used even if about to expire
+ stub_const 'Ably::Auth::TOKEN_DEFAULTS', Ably::Auth::TOKEN_DEFAULTS.merge(renew_token_buffer: 0) # Ensure tokens issued expire immediately after issue
+ end
+
+ context 'when received' do
+ # Ably in all environments other than locla will send AUTH 30 seconds before expiry
+ # We set the TTL to 33s and wait (3s window)
+ # In local env, that window is 5 seconds instead of 30 seconds
+ let(:local_offset) { ENV['ABLY_ENV'] == 'local' ? 25 : 0 }
+ let(:client_options) { default_options.merge(use_token_auth: :true, default_token_params: { ttl: 33 - local_offset }) }
+
+ it 'should immediately start a new authentication process (#RTN22)' do
+ client.connection.once(:connected) do
+ original_token = auth.current_token_details
+ received_auth = false
+
+ client.connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
+ received_auth = true if protocol_message.action == :auth
+ end
+
+ client.connection.once(:update) do
+ expect(received_auth).to be_truthy
+ expect(original_token).to_not eql(auth.current_token_details)
+ stop_reactor
+ end
+ end
+ end
+ end
+
+ context 'when not received' do
+ # Ably in all environments other than production will send AUTH 5 seconds before expiry, so
+ # set TTL to 5s so that the window for Realtime to send has passed
+ let(:client_options) { default_options.merge(use_token_auth: :true, default_token_params: { ttl: 5 }) }
+
+ it 'should expect the connection to be disconnected by the server but should resume automatically (#RTN22a)' do
+ client.connection.once(:connected) do
+ original_token = auth.current_token_details
+ original_conn_id = client.connection.id
+ received_auth = false
+
+ client.connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
+ received_auth = true if protocol_message.action == :auth
+ end
+
+ client.connection.once(:disconnected) do |state_change|
+ expect(state_change.reason.code).to eql(40142)
+
+ client.connection.once(:connected) do
+ expect(received_auth).to be_falsey
+ expect(original_token).to_not eql(auth.current_token_details)
+ expect(original_conn_id).to eql(client.connection.id)
+ stop_reactor
+ end
+ end
+ end
+ end
+ end
+ end
+
context '#auth_params' do
it 'returns the auth params asynchronously' do
auth.auth_params do |auth_params|
expect(auth_params).to be_a(Hash)
stop_reactor
@@ -696,10 +1013,10 @@
end
end
end
context 'deprecated #authorise' do
- let(:client_options) { default_options.merge(key: api_key, logger: custom_logger_object) }
+ let(:client_options) { default_options.merge(key: api_key, logger: custom_logger_object, use_token_auth: true) }
let(:custom_logger) do
Class.new do
def initialize
@messages = []
end