Merge branch 'main' of github.com:cisagov/manage.get.gov into es/1378-availability-bugfix

This commit is contained in:
Erin 2023-11-29 15:53:26 -08:00
commit 69dd932320
No known key found for this signature in database
GPG key ID: 1CAD275313C62460
12 changed files with 119 additions and 12 deletions

View file

@ -21,6 +21,7 @@ jobs:
|| startsWith(github.head_ref, 'dk/') || startsWith(github.head_ref, 'dk/')
|| startsWith(github.head_ref, 'es/') || startsWith(github.head_ref, 'es/')
|| startsWith(github.head_ref, 'ky/') || startsWith(github.head_ref, 'ky/')
|| startsWith(github.head_ref, 'backup/')
outputs: outputs:
environment: ${{ steps.var.outputs.environment}} environment: ${{ steps.var.outputs.environment}}
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"

View file

@ -16,6 +16,7 @@ on:
- stable - stable
- staging - staging
- development - development
- backup
- ky - ky
- es - es
- nl - nl

View file

@ -16,6 +16,7 @@ on:
options: options:
- staging - staging
- development - development
- backup
- ky - ky
- es - es
- nl - nl

View file

@ -0,0 +1,32 @@
---
applications:
- name: getgov-backup
buildpacks:
- python_buildpack
path: ../../src
instances: 1
memory: 512M
stack: cflinuxfs4
timeout: 180
command: ./run.sh
health-check-type: http
health-check-http-endpoint: /health
health-check-invocation-timeout: 40
env:
# Send stdout and stderr straight to the terminal without buffering
PYTHONUNBUFFERED: yup
# Tell Django where to find its configuration
DJANGO_SETTINGS_MODULE: registrar.config.settings
# Tell Django where it is being hosted
DJANGO_BASE_URL: https://getgov-backup.app.cloud.gov
# Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO
# default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
# Flag to disable/enable features in prod environments
IS_PRODUCTION: False
routes:
- route: getgov-backup.app.cloud.gov
services:
- getgov-credentials
- getgov-backup-database

View file

@ -2,6 +2,9 @@
from django.apps import apps from django.apps import apps
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.http import JsonResponse from django.http import JsonResponse
from django.utils.safestring import mark_safe
from registrar.templatetags.url_helpers import public_site_url
import requests import requests
@ -18,8 +21,13 @@ DOMAIN_API_MESSAGES = {
" For example, if you want www.city.gov, you would enter “city”" " For example, if you want www.city.gov, you would enter “city”"
" (without the quotes).", " (without the quotes).",
"extra_dots": "Enter the .gov domain you want without any periods.", "extra_dots": "Enter the .gov domain you want without any periods.",
"unavailable": "That domain isnt available. Try entering another one." # message below is considered safe; no user input can be inserted into the message
" Contact us if you need help coming up with a domain.", # 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).", "invalid": "Enter a domain using only letters, numbers, or hyphens (though we don't recommend using hyphens).",
"success": "That domain is available!", "success": "That domain is available!",
"error": "Error finding domain availability. Please wait a few minutes and try again. If you continue \ "error": "Error finding domain availability. Please wait a few minutes and try again. If you continue \

View file

@ -322,7 +322,7 @@ class WebsiteAdmin(ListHeaderAdmin):
class UserDomainRoleAdmin(ListHeaderAdmin): class UserDomainRoleAdmin(ListHeaderAdmin):
"""Custom domain role admin class.""" """Custom user domain role admin class."""
# Columns # Columns
list_display = [ list_display = [
@ -340,6 +340,8 @@ class UserDomainRoleAdmin(ListHeaderAdmin):
] ]
search_help_text = "Search by user, domain, or role." search_help_text = "Search by user, domain, or role."
autocomplete_fields = ["user", "domain"]
class DomainInvitationAdmin(ListHeaderAdmin): class DomainInvitationAdmin(ListHeaderAdmin):
"""Custom domain invitation admin class.""" """Custom domain invitation admin class."""

View file

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

View file

@ -245,3 +245,9 @@ h1, h2, h3 {
padding-left: 90px; padding-left: 90px;
} }
} }
// Combo box
#select2-id_domain-results,
#select2-id_user-results {
width: 100%;
}

View file

@ -627,6 +627,7 @@ ALLOWED_HOSTS = [
"getgov-stable.app.cloud.gov", "getgov-stable.app.cloud.gov",
"getgov-staging.app.cloud.gov", "getgov-staging.app.cloud.gov",
"getgov-development.app.cloud.gov", "getgov-development.app.cloud.gov",
"getgov-backup.app.cloud.gov",
"getgov-ky.app.cloud.gov", "getgov-ky.app.cloud.gov",
"getgov-es.app.cloud.gov", "getgov-es.app.cloud.gov",
"getgov-nl.app.cloud.gov", "getgov-nl.app.cloud.gov",

View file

@ -118,8 +118,34 @@ class DomainNameserverForm(forms.Form):
self.add_error("ip", str(e)) 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( NameserverFormset = formset_factory(
DomainNameserverForm, DomainNameserverForm,
formset=BaseNameserverFormset,
extra=1, extra=1,
max_num=13, max_num=13,
validate_max=True, validate_max=True,

View file

@ -1219,6 +1219,8 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest):
self.app.set_user(self.user.username) self.app.set_user(self.user.username)
self.client.force_login(self.user) self.client.force_login(self.user)
class TestDomainDetail(TestDomainOverview):
def test_domain_detail_link_works(self): def test_domain_detail_link_works(self):
home_page = self.app.get("/") home_page = self.app.get("/")
self.assertContains(home_page, "igorville.gov") self.assertContains(home_page, "igorville.gov")
@ -1227,7 +1229,7 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest):
self.assertContains(detail_page, "igorville.gov") self.assertContains(detail_page, "igorville.gov")
self.assertContains(detail_page, "Status") 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 """We could easily duplicate this test for all domain management
views, but a single url test should be solid enough since all domain views, but a single url test should be solid enough since all domain
management pages share the same permissions class""" 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})) response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
self.assertEqual(response.status_code, 403) 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""" """Test that the domain overview page displays for on hold domain"""
home_page = self.app.get("/") home_page = self.app.get("/")
self.assertContains(home_page, "on-hold.gov") 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})) detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain_on_hold.id}))
self.assertNotContains(detail_page, "Edit") 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("/") home_page = self.app.get("/")
self.assertContains(home_page, "justnameserver.com") 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, "ns1.justnameserver.com")
self.assertContains(detail_page, "ns2.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("/") home_page = self.app.get("/")
self.assertContains(home_page, "nameserverwithip.gov") 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, "(1.2.3.4,")
self.assertContains(detail_page, "2.3.4.5)") 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 """Test that domain management page returns 200 and displays error
when no domain information or domain application exist""" when no domain information or domain application exist"""
# have to use staff user for this test # have to use staff user for this test
@ -1506,6 +1508,30 @@ class TestDomainNameservers(TestDomainOverview):
status_code=200, 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): def test_domain_nameservers_form_submit_whitespace(self):
"""Nameserver form removes whitespace from ip. """Nameserver form removes whitespace from ip.

View file

@ -68,7 +68,8 @@ class NameserverErrorCodes(IntEnum):
- 4 TOO_MANY_HOSTS more than the max allowed host values - 4 TOO_MANY_HOSTS more than the max allowed host values
- 5 MISSING_HOST host is missing for a nameserver - 5 MISSING_HOST host is missing for a nameserver
- 6 INVALID_HOST host is invalid 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 MISSING_IP = 1
@ -77,7 +78,8 @@ class NameserverErrorCodes(IntEnum):
TOO_MANY_HOSTS = 4 TOO_MANY_HOSTS = 4
MISSING_HOST = 5 MISSING_HOST = 5
INVALID_HOST = 6 INVALID_HOST = 6
BAD_DATA = 7 DUPLICATE_HOST = 7
BAD_DATA = 8
class NameserverError(Exception): 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.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.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.INVALID_HOST: ("Enter a name server in the required format, like ns1.example.com"),
NameserverErrorCodes.DUPLICATE_HOST: ("Remove duplicate entry"),
NameserverErrorCodes.BAD_DATA: ( NameserverErrorCodes.BAD_DATA: (
"Theres something wrong with the name server information you provided. " "Theres something wrong with the name server information you provided. "
"If you need help email us at help@get.gov." "If you need help email us at help@get.gov."