@@ -592,9 +592,9 @@ def test_all_dataframes_serialize_to_parquet(self, key, df):
592592class TestFederatedAuth (unittest .TestCase ):
593593 @mock .patch ("deepnote_toolkit.sql.sql_execution.get_project_auth_headers" )
594594 @mock .patch ("deepnote_toolkit.sql.sql_execution.get_absolute_userpod_api_url" )
595- @mock .patch ("deepnote_toolkit.sql.sql_execution.requests.post " )
595+ @mock .patch ("deepnote_toolkit.sql.sql_execution._create_retry_session " )
596596 def test_get_federated_auth_credentials_returns_validated_response (
597- self , mock_post , mock_get_url , mock_get_headers
597+ self , mock_create_session , mock_get_url , mock_get_headers
598598 ):
599599 """Test that _get_federated_auth_credentials properly validates and returns response data."""
600600 from deepnote_toolkit .sql .sql_execution import _get_federated_auth_credentials
@@ -603,12 +603,14 @@ def test_get_federated_auth_credentials_returns_validated_response(
603603 mock_get_url .return_value = "https://api.example.com/integrations/federated-auth-token/test-integration-id"
604604 mock_get_headers .return_value = {"Authorization" : "Bearer project-token" }
605605
606+ mock_session = mock .Mock ()
606607 mock_response = mock .Mock ()
607608 mock_response .json .return_value = {
608609 "integrationType" : "trino" ,
609610 "accessToken" : "test-access-token-123" ,
610611 }
611- mock_post .return_value = mock_response
612+ mock_session .post .return_value = mock_response
613+ mock_create_session .return_value = mock_session
612614
613615 # Call the function
614616 result = _get_federated_auth_credentials (
@@ -621,7 +623,7 @@ def test_get_federated_auth_credentials_returns_validated_response(
621623 )
622624
623625 # Verify headers include both project auth and user pod auth context token
624- mock_post .assert_called_once_with (
626+ mock_session . post .assert_called_once_with (
625627 "https://api.example.com/integrations/federated-auth-token/test-integration-id" ,
626628 timeout = 10 ,
627629 headers = {
@@ -1019,3 +1021,241 @@ def test_databricks_connector_dialect_alias_is_registered(self):
10191021
10201022 self .assertEqual (url .drivername , "databricks+connector" )
10211023 self .assertIsNotNone (dialect_cls )
1024+
1025+
1026+ class TestCreateRetrySession (unittest .TestCase ):
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" )
1052+ @mock .patch ("deepnote_toolkit.sql.sql_execution.get_project_auth_headers" )
1053+ @mock .patch ("deepnote_toolkit.sql.sql_execution.get_absolute_userpod_api_url" )
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 ,
1060+ ):
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+
1065+ from deepnote_toolkit .sql .sql_execution import _generate_temporary_credentials
1066+
1067+ mock_get_url .return_value = (
1068+ "https://api.example.com/integrations/credentials/test-id"
1069+ )
1070+ mock_get_headers .return_value = {"Authorization" : "Bearer token" }
1071+
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
1115+
1116+ from deepnote_toolkit .sql .sql_execution import _generate_temporary_credentials
1117+
1118+ mock_get_url .return_value = (
1119+ "https://api.example.com/integrations/credentials/test-id"
1120+ )
1121+ mock_get_headers .return_value = {"Authorization" : "Bearer token" }
1122+
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" )
1181+ @mock .patch ("deepnote_toolkit.sql.sql_execution.get_project_auth_headers" )
1182+ @mock .patch ("deepnote_toolkit.sql.sql_execution.get_absolute_userpod_api_url" )
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 ,
1189+ ):
1190+ """A 503 followed by a 200 - retry loop must recover and return
1191+ valid ``FederatedAuthResponseData``."""
1192+ from urllib3 import HTTPResponse as Urllib3Response
1193+
1194+ from deepnote_toolkit .sql .sql_execution import _get_federated_auth_credentials
1195+
1196+ mock_get_url .return_value = (
1197+ "https://api.example.com/integrations/federated-auth-token/test-id"
1198+ )
1199+ mock_get_headers .return_value = {"Authorization" : "Bearer token" }
1200+
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" )
1220+
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 )
1225+
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"
1245+ )
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