Repository: cisagov/manage.get.gov

Issue:
Bug on name servers page
Not sure if it's a bug, but, if I enter the two name server minimum, hit save, and then copy on name server into the 1st or 2nd field I'm able to save without getting the "minimum 2 required" error message.

_Originally posted by @gabydisarli in https://github.com/cisagov/manage.get.gov/issues/1236#issuecomment-1791454411_
            

Expected behavior:
On Name server page: 

New error message that says: 

Remove duplicate entry (field-level & top of page) + forcing the 2 minimum error if they have less than 2 name servers.


Discussion:
This allows you to save with, effectively, only ONE name server. However, the status remains "DNS Needed"
https://cisa-corp.slack.com/archives/C05BDEA3C11/p1699024500799679


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
@@ -118,8 +118,34 @@ def validate_nameserver_ip_combo(self, domain, server, ip_list):
                 self.add_error("ip", str(e))
 
 
+class BaseNameserverFormset(forms.BaseFormSet):
+    def clean(self):
+        """
+        Check for duplicate entries in the formset.
+        """
+        if any(self.errors):
+            # Don't bother validating the formset unless each form is valid on its own
+            return
+
+        data = []
+        duplicates = []
+
+        for form in self.forms:
+            if form.cleaned_data:
+                value = form.cleaned_data["server"]
+                if value in data:
+                    form.add_error(
+                        "server",
+                        NameserverError(code=nsErrorCodes.DUPLICATE_HOST, nameserver=value),
+                    )
+                    duplicates.append(value)
+                else:
+                    data.append(value)
+
+
 NameserverFormset = formset_factory(
     DomainNameserverForm,
+    formset=BaseNameserverFormset,
     extra=1,
     max_num=13,
     validate_max=True,
diff --git a/src/registrar/utility/errors.py b/src/registrar/utility/errors.py
--- a/src/registrar/utility/errors.py
+++ b/src/registrar/utility/errors.py
@@ -68,7 +68,8 @@ class NameserverErrorCodes(IntEnum):
         - 4 TOO_MANY_HOSTS  more than the max allowed host values
         - 5 MISSING_HOST host is missing for a nameserver
         - 6 INVALID_HOST host is invalid for a nameserver
-        - 7 BAD_DATA bad data input for nameserver
+        - 7 DUPLICATE_HOST host is a duplicate
+        - 8 BAD_DATA bad data input for nameserver
     """
 
     MISSING_IP = 1
@@ -77,7 +78,8 @@ class NameserverErrorCodes(IntEnum):
     TOO_MANY_HOSTS = 4
     MISSING_HOST = 5
     INVALID_HOST = 6
-    BAD_DATA = 7
+    DUPLICATE_HOST = 7
+    BAD_DATA = 8
 
 
 class NameserverError(Exception):
@@ -93,6 +95,7 @@ class NameserverError(Exception):
         NameserverErrorCodes.TOO_MANY_HOSTS: ("Too many hosts provided, you may not have more than 13 nameservers."),
         NameserverErrorCodes.MISSING_HOST: ("Name server must be provided to enter IP address."),
         NameserverErrorCodes.INVALID_HOST: ("Enter a name server in the required format, like ns1.example.com"),
+        NameserverErrorCodes.DUPLICATE_HOST: ("Remove duplicate entry"),
         NameserverErrorCodes.BAD_DATA: (
             "There’s something wrong with the name server information you provided. "
             "If you need help email us at help@get.gov."


Test patch:
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
@@ -1508,6 +1508,30 @@ def test_domain_nameservers_form_submit_missing_host(self):
             status_code=200,
         )
 
+    def test_domain_nameservers_form_submit_duplicate_host(self):
+        """Nameserver form catches error when host is duplicated.
+
+        Uses self.app WebTest because we need to interact with forms.
+        """
+        # 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]
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        # attempt to submit the form with duplicate host names of fake.host.com
+        nameservers_page.form["form-0-ip"] = ""
+        nameservers_page.form["form-1-server"] = "fake.host.com"
+        with less_console_noise():  # swallow log warning message
+            result = nameservers_page.form.submit()
+        # form submission was a post with an error, response should be a 200
+        # error text appears twice, once at the top of the page, once around
+        # the required field.  remove duplicate entry
+        self.assertContains(
+            result,
+            str(NameserverError(code=NameserverErrorCodes.DUPLICATE_HOST)),
+            count=2,
+            status_code=200,
+        )
+
     def test_domain_nameservers_form_submit_whitespace(self):
         """Nameserver form removes whitespace from ip.