Skip to content

Commit 1f804b1

Browse files
committed
Add proper tests for requests retry mechanism
1 parent 790be77 commit 1f804b1

1 file changed

Lines changed: 208 additions & 39 deletions

File tree

tests/unit/test_sql_execution.py

Lines changed: 208 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,69 +1024,238 @@ def test_databricks_connector_dialect_alias_is_registered(self):
10241024

10251025

10261026
class TestCreateRetrySession(unittest.TestCase):
1027-
@mock.patch("deepnote_toolkit.sql.sql_execution._create_retry_session")
1027+
"""Tests that exercise the real urllib3 retry loop by mocking at the
1028+
connection level (``HTTPConnectionPool._make_request``) rather than
1029+
replacing ``_create_retry_session``. This lets the ``Retry`` adapter
1030+
actually fire retries on 5xx responses.
1031+
"""
1032+
1033+
def test_create_retry_session_configuration(self):
1034+
"""Verify the retry session is wired with the expected parameters."""
1035+
from deepnote_toolkit.sql.sql_execution import _create_retry_session
1036+
1037+
session = _create_retry_session()
1038+
1039+
for prefix in ("http://", "https://"):
1040+
adapter = session.get_adapter(f"{prefix}example.com")
1041+
retry = adapter.max_retries
1042+
1043+
self.assertEqual(retry.total, 3)
1044+
self.assertEqual(retry.backoff_factor, 0.5)
1045+
self.assertEqual(set(retry.status_forcelist), {500, 502, 503, 504})
1046+
self.assertIn("POST", retry.allowed_methods)
1047+
1048+
# -- _generate_temporary_credentials ------------------------------------
1049+
1050+
@mock.patch("urllib3.util.retry.Retry.sleep", return_value=None)
1051+
@mock.patch("urllib3.connectionpool.HTTPConnectionPool._make_request")
10281052
@mock.patch("deepnote_toolkit.sql.sql_execution.get_project_auth_headers")
10291053
@mock.patch("deepnote_toolkit.sql.sql_execution.get_absolute_userpod_api_url")
1030-
def test_generate_temporary_credentials_uses_retry_session(
1031-
self, mock_get_url, mock_get_headers, mock_create_session
1054+
def test_generate_credentials_retries_on_5xx_then_succeeds(
1055+
self,
1056+
mock_get_url,
1057+
mock_get_headers,
1058+
mock_make_request,
1059+
mock_retry_sleep,
10321060
):
1033-
"""Test that _generate_temporary_credentials uses a retry session."""
1061+
"""Two 5xx failures followed by a 200 - the retry loop must
1062+
transparently retry and ultimately return valid credentials."""
1063+
from urllib3 import HTTPResponse as Urllib3Response
1064+
10341065
from deepnote_toolkit.sql.sql_execution import _generate_temporary_credentials
10351066

10361067
mock_get_url.return_value = (
10371068
"https://api.example.com/integrations/credentials/test-id"
10381069
)
10391070
mock_get_headers.return_value = {"Authorization": "Bearer token"}
10401071

1041-
mock_session = mock.Mock()
1042-
mock_response = mock.Mock()
1043-
mock_response.json.return_value = {
1044-
"username": "user",
1045-
"password": "pass",
1046-
}
1047-
mock_session.post.return_value = mock_response
1048-
mock_create_session.return_value = mock_session
1072+
success_body = json.dumps({"username": "user", "password": "pass"}).encode()
1073+
mock_make_request.side_effect = [
1074+
Urllib3Response(
1075+
body=io.BytesIO(b"Internal Server Error"),
1076+
status=500,
1077+
headers={},
1078+
preload_content=False,
1079+
),
1080+
Urllib3Response(
1081+
body=io.BytesIO(b"Bad Gateway"),
1082+
status=502,
1083+
headers={},
1084+
preload_content=False,
1085+
),
1086+
Urllib3Response(
1087+
body=io.BytesIO(success_body),
1088+
status=200,
1089+
headers={"Content-Type": "application/json"},
1090+
preload_content=False,
1091+
),
1092+
]
1093+
1094+
result = _generate_temporary_credentials("test-id")
1095+
1096+
self.assertEqual(result, ("user", "pass"))
1097+
self.assertEqual(mock_make_request.call_count, 3)
1098+
self.assertEqual(mock_retry_sleep.call_count, 2)
1099+
1100+
@mock.patch("urllib3.util.retry.Retry.sleep", return_value=None)
1101+
@mock.patch("urllib3.connectionpool.HTTPConnectionPool._make_request")
1102+
@mock.patch("deepnote_toolkit.sql.sql_execution.get_project_auth_headers")
1103+
@mock.patch("deepnote_toolkit.sql.sql_execution.get_absolute_userpod_api_url")
1104+
def test_generate_credentials_exhausts_retries_on_persistent_5xx(
1105+
self,
1106+
mock_get_url,
1107+
mock_get_headers,
1108+
mock_make_request,
1109+
mock_retry_sleep,
1110+
):
1111+
"""All 4 attempts (1 original + 3 retries) return 500 -
1112+
must raise ``RetryError``."""
1113+
import requests
1114+
from urllib3 import HTTPResponse as Urllib3Response
10491115

1050-
_generate_temporary_credentials("test-id")
1116+
from deepnote_toolkit.sql.sql_execution import _generate_temporary_credentials
10511117

1052-
mock_create_session.assert_called_once()
1053-
mock_session.post.assert_called_once_with(
1054-
"https://api.example.com/integrations/credentials/test-id",
1055-
timeout=10,
1056-
headers={"Authorization": "Bearer token"},
1118+
mock_get_url.return_value = (
1119+
"https://api.example.com/integrations/credentials/test-id"
10571120
)
1121+
mock_get_headers.return_value = {"Authorization": "Bearer token"}
10581122

1059-
@mock.patch("deepnote_toolkit.sql.sql_execution._create_retry_session")
1123+
mock_make_request.side_effect = [
1124+
Urllib3Response(
1125+
body=io.BytesIO(b"Server Error"),
1126+
status=500,
1127+
headers={},
1128+
preload_content=False,
1129+
)
1130+
for _ in range(4)
1131+
]
1132+
1133+
with self.assertRaises(requests.exceptions.RetryError):
1134+
_generate_temporary_credentials("test-id")
1135+
1136+
self.assertEqual(mock_make_request.call_count, 4)
1137+
self.assertEqual(mock_retry_sleep.call_count, 3)
1138+
1139+
@mock.patch("urllib3.util.retry.Retry.sleep", return_value=None)
1140+
@mock.patch("urllib3.connectionpool.HTTPConnectionPool._make_request")
1141+
@mock.patch("deepnote_toolkit.sql.sql_execution.get_project_auth_headers")
1142+
@mock.patch("deepnote_toolkit.sql.sql_execution.get_absolute_userpod_api_url")
1143+
def test_generate_credentials_no_retry_on_4xx(
1144+
self,
1145+
mock_get_url,
1146+
mock_get_headers,
1147+
mock_make_request,
1148+
mock_retry_sleep,
1149+
):
1150+
"""A 400 is not in the retry status list - must fail immediately
1151+
without retrying."""
1152+
import requests
1153+
from urllib3 import HTTPResponse as Urllib3Response
1154+
1155+
from deepnote_toolkit.sql.sql_execution import _generate_temporary_credentials
1156+
1157+
mock_get_url.return_value = (
1158+
"https://api.example.com/integrations/credentials/test-id"
1159+
)
1160+
mock_get_headers.return_value = {"Authorization": "Bearer token"}
1161+
1162+
mock_make_request.side_effect = [
1163+
Urllib3Response(
1164+
body=io.BytesIO(b"Bad Request"),
1165+
status=400,
1166+
headers={},
1167+
preload_content=False,
1168+
),
1169+
]
1170+
1171+
with self.assertRaises(requests.exceptions.HTTPError):
1172+
_generate_temporary_credentials("test-id")
1173+
1174+
self.assertEqual(mock_make_request.call_count, 1)
1175+
mock_retry_sleep.assert_not_called()
1176+
1177+
# -- _get_federated_auth_credentials ------------------------------------
1178+
1179+
@mock.patch("urllib3.util.retry.Retry.sleep", return_value=None)
1180+
@mock.patch("urllib3.connectionpool.HTTPConnectionPool._make_request")
10601181
@mock.patch("deepnote_toolkit.sql.sql_execution.get_project_auth_headers")
10611182
@mock.patch("deepnote_toolkit.sql.sql_execution.get_absolute_userpod_api_url")
1062-
def test_get_federated_auth_credentials_uses_retry_session(
1063-
self, mock_get_url, mock_get_headers, mock_create_session
1183+
def test_federated_auth_retries_on_5xx_then_succeeds(
1184+
self,
1185+
mock_get_url,
1186+
mock_get_headers,
1187+
mock_make_request,
1188+
mock_retry_sleep,
10641189
):
1065-
"""Test that _get_federated_auth_credentials uses a retry session."""
1190+
"""A 503 followed by a 200 - retry loop must recover and return
1191+
valid ``FederatedAuthResponseData``."""
1192+
from urllib3 import HTTPResponse as Urllib3Response
1193+
10661194
from deepnote_toolkit.sql.sql_execution import _get_federated_auth_credentials
10671195

10681196
mock_get_url.return_value = (
10691197
"https://api.example.com/integrations/federated-auth-token/test-id"
10701198
)
10711199
mock_get_headers.return_value = {"Authorization": "Bearer token"}
10721200

1073-
mock_session = mock.Mock()
1074-
mock_response = mock.Mock()
1075-
mock_response.json.return_value = {
1076-
"integrationType": "trino",
1077-
"accessToken": "test-token",
1078-
}
1079-
mock_session.post.return_value = mock_response
1080-
mock_create_session.return_value = mock_session
1201+
success_body = json.dumps(
1202+
{"integrationType": "trino", "accessToken": "test-token"}
1203+
).encode()
1204+
mock_make_request.side_effect = [
1205+
Urllib3Response(
1206+
body=io.BytesIO(b"Service Unavailable"),
1207+
status=503,
1208+
headers={},
1209+
preload_content=False,
1210+
),
1211+
Urllib3Response(
1212+
body=io.BytesIO(success_body),
1213+
status=200,
1214+
headers={"Content-Type": "application/json"},
1215+
preload_content=False,
1216+
),
1217+
]
1218+
1219+
result = _get_federated_auth_credentials("test-id", "auth-context-token")
10811220

1082-
_get_federated_auth_credentials("test-id", "auth-context-token")
1221+
self.assertEqual(result.integrationType, "trino")
1222+
self.assertEqual(result.accessToken, "test-token")
1223+
self.assertEqual(mock_make_request.call_count, 2)
1224+
self.assertEqual(mock_retry_sleep.call_count, 1)
10831225

1084-
mock_create_session.assert_called_once()
1085-
mock_session.post.assert_called_once_with(
1086-
"https://api.example.com/integrations/federated-auth-token/test-id",
1087-
timeout=10,
1088-
headers={
1089-
"Authorization": "Bearer token",
1090-
"UserPodAuthContextToken": "auth-context-token",
1091-
},
1226+
@mock.patch("urllib3.util.retry.Retry.sleep", return_value=None)
1227+
@mock.patch("urllib3.connectionpool.HTTPConnectionPool._make_request")
1228+
@mock.patch("deepnote_toolkit.sql.sql_execution.get_project_auth_headers")
1229+
@mock.patch("deepnote_toolkit.sql.sql_execution.get_absolute_userpod_api_url")
1230+
def test_federated_auth_exhausts_retries_on_persistent_5xx(
1231+
self,
1232+
mock_get_url,
1233+
mock_get_headers,
1234+
mock_make_request,
1235+
mock_retry_sleep,
1236+
):
1237+
"""All 4 attempts return 504 - must raise ``RetryError``."""
1238+
import requests
1239+
from urllib3 import HTTPResponse as Urllib3Response
1240+
1241+
from deepnote_toolkit.sql.sql_execution import _get_federated_auth_credentials
1242+
1243+
mock_get_url.return_value = (
1244+
"https://api.example.com/integrations/federated-auth-token/test-id"
10921245
)
1246+
mock_get_headers.return_value = {"Authorization": "Bearer token"}
1247+
1248+
mock_make_request.side_effect = [
1249+
Urllib3Response(
1250+
body=io.BytesIO(b"Gateway Timeout"),
1251+
status=504,
1252+
headers={},
1253+
preload_content=False,
1254+
)
1255+
for _ in range(4)
1256+
]
1257+
1258+
with self.assertRaises(requests.exceptions.RetryError):
1259+
_get_federated_auth_credentials("test-id", "auth-context-token")
1260+
1261+
self.assertEqual(mock_make_request.call_count, 4)

0 commit comments

Comments
 (0)