Repository: cisagov/manage.get.gov

Issue:
Improper handling of IP address with blank spaces on DNS nameserver form
### Current Behavior

User enters "1.1.    2.2" into the IP address field of a nameserver form in domain management

1. IP address is NOT stripping the blanks
2. "1.1.       2.2" is submitted as the IP address value on form
3. Throws the error, "Unable to update domain, changes were not applied. Check logs as a Registry Error is the likely cause"

### Expected Behavior

Enter '1.1.      2.2' into the IP address field of a nameserver form 

1. Blank spaces are stripped from the user entered IP address (code handles scenario)
2. 1.1.2.2 is submitted as the IP address value on form

### Steps to Reproduce

1. Enter an IP address with spaces (e.g., "1.1.        2.2.")


### Environment

_No response_

### Additional Context

Acceptance criteria:
- fix bug
- create unit tests to check for scenario (to prevent IP address form fields with spaces from causing EPP errors)

### Issue Links

_No response_


Discussion:
The biggest issue here, IMO, is that the use may not know where the error occurred, even though it should be apparent. Rather than correcting the user's error, one possible way to address this is simply a format validation of the data entry, informing the user when there is a failure to validate. Let's take a look at the UX and design considerations for this error messaging.
I think this is a bug.  The initial spec had all spaces removed before IP addresses were submitted to registry.  Which was originally implemented, and done silently from the user perspective, with no errors.  Somehow though, the spaces stopped being removed properly and now a non-descript error message is presented.  I think we should fix the bug, and add unit test to prevent this bug in the future. 

Resolving patch:
diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py
--- a/src/registrar/forms/domain.py
+++ b/src/registrar/forms/domain.py
@@ -67,6 +67,7 @@ def clean(self):
         ip = cleaned_data.get("ip", None)
         # remove ANY spaces in the ip field
         ip = ip.replace(" ", "")
+        cleaned_data["ip"] = ip
         domain = cleaned_data.get("domain", "")
 
         ip_list = self.extract_ip_list(ip)


Test patch:
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -859,15 +859,9 @@ def mockSend(self, _request, cleaned):
             case commands.UpdateDomain:
                 return self.mockUpdateDomainCommands(_request, cleaned)
             case commands.CreateHost:
-                return MagicMock(
-                    res_data=[self.mockDataHostChange],
-                    code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
-                )
+                return self.mockCreateHostCommands(_request, cleaned)
             case commands.UpdateHost:
-                return MagicMock(
-                    res_data=[self.mockDataHostChange],
-                    code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
-                )
+                return self.mockUpdateHostCommands(_request, cleaned)
             case commands.DeleteHost:
                 return MagicMock(
                     res_data=[self.mockDataHostChange],
@@ -882,6 +876,28 @@ def mockSend(self, _request, cleaned):
             case _:
                 return MagicMock(res_data=[self.mockDataInfoHosts])
 
+    def mockCreateHostCommands(self, _request, cleaned):
+        test_ws_ip = common.Ip(addr="1.1. 1.1")
+        addrs_submitted = getattr(_request, "addrs", [])
+        if test_ws_ip in addrs_submitted:
+            raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR)
+        else:
+            return MagicMock(
+                res_data=[self.mockDataHostChange],
+                code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
+            )
+
+    def mockUpdateHostCommands(self, _request, cleaned):
+        test_ws_ip = common.Ip(addr="1.1. 1.1")
+        addrs_submitted = getattr(_request, "addrs", [])
+        if test_ws_ip in addrs_submitted:
+            raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR)
+        else:
+            return MagicMock(
+                res_data=[self.mockDataHostChange],
+                code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
+            )
+
     def mockUpdateDomainCommands(self, _request, cleaned):
         if getattr(_request, "name", None) == "dnssec-invalid.gov":
             raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR)
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py
--- a/src/registrar/tests/test_views.py
+++ b/src/registrar/tests/test_views.py
@@ -1462,6 +1462,38 @@ def test_domain_nameservers_form_submit_missing_host(self):
             status_code=200,
         )
 
+    def test_domain_nameservers_form_submit_whitespace(self):
+        """Nameserver form removes whitespace from ip.
+
+        Uses self.app WebTest because we need to interact with forms.
+        """
+        nameserver1 = "ns1.igorville.gov"
+        nameserver2 = "ns2.igorville.gov"
+        valid_ip = "1.1. 1.1"
+        # initial nameservers page has one server with two ips
+        # have to throw an error in order to test that the whitespace has been stripped from ip
+        nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
+        session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        # attempt to submit the form without one host and an ip with whitespace
+        nameservers_page.form["form-0-server"] = nameserver1
+        nameservers_page.form["form-1-ip"] = valid_ip
+        nameservers_page.form["form-1-server"] = nameserver2
+        with less_console_noise():  # swallow log warning message
+            result = nameservers_page.form.submit()
+        # form submission was a post with an ip address which has been stripped of whitespace,
+        # response should be a 302 to success page
+        self.assertEqual(result.status_code, 302)
+        self.assertEqual(
+            result["Location"],
+            reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}),
+        )
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        page = result.follow()
+        # in the event of a generic nameserver error from registry error, there will be a 302
+        # with an error message displayed, so need to follow 302 and test for success message
+        self.assertContains(page, "The name servers for this domain have been updated")
+
     def test_domain_nameservers_form_submit_glue_record_not_allowed(self):
         """Nameserver form catches error when IP is present
         but host not subdomain.
@@ -1553,7 +1585,7 @@ def test_domain_nameservers_form_submits_successfully(self):
         """
         nameserver1 = "ns1.igorville.gov"
         nameserver2 = "ns2.igorville.gov"
-        invalid_ip = "127.0.0.1"
+        valid_ip = "127.0.0.1"
         # initial nameservers page has one server with two ips
         nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
         session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
@@ -1562,7 +1594,7 @@ def test_domain_nameservers_form_submits_successfully(self):
         # only one has ips
         nameservers_page.form["form-0-server"] = nameserver1
         nameservers_page.form["form-1-server"] = nameserver2
-        nameservers_page.form["form-1-ip"] = invalid_ip
+        nameservers_page.form["form-1-ip"] = valid_ip
         with less_console_noise():  # swallow log warning message
             result = nameservers_page.form.submit()
         # form submission was a successful post, response should be a 302