ably 1.0.7 → 1.1.0
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/.editorconfig +14 -0
- data/.travis.yml +4 -4
- data/CHANGELOG.md +26 -3
- data/Rakefile +32 -0
- data/SPEC.md +920 -565
- data/ably.gemspec +9 -4
- data/lib/ably/auth.rb +28 -2
- data/lib/ably/exceptions.rb +8 -2
- data/lib/ably/models/channel_state_change.rb +1 -1
- data/lib/ably/models/connection_state_change.rb +1 -1
- data/lib/ably/models/device_details.rb +87 -0
- data/lib/ably/models/device_push_details.rb +86 -0
- data/lib/ably/models/error_info.rb +23 -2
- data/lib/ably/models/idiomatic_ruby_wrapper.rb +4 -4
- data/lib/ably/models/protocol_message.rb +32 -2
- data/lib/ably/models/push_channel_subscription.rb +89 -0
- data/lib/ably/modules/conversions.rb +1 -1
- data/lib/ably/modules/encodeable.rb +1 -1
- data/lib/ably/modules/exception_codes.rb +128 -0
- data/lib/ably/modules/model_common.rb +15 -2
- data/lib/ably/modules/state_machine.rb +1 -1
- data/lib/ably/realtime.rb +1 -0
- data/lib/ably/realtime/auth.rb +1 -1
- data/lib/ably/realtime/channel.rb +24 -102
- data/lib/ably/realtime/channel/channel_manager.rb +2 -6
- data/lib/ably/realtime/channel/channel_state_machine.rb +2 -2
- data/lib/ably/realtime/channel/publisher.rb +74 -0
- data/lib/ably/realtime/channel/push_channel.rb +62 -0
- data/lib/ably/realtime/client.rb +87 -0
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +6 -2
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
- data/lib/ably/realtime/connection.rb +8 -5
- data/lib/ably/realtime/connection/connection_manager.rb +7 -7
- data/lib/ably/realtime/connection/websocket_transport.rb +1 -1
- data/lib/ably/realtime/presence.rb +4 -4
- data/lib/ably/realtime/presence/members_map.rb +3 -3
- data/lib/ably/realtime/push.rb +40 -0
- data/lib/ably/realtime/push/admin.rb +61 -0
- data/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
- data/lib/ably/realtime/push/device_registrations.rb +105 -0
- data/lib/ably/rest.rb +1 -0
- data/lib/ably/rest/channel.rb +33 -5
- data/lib/ably/rest/channel/push_channel.rb +62 -0
- data/lib/ably/rest/client.rb +137 -28
- data/lib/ably/rest/middleware/parse_message_pack.rb +17 -1
- data/lib/ably/rest/presence.rb +1 -0
- data/lib/ably/rest/push.rb +42 -0
- data/lib/ably/rest/push/admin.rb +54 -0
- data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
- data/lib/ably/rest/push/device_registrations.rb +103 -0
- data/lib/ably/version.rb +7 -2
- data/spec/acceptance/realtime/auth_spec.rb +6 -8
- data/spec/acceptance/realtime/channel_spec.rb +166 -51
- data/spec/acceptance/realtime/client_spec.rb +149 -0
- data/spec/acceptance/realtime/connection_failures_spec.rb +1 -1
- data/spec/acceptance/realtime/connection_spec.rb +4 -4
- data/spec/acceptance/realtime/message_spec.rb +19 -17
- data/spec/acceptance/realtime/presence_spec.rb +5 -5
- data/spec/acceptance/realtime/push_admin_spec.rb +696 -0
- data/spec/acceptance/realtime/push_spec.rb +27 -0
- data/spec/acceptance/rest/auth_spec.rb +4 -3
- data/spec/acceptance/rest/base_spec.rb +2 -2
- data/spec/acceptance/rest/client_spec.rb +129 -10
- data/spec/acceptance/rest/message_spec.rb +175 -4
- data/spec/acceptance/rest/push_admin_spec.rb +896 -0
- data/spec/acceptance/rest/push_spec.rb +25 -0
- data/spec/acceptance/rest/time_spec.rb +1 -1
- data/spec/run_parallel_tests +33 -0
- data/spec/unit/logger_spec.rb +10 -3
- data/spec/unit/models/device_details_spec.rb +102 -0
- data/spec/unit/models/device_push_details_spec.rb +101 -0
- data/spec/unit/models/error_info_spec.rb +51 -3
- data/spec/unit/models/message_spec.rb +17 -2
- data/spec/unit/models/presence_message_spec.rb +1 -1
- data/spec/unit/models/push_channel_subscription_spec.rb +86 -0
- data/spec/unit/realtime/client_spec.rb +12 -0
- data/spec/unit/realtime/push_channel_spec.rb +36 -0
- data/spec/unit/rest/channel_spec.rb +8 -1
- data/spec/unit/rest/client_spec.rb +30 -0
- data/spec/unit/rest/push_channel_spec.rb +36 -0
- metadata +71 -8
| @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            # encoding: utf-8
         | 
| 2 | 
            +
            require 'spec_helper'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            describe Ably::Realtime::Push, :event_machine do
         | 
| 5 | 
            +
              vary_by_protocol do
         | 
| 6 | 
            +
                let(:default_options) { { key: api_key, environment: environment, protocol: protocol} }
         | 
| 7 | 
            +
                let(:client_options)  { default_options }
         | 
| 8 | 
            +
                let(:client) do
         | 
| 9 | 
            +
                  Ably::Realtime::Client.new(client_options)
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
                subject { client.push }
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                describe '#activate' do
         | 
| 14 | 
            +
                  it 'raises an unsupported exception' do
         | 
| 15 | 
            +
                    expect { subject.activate('foo') }.to raise_error(Ably::Exceptions::PushNotificationsNotSupported)
         | 
| 16 | 
            +
                    stop_reactor
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                describe '#deactivate' do
         | 
| 21 | 
            +
                  it 'raises an unsupported exception' do
         | 
| 22 | 
            +
                    expect { subject.deactivate('foo') }.to raise_error(Ably::Exceptions::PushNotificationsNotSupported)
         | 
| 23 | 
            +
                    stop_reactor
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         | 
| @@ -758,9 +758,10 @@ describe Ably::Auth do | |
| 758 758 | 
             
                  end
         | 
| 759 759 |  | 
| 760 760 | 
             
                  it 'updates the persisted auth options that are then used for subsequent authorize requests' do
         | 
| 761 | 
            -
                     | 
| 762 | 
            -
                    auth. | 
| 763 | 
            -
                     | 
| 761 | 
            +
                    auth_url = "https://echo.ably.io/?type=text&body=#{auth.request_token.token}"
         | 
| 762 | 
            +
                    expect(auth.options[:auth_url]).to be_nil
         | 
| 763 | 
            +
                    auth.authorize({}, auth_url: auth_url)
         | 
| 764 | 
            +
                    expect(auth.options[:auth_url]).to eql(auth_url)
         | 
| 764 765 | 
             
                  end
         | 
| 765 766 |  | 
| 766 767 | 
             
                  context 'with a lambda for the :auth_callback option' do
         | 
| @@ -7,7 +7,7 @@ describe Ably::Rest do | |
| 7 7 |  | 
| 8 8 | 
             
                let(:client_options) { {} }
         | 
| 9 9 | 
             
                let(:client) do
         | 
| 10 | 
            -
                  Ably::Rest::Client.new(client_options.merge(key: 'appid.keyuid:keysecret'))
         | 
| 10 | 
            +
                  Ably::Rest::Client.new(client_options.merge(key: 'appid.keyuid:keysecret', log_retries_as_info: true))
         | 
| 11 11 | 
             
                end
         | 
| 12 12 |  | 
| 13 13 | 
             
                let(:now) { Time.now - 1000 }
         | 
| @@ -67,7 +67,7 @@ describe Ably::Rest do | |
| 67 67 |  | 
| 68 68 | 
             
              vary_by_protocol do
         | 
| 69 69 | 
             
                let(:client) do
         | 
| 70 | 
            -
                  Ably::Rest::Client.new(key: api_key, environment: environment, protocol: protocol)
         | 
| 70 | 
            +
                  Ably::Rest::Client.new(key: api_key, environment: environment, protocol: protocol, log_retries_as_info: true)
         | 
| 71 71 | 
             
                end
         | 
| 72 72 |  | 
| 73 73 | 
             
                describe 'failed requests' do
         | 
| @@ -4,7 +4,7 @@ require 'webrick' | |
| 4 4 |  | 
| 5 5 | 
             
            describe Ably::Rest::Client do
         | 
| 6 6 | 
             
              vary_by_protocol do
         | 
| 7 | 
            -
                let(:default_options) { { environment: environment, protocol: protocol } }
         | 
| 7 | 
            +
                let(:default_options) { { environment: environment, protocol: protocol, log_retries_as_info: true } }
         | 
| 8 8 | 
             
                let(:client_options)  { default_options }
         | 
| 9 9 |  | 
| 10 10 | 
             
                let(:client) { Ably::Rest::Client.new(client_options) }
         | 
| @@ -27,6 +27,19 @@ describe Ably::Rest::Client do | |
| 27 27 | 
             
                    end
         | 
| 28 28 | 
             
                  end
         | 
| 29 29 |  | 
| 30 | 
            +
                  context 'with an invalid API key' do
         | 
| 31 | 
            +
                    let(:client) { Ably::Rest::Client.new(client_options.merge(key: 'app.key:secret', log_level: :fatal)) }
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                    it 'logs an entry with a help href url matching the code #TI5' do
         | 
| 34 | 
            +
                      begin
         | 
| 35 | 
            +
                        client.channels.get('foo').publish('test')
         | 
| 36 | 
            +
                        raise 'Expected Ably::Exceptions::ResourceMissing'
         | 
| 37 | 
            +
                      rescue Ably::Exceptions::ResourceMissing => err
         | 
| 38 | 
            +
                        expect err.to_s.match(%r{https://help.ably.io/error/40400})
         | 
| 39 | 
            +
                      end
         | 
| 40 | 
            +
                    end
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 30 43 | 
             
                  context 'with an explicit string :token' do
         | 
| 31 44 | 
             
                    let(:client) { Ably::Rest::Client.new(client_options.merge(token: random_str)) }
         | 
| 32 45 |  | 
| @@ -710,6 +723,109 @@ describe Ably::Rest::Client do | |
| 710 723 | 
             
                          expect(@fallback_request_count).to eql(2)
         | 
| 711 724 | 
             
                        end
         | 
| 712 725 | 
             
                      end
         | 
| 726 | 
            +
             | 
| 727 | 
            +
                      context 'to fail the primary host, allow a fallback to succeed, then later trigger a fallback to the primary host (#RSC15f)' do
         | 
| 728 | 
            +
                        before do
         | 
| 729 | 
            +
                          @request_count = 0
         | 
| 730 | 
            +
                          @primary_host_request_count = 0
         | 
| 731 | 
            +
                          @web_server = WEBrick::HTTPServer.new(:Port => port, :SSLEnable => false, :AccessLog => [], Logger: WEBrick::Log.new("/dev/null"))
         | 
| 732 | 
            +
                          @web_server.mount_proc "/channels/#{channel_name}/publish" do |req, res|
         | 
| 733 | 
            +
                            @request_count += 1
         | 
| 734 | 
            +
                            if req.header["host"].first.include?(primary_host)
         | 
| 735 | 
            +
                              @primary_host_request_count += 1
         | 
| 736 | 
            +
                              # Fail all requests to the primary host so that a fallback is used
         | 
| 737 | 
            +
                              # Except request 6 which should suceed and clear the fallback host preference
         | 
| 738 | 
            +
                              if @request_count == 6
         | 
| 739 | 
            +
                                res.status = 200
         | 
| 740 | 
            +
                                res['Content-Type'] = 'application/json'
         | 
| 741 | 
            +
                                res.body = '{}'
         | 
| 742 | 
            +
                              else
         | 
| 743 | 
            +
                                res.status = 500
         | 
| 744 | 
            +
                              end
         | 
| 745 | 
            +
                            else
         | 
| 746 | 
            +
                              # Fail the second request (first failed fallback of first request)
         | 
| 747 | 
            +
                              # Fail the third request on the previously succeeded fallback host to trigger an attempt on the primary host
         | 
| 748 | 
            +
                              if [2, 5].include?(@request_count)
         | 
| 749 | 
            +
                                res.status = 500
         | 
| 750 | 
            +
                              else
         | 
| 751 | 
            +
                                res.status = 200
         | 
| 752 | 
            +
                                res['Content-Type'] = 'application/json'
         | 
| 753 | 
            +
                                res.body = '{}'
         | 
| 754 | 
            +
                              end
         | 
| 755 | 
            +
                            end
         | 
| 756 | 
            +
                          end
         | 
| 757 | 
            +
             | 
| 758 | 
            +
                          Thread.new do
         | 
| 759 | 
            +
                            @web_server.start
         | 
| 760 | 
            +
                          end
         | 
| 761 | 
            +
                        end
         | 
| 762 | 
            +
             | 
| 763 | 
            +
                        let(:client_options) do
         | 
| 764 | 
            +
                          default_options.merge(
         | 
| 765 | 
            +
                            rest_host: primary_host,
         | 
| 766 | 
            +
                            fallback_hosts: fallbacks,
         | 
| 767 | 
            +
                            token: 'fake.token',
         | 
| 768 | 
            +
                            port: port,
         | 
| 769 | 
            +
                            tls: false,
         | 
| 770 | 
            +
                            log_level: :error
         | 
| 771 | 
            +
                          ).merge(additional_client_options)
         | 
| 772 | 
            +
                        end
         | 
| 773 | 
            +
             | 
| 774 | 
            +
                        let (:additional_client_options) { {} }
         | 
| 775 | 
            +
             | 
| 776 | 
            +
                        it 'succeeds and remembers fallback host preferences across requests' do
         | 
| 777 | 
            +
                          # Send a request, expect primary endpoint to fail, one fallback to fail, second fallback to succeed
         | 
| 778 | 
            +
                          client.channel(channel_name).publish('event', 'data')
         | 
| 779 | 
            +
                          expect(@request_count).to eql(3)
         | 
| 780 | 
            +
                          expect(fallbacks).to include(client.using_preferred_fallback_host?)
         | 
| 781 | 
            +
                          successfull_fallback = client.using_preferred_fallback_host?
         | 
| 782 | 
            +
                          expect(@primary_host_request_count).to eql(1)
         | 
| 783 | 
            +
             | 
| 784 | 
            +
                          # Send another request, which should go straight to the fallback as it succeeded previously
         | 
| 785 | 
            +
                          client.channel(channel_name).publish('event', 'data')
         | 
| 786 | 
            +
                          expect(@request_count).to eql(4)
         | 
| 787 | 
            +
                          expect(successfull_fallback).to eql(client.using_preferred_fallback_host?)
         | 
| 788 | 
            +
                          expect(@primary_host_request_count).to eql(1)
         | 
| 789 | 
            +
             | 
| 790 | 
            +
                          # A subsequent request should fail to the fallback, go the primary host and succeed
         | 
| 791 | 
            +
                          client.channel(channel_name).publish('event', 'data')
         | 
| 792 | 
            +
                          expect(@request_count).to eql(6)
         | 
| 793 | 
            +
                          expect(client.using_preferred_fallback_host?).to be_falsey
         | 
| 794 | 
            +
                          expect(@primary_host_request_count).to eql(2)
         | 
| 795 | 
            +
             | 
| 796 | 
            +
                          # A subsequent request will fail on the primary endpoint, and we expect the fallback to be used again
         | 
| 797 | 
            +
                          client.channel(channel_name).publish('event', 'data')
         | 
| 798 | 
            +
                          expect(@request_count).to eql(8)
         | 
| 799 | 
            +
                          expect(fallbacks).to include(client.using_preferred_fallback_host?)
         | 
| 800 | 
            +
                          successfull_fallback = client.using_preferred_fallback_host?
         | 
| 801 | 
            +
                          expect(@primary_host_request_count).to eql(3)
         | 
| 802 | 
            +
             | 
| 803 | 
            +
                          # Send another request, which should go straight to the fallback as it succeeded previously
         | 
| 804 | 
            +
                          client.channel(channel_name).publish('event', 'data')
         | 
| 805 | 
            +
                          expect(@request_count).to eql(9)
         | 
| 806 | 
            +
                          expect(successfull_fallback).to eql(client.using_preferred_fallback_host?)
         | 
| 807 | 
            +
                          expect(@primary_host_request_count).to eql(3)
         | 
| 808 | 
            +
                        end
         | 
| 809 | 
            +
             | 
| 810 | 
            +
                        context 'with custom :fallback_retry_timeout' do
         | 
| 811 | 
            +
                          let (:additional_client_options) { { fallback_retry_timeout: 5 } }
         | 
| 812 | 
            +
             | 
| 813 | 
            +
                          it 'stops using the preferred fallback after this time' do
         | 
| 814 | 
            +
                            # Send a request, expect primary endpoint to fail, one fallback to fail, second fallback to succeed
         | 
| 815 | 
            +
                            client.channel(channel_name).publish('event', 'data')
         | 
| 816 | 
            +
                            expect(@request_count).to eql(3)
         | 
| 817 | 
            +
                            expect(fallbacks).to include(client.using_preferred_fallback_host?)
         | 
| 818 | 
            +
                            expect(@primary_host_request_count).to eql(1)
         | 
| 819 | 
            +
             | 
| 820 | 
            +
                            # Wait for the preferred fallback cache to expire
         | 
| 821 | 
            +
                            sleep 5
         | 
| 822 | 
            +
             | 
| 823 | 
            +
                            # Send another request, which should go straight to the primary host again as fallback host is expired
         | 
| 824 | 
            +
                            client.channel(channel_name).publish('event', 'data')
         | 
| 825 | 
            +
                            expect(@primary_host_request_count).to eql(2)
         | 
| 826 | 
            +
                          end
         | 
| 827 | 
            +
                        end
         | 
| 828 | 
            +
                      end
         | 
| 713 829 | 
             
                    end
         | 
| 714 830 | 
             
                  end
         | 
| 715 831 |  | 
| @@ -724,7 +840,8 @@ describe Ably::Rest::Client do | |
| 724 840 | 
             
                        environment: env,
         | 
| 725 841 | 
             
                        key: api_key,
         | 
| 726 842 | 
             
                        http_max_retry_duration: max_retry_duration,
         | 
| 727 | 
            -
                        http_max_retry_count: max_retry_count
         | 
| 843 | 
            +
                        http_max_retry_count: max_retry_count,
         | 
| 844 | 
            +
                        log_level: :fatal,
         | 
| 728 845 | 
             
                      )
         | 
| 729 846 | 
             
                    end
         | 
| 730 847 |  | 
| @@ -751,7 +868,7 @@ describe Ably::Rest::Client do | |
| 751 868 | 
             
                      end
         | 
| 752 869 |  | 
| 753 870 | 
             
                      let(:client_options) {
         | 
| 754 | 
            -
                        production_options.merge(fallback_hosts: custom_hosts, log_level: : | 
| 871 | 
            +
                        production_options.merge(fallback_hosts: custom_hosts, log_level: :fatal)
         | 
| 755 872 | 
             
                      }
         | 
| 756 873 |  | 
| 757 874 | 
             
                      it 'attempts the fallback hosts as this is not an authentication failure' do
         | 
| @@ -764,7 +881,7 @@ describe Ably::Rest::Client do | |
| 764 881 |  | 
| 765 882 | 
             
                    context 'with an empty array of fallback hosts provided (#RSC15b, #TO3k6)' do
         | 
| 766 883 | 
             
                      let(:client_options) {
         | 
| 767 | 
            -
                        production_options.merge(fallback_hosts: [])
         | 
| 884 | 
            +
                        production_options.merge(fallback_hosts: [], log_level: :fatal)
         | 
| 768 885 | 
             
                      }
         | 
| 769 886 |  | 
| 770 887 | 
             
                      it 'does not attempt the fallback hosts as this is an authentication failure' do
         | 
| @@ -789,7 +906,7 @@ describe Ably::Rest::Client do | |
| 789 906 | 
             
                      end
         | 
| 790 907 |  | 
| 791 908 | 
             
                      let(:client_options) {
         | 
| 792 | 
            -
                        production_options.merge(fallback_hosts: custom_hosts, log_level: : | 
| 909 | 
            +
                        production_options.merge(fallback_hosts: custom_hosts, log_level: :fatal)
         | 
| 793 910 | 
             
                      }
         | 
| 794 911 |  | 
| 795 912 | 
             
                      it 'attempts the default fallback hosts as this is an authentication failure' do
         | 
| @@ -966,7 +1083,7 @@ describe Ably::Rest::Client do | |
| 966 1083 | 
             
                      it 'sends a protocol version and lib version header (#G4, #RSC7a, #RSC7b)' do
         | 
| 967 1084 | 
             
                        client.channels.get('foo').publish("event")
         | 
| 968 1085 | 
             
                        expect(publish_message_stub).to have_been_requested
         | 
| 969 | 
            -
                        expect(Ably::PROTOCOL_VERSION).to eql('1. | 
| 1086 | 
            +
                        expect(Ably::PROTOCOL_VERSION).to eql('1.1')
         | 
| 970 1087 | 
             
                      end
         | 
| 971 1088 | 
             
                    end
         | 
| 972 1089 | 
             
                  end
         | 
| @@ -1084,7 +1201,7 @@ describe Ably::Rest::Client do | |
| 1084 1201 | 
             
                    end
         | 
| 1085 1202 |  | 
| 1086 1203 | 
             
                    context 'option add_request_ids: true and specified fallback hosts', :webmock do
         | 
| 1087 | 
            -
                      let(:client_options) { { key: api_key, fallback_hosts_use_default: true, add_request_ids: true, log_level: :error } }
         | 
| 1204 | 
            +
                      let(:client_options) { { key: api_key, fallback_hosts_use_default: true, add_request_ids: true, log_level: :error, log_retries_as_info: true } }
         | 
| 1088 1205 | 
             
                      let(:requests)       { [] }
         | 
| 1089 1206 |  | 
| 1090 1207 | 
             
                      before do
         | 
| @@ -1140,7 +1257,7 @@ describe Ably::Rest::Client do | |
| 1140 1257 |  | 
| 1141 1258 | 
             
                context 'failed request logging', :prevent_log_stubbing do
         | 
| 1142 1259 | 
             
                  let(:custom_logger) { TestLogger.new }
         | 
| 1143 | 
            -
                  let(:client_options) { default_options.merge(key: api_key, logger: custom_logger) }
         | 
| 1260 | 
            +
                  let(:client_options) { default_options.merge(key: api_key, logger: custom_logger, log_retries_as_info: false) }
         | 
| 1144 1261 |  | 
| 1145 1262 | 
             
                  it 'is absent when requests do not fail' do
         | 
| 1146 1263 | 
             
                    client.time
         | 
| @@ -1153,7 +1270,8 @@ describe Ably::Rest::Client do | |
| 1153 1270 | 
             
                        rest_host: 'non.existent.domain.local',
         | 
| 1154 1271 | 
             
                        fallback_hosts: [[environment, Ably::Rest::Client::DOMAIN].join('-')],
         | 
| 1155 1272 | 
             
                        key: api_key,
         | 
| 1156 | 
            -
                        logger: custom_logger | 
| 1273 | 
            +
                        logger: custom_logger,
         | 
| 1274 | 
            +
                        log_retries_as_info: false)
         | 
| 1157 1275 | 
             
                    end
         | 
| 1158 1276 |  | 
| 1159 1277 | 
             
                    it 'is present with success message when requests do not actually fail' do
         | 
| @@ -1169,7 +1287,8 @@ describe Ably::Rest::Client do | |
| 1169 1287 | 
             
                        rest_host: 'non.existent.domain.local',
         | 
| 1170 1288 | 
             
                        fallback_hosts: ['non2.existent.domain.local'],
         | 
| 1171 1289 | 
             
                        key: api_key,
         | 
| 1172 | 
            -
                        logger: custom_logger | 
| 1290 | 
            +
                        logger: custom_logger,
         | 
| 1291 | 
            +
                        log_retries_as_info: false)
         | 
| 1173 1292 | 
             
                    end
         | 
| 1174 1293 |  | 
| 1175 1294 | 
             
                    it 'is present when all requests fail' do
         | 
| @@ -1,5 +1,6 @@ | |
| 1 1 | 
             
            # encoding: utf-8
         | 
| 2 2 | 
             
            require 'spec_helper'
         | 
| 3 | 
            +
            require 'base64'
         | 
| 3 4 | 
             
            require 'securerandom'
         | 
| 4 5 |  | 
| 5 6 | 
             
            describe Ably::Rest::Channel, 'messages' do
         | 
| @@ -91,11 +92,181 @@ describe Ably::Rest::Channel, 'messages' do | |
| 91 92 | 
             
                  end
         | 
| 92 93 | 
             
                end
         | 
| 93 94 |  | 
| 95 | 
            +
                context 'idempotency (#RSL1k)' do
         | 
| 96 | 
            +
                  let(:id) { random_str }
         | 
| 97 | 
            +
                  let(:name) { 'event' }
         | 
| 98 | 
            +
                  let(:data) { random_str }
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                  context 'when ID is not included (#RSL1k2)' do
         | 
| 101 | 
            +
                    context 'with Message object' do
         | 
| 102 | 
            +
                      let(:message) { Ably::Models::Message.new(data: data) }
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                      it 'publishes the same message three times' do
         | 
| 105 | 
            +
                        3.times { channel.publish [message] }
         | 
| 106 | 
            +
                        expect(channel.history.items.length).to eql(3)
         | 
| 107 | 
            +
                      end
         | 
| 108 | 
            +
                    end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                    context 'with #publish arguments only' do
         | 
| 111 | 
            +
                      it 'publishes the same message three times' do
         | 
| 112 | 
            +
                        3.times { channel.publish 'event', data }
         | 
| 113 | 
            +
                        expect(channel.history.items.length).to eql(3)
         | 
| 114 | 
            +
                      end
         | 
| 115 | 
            +
                    end
         | 
| 116 | 
            +
                  end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                  context 'when ID is included (#RSL1k2, #RSL1k5)' do
         | 
| 119 | 
            +
                    context 'with Message object' do
         | 
| 120 | 
            +
                      let(:message) { Ably::Models::Message.new(id: id, data: data) }
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                      specify 'three REST publishes result in only one message being published' do
         | 
| 123 | 
            +
                        pending 'idempotency rolled out to global cluster'
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                        3.times { channel.publish [message] }
         | 
| 126 | 
            +
                        expect(channel.history.items.length).to eql(1)
         | 
| 127 | 
            +
                        expect(channel.history.items[0].id).to eql(id)
         | 
| 128 | 
            +
                      end
         | 
| 129 | 
            +
                    end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                    context 'with #publish arguments only' do
         | 
| 132 | 
            +
                      it 'three REST publishes result in only one message being published' do
         | 
| 133 | 
            +
                        pending 'idempotency rolled out to global cluster'
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                        3.times { channel.publish 'event', data, id: id }
         | 
| 136 | 
            +
                        expect(channel.history.items.length).to eql(1)
         | 
| 137 | 
            +
                      end
         | 
| 138 | 
            +
                    end
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                    specify 'the ID provided is used for the published messages' do
         | 
| 141 | 
            +
                      channel.publish 'event', data, id: id
         | 
| 142 | 
            +
                      expect(channel.history.items[0].id).to eql(id)
         | 
| 143 | 
            +
                    end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                    specify 'for multiple messages in one publish operation (#RSL1k3)' do
         | 
| 146 | 
            +
                      pending 'idempotency rolled out to global cluster'
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                      message_arr = 3.times.map { Ably::Models::Message.new(id: id, data: data) }
         | 
| 149 | 
            +
                      expect { channel.publish message_arr }.to raise_error do |error|
         | 
| 150 | 
            +
                        expect(error.code).to eql(40031) # Invalid publish request (invalid client-specified id), see https://github.com/ably/ably-common/pull/30
         | 
| 151 | 
            +
                      end
         | 
| 152 | 
            +
                    end
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                    specify 'for multiple messages in one publish operation with IDs following the required format described in RSL1k1 (#RSL1k3)' do
         | 
| 155 | 
            +
                      pending 'idempotency rolled out to global cluster'
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                      message_arr = 3.times.map { |index| Ably::Models::Message.new(id: "#{id}:#{index}", data: data) }
         | 
| 158 | 
            +
                      channel.publish message_arr
         | 
| 159 | 
            +
                      expect(channel.history.items[0].id).to eql("{id}:0")
         | 
| 160 | 
            +
                      expect(channel.history.items[2].id).to eql("{id}:2")
         | 
| 161 | 
            +
                      expect(channel.history.items.length).to eql(3)
         | 
| 162 | 
            +
                    end
         | 
| 163 | 
            +
                  end
         | 
| 164 | 
            +
             | 
| 165 | 
            +
                  specify 'idempotent publishing is disabled by default with 1.1 (#TO3n)' do
         | 
| 166 | 
            +
                    client = Ably::Rest::Client.new(key: api_key, protocol: protocol)
         | 
| 167 | 
            +
                    expect(client.idempotent_rest_publishing).to be_falsey
         | 
| 168 | 
            +
                  end
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                  specify 'idempotent publishing is enabled by default with 1.2 (#TO3n)' do
         | 
| 171 | 
            +
                    stub_const 'Ably::VERSION', '1.2.0'
         | 
| 172 | 
            +
                    client = Ably::Rest::Client.new(key: api_key, protocol: protocol)
         | 
| 173 | 
            +
                    expect(client.idempotent_rest_publishing).to be_truthy
         | 
| 174 | 
            +
                  end
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                  context 'when idempotent publishing is enabled in the client library ClientOptions (#TO3n)' do
         | 
| 177 | 
            +
                    let(:client_options) { default_client_options.merge(idempotent_rest_publishing: true, log_level: :error) }
         | 
| 178 | 
            +
             | 
| 179 | 
            +
                    context 'when there is a network failure triggering an automatic retry (#RSL1k4)' do
         | 
| 180 | 
            +
                      def mock_for_two_publish_failures
         | 
| 181 | 
            +
                        @failed_http_posts = 0
         | 
| 182 | 
            +
                        allow(client).to receive(:can_fallback_to_alternate_ably_host?).and_return(true)
         | 
| 183 | 
            +
                        allow_any_instance_of(Faraday::Connection).to receive(:post) do |*args|
         | 
| 184 | 
            +
                          @failed_http_posts += 1
         | 
| 185 | 
            +
                          if @failed_http_posts == 2
         | 
| 186 | 
            +
                            # Ensure the 3rd requests operates as normal
         | 
| 187 | 
            +
                            allow_any_instance_of(Faraday::Connection).to receive(:post).and_call_original
         | 
| 188 | 
            +
                          end
         | 
| 189 | 
            +
                          raise Faraday::ClientError.new('Fake client error')
         | 
| 190 | 
            +
                        end
         | 
| 191 | 
            +
                      end
         | 
| 192 | 
            +
             | 
| 193 | 
            +
                      context 'with Message object' do
         | 
| 194 | 
            +
                        let(:message) { Ably::Models::Message.new(data: data) }
         | 
| 195 | 
            +
                        before { mock_for_two_publish_failures }
         | 
| 196 | 
            +
             | 
| 197 | 
            +
                        specify 'two REST publish retries result in only one message being published' do
         | 
| 198 | 
            +
                          pending 'idempotency rolled out to global cluster'
         | 
| 199 | 
            +
             | 
| 200 | 
            +
                          channel.publish [message]
         | 
| 201 | 
            +
                          expect(channel.history.items.length).to eql(1)
         | 
| 202 | 
            +
                          expect(@failed_http_posts).to eql(2)
         | 
| 203 | 
            +
                        end
         | 
| 204 | 
            +
                      end
         | 
| 205 | 
            +
             | 
| 206 | 
            +
                      context 'with #publish arguments only' do
         | 
| 207 | 
            +
                        before { mock_for_two_publish_failures }
         | 
| 208 | 
            +
             | 
| 209 | 
            +
                        specify 'two REST publish retries result in only one message being published' do
         | 
| 210 | 
            +
                          pending 'idempotency rolled out to global cluster'
         | 
| 211 | 
            +
             | 
| 212 | 
            +
                          channel.publish 'event', data
         | 
| 213 | 
            +
                          expect(channel.history.items.length).to eql(1)
         | 
| 214 | 
            +
                          expect(@failed_http_posts).to eql(2)
         | 
| 215 | 
            +
                        end
         | 
| 216 | 
            +
                      end
         | 
| 217 | 
            +
             | 
| 218 | 
            +
                      context 'with explicitly provided message ID' do
         | 
| 219 | 
            +
                        let(:id) { random_str }
         | 
| 220 | 
            +
             | 
| 221 | 
            +
                        before { mock_for_two_publish_failures }
         | 
| 222 | 
            +
             | 
| 223 | 
            +
                        specify 'two REST publish retries result in only one message being published' do
         | 
| 224 | 
            +
                          pending 'idempotency rolled out to global cluster'
         | 
| 225 | 
            +
             | 
| 226 | 
            +
                          channel.publish 'event', data, id: id
         | 
| 227 | 
            +
                          expect(channel.history.items.length).to eql(1)
         | 
| 228 | 
            +
                          expect(channel.history.items[0].id).to eql(id)
         | 
| 229 | 
            +
                          expect(@failed_http_posts).to eql(2)
         | 
| 230 | 
            +
                        end
         | 
| 231 | 
            +
                      end
         | 
| 232 | 
            +
             | 
| 233 | 
            +
                      specify 'for multiple messages in one publish operation' do
         | 
| 234 | 
            +
                        pending 'idempotency rolled out to global cluster'
         | 
| 235 | 
            +
             | 
| 236 | 
            +
                        message_arr = 3.times.map { Ably::Models::Message.new(data: data) }
         | 
| 237 | 
            +
                        3.times { channel.publish message_arr }
         | 
| 238 | 
            +
                        expect(channel.history.items.length).to eql(message_arr.length)
         | 
| 239 | 
            +
                      end
         | 
| 240 | 
            +
                    end
         | 
| 241 | 
            +
             | 
| 242 | 
            +
                    specify 'the ID is populated with a random ID and serial 0 from this lib (#RSL1k1)' do
         | 
| 243 | 
            +
                      channel.publish 'event'
         | 
| 244 | 
            +
                      expect(channel.history.items[0].id).to match(/^[A-Za-z0-9\+\/]+:0$/)
         | 
| 245 | 
            +
                      base_64_id = channel.history.items[0].id.split(':')[0]
         | 
| 246 | 
            +
                      expect(Base64.decode64(base_64_id).length).to eql(9)
         | 
| 247 | 
            +
                    end
         | 
| 248 | 
            +
             | 
| 249 | 
            +
                    context 'when publishing a batch of messages' do
         | 
| 250 | 
            +
                      specify 'the ID is populated with a single random ID and sequence of serials from this lib (#RSL1k1)' do
         | 
| 251 | 
            +
                        pending 'idempotency rolled out to global cluster'
         | 
| 252 | 
            +
             | 
| 253 | 
            +
                        message = { name: 'event' }
         | 
| 254 | 
            +
                        channel.publish [message, message, message]
         | 
| 255 | 
            +
                        expect(channel.history.items[0].length).to eql(3)
         | 
| 256 | 
            +
                        expect(channel.history.items[0].id).to match(/^[A-Za-z0-9\+\/]+:0$/)
         | 
| 257 | 
            +
                        expect(channel.history.items[2].id).to match(/^[A-Za-z0-9\+\/]+:2$/)
         | 
| 258 | 
            +
                        base_64_id = channel.history.items[0].id.split(':')[0]
         | 
| 259 | 
            +
                        expect(Base64.decode64(base_64_id).length).to eql(9)
         | 
| 260 | 
            +
                      end
         | 
| 261 | 
            +
                    end
         | 
| 262 | 
            +
                  end
         | 
| 263 | 
            +
                end
         | 
| 264 | 
            +
             | 
| 94 265 | 
             
                context 'with unsupported data payload content type' do
         | 
| 95 266 | 
             
                  context 'Integer' do
         | 
| 96 267 | 
             
                    let(:data) { 1 }
         | 
| 97 268 |  | 
| 98 | 
            -
                    it 'is raises an UnsupportedDataType  | 
| 269 | 
            +
                    it 'is raises an UnsupportedDataType 40013 exception' do
         | 
| 99 270 | 
             
                      expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
         | 
| 100 271 | 
             
                    end
         | 
| 101 272 | 
             
                  end
         | 
| @@ -103,7 +274,7 @@ describe Ably::Rest::Channel, 'messages' do | |
| 103 274 | 
             
                  context 'Float' do
         | 
| 104 275 | 
             
                    let(:data) { 1.1 }
         | 
| 105 276 |  | 
| 106 | 
            -
                    it 'is raises an UnsupportedDataType  | 
| 277 | 
            +
                    it 'is raises an UnsupportedDataType 40013 exception' do
         | 
| 107 278 | 
             
                      expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
         | 
| 108 279 | 
             
                    end
         | 
| 109 280 | 
             
                  end
         | 
| @@ -111,7 +282,7 @@ describe Ably::Rest::Channel, 'messages' do | |
| 111 282 | 
             
                  context 'Boolean' do
         | 
| 112 283 | 
             
                    let(:data) { true }
         | 
| 113 284 |  | 
| 114 | 
            -
                    it 'is raises an UnsupportedDataType  | 
| 285 | 
            +
                    it 'is raises an UnsupportedDataType 40013 exception' do
         | 
| 115 286 | 
             
                      expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
         | 
| 116 287 | 
             
                    end
         | 
| 117 288 | 
             
                  end
         | 
| @@ -119,7 +290,7 @@ describe Ably::Rest::Channel, 'messages' do | |
| 119 290 | 
             
                  context 'False' do
         | 
| 120 291 | 
             
                    let(:data) { false }
         | 
| 121 292 |  | 
| 122 | 
            -
                    it 'is raises an UnsupportedDataType  | 
| 293 | 
            +
                    it 'is raises an UnsupportedDataType 40013 exception' do
         | 
| 123 294 | 
             
                      expect { channel.publish 'event', data }.to raise_error(Ably::Exceptions::UnsupportedDataType)
         | 
| 124 295 | 
             
                    end
         | 
| 125 296 | 
             
                  end
         |