diff --git a/src/Pipfile b/src/Pipfile index 8e43d1bab..b40a8c3ea 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -31,6 +31,7 @@ gevent = "*" fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"} geventconnpool = {git = "https://github.com/rasky/geventconnpool.git", ref = "1bbb93a714a331a069adf27265fe582d9ba7ecd4"} pyzipper="*" +tblib = "*" [dev-packages] django-debug-toolbar = "*" diff --git a/src/Pipfile.lock b/src/Pipfile.lock index c410630e1..789422c5b 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8c15011f6c6e0447e4ca675ce840fe6b67048e90255e7c083be357b373f96a47" + "sha256": "8094a1c9461f860e928b51542adf891c0f6f6c4c62bd1bd8ac3bba55a67f918d" }, "pipfile-spec": 6, "requires": {}, @@ -32,20 +32,20 @@ }, "boto3": { "hashes": [ - "sha256:66303b5f26d92afb72656ff490b22ea72dfff8bf1a29e4a0c5d5f11ec56245dd", - "sha256:898ad2123b18cae8efd85adc56ac2d1925be54592aebc237020d4f16e9a9e7a9" + "sha256:300888f0c1b6f32f27f85a9aa876f50f46514ec619647af7e4d20db74d339714", + "sha256:b26928f9a21cf3649cea20a59061340f3294c6e7785ceb6e1a953eb8010dc3ba" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.52" + "version": "==1.34.56" }, "botocore": { "hashes": [ - "sha256:05567d8aba344826060481ea309555432c96f0febe22bee7cf5a3b6d3a03cec8", - "sha256:187da93aec3f2e87d8a31eced16fa2cb9c71fe2d69b0a797f9f7a9220f5bf7ae" + "sha256:bffeb71ab21d47d4ecf947d9bdb2fbd1b0bbd0c27742cea7cf0b77b701c41d9f", + "sha256:fff66e22a5589c2d58fba57d1d95c334ce771895e831f80365f6cff6453285ec" ], "markers": "python_version >= '3.8'", - "version": "==1.34.52" + "version": "==1.34.56" }, "cachetools": { "hashes": [ @@ -330,11 +330,11 @@ }, "django-csp": { "hashes": [ - "sha256:01443a07723f9a479d498bd7bb63571aaa771e690f64bde515db6cdb76e8041a", - "sha256:01eda02ad3f10261c74131cdc0b5a6a62b7c7ad4fd017fbefb7a14776e0a9727" + "sha256:19b2978b03fcd73517d7d67acbc04fbbcaec0facc3e83baa502965892d1e0719", + "sha256:ef0f1a9f7d8da68ae6e169c02e9ac661c0ecf04db70e0d1d85640512a68471c0" ], "index": "pypi", - "version": "==3.7" + "version": "==3.8" }, "django-fsm": { "hashes": [ @@ -376,20 +376,20 @@ "django" ], "hashes": [ - "sha256:cc421ddb143fa30183568164755aa113a160e555cd19e97e664c478662032c24", - "sha256:feeaf28f17fd0499f9cd7c0fcf408c6d82c308e69e335eb92d09322fc9ed8138" + "sha256:069727a8f73d8ba8d033d3cd95c0da231d44f38f1da773bf076cef168d312ee8", + "sha256:e0bcfd41c718c07a7db422f9109e490746450da38793fe4ee197f397b9343435" ], "markers": "python_version >= '3.8'", - "version": "==10.3.0" + "version": "==11.0.0" }, "faker": { "hashes": [ - "sha256:117ce1a2805c1bc5ca753b3dc6f9d567732893b2294b827d3164261ee8f20267", - "sha256:458d93580de34403a8dec1e8d5e6be2fee96c4deca63b95d71df7a6a80a690de" + "sha256:2456d674f40bd51eb3acbf85221277027822e529a90cc826453d9a25dff932b1", + "sha256:ea6f784c40730de0f77067e49e78cdd590efb00bec3d33f577492262206c17fc" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==23.3.0" + "version": "==24.0.0" }, "fred-epplib": { "git": "https://github.com/cisagov/epplib.git", @@ -460,7 +460,7 @@ }, "geventconnpool": { "git": "https://github.com/rasky/geventconnpool.git", - "ref": null + "ref": "1bbb93a714a331a069adf27265fe582d9ba7ecd4" }, "greenlet": { "hashes": [ @@ -712,11 +712,11 @@ }, "marshmallow": { "hashes": [ - "sha256:20f53be28c6e374a711a16165fb22a8dc6003e3f7cda1285e3ca777b9193885b", - "sha256:e7997f83571c7fd476042c2c188e4ee8a78900ca5e74bd9c8097afa56624e9bd" + "sha256:4e65e9e0d80fc9e609574b9983cf32579f305c718afb30d7233ab818571768c3", + "sha256:f085493f79efb0644f270a9bf2892843142d80d7174bbbd2f3713f2a589dc633" ], "markers": "python_version >= '3.8'", - "version": "==3.21.0" + "version": "==3.21.1" }, "oic": { "hashes": [ @@ -984,11 +984,11 @@ }, "python-dateutil": { "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" + "version": "==2.9.0.post0" }, "python-dotenv": { "hashes": [ @@ -1048,6 +1048,15 @@ "markers": "python_version >= '3.5'", "version": "==0.4.4" }, + "tblib": { + "hashes": [ + "sha256:80a6c77e59b55e83911e1e607c649836a69c103963c5f28a46cbeef44acf8129", + "sha256:93622790a0a29e04f0346458face1e144dc4d32f493714c6c3dff82a4adb77e6" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==3.0.0" + }, "typing-extensions": { "hashes": [ "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", @@ -1190,12 +1199,12 @@ }, "boto3": { "hashes": [ - "sha256:66303b5f26d92afb72656ff490b22ea72dfff8bf1a29e4a0c5d5f11ec56245dd", - "sha256:898ad2123b18cae8efd85adc56ac2d1925be54592aebc237020d4f16e9a9e7a9" + "sha256:300888f0c1b6f32f27f85a9aa876f50f46514ec619647af7e4d20db74d339714", + "sha256:b26928f9a21cf3649cea20a59061340f3294c6e7785ceb6e1a953eb8010dc3ba" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.52" + "version": "==1.34.56" }, "boto3-mocking": { "hashes": [ @@ -1208,28 +1217,28 @@ }, "boto3-stubs": { "hashes": [ - "sha256:644381a404fb5884154f7dcc40bb819f0c7f37de21b7a7b493585277b51c9a5f", - "sha256:823c41059f836d6877daaa1cbd20f813c8f1a78b9fdf290bc0b853127e127ba3" + "sha256:5ee40bdfba94fcdba26f36869339c849e918827ed1fb2f8e470474e6b1e923ff", + "sha256:cbbae1b811b97e4e1f1d00eba237ff987678e652502226b87e6276f7963935b4" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.52" + "version": "==1.34.55" }, "botocore": { "hashes": [ - "sha256:05567d8aba344826060481ea309555432c96f0febe22bee7cf5a3b6d3a03cec8", - "sha256:187da93aec3f2e87d8a31eced16fa2cb9c71fe2d69b0a797f9f7a9220f5bf7ae" + "sha256:bffeb71ab21d47d4ecf947d9bdb2fbd1b0bbd0c27742cea7cf0b77b701c41d9f", + "sha256:fff66e22a5589c2d58fba57d1d95c334ce771895e831f80365f6cff6453285ec" ], "markers": "python_version >= '3.8'", - "version": "==1.34.52" + "version": "==1.34.56" }, "botocore-stubs": { "hashes": [ - "sha256:8748b9fe01f66bb1e7b13f45e3336e2e2c5460d232816d45941573425459c66e", - "sha256:d0f4d9859d9f6affbe4b0b46e37fe729860eaab55ebefe7e09cf567396b2feda" + "sha256:018e001e3add5eb1828ef444b45fb8c9faf695e08334031bf2d96853cd9af703", + "sha256:25468ba6983987b704b1856bb155f297f576e6d4a690b021ab0c7122889ba907" ], "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==1.34.51" + "version": "==1.34.56" }, "click": { "hashes": [ @@ -1438,11 +1447,11 @@ }, "python-dateutil": { "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" + "version": "==2.9.0.post0" }, "pyyaml": { "hashes": [ @@ -1559,11 +1568,11 @@ }, "types-awscrt": { "hashes": [ - "sha256:10245570c7285e949362b4ae710c54bf285d64a27453d42762477bcee5cd77a3", - "sha256:73be0a2720d6f76b924df6917d4edf4c9958f83e5c25bf7d9f0c1e9cdf836941" + "sha256:61811bbf4de95248939f9276a434be93d2b95f6ccfe8aa94e56999e9778cfcc2", + "sha256:79d5bfb01f64701b6cf442e89a37d9c4dc6dbb79a46f2f611739b2418d30ecfd" ], "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==0.20.4" + "version": "==0.20.5" }, "types-cachetools": { "hashes": [ diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index 9ed437aef..a7856298b 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -2,10 +2,6 @@ import logging -from time import sleep -from gevent import Timeout -from epplibwrapper.utility.pool_status import PoolStatus - try: from epplib.client import Client from epplib import commands @@ -18,8 +14,6 @@ from django.conf import settings from .cert import Cert, Key from .errors import ErrorCode, LoginError, RegistryError -from .socket import Socket -from .utility.pool import EPPConnectionPool logger = logging.getLogger(__name__) @@ -43,8 +37,12 @@ class EPPLibWrapper: ATTN: This should not be used directly. Use `Domain` from domain.py. """ - def __init__(self, start_connection_pool=True) -> None: + def __init__(self) -> None: """Initialize settings which will be used for all connections.""" + # set _client to None initially. In the event that the __init__ fails + # before _client initializes, app should still start and be in a state + # that it can attempt _client initialization on send attempts + self._client = None # type: ignore # prepare (but do not send) a Login command self._login = commands.Login( cl_id=settings.SECRET_REGISTRY_CL_ID, @@ -54,9 +52,19 @@ class EPPLibWrapper: "urn:ietf:params:xml:ns:contact-1.0", ], ) + try: + self._initialize_client() + except Exception: + logger.warning("Unable to configure epplib. Registrar cannot contact registry.") + def _initialize_client(self) -> None: + """Initialize a client, assuming _login defined. Sets _client to initialized + client. Raises errors if initialization fails. + This method will be called at app initialization, and also during retries.""" # establish a client object with a TCP socket transport - self._client = Client( + # note that type: ignore added in several places because linter complains + # about _client initially being set to None, and None type doesn't match code + self._client = Client( # type: ignore SocketTransport( settings.SECRET_REGISTRY_HOSTNAME, cert_file=CERT.filename, @@ -64,176 +72,95 @@ class EPPLibWrapper: password=settings.SECRET_REGISTRY_KEY_PASSPHRASE, ) ) + try: + # use the _client object to connect + self._client.connect() # type: ignore + response = self._client.send(self._login) # type: ignore + if response.code >= 2000: # type: ignore + self._client.close() # type: ignore + raise LoginError(response.msg) # type: ignore + except TransportError as err: + message = "_initialize_client failed to execute due to a connection error." + logger.error(f"{message} Error: {err}") + raise RegistryError(message, code=ErrorCode.TRANSPORT_ERROR) from err + except LoginError as err: + raise err + except Exception as err: + message = "_initialize_client failed to execute due to an unknown error." + logger.error(f"{message} Error: {err}") + raise RegistryError(message) from err - self.pool_options = { - # Pool size - "size": settings.EPP_CONNECTION_POOL_SIZE, - # Which errors the pool should look out for. - # Avoid changing this unless necessary, - # it can and will break things. - "exc_classes": (TransportError,), - # Occasionally pings the registry to keep the connection alive. - # Value in seconds => (keepalive / size) - "keepalive": settings.POOL_KEEP_ALIVE, - } - - self._pool = None - - # Tracks the status of the pool - self.pool_status = PoolStatus() - - if start_connection_pool: - self.start_connection_pool() + def _disconnect(self) -> None: + """Close the connection.""" + try: + self._client.send(commands.Logout()) # type: ignore + self._client.close() # type: ignore + except Exception: + logger.warning("Connection to registry was not cleanly closed.") def _send(self, command): """Helper function used by `send`.""" cmd_type = command.__class__.__name__ - # Start a timeout to check if the pool is hanging - timeout = Timeout(settings.POOL_TIMEOUT) - timeout.start() - try: - if not self.pool_status.connection_success: - raise LoginError("Couldn't connect to the registry after three attempts") - with self._pool.get() as connection: - response = connection.send(command) - except Timeout as t: - # If more than one pool exists, - # multiple timeouts can be floating around. - # We need to be specific as to which we are targeting. - if t is timeout: - # Flag that the pool is frozen, - # then restart the pool. - self.pool_status.pool_hanging = True - logger.error("Pool timed out") - self.start_connection_pool() + # check for the condition that the _client was not initialized properly + # at app initialization + if self._client is None: + self._initialize_client() + response = self._client.send(command) except (ValueError, ParsingError) as err: message = f"{cmd_type} failed to execute due to some syntax error." - logger.error(f"{message} Error: {err}", exc_info=True) + logger.error(f"{message} Error: {err}") raise RegistryError(message) from err except TransportError as err: message = f"{cmd_type} failed to execute due to a connection error." - logger.error(f"{message} Error: {err}", exc_info=True) + logger.error(f"{message} Error: {err}") raise RegistryError(message, code=ErrorCode.TRANSPORT_ERROR) from err except LoginError as err: # For linter due to it not liking this line length text = "failed to execute due to a registry login error." message = f"{cmd_type} {text}" - logger.error(f"{message} Error: {err}", exc_info=True) + logger.error(f"{message} Error: {err}") raise RegistryError(message) from err except Exception as err: message = f"{cmd_type} failed to execute due to an unknown error." - logger.error(f"{message} Error: {err}", exc_info=True) + logger.error(f"{message} Error: {err}") raise RegistryError(message) from err else: if response.code >= 2000: raise RegistryError(response.msg, code=response.code) else: return response - finally: - # Close the timeout no matter what happens - timeout.close() + + def _retry(self, command): + """Retry sending a command through EPP by re-initializing the client + and then sending the command.""" + # re-initialize by disconnecting and initial + self._disconnect() + self._initialize_client() + return self._send(command) def send(self, command, *, cleaned=False): - """Login, send the command, then close the connection. Tries 3 times.""" + """Login, the send the command. Retry once if an error is found""" # try to prevent use of this method without appropriate safeguards + cmd_type = command.__class__.__name__ if not cleaned: raise ValueError("Please sanitize user input before sending it.") - - # Reopen the pool if its closed - # Only occurs when a login error is raised, after connection is successful - if not self.pool_status.pool_running: - # We want to reopen the connection pool, - # but we don't want the end user to wait while it opens. - # Raise syntax doesn't allow this, so we use a try/catch - # block. - try: - logger.error("Can't contact the Registry. Pool was not running.") - raise RegistryError("Can't contact the Registry. Pool was not running.") - except RegistryError as err: + try: + return self._send(command) + except RegistryError as err: + if ( + err.is_transport_error() + or err.is_connection_error() + or err.is_session_error() + or err.is_server_error() + or err.should_retry() + ): + message = f"{cmd_type} failed and will be retried" + logger.info(f"{message} Error: {err}") + return self._retry(command) + else: raise err - finally: - # Code execution will halt after here. - # The end user will need to recall .send. - self.start_connection_pool() - - counter = 0 # we'll try 3 times - while True: - try: - return self._send(command) - except RegistryError as err: - if counter < 3 and (err.should_retry() or err.is_transport_error()): - logger.info(f"Retrying transport error. Attempt #{counter+1} of 3.") - counter += 1 - sleep((counter * 50) / 1000) # sleep 50 ms to 150 ms - else: # don't try again - raise err - - def get_pool(self): - """Get the current pool instance""" - return self._pool - - def _create_pool(self, client, login, options): - """Creates and returns new pool instance""" - logger.info("New pool was created") - return EPPConnectionPool(client, login, options) - - def start_connection_pool(self, restart_pool_if_exists=True): - """Starts a connection pool for the registry. - - restart_pool_if_exists -> bool: - If an instance of the pool already exists, - then then that instance will be killed first. - It is generally recommended to keep this enabled. - """ - # Since we reuse the same creds for each pool, we can test on - # one socket, and if successful, then we know we can connect. - if not self._test_registry_connection_success(): - logger.warning("start_connection_pool() -> Cannot contact the Registry") - self.pool_status.connection_success = False - else: - self.pool_status.connection_success = True - - # If this function is reinvoked, then ensure - # that we don't have duplicate data sitting around. - if self._pool is not None and restart_pool_if_exists: - logger.info("Connection pool restarting...") - self.kill_pool() - logger.info("Old pool killed") - - self._pool = self._create_pool(self._client, self._login, self.pool_options) - - self.pool_status.pool_running = True - self.pool_status.pool_hanging = False - - logger.info("Connection pool started") - - def kill_pool(self): - """Kills the existing pool. Use this instead - of self._pool = None, as that doesn't clear - gevent instances.""" - if self._pool is not None: - self._pool.kill_all_connections() - self._pool = None - self.pool_status.pool_running = False - return None - logger.info("kill_pool() was invoked but there was no pool to delete") - - def _test_registry_connection_success(self): - """Check that determines if our login - credentials are valid, and/or if the Registrar - can be contacted - """ - # This is closed in test_connection_success - socket = Socket(self._client, self._login) - can_login = False - - # Something went wrong if this doesn't exist - if hasattr(socket, "test_connection_success"): - can_login = socket.test_connection_success() - - return can_login try: @@ -241,5 +168,4 @@ try: CLIENT = EPPLibWrapper() logger.info("registry client initialized") except Exception: - CLIENT = None # type: ignore - logger.warning("Unable to configure epplib. Registrar cannot contact registry.", exc_info=True) + logger.warning("Unable to configure epplib. Registrar cannot contact registry.") diff --git a/src/epplibwrapper/socket.py b/src/epplibwrapper/socket.py deleted file mode 100644 index 79c44aa9a..000000000 --- a/src/epplibwrapper/socket.py +++ /dev/null @@ -1,102 +0,0 @@ -import logging -from time import sleep - -try: - from epplib import commands - from epplib.client import Client -except ImportError: - pass - -from .errors import LoginError - - -logger = logging.getLogger(__name__) - - -class Socket: - """Context manager which establishes a TCP connection with registry.""" - - def __init__(self, client: Client, login: commands.Login) -> None: - """Save the epplib client and login details.""" - self.client = client - self.login = login - - def __enter__(self): - """Runs connect(), which opens a connection with EPPLib.""" - self.connect() - - def __exit__(self, *args, **kwargs): - """Runs disconnect(), which closes a connection with EPPLib.""" - self.disconnect() - - def connect(self): - """Use epplib to connect.""" - logger.info("Opening socket on connection pool") - self.client.connect() - response = self.client.send(self.login) - if self.is_login_error(response.code): - self.client.close() - raise LoginError(response.msg) - return self.client - - def disconnect(self): - """Close the connection.""" - logger.info("Closing socket on connection pool") - try: - self.client.send(commands.Logout()) - self.client.close() - except Exception as err: - logger.warning("Connection to registry was not cleanly closed.") - logger.error(err) - - def send(self, command): - """Sends a command to the registry. - If the RegistryError code is >= 2000, - then this function raises a LoginError. - The calling function should handle this.""" - response = self.client.send(command) - if self.is_login_error(response.code): - self.client.close() - raise LoginError(response.msg) - - return response - - def is_login_error(self, code): - """Returns the result of code >= 2000 for RegistryError. - This indicates that something weird happened on the Registry, - and that we should return a LoginError.""" - return code >= 2000 - - def test_connection_success(self): - """Tests if a successful connection can be made with the registry. - Tries 3 times.""" - # Something went wrong if this doesn't exist - if not hasattr(self.client, "connect"): - logger.warning("self.client does not have a connect attribute") - return False - - counter = 0 # we'll try 3 times - while True: - try: - self.client.connect() - response = self.client.send(self.login) - except (LoginError, OSError) as err: - logger.error(err) - should_retry = True - if isinstance(err, LoginError): - should_retry = err.should_retry() - if should_retry and counter < 3: - counter += 1 - sleep((counter * 50) / 1000) # sleep 50 ms to 150 ms - else: # don't try again - return False - else: - # If we encounter a login error, fail - if self.is_login_error(response.code): - logger.warning("A login error was found in test_connection_success") - return False - - # Otherwise, just return true - return True - finally: - self.disconnect() diff --git a/src/epplibwrapper/tests/test_client.py b/src/epplibwrapper/tests/test_client.py new file mode 100644 index 000000000..f95b37dcd --- /dev/null +++ b/src/epplibwrapper/tests/test_client.py @@ -0,0 +1,257 @@ +from unittest.mock import MagicMock, patch +from django.test import TestCase +from epplibwrapper.client import EPPLibWrapper +from epplibwrapper.errors import RegistryError, LoginError +from .common import less_console_noise +import logging + +try: + from epplib.exceptions import TransportError + from epplib.responses import Result +except ImportError: + pass + +logger = logging.getLogger(__name__) + + +class TestClient(TestCase): + """Test the EPPlibwrapper client""" + + def fake_result(self, code, msg): + """Helper function to create a fake Result object""" + return Result(code=code, msg=msg, res_data=[], cl_tr_id="cl_tr_id", sv_tr_id="sv_tr_id") + + @patch("epplibwrapper.client.Client") + def test_initialize_client_success(self, mock_client): + """Test when the initialize_client is successful""" + with less_console_noise(): + # Mock the Client instance and its methods + mock_connect = MagicMock() + # Create a mock Result instance + mock_result = MagicMock(spec=Result) + mock_result.code = 200 + mock_result.msg = "Success" + mock_result.res_data = ["data1", "data2"] + mock_result.cl_tr_id = "client_id" + mock_result.sv_tr_id = "server_id" + mock_send = MagicMock(return_value=mock_result) + mock_client.return_value.connect = mock_connect + mock_client.return_value.send = mock_send + + # Create EPPLibWrapper instance and initialize client + wrapper = EPPLibWrapper() + + # Assert that connect method is called once + mock_connect.assert_called_once() + # Assert that _client is not None after initialization + self.assertIsNotNone(wrapper._client) + + @patch("epplibwrapper.client.Client") + def test_initialize_client_transport_error(self, mock_client): + """Test when the send(login) step of initialize_client raises a TransportError.""" + with less_console_noise(): + # Mock the Client instance and its methods + mock_connect = MagicMock() + mock_send = MagicMock(side_effect=TransportError("Transport error")) + mock_client.return_value.connect = mock_connect + mock_client.return_value.send = mock_send + + with self.assertRaises(RegistryError): + # Create EPPLibWrapper instance and initialize client + # if functioning as expected, initial __init__ should except + # and log any Exception raised + wrapper = EPPLibWrapper() + # so call _initialize_client a second time directly to test + # the raised exception + wrapper._initialize_client() + + @patch("epplibwrapper.client.Client") + def test_initialize_client_login_error(self, mock_client): + """Test when the send(login) step of initialize_client returns (2400) comamnd failed code.""" + with less_console_noise(): + # Mock the Client instance and its methods + mock_connect = MagicMock() + # Create a mock Result instance + mock_result = MagicMock(spec=Result) + mock_result.code = 2400 + mock_result.msg = "Login failed" + mock_result.res_data = ["data1", "data2"] + mock_result.cl_tr_id = "client_id" + mock_result.sv_tr_id = "server_id" + mock_send = MagicMock(return_value=mock_result) + mock_client.return_value.connect = mock_connect + mock_client.return_value.send = mock_send + + with self.assertRaises(LoginError): + # Create EPPLibWrapper instance and initialize client + # if functioning as expected, initial __init__ should except + # and log any Exception raised + wrapper = EPPLibWrapper() + # so call _initialize_client a second time directly to test + # the raised exception + wrapper._initialize_client() + + @patch("epplibwrapper.client.Client") + def test_initialize_client_unknown_exception(self, mock_client): + """Test when the send(login) step of initialize_client raises an unexpected Exception.""" + with less_console_noise(): + # Mock the Client instance and its methods + mock_connect = MagicMock() + mock_send = MagicMock(side_effect=Exception("Unknown exception")) + mock_client.return_value.connect = mock_connect + mock_client.return_value.send = mock_send + + with self.assertRaises(RegistryError): + # Create EPPLibWrapper instance and initialize client + # if functioning as expected, initial __init__ should except + # and log any Exception raised + wrapper = EPPLibWrapper() + # so call _initialize_client a second time directly to test + # the raised exception + wrapper._initialize_client() + + @patch("epplibwrapper.client.Client") + def test_initialize_client_fails_recovers_with_send_command(self, mock_client): + """Test when the initialize_client fails on the connect() step. And then a subsequent + call to send() should recover and re-initialize the client and properly return + the successful send command. + Flow: + Initialization step fails at app init + Send command fails (with 2400 code) prompting retry + Client closes and re-initializes, and command is sent successfully""" + with less_console_noise(): + # Mock the Client instance and its methods + # close() should return successfully + mock_close = MagicMock() + mock_client.return_value.close = mock_close + # Create success and failure results + command_success_result = self.fake_result(1000, "Command completed successfully") + command_failure_result = self.fake_result(2400, "Command failed") + # side_effect for the connect() calls + # first connect() should raise an Exception + # subsequent connect() calls should return success + connect_call_count = 0 + + def connect_side_effect(*args, **kwargs): + nonlocal connect_call_count + connect_call_count += 1 + if connect_call_count == 1: + raise Exception("Connection failed") + else: + return command_success_result + + mock_connect = MagicMock(side_effect=connect_side_effect) + mock_client.return_value.connect = mock_connect + # side_effect for the send() calls + # first send will be the send("InfoDomainCommand") and should fail + # subsequend send() calls should return success + send_call_count = 0 + + def send_side_effect(*args, **kwargs): + nonlocal send_call_count + send_call_count += 1 + if send_call_count == 1: + return command_failure_result + else: + return command_success_result + + mock_send = MagicMock(side_effect=send_side_effect) + mock_client.return_value.send = mock_send + # Create EPPLibWrapper instance and call send command + wrapper = EPPLibWrapper() + wrapper.send("InfoDomainCommand", cleaned=True) + # two connect() calls should be made, the initial failed connect() + # and the successful connect() during retry() + self.assertEquals(mock_connect.call_count, 2) + # close() should only be called once, during retry() + mock_close.assert_called_once() + # send called 4 times: failed send("InfoDomainCommand"), passed send(logout), + # passed send(login), passed send("InfoDomainCommand") + self.assertEquals(mock_send.call_count, 4) + + @patch("epplibwrapper.client.Client") + def test_send_command_failed_retries_and_fails_again(self, mock_client): + """Test when the send("InfoDomainCommand) call fails with a 2400, prompting a retry + and the subsequent send("InfoDomainCommand) call also fails with a 2400, raise + a RegistryError + Flow: + Initialization succeeds + Send command fails (with 2400 code) prompting retry + Client closes and re-initializes, and command fails again with 2400""" + with less_console_noise(): + # Mock the Client instance and its methods + # connect() and close() should succeed throughout + mock_connect = MagicMock() + mock_close = MagicMock() + # Create a mock Result instance + send_command_success_result = self.fake_result(1000, "Command completed successfully") + send_command_failure_result = self.fake_result(2400, "Command failed") + + # side_effect for send command, passes for all other sends (login, logout), but + # fails for send("InfoDomainCommand") + def side_effect(*args, **kwargs): + if args[0] == "InfoDomainCommand": + 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 + + with self.assertRaises(RegistryError): + # Create EPPLibWrapper instance and initialize client + wrapper = EPPLibWrapper() + # call send, which should throw a RegistryError (after retry) + wrapper.send("InfoDomainCommand", cleaned=True) + # connect() should be called twice, once during initialization, second time + # 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) fails, send(logout) + # send(login), send(command) + self.assertEquals(mock_send.call_count, 5) + + @patch("epplibwrapper.client.Client") + def test_send_command_failure_prompts_successful_retry(self, mock_client): + """Test when the send("InfoDomainCommand) call fails with a 2400, prompting a retry + and the subsequent send("InfoDomainCommand) call succeeds + Flow: + Initialization succeeds + Send command fails (with 2400 code) prompting retry + Client closes and re-initializes, and command succeeds""" + with less_console_noise(): + # 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(2400, "Command failed") + # 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 + + 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("InfoDomainCommand", 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) diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py deleted file mode 100644 index f8e556445..000000000 --- a/src/epplibwrapper/tests/test_pool.py +++ /dev/null @@ -1,262 +0,0 @@ -import datetime -from pathlib import Path -from unittest.mock import MagicMock, patch -from dateutil.tz import tzlocal # type: ignore -from django.test import TestCase -from epplibwrapper.client import EPPLibWrapper -from epplibwrapper.errors import RegistryError -from epplibwrapper.socket import Socket -from epplibwrapper.utility.pool import EPPConnectionPool -from registrar.models.domain import registry -from contextlib import ExitStack -from .common import less_console_noise -import logging - -try: - from epplib import commands - from epplib.client import Client - from epplib.exceptions import TransportError - from epplib.transport import SocketTransport - from epplib.models import common, info -except ImportError: - pass - -logger = logging.getLogger(__name__) - - -class TestConnectionPool(TestCase): - """Tests for our connection pooling behaviour""" - - def setUp(self): - # Mimic the settings added to settings.py - self.pool_options = { - # Current pool size - "size": 1, - # Which errors the pool should look out for - "exc_classes": (TransportError,), - # Occasionally pings the registry to keep the connection alive. - # Value in seconds => (keepalive / size) - "keepalive": 60, - } - - def fake_socket(self, login, client): - # Linter reasons - pw = "none" - # Create a fake client object - fake_client = Client( - SocketTransport( - "none", - cert_file="path/to/cert_file", - key_file="path/to/key_file", - password=pw, - ) - ) - - return Socket(fake_client, MagicMock()) - - def patch_success(self): - return True - - def fake_send(self, command, cleaned=None): - mock = MagicMock( - code=1000, - msg="Command completed successfully", - res_data=None, - cl_tr_id="xkw1uo#2023-10-17T15:29:09.559376", - sv_tr_id="5CcH4gxISuGkq8eqvr1UyQ==-35a", - extensions=[], - msg_q=None, - ) - return mock - - def fake_client(mock_client): - pw = "none" - client = Client( - SocketTransport( - "none", - cert_file="path/to/cert_file", - key_file="path/to/key_file", - password=pw, - ) - ) - return client - - @patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success) - def test_pool_sends_data(self): - """A .send is invoked on the pool successfully""" - expected_result = { - "cl_tr_id": None, - "code": 1000, - "extensions": [], - "msg": "Command completed successfully", - "msg_q": None, - "res_data": [ - info.InfoDomainResultData( - roid="DF1340360-GOV", - statuses=[ - common.Status( - state="serverTransferProhibited", - description=None, - lang="en", - ), - common.Status(state="inactive", description=None, lang="en"), - ], - cl_id="gov2023-ote", - cr_id="gov2023-ote", - cr_date=datetime.datetime(2023, 8, 15, 23, 56, 36, tzinfo=tzlocal()), - up_id="gov2023-ote", - up_date=datetime.datetime(2023, 8, 17, 2, 3, 19, tzinfo=tzlocal()), - tr_date=None, - name="test3.gov", - registrant="TuaWnx9hnm84GCSU", - admins=[], - nsset=None, - keyset=None, - ex_date=datetime.date(2024, 8, 15), - auth_info=info.DomainAuthInfo(pw="2fooBAR123fooBaz"), - ) - ], - "sv_tr_id": "wRRNVhKhQW2m6wsUHbo/lA==-29a", - } - - # Mock a response from EPP - def fake_receive(command, cleaned=None): - location = Path(__file__).parent / "utility" / "infoDomain.xml" - xml = (location).read_bytes() - return xml - - def do_nothing(command): - pass - - # Mock what happens inside the "with" - with ExitStack() as stack: - stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket)) - stack.enter_context(patch.object(Socket, "connect", self.fake_client)) - stack.enter_context(patch.object(EPPConnectionPool, "kill_all_connections", do_nothing)) - stack.enter_context(patch.object(SocketTransport, "send", self.fake_send)) - stack.enter_context(patch.object(SocketTransport, "receive", fake_receive)) - with less_console_noise(): - # Restart the connection pool - registry.start_connection_pool() - # Pool should be running, and be the right size - self.assertEqual(registry.pool_status.connection_success, True) - self.assertEqual(registry.pool_status.pool_running, True) - - # Send a command - result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) - - # Should this ever fail, it either means that the schema has changed, - # or the pool is broken. - # If the schema has changed: Update the associated infoDomain.xml file - self.assertEqual(result.__dict__, expected_result) - - # The number of open pools should match the number of requested ones. - # If it is 0, then they failed to open - self.assertEqual(len(registry._pool.conn), self.pool_options["size"]) - # Kill the connection pool - registry.kill_pool() - - @patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success) - def test_pool_restarts_on_send(self): - """A .send is invoked, but the pool isn't running. - The pool should restart.""" - expected_result = { - "cl_tr_id": None, - "code": 1000, - "extensions": [], - "msg": "Command completed successfully", - "msg_q": None, - "res_data": [ - info.InfoDomainResultData( - roid="DF1340360-GOV", - statuses=[ - common.Status( - state="serverTransferProhibited", - description=None, - lang="en", - ), - common.Status(state="inactive", description=None, lang="en"), - ], - cl_id="gov2023-ote", - cr_id="gov2023-ote", - cr_date=datetime.datetime(2023, 8, 15, 23, 56, 36, tzinfo=tzlocal()), - up_id="gov2023-ote", - up_date=datetime.datetime(2023, 8, 17, 2, 3, 19, tzinfo=tzlocal()), - tr_date=None, - name="test3.gov", - registrant="TuaWnx9hnm84GCSU", - admins=[], - nsset=None, - keyset=None, - ex_date=datetime.date(2024, 8, 15), - auth_info=info.DomainAuthInfo(pw="2fooBAR123fooBaz"), - ) - ], - "sv_tr_id": "wRRNVhKhQW2m6wsUHbo/lA==-29a", - } - - # Mock a response from EPP - def fake_receive(command, cleaned=None): - location = Path(__file__).parent / "utility" / "infoDomain.xml" - xml = (location).read_bytes() - return xml - - def do_nothing(command): - pass - - # Mock what happens inside the "with" - with ExitStack() as stack: - stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket)) - stack.enter_context(patch.object(Socket, "connect", self.fake_client)) - stack.enter_context(patch.object(EPPConnectionPool, "kill_all_connections", do_nothing)) - stack.enter_context(patch.object(SocketTransport, "send", self.fake_send)) - stack.enter_context(patch.object(SocketTransport, "receive", fake_receive)) - with less_console_noise(): - # Start the connection pool - registry.start_connection_pool() - # Kill the connection pool - registry.kill_pool() - - self.assertEqual(registry.pool_status.pool_running, False) - - # An exception should be raised as end user will be informed - # that they cannot connect to EPP - with self.assertRaises(RegistryError): - expected = "InfoDomain failed to execute due to a connection error." - result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) - self.assertEqual(result, expected) - - # A subsequent command should be successful, as the pool restarts - result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) - # Should this ever fail, it either means that the schema has changed, - # or the pool is broken. - # If the schema has changed: Update the associated infoDomain.xml file - self.assertEqual(result.__dict__, expected_result) - - # The number of open pools should match the number of requested ones. - # If it is 0, then they failed to open - self.assertEqual(len(registry._pool.conn), self.pool_options["size"]) - # Kill the connection pool - registry.kill_pool() - - @patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success) - def test_raises_connection_error(self): - """A .send is invoked on the pool, but registry connection is lost - right as we send a command.""" - - with ExitStack() as stack: - stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket)) - stack.enter_context(patch.object(Socket, "connect", self.fake_client)) - with less_console_noise(): - # Start the connection pool - registry.start_connection_pool() - - # Pool should be running - self.assertEqual(registry.pool_status.connection_success, True) - self.assertEqual(registry.pool_status.pool_running, True) - - # Try to send a command out - should fail - with self.assertRaises(RegistryError): - expected = "InfoDomain failed to execute due to a connection error." - result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) - self.assertEqual(result, expected) diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py deleted file mode 100644 index 4f54e14ce..000000000 --- a/src/epplibwrapper/utility/pool.py +++ /dev/null @@ -1,151 +0,0 @@ -import logging -from typing import List -import gevent -from geventconnpool import ConnectionPool -from epplibwrapper.socket import Socket -from epplibwrapper.utility.pool_error import PoolError, PoolErrorCodes - -try: - from epplib.commands import Hello - from epplib.exceptions import TransportError -except ImportError: - pass - -from gevent.lock import BoundedSemaphore -from collections import deque - -logger = logging.getLogger(__name__) - - -class EPPConnectionPool(ConnectionPool): - """A connection pool for EPPLib. - - Args: - client (Client): The client - login (commands.Login): Login creds - options (dict): Options for the ConnectionPool - base class - """ - - def __init__(self, client, login, options: dict): - # For storing shared credentials - self._client = client - self._login = login - - # Keep track of each greenlet - self.greenlets: List[gevent.Greenlet] = [] - - # Define optional pool settings. - # Kept in a dict so that the parent class, - # client.py, can maintain seperation/expandability - self.size = 1 - if "size" in options: - self.size = options["size"] - - self.exc_classes = tuple((TransportError,)) - if "exc_classes" in options: - self.exc_classes = options["exc_classes"] - - self.keepalive = None - if "keepalive" in options: - self.keepalive = options["keepalive"] - - # Determines the period in which new - # gevent threads are spun up. - # This time period is in seconds. So for instance, .1 would be .1 seconds. - self.spawn_frequency = 0.1 - if "spawn_frequency" in options: - self.spawn_frequency = options["spawn_frequency"] - - self.conn: deque = deque() - self.lock = BoundedSemaphore(self.size) - - self.populate_all_connections() - - def _new_connection(self): - socket = self._create_socket(self._client, self._login) - try: - connection = socket.connect() - return connection - except Exception as err: - message = f"Failed to execute due to a registry error: {err}" - logger.error(message, exc_info=True) - # We want to raise a pool error rather than a LoginError here - # because if this occurs internally, we should handle this - # differently than we otherwise would for LoginError. - raise PoolError(code=PoolErrorCodes.NEW_CONNECTION_FAILED) from err - - def _keepalive(self, c): - """Sends a command to the server to keep the connection alive.""" - try: - # Sends a ping to the registry via EPPLib - c.send(Hello()) - except Exception as err: - message = "Failed to keep the connection alive." - logger.error(message, exc_info=True) - raise PoolError(code=PoolErrorCodes.KEEP_ALIVE_FAILED) from err - - def _keepalive_periodic(self): - """Overriding _keepalive_periodic from geventconnpool so that PoolErrors - are properly handled, as opposed to printing to stdout""" - delay = float(self.keepalive) / self.size - while 1: - try: - with self.get() as c: - self._keepalive(c) - except PoolError as err: - logger.error(err.message, exc_info=True) - except self.exc_classes: - # Nothing to do, the pool will generate a new connection later - pass - gevent.sleep(delay) - - def _create_socket(self, client, login) -> Socket: - """Creates and returns a socket instance""" - socket = Socket(client, login) - return socket - - def get_connections(self): - """Returns the connection queue""" - return self.conn - - def kill_all_connections(self): - """Kills all active connections in the pool.""" - try: - if len(self.conn) > 0 or len(self.greenlets) > 0: - logger.info("Attempting to kill connections") - gevent.killall(self.greenlets) - - self.greenlets.clear() - for connection in self.conn: - connection.disconnect() - self.conn.clear() - - # Clear the semaphore - self.lock = BoundedSemaphore(self.size) - logger.info("Finished killing connections") - else: - logger.info("No connections to kill.") - except Exception as err: - logger.error("Could not kill all connections.") - raise PoolError(code=PoolErrorCodes.KILL_ALL_FAILED) from err - - def populate_all_connections(self): - """Generates the connection pool. - If any connections exist, kill them first. - Based off of the __init__ definition for geventconnpool. - """ - if len(self.conn) > 0 or len(self.greenlets) > 0: - self.kill_all_connections() - - # Setup the lock - for i in range(self.size): - self.lock.acquire() - - # Open multiple connections - for i in range(self.size): - self.greenlets.append(gevent.spawn_later(self.spawn_frequency * i, self._addOne)) - - # Open a "keepalive" thread if we want to ping open connections - if self.keepalive: - self.greenlets.append(gevent.spawn(self._keepalive_periodic)) diff --git a/src/epplibwrapper/utility/pool_error.py b/src/epplibwrapper/utility/pool_error.py deleted file mode 100644 index bdf955afe..000000000 --- a/src/epplibwrapper/utility/pool_error.py +++ /dev/null @@ -1,46 +0,0 @@ -from enum import IntEnum - - -class PoolErrorCodes(IntEnum): - """Used in the PoolError class for - error mapping. - - Overview of contact error codes: - - 2000 KILL_ALL_FAILED - - 2001 NEW_CONNECTION_FAILED - - 2002 KEEP_ALIVE_FAILED - """ - - KILL_ALL_FAILED = 2000 - NEW_CONNECTION_FAILED = 2001 - KEEP_ALIVE_FAILED = 2002 - - -class PoolError(Exception): - """ - Overview of contact error codes: - - 2000 KILL_ALL_FAILED - - 2001 NEW_CONNECTION_FAILED - - 2002 KEEP_ALIVE_FAILED - - Note: These are separate from the error codes returned from EppLib - """ - - # Used variables due to linter requirements - kill_failed = "Could not kill all connections. Are multiple pools running?" - conn_failed = "Failed to execute due to a registry error. See previous logs to determine the cause of the error." - alive_failed = "Failed to keep the connection alive. It is likely that the registry returned a LoginError." - _error_mapping = { - PoolErrorCodes.KILL_ALL_FAILED: kill_failed, - PoolErrorCodes.NEW_CONNECTION_FAILED: conn_failed, - PoolErrorCodes.KEEP_ALIVE_FAILED: alive_failed, - } - - def __init__(self, *args, code=None, **kwargs): - super().__init__(*args, **kwargs) - self.code = code - if self.code in self._error_mapping: - self.message = self._error_mapping.get(self.code) - - def __str__(self): - return f"{self.message}" diff --git a/src/epplibwrapper/utility/pool_status.py b/src/epplibwrapper/utility/pool_status.py deleted file mode 100644 index 3a0ae750f..000000000 --- a/src/epplibwrapper/utility/pool_status.py +++ /dev/null @@ -1,12 +0,0 @@ -class PoolStatus: - """A list of Booleans to keep track of Pool Status. - - pool_running -> bool: Tracks if the pool itself is active or not. - connection_success -> bool: Tracks if connection is possible with the registry. - pool_hanging -> pool: Tracks if the pool has exceeded its timeout period. - """ - - def __init__(self): - self.pool_running = False - self.connection_success = False - self.pool_hanging = False diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 855321877..65b372fac 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -604,20 +604,6 @@ SECRET_REGISTRY_KEY = secret_registry_key SECRET_REGISTRY_KEY_PASSPHRASE = secret_registry_key_passphrase SECRET_REGISTRY_HOSTNAME = secret_registry_hostname -# Use this variable to set the size of our connection pool in client.py -# WARNING: Setting this value too high could cause frequent app crashes! -# Having too many connections open could cause the sandbox to timeout, -# as the spinup time could exceed the timeout time. -EPP_CONNECTION_POOL_SIZE = 1 - -# Determines the interval in which we ping open connections in seconds -# Calculated as POOL_KEEP_ALIVE / EPP_CONNECTION_POOL_SIZE -POOL_KEEP_ALIVE = 60 - -# Determines how long we try to keep a pool alive for, -# before restarting it. -POOL_TIMEOUT = 60 - # endregion # region: Security and Privacy----------------------------------------------### diff --git a/src/registrar/migrations/0071_alter_contact_first_name_alter_contact_last_name_and_more.py b/src/registrar/migrations/0071_alter_contact_first_name_alter_contact_last_name_and_more.py index bc594138e..9e5eddfc3 100644 --- a/src/registrar/migrations/0071_alter_contact_first_name_alter_contact_last_name_and_more.py +++ b/src/registrar/migrations/0071_alter_contact_first_name_alter_contact_last_name_and_more.py @@ -5,7 +5,6 @@ import phonenumber_field.modelfields class Migration(migrations.Migration): - dependencies = [ ("registrar", "0070_domainapplication_rejection_reason"), ] diff --git a/src/registrar/migrations/0072_alter_publiccontact_fax_alter_publiccontact_voice.py b/src/registrar/migrations/0072_alter_publiccontact_fax_alter_publiccontact_voice.py new file mode 100644 index 000000000..bc4dc7015 --- /dev/null +++ b/src/registrar/migrations/0072_alter_publiccontact_fax_alter_publiccontact_voice.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.10 on 2024-03-05 21:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0071_alter_contact_first_name_alter_contact_last_name_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="publiccontact", + name="fax", + field=models.CharField( + help_text="Contact's fax number (null ok). Must be in ITU.E164.2005 format.", null=True + ), + ), + migrations.AlterField( + model_name="publiccontact", + name="voice", + field=models.CharField(help_text="Contact's phone number. Must be in ITU.E164.2005 format"), + ), + ] diff --git a/src/registrar/models/public_contact.py b/src/registrar/models/public_contact.py index 989dfb0cd..cdd0d6a42 100644 --- a/src/registrar/models/public_contact.py +++ b/src/registrar/models/public_contact.py @@ -8,8 +8,6 @@ from registrar.utility.enums import DefaultEmail from .utility.time_stamped_model import TimeStampedModel -from phonenumber_field.modelfields import PhoneNumberField # type: ignore - def get_id(): """Generate a 16 character registry ID with a low probability of collision.""" @@ -71,8 +69,8 @@ class PublicContact(TimeStampedModel): pc = models.CharField(null=False, help_text="Contact's postal code") cc = models.CharField(null=False, help_text="Contact's country code") email = models.EmailField(null=False, help_text="Contact's email address") - voice = PhoneNumberField(null=False, help_text="Contact's phone number. Must be in ITU.E164.2005 format") - fax = PhoneNumberField( + voice = models.CharField(null=False, help_text="Contact's phone number. Must be in ITU.E164.2005 format") + fax = models.CharField( null=True, help_text="Contact's fax number (null ok). Must be in ITU.E164.2005 format.", ) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index ee1ab8b68..be3643a51 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -475,7 +475,6 @@ class AuditedAdminMockData: def mock_user(): """A simple user.""" user_kwargs = dict( - id=4, first_name="Jeff", last_name="Lebowski", ) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index d76f12f35..3010247e7 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1542,6 +1542,8 @@ class DomainInvitationAdminTest(TestCase): def tearDown(self): """Delete all DomainInvitation objects""" DomainInvitation.objects.all().delete() + User.objects.all().delete() + Contact.objects.all().delete() def test_get_filters(self): """Ensures that our filters are displaying correctly""" diff --git a/src/requirements.txt b/src/requirements.txt index 4b904cddd..b24e3575a 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,8 +1,8 @@ -i https://pypi.python.org/simple annotated-types==0.6.0; python_version >= '3.8' asgiref==3.7.2; python_version >= '3.7' -boto3==1.34.52; python_version >= '3.8' -botocore==1.34.52; python_version >= '3.8' +boto3==1.34.56; python_version >= '3.8' +botocore==1.34.56; python_version >= '3.8' cachetools==5.3.3; python_version >= '3.7' certifi==2024.2.2; python_version >= '3.6' cfenv==0.5.3 @@ -17,18 +17,18 @@ django-allow-cidr==0.7.1 django-auditlog==2.3.0; python_version >= '3.7' django-cache-url==3.4.5 django-cors-headers==4.3.1; python_version >= '3.8' -django-csp==3.7 +django-csp==3.8 django-fsm==2.8.1 django-login-required-middleware==0.9.0 django-phonenumber-field[phonenumberslite]==7.3.0; python_version >= '3.8' django-widget-tweaks==1.5.0; python_version >= '3.8' -environs[django]==10.3.0; python_version >= '3.8' -faker==23.3.0; python_version >= '3.8' +environs[django]==11.0.0; python_version >= '3.8' +faker==24.0.0; python_version >= '3.8' fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c furl==2.1.3 future==1.0.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' gevent==24.2.1; python_version >= '3.8' -geventconnpool@ git+https://github.com/rasky/geventconnpool.git +geventconnpool@ git+https://github.com/rasky/geventconnpool.git@1bbb93a714a331a069adf27265fe582d9ba7ecd4 greenlet==3.0.3; python_version >= '3.7' gunicorn==21.2.0; python_version >= '3.5' idna==3.6; python_version >= '3.5' @@ -36,7 +36,7 @@ jmespath==1.0.1; python_version >= '3.7' lxml==5.1.0; python_version >= '3.6' mako==1.3.2; python_version >= '3.8' markupsafe==2.1.5; python_version >= '3.7' -marshmallow==3.21.0; python_version >= '3.8' +marshmallow==3.21.1; python_version >= '3.8' oic==1.6.1; python_version ~= '3.7' orderedmultidict==1.0.1 packaging==23.2; python_version >= '3.7' @@ -48,7 +48,7 @@ pydantic==2.6.3; python_version >= '3.8' pydantic-core==2.16.3; python_version >= '3.8' pydantic-settings==2.2.1; python_version >= '3.8' pyjwkest==1.4.2 -python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +python-dateutil==2.9.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' python-dotenv==1.0.1; python_version >= '3.8' pyzipper==0.3.6; python_version >= '3.4' requests==2.31.0; python_version >= '3.7' @@ -56,6 +56,7 @@ s3transfer==0.10.0; python_version >= '3.8' setuptools==69.1.1; python_version >= '3.8' six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' sqlparse==0.4.4; python_version >= '3.5' +tblib==3.0.0; python_version >= '3.8' typing-extensions==4.10.0; python_version >= '3.8' urllib3==2.0.7; python_version >= '3.7' whitenoise==6.6.0; python_version >= '3.8'