spec/acceptance/realtime/presence_spec.rb in ably-0.6.2 vs spec/acceptance/realtime/presence_spec.rb in ably-0.7.0
- old
+ new
@@ -1,419 +1,949 @@
+# encoding: utf-8
require 'spec_helper'
-require 'securerandom'
-describe 'Ably::Realtime::Presence Messages' do
- include RSpec::EventMachine
+describe Ably::Realtime::Presence, :event_machine do
+ vary_by_protocol do
+ let(:default_options) { { api_key: api_key, environment: environment, protocol: protocol } }
+ let(:client_options) { default_options }
- [:msgpack, :json].each do |protocol|
- context "over #{protocol}" do
- let(:default_options) { { api_key: api_key, environment: environment, protocol: protocol } }
+ let(:anonymous_client) { Ably::Realtime::Client.new(client_options) }
+ let(:client_one) { Ably::Realtime::Client.new(client_options.merge(client_id: random_str)) }
+ let(:client_two) { Ably::Realtime::Client.new(client_options.merge(client_id: random_str)) }
- let(:channel_name) { "presence-#{SecureRandom.hex(2)}" }
+ let(:channel_name) { "presence-#{random_str(4)}" }
+ let(:channel_anonymous_client) { anonymous_client.channel(channel_name) }
+ let(:presence_anonymous_client) { channel_anonymous_client.presence }
+ let(:channel_client_one) { client_one.channel(channel_name) }
+ let(:channel_rest_client_one) { client_one.rest_client.channel(channel_name) }
+ let(:presence_client_one) { channel_client_one.presence }
+ let(:channel_client_two) { client_two.channel(channel_name) }
+ let(:presence_client_two) { channel_client_two.presence }
+ let(:data_payload) { random_str }
- let(:anonymous_client) { Ably::Realtime::Client.new(default_options) }
- let(:client_one) { Ably::Realtime::Client.new(default_options.merge(client_id: SecureRandom.hex(4))) }
- let(:client_two) { Ably::Realtime::Client.new(default_options.merge(client_id: SecureRandom.hex(4))) }
+ context 'when attached (but not present) on a presence channel with an anonymous client (no client ID)' do
+ it 'maintains state as other clients enter and leave the channel' do
+ channel_anonymous_client.attach do
+ presence_anonymous_client.subscribe(:enter) do |presence_message|
+ expect(presence_message.client_id).to eql(client_one.client_id)
- let(:channel_anonymous_client) { anonymous_client.channel(channel_name) }
- let(:presence_anonymous_client) { channel_anonymous_client.presence }
- let(:channel_client_one) { client_one.channel(channel_name) }
- let(:channel_rest_client_one) { client_one.rest_client.channel(channel_name) }
- let(:presence_client_one) { channel_client_one.presence }
- let(:channel_client_two) { client_two.channel(channel_name) }
- let(:presence_client_two) { channel_client_two.presence }
-
- let(:data_payload) { SecureRandom.hex(8) }
-
- specify 'an attached channel that is not presence maintains presence state' do
- run_reactor do
- channel_anonymous_client.attach do
- presence_anonymous_client.subscribe(:enter) do |presence_message|
- expect(presence_message.client_id).to eql(client_one.client_id)
- members = presence_anonymous_client.get
+ presence_anonymous_client.get do |members|
expect(members.first.client_id).to eql(client_one.client_id)
expect(members.first.action).to eq(:enter)
presence_anonymous_client.subscribe(:leave) do |presence_message|
expect(presence_message.client_id).to eql(client_one.client_id)
- members = presence_anonymous_client.get
- expect(members.count).to eql(0)
- stop_reactor
+ presence_anonymous_client.get do |members|
+ expect(members.count).to eql(0)
+ stop_reactor
+ end
end
end
end
+ end
- presence_client_one.enter do
- presence_client_one.leave
- end
+ presence_client_one.enter do
+ presence_client_one.leave
end
end
+ end
- it '#enter allows client_id to be set on enter for anonymous clients' do
- run_reactor do
- channel_anonymous_client.presence.enter client_id: "123"
-
- channel_anonymous_client.presence.subscribe do |presence|
- expect(presence.client_id).to eq("123")
+ context '#sync_complete?' do
+ context 'when attaching to a channel without any members present' do
+ it 'is true and the presence channel is considered synced immediately' do
+ channel_anonymous_client.attach do
+ expect(channel_anonymous_client.presence).to be_sync_complete
stop_reactor
end
end
end
- it 'enters and then leaves' do
- leave_callback_called = false
- run_reactor do
+ context 'when attaching to a channel with members present' do
+ it 'is false and the presence channel will subsequently be synced' do
presence_client_one.enter do
- presence_client_one.leave do |presence|
- leave_callback_called = true
- end
- presence_client_one.on(:left) do
- EventMachine.next_tick do
- expect(leave_callback_called).to eql(true)
+ channel_anonymous_client.attach do
+ expect(channel_anonymous_client.presence).to_not be_sync_complete
+ channel_anonymous_client.presence.get do
+ expect(channel_anonymous_client.presence).to be_sync_complete
stop_reactor
end
end
end
end
end
+ end
- it 'enters the :left state if the channel detaches' do
- detached = false
- run_reactor do
+ context 'when the SYNC of a presence channel spans multiple ProtocolMessage messages' do
+ context 'with 250 existing (present) members' do
+ let(:enter_expected_count) { 250 }
+ let(:present) { [] }
+ let(:entered) { [] }
+
+ context 'when a new client attaches to the presence channel', em_timeout: 10 do
+ it 'emits :present for each member' do
+ enter_expected_count.times do |index|
+ presence_client_one.enter_client("client:#{index}") do |message|
+ entered << message
+ next unless entered.count == enter_expected_count
+
+ presence_anonymous_client.subscribe(:present) do |present_message|
+ expect(present_message.action).to eq(:present)
+ present << present_message
+ next unless present.count == enter_expected_count
+
+ expect(present.map(&:client_id).uniq.count).to eql(enter_expected_count)
+ stop_reactor
+ end
+ end
+ end
+ end
+
+ context '#get' do
+ it '#waits until sync is complete', event_machine: 15 do
+ enter_expected_count.times do |index|
+ presence_client_one.enter_client("client:#{index}") do |message|
+ entered << message
+ next unless entered.count == enter_expected_count
+
+ presence_anonymous_client.get do |members|
+ expect(members.map(&:client_id).uniq.count).to eql(enter_expected_count)
+ expect(members.count).to eql(enter_expected_count)
+ stop_reactor
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ context 'automatic attachment of channel on access to presence object' do
+ it 'is implicit if presence state is initalized' do
+ channel_client_one.presence
+ channel_client_one.on(:attached) do
+ expect(channel_client_one.state).to eq(:attached)
+ stop_reactor
+ end
+ end
+
+ it 'is disabled if presence state is not initialized' do
+ channel_client_one.attach do
+ channel_client_one.detach do
+ expect(channel_client_one.state).to eq(:detached)
+
+ channel_client_one.presence # access the presence object
+ EventMachine.add_timer(1) do
+ expect(channel_client_one.state).to eq(:detached)
+ stop_reactor
+ end
+ end
+ end
+ end
+ end
+
+ context 'state' do
+ context 'once opened' do
+ it 'once opened, enters the :left state if the channel detaches' do
+ detached = false
+
channel_client_one.presence.on(:left) do
expect(channel_client_one.presence.state).to eq(:left)
EventMachine.next_tick do
expect(detached).to eq(true)
stop_reactor
end
end
+
channel_client_one.presence.enter do |presence|
expect(presence.state).to eq(:entered)
channel_client_one.detach do
expect(channel_client_one.state).to eq(:detached)
detached = true
end
end
end
end
+ end
- specify '#get returns the current member on the channel' do
- run_reactor do
- presence_client_one.enter do
- members = presence_client_one.get
- expect(members.count).to eq(1)
+ context '#enter' do
+ it 'allows client_id to be set on enter for anonymous clients' do
+ channel_anonymous_client.presence.enter client_id: "123"
- expect(client_one.client_id).to_not be_nil
+ channel_anonymous_client.presence.subscribe do |presence|
+ expect(presence.client_id).to eq("123")
+ stop_reactor
+ end
+ end
- this_member = members.first
- expect(this_member.client_id).to eql(client_one.client_id)
+ context 'data attribute' do
+ context 'when provided as argument option to #enter' do
+ it 'remains intact following #leave' do
+ leave_callback_called = false
+ presence_client_one.enter(data: 'stored') do
+ expect(presence_client_one.data).to eql('stored')
+
+ presence_client_one.leave do |presence|
+ leave_callback_called = true
+ end
+
+ presence_client_one.on(:left) do
+ expect(presence_client_one.data).to eql('stored')
+
+ EventMachine.next_tick do
+ expect(leave_callback_called).to eql(true)
+ stop_reactor
+ end
+ end
+ end
+ end
+ end
+ end
+
+ it 'raises an exception if client_id is not set' do
+ expect { channel_anonymous_client.presence.enter }.to raise_error(Ably::Exceptions::Standard, /without a client_id/)
+ stop_reactor
+ end
+
+ it 'returns a Deferrable' do
+ expect(presence_client_one.enter).to be_a(EventMachine::Deferrable)
+ stop_reactor
+ end
+
+ it 'calls the Deferrable callback on success' do
+ presence_client_one.enter.callback do |presence|
+ expect(presence).to eql(presence_client_one)
+ expect(presence_client_one.state).to eq(:entered)
+ stop_reactor
+ end
+ end
+ end
+
+ context '#update' do
+ it 'without previous #enter automatically enters' do
+ presence_client_one.update(data: data_payload) do
+ EventMachine.add_timer(1) do
+ expect(presence_client_one.state).to eq(:entered)
stop_reactor
end
end
end
- specify '#get returns no members on the channel following an enter and leave' do
- run_reactor do
+ context 'when ENTERED' do
+ it 'has no effect on the state' do
presence_client_one.enter do
- presence_client_one.leave do
- expect(presence_client_one.get).to eq([])
- stop_reactor
+ presence_client_one.once_state_changed { fail 'State should not have changed ' }
+
+ presence_client_one.update(data: data_payload) do
+ EventMachine.add_timer(1) do
+ expect(presence_client_one.state).to eq(:entered)
+ presence_client_one.off
+ stop_reactor
+ end
end
end
end
end
- specify 'verify two clients appear in members from #get' do
- run_reactor do
- presence_client_one.enter(data: data_payload)
- presence_client_two.enter
+ it 'updates the data if :data argument provided' do
+ presence_client_one.enter(data: 'prior') do
+ presence_client_one.update(data: data_payload)
+ end
+ presence_client_one.subscribe(:update) do |message|
+ expect(message.data).to eql(data_payload)
+ stop_reactor
+ end
+ end
- entered_callback = Proc.new do
- next unless presence_client_one.state == :entered && presence_client_two.state == :entered
+ it 'returns a Deferrable' do
+ presence_client_one.enter do
+ expect(presence_client_one.update).to be_a(EventMachine::Deferrable)
+ stop_reactor
+ end
+ end
- EventMachine.add_timer(0.25) do
- expect(presence_client_one.get.count).to eq(presence_client_two.get.count)
+ it 'calls the Deferrable callback on success' do
+ presence_client_one.enter do
+ presence_client_one.update.callback do |presence|
+ expect(presence).to eql(presence_client_one)
+ expect(presence_client_one.state).to eq(:entered)
+ stop_reactor
+ end
+ end
+ end
+ end
- members = presence_client_one.get
- member_client_one = members.find { |presence| presence.client_id == client_one.client_id }
- member_client_two = members.find { |presence| presence.client_id == client_two.client_id }
+ context '#leave' do
+ context ':data option' do
+ let(:data) { random_str }
- expect(member_client_one).to be_a(Ably::Models::PresenceMessage)
- expect(member_client_one.data).to eql(data_payload)
- expect(member_client_two).to be_a(Ably::Models::PresenceMessage)
+ context 'when set to a string' do
+ it 'emits the new data for the leave event' do
+ presence_client_one.enter data: random_str do
+ presence_client_one.leave data: data
+ end
+ presence_client_one.subscribe(:leave) do |presence_message|
+ expect(presence_message.data).to eql(data)
stop_reactor
end
end
+ end
- presence_client_one.on :entered, &entered_callback
- presence_client_two.on :entered, &entered_callback
+ context 'when set to nil' do
+ it 'emits nil data for the leave event' do
+ presence_client_one.enter data: random_str do
+ presence_client_one.leave data: nil
+ end
+
+ presence_client_one.subscribe(:leave) do |presence_message|
+ expect(presence_message.data).to be_nil
+ stop_reactor
+ end
+ end
end
+
+ context 'when not passed as an argument' do
+ it 'emits the original data for the leave event' do
+ presence_client_one.enter data: data do
+ presence_client_one.leave
+ end
+
+ presence_client_one.subscribe(:leave) do |presence_message|
+ expect(presence_message.data).to eql(data)
+ stop_reactor
+ end
+ end
+ end
end
- specify '#subscribe and #unsubscribe to presence events' do
- run_reactor do
- client_two_subscribe_messages = []
+ it 'raises an exception if not entered' do
+ expect { channel_anonymous_client.presence.leave }.to raise_error(Ably::Exceptions::Standard, /Unable to leave presence channel that is not entered/)
+ stop_reactor
+ end
- subscribe_client_one_leaving_callback = Proc.new do |presence_message|
- expect(presence_message.client_id).to eql(client_one.client_id)
- expect(presence_message.data).to eql(data_payload)
- expect(presence_message.action).to eq(:leave)
+ it 'returns a Deferrable' do
+ presence_client_one.enter do
+ expect(presence_client_one.leave).to be_a(EventMachine::Deferrable)
+ stop_reactor
+ end
+ end
+ it 'calls the Deferrable callback on success' do
+ presence_client_one.enter do
+ presence_client_one.leave.callback do |presence|
+ expect(presence).to eql(presence_client_one)
+ expect(presence_client_one.state).to eq(:left)
stop_reactor
end
+ end
+ end
+ end
- subscribe_self_callback = Proc.new do |presence_message|
- if presence_message.client_id == client_two.client_id
- expect(presence_message.action).to eq(:enter)
+ context ':left event' do
+ it 'emits the data defined in enter' do
+ channel_client_one.presence.enter(data: 'data') do
+ channel_client_one.presence.leave
+ end
- presence_client_two.unsubscribe &subscribe_self_callback
- presence_client_two.subscribe &subscribe_client_one_leaving_callback
+ channel_client_two.presence.subscribe(:leave) do |message|
+ expect(message.data).to eql('data')
+ stop_reactor
+ end
+ end
- presence_client_one.leave data: data_payload
- end
+ it 'emits the data defined in update' do
+ channel_client_one.presence.enter(data: 'something else') do
+ channel_client_one.presence.update(data: 'data') do
+ channel_client_one.presence.leave
end
+ end
- presence_client_one.enter do
- presence_client_two.enter
- presence_client_two.subscribe &subscribe_self_callback
- end
+ channel_client_two.presence.subscribe(:leave) do |message|
+ expect(message.data).to eql('data')
+ stop_reactor
end
end
+ end
- specify 'REST #get returns current members' do
- run_reactor do
- presence_client_one.enter(data: data_payload) do
- members = channel_rest_client_one.presence.get
- this_member = members.first
+ context 'entering/updating/leaving presence state on behalf of another client_id' do
+ let(:client_count) { 5 }
+ let(:clients) { [] }
+ let(:data) { random_str }
- expect(this_member).to be_a(Ably::Models::PresenceMessage)
- expect(this_member.client_id).to eql(client_one.client_id)
- expect(this_member.data).to eql(data_payload)
+ context '#enter_client' do
+ context 'multiple times on the same channel with different client_ids' do
+ it "has no affect on the client's presence state and only enters on behalf of the provided client_id" do
+ client_count.times do |client_id|
+ presence_client_one.enter_client("client:#{client_id}") do
+ presence_client_one.on(:entered) { raise 'Should not have entered' }
+ next unless client_id == client_count - 1
+ EventMachine.add_timer(0.5) do
+ expect(presence_client_one.state).to eq(:initialized)
+ stop_reactor
+ end
+ end
+ end
+ end
+
+ it 'enters a channel and sets the data based on the provided :data option' do
+ client_count.times do |client_id|
+ presence_client_one.enter_client("client:#{client_id}", data: data)
+ end
+
+ presence_anonymous_client.subscribe(:enter) do |presence|
+ expect(presence.data).to eql(data)
+ clients << presence
+ next unless clients.count == 5
+
+ expect(clients.map(&:client_id).uniq.count).to eql(5)
+ stop_reactor
+ end
+ end
+ end
+
+ it 'returns a Deferrable' do
+ expect(presence_client_one.enter_client('client_id')).to be_a(EventMachine::Deferrable)
+ stop_reactor
+ end
+
+ it 'calls the Deferrable callback on success' do
+ presence_client_one.enter_client('client_id').callback do |presence|
+ expect(presence).to eql(presence_client_one)
stop_reactor
end
end
end
- specify 'REST #get returns no members once left' do
- run_reactor do
- presence_client_one.enter(data: data_payload) do
- presence_client_one.leave do
- members = channel_rest_client_one.presence.get
- expect(members.count).to eql(0)
- stop_reactor
+ context '#update_client' do
+ context 'multiple times on the same channel with different client_ids' do
+ it 'updates the data attribute for the member when :data option provided' do
+ updated_callback_count = 0
+
+ client_count.times do |client_id|
+ presence_client_one.enter_client("client:#{client_id}") do
+ presence_client_one.update_client("client:#{client_id}", data: data) do
+ updated_callback_count += 1
+ end
+ end
end
+
+ presence_anonymous_client.subscribe(:update) do |presence|
+ expect(presence.data).to eql(data)
+ clients << presence
+ next unless clients.count == 5
+
+ EventMachine.add_timer(0.5) do
+ expect(clients.map(&:client_id).uniq.count).to eql(5)
+ expect(updated_callback_count).to eql(5)
+ stop_reactor
+ end
+ end
end
+
+ it 'enters if not already entered' do
+ updated_callback_count = 0
+
+ client_count.times do |client_id|
+ presence_client_one.update_client("client:#{client_id}", data: data) do
+ updated_callback_count += 1
+ end
+ end
+
+ presence_anonymous_client.subscribe(:enter) do |presence|
+ expect(presence.data).to eql(data)
+ clients << presence
+ next unless clients.count == 5
+
+ EventMachine.add_timer(0.5) do
+ expect(clients.map(&:client_id).uniq.count).to eql(5)
+ expect(updated_callback_count).to eql(5)
+ stop_reactor
+ end
+ end
+ end
end
+
+ it 'returns a Deferrable' do
+ expect(presence_client_one.update_client('client_id')).to be_a(EventMachine::Deferrable)
+ stop_reactor
+ end
+
+ it 'calls the Deferrable callback on success' do
+ presence_client_one.update_client('client_id').callback do |presence|
+ expect(presence).to eql(presence_client_one)
+ stop_reactor
+ end
+ end
end
- context 'encoding and decoding of presence message data' do
- let(:secret_key) { SecureRandom.hex(32) }
- let(:cipher_options) { { key: secret_key, algorithm: 'aes', mode: 'cbc', key_length: 256 } }
- let(:channel_name) { SecureRandom.hex(32) }
- let(:encrypted_channel) { client_one.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
- let(:channel_rest_client_one) { client_one.rest_client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
+ context '#leave_client' do
+ context 'leaves a channel' do
+ context 'multiple times on the same channel with different client_ids' do
+ it 'emits the :leave event for each client_id' do
+ left_callback_count = 0
- let(:crypto) { Ably::Util::Crypto.new(cipher_options) }
+ client_count.times do |client_id|
+ presence_client_one.enter_client("client:#{client_id}", data: random_str) do
+ presence_client_one.leave_client("client:#{client_id}", data: data) do
+ left_callback_count += 1
+ end
+ end
+ end
- let(:data) { { 'key' => SecureRandom.hex(64) } }
- let(:data_as_json) { data.to_json }
- let(:data_as_cipher) { crypto.encrypt(data.to_json) }
+ presence_anonymous_client.subscribe(:leave) do |presence|
+ expect(presence.data).to eql(data)
+ clients << presence
+ next unless clients.count == 5
- it 'encrypts presence message data' do
- run_reactor do
- encrypted_channel.attach do
- encrypted_channel.presence.enter data: data
+ EventMachine.add_timer(0.5) do
+ expect(clients.map(&:client_id).uniq.count).to eql(5)
+ expect(left_callback_count).to eql(5)
+ stop_reactor
+ end
+ end
end
- encrypted_channel.presence.__incoming_msgbus__.unsubscribe(:presence) # remove all subscribe callbacks that could decrypt the message
- encrypted_channel.presence.__incoming_msgbus__.subscribe(:presence) do |presence|
- if protocol == :json
- expect(presence['encoding']).to eql('json/utf-8/cipher+aes-256-cbc/base64')
- expect(crypto.decrypt(Base64.decode64(presence['data']))).to eql(data_as_json)
- else
- expect(presence['encoding']).to eql('json/utf-8/cipher+aes-256-cbc')
- expect(crypto.decrypt(presence['data'])).to eql(data_as_json)
+ it 'succeeds if that client_id has not previously entered the channel' do
+ left_callback_count = 0
+
+ client_count.times do |client_id|
+ presence_client_one.leave_client("client:#{client_id}") do
+ left_callback_count += 1
+ end
end
- stop_reactor
+
+ presence_anonymous_client.subscribe(:leave) do |presence|
+ expect(presence.data).to be_nil
+ clients << presence
+ next unless clients.count == 5
+
+ EventMachine.add_timer(1) do
+ expect(clients.map(&:client_id).uniq.count).to eql(5)
+ expect(left_callback_count).to eql(5)
+ stop_reactor
+ end
+ end
end
end
- end
- it '#subscribe emits decrypted enter events' do
- run_reactor do
- encrypted_channel.attach do
- encrypted_channel.presence.enter data: data
+ context 'with a new value in :data option' do
+ it 'emits the leave event with the new data value' do
+ presence_client_one.enter_client("client:unique", data: random_str) do
+ presence_client_one.leave_client("client:unique", data: data)
+ end
+
+ presence_client_one.subscribe(:leave) do |presence_message|
+ expect(presence_message.data).to eql(data)
+ stop_reactor
+ end
end
+ end
- encrypted_channel.presence.subscribe(:enter) do |presence_message|
- expect(presence_message.encoding).to be_nil
- expect(presence_message.data).to eql(data)
- stop_reactor
+ context 'with a nil value in :data option' do
+ it 'emits the leave event with a nil value' do
+ presence_client_one.enter_client("client:unique", data: data) do
+ presence_client_one.leave_client("client:unique", data: nil)
+ end
+
+ presence_client_one.subscribe(:leave) do |presence_message|
+ expect(presence_message.data).to be_nil
+ stop_reactor
+ end
end
end
- end
- it '#subscribe emits decrypted update events' do
- run_reactor do
- encrypted_channel.attach do
- encrypted_channel.presence.enter(data: 'to be updated') do
- encrypted_channel.presence.update data: data
+ context 'with no :data option' do
+ it 'emits the leave event with the previous data value' do
+ presence_client_one.enter_client("client:unique", data: data) do
+ presence_client_one.leave_client("client:unique")
end
+
+ presence_client_one.subscribe(:leave) do |presence_message|
+ expect(presence_message.data).to eql(data)
+ stop_reactor
+ end
end
+ end
+ end
- encrypted_channel.presence.subscribe(:update) do |presence_message|
- expect(presence_message.encoding).to be_nil
- expect(presence_message.data).to eql(data)
+ it 'returns a Deferrable' do
+ expect(presence_client_one.leave_client('client_id')).to be_a(EventMachine::Deferrable)
+ stop_reactor
+ end
+
+ it 'calls the Deferrable callback on success' do
+ presence_client_one.leave_client('client_id').callback do |presence|
+ expect(presence).to eql(presence_client_one)
+ stop_reactor
+ end
+ end
+ end
+ end
+
+ context '#get' do
+ it 'returns a Deferrable' do
+ expect(presence_client_one.get).to be_a(EventMachine::Deferrable)
+ stop_reactor
+ end
+
+ it 'calls the Deferrable callback on success' do
+ presence_client_one.get.callback do |presence|
+ expect(presence).to eq([])
+ stop_reactor
+ end
+ end
+
+ it 'returns the current members on the channel' do
+ presence_client_one.enter do
+ presence_client_one.get do |members|
+ expect(members.count).to eq(1)
+
+ expect(client_one.client_id).to_not be_nil
+
+ this_member = members.first
+ expect(this_member.client_id).to eql(client_one.client_id)
+
+ stop_reactor
+ end
+ end
+ end
+
+ it 'filters by connection_id option if provided' do
+ when_all(presence_client_one.enter, presence_client_two.enter, and_wait: 0.5) do
+ presence_client_one.get(connection_id: client_one.connection.id) do |members|
+ expect(members.count).to eq(1)
+ expect(members.first.connection_id).to eql(client_one.connection.id)
+
+ presence_client_one.get(connection_id: client_two.connection.id) do |members|
+ expect(members.count).to eq(1)
+ expect(members.first.connection_id).to eql(client_two.connection.id)
stop_reactor
end
end
end
+ end
- it '#subscribe emits decrypted leave events' do
- run_reactor do
- encrypted_channel.attach do
- encrypted_channel.presence.enter(data: 'to be updated') do
- encrypted_channel.presence.leave data: data
- end
- end
+ it 'filters by client_id option if provided' do
+ when_all(presence_client_one.enter(client_id: 'one'), presence_client_two.enter(client_id: 'two')) do
+ presence_client_one.get(client_id: 'one') do |members|
+ expect(members.count).to eq(1)
+ expect(members.first.client_id).to eql('one')
+ expect(members.first.connection_id).to eql(client_one.connection.id)
- encrypted_channel.presence.subscribe(:leave) do |presence_message|
- expect(presence_message.encoding).to be_nil
- expect(presence_message.data).to eql(data)
+ presence_client_one.get(client_id: 'two') do |members|
+ expect(members.count).to eq(1)
+ expect(members.first.client_id).to eql('two')
+ expect(members.first.connection_id).to eql(client_two.connection.id)
stop_reactor
end
end
end
+ end
- it '#get returns a list of members with decrypted data' do
- run_reactor do
- encrypted_channel.attach do
- encrypted_channel.presence.enter(data: data) do
- member = encrypted_channel.presence.get.first
- expect(member.encoding).to be_nil
- expect(member.data).to eql(data)
+ it 'does not wait for SYNC to complete if :wait_for_sync option is false' do
+ presence_client_one.enter(client_id: 'one') do
+ presence_client_two.get(wait_for_sync: false) do |members|
+ expect(members.count).to eql(0)
+ stop_reactor
+ end
+ end
+ end
+
+ context 'when a member enters and then leaves' do
+ it 'has no members' do
+ presence_client_one.enter do
+ presence_client_one.leave do
+ presence_client_one.get do |members|
+ expect(members.count).to eq(0)
stop_reactor
end
end
end
end
+ end
- it 'REST #get returns a list of members with decrypted data' do
- run_reactor do
- encrypted_channel.attach do
- encrypted_channel.presence.enter(data: data) do
- member = channel_rest_client_one.presence.get.first
- expect(member.encoding).to be_nil
- expect(member.data).to eql(data)
+ it 'returns both members on both simultaneously connected clients' do
+ when_all(presence_client_one.enter(data: data_payload), presence_client_two.enter) do
+ EventMachine.add_timer(0.5) do
+ presence_client_one.get do |client_one_members|
+ presence_client_two.get do |client_two_members|
+ expect(client_one_members.count).to eq(client_two_members.count)
+
+ member_client_one = client_one_members.find { |presence| presence.client_id == client_one.client_id }
+ member_client_two = client_one_members.find { |presence| presence.client_id == client_two.client_id }
+
+ expect(member_client_one).to be_a(Ably::Models::PresenceMessage)
+ expect(member_client_one.data).to eql(data_payload)
+ expect(member_client_two).to be_a(Ably::Models::PresenceMessage)
+
stop_reactor
end
end
end
end
+ end
+ end
- context 'when cipher settings do not match publisher' do
- let(:incompatible_cipher_options) { { key: secret_key, algorithm: 'aes', mode: 'cbc', key_length: 128 } }
- let(:incompatible_encrypted_channel) { client_two.channel(channel_name, encrypted: true, cipher_params: incompatible_cipher_options) }
+ context '#subscribe' do
+ let(:messages) { [] }
- it 'delivers an unencoded presence message left with encoding value' do
- run_reactor do
- incompatible_encrypted_channel.attach do
- encrypted_channel.attach do
- encrypted_channel.presence.enter(data: data) do
- member = incompatible_encrypted_channel.presence.get.first
- expect(member.encoding).to match(/cipher\+aes-256-cbc/)
- expect(member.data).to_not eql(data)
- stop_reactor
- end
- end
- end
+ context 'with no arguments' do
+ it 'calls the callback for all presence events' do
+ when_all(channel_client_one.attach, channel_client_two.attach) do
+ presence_client_two.subscribe do |presence_message|
+ messages << presence_message
+ next unless messages.count == 3
+
+ expect(messages.map(&:action).map(&:to_sym)).to contain_exactly(:enter, :update, :leave)
+ stop_reactor
end
+
+ presence_client_one.enter
+ presence_client_one.update
+ presence_client_one.leave
end
+ end
+ end
+ end
- it 'emits an error when cipher does not match and presence data cannot be decoded' do
- run_reactor do
- incompatible_encrypted_channel.attach do
- incompatible_encrypted_channel.on(:error) do |error|
- expect(error).to be_a(Ably::Exceptions::CipherError)
- expect(error.message).to match(/Cipher algorithm AES-128-CBC does not match/)
- stop_reactor
- end
+ context '#unsubscribe' do
+ context 'with no arguments' do
+ it 'removes the callback for all presence events' do
+ when_all(channel_client_one.attach, channel_client_two.attach) do
+ subscribe_callback = proc { raise 'Should not be called' }
+ presence_client_two.subscribe &subscribe_callback
+ presence_client_two.unsubscribe &subscribe_callback
- encrypted_channel.attach do
- encrypted_channel.presence.enter data: data
- end
+ presence_client_one.enter
+ presence_client_one.update
+ presence_client_one.leave do
+ EventMachine.add_timer(0.5) do
+ stop_reactor
end
end
end
end
end
+ end
- specify 'expect :left event once underlying connection is closed' do
- run_reactor do
- presence_client_one.on(:left) do
- expect(presence_client_one.state).to eq(:left)
+ context 'REST #get' do
+ it 'returns current members' do
+ presence_client_one.enter(data: data_payload) do
+ members = channel_rest_client_one.presence.get
+ this_member = members.first
+
+ expect(this_member).to be_a(Ably::Models::PresenceMessage)
+ expect(this_member.client_id).to eql(client_one.client_id)
+ expect(this_member.data).to eql(data_payload)
+
+ stop_reactor
+ end
+ end
+
+ it 'returns no members once left' do
+ presence_client_one.enter(data: data_payload) do
+ presence_client_one.leave do
+ members = channel_rest_client_one.presence.get
+ expect(members.count).to eql(0)
stop_reactor
end
- presence_client_one.enter do
- client_one.close
+ end
+ end
+ end
+
+ context 'client_id with ASCII_8BIT' do
+ let(:client_id) { random_str.encode(Encoding::ASCII_8BIT) }
+
+ context 'in connection set up' do
+ let(:client_one) { Ably::Realtime::Client.new(default_options.merge(client_id: client_id)) }
+
+ it 'is converted into UTF_8' do
+ presence_client_one.enter
+ presence_client_one.on(:entered) do |presence|
+ expect(presence.client_id.encoding).to eql(Encoding::UTF_8)
+ expect(presence.client_id.encode(Encoding::ASCII_8BIT)).to eql(client_id)
+ stop_reactor
end
end
end
- specify 'expect :left event with no client data to retain original data in Leave event' do
- run_reactor do
- presence_client_one.subscribe(:leave) do |message|
- expect(presence_client_one.get.count).to eq(0)
- expect(message.data).to eq(data_payload)
+ context 'in channel options' do
+ let(:client_one) { Ably::Realtime::Client.new(default_options) }
+
+ it 'is converted into UTF_8' do
+ presence_client_one.enter(client_id: client_id)
+ presence_client_one.on(:entered) do |presence|
+ expect(presence.client_id.encoding).to eql(Encoding::UTF_8)
+ expect(presence.client_id.encode(Encoding::ASCII_8BIT)).to eql(client_id)
stop_reactor
end
- presence_client_one.enter(data: data_payload) do
- presence_client_one.leave
+ end
+ end
+ end
+
+ context 'encoding and decoding of presence message data' do
+ let(:secret_key) { random_str }
+ let(:cipher_options) { { key: secret_key, algorithm: 'aes', mode: 'cbc', key_length: 256 } }
+ let(:channel_name) { random_str }
+ let(:encrypted_channel) { client_one.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
+ let(:channel_rest_client_one) { client_one.rest_client.channel(channel_name, encrypted: true, cipher_params: cipher_options) }
+
+ let(:crypto) { Ably::Util::Crypto.new(cipher_options) }
+
+ let(:data) { { 'key' => random_str } }
+ let(:data_as_json) { data.to_json }
+ let(:data_as_cipher) { crypto.encrypt(data.to_json) }
+
+ it 'encrypts presence message data' do
+ encrypted_channel.attach do
+ encrypted_channel.presence.enter data: data
+ end
+
+ encrypted_channel.presence.__incoming_msgbus__.unsubscribe(:presence) # remove all subscribe callbacks that could decrypt the message
+ encrypted_channel.presence.__incoming_msgbus__.subscribe(:presence) do |presence|
+ if protocol == :json
+ expect(presence['encoding']).to eql('json/utf-8/cipher+aes-256-cbc/base64')
+ expect(crypto.decrypt(Base64.decode64(presence['data']))).to eql(data_as_json)
+ else
+ expect(presence['encoding']).to eql('json/utf-8/cipher+aes-256-cbc')
+ expect(crypto.decrypt(presence['data'])).to eql(data_as_json)
end
+ stop_reactor
end
end
- specify '#update automatically connects' do
- run_reactor do
- presence_client_one.update(data: data_payload) do
- expect(presence_client_one.state).to eq(:entered)
+ context '#subscribe' do
+ it 'emits decrypted enter events' do
+ encrypted_channel.attach do
+ encrypted_channel.presence.enter data: data
+ end
+
+ encrypted_channel.presence.subscribe(:enter) do |presence_message|
+ expect(presence_message.encoding).to be_nil
+ expect(presence_message.data).to eql(data)
stop_reactor
end
end
+
+ it 'emits decrypted update events' do
+ encrypted_channel.attach do
+ encrypted_channel.presence.enter(data: 'to be updated') do
+ encrypted_channel.presence.update data: data
+ end
+ end
+
+ encrypted_channel.presence.subscribe(:update) do |presence_message|
+ expect(presence_message.encoding).to be_nil
+ expect(presence_message.data).to eql(data)
+ stop_reactor
+ end
+ end
+
+ it 'emits previously set data for leave events' do
+ encrypted_channel.attach do
+ encrypted_channel.presence.enter(data: data) do
+ encrypted_channel.presence.leave
+ end
+ end
+
+ encrypted_channel.presence.subscribe(:leave) do |presence_message|
+ expect(presence_message.encoding).to be_nil
+ expect(presence_message.data).to eql(data)
+ stop_reactor
+ end
+ end
end
- specify '#update changes the data' do
- run_reactor do
- presence_client_one.enter(data: 'prior') do
- presence_client_one.update(data: data_payload)
+ context '#get' do
+ it 'returns a list of members with decrypted data' do
+ encrypted_channel.presence.enter(data: data) do
+ encrypted_channel.presence.get do |members|
+ member = members.first
+ expect(member.encoding).to be_nil
+ expect(member.data).to eql(data)
+ stop_reactor
+ end
end
- presence_client_one.subscribe(:update) do |message|
- expect(message.data).to eql(data_payload)
+ end
+ end
+
+ context 'REST #get' do
+ it 'returns a list of members with decrypted data' do
+ encrypted_channel.presence.enter(data: data) do
+ member = channel_rest_client_one.presence.get.first
+ expect(member.encoding).to be_nil
+ expect(member.data).to eql(data)
stop_reactor
end
end
end
- it 'raises an exception if client_id is not set' do
- run_reactor do
- expect { channel_anonymous_client.presence.enter }.to raise_error(Ably::Exceptions::Standard, /without a client_id/)
- stop_reactor
+ context 'when cipher settings do not match publisher' do
+ let(:client_options) { default_options.merge(log_level: :fatal) }
+ let(:incompatible_cipher_options) { { key: secret_key, algorithm: 'aes', mode: 'cbc', key_length: 128 } }
+ let(:incompatible_encrypted_channel) { client_two.channel(channel_name, encrypted: true, cipher_params: incompatible_cipher_options) }
+
+ it 'delivers an unencoded presence message left with encoding value' do
+ encrypted_channel.presence.enter data: data
+
+ incompatible_encrypted_channel.presence.subscribe(:enter) do
+ incompatible_encrypted_channel.presence.get do |members|
+ member = members.first
+ expect(member.encoding).to match(/cipher\+aes-256-cbc/)
+ expect(member.data).to_not eql(data)
+ stop_reactor
+ end
+ end
end
+
+ it 'emits an error when cipher does not match and presence data cannot be decoded' do
+ incompatible_encrypted_channel.attach do
+ incompatible_encrypted_channel.on(:error) do |error|
+ expect(error).to be_a(Ably::Exceptions::CipherError)
+ expect(error.message).to match(/Cipher algorithm AES-128-CBC does not match/)
+ stop_reactor
+ end
+
+ encrypted_channel.attach do
+ encrypted_channel.presence.enter data: data
+ end
+ end
+ end
end
+ end
- it '#leave raises an exception if not entered' do
- run_reactor do
- expect { channel_anonymous_client.presence.leave }.to raise_error(Ably::Exceptions::Standard, /Unable to leave presence channel that is not entered/)
+ context 'leaving' do
+ specify 'expect :left event once underlying connection is closed' do
+ presence_client_one.on(:left) do
+ expect(presence_client_one.state).to eq(:left)
stop_reactor
end
+ presence_client_one.enter do
+ client_one.close
+ end
end
- skip 'ensure member_id is unique an updated on ENTER'
- skip 'stop a call to get when the channel has not been entered'
- skip 'stop a call to get when the channel has been entered but the list is not up to date'
+ specify 'expect :left event with client data from enter event' do
+ presence_client_one.subscribe(:leave) do |message|
+ presence_client_one.get do |members|
+ expect(members.count).to eq(0)
+ expect(message.data).to eql(data_payload)
+ stop_reactor
+ end
+ end
+ presence_client_one.enter(data: data_payload) do
+ presence_client_one.leave
+ end
+ end
end
+
+ skip 'ensure connection_id is unique and updated on ENTER'
+ skip 'ensure connection_id for presence member matches the messages they publish on the channel'
+ skip 'stop a call to get when the channel has not been entered'
+ skip 'stop a call to get when the channel has been entered but the list is not up to date'
+ skip 'presence will resume sync if connection is dropped mid-way'
end
end