Merge branch 'main' into dk/1298-adjango-admin-searchable-dropdown

This commit is contained in:
David Kennedy 2023-11-28 19:07:55 -05:00
commit 690a9b5560
No known key found for this signature in database
GPG key ID: 6528A5386E66B96B
13 changed files with 147 additions and 35 deletions

View file

@ -2,6 +2,9 @@
from django.apps import apps
from django.views.decorators.http import require_http_methods
from django.http import JsonResponse
from django.utils.safestring import mark_safe
from registrar.templatetags.url_helpers import public_site_url
import requests
@ -18,8 +21,13 @@ DOMAIN_API_MESSAGES = {
" For example, if you want www.city.gov, you would enter “city”"
" (without the quotes).",
"extra_dots": "Enter the .gov domain you want without any periods.",
"unavailable": "That domain isnt available. Try entering another one."
" Contact us if you need help coming up with a domain.",
# message below is considered safe; no user input can be inserted into the message
# body; public_site_url() function reads from local app settings and therefore safe
"unavailable": mark_safe( # nosec
"That domain isnt available. "
"<a class='usa-link' href='{}' target='_blank'>"
"Read more about choosing your .gov domain.</a>".format(public_site_url("domains/choosing"))
),
"invalid": "Enter a domain using only letters, numbers, or hyphens (though we don't recommend using hyphens).",
"success": "That domain is available!",
"error": "Error finding domain availability.",

View file

@ -115,14 +115,14 @@ function inlineToast(el, id, style, msg) {
toast.className = `usa-alert usa-alert--${style} usa-alert--slim`;
toastBody.classList.add("usa-alert__body");
p.classList.add("usa-alert__text");
p.innerText = msg;
p.innerHTML = msg;
toastBody.appendChild(p);
toast.appendChild(toastBody);
el.parentNode.insertBefore(toast, el.nextSibling);
} else {
// update and show the existing message div
toast.className = `usa-alert usa-alert--${style} usa-alert--slim`;
toast.querySelector("div p").innerText = msg;
toast.querySelector("div p").innerHTML = msg;
makeVisible(toast);
}
} else {

View file

@ -67,6 +67,7 @@ class DomainNameserverForm(forms.Form):
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)
@ -117,8 +118,34 @@ class DomainNameserverForm(forms.Form):
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,

View file

@ -25,8 +25,8 @@
{% if domain.permissions %}
<section class="section--outlined">
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table--stacked dotgov-table">
<h2 class> Active users </h2>
<caption class="sr-only">Domain users</caption>
<h2 class> Domain managers </h2>
<caption class="sr-only">Domain managers</caption>
<thead>
<tr>
<th data-sortable scope="col" role="columnheader">Email</th>

View file

@ -860,15 +860,9 @@ class MockEppLib(TestCase):
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],
@ -883,6 +877,28 @@ class MockEppLib(TestCase):
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)

View file

@ -1219,6 +1219,8 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest):
self.app.set_user(self.user.username)
self.client.force_login(self.user)
class TestDomainDetail(TestDomainOverview):
def test_domain_detail_link_works(self):
home_page = self.app.get("/")
self.assertContains(home_page, "igorville.gov")
@ -1227,7 +1229,7 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest):
self.assertContains(detail_page, "igorville.gov")
self.assertContains(detail_page, "Status")
def test_domain_overview_blocked_for_ineligible_user(self):
def test_domain_detail_blocked_for_ineligible_user(self):
"""We could easily duplicate this test for all domain management
views, but a single url test should be solid enough since all domain
management pages share the same permissions class"""
@ -1239,7 +1241,7 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest):
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
self.assertEqual(response.status_code, 403)
def test_domain_overview_allowed_for_on_hold(self):
def test_domain_detail_allowed_for_on_hold(self):
"""Test that the domain overview page displays for on hold domain"""
home_page = self.app.get("/")
self.assertContains(home_page, "on-hold.gov")
@ -1248,7 +1250,7 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest):
detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain_on_hold.id}))
self.assertNotContains(detail_page, "Edit")
def test_domain_see_just_nameserver(self):
def test_domain_detail_see_just_nameserver(self):
home_page = self.app.get("/")
self.assertContains(home_page, "justnameserver.com")
@ -1259,7 +1261,7 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest):
self.assertContains(detail_page, "ns1.justnameserver.com")
self.assertContains(detail_page, "ns2.justnameserver.com")
def test_domain_see_nameserver_and_ip(self):
def test_domain_detail_see_nameserver_and_ip(self):
home_page = self.app.get("/")
self.assertContains(home_page, "nameserverwithip.gov")
@ -1275,7 +1277,7 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest):
self.assertContains(detail_page, "(1.2.3.4,")
self.assertContains(detail_page, "2.3.4.5)")
def test_domain_with_no_information_or_application(self):
def test_domain_detail_with_no_information_or_application(self):
"""Test that domain management page returns 200 and displays error
when no domain information or domain application exist"""
# have to use staff user for this test
@ -1506,6 +1508,62 @@ class TestDomainNameservers(TestDomainOverview):
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.
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.
@ -1597,7 +1655,7 @@ class TestDomainNameservers(TestDomainOverview):
"""
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]
@ -1606,7 +1664,7 @@ class TestDomainNameservers(TestDomainOverview):
# 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

View file

@ -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: (
"Theres something wrong with the name server information you provided. "
"If you need help email us at help@get.gov."

View file

@ -197,7 +197,7 @@ class DomainOrgNameAddressView(DomainFormBaseView):
"""The form is valid, save the organization name and mailing address."""
form.save()
messages.success(self.request, "The organization name and mailing address has been updated.")
messages.success(self.request, "The organization information has been updated.")
# superclass has the redirect
return super().form_valid(form)