diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 9d203b246..a7102b0e9 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -151,7 +151,7 @@ class EPPLibWrapper: raise RegistryError(message) from err else: if response.code >= 2000: - raise RegistryError(response.msg, code=response.code) + raise RegistryError(response.msg, code=response.code, response=response) else: return response @@ -174,6 +174,8 @@ class EPPLibWrapper: try: return self._send(command) except RegistryError as err: + if err.response: + logger.info(f"cltrid is {err.response.cl_tr_id} svtrid is {err.response.sv_tr_id}") if ( err.is_transport_error() or err.is_connection_error() diff --git a/src/epplibwrapper/errors.py b/src/epplibwrapper/errors.py index 95db40ab8..234bed611 100644 --- a/src/epplibwrapper/errors.py +++ b/src/epplibwrapper/errors.py @@ -1,4 +1,4 @@ -from enum import IntEnum +from enum import IntEnum, Enum class ErrorCode(IntEnum): @@ -52,6 +52,10 @@ class ErrorCode(IntEnum): SESSION_LIMIT_EXCEEDED_SERVER_CLOSING_CONNECTION = 2502 +class RegistryErrorMessage(Enum): + REGISTRAR_NOT_LOGGED_IN = "Registrar is not logged in." + + class RegistryError(Exception): """ Overview of registry response codes from RFC 5730. See RFC 5730 for full text. @@ -62,14 +66,21 @@ class RegistryError(Exception): - 2501 - 2502 Something malicious or abusive may have occurred """ - def __init__(self, *args, code=None, note="", **kwargs): + def __init__(self, *args, code=None, note="", response=None, **kwargs): super().__init__(*args, **kwargs) self.code = code + self.response = response # note is a string that can be used to provide additional context self.note = note def should_retry(self): - return self.code == ErrorCode.COMMAND_FAILED + # COMMAND_USE_ERROR is returning with message, Registrar is not logged in, + # which can be recovered from with a retry + return self.code == ErrorCode.COMMAND_FAILED or ( + self.code == ErrorCode.COMMAND_USE_ERROR + and self.response + and getattr(self.response, "msg", None) == RegistryErrorMessage.REGISTRAR_NOT_LOGGED_IN.value + ) def is_transport_error(self): return self.code == ErrorCode.TRANSPORT_ERROR diff --git a/src/epplibwrapper/tests/test_client.py b/src/epplibwrapper/tests/test_client.py index 57c99a05f..2850ae316 100644 --- a/src/epplibwrapper/tests/test_client.py +++ b/src/epplibwrapper/tests/test_client.py @@ -264,6 +264,58 @@ class TestClient(TestCase): # send() is called 5 times: send(login), send(command) fail, send(logout), send(login), send(command) self.assertEquals(mock_send.call_count, 5) + @less_console_noise_decorator + @patch("epplibwrapper.client.Client") + @patch("epplibwrapper.client.logger") + def test_send_command_2002_failure_prompts_successful_retry(self, mock_logger, mock_client): + """Test when the send("InfoDomainCommand) call fails with a 2002, prompting a retry + and the subsequent send("InfoDomainCommand) call succeeds + Flow: + Initialization succeeds + Send command fails (with 2002 code) prompting retry + Client closes and re-initializes, and command succeeds""" + # Mock the Client instance and its methods + # connect() and close() should succeed throughout + mock_connect = MagicMock() + mock_close = MagicMock() + # create success and failure result messages + send_command_success_result = self.fake_result(1000, "Command completed successfully") + send_command_failure_result = self.fake_result(2002, "Registrar is not logged in.") + # side_effect for send call, initial send(login) succeeds during initialization, next send(command) + # fails, subsequent sends (logout, login, command) all succeed + send_call_count = 0 + + # Create a mock command + mock_command = MagicMock() + mock_command.__class__.__name__ = "InfoDomainCommand" + + def side_effect(*args, **kwargs): + nonlocal send_call_count + send_call_count += 1 + if send_call_count == 2: + return send_command_failure_result + else: + return send_command_success_result + + mock_send = MagicMock(side_effect=side_effect) + mock_client.return_value.connect = mock_connect + mock_client.return_value.close = mock_close + mock_client.return_value.send = mock_send + # Create EPPLibWrapper instance and initialize client + wrapper = EPPLibWrapper() + wrapper.send(mock_command, cleaned=True) + # connect() is called twice, once during initialization of app, once during retry + self.assertEquals(mock_connect.call_count, 2) + # close() is called once, during retry + mock_close.assert_called_once() + # send() is called 5 times: send(login), send(command) fail, send(logout), send(login), send(command) + self.assertEquals(mock_send.call_count, 5) + # Assertion proper logging; note that the + mock_logger.info.assert_any_call( + "InfoDomainCommand failed and will be retried Error: Registrar is not logged in." + ) + mock_logger.info.assert_any_call("cltrid is cl_tr_id svtrid is sv_tr_id") + @less_console_noise_decorator def fake_failure_send_concurrent_threads(self, command=None, cleaned=None): """