Repository: ably/ably-python (python)

Issue:
Remove soft-deprecated APIs in 2.0 branch

Pull request:
Remove deprecated APIs
Resolves #475, #123 

Patch:
diff --git a/ably/rest/auth.py b/ably/rest/auth.py
index 15eaf16..06af243 100644
--- a/ably/rest/auth.py
+++ b/ably/rest/auth.py
@@ -5,7 +5,6 @@ import logging
 import time
 from typing import Optional, TYPE_CHECKING, Union
 import uuid
-import warnings
 import httpx
 
 from ably.types.options import Options
@@ -152,12 +151,6 @@ class Auth:
     async def authorize(self, token_params: Optional[dict] = None, auth_options=None):
         return await self.__authorize_when_necessary(token_params, auth_options, force=True)
 
-    async def authorise(self, *args, **kwargs):
-        warnings.warn(
-            "authorise is deprecated and will be removed in v2.0, please use authorize",
-            DeprecationWarning)
-        return await self.authorize(*args, **kwargs)
-
     async def request_token(self, token_params: Optional[dict] = None,
                             # auth_options
                             key_name: Optional[str] = None, key_secret: Optional[str] = None, auth_callback=None,
diff --git a/ably/rest/channel.py b/ably/rest/channel.py
index df84043..d799560 100644
--- a/ably/rest/channel.py
+++ b/ably/rest/channel.py
@@ -5,7 +5,6 @@ import json
 import os
 from typing import Iterator
 from urllib import parse
-import warnings
 
 from methoddispatch import SingleDispatch, singledispatch
 import msgpack
@@ -100,15 +99,8 @@ class Channel(SingleDispatch):
         return await self.ably.http.post(path, body=request_body, timeout=timeout)
 
     @_publish.register(str)
-    async def publish_name_data(self, name, data, client_id=None, extras=None, timeout=None):
-        # RSL1h
-        if client_id or extras:
-            warnings.warn(
-                "Support for client_id and extras will be removed in 2.0",
-                DeprecationWarning
-            )
-
-        messages = [Message(name, data, client_id, extras=extras)]
+    async def publish_name_data(self, name, data, timeout=None):
+        messages = [Message(name, data)]
         return await self.publish_messages(messages, timeout=timeout)
 
     async def publish(self, *args, **kwargs):
diff --git a/ably/types/options.py b/ably/types/options.py
index 4db971e..61b1a84 100644
--- a/ably/types/options.py
+++ b/ably/types/options.py
@@ -1,5 +1,4 @@
 import random
-import warnings
 import logging
 
 from ably.transport.defaults import Defaults
@@ -13,9 +12,9 @@ class Options(AuthOptions):
                  tls_port=0, use_binary_protocol=True, queue_messages=False, recover=False, environment=None,
                  http_open_timeout=None, http_request_timeout=None, realtime_request_timeout=None,
                  http_max_retry_count=None, http_max_retry_duration=None, fallback_hosts=None,
-                 fallback_hosts_use_default=None, fallback_retry_timeout=None, disconnected_retry_timeout=None,
-                 idempotent_rest_publishing=None, loop=None, auto_connect=True, suspended_retry_timeout=None,
-                 connectivity_check_url=None, channel_retry_timeout=Defaults.channel_retry_timeout, **kwargs):
+                 fallback_retry_timeout=None, disconnected_retry_timeout=None, idempotent_rest_publishing=None,
+                 loop=None, auto_connect=True, suspended_retry_timeout=None, connectivity_check_url=None,
+                 channel_retry_timeout=Defaults.channel_retry_timeout, **kwargs):
         super().__init__(**kwargs)
 
         # TODO check these defaults
@@ -66,7 +65,6 @@ class Options(AuthOptions):
         self.__http_max_retry_count = http_max_retry_count
         self.__http_max_retry_duration = http_max_retry_duration
         self.__fallback_hosts = fallback_hosts
-        self.__fallback_hosts_use_default = fallback_hosts_use_default
         self.__fallback_retry_timeout = fallback_retry_timeout
         self.__disconnected_retry_timeout = disconnected_retry_timeout
         self.__channel_retry_timeout = channel_retry_timeout
@@ -206,10 +204,6 @@ class Options(AuthOptions):
     def fallback_hosts(self):
         return self.__fallback_hosts
 
-    @property
-    def fallback_hosts_use_default(self):
-        return self.__fallback_hosts_use_default
-
     @property
     def fallback_retry_timeout(self):
         return self.__fallback_retry_timeout
@@ -283,27 +277,13 @@ class Options(AuthOptions):
         # Fallback hosts
         fallback_hosts = self.fallback_hosts
         if fallback_hosts is None:
-            if host == Defaults.rest_host or self.fallback_hosts_use_default:
+            if host == Defaults.rest_host:
                 fallback_hosts = Defaults.fallback_hosts
             elif environment != 'production':
                 fallback_hosts = Defaults.get_environment_fallback_hosts(environment)
             else:
                 fallback_hosts = []
 
-        # Explicit warning about deprecating the option
-        if self.fallback_hosts_use_default:
-            if environment != Defaults.environment:
-                warnings.warn(
-                    "It is no longer required to set 'fallback_hosts_use_default', the correct fallback hosts "
-                    "are now inferred from the environment, 'fallback_hosts': {}"
-                    .format(','.join(fallback_hosts)), DeprecationWarning
-                )
-            else:
-                warnings.warn(
-                    "It is no longer required to set 'fallback_hosts_use_default': 'fallback_hosts': {}"
-                    .format(','.join(fallback_hosts)), DeprecationWarning
-                )
-
         # Shuffle
         fallback_hosts = list(fallback_hosts)
         random.shuffle(fallback_hosts)
@@ -317,6 +297,7 @@ class Options(AuthOptions):
     def __get_realtime_hosts(self):
         if self.realtime_host is not None:
             host = self.realtime_host
+            return [host]
         elif self.environment != "production":
             host = f'{self.environment}-{Defaults.realtime_host}'
         else:
diff --git a/ably/util/crypto.py b/ably/util/crypto.py
index 3ed24f2..acd558b 100644
--- a/ably/util/crypto.py
+++ b/ably/util/crypto.py
@@ -142,10 +142,8 @@ def generate_random_key(length=DEFAULT_KEYLENGTH):
 
 
 def get_default_params(params=None):
-    # Backwards compatibility
     if type(params) in [str, bytes]:
-        log.warning("Calling get_default_params with a key directly is deprecated, it expects a params dict")
-        return get_default_params({'key': params})
+        raise ValueError("Calling get_default_params with a key directly is deprecated, it expects a params dict")
 
     key = params.get('key')
     algorithm = params.get('algorithm') or 'AES'


Test patch:
diff --git a/test/ably/realtime/realtimeconnection_test.py b/test/ably/realtime/realtimeconnection_test.py
index 2017f1c..76ba1d1 100644
--- a/test/ably/realtime/realtimeconnection_test.py
+++ b/test/ably/realtime/realtimeconnection_test.py
@@ -288,40 +288,61 @@ class TestRealtimeConnection(BaseAsyncTestCase):
         await ably.close()
 
     async def test_fallback_host(self):
-        fallback_host = 'sandbox-realtime.ably.io'
-        ably = await TestApp.get_ably_realtime(realtime_host="iamnotahost", disconnected_retry_timeout=400000,
-                                               fallback_hosts=[fallback_host])
+        ably = await TestApp.get_ably_realtime()
+
+        await ably.connection.connection_manager.once_async('transport.pending')
+        assert ably.connection.connection_manager.transport
+        ably.connection.connection_manager.transport._emit('failed', AblyException("test exception", 502, 50200))
+
         await ably.connection.once_async(ConnectionState.CONNECTED)
-        assert ably.connection.connection_manager.transport.host == fallback_host
-        assert ably.options.fallback_realtime_host == fallback_host
+
+        assert ably.connection.connection_manager.transport.host != self.test_vars["realtime_host"]
+        assert ably.options.fallback_realtime_host != self.test_vars["realtime_host"]
         await ably.close()
 
     async def test_fallback_host_no_connection(self):
-        fallback_host = 'sandbox-realtime.ably.io'
-        ably = await TestApp.get_ably_realtime(realtime_host="iamnotahost", disconnected_retry_timeout=400000,
-                                               fallback_hosts=[fallback_host])
+        ably = await TestApp.get_ably_realtime()
+
+        await ably.connection.connection_manager.once_async('transport.pending')
+        assert ably.connection.connection_manager.transport
 
         def check_connection():
             return False
 
         ably.connection.connection_manager.check_connection = check_connection
 
+        asyncio.create_task(ably.connection.connection_manager.transport.on_protocol_message({
+            "action": ProtocolMessageAction.DISCONNECTED,
+            "error": {
+                "statusCode": 502,
+                "code": 50200,
+                "message": "test exception"
+            }
+        }))
+
         await ably.connection.once_async(ConnectionState.DISCONNECTED)
-        assert ably.connection.connection_manager.transport.host == "iamnotahost"
+
+        assert ably.options.fallback_realtime_host is None
         await ably.close()
 
     async def test_fallback_host_disconnected_protocol_msg(self):
-        fallback_host = 'sandbox-realtime.ably.io'
-        ably = await TestApp.get_ably_realtime(realtime_host="iamnotahost", disconnected_retry_timeout=400000,
-                                               fallback_hosts=[fallback_host])
-
-        async def on_transport_pending(transport):
-            await transport.on_protocol_message({'action': 6, "error": {"statusCode": 500, "code": 500}})
+        ably = await TestApp.get_ably_realtime()
 
-        ably.connection.connection_manager.once('transport.pending', on_transport_pending)
+        await ably.connection.connection_manager.once_async('transport.pending')
+        assert ably.connection.connection_manager.transport
+        asyncio.create_task(ably.connection.connection_manager.transport.on_protocol_message({
+            "action": ProtocolMessageAction.DISCONNECTED,
+            "error": {
+                "statusCode": 502,
+                "code": 50200,
+                "message": "test exception"
+            }
+        }))
 
         await ably.connection.once_async(ConnectionState.CONNECTED)
-        assert ably.connection.connection_manager.transport.host == fallback_host
+
+        assert ably.connection.connection_manager.transport.host != self.test_vars["realtime_host"]
+        assert ably.options.fallback_realtime_host != self.test_vars["realtime_host"]
         await ably.close()
 
     #  RTN2d
diff --git a/test/ably/rest/restauth_test.py b/test/ably/rest/restauth_test.py
index 9e5494c..a6ac0ce 100644
--- a/test/ably/rest/restauth_test.py
+++ b/test/ably/rest/restauth_test.py
@@ -4,7 +4,6 @@ import time
 import uuid
 import base64
 
-import warnings
 from urllib.parse import parse_qs
 import mock
 import pytest
@@ -183,7 +182,7 @@ class TestAuthAuthorize(BaseAsyncTestCase, metaclass=VaryByProtocolTestsMetaclas
 
         await self.ably.auth.authorize()
 
-        assert Auth.Method.TOKEN == self.ably.auth.auth_mechanism, "Authorise should change the Auth method"
+        assert Auth.Method.TOKEN == self.ably.auth.auth_mechanism, "Authorize should change the Auth method"
 
     # RSA10a
     @dont_vary_protocol
@@ -217,7 +216,7 @@ class TestAuthAuthorize(BaseAsyncTestCase, metaclass=VaryByProtocolTestsMetaclas
         token_called, auth_called = request_mock.call_args
         assert token_called[0] == token_params
 
-        # Authorise may call request_token with some default auth_options.
+        # Authorize may call request_token with some default auth_options.
         for arg, value in auth_params.items():
             assert auth_called[arg] == value, "%s called with wrong value: %s" % (arg, value)
 
@@ -319,20 +318,6 @@ class TestAuthAuthorize(BaseAsyncTestCase, metaclass=VaryByProtocolTestsMetaclas
         assert history.items[0].client_id == client_id
         await ably.close()
 
-    # RSA10l
-    @dont_vary_protocol
-    async def test_authorise(self):
-        with warnings.catch_warnings(record=True) as ws:
-            # Cause all warnings to always be triggered
-            warnings.simplefilter("always")
-
-            token = await self.ably.auth.authorise()
-            assert isinstance(token, TokenDetails)
-
-            # Verify warning is raised
-            ws = [w for w in ws if issubclass(w.category, DeprecationWarning)]
-            assert len(ws) == 1
-
 
 class TestRequestToken(BaseAsyncTestCase, metaclass=VaryByProtocolTestsMetaclass):
 
@@ -496,14 +481,14 @@ class TestRenewToken(BaseAsyncTestCase):
 
     async def asyncSetUp(self):
         self.test_vars = await TestApp.get_test_vars()
-        self.ably = await TestApp.get_ably_rest(use_binary_protocol=False)
+        self.host = 'fake-host.ably.io'
+        self.ably = await TestApp.get_ably_rest(use_binary_protocol=False, rest_host=self.host)
         # with headers
         self.publish_attempts = 0
         self.channel = uuid.uuid4().hex
-        host = self.test_vars['host']
         tokens = ['a_token', 'another_token']
         headers = {'Content-Type': 'application/json'}
-        self.mocked_api = respx.mock(base_url='https://{}'.format(host))
+        self.mocked_api = respx.mock(base_url='https://{}'.format(self.host))
         self.request_token_route = self.mocked_api.post(
             "/keys/{}/requestToken".format(self.test_vars["keys"][0]['key_name']),
             name="request_token_route")
@@ -561,6 +546,7 @@ class TestRenewToken(BaseAsyncTestCase):
 
         self.ably = await TestApp.get_ably_rest(
             key=None,
+            rest_host=self.host,
             token='token ID cannot be used to create a new token',
             use_binary_protocol=False)
         await self.ably.channels[self.channel].publish('evt', 'msg')
@@ -579,6 +565,7 @@ class TestRenewToken(BaseAsyncTestCase):
         token_details = TokenDetails(token='a_dummy_token')
         self.ably = await TestApp.get_ably_rest(
             key=None,
+            rest_host=self.host,
             token_details=token_details,
             use_binary_protocol=False)
         await self.ably.channels[self.channel].publish('evt', 'msg')
@@ -600,11 +587,11 @@ class TestRenewExpiredToken(BaseAsyncTestCase):
         self.publish_attempts = 0
         self.channel = uuid.uuid4().hex
 
-        host = self.test_vars['host']
+        self.host = 'fake-host.ably.io'
         key = self.test_vars["keys"][0]['key_name']
         headers = {'Content-Type': 'application/json'}
 
-        self.mocked_api = respx.mock(base_url='https://{}'.format(host))
+        self.mocked_api = respx.mock(base_url='https://{}'.format(self.host))
         self.request_token_route = self.mocked_api.post("/keys/{}/requestToken".format(key),
                                                         name="request_token_route")
         self.request_token_route.return_value = Response(
@@ -648,7 +635,7 @@ class TestRenewExpiredToken(BaseAsyncTestCase):
 
     # RSA4b1
     async def test_query_time_false(self):
-        ably = await TestApp.get_ably_rest()
+        ably = await TestApp.get_ably_rest(rest_host=self.host)
         await ably.auth.authorize()
         self.publish_fail = True
         await ably.channels[self.channel].publish('evt', 'msg')
@@ -657,7 +644,7 @@ class TestRenewExpiredToken(BaseAsyncTestCase):
 
     # RSA4b1
     async def test_query_time_true(self):
-        ably = await TestApp.get_ably_rest(query_time=True)
+        ably = await TestApp.get_ably_rest(query_time=True, rest_host=self.host)
         await ably.auth.authorize()
         self.publish_fail = False
         await ably.channels[self.channel].publish('evt', 'msg')
diff --git a/test/ably/rest/restchannelhistory_test.py b/test/ably/rest/restchannelhistory_test.py
index 30d94e9..d1ea159 100644
--- a/test/ably/rest/restchannelhistory_test.py
+++ b/test/ably/rest/restchannelhistory_test.py
@@ -14,7 +14,7 @@ log = logging.getLogger(__name__)
 class TestRestChannelHistory(BaseAsyncTestCase, metaclass=VaryByProtocolTestsMetaclass):
 
     async def asyncSetUp(self):
-        self.ably = await TestApp.get_ably_rest()
+        self.ably = await TestApp.get_ably_rest(fallback_hosts=[])
         self.test_vars = await TestApp.get_test_vars()
 
     async def asyncTearDown(self):
diff --git a/test/ably/rest/restchannelpublish_test.py b/test/ably/rest/restchannelpublish_test.py
index ed41552..6cf458e 100644
--- a/test/ably/rest/restchannelpublish_test.py
+++ b/test/ably/rest/restchannelpublish_test.py
@@ -279,9 +279,8 @@ class TestRestChannelPublish(BaseAsyncTestCase, metaclass=VaryByProtocolTestsMet
         # works if same
         channel = self.ably_with_client_id.channels[
             self.get_channel_name('persisted:with_client_id_identified_client')]
-        await channel.publish(name='publish',
-                              data='test',
-                              client_id=self.ably_with_client_id.client_id)
+        message = Message(name='publish', data='test', client_id=self.ably_with_client_id.client_id)
+        await channel.publish(message)
 
         history = await channel.history()
         messages = history.items
@@ -291,9 +290,10 @@ class TestRestChannelPublish(BaseAsyncTestCase, metaclass=VaryByProtocolTestsMet
 
         assert messages[0].client_id == self.ably_with_client_id.client_id
 
+        message = Message(name='publish', data='test', client_id='invalid')
         # fails if different
         with pytest.raises(IncompatibleClientIdException):
-            await channel.publish(name='publish', data='test', client_id='invalid')
+            await channel.publish(message)
 
     async def test_publish_message_with_wrong_client_id_on_implicit_identified_client(self):
         new_token = await self.ably.auth.authorize(token_params={'client_id': uuid.uuid4().hex})
@@ -304,8 +304,9 @@ class TestRestChannelPublish(BaseAsyncTestCase, metaclass=VaryByProtocolTestsMet
         channel = new_ably.channels[
             self.get_channel_name('persisted:wrong_client_id_implicit_client')]
 
+        message = Message(name='publish', data='test', client_id='invalid')
         with pytest.raises(AblyException) as excinfo:
-            await channel.publish(name='publish', data='test', client_id='invalid')
+            await channel.publish(message)
 
         assert 400 == excinfo.value.status_code
         assert 40012 == excinfo.value.code
@@ -324,8 +325,8 @@ class TestRestChannelPublish(BaseAsyncTestCase, metaclass=VaryByProtocolTestsMet
             self.get_channel_name('persisted:wildcard_client_id')]
         await channel.publish(name='publish1', data='no client_id')
         some_client_id = uuid.uuid4().hex
-        await channel.publish(name='publish2', data='some client_id',
-                              client_id=some_client_id)
+        message = Message(name='publish2', data='some client_id', client_id=some_client_id)
+        await channel.publish(message)
 
         history = await channel.history()
         messages = history.items
@@ -358,7 +359,8 @@ class TestRestChannelPublish(BaseAsyncTestCase, metaclass=VaryByProtocolTestsMet
                 'notification': {"title": "Testing"},
             }
         }
-        await channel.publish(name='test-name', data='test-data', extras=extras)
+        message = Message(name='test-name', data='test-data', extras=extras)
+        await channel.publish(message)
 
         # Get the history for this channel
         history = await channel.history()
@@ -527,10 +529,8 @@ class TestRestChannelPublishIdempotent(BaseAsyncTestCase, metaclass=VaryByProtoc
 
     # RSL1k4
     async def test_idempotent_library_generated_retry(self):
-        ably = await self.get_ably_rest(idempotent_rest_publishing=True)
-        if not ably.options.fallback_hosts:
-            host = ably.options.get_rest_host()
-            ably = await self.get_ably_rest(idempotent_rest_publishing=True, fallback_hosts=[host] * 3)
+        test_vars = await TestApp.get_test_vars()
+        ably = await self.get_ably_rest(idempotent_rest_publishing=True, fallback_hosts=[test_vars["host"]] * 3)
         channel = ably.channels[self.get_channel_name()]
 
         state = {'failures': 0}
diff --git a/test/ably/rest/resthttp_test.py b/test/ably/rest/resthttp_test.py
index bab4534..db219b5 100644
--- a/test/ably/rest/resthttp_test.py
+++ b/test/ably/rest/resthttp_test.py
@@ -94,11 +94,9 @@ class TestRestHttp(BaseAsyncTestCase):
         await ably.close()
 
     # RSC15f
-    # Ignore library warning regarding fallback_hosts_use_default
-    @pytest.mark.filterwarnings('ignore::DeprecationWarning')
     async def test_cached_fallback(self):
         timeout = 2000
-        ably = await TestApp.get_ably_rest(fallback_hosts_use_default=True, fallback_retry_timeout=timeout)
+        ably = await TestApp.get_ably_rest(fallback_retry_timeout=timeout)
         host = ably.options.get_rest_host()
 
         state = {'errors': 0}
diff --git a/test/ably/rest/restinit_test.py b/test/ably/rest/restinit_test.py
index 88a433d..10dd828 100644
--- a/test/ably/rest/restinit_test.py
+++ b/test/ably/rest/restinit_test.py
@@ -1,4 +1,3 @@
-import warnings
 from mock import patch
 import pytest
 from httpx import AsyncClient
@@ -91,8 +90,6 @@ class TestRestInit(BaseAsyncTestCase, metaclass=VaryByProtocolTestsMetaclass):
 
     # RSC15
     @dont_vary_protocol
-    # Ignore library warning regarding fallback_hosts_use_default
-    @pytest.mark.filterwarnings('ignore::DeprecationWarning')
     def test_fallback_hosts(self):
         # Specify the fallback_hosts (RSC15a)
         fallback_hosts = [
@@ -114,26 +111,12 @@ class TestRestInit(BaseAsyncTestCase, metaclass=VaryByProtocolTestsMetaclass):
         ably = AblyRest(token='foo', http_max_retry_count=10)
         assert sorted(Defaults.fallback_hosts) == sorted(ably.options.get_fallback_rest_hosts())
 
-        # Specify environment and fallback_hosts_use_default, no fallback hosts (RSC15g4)
-        # We specify http_max_retry_count=10 so all the fallback hosts get in the list
-        ably = AblyRest(token='foo', environment='not_considered', fallback_hosts_use_default=True,
-                        http_max_retry_count=10)
-        assert sorted(Defaults.fallback_hosts) == sorted(ably.options.get_fallback_rest_hosts())
-
         # RSC15f
         ably = AblyRest(token='foo')
         assert 600000 == ably.options.fallback_retry_timeout
         ably = AblyRest(token='foo', fallback_retry_timeout=1000)
         assert 1000 == ably.options.fallback_retry_timeout
 
-        with warnings.catch_warnings(record=True) as ws:
-            # Cause all warnings to always be triggered
-            warnings.simplefilter("always")
-            AblyRest(token='foo', fallback_hosts_use_default=True)
-            # Verify warning is raised for fallback_hosts_use_default
-            ws = [w for w in ws if issubclass(w.category, DeprecationWarning)]
-            assert len(ws) == 1
-
     @dont_vary_protocol
     def test_specified_realtime_host(self):
         ably = AblyRest(token='foo', realtime_host="some.other.host")
diff --git a/test/ably/rest/restrequest_test.py b/test/ably/rest/restrequest_test.py
index 78702bc..2824d57 100644
--- a/test/ably/rest/restrequest_test.py
+++ b/test/ably/rest/restrequest_test.py
@@ -1,5 +1,6 @@
 import httpx
 import pytest
+import respx
 
 from ably import AblyRest
 from ably.http.paginatedresult import HttpPaginatedResponse
@@ -90,8 +91,6 @@ class TestRestRequest(BaseAsyncTestCase, metaclass=VaryByProtocolTestsMetaclass)
 
     # RSC19e
     @dont_vary_protocol
-    # Ignore library warning regarding fallback_hosts_use_default
-    @pytest.mark.filterwarnings('ignore::DeprecationWarning')
     async def test_timeout(self):
         # Timeout
         timeout = 0.000001
@@ -101,17 +100,19 @@ class TestRestRequest(BaseAsyncTestCase, metaclass=VaryByProtocolTestsMetaclass)
             await ably.request('GET', '/time')
         await ably.close()
 
-        # Bad host, use fallback
-        ably = AblyRest(key=self.test_vars["keys"][0]["key_str"],
-                        rest_host='some.other.host',
-                        port=self.test_vars["port"],
-                        tls_port=self.test_vars["tls_port"],
-                        tls=self.test_vars["tls"],
-                        fallback_hosts_use_default=True)
-        result = await ably.request('GET', '/time')
-        assert isinstance(result, HttpPaginatedResponse)
-        assert len(result.items) == 1
-        assert isinstance(result.items[0], int)
+        default_endpoint = 'https://sandbox-rest.ably.io/time'
+        fallback_host = 'sandbox-a-fallback.ably-realtime.com'
+        fallback_endpoint = f'https://{fallback_host}/time'
+        ably = await TestApp.get_ably_rest(fallback_hosts=[fallback_host])
+        with respx.mock:
+            default_route = respx.get(default_endpoint)
+            fallback_route = respx.get(fallback_endpoint)
+            headers = {
+                "Content-Type": "application/json"
+            }
+            default_route.side_effect = httpx.ConnectError('')
+            fallback_route.return_value = httpx.Response(200, headers=headers, text='[123]')
+            await ably.request('GET', '/time')
         await ably.close()
 
         # Bad host, no Fallback
diff --git a/test/ably/testapp.py b/test/ably/testapp.py
index 80bfe92..86741f3 100644
--- a/test/ably/testapp.py
+++ b/test/ably/testapp.py
@@ -14,20 +14,21 @@ with open(os.path.dirname(__file__) + '/../assets/testAppSpec.json', 'r') as f:
     app_spec_local = json.loads(f.read())
 
 tls = (os.environ.get('ABLY_TLS') or "true").lower() == "true"
-host = os.environ.get('ABLY_HOST', 'sandbox-rest.ably.io')
-realtime_host = os.environ.get('ABLY_HOST', 'sandbox-realtime.ably.io')
-environment = os.environ.get('ABLY_ENV')
+rest_host = os.environ.get('ABLY_REST_HOST', 'sandbox-rest.ably.io')
+realtime_host = os.environ.get('ABLY_REALTIME_HOST', 'sandbox-realtime.ably.io')
+
+environment = os.environ.get('ABLY_ENV', 'sandbox')
 
 port = 80
 tls_port = 443
 
-if host and not host.endswith("rest.ably.io"):
-    tls = tls and host != "localhost"
+if rest_host and not rest_host.endswith("rest.ably.io"):
+    tls = tls and rest_host != "localhost"
     port = 8080
     tls_port = 8081
 
 
-ably = AblyRest(token='not_a_real_token', rest_host=host,
+ably = AblyRest(token='not_a_real_token',
                 port=port, tls_port=tls_port, tls=tls,
                 environment=environment,
                 use_binary_protocol=False)
@@ -48,7 +49,7 @@ class TestApp:
 
             test_vars = {
                 "app_id": app_id,
-                "host": host,
+                "host": rest_host,
                 "port": port,
                 "tls_port": tls_port,
                 "tls": tls,
@@ -71,14 +72,7 @@ class TestApp:
     @staticmethod
     async def get_ably_rest(**kw):
         test_vars = await TestApp.get_test_vars()
-        options = {
-            'key': test_vars["keys"][0]["key_str"],
-            'rest_host': test_vars["host"],
-            'port': test_vars["port"],
-            'tls_port': test_vars["tls_port"],
-            'tls': test_vars["tls"],
-            'environment': test_vars["environment"],
-        }
+        options = TestApp.get_options(test_vars, **kw)
         options.update(kw)
         return AblyRest(**options)
 
@@ -91,8 +85,6 @@ class TestApp:
     @staticmethod
     def get_options(test_vars, **kwargs):
         options = {
-            'realtime_host': test_vars["realtime_host"],
-            'rest_host': test_vars["host"],
             'port': test_vars["port"],
             'tls_port': test_vars["tls_port"],
             'tls': test_vars["tls"],
@@ -102,7 +94,11 @@ class TestApp:
         if not any(x in kwargs for x in auth_methods):
             options["key"] = test_vars["keys"][0]["key_str"]
 
+        if any(x in kwargs for x in ["rest_host", "realtime_host"]):
+            options["environment"] = None
+
         options.update(kwargs)
+
         return options
 
     @staticmethod