@@ -1122,6 +1122,149 @@ def test_validate_token_with_retries_access_denied_reports_request_id(
11221122 assert get_auth_request_id () in errors [- 1 ]
11231123
11241124
1125+ @pytest .mark .parametrize (
1126+ ("status_code" , "retryable" ),
1127+ [
1128+ (400 , False ),
1129+ (404 , False ),
1130+ (422 , False ),
1131+ (408 , True ),
1132+ (429 , True ),
1133+ (500 , True ),
1134+ (503 , True ),
1135+ ],
1136+ )
1137+ def test_validate_reports_the_status_of_a_refused_request (
1138+ status_code : int , retryable : bool
1139+ ):
1140+ """A refused validation request reports its status and whether to retry.
1141+
1142+ Args:
1143+ status_code: The status the API answered with.
1144+ retryable: Whether trying again later may help.
1145+ """
1146+ client = _client ()
1147+ client .api .auth .me .side_effect = api_error (
1148+ status_code , "Malformed authentication request"
1149+ )
1150+
1151+ with pytest .raises (TokenValidationError ) as exc_info :
1152+ hosting ._validate ("fictional-token" , client .api )
1153+
1154+ assert not isinstance (exc_info .value , TokenAccessDeniedError )
1155+ assert str (exc_info .value ) == f"HTTP { status_code } "
1156+ assert exc_info .value .retryable is retryable
1157+
1158+
1159+ @pytest .mark .parametrize ("status_code" , [401 , 403 ])
1160+ def test_validate_does_not_call_a_rejected_token_retryable (status_code : int ):
1161+ """A token the control plane refused is not worth trying again.
1162+
1163+ Args:
1164+ status_code: The status the API answered with.
1165+ """
1166+ client = _client ()
1167+ client .api .auth .me .side_effect = api_error (status_code , "denied" )
1168+
1169+ with pytest .raises (TokenAccessDeniedError ) as exc_info :
1170+ hosting ._validate ("fictional-token" , client .api )
1171+
1172+ assert exc_info .value .retryable is False
1173+ assert isinstance (exc_info .value , TokenValidationError )
1174+
1175+
1176+ def test_token_access_denied_error_is_never_retryable ():
1177+ """The error is not retryable however it is built."""
1178+ assert TokenAccessDeniedError ("access denied" ).retryable is False
1179+ assert TokenAccessDeniedError ("access denied" , "req-1" ).retryable is False
1180+ assert TokenValidationError ("server error" ).retryable is True
1181+
1182+
1183+ @pytest .mark .parametrize ("error" , [APIConnectionError , APITimeoutError ])
1184+ def test_validate_retries_connection_failures (error : type [APIConnectionError ]):
1185+ """A request that never got an answer is worth trying again.
1186+
1187+ Args:
1188+ error: The connection failure the SDK raises.
1189+ """
1190+ client = _client ()
1191+ client .api .auth .me .side_effect = error ("no answer" , request = MagicMock ())
1192+
1193+ with pytest .raises (TokenValidationError ) as exc_info :
1194+ hosting ._validate ("fictional-token" , client .api )
1195+
1196+ assert exc_info .value .retryable is True
1197+
1198+
1199+ @pytest .mark .parametrize (
1200+ "detail" ,
1201+ [
1202+ "Invalid token fictional-token-abc123" ,
1203+ "Authorization: Bearer fictional-token-abc123 was malformed" ,
1204+ "fictional%2Dtoken%2Dabc123" ,
1205+ "fictional\\ u002dtoken\\ u002dabc123" ,
1206+ "fictional-\n token-abc123" ,
1207+ "Basic ZmljdGlvbmFsOnBhc3M= api_key=other-credential-xyz password=hunter2" ,
1208+ "x" * 5000 ,
1209+ ],
1210+ )
1211+ def test_validate_does_not_surface_the_api_detail (
1212+ caplog : pytest .LogCaptureFixture , detail : str
1213+ ):
1214+ """The API's explanation, which may echo the request, is never shown or logged.
1215+
1216+ Args:
1217+ caplog: Pytest log capture fixture.
1218+ detail: An explanation that echoes what the request carried.
1219+ """
1220+ caplog .set_level (logging .DEBUG )
1221+ client = _client ()
1222+ client .api .auth .me .side_effect = api_error (400 , detail )
1223+
1224+ with pytest .raises (TokenValidationError ) as exc_info :
1225+ hosting ._validate ("fictional-token-abc123" , client .api )
1226+
1227+ assert str (exc_info .value ) == "HTTP 400"
1228+ for record in caplog .records :
1229+ message = record .getMessage ()
1230+ assert detail [:20 ] not in message
1231+ assert "abc123" not in message
1232+ assert "other-credential-xyz" not in message
1233+ assert "hunter2" not in message
1234+
1235+
1236+ @pytest .mark .parametrize (
1237+ ("status_code" , "advises_retry" ), [(400 , False ), (429 , True ), (503 , True )]
1238+ )
1239+ def test_non_interactive_validation_failure_advises_retry_only_when_it_may_help (
1240+ mocker : MockerFixture ,
1241+ caplog : pytest .LogCaptureFixture ,
1242+ status_code : int ,
1243+ advises_retry : bool ,
1244+ ):
1245+ """Only a failure worth trying again is told to try again later.
1246+
1247+ Args:
1248+ mocker: Pytest mocker fixture.
1249+ caplog: Pytest log capture fixture.
1250+ status_code: The status the API answered with.
1251+ advises_retry: Whether the report suggests trying again.
1252+ """
1253+ client = _client ()
1254+ client .api .auth .me .side_effect = api_error (status_code , "Malformed request" )
1255+ mocker .patch ("reflex_cli.utils.hosting.new_client" , return_value = client .api )
1256+ delete = mocker .patch ("reflex_cli.utils.hosting.delete_token_from_config" )
1257+
1258+ with pytest .raises (click .exceptions .Exit ):
1259+ get_authenticated_client (token = "fictional-token" , interactive = False )
1260+
1261+ delete .assert_not_called ()
1262+ errors = [r .getMessage () for r in caplog .records if r .levelno == logging .ERROR ]
1263+ assert len (errors ) == 1
1264+ assert f"HTTP { status_code } (auth request id:" in errors [0 ]
1265+ assert ("Please try again later." in errors [0 ]) is advises_retry
1266+
1267+
11251268def test_validate_token_failure_carries_request_id_on_exception (
11261269 mocker : MockerFixture ,
11271270):
0 commit comments