ably 0.8.2 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/CHANGELOG.md +185 -0
- data/LICENSE +15 -0
- data/README.md +8 -4
- data/SPEC.md +999 -531
- data/ably.gemspec +1 -1
- data/lib/ably.rb +1 -1
- data/lib/ably/auth.rb +114 -87
- data/lib/ably/exceptions.rb +40 -14
- data/lib/ably/models/message.rb +3 -5
- data/lib/ably/models/paginated_result.rb +3 -12
- data/lib/ably/models/presence_message.rb +8 -2
- data/lib/ably/models/protocol_message.rb +15 -3
- data/lib/ably/models/stat.rb +1 -1
- data/lib/ably/models/token_details.rb +1 -1
- data/lib/ably/modules/channels_collection.rb +7 -1
- data/lib/ably/modules/conversions.rb +1 -1
- data/lib/ably/modules/encodeable.rb +6 -3
- data/lib/ably/modules/message_pack.rb +2 -2
- data/lib/ably/modules/model_common.rb +1 -1
- data/lib/ably/modules/state_machine.rb +2 -2
- data/lib/ably/realtime.rb +1 -0
- data/lib/ably/realtime/auth.rb +191 -0
- data/lib/ably/realtime/channel.rb +97 -25
- data/lib/ably/realtime/channel/channel_manager.rb +11 -3
- data/lib/ably/realtime/client.rb +22 -6
- data/lib/ably/realtime/connection.rb +74 -41
- data/lib/ably/realtime/connection/connection_manager.rb +48 -33
- data/lib/ably/realtime/presence.rb +17 -3
- data/lib/ably/rest/channel.rb +43 -16
- data/lib/ably/rest/client.rb +57 -26
- data/lib/ably/rest/middleware/exceptions.rb +3 -1
- data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +4 -2
- data/lib/ably/rest/presence.rb +1 -0
- data/lib/ably/version.rb +1 -1
- data/spec/acceptance/realtime/auth_spec.rb +242 -0
- data/spec/acceptance/realtime/channel_spec.rb +277 -5
- data/spec/acceptance/realtime/channels_spec.rb +64 -0
- data/spec/acceptance/realtime/client_spec.rb +26 -5
- data/spec/acceptance/realtime/connection_failures_spec.rb +23 -6
- data/spec/acceptance/realtime/connection_spec.rb +167 -16
- data/spec/acceptance/realtime/message_spec.rb +9 -8
- data/spec/acceptance/realtime/presence_history_spec.rb +1 -0
- data/spec/acceptance/realtime/presence_spec.rb +121 -10
- data/spec/acceptance/realtime/stats_spec.rb +13 -1
- data/spec/acceptance/rest/auth_spec.rb +161 -79
- data/spec/acceptance/rest/base_spec.rb +3 -3
- data/spec/acceptance/rest/channel_spec.rb +142 -15
- data/spec/acceptance/rest/channels_spec.rb +23 -0
- data/spec/acceptance/rest/client_spec.rb +180 -18
- data/spec/acceptance/rest/message_spec.rb +8 -8
- data/spec/acceptance/rest/presence_spec.rb +136 -25
- data/spec/acceptance/rest/stats_spec.rb +60 -4
- data/spec/shared/client_initializer_behaviour.rb +54 -3
- data/spec/unit/auth_spec.rb +7 -6
- data/spec/unit/models/message_spec.rb +1 -9
- data/spec/unit/models/paginated_result_spec.rb +1 -18
- data/spec/unit/models/presence_message_spec.rb +1 -1
- data/spec/unit/models/protocol_message_spec.rb +21 -1
- data/spec/unit/realtime/channel_spec.rb +10 -3
- data/spec/unit/realtime/channels_spec.rb +27 -8
- data/spec/unit/rest/channel_spec.rb +0 -8
- data/spec/unit/rest/client_spec.rb +7 -7
- metadata +13 -7
- data/LICENSE.txt +0 -22
| @@ -130,7 +130,7 @@ describe Ably::Rest do | |
| 130 130 | 
             
                      if [1, 3].include?(@publish_attempts)
         | 
| 131 131 | 
             
                        { status: 201, :body => '[]', :headers => { 'Content-Type' => 'application/json' } }
         | 
| 132 132 | 
             
                      else
         | 
| 133 | 
            -
                        raise Ably::Exceptions:: | 
| 133 | 
            +
                        raise Ably::Exceptions::TokenExpired.new('Authentication failure', 401, 40140)
         | 
| 134 134 | 
             
                      end
         | 
| 135 135 | 
             
                    end
         | 
| 136 136 | 
             
                  end
         | 
| @@ -155,10 +155,10 @@ describe Ably::Rest do | |
| 155 155 | 
             
                  context 'when NOT auth#token_renewable?' do
         | 
| 156 156 | 
             
                    let(:client) { Ably::Rest::Client.new(token: 'token ID cannot be used to create a new token', environment: environment, protocol: protocol) }
         | 
| 157 157 |  | 
| 158 | 
            -
                    it 'should raise an  | 
| 158 | 
            +
                    it 'should raise an TokenExpired exception' do
         | 
| 159 159 | 
             
                      client.channel(channel).publish('evt', 'msg')
         | 
| 160 160 | 
             
                      expect(@publish_attempts).to eql(1)
         | 
| 161 | 
            -
                      expect { client.channel(channel).publish('evt', 'msg') }.to raise_error Ably::Exceptions:: | 
| 161 | 
            +
                      expect { client.channel(channel).publish('evt', 'msg') }.to raise_error Ably::Exceptions::TokenExpired
         | 
| 162 162 | 
             
                      expect(@token_requests).to eql(0)
         | 
| 163 163 | 
             
                    end
         | 
| 164 164 | 
             
                  end
         | 
| @@ -5,17 +5,97 @@ describe Ably::Rest::Channel do | |
| 5 5 | 
             
              include Ably::Modules::Conversions
         | 
| 6 6 |  | 
| 7 7 | 
             
              vary_by_protocol do
         | 
| 8 | 
            +
                let(:default_options) { { key: api_key, environment: environment, protocol: protocol} }
         | 
| 9 | 
            +
                let(:client_options)  { default_options }
         | 
| 8 10 | 
             
                let(:client) do
         | 
| 9 | 
            -
                  Ably::Rest::Client.new( | 
| 11 | 
            +
                  Ably::Rest::Client.new(client_options)
         | 
| 10 12 | 
             
                end
         | 
| 11 13 |  | 
| 12 14 | 
             
                describe '#publish' do
         | 
| 13 | 
            -
                  let(:channel) { client.channel( | 
| 14 | 
            -
                  let(: | 
| 15 | 
            -
                  let(: | 
| 15 | 
            +
                  let(:channel) { client.channel(random_str) }
         | 
| 16 | 
            +
                  let(:name)   { 'foo' }
         | 
| 17 | 
            +
                  let(:data)    { 'woop!' }
         | 
| 16 18 |  | 
| 17 | 
            -
                   | 
| 18 | 
            -
                     | 
| 19 | 
            +
                  context 'with name and data arguments' do
         | 
| 20 | 
            +
                    it 'publishes the message and return true indicating success' do
         | 
| 21 | 
            +
                      expect(channel.publish(name, data)).to eql(true)
         | 
| 22 | 
            +
                      expect(channel.history.items.first.name).to eql(name)
         | 
| 23 | 
            +
                      expect(channel.history.items.first.data).to eql(data)
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  context 'with an array of Hash objects with :name and :data attributes' do
         | 
| 28 | 
            +
                    let(:messages) do
         | 
| 29 | 
            +
                      10.times.map do |index|
         | 
| 30 | 
            +
                        { name: index.to_s, data: { "index" => index + 10 } }
         | 
| 31 | 
            +
                      end
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                    it 'publishes an array of messages in one HTTP request' do
         | 
| 35 | 
            +
                      expect(client).to receive(:post).once.and_call_original
         | 
| 36 | 
            +
                      expect(channel.publish(messages)).to eql(true)
         | 
| 37 | 
            +
                      expect(channel.history.items.map(&:name)).to match_array(messages.map { |message| message[:name] })
         | 
| 38 | 
            +
                      expect(channel.history.items.map(&:data)).to match_array(messages.map { |message| message[:data] })
         | 
| 39 | 
            +
                    end
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  context 'with an array of Message objects' do
         | 
| 43 | 
            +
                    let(:messages) do
         | 
| 44 | 
            +
                      10.times.map do |index|
         | 
| 45 | 
            +
                        Ably::Models::Message(name: index.to_s, data: { "index" => index + 10 })
         | 
| 46 | 
            +
                      end
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    it 'publishes an array of messages in one HTTP request' do
         | 
| 50 | 
            +
                      expect(client).to receive(:post).once.and_call_original
         | 
| 51 | 
            +
                      expect(channel.publish(messages)).to eql(true)
         | 
| 52 | 
            +
                      expect(channel.history.items.map(&:name)).to match_array(messages.map(&:name))
         | 
| 53 | 
            +
                      expect(channel.history.items.map(&:data)).to match_array(messages.map(&:data))
         | 
| 54 | 
            +
                    end
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                  context 'without adequate permissions on the channel' do
         | 
| 58 | 
            +
                    let(:capability)     { { onlyChannel: ['subscribe'] } }
         | 
| 59 | 
            +
                    let(:client_options) { default_options.merge(use_token_auth: true, token_params: { capability: capability }) }
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                    it 'raises a permission error when publishing' do
         | 
| 62 | 
            +
                      expect { channel.publish(name, data) }.to raise_error(Ably::Exceptions::InvalidRequest, /not permitted/)
         | 
| 63 | 
            +
                    end
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  context 'null attributes' do
         | 
| 67 | 
            +
                    context 'when name is null' do
         | 
| 68 | 
            +
                      let(:data) { random_str }
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                      it 'publishes the message without a name attribute in the payload' do
         | 
| 71 | 
            +
                        expect(client).to receive(:post).with(anything, { "data" => data }).once.and_call_original
         | 
| 72 | 
            +
                        expect(channel.publish(nil, data)).to eql(true)
         | 
| 73 | 
            +
                        expect(channel.history.items.first.name).to be_nil
         | 
| 74 | 
            +
                        expect(channel.history.items.first.data).to eql(data)
         | 
| 75 | 
            +
                      end
         | 
| 76 | 
            +
                    end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                    context 'when data is null' do
         | 
| 79 | 
            +
                      let(:name) { random_str }
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                      it 'publishes the message without a data attribute in the payload' do
         | 
| 82 | 
            +
                        expect(client).to receive(:post).with(anything, { "name" => name }).once.and_call_original
         | 
| 83 | 
            +
                        expect(channel.publish(name)).to eql(true)
         | 
| 84 | 
            +
                        expect(channel.history.items.first.name).to eql(name)
         | 
| 85 | 
            +
                        expect(channel.history.items.first.data).to be_nil
         | 
| 86 | 
            +
                      end
         | 
| 87 | 
            +
                    end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                    context 'with neither name or data attributes' do
         | 
| 90 | 
            +
                      let(:name) { random_str }
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                      it 'publishes the message without any attributes in the payload' do
         | 
| 93 | 
            +
                        expect(client).to receive(:post).with(anything, {}).once.and_call_original
         | 
| 94 | 
            +
                        expect(channel.publish(nil)).to eql(true)
         | 
| 95 | 
            +
                        expect(channel.history.items.first.name).to be_nil
         | 
| 96 | 
            +
                        expect(channel.history.items.first.data).to be_nil
         | 
| 97 | 
            +
                      end
         | 
| 98 | 
            +
                    end
         | 
| 19 99 | 
             
                  end
         | 
| 20 100 | 
             
                end
         | 
| 21 101 |  | 
| @@ -36,7 +116,11 @@ describe Ably::Rest::Channel do | |
| 36 116 | 
             
                    end
         | 
| 37 117 | 
             
                  end
         | 
| 38 118 |  | 
| 39 | 
            -
                  it ' | 
| 119 | 
            +
                  it 'returns a PaginatedResult model' do
         | 
| 120 | 
            +
                    expect(channel.history).to be_a(Ably::Models::PaginatedResult)
         | 
| 121 | 
            +
                  end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                  it 'returns the current message history for the channel' do
         | 
| 40 124 | 
             
                    actual_history_items = channel.history.items
         | 
| 41 125 |  | 
| 42 126 | 
             
                    expect(actual_history_items.size).to eql(3)
         | 
| @@ -49,7 +133,7 @@ describe Ably::Rest::Channel do | |
| 49 133 | 
             
                  end
         | 
| 50 134 |  | 
| 51 135 | 
             
                  context 'message timestamps' do
         | 
| 52 | 
            -
                     | 
| 136 | 
            +
                    specify 'are after the messages were published' do
         | 
| 53 137 | 
             
                      channel.history.items.each do |message|
         | 
| 54 138 | 
             
                        expect(before_published.to_f).to be < message.timestamp.to_f
         | 
| 55 139 | 
             
                      end
         | 
| @@ -57,14 +141,14 @@ describe Ably::Rest::Channel do | |
| 57 141 | 
             
                  end
         | 
| 58 142 |  | 
| 59 143 | 
             
                  context 'message IDs' do
         | 
| 60 | 
            -
                    it ' | 
| 144 | 
            +
                    it 'is unique' do
         | 
| 61 145 | 
             
                      message_ids = channel.history.items.map(&:id).compact
         | 
| 62 146 | 
             
                      expect(message_ids.count).to eql(3)
         | 
| 63 147 | 
             
                      expect(message_ids.uniq.count).to eql(3)
         | 
| 64 148 | 
             
                    end
         | 
| 65 149 | 
             
                  end
         | 
| 66 150 |  | 
| 67 | 
            -
                  it ' | 
| 151 | 
            +
                  it 'returns paged history using the PaginatedResult model' do
         | 
| 68 152 | 
             
                    page_1 = channel.history(limit: 1)
         | 
| 69 153 | 
             
                    page_2 = page_1.next
         | 
| 70 154 | 
             
                    page_3 = page_2.next
         | 
| @@ -74,17 +158,42 @@ describe Ably::Rest::Channel do | |
| 74 158 |  | 
| 75 159 | 
             
                    expect(page_1.items.size).to eql(1)
         | 
| 76 160 | 
             
                    expect(page_1).to_not be_last
         | 
| 77 | 
            -
                    expect(page_1).to be_first
         | 
| 78 161 |  | 
| 79 162 | 
             
                    # Page 2
         | 
| 80 163 | 
             
                    expect(page_2.items.size).to eql(1)
         | 
| 81 164 | 
             
                    expect(page_2).to_not be_last
         | 
| 82 | 
            -
                    expect(page_2).to_not be_first
         | 
| 83 165 |  | 
| 84 166 | 
             
                    # Page 3
         | 
| 85 167 | 
             
                    expect(page_3.items.size).to eql(1)
         | 
| 86 168 | 
             
                    expect(page_3).to be_last
         | 
| 87 | 
            -
             | 
| 169 | 
            +
                  end
         | 
| 170 | 
            +
             | 
| 171 | 
            +
                  context 'direction' do
         | 
| 172 | 
            +
                    it 'returns paged history backwards by default' do
         | 
| 173 | 
            +
                      items = channel.history.items
         | 
| 174 | 
            +
                      expect(items.first.name).to eql(expected_history.last.fetch(:name))
         | 
| 175 | 
            +
                      expect(items.last.name).to eql(expected_history.first.fetch(:name))
         | 
| 176 | 
            +
                    end
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                    it 'returns history forward if specified in the options' do
         | 
| 179 | 
            +
                      items = channel.history(direction: :forwards).items
         | 
| 180 | 
            +
                      expect(items.first.name).to eql(expected_history.first.fetch(:name))
         | 
| 181 | 
            +
                      expect(items.last.name).to eql(expected_history.last.fetch(:name))
         | 
| 182 | 
            +
                    end
         | 
| 183 | 
            +
                  end
         | 
| 184 | 
            +
             | 
| 185 | 
            +
                  context 'limit' do
         | 
| 186 | 
            +
                    before do
         | 
| 187 | 
            +
                      channel.publish 120.times.to_a.map { |i| { name: 'event' } }
         | 
| 188 | 
            +
                    end
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                    it 'defaults to 100' do
         | 
| 191 | 
            +
                      page = channel.history
         | 
| 192 | 
            +
                      expect(page.items.count).to eql(100)
         | 
| 193 | 
            +
                      next_page = page.next
         | 
| 194 | 
            +
                      expect(next_page.items.count).to be >= 20
         | 
| 195 | 
            +
                      expect(next_page.items.count).to be < 100
         | 
| 196 | 
            +
                    end
         | 
| 88 197 | 
             
                  end
         | 
| 89 198 | 
             
                end
         | 
| 90 199 |  | 
| @@ -97,7 +206,7 @@ describe Ably::Rest::Channel do | |
| 97 206 | 
             
                      client_end_point.password = key_secret
         | 
| 98 207 | 
             
                    end
         | 
| 99 208 | 
             
                  end
         | 
| 100 | 
            -
                  let(: | 
| 209 | 
            +
                  let(:default_history_options) do
         | 
| 101 210 | 
             
                      {
         | 
| 102 211 | 
             
                        direction: :backwards,
         | 
| 103 212 | 
             
                        limit: 100
         | 
| @@ -107,7 +216,8 @@ describe Ably::Rest::Channel do | |
| 107 216 | 
             
                  [:start, :end].each do |option|
         | 
| 108 217 | 
             
                    describe ":#{option}", :webmock do
         | 
| 109 218 | 
             
                      let!(:history_stub) {
         | 
| 110 | 
            -
                        query_params =  | 
| 219 | 
            +
                        query_params = default_history_options
         | 
| 220 | 
            +
                        .merge(option => milliseconds).map { |k, v| "#{k}=#{v}" }.join('&')
         | 
| 111 221 | 
             
                        stub_request(:get, "#{endpoint}/channels/#{CGI.escape(channel_name)}/messages?#{query_params}").
         | 
| 112 222 | 
             
                          to_return(:body => '{}', :headers => { 'Content-Type' => 'application/json' })
         | 
| 113 223 | 
             
                      }
         | 
| @@ -136,6 +246,23 @@ describe Ably::Rest::Channel do | |
| 136 246 | 
             
                      end
         | 
| 137 247 | 
             
                    end
         | 
| 138 248 | 
             
                  end
         | 
| 249 | 
            +
             | 
| 250 | 
            +
                  context 'when argument start is after end' do
         | 
| 251 | 
            +
                    let(:subject) { channel.history(start: as_since_epoch(Time.now), end: Time.now - 120) }
         | 
| 252 | 
            +
             | 
| 253 | 
            +
                    it 'should raise an exception' do
         | 
| 254 | 
            +
                      expect { subject.items }.to raise_error ArgumentError
         | 
| 255 | 
            +
                    end
         | 
| 256 | 
            +
                  end
         | 
| 257 | 
            +
                end
         | 
| 258 | 
            +
             | 
| 259 | 
            +
                context '#presence' do
         | 
| 260 | 
            +
                  let(:channel_name) { "persisted:#{random_str(4)}" }
         | 
| 261 | 
            +
                  let(:channel) { client.channel(channel_name) }
         | 
| 262 | 
            +
             | 
| 263 | 
            +
                  it 'returns a REST Presence object' do
         | 
| 264 | 
            +
                    expect(channel.presence).to be_a(Ably::Rest::Presence)
         | 
| 265 | 
            +
                  end
         | 
| 139 266 | 
             
                end
         | 
| 140 267 | 
             
              end
         | 
| 141 268 | 
             
            end
         | 
| @@ -32,6 +32,29 @@ describe Ably::Rest::Channels do | |
| 32 32 | 
             
                  it_behaves_like 'a channel'
         | 
| 33 33 | 
             
                end
         | 
| 34 34 |  | 
| 35 | 
            +
                describe 'accessing an existing channel object with different options' do
         | 
| 36 | 
            +
                  let(:new_channel_options) { { encrypted: true } }
         | 
| 37 | 
            +
                  let(:original_channel)    { client.channels.get(channel_name, options) }
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  it 'overrides the existing channel options and returns the channel object' do
         | 
| 40 | 
            +
                    expect(original_channel.options).to_not include(:encrypted)
         | 
| 41 | 
            +
                    new_channel = client.channels.get(channel_name, new_channel_options)
         | 
| 42 | 
            +
                    expect(new_channel).to be_a(Ably::Rest::Channel)
         | 
| 43 | 
            +
                    expect(new_channel.options[:encrypted]).to eql(true)
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                describe 'accessing an existing channel object without specifying any channel options' do
         | 
| 48 | 
            +
                  let(:original_channel) { client.channels.get(channel_name, options) }
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  it 'returns the existing channel without modifying the channel options' do
         | 
| 51 | 
            +
                    expect(original_channel.options).to eql(options)
         | 
| 52 | 
            +
                    new_channel = client.channels.get(channel_name)
         | 
| 53 | 
            +
                    expect(new_channel).to be_a(Ably::Rest::Channel)
         | 
| 54 | 
            +
                    expect(original_channel.options).to eql(options)
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 35 58 | 
             
                describe 'using undocumented array accessor [] method on client#channels' do
         | 
| 36 59 | 
             
                  let(:channel) { client.channels[channel_name] }
         | 
| 37 60 | 
             
                  let(:channel_with_options) { client.channels[channel_name, options] }
         | 
| @@ -10,30 +10,128 @@ describe Ably::Rest::Client do | |
| 10 10 |  | 
| 11 11 | 
             
                connection_retry = Ably::Rest::Client::CONNECTION_RETRY
         | 
| 12 12 |  | 
| 13 | 
            +
                def encode64(text)
         | 
| 14 | 
            +
                  Base64.encode64(text).gsub("\n", '')
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 13 17 | 
             
                context '#initialize' do
         | 
| 14 18 | 
             
                  let(:client_id)     { random_str }
         | 
| 15 19 | 
             
                  let(:token_request) { client.auth.create_token_request(key_name: key_name, key_secret: key_secret, client_id: client_id) }
         | 
| 16 20 |  | 
| 17 | 
            -
                  context 'with  | 
| 21 | 
            +
                  context 'with only an API key' do
         | 
| 22 | 
            +
                    let(:client) { Ably::Rest::Client.new(client_options.merge(key: api_key)) }
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    it 'uses basic authentication' do
         | 
| 25 | 
            +
                      expect(client.auth).to be_using_basic_auth
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  context 'with an explicit string :token' do
         | 
| 30 | 
            +
                    let(:client) { Ably::Rest::Client.new(client_options.merge(token: random_str)) }
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                    it 'uses token authentication' do
         | 
| 33 | 
            +
                      expect(client.auth).to be_using_token_auth
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  context 'with :use_token_auth set to true' do
         | 
| 38 | 
            +
                    let(:client) { Ably::Rest::Client.new(client_options.merge(key: api_key, use_token_auth: true)) }
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    it 'uses token authentication' do
         | 
| 41 | 
            +
                      expect(client.auth).to be_using_token_auth
         | 
| 42 | 
            +
                    end
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  context 'with a :client_id configured' do
         | 
| 46 | 
            +
                    let(:client) { Ably::Rest::Client.new(client_options.merge(key: api_key, client_id: random_str)) }
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    it 'uses token authentication' do
         | 
| 49 | 
            +
                      expect(client.auth).to be_using_token_auth
         | 
| 50 | 
            +
                    end
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  context 'with an :auth_callback Proc' do
         | 
| 18 54 | 
             
                    let(:client) { Ably::Rest::Client.new(client_options.merge(auth_callback: Proc.new { token_request })) }
         | 
| 19 55 |  | 
| 20 56 | 
             
                    it 'calls the auth Proc to get a new token' do
         | 
| 21 57 | 
             
                      expect { client.channel('channel_name').publish('event', 'message') }.to change { client.auth.current_token_details }
         | 
| 22 58 | 
             
                      expect(client.auth.current_token_details.client_id).to eql(client_id)
         | 
| 23 59 | 
             
                    end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                    it 'uses token authentication' do
         | 
| 62 | 
            +
                      expect(client.auth).to be_using_token_auth
         | 
| 63 | 
            +
                    end
         | 
| 24 64 | 
             
                  end
         | 
| 25 65 |  | 
| 26 66 | 
             
                  context 'with an auth URL' do
         | 
| 27 | 
            -
                    let(:client_options) | 
| 67 | 
            +
                    let(:client_options)    { default_options.merge(key: api_key, auth_url: token_request_url, auth_method: :get) }
         | 
| 28 68 | 
             
                    let(:token_request_url) { 'http://get.token.request.com/' }
         | 
| 29 69 |  | 
| 30 | 
            -
                     | 
| 31 | 
            -
                       | 
| 70 | 
            +
                    it 'uses token authentication' do
         | 
| 71 | 
            +
                      expect(client.auth).to be_using_token_auth
         | 
| 32 72 | 
             
                    end
         | 
| 33 73 |  | 
| 34 | 
            -
                     | 
| 35 | 
            -
                       | 
| 36 | 
            -
             | 
| 74 | 
            +
                    context 'before any REST request' do
         | 
| 75 | 
            +
                      before do
         | 
| 76 | 
            +
                        expect(client.auth).to receive(:token_request_from_auth_url).with(token_request_url, hash_including(:auth_method => :get)).once do
         | 
| 77 | 
            +
                          client.auth.create_token_request(token_params: { client_id: client_id })
         | 
| 78 | 
            +
                        end
         | 
| 79 | 
            +
                      end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                      it 'sends an HTTP request to the provided auth URL to get a new token' do
         | 
| 82 | 
            +
                        expect { client.channel('channel_name').publish('event', 'message') }.to change { client.auth.current_token_details }
         | 
| 83 | 
            +
                        expect(client.auth.current_token_details.client_id).to eql(client_id)
         | 
| 84 | 
            +
                      end
         | 
| 85 | 
            +
                    end
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  context 'auth headers', webmock: true do
         | 
| 89 | 
            +
                    let(:channel_name)        { random_str }
         | 
| 90 | 
            +
                    let(:history_params)      { { 'direction' => 'backwards', 'limit' => 100 } }
         | 
| 91 | 
            +
                    let(:history_querystring) { history_params.map { |k, v| "#{k}=#{v}" }.join("&") }
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                    context 'with basic auth', webmock: true do
         | 
| 94 | 
            +
                      let(:client_options)      { default_options.merge(key: api_key) }
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                      let!(:get_message_history_stub) do
         | 
| 97 | 
            +
                        stub_request(:get, "https://#{api_key}@#{environment}-#{Ably::Rest::Client::DOMAIN}/channels/#{channel_name}/messages?#{history_querystring}").
         | 
| 98 | 
            +
                          to_return(body: [], headers: { 'Content-Type' => 'application/json' })
         | 
| 99 | 
            +
                      end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                      it 'sends the API key in authentication part of the secure URL (the Authorization: Basic header is not used with the Faraday HTTP library by default)' do
         | 
| 102 | 
            +
                        client.channel(channel_name).history history_params
         | 
| 103 | 
            +
                        expect(get_message_history_stub).to have_been_requested
         | 
| 104 | 
            +
                      end
         | 
| 105 | 
            +
                    end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                    context 'with token auth', webmock: true do
         | 
| 108 | 
            +
                      let(:token_string)   { random_str }
         | 
| 109 | 
            +
                      let(:client_options) { default_options.merge(token: token_string) }
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                      let!(:get_message_history_stub) do
         | 
| 112 | 
            +
                        stub_request(:get, "#{http_protocol}://#{environment}-#{Ably::Rest::Client::DOMAIN}/channels/#{channel_name}/messages?#{history_querystring}").
         | 
| 113 | 
            +
                          with(headers: { 'Authorization' => "Bearer #{encode64(token_string)}" }).
         | 
| 114 | 
            +
                          to_return(body: [], headers: { 'Content-Type' => 'application/json' })
         | 
| 115 | 
            +
                      end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                      context 'without specifying protocol' do
         | 
| 118 | 
            +
                        let(:http_protocol) { 'https' }
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                        it 'sends the token string over HTTPS in the Authorization Bearer header with Base64 encoding' do
         | 
| 121 | 
            +
                          client.channel(channel_name).history history_params
         | 
| 122 | 
            +
                          expect(get_message_history_stub).to have_been_requested
         | 
| 123 | 
            +
                        end
         | 
| 124 | 
            +
                      end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                      context 'when setting constructor ClientOption :tls to false' do
         | 
| 127 | 
            +
                        let(:client_options) { default_options.merge(token: token_string, tls: false) }
         | 
| 128 | 
            +
                        let(:http_protocol)  { 'http' }
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                        it 'sends the token string over HTTP in the Authorization Bearer header with Base64 encoding' do
         | 
| 131 | 
            +
                          client.channel(channel_name).history history_params
         | 
| 132 | 
            +
                          expect(get_message_history_stub).to have_been_requested
         | 
| 133 | 
            +
                        end
         | 
| 134 | 
            +
                      end
         | 
| 37 135 | 
             
                    end
         | 
| 38 136 | 
             
                  end
         | 
| 39 137 | 
             
                end
         | 
| @@ -53,7 +151,12 @@ describe Ably::Rest::Client do | |
| 53 151 | 
             
                  let(:token_request_next) { client.auth.create_token_request(token_request_options.merge(client_id: random_str)) }
         | 
| 54 152 |  | 
| 55 153 | 
             
                  context 'when expired' do
         | 
| 56 | 
            -
                     | 
| 154 | 
            +
                    before do
         | 
| 155 | 
            +
                      # Ensure tokens issued expire immediately after issue
         | 
| 156 | 
            +
                      stub_const 'Ably::Auth::TOKEN_DEFAULTS', Ably::Auth::TOKEN_DEFAULTS.merge(renew_token_buffer: 0)
         | 
| 157 | 
            +
                    end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                    let(:token_request_options) { { key_name: key_name, key_secret: key_secret, token_params: { ttl: Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER } } }
         | 
| 57 160 |  | 
| 58 161 | 
             
                    it 'creates a new token automatically when the old token expires' do
         | 
| 59 162 | 
             
                      expect { client.channel('channel_name').publish('event', 'message') }.to change { client.auth.current_token_details }
         | 
| @@ -130,7 +233,7 @@ describe Ably::Rest::Client do | |
| 130 233 | 
             
                    end
         | 
| 131 234 |  | 
| 132 235 | 
             
                    it 'does not retry failed requests with fallback hosts when there is a connection error' do
         | 
| 133 | 
            -
                      expect { publish_block.call }.to raise_error Ably::Exceptions:: | 
| 236 | 
            +
                      expect { publish_block.call }.to raise_error Ably::Exceptions::ConnectionTimeout
         | 
| 134 237 | 
             
                    end
         | 
| 135 238 | 
             
                  end
         | 
| 136 239 |  | 
| @@ -139,6 +242,7 @@ describe Ably::Rest::Client do | |
| 139 242 | 
             
                    let(:max_attempts)       { 2 }
         | 
| 140 243 | 
             
                    let(:cumulative_timeout) { 0.5 }
         | 
| 141 244 | 
             
                    let(:client_options)     { default_options.merge(environment: nil, key: api_key) }
         | 
| 245 | 
            +
                    let(:fallback_block)     { Proc.new { raise Faraday::SSLError.new('ssl error message') } }
         | 
| 142 246 |  | 
| 143 247 | 
             
                    before do
         | 
| 144 248 | 
             
                      stub_const 'Ably::FALLBACK_HOSTS', custom_hosts
         | 
| @@ -151,15 +255,11 @@ describe Ably::Rest::Client do | |
| 151 255 | 
             
                    end
         | 
| 152 256 |  | 
| 153 257 | 
             
                    let!(:first_fallback_request_stub) do
         | 
| 154 | 
            -
                      stub_request(:post, "https://#{api_key}@#{custom_hosts[0]}#{path}").to_return | 
| 155 | 
            -
                        raise Faraday::SSLError.new('ssl error message')
         | 
| 156 | 
            -
                      end
         | 
| 258 | 
            +
                      stub_request(:post, "https://#{api_key}@#{custom_hosts[0]}#{path}").to_return(&fallback_block)
         | 
| 157 259 | 
             
                    end
         | 
| 158 260 |  | 
| 159 261 | 
             
                    let!(:second_fallback_request_stub) do
         | 
| 160 | 
            -
                      stub_request(:post, "https://#{api_key}@#{custom_hosts[1]}#{path}").to_return | 
| 161 | 
            -
                        raise Faraday::SSLError.new('ssl error message')
         | 
| 162 | 
            -
                      end
         | 
| 262 | 
            +
                      stub_request(:post, "https://#{api_key}@#{custom_hosts[1]}#{path}").to_return(&fallback_block)
         | 
| 163 263 | 
             
                    end
         | 
| 164 264 |  | 
| 165 265 | 
             
                    context 'and connection times out' do
         | 
| @@ -185,7 +285,7 @@ describe Ably::Rest::Client do | |
| 185 285 | 
             
                        end
         | 
| 186 286 |  | 
| 187 287 | 
             
                        it 'makes no further attempts to any fallback hosts' do
         | 
| 188 | 
            -
                          expect { publish_block.call }.to raise_error Ably::Exceptions:: | 
| 288 | 
            +
                          expect { publish_block.call }.to raise_error Ably::Exceptions::ConnectionTimeout
         | 
| 189 289 | 
             
                          expect(default_host_request_stub).to have_been_requested
         | 
| 190 290 | 
             
                          expect(first_fallback_request_stub).to_not have_been_requested
         | 
| 191 291 | 
             
                          expect(second_fallback_request_stub).to_not have_been_requested
         | 
| @@ -207,6 +307,52 @@ describe Ably::Rest::Client do | |
| 207 307 | 
             
                        expect(second_fallback_request_stub).to have_been_requested
         | 
| 208 308 | 
             
                      end
         | 
| 209 309 | 
             
                    end
         | 
| 310 | 
            +
             | 
| 311 | 
            +
                    context 'and basic authentication fails' do
         | 
| 312 | 
            +
                      let(:status) { 401 }
         | 
| 313 | 
            +
                      let!(:default_host_request_stub) do
         | 
| 314 | 
            +
                        stub_request(:post, "https://#{api_key}@#{Ably::Rest::Client::DOMAIN}#{path}").to_return(
         | 
| 315 | 
            +
                          headers: { 'Content-Type' => 'application/json' },
         | 
| 316 | 
            +
                          status: status,
         | 
| 317 | 
            +
                          body: {
         | 
| 318 | 
            +
            	              "error" => {
         | 
| 319 | 
            +
            		              "statusCode" => 401,
         | 
| 320 | 
            +
            		              "code" => 40101,
         | 
| 321 | 
            +
            		              "message" => "Invalid credentials"
         | 
| 322 | 
            +
            	              }
         | 
| 323 | 
            +
                          }.to_json
         | 
| 324 | 
            +
                        )
         | 
| 325 | 
            +
                      end
         | 
| 326 | 
            +
             | 
| 327 | 
            +
                      it 'does not attempt the fallback hosts as this is an authentication failure' do
         | 
| 328 | 
            +
                        expect { publish_block.call }.to raise_error(Ably::Exceptions::InvalidRequest)
         | 
| 329 | 
            +
                        expect(default_host_request_stub).to have_been_requested
         | 
| 330 | 
            +
                        expect(first_fallback_request_stub).to_not have_been_requested
         | 
| 331 | 
            +
                        expect(second_fallback_request_stub).to_not have_been_requested
         | 
| 332 | 
            +
                      end
         | 
| 333 | 
            +
                    end
         | 
| 334 | 
            +
             | 
| 335 | 
            +
                    context 'and server returns a 50x error' do
         | 
| 336 | 
            +
                      let(:status) { 502 }
         | 
| 337 | 
            +
                      let(:fallback_block) do
         | 
| 338 | 
            +
                        Proc.new do
         | 
| 339 | 
            +
                          {
         | 
| 340 | 
            +
                            headers: { 'Content-Type' => 'text/html' },
         | 
| 341 | 
            +
                            status: status
         | 
| 342 | 
            +
                          }
         | 
| 343 | 
            +
                        end
         | 
| 344 | 
            +
                      end
         | 
| 345 | 
            +
                      let!(:default_host_request_stub) do
         | 
| 346 | 
            +
                        stub_request(:post, "https://#{api_key}@#{Ably::Rest::Client::DOMAIN}#{path}").to_return(&fallback_block)
         | 
| 347 | 
            +
                      end
         | 
| 348 | 
            +
             | 
| 349 | 
            +
                      it 'attempts the fallback hosts as this is an authentication failure' do
         | 
| 350 | 
            +
                        expect { publish_block.call }.to raise_error(Ably::Exceptions::ServerError)
         | 
| 351 | 
            +
                        expect(default_host_request_stub).to have_been_requested
         | 
| 352 | 
            +
                        expect(first_fallback_request_stub).to have_been_requested
         | 
| 353 | 
            +
                        expect(second_fallback_request_stub).to have_been_requested
         | 
| 354 | 
            +
                      end
         | 
| 355 | 
            +
                    end
         | 
| 210 356 | 
             
                  end
         | 
| 211 357 | 
             
                end
         | 
| 212 358 |  | 
| @@ -253,7 +399,7 @@ describe Ably::Rest::Client do | |
| 253 399 | 
             
                    end
         | 
| 254 400 |  | 
| 255 401 | 
             
                    it 'fails immediately and raises a Faraday Error' do
         | 
| 256 | 
            -
                      expect { client.auth.request_token }.to raise_error Ably::Exceptions:: | 
| 402 | 
            +
                      expect { client.auth.request_token }.to raise_error Ably::Exceptions::ConnectionTimeout
         | 
| 257 403 | 
             
                    end
         | 
| 258 404 |  | 
| 259 405 | 
             
                    context 'fallback hosts' do
         | 
| @@ -266,11 +412,27 @@ describe Ably::Rest::Client do | |
| 266 412 | 
             
                      end
         | 
| 267 413 |  | 
| 268 414 | 
             
                      specify 'are never used' do
         | 
| 269 | 
            -
                        expect { client.auth.request_token }.to raise_error Ably::Exceptions:: | 
| 415 | 
            +
                        expect { client.auth.request_token }.to raise_error Ably::Exceptions::ConnectionTimeout
         | 
| 270 416 | 
             
                        expect(custom_host_request_stub).to have_been_requested
         | 
| 271 417 | 
             
                      end
         | 
| 272 418 | 
             
                    end
         | 
| 273 419 | 
             
                  end
         | 
| 274 420 | 
             
                end
         | 
| 421 | 
            +
             | 
| 422 | 
            +
                context '#auth' do
         | 
| 423 | 
            +
                  let(:dummy_auth_url) { 'http://dummy.url' }
         | 
| 424 | 
            +
                  let(:unique_ttl)     { 1234 }
         | 
| 425 | 
            +
                  let(:client_options) { default_options.merge(auth_url: dummy_auth_url, ttl: unique_ttl) }
         | 
| 426 | 
            +
             | 
| 427 | 
            +
             | 
| 428 | 
            +
                  it 'is provides access to the Auth object' do
         | 
| 429 | 
            +
                    expect(client.auth).to be_kind_of(Ably::Auth)
         | 
| 430 | 
            +
                  end
         | 
| 431 | 
            +
             | 
| 432 | 
            +
                  it 'configures the Auth object with all ClientOptions passed to client in the initializer' do
         | 
| 433 | 
            +
                    expect(client.auth.options[:ttl]).to eql(unique_ttl)
         | 
| 434 | 
            +
                    expect(client.auth.options[:auth_url]).to eql(dummy_auth_url)
         | 
| 435 | 
            +
                  end
         | 
| 436 | 
            +
                end
         | 
| 275 437 | 
             
              end
         | 
| 276 438 | 
             
            end
         |