lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb in ably-rest-0.9.3 vs lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb in ably-rest-1.0.0
- old
+ new
@@ -5,15 +5,20 @@
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(:connection) { client.connection }
let(:channel_name) { random_str }
let(:payload) { random_str }
let(:channel) { client.channel(channel_name) }
let(:messages) { [] }
+ def disconnect_transport
+ connection.transport.unbind
+ end
+
describe 'initialization' do
context 'with :auto_connect option set to false on connection' do
let(:client) do
auto_close Ably::Realtime::Client.new(default_options.merge(auto_connect: false))
end
@@ -34,85 +39,204 @@
end
end
end
describe '#attach' do
- it 'emits attaching then attached events' do
- channel.once(:attaching) do
- channel.once(:attached) do
- stop_reactor
+ context 'when initialized' do
+ it 'emits attaching then attached events' do
+ channel.once(:attaching) do
+ channel.once(:attached) do
+ stop_reactor
+ end
end
+
+ channel.attach
end
- channel.attach
- end
+ it 'ignores subsequent #attach calls but calls the success callback if provided' do
+ channel.once(:attaching) do
+ channel.attach
+ channel.once(:attached) do
+ channel.attach do
+ stop_reactor
+ end
+ end
+ end
- it 'ignores subsequent #attach calls but calls the success callback if provided' do
- channel.once(:attaching) do
channel.attach
- channel.once(:attached) do
+ end
+
+ it 'attaches to a channel' do
+ channel.attach
+ channel.on(:attached) do
+ expect(channel.state).to eq(:attached)
+ stop_reactor
+ end
+ end
+
+ it 'attaches to a channel and calls the provided block (#RTL4d)' do
+ channel.attach do
+ expect(channel.state).to eq(:attached)
+ stop_reactor
+ end
+ end
+
+ it 'sends an ATTACH and waits for an ATTACHED (#RTL4c)' do
+ connection.once(:connected) do
+ attach_count = 0
+ attached_count = 0
+ test_complete = false
+ client.connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
+ next if test_complete
+ attached_count += 1 if protocol_message.action == :attached
+ end
+ client.connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
+ next if test_complete
+ attach_count += 1 if protocol_message.action == :attach
+ end
channel.attach do
- stop_reactor
+ EventMachine.add_timer(1) do
+ test_complete = true
+ expect(attach_count).to eql(1)
+ expect(attached_count).to eql(1)
+ stop_reactor
+ end
end
end
end
- channel.attach
- end
-
- it 'attaches to a channel' do
- channel.attach
- channel.on(:attached) do
- expect(channel.state).to eq(:attached)
- stop_reactor
+ it 'implicitly attaches the channel (#RTL7c)' do
+ expect(channel).to be_initialized
+ channel.subscribe { |message| }
+ channel.once(:attached) do
+ stop_reactor
+ end
end
- end
- it 'attaches to a channel and calls the provided block' do
- channel.attach do
- expect(channel.state).to eq(:attached)
- stop_reactor
+ context 'when the implicit channel attach fails' do
+ let(:allowed_params) do
+ { capability: { "*" => ["*"] } }
+ end
+ let(:not_allowed_params) do
+ { capability: { "only_this_channel" => ["*"] } }
+ end
+ let(:client_options) { default_options.merge(default_token_params: not_allowed_params, use_token_auth: true, log_level: :fatal) }
+
+ it 'registers the listener anyway (#RTL7c)' do
+ channel.subscribe do |message|
+ stop_reactor
+ end
+ channel.once(:failed) do
+ client.auth.authorize(allowed_params) do
+ channel.attach do
+ channel.publish 'foo'
+ end
+ end
+ end
+ end
end
end
it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
expect(channel.attach).to be_a(Ably::Util::SafeDeferrable)
stop_reactor
end
- it 'calls the SafeDeferrable callback on success' do
+ it 'calls the SafeDeferrable callback on success (#RTL4d)' do
channel.attach.callback do
expect(channel).to be_a(Ably::Realtime::Channel)
expect(channel.state).to eq(:attached)
stop_reactor
end
end
+ context 'when an ATTACHED acknowledge is not received on the current connection' do
+ # As soon as the client sends the ATTACH on a CONNECTED connection
+ # simulate a transport failure that triggers the DISCONNECTED state twice
+ it 'sends another ATTACH each time the connection becomes connected' do
+ attached_messages = []
+ client.connection.__outgoing_protocol_msgbus__.on(:protocol_message) do |protocol_message|
+ if protocol_message.action == :attach
+ attached_messages << protocol_message
+ if attached_messages.count < 3
+ EventMachine.next_tick do
+ disconnect_transport
+ end
+ end
+ end
+ end
+
+ connection.once(:connected) do
+ connection.once(:disconnected) do
+ expect(attached_messages.count).to eql(1)
+ connection.once(:disconnected) do
+ expect(attached_messages.count).to eql(2)
+ connection.once(:connected) do
+ EventMachine.add_timer(0.1) do
+ expect(attached_messages.count).to eql(3)
+ end
+ end
+ end
+ end
+ channel.attach
+ end
+
+ channel.once(:attached) do
+ EventMachine.add_timer(1) do
+ expect(attached_messages.count).to eql(3)
+ stop_reactor
+ end
+ end
+ end
+ end
+
+ context 'when state is :attached' do
+ it 'does nothing (#RTL4a)' do
+ channel.attach do
+ stopping = false
+ client.connection.__outgoing_protocol_msgbus__.once(:protocol_message) do |protocol_message|
+ raise "No outgoing messages should be sent as already ATTACHED" unless stopping
+ end
+ 5.times do |index|
+ EventMachine.add_timer(0.2 * index) { channel.attach }
+ end
+ EventMachine.add_timer(1.5) do
+ stopping = true
+ stop_reactor
+ end
+ end
+ end
+ end
+
context 'when state is :failed' do
let(:client_options) { default_options.merge(log_level: :fatal) }
- it 'reattaches' do
+ it 'reattaches and sets the errorReason to nil (#RTL4g)' do
channel.attach do
channel.transition_state_machine :failed, reason: RuntimeError.new
expect(channel).to be_failed
+ expect(channel.error_reason).to_not be_nil
channel.attach do
expect(channel).to be_attached
+ expect(channel.error_reason).to be_nil
stop_reactor
end
end
end
end
context 'when state is :detaching' do
- it 'moves straight to attaching and skips detached' do
+ it 'does the attach operation after the completion of the pending request (#RTL4h)' do
channel.once(:detaching) do
- channel.once(:detached) { raise 'Detach should not have been reached' }
-
- channel.once(:attaching) do
- channel.once(:attached) do
- channel.off
- stop_reactor
+ channel.once(:detached) do
+ channel.once(:attaching) do
+ channel.once(:attached) do
+ EventMachine.add_timer(1) do
+ expect(channel).to be_attached
+ stop_reactor
+ end
+ end
end
end
channel.attach
end
@@ -145,41 +269,41 @@
end
end
end
context 'failure as a result of insufficient key permissions' do
+ let(:auth_options) do
+ default_options.merge(
+ key: restricted_api_key,
+ log_level: :fatal,
+ use_token_auth: true,
+ # TODO: Use wildcard / default when intersection issue resolved, realtime#780
+ default_token_params: { capability: { "canpublish:foo" => ["publish"] } }
+ )
+ end
let(:restricted_client) do
- auto_close Ably::Realtime::Client.new(default_options.merge(key: restricted_api_key, log_level: :fatal))
+ auto_close Ably::Realtime::Client.new(auth_options)
end
- let(:restricted_channel) { restricted_client.channel("cannot_subscribe") }
+ let(:restricted_channel) { restricted_client.channel("cansubscribe:foo") }
- it 'emits failed event' do
+ it 'emits failed event (#RTL4e)' do
restricted_channel.attach
restricted_channel.on(:failed) do |connection_state|
expect(restricted_channel.state).to eq(:failed)
expect(connection_state.reason.status).to eq(401)
stop_reactor
end
end
- it 'calls the errback of the returned Deferrable' do
+ it 'calls the errback of the returned Deferrable (#RTL4d)' do
restricted_channel.attach.errback do |error|
expect(restricted_channel.state).to eq(:failed)
expect(error.status).to eq(401)
stop_reactor
end
end
- it 'emits an error event' do
- restricted_channel.attach
- restricted_channel.on(:error) do |error|
- expect(restricted_channel.state).to eq(:failed)
- expect(error.status).to eq(401)
- stop_reactor
- end
- end
-
it 'updates the error_reason' do
restricted_channel.attach
restricted_channel.on(:failed) do
expect(restricted_channel.error_reason.status).to eq(401)
stop_reactor
@@ -189,14 +313,12 @@
context 'and subsequent authorisation with suitable permissions' do
it 'attaches to the channel successfully and resets the channel error_reason' do
restricted_channel.attach
restricted_channel.once(:failed) do
restricted_client.close do
- # A direct call to #authorize is synchronous
- restricted_client.auth.authorize({}, key: api_key)
-
- restricted_client.connect do
+ token_params = { capability: { "cansubscribe:foo" => ["subscribe"] } }
+ restricted_client.auth.authorize(token_params) do
restricted_channel.once(:attached) do
expect(restricted_channel.error_reason).to be_nil
stop_reactor
end
restricted_channel.attach
@@ -204,112 +326,183 @@
end
end
end
end
end
- end
- describe '#detach' do
- it 'detaches from a channel' do
- channel.attach do
- channel.detach
- channel.on(:detached) do
- expect(channel.state).to eq(:detached)
+ context 'with connection state' do
+ it 'is initialized (#RTL4i)' do
+ expect(connection).to be_initialized
+ channel.attach do
stop_reactor
end
end
- end
- it 'detaches from a channel and calls the provided block' do
- channel.attach do
- expect(channel.state).to eq(:attached)
- channel.detach do
- expect(channel.state).to eq(:detached)
- stop_reactor
+ it 'is connecting (#RTL4i)' do
+ connection.once(:connecting) do
+ channel.attach do
+ stop_reactor
+ end
end
end
+
+ it 'is disconnected (#RTL4i)' do
+ connection.once(:connected) do
+ connection.once(:disconnected) do
+ channel.attach do
+ stop_reactor
+ end
+ end
+ disconnect_transport
+ end
+ end
end
+ end
- it 'emits :detaching then :detached events' do
- channel.once(:detaching) do
- channel.once(:detached) do
- stop_reactor
+ describe '#detach' do
+ context 'when state is :attached' do
+ it 'it detaches from a channel (#RTL5d)' do
+ channel.attach do
+ channel.detach
+ channel.on(:detached) do
+ expect(channel.state).to eq(:detached)
+ stop_reactor
+ end
end
end
- channel.attach do
- channel.detach
+ it 'detaches from a channel and calls the provided block (#RTL5d, #RTL5e)' do
+ channel.attach do
+ expect(channel.state).to eq(:attached)
+ channel.detach do
+ expect(channel.state).to eq(:detached)
+ stop_reactor
+ end
+ end
end
- end
- it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
- channel.attach do
- expect(channel.detach).to be_a(Ably::Util::SafeDeferrable)
- stop_reactor
+ it 'emits :detaching then :detached events' do
+ channel.once(:detaching) do
+ channel.once(:detached) do
+ stop_reactor
+ end
+ end
+
+ channel.attach do
+ channel.detach
+ end
end
- end
- it 'calls the Deferrable callback on success' do
- channel.attach do
- channel.detach.callback do
- expect(channel).to be_a(Ably::Realtime::Channel)
- expect(channel.state).to eq(:detached)
+ it 'returns a SafeDeferrable that catches exceptions in callbacks and logs them' do
+ channel.attach do
+ expect(channel.detach).to be_a(Ably::Util::SafeDeferrable)
stop_reactor
end
end
+
+ it 'calls the Deferrable callback on success' do
+ channel.attach do
+ channel.detach.callback do
+ expect(channel).to be_a(Ably::Realtime::Channel)
+ expect(channel.state).to eq(:detached)
+ stop_reactor
+ end
+ end
+ end
+
+ context 'and DETACHED message is not received within realtime request timeout' do
+ let(:request_timeout) { 2 }
+ let(:client_options) { default_options.merge(realtime_request_timeout: request_timeout) }
+
+ it 'fails the deferrable and returns to the previous state (#RTL5f, #RTL5e)' do
+ channel.attach do
+ # don't process any incoming ProtocolMessages so the channel never becomes detached
+ connection.__incoming_protocol_msgbus__.unsubscribe
+ detached_requested_at = Time.now.to_i
+ channel.detach do
+ raise "The detach should not succeed if no incoming protocol messages are processed"
+ end.errback do
+ expect(channel).to be_attached
+ expect(Time.now.to_i - detached_requested_at).to be_within(1).of(request_timeout)
+ stop_reactor
+ end
+ end
+ end
+ end
end
context 'when state is :failed' do
let(:client_options) { default_options.merge(log_level: :fatal) }
- it 'raises an exception' do
+ it 'fails the deferrable (#RTL5b)' do
channel.attach do
channel.transition_state_machine :failed, reason: RuntimeError.new
expect(channel).to be_failed
- expect { channel.detach }.to raise_error Ably::Exceptions::InvalidStateChange
- stop_reactor
+ channel.detach.errback do |error|
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
+ stop_reactor
+ end
end
end
end
context 'when state is :attaching' do
- it 'moves straight to :detaching state and skips :attached' do
- channel.once(:attaching) do
- channel.once(:attached) { raise 'Attached should never be reached' }
-
- channel.once(:detaching) do
- channel.once(:detached) do
- stop_reactor
+ it 'waits for the attach to complete and then moves to detached' do
+ connection.once(:connected) do
+ channel.once(:attaching) do
+ reached_attached = false
+ channel.once(:attached) do
+ channel.once(:detached) do
+ stop_reactor
+ end
end
+ channel.detach
end
-
- channel.detach
+ channel.attach
end
-
- channel.attach
end
end
context 'when state is :detaching' do
- it 'ignores subsequent #detach calls but calls the callback if provided' do
+ it 'ignores subsequent #detach calls but calls the callback if provided (#RTL5i)' do
channel.once(:detaching) do
- channel.detach
channel.once(:detached) do
channel.detach do
stop_reactor
end
end
+ channel.detach
end
channel.attach do
channel.detach
end
end
end
+ context 'when state is :suspended' do
+ it 'moves the channel state immediately to DETACHED state (#RTL5j)' do
+ channel.attach do
+ channel.once(:suspended) do
+ channel.on do |channel_state_change|
+ expect(channel_state_change.current).to eq(:detached)
+ expect(channel.state).to eq(:detached)
+ EventMachine.add_timer(1) do
+ stop_reactor
+ end
+ end
+ EventMachine.next_tick do
+ channel.detach
+ end
+ end
+ channel.transition_state_machine :suspended
+ end
+ end
+ end
+
context 'when state is :initialized' do
- it 'does nothing as there is no channel to detach' do
+ it 'does nothing as there is no channel to detach (#RTL5a)' do
expect(channel).to be_initialized
channel.detach do
expect(channel).to be_initialized
stop_reactor
end
@@ -321,18 +514,214 @@
expect(channel).to be_initialized
stop_reactor
end
end
end
+
+ context 'when state is :detached' do
+ it 'does nothing as the channel is detached (#RTL5a)' do
+ channel.attach do
+ channel.detach do
+ expect(channel).to be_detached
+ channel.on do
+ raise "Channel state should not change when calling detached if already detached"
+ end
+ channel.detach do
+ EventMachine.add_timer(1) { stop_reactor }
+ end
+ end
+ end
+ end
+ end
+
+ context 'when connection state is' do
+ context 'closing' do
+ it 'fails the deferrable (#RTL5b)' do
+ connection.once(:connected) do
+ channel.attach do
+ connection.once(:closing) do
+ channel.detach.errback do |error|
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
+ stop_reactor
+ end
+ end
+ connection.close
+ end
+ end
+ end
+ end
+
+ context 'failed and channel is failed' do
+ let(:client_options) do
+ default_options.merge(log_level: :none)
+ end
+
+ it 'fails the deferrable (#RTL5b)' do
+ connection.once(:connected) do
+ channel.attach do
+ connection.once(:failed) do
+ expect(channel).to be_failed
+ channel.detach.errback do |error|
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
+ stop_reactor
+ end
+ end
+ error = Ably::Exceptions::ConnectionFailed.new('forced failure', 500, 50000)
+ client.connection.manager.error_received_from_server error
+ end
+ end
+ end
+ end
+
+ context 'failed and channel is detached' do
+ let(:client_options) do
+ default_options.merge(log_level: :none)
+ end
+
+ it 'fails the deferrable (#RTL5b)' do
+ connection.once(:connected) do
+ channel.attach do
+ channel.detach do
+ connection.once(:failed) do
+ expect(channel).to be_detached
+ channel.detach.errback do |error|
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
+ stop_reactor
+ end
+ end
+ error = Ably::Exceptions::ConnectionFailed.new('forced failure', 500, 50000)
+ client.connection.manager.error_received_from_server error
+ end
+ end
+ end
+ end
+ end
+
+ context 'initialized' do
+ it 'does the detach operation once the connection state is connected (#RTL5h)' do
+ expect(connection).to be_initialized
+ channel.attach
+ channel.detach
+ connection.once(:connected) do
+ channel.once(:attached) do
+ channel.once(:detached) do
+ stop_reactor
+ end
+ end
+ end
+ end
+ end
+
+ context 'connecting' do
+ it 'does the detach operation once the connection state is connected (#RTL5h)' do
+ connection.once(:connecting) do
+ channel.attach
+ channel.detach
+ connection.once(:connected) do
+ channel.once(:attached) do
+ channel.once(:detached) do
+ stop_reactor
+ end
+ end
+ end
+ end
+ end
+ end
+
+ context 'disconnected' do
+ let(:client_options) do
+ default_options.merge(log_level: :fatal)
+ end
+ it 'does the detach operation once the connection state is connected (#RTL5h)' do
+ connection.once(:connected) do
+ connection.once(:disconnected) do
+ channel.attach
+ channel.detach
+ connection.once(:connected) do
+ channel.once(:attached) do
+ channel.once(:detached) do
+ stop_reactor
+ end
+ end
+ end
+ end
+ disconnect_transport
+ end
+ end
+ end
+ end
end
- describe 'channel recovery in :attaching state' do
- context 'the transport is disconnected before the ATTACHED protocol message is received' do
- skip 'attach times out and fails if not ATTACHED protocol message received'
- skip 'channel is ATTACHED if ATTACHED protocol message is later received'
- skip 'sends an ATTACH protocol message in response to a channel message being received on the attaching channel'
+ describe 'automatic channel recovery' do
+ let(:realtime_request_timeout) { 2 }
+ let(:client_options) do
+ default_options.merge(realtime_request_timeout: 2, log_level: :fatal)
end
+
+ context 'when an ATTACH request times out' do
+ it 'moves to the SUSPENDED state (#RTL4f)' do
+ connection.once(:connected) do
+ attach_request_sent_at = Time.now
+ channel.attach
+ client.connection.__incoming_protocol_msgbus__.unsubscribe
+ channel.once(:suspended) do
+ expect(attach_request_sent_at.to_i).to be_within(realtime_request_timeout + 1).of(Time.now.to_i)
+ stop_reactor
+ end
+ end
+ end
+ end
+
+ context 'if a subsequent ATTACHED is received on an ATTACHED channel' do
+ it 'ignores the additional ATTACHED if resumed is true (#RTL12)' do
+ channel.attach do
+ channel.once do |obj|
+ fail "No state change expected: #{obj}"
+ end
+ attached_message = Ably::Models::ProtocolMessage.new(action: 11, channel: channel_name, flags: 4) # ATTACHED with resumed flag
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, attached_message
+ EventMachine.add_timer(1) do
+ channel.off
+ stop_reactor
+ end
+ end
+ end
+
+ it 'emits an UPDATE only when resumed is true (#RTL12)' do
+ channel.attach do
+ expect(channel.error_reason).to be_nil
+ channel.on(:update) do |state_change|
+ expect(state_change.current).to eq(:attached)
+ expect(state_change.previous).to eq(:attached)
+ expect(state_change.resumed).to be_falsey
+ expect(state_change.reason).to be_nil
+ expect(channel.error_reason).to be_nil
+ stop_reactor
+ end
+ attached_message = Ably::Models::ProtocolMessage.new(action: 11, channel: channel_name, flags: 0) # No resumed flag
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, attached_message
+ end
+ end
+
+ it 'emits an UPDATE when resumed is true and includes the reason error from the ProtocolMessage (#RTL12)' do
+ channel.attach do
+ expect(channel.error_reason).to be_nil
+ channel.on(:update) do |state_change|
+ expect(state_change.current).to eq(:attached)
+ expect(state_change.previous).to eq(:attached)
+ expect(state_change.resumed).to be_falsey
+ expect(state_change.reason.code).to eql(50505)
+ expect(channel.error_reason.code).to eql(50505)
+ stop_reactor
+ end
+ attached_message = Ably::Models::ProtocolMessage.new(action: 11, channel: channel_name, error: { code: 50505 }, flags: 0) # No resumed flag with error
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, attached_message
+ end
+ end
+ end
+
+ # skip 'sends an ATTACH protocol message in response to a channel message being received on the attaching channel'
end
context '#publish' do
let(:name) { random_str }
let(:data) { random_str }
@@ -379,36 +768,42 @@
context 'with :queue_messages client option set to false' do
let(:client_options) { default_options.merge(queue_messages: false) }
context 'and connection state initialized' do
- it 'raises an exception' do
- expect { channel.publish('event') }.to raise_error Ably::Exceptions::MessageQueueingDisabled
+ it 'fails the deferrable' do
expect(client.connection).to be_initialized
- stop_reactor
+ channel.publish('event').errback do |error|
+ expect(error).to be_a(Ably::Exceptions::MessageQueueingDisabled)
+ stop_reactor
+ end
end
end
context 'and connection state connecting' do
- it 'raises an exception' do
+ it 'fails the deferrable' do
client.connect
EventMachine.next_tick do
- expect { channel.publish('event') }.to raise_error Ably::Exceptions::MessageQueueingDisabled
expect(client.connection).to be_connecting
- stop_reactor
+ channel.publish('event').errback do |error|
+ expect(error).to be_a(Ably::Exceptions::MessageQueueingDisabled)
+ stop_reactor
+ end
end
end
end
context 'and connection state disconnected' do
- let(:client_options) { default_options.merge(queue_messages: false, :log_level => :error ) }
- it 'raises an exception' do
+ let(:client_options) { default_options.merge(queue_messages: false) }
+ it 'fails the deferrable' do
client.connection.once(:connected) do
client.connection.once(:disconnected) do
- expect { channel.publish('event') }.to raise_error Ably::Exceptions::MessageQueueingDisabled
expect(client.connection).to be_disconnected
- stop_reactor
+ channel.publish('event').errback do |error|
+ expect(error).to be_a(Ably::Exceptions::MessageQueueingDisabled)
+ stop_reactor
+ end
end
client.connection.transition_state_machine :disconnected
end
end
end
@@ -445,10 +840,19 @@
stop_reactor
end
end
end
end
+
+ context 'and additional invalid attributes' do
+ let(:client_id) { 1 }
+
+ it 'throws an exception' do
+ expect { channel.publish([name: 'event', client_id: 1]) }.to raise_error ArgumentError, /client_id must be a String/
+ stop_reactor
+ end
+ end
end
context 'with an array of Hash objects with :name and :data attributes' do
let(:messages) do
10.times.map do |index|
@@ -694,10 +1098,17 @@
expect { channel.publish([name: 'event', client_id: '*']) }.to raise_error Ably::Exceptions::IncompatibleClientId
stop_reactor
end
end
+ context 'with a non-String client_id in the message' do
+ it 'throws an exception' do
+ expect { channel.publish([name: 'event', client_id: 1]) }.to raise_error ArgumentError, /client_id must be a String/
+ stop_reactor
+ end
+ end
+
context 'with an empty client_id in the message' do
it 'succeeds and publishes without a client_id' do
channel.publish([name: 'event', client_id: nil]).tap do |deferrable|
deferrable.errback { raise 'Should have succeeded' }
end
@@ -911,11 +1322,13 @@
context 'with a callback that raises an exception' do
let(:exception) { StandardError.new("Intentional error") }
it 'logs the error and continues' do
emitted_exception = false
- expect(client.logger).to receive(:error).with(/#{exception.message}/)
+ expect(client.logger).to receive(:error) do |*args, &block|
+ expect(args.concat([block ? block.call : nil]).join(',')).to match(/#{exception.message}/)
+ end
channel.subscribe('click') do |message|
emitted_exception = true
raise exception
end
channel.publish('click', 'data') do
@@ -984,52 +1397,57 @@
def fake_error(error)
client.connection.manager.error_received_from_server error
end
- context 'an :attached channel' do
- it 'transitions state to :failed' do
- channel.attach do
- channel.on(:failed) do |connection_state_change|
- error = connection_state_change.reason
- expect(error).to be_a(Ably::Exceptions::ConnectionFailed)
- expect(error.code).to eql(80002)
- stop_reactor
+ context 'an :attaching channel' do
+ it 'transitions state to :failed (#RTL3a)' do
+ connection.once(:connected) do
+ channel.once(:attaching) do
+ channel.on(:failed) do |connection_state_change|
+ error = connection_state_change.reason
+ expect(error).to be_a(Ably::Exceptions::ConnectionFailed)
+ expect(error.code).to eql(50000)
+ stop_reactor
+ end
+ fake_error connection_error
end
- fake_error connection_error
+ channel.attach
end
end
+ end
- it 'emits an error event on the channel' do
+ context 'an :attached channel' do
+ it 'transitions state to :failed (#RTL3a)' do
channel.attach do
- channel.on(:error) do |error|
+ channel.on(:failed) do |connection_state_change|
+ error = connection_state_change.reason
expect(error).to be_a(Ably::Exceptions::ConnectionFailed)
- expect(error.code).to eql(80002)
+ expect(error.code).to eql(50000)
stop_reactor
end
fake_error connection_error
end
end
- it 'updates the channel error_reason' do
+ it 'updates the channel error_reason (#RTL3a)' do
channel.attach do
channel.on(:failed) do |connection_state_change|
error = connection_state_change.reason
expect(error).to be_a(Ably::Exceptions::ConnectionFailed)
- expect(error.code).to eql(80002)
+ expect(error.code).to eql(50000)
stop_reactor
end
fake_error connection_error
end
end
end
context 'a :detached channel' do
- it 'remains in the :detached state' do
+ it 'remains in the :detached state (#RTL3a)' do
channel.attach do
channel.on(:failed) { raise 'Failed state should not have been reached' }
- channel.on(:error) { raise 'Error should not have been emitted' }
channel.detach do
EventMachine.add_timer(1) do
expect(channel).to be_detached
stop_reactor
@@ -1042,15 +1460,14 @@
end
context 'a :failed channel' do
let(:original_error) { RuntimeError.new }
- it 'remains in the :failed state and ignores the failure error' do
+ it 'remains in the :failed state and ignores the failure error (#RTL3a)' do
channel.attach do
- channel.on(:error) do
+ channel.on(:failed) do
channel.on(:failed) { raise 'Failed state should not have been reached' }
- channel.on(:error) { raise 'Error should not have been emitted' }
EventMachine.add_timer(1) do
expect(channel).to be_failed
expect(channel.error_reason).to eql(original_error)
stop_reactor
@@ -1063,40 +1480,56 @@
end
end
end
context 'a channel ATTACH request' do
- it 'raises an exception' do
+ it 'fails the deferrable (#RTL4b)' do
client.connect do
client.connection.once(:failed) do
- expect { channel.attach }.to raise_error Ably::Exceptions::InvalidStateChange
- stop_reactor
+ channel.attach.errback do |error|
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
+ stop_reactor
+ end
end
fake_error connection_error
end
end
end
end
context ':closed' do
context 'an :attached channel' do
- it 'transitions state to :detached' do
+ it 'transitions state to :detached (#RTL3b)' do
channel.attach do
channel.on(:detached) do
stop_reactor
end
client.connection.close
end
end
end
+ context 'an :attaching channel (#RTL3b)' do
+ it 'transitions state to :detached' do
+ channel.on(:attaching) do
+ channel.on(:detached) do
+ stop_reactor
+ end
+ client.connection.__incoming_protocol_msgbus__.unsubscribe
+ client.connection.close
+ closed_message = Ably::Models::ProtocolMessage.new(action: 8) # CLOSED
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, closed_message
+ end
+ channel.attach
+ end
+ end
+
context 'a :detached channel' do
- it 'remains in the :detached state' do
+ it 'remains in the :detached state (#RTL3b)' do
channel.attach do
channel.detach do
channel.on(:detached) { raise 'Detached state should not have been reached' }
- channel.on(:error) { raise 'Error should not have been emitted' }
EventMachine.add_timer(1) do
expect(channel).to be_detached
stop_reactor
end
@@ -1109,15 +1542,14 @@
context 'a :failed channel' do
let(:client_options) { default_options.merge(log_level: :fatal) }
let(:original_error) { Ably::Models::ErrorInfo.new(message: 'Error') }
- it 'remains in the :failed state and retains the error_reason' do
+ it 'remains in the :failed state and retains the error_reason (#RTL3b)' do
channel.attach do
- channel.once(:error) do
+ channel.once(:failed) do
channel.on(:detached) { raise 'Detached state should not have been reached' }
- channel.on(:error) { raise 'Error should not have been emitted' }
EventMachine.add_timer(1) do
expect(channel).to be_failed
expect(channel.error_reason).to eql(original_error)
stop_reactor
@@ -1130,54 +1562,81 @@
end
end
end
context 'a channel ATTACH request when connection CLOSED' do
- it 'raises an exception' do
+ it 'fails the deferrable (#RTL4b)' do
client.connect do
client.connection.once(:closed) do
- expect { channel.attach }.to raise_error Ably::Exceptions::InvalidStateChange
- stop_reactor
+ channel.attach.errback do |error|
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
+ stop_reactor
+ end
end
client.close
end
end
end
context 'a channel ATTACH request when connection CLOSING' do
- it 'raises an exception' do
+ it 'fails the deferrable (#RTL4b)' do
client.connect do
client.connection.once(:closing) do
- expect { channel.attach }.to raise_error Ably::Exceptions::InvalidStateChange
- stop_reactor
+ channel.attach.errback do |error|
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
+ stop_reactor
+ end
end
client.close
end
end
end
end
context ':suspended' do
- context 'an :attached channel' do
- let(:client_options) { default_options.merge(log_level: :fatal) }
+ context 'an :attaching channel' do
+ it 'transitions state to :suspended (#RTL3c)' do
+ channel.on(:attaching) do
+ channel.on(:suspended) do
+ stop_reactor
+ end
+ client.connection.once_or_if(:connecting) do
+ client.connection.transition_state_machine :suspended
+ end
+ end
+ channel.attach
+ end
+ end
- it 'transitions state to :detached' do
+ context 'an :attached channel' do
+ it 'transitions state to :suspended (#RTL3c)' do
channel.attach do
- channel.on(:detached) do
+ channel.on(:suspended) do
stop_reactor
end
client.connection.transition_state_machine :suspended
end
end
+
+ it 'transitions state automatically to :attaching once the connection is re-established (#RTN15c3)' do
+ channel.attach do
+ channel.on(:suspended) do
+ client.connection.connect
+ channel.once(:attached) do
+ stop_reactor
+ end
+ end
+ client.connection.transition_state_machine :suspended
+ end
+ end
end
context 'a :detached channel' do
- it 'remains in the :detached state' do
+ it 'remains in the :detached state (#RTL3c)' do
channel.attach do
channel.detach do
channel.on(:detached) { raise 'Detached state should not have been reached' }
- channel.on(:error) { raise 'Error should not have been emitted' }
EventMachine.add_timer(1) do
expect(channel).to be_detached
stop_reactor
end
@@ -1190,15 +1649,14 @@
context 'a :failed channel' do
let(:original_error) { RuntimeError.new }
let(:client_options) { default_options.merge(log_level: :fatal) }
- it 'remains in the :failed state and retains the error_reason' do
+ it 'remains in the :failed state and retains the error_reason (#RTL3c)' do
channel.attach do
- channel.once(:error) do
+ channel.once(:failed) do
channel.on(:detached) { raise 'Detached state should not have been reached' }
- channel.on(:error) { raise 'Error should not have been emitted' }
EventMachine.add_timer(1) do
expect(channel).to be_failed
expect(channel.error_reason).to eql(original_error)
stop_reactor
@@ -1210,24 +1668,146 @@
channel.transition_state_machine :failed, reason: original_error
end
end
end
- context 'a channel ATTACH request when connection SUSPENDED' do
+ context 'a channel ATTACH request when connection SUSPENDED (#RTL4b)' do
let(:client_options) { default_options.merge(log_level: :fatal) }
- it 'raises an exception' do
+ it 'fails the deferrable' do
client.connect do
client.connection.once(:suspended) do
- expect { channel.attach }.to raise_error Ably::Exceptions::InvalidStateChange
- stop_reactor
+ channel.attach.errback do |error|
+ expect(error).to be_a(Ably::Exceptions::InvalidStateChange)
+ stop_reactor
+ end
end
client.connection.transition_state_machine :suspended
end
end
end
end
+
+ context ':connected' do
+ context 'a :suspended channel' do
+ it 'is automatically reattached (#RTL3d)' do
+ channel.attach do
+ channel.once(:suspended) do
+ client.connection.connect
+ channel.once(:attached) do
+ stop_reactor
+ end
+ end
+ client.connection.transition_state_machine :suspended
+ end
+ end
+
+ context 'when re-attach attempt fails' do
+ let(:client_options) do
+ default_options.merge(realtime_request_timeout: 2, log_level: :fatal)
+ end
+
+ it 'returns to a suspended state (#RTL3d)' do
+ channel.attach do
+ channel.once(:attached) do
+ fail "Channel should not have become attached"
+ end
+
+ channel.once(:suspended) do
+ client.connection.connect
+ channel.once(:attaching) do
+ # don't process any incoming ProtocolMessages so the connection never opens
+ client.connection.__incoming_protocol_msgbus__.unsubscribe
+ channel.once(:suspended) do |state_change|
+ expect(state_change.reason.code).to eql(90007)
+ stop_reactor
+ end
+ end
+ end
+ client.connection.transition_state_machine :suspended
+ end
+ end
+ end
+ end
+ end
+
+ context ':disconnected' do
+ context 'with an initialized channel' do
+ it 'has no effect on the channel states (#RTL3e)' do
+ connection.once(:connected) do
+ expect(channel).to be_initialized
+ connection.once(:disconnected) do
+ expect(channel).to be_initialized
+ stop_reactor
+ end
+ disconnect_transport
+ end
+ end
+ end
+
+ context 'with an attaching channel' do
+ it 'has no effect on the channel states (#RTL3e)' do
+ connection.once(:connected) do
+ channel.once(:attaching) do
+ connection.once(:disconnected) do
+ expect(channel).to be_attaching
+ stop_reactor
+ end
+ disconnect_transport
+ end
+ channel.attach
+ end
+ end
+ end
+
+ context 'with an attached channel' do
+ it 'has no effect on the channel states (#RTL3e)' do
+ channel.attach do
+ connection.once(:disconnected) do
+ expect(channel).to be_attached
+ stop_reactor
+ end
+ disconnect_transport
+ end
+ end
+ end
+
+ context 'with a detached channel' do
+ it 'has no effect on the channel states (#RTL3e)' do
+ channel.attach do
+ channel.detach do
+ connection.once(:disconnected) do
+ expect(channel).to be_detached
+ stop_reactor
+ end
+ disconnect_transport
+ end
+ end
+ end
+ end
+
+ context 'with a failed channel' do
+ let(:client_options) do
+ default_options.merge(
+ default_token_params: { capability: { "foo" =>["*"] } },
+ use_token_auth: true,
+ log_level: :fatal
+ )
+ end
+
+ it 'has no effect on the channel states (#RTL3e)' do
+ channel.once(:failed) do
+ connection.once(:disconnected) do
+ expect(channel).to be_failed
+ stop_reactor
+ end
+ disconnect_transport
+ end
+ channel.attach
+ end
+ end
+ end
end
describe '#presence' do
it 'returns a Ably::Realtime::Presence object' do
expect(channel.presence).to be_a(Ably::Realtime::Presence)
@@ -1245,24 +1825,35 @@
end
context 'ChannelStateChange object' do
it 'has current state' do
channel.on(:attached) do |channel_state_change|
+ expect(channel_state_change.current).to be_a(Ably::Realtime::Channel::STATE)
expect(channel_state_change.current).to eq(:attached)
stop_reactor
end
channel.attach
end
it 'has a previous state' do
channel.on(:attached) do |channel_state_change|
+ expect(channel_state_change.previous).to be_a(Ably::Realtime::Channel::STATE)
expect(channel_state_change.previous).to eq(:attaching)
stop_reactor
end
channel.attach
end
+ it 'has the event that generated the state change (#TA5)' do
+ channel.on(:attached) do |channel_state_change|
+ expect(channel_state_change.event).to be_a(Ably::Realtime::Channel::EVENT)
+ expect(channel_state_change.event).to eq(:attached)
+ stop_reactor
+ end
+ channel.attach
+ end
+
it 'contains a private API protocol_message attribute that is used for special state change events', :api_private do
channel.on(:attached) do |channel_state_change|
expect(channel_state_change.protocol_message).to be_a(Ably::Models::ProtocolMessage)
expect(channel_state_change.reason).to be_nil
stop_reactor
@@ -1291,9 +1882,269 @@
channel.attach do
error = Ably::Exceptions::ConnectionFailed.new('forced failure', 500, 50000)
client.connection.manager.error_received_from_server error
end
end
+ end
+
+ context '#resume (#RTL2f)' do
+ it 'is false when a channel first attaches' do
+ channel.attach
+ channel.on(:attached) do |channel_state_change|
+ expect(channel_state_change.resumed).to be_falsey
+ stop_reactor
+ end
+ end
+
+ it 'is true when a connection is recovered and the channel is attached' do
+ channel.attach
+ channel.once(:attached) do |channel_state_change|
+ connection_id = client.connection.id
+ expect(channel_state_change.resumed).to be_falsey
+
+ recover_client = auto_close Ably::Realtime::Client.new(client_options.merge(recover: client.connection.recovery_key))
+ recover_client.connection.once(:connected) do
+ expect(recover_client.connection.id).to eql(connection_id)
+ recover_channel = recover_client.channels.get(channel_name)
+ recover_channel.attach
+ recover_channel.once(:attached) do |recover_channel_state_change|
+ expect(recover_channel_state_change.resumed).to be_truthy
+ stop_reactor
+ end
+ end
+ end
+ end
+
+ it 'is false when a connection fails to recover and the channel is attached' do
+ client.connection.once(:connected) do
+ recovery_key = client.connection.recovery_key
+ client.connection.once(:closed) do
+ recover_client = auto_close Ably::Realtime::Client.new(client_options.merge(recover: recovery_key, log_level: :error))
+ recover_client.connection.once(:connected) do
+ recover_channel = recover_client.channels.get(channel_name)
+ recover_channel.attach
+ recover_channel.once(:attached) do |recover_channel_state_change|
+ expect(recover_channel_state_change.resumed).to be_falsey
+ stop_reactor
+ end
+ end
+ end
+
+ client.close
+ end
+ end
+
+ context 'when a resume fails' do
+ let(:client_options) { default_options.merge(log_level: :error) }
+
+ it 'is false when a resume fails to recover and the channel is automatically re-attached' do
+ channel.attach do
+ connection_id = client.connection.id
+ channel.once(:attached) do |channel_state_change|
+ expect(client.connection.id).to_not eql(connection_id)
+ expect(channel_state_change.resumed).to be_falsey
+ stop_reactor
+ end
+ client.connection.transport.close_connection_after_writing
+ client.connection.configure_new '0123456789abcdef', 'wVIsgTHAB1UvXh7z-1991d8586', -1 # force the resume connection key to be invalid
+ end
+ end
+ end
+ end
+ end
+
+ context 'moves to' do
+ %w(suspended detached failed).each do |channel_state|
+ context(channel_state) do
+ specify 'all queued messages fail with NACK (#RTL11)' do
+ channel.attach do
+ # Move to disconnected
+ disconnect_transport_proc = Proc.new do
+ if connection.transport
+ connection.transport.close_connection_after_writing
+ else
+ EventMachine.next_tick { disconnect_transport_proc.call }
+ end
+ end
+ disconnect_transport_proc.call
+
+ connection.on(:connecting) { disconnect_transport_proc.call }
+
+ connection.once(:disconnected) do
+ channel.publish("foo").errback do |error|
+ stop_reactor
+ end
+ channel.transition_state_machine channel_state.to_sym
+ end
+ end
+ end
+
+ specify 'all published messages awaiting an ACK do nothing (#RTL11a)' do
+ connection_been_disconnected = false
+
+ channel.attach do
+ deferrable = channel.publish("foo")
+ deferrable.errback do |error|
+ fail "Message publish should not fail"
+ end
+ deferrable.callback do |error|
+ EventMachine.add_timer(0.5) do
+ expect(connection_been_disconnected).to be_truthy
+ stop_reactor
+ end
+ end
+
+ # Allow 5ms for message to be sent into the socket TCP/IP stack
+ EventMachine.add_timer(0.005) do
+ connection.transport.close_connection_after_writing
+ connection.once(:disconnected) do
+ connection_been_disconnected = true
+ channel.transition_state_machine channel_state.to_sym
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ context 'when it receives a server-initiated DETACHED (#RTL13)' do
+ let(:detached_action) { 13 }
+
+ context 'and channel is initialized (#RTL13)' do
+ it 'does nothing' do
+ connection.once(:connected) do
+ channel.on { raise 'Channel state should not change' }
+
+ detach_message = Ably::Models::ProtocolMessage.new(action: detached_action, channel: channel_name)
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, detach_message
+
+ EventMachine.add_timer(1) { stop_reactor }
+ end
+ end
+ end
+
+ context 'and channel is failed' do
+ let(:client_options) {
+ default_options.merge(
+ use_token_auth: true,
+ default_token_params: { capability: { "foo" => ["publish"] } },
+ log_level: :fatal
+ )
+ }
+
+ it 'does nothing (#RTL13)' do
+ connection.once(:connected) do
+ channel.attach
+ channel.once(:failed) do
+ channel.on { raise 'Channel state should not change' }
+
+ detach_message = Ably::Models::ProtocolMessage.new(action: detached_action, channel: channel_name)
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, detach_message
+
+ EventMachine.add_timer(1) { stop_reactor }
+ end
+ end
+ end
+ end
+
+ context 'and channel is attached' do
+ it 'reattaches immediately (#RTL13a)' do
+ channel.attach do
+ channel.once(:attaching) do |state_change|
+ expect(state_change.reason.code).to eql(50505)
+ channel.once(:attached) do
+ stop_reactor
+ end
+ end
+
+ detach_message = Ably::Models::ProtocolMessage.new(action: detached_action, channel: channel_name, error: { code: 50505 })
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, detach_message
+ end
+ end
+ end
+
+ context 'and channel is suspended' do
+ it 'reattaches immediately (#RTL13a)' do
+ channel.attach do
+ channel.once(:suspended) do
+ channel.once(:attaching) do |state_change|
+ expect(state_change.reason.code).to eql(50505)
+ channel.once(:attached) do
+ stop_reactor
+ end
+ end
+
+ detach_message = Ably::Models::ProtocolMessage.new(action: detached_action, channel: channel_name, error: { code: 50505 })
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, detach_message
+ end
+
+ channel.transition_state_machine! :suspended
+ end
+ end
+ end
+
+ context 'and channel is attaching' do
+ let(:client_options) { default_options.merge(channel_retry_timeout: 2, realtime_request_timeout: 1, log_level: :fatal) }
+
+ it 'will move to the SUSPENDED state and then attempt to ATTACH with the ATTACHING state (#RTL13b)' do
+ connection.once(:connected) do
+ # Prevent any incoming or outgoing ATTACH/ATTACHED message from Ably
+ prevent_protocol_messages_proc = Proc.new do
+ if client.connection.transport
+ client.connection.transport.__incoming_protocol_msgbus__.unsubscribe
+ client.connection.transport.__outgoing_protocol_msgbus__.unsubscribe
+ else
+ EventMachine.next_tick { prevent_protocol_messages_proc.call }
+ end
+ end
+ prevent_protocol_messages_proc.call
+ end
+
+ channel.once(:attaching) do
+ attaching_at = Time.now
+ # First attaching fails during server-initiated ATTACHED received
+ channel.once(:suspended) do |state_change|
+ expect(Time.now.to_i - attaching_at.to_i).to be_within(1).of(1)
+
+ suspended_at = Time.now
+ # Automatic attach happens at channel_retry_timeout
+ channel.once(:attaching) do
+ expect(Time.now.to_i - attaching_at.to_i).to be_within(1).of(2)
+ channel.once(:suspended) do
+ channel.once(:attaching) do
+ channel.once(:attached) do
+ stop_reactor
+ end
+ # Simulate ATTACHED from Ably
+ attached_message = Ably::Models::ProtocolMessage.new(action: 11, channel: channel_name) # ATTACHED
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, attached_message
+ end
+ end
+ end
+ end
+
+ detach_message = Ably::Models::ProtocolMessage.new(action: detached_action, channel: channel_name)
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, detach_message
+ end
+ channel.attach
+ end
+ end
+ end
+
+ context 'when it receives an ERROR ProtocolMessage' do
+ let(:client_options) { default_options.merge(log_level: :fatal) }
+
+ it 'should transition to the failed state and the error_reason should be set (#RTL14)' do
+ channel.attach do
+ channel.once(:failed) do |state_change|
+ expect(state_change.reason.code).to eql(50505)
+ expect(channel.error_reason.code).to eql(50505)
+ stop_reactor
+ end
+ error_message = Ably::Models::ProtocolMessage.new(action: 9, channel: channel_name, error: { code: 50505 }) # ProtocolMessage ERROR type
+ client.connection.__incoming_protocol_msgbus__.publish :protocol_message, error_message
end
end
end
end
end