Skip to content

Commit 0b69525

Browse files
committed
fix: auth_url handling and add timeout to once_async calls in realtime tests
- Replaced all `connection.once_async` calls with `asyncio.wait_for` to include a 5-second timeout. - Ensures tests fail gracefully if connection isn't established within the specified timeframe.
1 parent 61cedb0 commit 0b69525

6 files changed

Lines changed: 87 additions & 59 deletions

File tree

ably/rest/auth.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
from __future__ import annotations
2+
23
import base64
3-
from datetime import timedelta
44
import logging
55
import time
6-
from typing import Optional, TYPE_CHECKING, Union
76
import uuid
7+
from datetime import timedelta
8+
from typing import Optional, TYPE_CHECKING, Union
9+
from urllib.parse import urlparse, parse_qs
10+
811
import httpx
912

1013
from ably.types.options import Options
14+
1115
if TYPE_CHECKING:
1216
from ably.rest.rest import AblyRest
1317
from ably.realtime.realtime import AblyRealtime
@@ -23,7 +27,6 @@
2327

2428

2529
class Auth:
26-
2730
class Method:
2831
BASIC = "BASIC"
2932
TOKEN = "TOKEN"
@@ -271,8 +274,7 @@ async def create_token_request(self, token_params: Optional[dict | str] = None,
271274
if capability is not None:
272275
token_request['capability'] = str(Capability(capability))
273276

274-
token_request["client_id"] = (
275-
token_params.get('client_id') or self.client_id)
277+
token_request["client_id"] = token_params.get('client_id') or self.client_id
276278

277279
# Note: There is no expectation that the client
278280
# specifies the nonce; this is done by the library
@@ -388,17 +390,39 @@ def _random_nonce(self):
388390

389391
async def token_request_from_auth_url(self, method: str, url: str, token_params,
390392
headers, auth_params):
393+
# Parse URL to extract existing query parameters
394+
parsed_url = urlparse(url)
395+
url_params = {}
396+
if parsed_url.query:
397+
# Convert query parameters to a flat dictionary
398+
query_params = parse_qs(parsed_url.query)
399+
for key, values in query_params.items():
400+
# Take the last value if multiple values exist for the same key
401+
url_params[key] = values[-1]
402+
403+
# Reconstruct clean URL without query parameters
404+
clean_url = f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}"
405+
if parsed_url.fragment:
406+
clean_url += f"#{parsed_url.fragment}"
407+
391408
body = None
392409
params = None
393410
if method == 'GET':
394411
body = {}
395-
params = dict(auth_params, **token_params)
412+
# Merge URL params, auth_params, and token_params (later params override earlier ones)
413+
# we do this because httpx version has inconsistency and some versions override query params
414+
# that are specified in url string
415+
params = dict(url_params, **auth_params, **token_params)
396416
elif method == 'POST':
397417
if isinstance(auth_params, TokenDetails):
398418
auth_params = auth_params.to_dict()
399-
params = {}
419+
# For POST, URL params go in query string, auth_params and token_params go in body
420+
params = url_params
400421
body = dict(auth_params, **token_params)
401422

423+
# Use clean URL for the request
424+
url = clean_url
425+
402426
from ably.http.http import Response
403427
async with httpx.AsyncClient(http2=True) as client:
404428
resp = await client.request(method=method, url=url, headers=headers, params=params, data=body)
@@ -420,6 +444,6 @@ async def token_request_from_auth_url(self, method: str, url: str, token_params,
420444
token_request = response.text
421445
else:
422446
msg = 'auth_url responded with unacceptable content-type ' + content_type + \
423-
', should be either text/plain, application/jwt or application/json',
447+
', should be either text/plain, application/jwt or application/json',
424448
raise AblyAuthException(msg, 401, 40170)
425449
return token_request

test/ably/realtime/realtimeauth_test.py

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ async def auth_callback_failure(options, expect_failure=False):
3434
class TestRealtimeAuth(BaseAsyncTestCase):
3535
async def test_auth_valid_api_key(self):
3636
ably = await TestApp.get_ably_realtime()
37-
await ably.connection.once_async(ConnectionState.CONNECTED)
37+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
3838
assert ably.connection.error_reason is None
3939
response_time_ms = await ably.connection.ping()
4040
assert response_time_ms is not None
@@ -53,7 +53,7 @@ async def test_auth_with_token_string(self):
5353
rest = await TestApp.get_ably_rest()
5454
token_details = await rest.auth.request_token()
5555
ably = await TestApp.get_ably_realtime(token=token_details.token)
56-
await ably.connection.once_async(ConnectionState.CONNECTED)
56+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
5757
response_time_ms = await ably.connection.ping()
5858
assert response_time_ms is not None
5959
assert ably.connection.error_reason is None
@@ -71,7 +71,7 @@ async def test_auth_with_token_details(self):
7171
rest = await TestApp.get_ably_rest()
7272
token_details = await rest.auth.request_token()
7373
ably = await TestApp.get_ably_realtime(token_details=token_details)
74-
await ably.connection.once_async(ConnectionState.CONNECTED)
74+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
7575
response_time_ms = await ably.connection.ping()
7676
assert response_time_ms is not None
7777
assert ably.connection.error_reason is None
@@ -93,7 +93,7 @@ async def callback(params):
9393
return token_details
9494

9595
ably = await TestApp.get_ably_realtime(auth_callback=callback)
96-
await ably.connection.once_async(ConnectionState.CONNECTED)
96+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
9797
response_time_ms = await ably.connection.ping()
9898
assert response_time_ms is not None
9999
assert ably.connection.error_reason is None
@@ -107,7 +107,7 @@ async def callback(params):
107107
return token_details
108108

109109
ably = await TestApp.get_ably_realtime(auth_callback=callback)
110-
await ably.connection.once_async(ConnectionState.CONNECTED)
110+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
111111
response_time_ms = await ably.connection.ping()
112112
assert response_time_ms is not None
113113
assert ably.connection.error_reason is None
@@ -121,7 +121,7 @@ async def callback(params):
121121
return token_details.token
122122

123123
ably = await TestApp.get_ably_realtime(auth_callback=callback)
124-
await ably.connection.once_async(ConnectionState.CONNECTED)
124+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
125125
response_time_ms = await ably.connection.ping()
126126
assert response_time_ms is not None
127127
assert ably.connection.error_reason is None
@@ -144,7 +144,10 @@ async def test_auth_with_auth_url_json(self):
144144
url_path = f"{echo_url}/?type=json&body={urllib.parse.quote_plus(token_details_json)}"
145145

146146
ably = await TestApp.get_ably_realtime(auth_url=url_path)
147-
await ably.connection.once_async(ConnectionState.CONNECTED)
147+
await asyncio.wait_for(
148+
ably.connection.once_async(ConnectionState.CONNECTED),
149+
timeout=5,
150+
)
148151
response_time_ms = await ably.connection.ping()
149152
assert response_time_ms is not None
150153
assert ably.connection.error_reason is None
@@ -156,7 +159,7 @@ async def test_auth_with_auth_url_text_plain(self):
156159
url_path = f"{echo_url}/?type=text&body={token_details.token}"
157160

158161
ably = await TestApp.get_ably_realtime(auth_url=url_path)
159-
await ably.connection.once_async(ConnectionState.CONNECTED)
162+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
160163
response_time_ms = await ably.connection.ping()
161164
assert response_time_ms is not None
162165
assert ably.connection.error_reason is None
@@ -169,7 +172,7 @@ async def test_auth_with_auth_url_post(self):
169172

170173
ably = await TestApp.get_ably_realtime(auth_url=url_path, auth_method='POST',
171174
auth_params=token_details)
172-
await ably.connection.once_async(ConnectionState.CONNECTED)
175+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
173176
response_time_ms = await ably.connection.ping()
174177
assert response_time_ms is not None
175178
assert ably.connection.error_reason is None
@@ -183,7 +186,7 @@ async def callback(params):
183186
return token_details.token
184187

185188
ably = await TestApp.get_ably_realtime(auth_callback=callback)
186-
await ably.connection.once_async(ConnectionState.CONNECTED)
189+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
187190

188191
assert ably.connection.connection_manager.transport
189192
original_access_token = ably.connection.connection_manager.transport.params.get('accessToken')
@@ -307,7 +310,7 @@ async def callback(params):
307310
"action": ProtocolMessageAction.AUTH,
308311
}
309312

310-
await ably.connection.once_async(ConnectionState.CONNECTED)
313+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
311314
auth_future = asyncio.Future()
312315

313316
def on_update(state_change):
@@ -334,7 +337,7 @@ async def auth_callback(_):
334337

335338
ably = await TestApp.get_ably_realtime(auth_callback=auth_callback)
336339

337-
await ably.connection.once_async(ConnectionState.CONNECTED)
340+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
338341
original_token_details = ably.auth.token_details
339342
await ably.connection.once_async(ConnectionEvent.UPDATE)
340343
assert ably.auth.token_details is not original_token_details
@@ -496,7 +499,7 @@ async def callback(params):
496499
}
497500
}
498501

499-
await ably.connection.once_async(ConnectionState.CONNECTED)
502+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
500503
original_token_details = ably.auth.token_details
501504
assert ably.connection.connection_manager.transport
502505
await ably.connection.connection_manager.transport.on_protocol_message(msg)
@@ -511,7 +514,7 @@ async def test_renew_token_no_renew_means_provided_upon_disconnection(self):
511514

512515
ably = await TestApp.get_ably_realtime(token_details=token_details)
513516

514-
state_change = await ably.connection.once_async(ConnectionState.CONNECTED)
517+
state_change = await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
515518
msg = {
516519
"action": ProtocolMessageAction.DISCONNECTED,
517520
"error": {
@@ -544,7 +547,7 @@ async def callback(params):
544547
}
545548
}
546549

547-
await ably.connection.once_async(ConnectionState.CONNECTED)
550+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
548551
connection_key = ably.connection.connection_details.connection_key
549552
await ably.connection.connection_manager.transport.dispose()
550553
ably.connection.connection_manager.notify_state(ConnectionState.DISCONNECTED)
@@ -572,12 +575,12 @@ async def test_renew_token_no_renew_means_provided_on_resume(self):
572575
}
573576
}
574577

575-
await ably.connection.once_async(ConnectionState.CONNECTED)
578+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
576579
connection_key = ably.connection.connection_details.connection_key
577580
await ably.connection.connection_manager.transport.dispose()
578581
ably.connection.connection_manager.notify_state(ConnectionState.DISCONNECTED)
579582

580-
state_change = await ably.connection.once_async(ConnectionState.CONNECTED)
583+
state_change = await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
581584
assert ably.connection.connection_manager.transport.params["resume"] == connection_key
582585

583586
assert ably.connection.connection_manager.transport

test/ably/realtime/realtimechannel_test.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ async def test_channels_release(self):
3333

3434
async def test_channel_attach(self):
3535
ably = await TestApp.get_ably_realtime()
36-
await ably.connection.once_async(ConnectionState.CONNECTED)
36+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
3737
channel = ably.channels.get('my_channel')
3838
assert channel.state == ChannelState.INITIALIZED
3939
await channel.attach()
@@ -42,7 +42,7 @@ async def test_channel_attach(self):
4242

4343
async def test_channel_detach(self):
4444
ably = await TestApp.get_ably_realtime()
45-
await ably.connection.once_async(ConnectionState.CONNECTED)
45+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
4646
channel = ably.channels.get('my_channel')
4747
await channel.attach()
4848
await channel.detach()
@@ -62,7 +62,7 @@ def listener(message):
6262
else:
6363
second_message_future.set_result(message)
6464

65-
await ably.connection.once_async(ConnectionState.CONNECTED)
65+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
6666
channel = ably.channels.get('my_channel')
6767
await channel.attach()
6868
await channel.subscribe('event', listener)
@@ -91,7 +91,7 @@ def listener(msg: Message):
9191
if not message_future.done():
9292
message_future.set_result(msg)
9393

94-
await ably.connection.once_async(ConnectionState.CONNECTED)
94+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
9595
channel = ably.channels.get('my_channel')
9696
await channel.attach()
9797
await channel.subscribe('event', listener)
@@ -110,7 +110,7 @@ def listener(msg: Message):
110110

111111
async def test_subscribe_coroutine(self):
112112
ably = await TestApp.get_ably_realtime()
113-
await ably.connection.once_async(ConnectionState.CONNECTED)
113+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
114114
channel = ably.channels.get('my_channel')
115115
await channel.attach()
116116

@@ -138,7 +138,7 @@ async def listener(msg):
138138
# RTL7a
139139
async def test_subscribe_all_events(self):
140140
ably = await TestApp.get_ably_realtime()
141-
await ably.connection.once_async(ConnectionState.CONNECTED)
141+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
142142
channel = ably.channels.get('my_channel')
143143
await channel.attach()
144144

@@ -165,7 +165,7 @@ def listener(msg):
165165
# RTL7c
166166
async def test_subscribe_auto_attach(self):
167167
ably = await TestApp.get_ably_realtime()
168-
await ably.connection.once_async(ConnectionState.CONNECTED)
168+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
169169
channel = ably.channels.get('my_channel')
170170
assert channel.state == ChannelState.INITIALIZED
171171

@@ -181,7 +181,7 @@ def listener(_):
181181
# RTL8b
182182
async def test_unsubscribe(self):
183183
ably = await TestApp.get_ably_realtime()
184-
await ably.connection.once_async(ConnectionState.CONNECTED)
184+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
185185
channel = ably.channels.get('my_channel')
186186
await channel.attach()
187187

@@ -216,7 +216,7 @@ def listener(msg):
216216
# RTL8c
217217
async def test_unsubscribe_all(self):
218218
ably = await TestApp.get_ably_realtime()
219-
await ably.connection.once_async(ConnectionState.CONNECTED)
219+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
220220
channel = ably.channels.get('my_channel')
221221
await channel.attach()
222222

@@ -250,7 +250,7 @@ def listener(msg):
250250

251251
async def test_realtime_request_timeout_attach(self):
252252
ably = await TestApp.get_ably_realtime(realtime_request_timeout=2000)
253-
await ably.connection.once_async(ConnectionState.CONNECTED)
253+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
254254
original_send_protocol_message = ably.connection.connection_manager.send_protocol_message
255255

256256
async def new_send_protocol_message(msg):
@@ -268,7 +268,7 @@ async def new_send_protocol_message(msg):
268268

269269
async def test_realtime_request_timeout_detach(self):
270270
ably = await TestApp.get_ably_realtime(realtime_request_timeout=2000)
271-
await ably.connection.once_async(ConnectionState.CONNECTED)
271+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
272272
original_send_protocol_message = ably.connection.connection_manager.send_protocol_message
273273

274274
async def new_send_protocol_message(msg):
@@ -287,7 +287,7 @@ async def new_send_protocol_message(msg):
287287

288288
async def test_channel_detached_once_connection_closed(self):
289289
ably = await TestApp.get_ably_realtime(realtime_request_timeout=2000)
290-
await ably.connection.once_async(ConnectionState.CONNECTED)
290+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
291291
channel = ably.channels.get(random_string(5))
292292
await channel.attach()
293293

@@ -296,7 +296,7 @@ async def test_channel_detached_once_connection_closed(self):
296296

297297
async def test_channel_failed_once_connection_failed(self):
298298
ably = await TestApp.get_ably_realtime(realtime_request_timeout=2000)
299-
await ably.connection.once_async(ConnectionState.CONNECTED)
299+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
300300
channel = ably.channels.get(random_string(5))
301301
await channel.attach()
302302

@@ -307,7 +307,7 @@ async def test_channel_failed_once_connection_failed(self):
307307

308308
async def test_channel_suspended_once_connection_suspended(self):
309309
ably = await TestApp.get_ably_realtime(realtime_request_timeout=2000)
310-
await ably.connection.once_async(ConnectionState.CONNECTED)
310+
await asyncio.wait_for(ably.connection.once_async(ConnectionState.CONNECTED), timeout=5)
311311
channel = ably.channels.get(random_string(5))
312312
await channel.attach()
313313

0 commit comments

Comments
 (0)