diff --git a/lib/ably/models/channel_options.rb b/lib/ably/models/channel_options.rb new file mode 100644 index 000000000..ec23f0be4 --- /dev/null +++ b/lib/ably/models/channel_options.rb @@ -0,0 +1,97 @@ +module Ably::Models + # Convert token details argument to a {ChannelOptions} object + # + # @param attributes (see #initialize) + # + # @return [ChannelOptions] + def self.ChannelOptions(attributes) + case attributes + when ChannelOptions + return attributes + else + ChannelOptions.new(attributes) + end + end + + # Represents options of a channel + class ChannelOptions + extend Ably::Modules::Enum + extend Forwardable + include Ably::Modules::ModelCommon + + MODES = ruby_enum('MODES', + presence: 0, + publish: 1, + subscribe: 2, + presence_subscribe: 3 + ) + + attr_reader :attributes + + alias_method :to_h, :attributes + + def_delegators :attributes, :fetch, :size, :empty? + # Initialize a new ChannelOptions + # + # @option params [Hash] (TB2c) params (for realtime client libraries only) a of key/value pairs + # @option modes [Hash] modes (for realtime client libraries only) an array of ChannelMode + # @option cipher [Hash,Ably::Models::CipherParams] :cipher A hash of options or a {Ably::Models::CipherParams} to configure the encryption. *:key* is required, all other options are optional. + # + def initialize(attrs) + @attributes = IdiomaticRubyWrapper(attrs.clone) + + attributes[:modes] = modes.to_a.map { |mode| Ably::Models::ChannelOptions::MODES[mode] } if modes + attributes[:cipher] = Ably::Models::CipherParams(cipher) if cipher + attributes.clone + end + + # @!attribute cipher + # + # @return [CipherParams] + def cipher + attributes[:cipher] + end + + # @!attribute params + # + # @return [Hash] + def params + attributes[:params].to_h + end + + # @!attribute modes + # + # @return [Array] + def modes + attributes[:modes] + end + + # Converts modes to a bitfield that coresponds to ProtocolMessage#flags + # + # @return [Integer] + def modes_to_flags + modes.map { |mode| Ably::Models::ProtocolMessage::ATTACH_FLAGS_MAPPING[mode.to_sym] }.reduce(:|) + end + + # @return [Hash] + # @api private + def set_params(hash) + attributes[:params] = hash + end + + # Sets modes from ProtocolMessage#flags + # + # @return [Array] + # @api private + def set_modes_from_flags(flags) + return unless flags + + message_modes = MODES.select do |mode| + flag = Ably::Models::ProtocolMessage::ATTACH_FLAGS_MAPPING[mode.to_sym] + flags & flag == flag + end + + attributes[:modes] = message_modes.map { |mode| Ably::Models::ChannelOptions::MODES[mode] } + end + end +end diff --git a/lib/ably/models/idiomatic_ruby_wrapper.rb b/lib/ably/models/idiomatic_ruby_wrapper.rb index ce3e2c3ce..a9e964a1e 100644 --- a/lib/ably/models/idiomatic_ruby_wrapper.rb +++ b/lib/ably/models/idiomatic_ruby_wrapper.rb @@ -94,6 +94,10 @@ def size attributes.size end + def empty? + attributes.empty? + end + def keys map { |key, value| key } end diff --git a/lib/ably/models/protocol_message.rb b/lib/ably/models/protocol_message.rb index 1534a135e..b07e1e64f 100644 --- a/lib/ably/models/protocol_message.rb +++ b/lib/ably/models/protocol_message.rb @@ -66,6 +66,14 @@ class ProtocolMessage auth: 17 ) + ATTACH_FLAGS_MAPPING = { + resume: 32, # 2^5 + presence: 65536, # 2^16 + publish: 131072, # 2^17 + subscribe: 262144, # 2^18 + presence_subscribe: 524288, # 2^19 + } + # Indicates this protocol message action will generate an ACK response such as :message or :presence # @api private def self.ack_required?(for_action) @@ -185,6 +193,10 @@ def has_correct_message_size? message_size <= connection_details.max_message_size end + def params + @params ||= attributes[:params].to_h + end + def flags Integer(attributes[:flags]) rescue TypeError @@ -218,27 +230,27 @@ def has_transient_flag? # @api private def has_attach_resume_flag? - flags & 32 == 32 # 2^5 + flags & ATTACH_FLAGS_MAPPING[:resume] == ATTACH_FLAGS_MAPPING[:resume] # 2^5 end # @api private def has_attach_presence_flag? - flags & 65536 == 65536 # 2^16 + flags & ATTACH_FLAGS_MAPPING[:presence] == ATTACH_FLAGS_MAPPING[:presence] # 2^16 end # @api private def has_attach_publish_flag? - flags & 131072 == 131072 # 2^17 + flags & ATTACH_FLAGS_MAPPING[:publish] == ATTACH_FLAGS_MAPPING[:publish] # 2^17 end # @api private def has_attach_subscribe_flag? - flags & 262144 == 262144 # 2^18 + flags & ATTACH_FLAGS_MAPPING[:subscribe] == ATTACH_FLAGS_MAPPING[:subscribe] # 2^18 end # @api private def has_attach_presence_subscribe_flag? - flags & 524288 == 524288 # 2^19 + flags & ATTACH_FLAGS_MAPPING[:presence_subscribe] == ATTACH_FLAGS_MAPPING[:presence_subscribe] # 2^19 end def connection_details diff --git a/lib/ably/modules/channels_collection.rb b/lib/ably/modules/channels_collection.rb index 113fdbdb3..aa9c14214 100644 --- a/lib/ably/modules/channels_collection.rb +++ b/lib/ably/modules/channels_collection.rb @@ -13,14 +13,21 @@ def initialize(client, channel_klass) # Return a Channel for the given name # # @param name [String] The name of the channel - # @param channel_options [Hash] Channel options including the encryption options + # @param channel_options [Hash, Ably::Models::ChannelOptions] A hash of options or a {Ably::Models::ChannelOptions} # # @return [Channel] # def get(name, channel_options = {}) if channels.has_key?(name) channels[name].tap do |channel| - channel.update_options channel_options if channel_options && !channel_options.empty? + if channel_options && !channel_options.empty? + if channel.respond_to?(:need_reattach?) && channel.need_reattach? + raise_implicit_options_update + else + warn_implicit_options_update + channel.options = channel_options + end + end end else channels[name] ||= channel_klass.new(client, name, channel_options) @@ -70,6 +77,19 @@ def each(&block) end private + + def raise_implicit_options_update + raise ArgumentError, "You are trying to indirectly update channel options which will trigger reattachment of the channel. Please use Channel#set_options directly if you wish to continue" + end + + def warn_implicit_options_update + logger.warn { "Channels#get: Using this method to update channel options is deprecated and may be removed in a future version of ably-ruby. Please use Channel#setOptions instead" } + end + + def logger + client.logger + end + def client @client end diff --git a/lib/ably/realtime/channel.rb b/lib/ably/realtime/channel.rb index 6781b1071..2b16276c2 100644 --- a/lib/ably/realtime/channel.rb +++ b/lib/ably/realtime/channel.rb @@ -36,6 +36,7 @@ class Channel include Ably::Modules::MessageEmitter include Ably::Realtime::Channel::Publisher extend Ably::Modules::Enum + extend Forwardable # ChannelState # The permited states for this channel @@ -92,17 +93,20 @@ class Channel # @api private attr_reader :manager + # ChannelOptions params attrribute (#RTL4k) + # return [Hash] + def_delegators :options, :params + # Initialize a new Channel object # # @param client [Ably::Rest::Client] # @param name [String] The name of the channel - # @param channel_options [Hash] Channel options, currently reserved for Encryption options - # @option channel_options [Hash,Ably::Models::CipherParams] :cipher A hash of options or a {Ably::Models::CipherParams} to configure the encryption. *:key* is required, all other options are optional. See {Ably::Util::Crypto#initialize} for a list of +:cipher+ options + # @param channel_options [Hash, Ably::Models::ChannelOptions] A hash of options or a {Ably::Models::ChannelOptions} # def initialize(client, name, channel_options = {}) name = ensure_utf_8(:name, name) - update_options channel_options + @options = Ably::Models::ChannelOptions(channel_options) @client = client @name = name @queue = [] @@ -309,6 +313,16 @@ def __incoming_msgbus__ ) end + # Sets or updates the stored channel options. (#RTL16) + # @param channel_options [Hash, Ably::Models::ChannelOptions] A hash of options or a {Ably::Models::ChannelOptions} + # @return [Ably::Models::ChannelOptions] + def set_options(channel_options) + @options = Ably::Models::ChannelOptions(channel_options) + + manager.request_reattach if need_reattach? + end + alias options= set_options + # @api private def set_channel_error_reason(error) @error_reason = error @@ -319,13 +333,6 @@ def clear_error_reason @error_reason = nil end - # @api private - def update_options(channel_options) - @options = channel_options.clone.freeze - end - alias set_options update_options # (RSL7) - alias options= update_options - # Used by {Ably::Modules::StateEmitter} to debug state changes # @api private def logger @@ -336,7 +343,12 @@ def logger # #transition_state_machine must be used instead private :change_state + def need_reattach? + !!(attaching? || attached?) && !!(options.modes || options.params) + end + private + def setup_event_handlers __incoming_msgbus__.subscribe(:message) do |message| message.decode(client.encoders, options) do |encode_error, error_message| diff --git a/lib/ably/realtime/channel/channel_manager.rb b/lib/ably/realtime/channel/channel_manager.rb index 4e61a19ca..5cec145be 100644 --- a/lib/ably/realtime/channel/channel_manager.rb +++ b/lib/ably/realtime/channel/channel_manager.rb @@ -38,6 +38,8 @@ def attached(attached_protocol_message) if attached_protocol_message update_presence_sync_state_following_attached attached_protocol_message channel.properties.set_attach_serial(attached_protocol_message.channel_serial) + channel.options.set_modes_from_flags(attached_protocol_message.flags) + channel.options.set_params(attached_protocol_message.params) end end @@ -64,6 +66,7 @@ def duplicate_attached_received(protocol_message) end channel.properties.set_attach_serial(protocol_message.channel_serial) + channel.options.set_modes_from_flags(protocol_message.flags) if protocol_message.has_channel_resumed_flag? logger.debug { "ChannelManager: Additional resumed ATTACHED message received for #{channel.state} channel '#{channel.name}'" } @@ -199,14 +202,18 @@ def channel_retry_timeout end def send_attach_protocol_message - send_state_change_protocol_message Ably::Models::ProtocolMessage::ACTION.Attach, :suspended # move to suspended + message_options = {} + message_options[:flags] = channel.options.modes_to_flags if channel.options.modes + message_options[:params] = channel.options.params if channel.options.params.any? + + send_state_change_protocol_message Ably::Models::ProtocolMessage::ACTION.Attach, :suspended, message_options end def send_detach_protocol_message(previous_state) send_state_change_protocol_message Ably::Models::ProtocolMessage::ACTION.Detach, previous_state # return to previous state if failed end - def send_state_change_protocol_message(new_state, state_if_failed) + def send_state_change_protocol_message(new_state, state_if_failed, message_options = {}) state_at_time_of_request = channel.state @pending_state_change_timer = EventMachine::Timer.new(realtime_request_timeout) do if channel.state == state_at_time_of_request @@ -227,7 +234,8 @@ def send_state_change_protocol_message(new_state, state_if_failed) next unless pending_state_change_timer connection.send_protocol_message( action: new_state.to_i, - channel: channel.name + channel: channel.name, + **message_options.to_h ) resend_if_disconnected_and_connected.call end @@ -237,7 +245,8 @@ def send_state_change_protocol_message(new_state, state_if_failed) connection.send_protocol_message( action: new_state.to_i, - channel: channel.name + channel: channel.name, + **message_options.to_h ) end diff --git a/lib/ably/realtime/channels.rb b/lib/ably/realtime/channels.rb index 9b06dea26..ef52e8d3d 100644 --- a/lib/ably/realtime/channels.rb +++ b/lib/ably/realtime/channels.rb @@ -13,7 +13,7 @@ def initialize(client) # Return a {Ably::Realtime::Channel} for the given name # # @param name [String] The name of the channel - # @param channel_options [Hash] Channel options, currently reserved for Encryption options + # @param channel_options [Hash, Ably::Models::ChannelOptions] A hash of options or a {Ably::Models::ChannelOptions} # @return [Ably::Realtime::Channel} # def get(*args) diff --git a/lib/ably/rest/channel.rb b/lib/ably/rest/channel.rb index 2a187fe2e..972dcb217 100644 --- a/lib/ably/rest/channel.rb +++ b/lib/ably/rest/channel.rb @@ -29,13 +29,12 @@ class Channel # # @param client [Ably::Rest::Client] # @param name [String] The name of the channel - # @param channel_options [Hash] Channel options, currently reserved for Encryption options - # @option channel_options [Hash,Ably::Models::CipherParams] :cipher A hash of options or a {Ably::Models::CipherParams} to configure the encryption. *:key* is required, all other options are optional. See {Ably::Util::Crypto#initialize} for a list of +:cipher+ options + # @param channel_options [Hash, Ably::Models::ChannelOptions] A hash of options or a {Ably::Models::ChannelOptions} # def initialize(client, name, channel_options = {}) name = (ensure_utf_8 :name, name) - update_options channel_options + @options = Ably::Models::ChannelOptions(channel_options) @client = client @name = name @push = PushChannel.new(self) @@ -164,14 +163,16 @@ def presence @presence ||= Presence.new(client, self) end - # @api private - def update_options(channel_options) - @options = channel_options.clone.freeze + # Sets or updates the stored channel options. (#RSL7) + # @param channel_options [Hash, Ably::Models::ChannelOptions] A hash of options or a {Ably::Models::ChannelOptions} + # @return [Ably::Models::ChannelOptions] + def set_options(channel_options) + @options = Ably::Models::ChannelOptions(channel_options) end - alias set_options update_options # (RSL7) - alias options= update_options + alias options= set_options private + def base_path "/channels/#{URI.encode_www_form_component(name)}" end diff --git a/lib/ably/util/crypto.rb b/lib/ably/util/crypto.rb index 06fae0d19..d8202812a 100644 --- a/lib/ably/util/crypto.rb +++ b/lib/ably/util/crypto.rb @@ -30,7 +30,7 @@ class Crypto # crypto.decrypt(decrypted) # => 'secret text' # def initialize(params) - @fixed_iv = params.delete(:fixed_iv) if params.kind_of?(Hash) + @fixed_iv = params[:fixed_iv] @cipher_params = Ably::Models::CipherParams(params) end diff --git a/spec/acceptance/realtime/channel_spec.rb b/spec/acceptance/realtime/channel_spec.rb index 8f437aad6..38953e986 100644 --- a/spec/acceptance/realtime/channel_spec.rb +++ b/spec/acceptance/realtime/channel_spec.rb @@ -117,6 +117,75 @@ def disconnect_transport end end + context 'context when channel options contain modes' do + before do + channel.options = { modes: %i[publish] } + end + + it 'sends an ATTACH with options as flags (#RTL4l)' do + connection.once(:connected) do + client.connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message| + next if protocol_message.action != :attach + + expect(protocol_message.has_attach_publish_flag?).to eq(true) + stop_reactor + end + + channel.attach + end + end + end + + context 'context when channel options contain params' do + let(:params) do + { foo: 'foo', bar: 'bar'} + end + + before do + channel.options = { params: params } + end + + it 'sends an ATTACH with params (#RTL4k)' do + connection.once(:connected) do + client.connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message| + next if protocol_message.action != :attach + + expect(protocol_message.params).to eq(params) + stop_reactor + end + + channel.attach + end + end + end + + context 'when received attached' do + it 'decodes flags and sets it as modes on channel options (#RTL4m)'do + channel.on(:attached) do + expect(channel.options.modes.map(&:to_sym)).to eq(%i[subscribe]) + stop_reactor + end + + channel.transition_state_machine(:attaching) + attached_message = Ably::Models::ProtocolMessage.new(action: 11, channel: channel_name, flags: 262144) + client.connection.__incoming_protocol_msgbus__.publish :protocol_message, attached_message + end + + it 'set params as channel options params (#RTL4k1)' do + params = { param: :something } + + channel.on(:attached) do + expect(channel.params).to eq(channel.options.params) + expect(channel.params).to eq(params) + stop_reactor + end + + channel.transition_state_machine(:attaching) + attached_message = Ably::Models::ProtocolMessage.new(action: 11, channel: channel_name, params: params) + client.connection.__incoming_protocol_msgbus__.publish :protocol_message, attached_message + end + end + it 'implicitly attaches the channel (#RTL7c)' do expect(channel).to be_initialized channel.subscribe { |message| } @@ -1950,6 +2019,64 @@ def fake_error(error) end end + describe '#set_options (#RTL16a)' do + let(:modes) { %i[subscribe] } + let(:channel_options) do + { modes: modes } + end + + shared_examples 'an update that sends ATTACH message' do |state| + it 'sends an ATTACH message on options change' do + attach_sent = nil + + client.connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message| + if protocol_message.action == :attach && protocol_message.flags.nonzero? + attach_sent = true + expect(protocol_message.flags).to eq(262144) + end + end + + channel.once(state) do + channel.options = channel_options + end + + channel.on(:attached) do + client.connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message| + next if protocol_message.action != :attached + + expect(attach_sent).to eq(true) + stop_reactor + end + end + + channel.attach + end + end + + context 'when channel is attaching' do + it_behaves_like 'an update that sends ATTACH message', :attaching + end + + context 'when channel is attaching' do + it_behaves_like 'an update that sends ATTACH message', :attached + end + + context 'when channel is initialized' do + it "doesn't send ATTACH message" do + client.connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message| + raise "Unexpected message" if protocol_message.action == :attach + end + + channel.options = channel_options + expect(channel.options.modes.map(&:to_sym)).to eq(modes) + + EventMachine.next_tick do + stop_reactor + end + end + end + end + context 'channel state change' do it 'emits a ChannelStateChange object' do channel.on(:attached) do |channel_state_change| diff --git a/spec/acceptance/realtime/channels_spec.rb b/spec/acceptance/realtime/channels_spec.rb index 992aeecb3..9d385da7c 100644 --- a/spec/acceptance/realtime/channels_spec.rb +++ b/spec/acceptance/realtime/channels_spec.rb @@ -10,17 +10,56 @@ end it 'returns channel object and passes the provided options' do - expect(channel_with_options.options).to eql(options) + expect(channel_options).to be_a(Ably::Models::ChannelOptions) + expect(channel_options.to_h).to eq(options) stop_reactor end end vary_by_protocol do + let(:client_options) do + { key: api_key, environment: environment, protocol: protocol } + end let(:client) do - auto_close Ably::Realtime::Client.new(key: api_key, environment: environment, protocol: protocol) + auto_close Ably::Realtime::Client.new(client_options) end let(:channel_name) { random_str } - let(:options) { { key: 'value' } } + let(:options) do + { params: { key: 'value' } } + end + + subject(:channel_options) { channel_with_options.options } + + context 'when channel supposed to trigger reattachment per RTL16a (#RTS3c1)' do + it 'will raise an error' do + channel = client.channels.get(channel_name, options) + + channel.on(:attached) do + expect { client.channels.get(channel_name, { modes: [] }) }.to raise_error ArgumentError, /use Channel#set_options directly/ + stop_reactor + end + + channel.attach + end + + context 'params keys are the same but values are different' do + let(:options) do + { params: { x: '1' } } + end + + it 'will raise an error' do + channel = client.channels.get(channel_name, options) + + channel.on(:attached) do + expect { client.channels.get(channel_name, { params: { x: '2' } }) }.to raise_error ArgumentError, /use Channel#set_options directly/ + + stop_reactor + end + + channel.attach + end + end + end describe 'using shortcut method #channel on the client object' do let(:channel) { client.channel(channel_name) } @@ -35,26 +74,39 @@ end describe 'accessing an existing channel object with different options' do + let(:client_options) { super().merge(logger: custom_logger_object) } + let(:custom_logger_object) { TestLogger.new } let(:new_channel_options) { { encrypted: true } } - let(:original_channel) { client.channels.get(channel_name, options) } + let!(:original_channel) { client.channels.get(channel_name, options) } it 'overrides the existing channel options and returns the channel object' do - expect(original_channel.options).to_not include(:encrypted) + expect(original_channel.options.to_h).to_not include(:encrypted) new_channel = client.channels.get(channel_name, new_channel_options) expect(new_channel).to be_a(Ably::Realtime::Channel) expect(new_channel.options[:encrypted]).to eql(true) stop_reactor end + + it 'shows deprecation warning' do + client.channels.get(channel_name, new_channel_options) + + warning = custom_logger_object.logs.find do |severity, message| + message.match(/Using this method to update channel options is deprecated and may be removed/) + end + + expect(warning).to_not be_nil + stop_reactor + end end describe 'accessing an existing channel object without specifying any channel options' do let(:original_channel) { client.channels.get(channel_name, options) } it 'returns the existing channel without modifying the channel options' do - expect(original_channel.options).to eql(options) + expect(original_channel.options.to_h).to eq(options) new_channel = client.channels.get(channel_name) expect(new_channel).to be_a(Ably::Realtime::Channel) - expect(original_channel.options).to eql(options) + expect(original_channel.options.to_h).to eq(options) stop_reactor end end diff --git a/spec/acceptance/rest/channels_spec.rb b/spec/acceptance/rest/channels_spec.rb index 8f82d5bb3..6b6485e6d 100644 --- a/spec/acceptance/rest/channels_spec.rb +++ b/spec/acceptance/rest/channels_spec.rb @@ -5,11 +5,11 @@ shared_examples 'a channel' do it 'returns a channel object' do expect(channel).to be_a Ably::Rest::Channel - expect(channel.name).to eql(channel_name) + expect(channel.name).to eq(channel_name) end it 'returns channel object and passes the provided options' do - expect(channel_with_options.options).to eql(options) + expect(channel_with_options.options.to_h).to eq(options) end end @@ -32,12 +32,29 @@ it_behaves_like 'a channel' end + describe '#set_options (#RTL16)' do + let(:channel) { client.channel(channel_name) } + + it "updates channel's options" do + expect { channel.options = options }.to change { channel.options.to_h }.from({}).to(options) + end + + context 'when providing Ably::Models::ChannelOptions object' do + let(:options_object) { Ably::Models::ChannelOptions.new(options) } + + it "updates channel's options" do + expect { channel.options = options_object}.to change { channel.options.to_h }.from({}).to(options) + end + end + end + describe 'accessing an existing channel object with different options' do let(:new_channel_options) { { encrypted: true } } let(:original_channel) { client.channels.get(channel_name, options) } it 'overrides the existing channel options and returns the channel object (RSN3c)' do - expect(original_channel.options).to_not include(:encrypted) + expect(original_channel.options.to_h).to_not include(:encrypted) + new_channel = client.channels.get(channel_name, new_channel_options) expect(new_channel).to be_a(Ably::Rest::Channel) expect(new_channel.options[:encrypted]).to eql(true) @@ -48,10 +65,10 @@ let(:original_channel) { client.channels.get(channel_name, options) } it 'returns the existing channel without modifying the channel options' do - expect(original_channel.options).to eql(options) + expect(original_channel.options.to_h).to eq(options) new_channel = client.channels.get(channel_name) expect(new_channel).to be_a(Ably::Rest::Channel) - expect(original_channel.options).to eql(options) + expect(original_channel.options.to_h).to eq(options) end end diff --git a/spec/lib/unit/models/channel_options_spec.rb b/spec/lib/unit/models/channel_options_spec.rb new file mode 100644 index 000000000..1d9d16a8d --- /dev/null +++ b/spec/lib/unit/models/channel_options_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ably::Models::ChannelOptions do + let(:modes) { nil } + let(:params) { {} } + let(:options) { described_class.new(modes: modes, params: params) } + + describe '#modes_to_flags' do + let(:modes) { %w[publish subscribe presence_subscribe] } + + subject(:protocol_message) do + Ably::Models::ProtocolMessage.new(action: Ably::Models::ProtocolMessage::ACTION.Attach, flags: options.modes_to_flags) + end + + it 'converts modes to ProtocolMessage#flags correctly' do + expect(protocol_message.has_attach_publish_flag?).to eq(true) + expect(protocol_message.has_attach_subscribe_flag?).to eq(true) + expect(protocol_message.has_attach_presence_subscribe_flag?).to eq(true) + + expect(protocol_message.has_attach_resume_flag?).to eq(false) + expect(protocol_message.has_attach_presence_flag?).to eq(false) + end + end + + describe '#set_modes_from_flags' do + let(:subscribe_flag) { 262144 } + + it 'converts flags to ChannelOptions#modes correctly' do + result = options.set_modes_from_flags(subscribe_flag) + + expect(result).to eq(options.modes) + expect(options.modes.map(&:to_sym)).to eq(%i[subscribe]) + end + end + + describe '#set_params' do + let(:previous_params) { { example_attribute: 1 } } + let(:new_params) { { new_attribute: 1 } } + let(:params) { previous_params } + + it 'should be able to overwrite attributes' do + expect { options.set_params(new_params) }.to \ + change { options.params }.from(previous_params).to(new_params) + end + + it 'should be able to make params empty' do # (1) + expect { options.set_params({}) }.to change { options.params }.from(previous_params).to({}) + end + end +end diff --git a/spec/unit/models/protocol_message_spec.rb b/spec/unit/models/protocol_message_spec.rb index 53c7a20a6..44d6e2fe2 100644 --- a/spec/unit/models/protocol_message_spec.rb +++ b/spec/unit/models/protocol_message_spec.rb @@ -223,6 +223,24 @@ def new_protocol_message(options) end end + context '#params (#RTL4k1)' do + let(:params) do + { foo: :bar } + end + + context 'when present' do + specify do + expect(new_protocol_message({ params: params }).params).to eq(params) + end + end + + context 'when empty' do + specify do + expect(new_protocol_message({}).params).to eq({}) + end + end + end + context '#has_connection_serial?' do context 'without connection_serial' do let(:protocol_message) { new_protocol_message({}) } diff --git a/spec/unit/realtime/channels_spec.rb b/spec/unit/realtime/channels_spec.rb index 6382a9f51..9fbc70b6f 100644 --- a/spec/unit/realtime/channels_spec.rb +++ b/spec/unit/realtime/channels_spec.rb @@ -3,39 +3,77 @@ describe Ably::Realtime::Channels do let(:connection) { instance_double('Ably::Realtime::Connection', unsafe_on: true, on_resume: true) } - let(:client) { instance_double('Ably::Realtime::Client', connection: connection, client_id: 'clientId') } + let(:client) do + instance_double('Ably::Realtime::Client', connection: connection, client_id: 'clientId', logger: double('logger').as_null_object) + end let(:channel_name) { 'unique' } - let(:options) { { 'bizarre' => 'value' } } + let(:options) do + { params: { bizarre: 'value' } } + end subject { Ably::Realtime::Channels.new(client) } context 'creating channels' do context '#get' do - it 'creates a channel if it does not exist (RSN3a)' do - expect(Ably::Realtime::Channel).to receive(:new).with(client, channel_name, options) - subject.get(channel_name, options) + context "when channel doesn't exist" do + shared_examples 'creates a channel' do + it 'creates a channel (RTS3a)' do + expect(Ably::Realtime::Channel).to receive(:new).with(client, channel_name, channel_options) + subject.get(channel_name, channel_options) + end + end + + describe 'hash' do + let(:channel_options) { options } + it { expect(channel_options).to be_a(Hash) } + + include_examples 'creates a channel' + end + + describe 'ChannelOptions object' do + let(:channel_options) { Ably::Models::ChannelOptions.new(options) } + it { expect(channel_options).to be_a(Ably::Models::ChannelOptions) } + + include_examples 'creates a channel' + end end context 'when an existing channel exists' do - it 'will reuse a channel object if it exists (RSN3a)' do - channel = subject.get(channel_name, options) - expect(channel).to be_a(Ably::Realtime::Channel) - expect(subject.get(channel_name, options).object_id).to eql(channel.object_id) + shared_examples 'reuse a channel object if it exists' do + it 'will reuse a channel object if it exists (RTS3a)' do + channel = subject.get(channel_name, channel_options) + expect(channel).to be_a(Ably::Realtime::Channel) + expect(subject.get(channel_name, channel_options).object_id).to eql(channel.object_id) + end + end + + describe 'hash' do + let(:channel_options) { options } + it { expect(channel_options).to be_a(Hash) } + + include_examples 'reuse a channel object if it exists' + end + + describe 'ChannelOptions object' do + let(:channel_options) { Ably::Models::ChannelOptions.new(options) } + it { expect(channel_options).to be_a(Ably::Models::ChannelOptions) } + + include_examples 'reuse a channel object if it exists' end it 'will update the options on the channel if provided (RSN3c)' do channel = subject.get(channel_name, options) - expect(channel.options).to eql(options) - expect(channel.options).to_not include(:encrypted) + expect(channel.options.to_h).to eq(options) + expect(channel.options.to_h).to_not include(:encrypted) subject.get(channel_name, encrypted: true) - expect(channel.options[:encrypted]).to eql(true) + expect(channel.options[:encrypted]).to eq(true) end it 'will leave the options intact on the channel if not provided' do channel = subject.get(channel_name, options) - expect(channel.options).to eql(options) + expect(channel.options.to_h).to eq(options) subject.get(channel_name) - expect(channel.options).to eql(options) + expect(channel.options.to_h).to eq(options) end end end diff --git a/spec/unit/rest/channels_spec.rb b/spec/unit/rest/channels_spec.rb index 1a6221ac2..5b4eba0dd 100644 --- a/spec/unit/rest/channels_spec.rb +++ b/spec/unit/rest/channels_spec.rb @@ -2,30 +2,97 @@ require 'spec_helper' describe Ably::Rest::Channels do - let(:client) { instance_double('Ably::Rest::Client') } + let(:client) { instance_double('Ably::Rest::Client', logger: double('logger').as_null_object) } let(:channel_name) { 'unique'.encode(Encoding::UTF_8) } - let(:options) { { 'bizarre' => 'value' } } + let(:options) do + { params: { 'bizarre' => 'value' } } + end subject { Ably::Rest::Channels.new(client) } - context 'creating channels' do - it '#get creates a channel' do - expect(Ably::Rest::Channel).to receive(:new).with(client, channel_name, options) - subject.get(channel_name, options) - end + describe '#get' do + context "when channel doesn't exist" do + shared_examples 'creates a channel' do + it 'creates a channel (RSN3a)' do + expect(Ably::Rest::Channel).to receive(:new).with(client, channel_name, options) + subject.get(channel_name, options) + end + end - it '#get will reuse the channel object' do - channel = subject.get(channel_name, options) - expect(channel).to be_a(Ably::Rest::Channel) - expect(subject.get(channel_name, options).object_id).to eql(channel.object_id) + describe 'hash' do + let(:channel_options) { options } + it { expect(channel_options).to be_a(Hash) } + + include_examples 'creates a channel' + end + + describe 'ChannelOptions object' do + let(:channel_options) { Ably::Models::ChannelOptions.new(options) } + it { expect(channel_options).to be_a(Ably::Models::ChannelOptions) } + + include_examples 'creates a channel' + end end - it '[] creates a channel' do - expect(Ably::Rest::Channel).to receive(:new).with(client, channel_name, options) - subject.get(channel_name, options) + context 'when an existing channel exists' do + shared_examples 'reuse a channel object if it exists' do + it 'will reuse a channel object if it exists (RSN3a)' do + channel = subject.get(channel_name, channel_options) + expect(channel).to be_a(Ably::Rest::Channel) + expect(subject.get(channel_name, channel_options).object_id).to eql(channel.object_id) + end + end + + describe 'hash' do + let(:channel_options) { options } + it { expect(channel_options).to be_a(Hash) } + + include_examples 'reuse a channel object if it exists' + end + + describe 'ChannelOptions object' do + let(:channel_options) { Ably::Models::ChannelOptions.new(options) } + it { expect(channel_options).to be_a(Ably::Models::ChannelOptions) } + + include_examples 'reuse a channel object if it exists' + end + + context 'with new channel_options modes' do + shared_examples 'update channel with provided options :modes' do + it 'will update channel with provided options modes (RSN3c)' do + channel = subject.get(channel_name, channel_options) + expect(channel.options.modes).to eq(modes) + + subject.get(channel_name, channel_options) + expect(channel.options.modes).to eq(modes) + end + end + + let(:modes) { %i[subscribe] } + let(:new_options) { { modes: modes } } + + describe 'hash' do + let(:channel_options) { new_options } + it { expect(channel_options).to be_a(Hash) } + + include_examples 'update channel with provided options :modes' + end + + describe 'ChannelOptions object' do + let(:channel_options) { Ably::Models::ChannelOptions.new(new_options) } + it { expect(channel_options).to be_a(Ably::Models::ChannelOptions) } + + include_examples 'update channel with provided options :modes' + end + end end end + it '[] creates a channel' do + expect(Ably::Rest::Channel).to receive(:new).with(client, channel_name, options) + subject.get(channel_name, options) + end + context '#fetch' do it 'retrieves a channel if it exists' do channel = subject.get(channel_name, options)