@@ -1024,69 +1024,238 @@ def test_databricks_connector_dialect_alias_is_registered(self):
10241024
10251025
10261026class 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