From 6da9fdab2abb8f93ab6d3d11c4a95dc0dbc98a0f Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 22 Nov 2023 10:36:13 -0500 Subject: [PATCH 01/35] added is_editable to domain; added editable to domain_detail.html; added editable to domain_sidebar.html; added editable to summary_item.html; updated has_permission in domain views to check for domain editable --- src/registrar/models/domain.py | 8 ++++++++ src/registrar/templates/domain_detail.html | 14 +++++++------- src/registrar/templates/domain_sidebar.html | 2 ++ src/registrar/templates/includes/summary_item.html | 2 +- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 37e78ec6e..93b50bf81 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -880,6 +880,14 @@ class Domain(TimeStampedModel, DomainHelper): """ return self.state == self.State.READY + def is_editable(self) -> bool: + """domain is editable unless state is on hold or deleted""" + return self.state in [ + self.State.UNKNOWN, + self.State.DNS_NEEDED, + self.State.READY, + ] + def transfer(self): """Going somewhere. Not implemented.""" raise NotImplementedError() diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index e220fe1aa..2bd6aa0ed 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -29,7 +29,7 @@ {% url 'domain-dns-nameservers' pk=domain.id as url %} {% if domain.nameservers|length > 0 %} - {% include "includes/summary_item.html" with title='DNS name servers' domains='true' value=domain.nameservers list='true' edit_link=url %} + {% include "includes/summary_item.html" with title='DNS name servers' domains='true' value=domain.nameservers list='true' edit_link=url editable=domain.is_editable %} {% else %}

DNS name servers

No DNS name servers have been added yet. Before your domain can be used we’ll need information about your domain name servers.

@@ -37,22 +37,22 @@ {% endif %} {% url 'domain-org-name-address' pk=domain.id as url %} - {% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url %} + {% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url editable=domain.is_editable %} {% url 'domain-authorizing-official' pk=domain.id as url %} - {% include "includes/summary_item.html" with title='Authorizing official' value=domain.domain_info.authorizing_official contact='true' edit_link=url %} + {% include "includes/summary_item.html" with title='Authorizing official' value=domain.domain_info.authorizing_official contact='true' edit_link=url editable=domain.is_editable %} {% url 'domain-your-contact-information' pk=domain.id as url %} - {% include "includes/summary_item.html" with title='Your contact information' value=request.user.contact contact='true' edit_link=url %} + {% include "includes/summary_item.html" with title='Your contact information' value=request.user.contact contact='true' edit_link=url editable=domain.is_editable %} {% url 'domain-security-email' pk=domain.id as url %} {% if security_email is not None and security_email != default_security_email%} - {% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url %} + {% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url editable=domain.is_editable %} {% else %} - {% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url %} + {% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url editable=domain.is_editable %} {% endif %} {% url 'domain-users' pk=domain.id as url %} - {% include "includes/summary_item.html" with title='Domain managers' users='true' list=True value=domain.permissions.all edit_link=url %} + {% include "includes/summary_item.html" with title='Domain managers' users='true' list=True value=domain.permissions.all edit_link=url editable=domain.is_editable %} {% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index 6be8f655d..c224d60c1 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -12,6 +12,7 @@ + {% if domain.is_editable %}
  • {% url 'domain-dns' pk=domain.id as url %} @@ -98,6 +99,7 @@ Domain managers
  • + {% endif %} diff --git a/src/registrar/templates/includes/summary_item.html b/src/registrar/templates/includes/summary_item.html index 8a33bb1d5..dea14553b 100644 --- a/src/registrar/templates/includes/summary_item.html +++ b/src/registrar/templates/includes/summary_item.html @@ -85,7 +85,7 @@ {% endif %} - {% if edit_link %} + {% if editable and edit_link %}
    Date: Wed, 22 Nov 2023 10:48:13 -0500 Subject: [PATCH 02/35] domain views has_permissions updated --- src/registrar/views/domain.py | 22 ++++++++++++++++++++++ src/registrar/views/utility/mixins.py | 17 +++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 0b950ba7a..a9d0d4510 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -152,6 +152,28 @@ class DomainView(DomainBaseView): context["security_email"] = security_email return context + def in_editable_state(self, pk): + """Override in_editable_state from DomainPermission + Allow detail page to be editable""" + + requested_domain = None + if Domain.objects.filter(id=pk).exists(): + requested_domain = Domain.objects.get(id=pk) + + # if domain is editable return true + if requested_domain: + return True + return False + + def _get_domain(self, request): + """ + override get_domain for this view so that domain overview + always resets the cache for the domain object + """ + self.session = request.session + self.object = self.get_object() + self._update_session_with_domain() + class DomainOrgNameAddressView(DomainFormBaseView): """Organization name and mailing address view""" diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index e37ff4927..596873cf3 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -3,6 +3,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin from registrar.models import ( + Domain, DomainApplication, DomainInvitation, DomainInformation, @@ -52,9 +53,25 @@ class DomainPermission(PermissionsLoginMixin): if not UserDomainRole.objects.filter(user=self.request.user, domain__id=pk).exists(): return False + # test if domain in editable state + if not self.in_editable_state(pk): + return False + # if we need to check more about the nature of role, do it here. return True + def in_editable_state(self, pk): + """Is the domain in an editable state""" + + requested_domain = None + if Domain.objects.filter(id=pk).exists(): + requested_domain = Domain.objects.get(id=pk) + + # if domain is editable return true + if requested_domain and requested_domain.is_editable(): + return True + return False + def can_access_other_user_domains(self, pk): """Checks to see if an authorized user (staff or superuser) can access a domain that they did not create or was invited to. From 3ec910c37a7a9c8c651750ccd9a2b66b6b3cb529 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 22 Nov 2023 11:18:54 -0500 Subject: [PATCH 03/35] wrote unit tests --- src/registrar/tests/test_views.py | 80 +++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 936c344f7..5b9ffa4bb 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1082,6 +1082,7 @@ class TestWithDomainPermissions(TestWithUser): self.domain_with_ip, _ = Domain.objects.get_or_create(name="nameserverwithip.gov") self.domain_just_nameserver, _ = Domain.objects.get_or_create(name="justnameserver.com") self.domain_no_information, _ = Domain.objects.get_or_create(name="noinformation.gov") + self.domain_on_hold, _ = Domain.objects.get_or_create(name="on-hold.gov", state=Domain.State.ON_HOLD) self.domain_dsdata, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") self.domain_multdsdata, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov") @@ -1096,6 +1097,7 @@ class TestWithDomainPermissions(TestWithUser): DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dnssec_none) DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_ip) DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_just_nameserver) + DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_on_hold) self.role, _ = UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER @@ -1124,6 +1126,9 @@ class TestWithDomainPermissions(TestWithUser): domain=self.domain_just_nameserver, role=UserDomainRole.Roles.MANAGER, ) + UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain_on_hold, role=UserDomainRole.Roles.MANAGER + ) def tearDown(self): try: @@ -1204,6 +1209,15 @@ 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): + """Test that the domain overview page displays for on hold domain""" + home_page = self.app.get("/") + self.assertContains(home_page, "on-hold.gov") + + # View domain overview page + 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): home_page = self.app.get("/") self.assertContains(home_page, "justnameserver.com") @@ -1258,6 +1272,19 @@ class TestDomainManagers(TestDomainOverview): response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) self.assertContains(response, "Domain managers") + def test_domain_users_blocked_for_on_hold(self): + """Test that the domain users page blocked for on hold domain""" + + # attempt to view domain users page + with less_console_noise(): + response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain_on_hold.id})) + self.assertEqual(response.status_code, 403) + + # attempt to view domain users add page + with less_console_noise(): + response = self.client.get(reverse("domain-users-add", kwargs={"pk": self.domain_on_hold.id})) + self.assertEqual(response.status_code, 403) + def test_domain_managers_add_link(self): """Button to get to user add page works.""" management_page = self.app.get(reverse("domain-users", kwargs={"pk": self.domain.id})) @@ -1391,6 +1418,14 @@ class TestDomainNameservers(TestDomainOverview): page = self.client.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) self.assertContains(page, "DNS name servers") + def test_domain_nameservers_blocked_for_on_hold(self): + """Test that the domain nameservers page blocked for on hold domain""" + + # attempt to view domain nameservers page + with less_console_noise(): + response = self.client.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain_on_hold.id})) + self.assertEqual(response.status_code, 403) + def test_domain_nameservers_form_submit_one_nameserver(self): """Nameserver form submitted with one nameserver throws error. @@ -1606,6 +1641,14 @@ class TestDomainAuthorizingOfficial(TestDomainOverview): # once on the sidebar, once in the title self.assertContains(page, "Authorizing official", count=2) + def test_domain_authorizing_official_blocked_for_on_hold(self): + """Test that the domain authorizing official page blocked for on hold domain""" + + # attempt to view domain authorizing official page + with less_console_noise(): + response = self.client.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain_on_hold.id})) + self.assertEqual(response.status_code, 403) + def test_domain_authorizing_official_content(self): """Authorizing official information appears on the page.""" self.domain_information.authorizing_official = Contact(first_name="Testy") @@ -1622,6 +1665,14 @@ class TestDomainOrganization(TestDomainOverview): # once on the sidebar, once in the page title, once as H1 self.assertContains(page, "Organization name and mailing address", count=3) + def test_domain_org_name_blocked_for_on_hold(self): + """Test that the domain org name page blocked for on hold domain""" + + # attempt to view domain org name page + with less_console_noise(): + response = self.client.get(reverse("domain-org-name-address", kwargs={"pk": self.domain_on_hold.id})) + self.assertEqual(response.status_code, 403) + def test_domain_org_name_address_content(self): """Org name and address information appears on the page.""" self.domain_information.organization_name = "Town of Igorville" @@ -1653,6 +1704,14 @@ class TestDomainContactInformation(TestDomainOverview): page = self.client.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})) self.assertContains(page, "Your contact information") + def test_domain_contact_information_blocked_for_on_hold(self): + """Test that the domain contact information page blocked for on hold domain""" + + # attempt to view domain contact information page + with less_console_noise(): + response = self.client.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain_on_hold.id})) + self.assertEqual(response.status_code, 403) + def test_domain_your_contact_information_content(self): """Logged-in user's contact information appears on the page.""" self.user.contact.first_name = "Testy" @@ -1678,6 +1737,14 @@ class TestDomainSecurityEmail(TestDomainOverview): self.assertContains(page, "security@mail.gov") self.mockSendPatch.stop() + def test_domain_security_email_blocked_for_on_hold(self): + """Test that the domain security email page blocked for on hold domain""" + + # attempt to view domain security email page + with less_console_noise(): + response = self.client.get(reverse("domain-security-email", kwargs={"pk": self.domain_on_hold.id})) + self.assertEqual(response.status_code, 403) + def test_domain_security_email_no_security_contact(self): """Loads a domain with no defined security email. We should not show the default.""" @@ -1805,6 +1872,19 @@ class TestDomainDNSSEC(TestDomainOverview): page = self.client.get(reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id})) self.assertContains(page, "Enable DNSSEC") + def test_domain_dnssec_blocked_for_on_hold(self): + """Test that the domain dnssec page blocked for on hold domain""" + + # attempt to view domain dnssec page + with less_console_noise(): + response = self.client.get(reverse("domain-dns-dnssec", kwargs={"pk": self.domain_on_hold.id})) + self.assertEqual(response.status_code, 403) + + # attempt to view domain dnssec dsdata page + with less_console_noise(): + response = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_on_hold.id})) + self.assertEqual(response.status_code, 403) + def test_dnssec_page_loads_with_data_in_domain(self): """DNSSEC overview page loads when domain has DNSSEC data and the template contains a button to disable DNSSEC.""" From 570c5c3020f148e1732a9d0a49bfc4c6cc919698 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 22 Nov 2023 11:50:18 -0500 Subject: [PATCH 04/35] formatted for linter --- src/registrar/tests/test_views.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 5b9ffa4bb..8818ae94f 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1213,11 +1213,11 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest): """Test that the domain overview page displays for on hold domain""" home_page = self.app.get("/") self.assertContains(home_page, "on-hold.gov") - + # View domain overview page 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): home_page = self.app.get("/") self.assertContains(home_page, "justnameserver.com") @@ -1274,7 +1274,7 @@ class TestDomainManagers(TestDomainOverview): def test_domain_users_blocked_for_on_hold(self): """Test that the domain users page blocked for on hold domain""" - + # attempt to view domain users page with less_console_noise(): response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain_on_hold.id})) @@ -1420,7 +1420,7 @@ class TestDomainNameservers(TestDomainOverview): def test_domain_nameservers_blocked_for_on_hold(self): """Test that the domain nameservers page blocked for on hold domain""" - + # attempt to view domain nameservers page with less_console_noise(): response = self.client.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain_on_hold.id})) @@ -1643,7 +1643,7 @@ class TestDomainAuthorizingOfficial(TestDomainOverview): def test_domain_authorizing_official_blocked_for_on_hold(self): """Test that the domain authorizing official page blocked for on hold domain""" - + # attempt to view domain authorizing official page with less_console_noise(): response = self.client.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain_on_hold.id})) @@ -1667,7 +1667,7 @@ class TestDomainOrganization(TestDomainOverview): def test_domain_org_name_blocked_for_on_hold(self): """Test that the domain org name page blocked for on hold domain""" - + # attempt to view domain org name page with less_console_noise(): response = self.client.get(reverse("domain-org-name-address", kwargs={"pk": self.domain_on_hold.id})) @@ -1706,10 +1706,12 @@ class TestDomainContactInformation(TestDomainOverview): def test_domain_contact_information_blocked_for_on_hold(self): """Test that the domain contact information page blocked for on hold domain""" - + # attempt to view domain contact information page with less_console_noise(): - response = self.client.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain_on_hold.id})) + response = self.client.get( + reverse("domain-your-contact-information", kwargs={"pk": self.domain_on_hold.id}) + ) self.assertEqual(response.status_code, 403) def test_domain_your_contact_information_content(self): @@ -1739,7 +1741,7 @@ class TestDomainSecurityEmail(TestDomainOverview): def test_domain_security_email_blocked_for_on_hold(self): """Test that the domain security email page blocked for on hold domain""" - + # attempt to view domain security email page with less_console_noise(): response = self.client.get(reverse("domain-security-email", kwargs={"pk": self.domain_on_hold.id})) @@ -1874,7 +1876,7 @@ class TestDomainDNSSEC(TestDomainOverview): def test_domain_dnssec_blocked_for_on_hold(self): """Test that the domain dnssec page blocked for on hold domain""" - + # attempt to view domain dnssec page with less_console_noise(): response = self.client.get(reverse("domain-dns-dnssec", kwargs={"pk": self.domain_on_hold.id})) @@ -1884,7 +1886,7 @@ class TestDomainDNSSEC(TestDomainOverview): with less_console_noise(): response = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_on_hold.id})) self.assertEqual(response.status_code, 403) - + def test_dnssec_page_loads_with_data_in_domain(self): """DNSSEC overview page loads when domain has DNSSEC data and the template contains a button to disable DNSSEC.""" From 8300694a3a63760dc478f69fa605a0a776c7dd6a Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 22 Nov 2023 14:26:24 -0500 Subject: [PATCH 05/35] added test cases for deleted state; updated fetch_cache logic to properly handle deleted domains --- src/registrar/models/domain.py | 2 +- src/registrar/tests/test_views.py | 98 ++++++++++--------------------- 2 files changed, 31 insertions(+), 69 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 93b50bf81..d28008c9a 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1196,7 +1196,7 @@ class Domain(TimeStampedModel, DomainHelper): logger.error(e) logger.error(e.code) raise e - if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST: + if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST and self.state != Domain.State.DELETED: # avoid infinite loop already_tried_to_create = True self.dns_needed_from_unknown() diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 8818ae94f..d6bad9cdf 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1083,6 +1083,7 @@ class TestWithDomainPermissions(TestWithUser): self.domain_just_nameserver, _ = Domain.objects.get_or_create(name="justnameserver.com") self.domain_no_information, _ = Domain.objects.get_or_create(name="noinformation.gov") self.domain_on_hold, _ = Domain.objects.get_or_create(name="on-hold.gov", state=Domain.State.ON_HOLD) + self.domain_deleted, _ = Domain.objects.get_or_create(name="deleted.gov", state=Domain.State.DELETED) self.domain_dsdata, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") self.domain_multdsdata, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov") @@ -1098,6 +1099,7 @@ class TestWithDomainPermissions(TestWithUser): DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_ip) DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_just_nameserver) DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_on_hold) + DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_deleted) self.role, _ = UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER @@ -1129,6 +1131,9 @@ class TestWithDomainPermissions(TestWithUser): UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain_on_hold, role=UserDomainRole.Roles.MANAGER ) + UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain_deleted, role=UserDomainRole.Roles.MANAGER + ) def tearDown(self): try: @@ -1182,6 +1187,31 @@ class TestDomainPermissions(TestWithDomainPermissions): response = self.client.get(reverse(view_name, kwargs={"pk": self.domain.id})) self.assertEqual(response.status_code, 403) + def test_domain_pages_blocked_for_on_hold_and_deleted(self): + """Test that the domain pages are blocked for on hold and deleted domains""" + + self.client.force_login(self.user) + for view_name in [ + "domain-users", + "domain-users-add", + "domain-dns", + "domain-dns-nameservers", + "domain-dns-dnssec", + "domain-dns-dnssec-dsdata", + "domain-org-name-address", + "domain-authorizing-official", + "domain-your-contact-information", + "domain-security-email", + ]: + for domain in [ + self.domain_on_hold, + self.domain_deleted, + ]: + with self.subTest(view_name=view_name, domain=domain): + with less_console_noise(): + response = self.client.get(reverse(view_name, kwargs={"pk": domain.id})) + self.assertEqual(response.status_code, 403) + class TestDomainOverview(TestWithDomainPermissions, WebTest): def setUp(self): @@ -1272,19 +1302,6 @@ class TestDomainManagers(TestDomainOverview): response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) self.assertContains(response, "Domain managers") - def test_domain_users_blocked_for_on_hold(self): - """Test that the domain users page blocked for on hold domain""" - - # attempt to view domain users page - with less_console_noise(): - response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain_on_hold.id})) - self.assertEqual(response.status_code, 403) - - # attempt to view domain users add page - with less_console_noise(): - response = self.client.get(reverse("domain-users-add", kwargs={"pk": self.domain_on_hold.id})) - self.assertEqual(response.status_code, 403) - def test_domain_managers_add_link(self): """Button to get to user add page works.""" management_page = self.app.get(reverse("domain-users", kwargs={"pk": self.domain.id})) @@ -1418,14 +1435,6 @@ class TestDomainNameservers(TestDomainOverview): page = self.client.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) self.assertContains(page, "DNS name servers") - def test_domain_nameservers_blocked_for_on_hold(self): - """Test that the domain nameservers page blocked for on hold domain""" - - # attempt to view domain nameservers page - with less_console_noise(): - response = self.client.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain_on_hold.id})) - self.assertEqual(response.status_code, 403) - def test_domain_nameservers_form_submit_one_nameserver(self): """Nameserver form submitted with one nameserver throws error. @@ -1641,14 +1650,6 @@ class TestDomainAuthorizingOfficial(TestDomainOverview): # once on the sidebar, once in the title self.assertContains(page, "Authorizing official", count=2) - def test_domain_authorizing_official_blocked_for_on_hold(self): - """Test that the domain authorizing official page blocked for on hold domain""" - - # attempt to view domain authorizing official page - with less_console_noise(): - response = self.client.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain_on_hold.id})) - self.assertEqual(response.status_code, 403) - def test_domain_authorizing_official_content(self): """Authorizing official information appears on the page.""" self.domain_information.authorizing_official = Contact(first_name="Testy") @@ -1665,14 +1666,6 @@ class TestDomainOrganization(TestDomainOverview): # once on the sidebar, once in the page title, once as H1 self.assertContains(page, "Organization name and mailing address", count=3) - def test_domain_org_name_blocked_for_on_hold(self): - """Test that the domain org name page blocked for on hold domain""" - - # attempt to view domain org name page - with less_console_noise(): - response = self.client.get(reverse("domain-org-name-address", kwargs={"pk": self.domain_on_hold.id})) - self.assertEqual(response.status_code, 403) - def test_domain_org_name_address_content(self): """Org name and address information appears on the page.""" self.domain_information.organization_name = "Town of Igorville" @@ -1704,16 +1697,6 @@ class TestDomainContactInformation(TestDomainOverview): page = self.client.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})) self.assertContains(page, "Your contact information") - def test_domain_contact_information_blocked_for_on_hold(self): - """Test that the domain contact information page blocked for on hold domain""" - - # attempt to view domain contact information page - with less_console_noise(): - response = self.client.get( - reverse("domain-your-contact-information", kwargs={"pk": self.domain_on_hold.id}) - ) - self.assertEqual(response.status_code, 403) - def test_domain_your_contact_information_content(self): """Logged-in user's contact information appears on the page.""" self.user.contact.first_name = "Testy" @@ -1739,14 +1722,6 @@ class TestDomainSecurityEmail(TestDomainOverview): self.assertContains(page, "security@mail.gov") self.mockSendPatch.stop() - def test_domain_security_email_blocked_for_on_hold(self): - """Test that the domain security email page blocked for on hold domain""" - - # attempt to view domain security email page - with less_console_noise(): - response = self.client.get(reverse("domain-security-email", kwargs={"pk": self.domain_on_hold.id})) - self.assertEqual(response.status_code, 403) - def test_domain_security_email_no_security_contact(self): """Loads a domain with no defined security email. We should not show the default.""" @@ -1874,19 +1849,6 @@ class TestDomainDNSSEC(TestDomainOverview): page = self.client.get(reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id})) self.assertContains(page, "Enable DNSSEC") - def test_domain_dnssec_blocked_for_on_hold(self): - """Test that the domain dnssec page blocked for on hold domain""" - - # attempt to view domain dnssec page - with less_console_noise(): - response = self.client.get(reverse("domain-dns-dnssec", kwargs={"pk": self.domain_on_hold.id})) - self.assertEqual(response.status_code, 403) - - # attempt to view domain dnssec dsdata page - with less_console_noise(): - response = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_on_hold.id})) - self.assertEqual(response.status_code, 403) - def test_dnssec_page_loads_with_data_in_domain(self): """DNSSEC overview page loads when domain has DNSSEC data and the template contains a button to disable DNSSEC.""" From 441a6027baf23d180792eab6d699b22cb978390b Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 22 Nov 2023 17:41:27 -0500 Subject: [PATCH 06/35] expiration date written to db on fetch_cache --- src/registrar/models/domain.py | 10 +++++++--- src/registrar/tests/common.py | 1 + src/registrar/tests/test_models_domain.py | 7 +++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 37e78ec6e..4cb2e72a3 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -214,9 +214,7 @@ class Domain(TimeStampedModel, DomainHelper): """Get or set the `ex_date` element from the registry. Additionally, update the expiration date in the registrar""" try: - self.expiration_date = self._get_property("ex_date") - self.save() - return self.expiration_date + return self._get_property("ex_date") except Exception as e: # exception raised during the save to registrar logger.error(f"error updating expiration date in registrar: {e}") @@ -1602,6 +1600,12 @@ class Domain(TimeStampedModel, DomainHelper): if old_cache_contacts is not None: cleaned["contacts"] = old_cache_contacts + # if expiration date from registry does not match what is in db, + # update the db + if "ex_date" in cleaned and cleaned["ex_date"] != self.expiration_date: + self.expiration_date = cleaned["ex_date"] + self.save() + self._cache = cleaned except RegistryError as e: diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 9a062106f..46081e98f 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -617,6 +617,7 @@ class MockEppLib(TestCase): common.Status(state="serverTransferProhibited", description="", lang="en"), common.Status(state="inactive", description="", lang="en"), ], + ex_date=datetime.date(2023, 5, 25), ) mockDataInfoContact = mockDataInfoDomain.dummyInfoContactResultData( "123", "123@mail.gov", datetime.datetime(2023, 5, 25, 19, 45, 35), "lastPw" diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index c75b1b935..4a2023243 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -1987,6 +1987,13 @@ class TestExpirationDate(MockEppLib): with self.assertRaises(RegistryError): self.domain_w_error.renew_domain() + def test_expiration_date_updated_on_info_domain_call(self): + """assert that expiration date in db is updated on info domain call""" + # force fetch_cache to be called + self.domain.statuses + test_date = datetime.date(2023, 5, 25) + self.assertEquals(self.domain.expiration_date, test_date) + class TestAnalystClientHold(MockEppLib): """Rule: Analysts may suspend or restore a domain by using client hold""" From 46677f4588458d7aa8c8157de17eef79888320bd Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 24 Nov 2023 07:43:25 -0700 Subject: [PATCH 07/35] Add filter + test skeleton --- src/registrar/admin.py | 4 ++++ src/registrar/tests/test_admin.py | 37 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index c059e5674..9921b1194 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -356,6 +356,10 @@ class DomainInvitationAdmin(ListHeaderAdmin): "email", "domain__name", ] + + # Filters + list_filter = ("status",) + search_help_text = "Search by email or domain." # Mark the FSM field 'status' as readonly diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index a7cbb5d33..1ad5f95da 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -8,6 +8,7 @@ from registrar.admin import ( DomainAdmin, DomainApplicationAdmin, DomainApplicationAdminForm, + DomainInvitationAdmin, ListHeaderAdmin, MyUserAdmin, AuditedAdmin, @@ -847,6 +848,42 @@ class TestDomainApplicationAdmin(MockEppLib): User.objects.all().delete() +class DomainInvitationAdminTest(TestCase): + def setUp(self): + self.site = AdminSite() + self.factory = RequestFactory() + self.admin = ListHeaderAdmin(model=DomainInvitationAdmin, admin_site=None) + self.client = Client(HTTP_HOST="localhost:8080") + self.superuser = create_superuser() + + def tearDown(self): + # delete any applications too + DomainInvitation.objects.all().delete() + DomainApplication.objects.all().delete() + User.objects.all().delete() + + def test_get_filters(self): + # Create a mock request object + request = self.factory.get("/admin/yourmodel/") + # Set the GET parameters for testing + request.GET = { + "status": "started", + "investigator": "Rachid Mrad", + "q": "search_value", + } + # Call the get_filters method + filters = self.admin.get_filters(request) + + # Assert the filters extracted from the request GET + self.assertEqual( + filters, + [ + {"parameter_name": "status", "parameter_value": "started"}, + {"parameter_name": "investigator", "parameter_value": "Rachid Mrad"}, + ], + ) + + class ListHeaderAdminTest(TestCase): def setUp(self): self.site = AdminSite() From 377786cccfbccfc3e55f551b20cd469eee457522 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 24 Nov 2023 14:16:34 -0700 Subject: [PATCH 08/35] Add test cases --- src/registrar/admin.py | 5 +++++ src/registrar/tests/test_admin.py | 34 ++++++++++++++----------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 9921b1194..ff0f71426 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -343,6 +343,11 @@ class UserDomainRoleAdmin(ListHeaderAdmin): class DomainInvitationAdmin(ListHeaderAdmin): """Custom domain invitation admin class.""" + class Meta: + model = models.DomainInvitation + fields = "__all__" + + _meta = Meta() # Columns list_display = [ diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 1ad5f95da..089e569eb 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -850,9 +850,8 @@ class TestDomainApplicationAdmin(MockEppLib): class DomainInvitationAdminTest(TestCase): def setUp(self): - self.site = AdminSite() self.factory = RequestFactory() - self.admin = ListHeaderAdmin(model=DomainInvitationAdmin, admin_site=None) + self.admin = ListHeaderAdmin(model=DomainInvitationAdmin, admin_site=AdminSite()) self.client = Client(HTTP_HOST="localhost:8080") self.superuser = create_superuser() @@ -863,26 +862,23 @@ class DomainInvitationAdminTest(TestCase): User.objects.all().delete() def test_get_filters(self): - # Create a mock request object - request = self.factory.get("/admin/yourmodel/") - # Set the GET parameters for testing - request.GET = { - "status": "started", - "investigator": "Rachid Mrad", - "q": "search_value", - } - # Call the get_filters method - filters = self.admin.get_filters(request) + # Have to get creative to get past linter + p = "adminpass" + self.client.login(username="superuser", password=p) - # Assert the filters extracted from the request GET - self.assertEqual( - filters, - [ - {"parameter_name": "status", "parameter_value": "started"}, - {"parameter_name": "investigator", "parameter_value": "Rachid Mrad"}, - ], + # Mock a user + user = mock_user() + + response = self.client.get( + "/admin/registrar/domaininvitation/", + {}, + follow=True, ) + # Assert that the filters are added + self.assertContains(response, "invited", count=4) + self.assertContains(response, "retrieved", count=4) + class ListHeaderAdminTest(TestCase): def setUp(self): From 7bac5185b07af4b2ba1463186aa0a4ad838f6fa1 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Sat, 25 Nov 2023 06:06:37 -0500 Subject: [PATCH 09/35] updated check availability message in api and in form; modified js to display html rather than text --- src/api/views.py | 10 ++++++++-- src/registrar/assets/js/get-gov.js | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/api/views.py b/src/api/views.py index 2cb23a9b2..5e5365e58 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -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,11 @@ 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 isn’t available. Try entering another one." - " Contact us if you need help coming up with a domain.", + "unavailable": mark_safe( # nosec + "That domain isn’t available. " + "" + "Read more about choosing your .gov domain.".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.", diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index b659b117e..4ef4efbba 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -115,7 +115,7 @@ 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); From 614da492dbb9f98cfe8121b710b21afc5971fc77 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Sat, 25 Nov 2023 07:45:46 -0500 Subject: [PATCH 10/35] added form level checking for duplicate entries in nameserver form --- src/registrar/forms/domain.py | 26 ++++++++++++++++++++++++++ src/registrar/utility/errors.py | 7 +++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index ae83650cb..965880354 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -117,8 +117,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, diff --git a/src/registrar/utility/errors.py b/src/registrar/utility/errors.py index 420c616cb..52b1ea1d3 100644 --- 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." From 952cc6f46bb9e45932590a764fd7c2ea718b71cb Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Sat, 25 Nov 2023 08:09:33 -0500 Subject: [PATCH 11/35] added test case --- src/registrar/tests/test_views.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 936c344f7..9f423e276 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1462,6 +1462,30 @@ 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_glue_record_not_allowed(self): """Nameserver form catches error when IP is present but host not subdomain. From b56b95ba086316c9c3a30e19a073f9e4e14810ba Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Sat, 25 Nov 2023 08:11:42 -0500 Subject: [PATCH 12/35] formatted for linter --- src/registrar/forms/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 965880354..b8efdae49 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -131,7 +131,7 @@ class BaseNameserverFormset(forms.BaseFormSet): for form in self.forms: if form.cleaned_data: - value = form.cleaned_data['server'] + value = form.cleaned_data["server"] if value in data: form.add_error( "server", From 1f4fcf1225a5aa605b1c2b1a6d256dc3b94a0570 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Sat, 25 Nov 2023 10:04:24 -0500 Subject: [PATCH 13/35] fixed whitespace bug in ips in nameserver; created test case --- src/registrar/forms/domain.py | 1 + src/registrar/tests/common.py | 32 ++++++++++++++++++++------- src/registrar/tests/test_views.py | 36 +++++++++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index ae83650cb..9c09467cd 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -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) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 9a062106f..8a971474d 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -859,15 +859,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], @@ -882,6 +876,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) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 936c344f7..39b23b546 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1462,6 +1462,38 @@ class TestDomainNameservers(TestDomainOverview): 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 @@ 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] @@ -1562,7 +1594,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 From 22a21b5bf4a9af428c3349de94ec8cc8674c9b81 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Sat, 25 Nov 2023 10:22:34 -0500 Subject: [PATCH 14/35] format for linting --- src/registrar/tests/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 8a971474d..d745669e5 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -897,7 +897,7 @@ class MockEppLib(TestCase): 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) From e314a5ff77fd728f64b29791a666a2105fd3cca8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 27 Nov 2023 08:02:58 -0700 Subject: [PATCH 15/35] Update tests + linter --- src/registrar/admin.py | 3 ++- src/registrar/tests/test_admin.py | 22 ++++++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index ff0f71426..6585d602a 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -343,10 +343,11 @@ class UserDomainRoleAdmin(ListHeaderAdmin): class DomainInvitationAdmin(ListHeaderAdmin): """Custom domain invitation admin class.""" + class Meta: model = models.DomainInvitation fields = "__all__" - + _meta = Meta() # Columns diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 089e569eb..bff27a83d 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -849,26 +849,25 @@ class TestDomainApplicationAdmin(MockEppLib): class DomainInvitationAdminTest(TestCase): + """Tests for the DomainInvitation page""" + def setUp(self): + """Create a client object""" + self.client = Client(HTTP_HOST="localhost:8080") self.factory = RequestFactory() self.admin = ListHeaderAdmin(model=DomainInvitationAdmin, admin_site=AdminSite()) - self.client = Client(HTTP_HOST="localhost:8080") self.superuser = create_superuser() - + def tearDown(self): - # delete any applications too + """Delete all DomainInvitation objects""" DomainInvitation.objects.all().delete() - DomainApplication.objects.all().delete() - User.objects.all().delete() def test_get_filters(self): + """Ensures that our filters are displaying correctly""" # Have to get creative to get past linter p = "adminpass" self.client.login(username="superuser", password=p) - # Mock a user - user = mock_user() - response = self.client.get( "/admin/registrar/domaininvitation/", {}, @@ -879,6 +878,13 @@ class DomainInvitationAdminTest(TestCase): self.assertContains(response, "invited", count=4) self.assertContains(response, "retrieved", count=4) + # Check for the HTML context specificially + invited_html = 'invited' + retrieved_html = 'retrieved' + + self.assertContains(response, invited_html, count=1) + self.assertContains(response, retrieved_html, count=1) + class ListHeaderAdminTest(TestCase): def setUp(self): From 6ace0f2862ef8862c03851971c04f440159bee12 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 27 Nov 2023 13:02:26 -0700 Subject: [PATCH 16/35] Update test_reports.py --- src/registrar/tests/test_reports.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index 14573ab65..52b971601 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -50,6 +50,7 @@ class ExportDataTest(TestCase): ) def tearDown(self): + # Dummy push - will remove Domain.objects.all().delete() DomainInformation.objects.all().delete() User.objects.all().delete() From 315638c020742c17d92c4933477a81bb65e0bd9e Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 27 Nov 2023 15:08:42 -0500 Subject: [PATCH 17/35] updated comments for code legibility --- src/registrar/views/domain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index a9d0d4510..3aac32531 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -154,13 +154,13 @@ class DomainView(DomainBaseView): def in_editable_state(self, pk): """Override in_editable_state from DomainPermission - Allow detail page to be editable""" + Allow detail page to be viewable""" requested_domain = None if Domain.objects.filter(id=pk).exists(): requested_domain = Domain.objects.get(id=pk) - # if domain is editable return true + # return true if the domain exists, this will allow the detail page to load if requested_domain: return True return False From abe35b9d633b7a2952e148a6cb6ad842539409b7 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 27 Nov 2023 18:02:43 -0500 Subject: [PATCH 18/35] changed the order of the permissions checking as the manage domain check was firing before editable check and allowing access --- src/registrar/views/utility/mixins.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 596873cf3..aaa2e849c 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -46,6 +46,10 @@ class DomainPermission(PermissionsLoginMixin): if pk is None: raise ValueError("Primary key is None") + # test if domain in editable state + if not self.in_editable_state(pk): + return False + if self.can_access_other_user_domains(pk): return True @@ -53,10 +57,6 @@ class DomainPermission(PermissionsLoginMixin): if not UserDomainRole.objects.filter(user=self.request.user, domain__id=pk).exists(): return False - # test if domain in editable state - if not self.in_editable_state(pk): - return False - # if we need to check more about the nature of role, do it here. return True From 20c13a5d2f97dfc8a43cf3d95e29d4fd8f94bf5f Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 27 Nov 2023 19:16:45 -0500 Subject: [PATCH 19/35] when in read only mode remove add dns name server button and text --- src/registrar/templates/domain_detail.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index 2bd6aa0ed..8d471d57e 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -31,9 +31,11 @@ {% if domain.nameservers|length > 0 %} {% include "includes/summary_item.html" with title='DNS name servers' domains='true' value=domain.nameservers list='true' edit_link=url editable=domain.is_editable %} {% else %} + {% if domain.is_editable %}

    DNS name servers

    No DNS name servers have been added yet. Before your domain can be used we’ll need information about your domain name servers.

    - Add DNS name servers + Add DNS name servers + {% endif %} {% endif %} {% url 'domain-org-name-address' pk=domain.id as url %} From 3ce76de1ff61b370f6e24e145cc413b39abcfa80 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 27 Nov 2023 19:29:00 -0500 Subject: [PATCH 20/35] when in read only mode and no dns name servers, add empty section for dns name servers instead of button to add --- src/registrar/templates/domain_detail.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index 8d471d57e..81a350f82 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -35,6 +35,8 @@

    DNS name servers

    No DNS name servers have been added yet. Before your domain can be used we’ll need information about your domain name servers.

    Add DNS name servers + {% else %} + {% include "includes/summary_item.html" with title='DNS name servers' domains='true' value='' edit_link=url editable=domain.is_editable %} {% endif %} {% endif %} From 128efb6a54b2a489268cab555720dd213a3716ef Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 27 Nov 2023 20:03:38 -0500 Subject: [PATCH 21/35] updated comment --- src/registrar/models/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 1e4ab0e17..94430fb36 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -212,7 +212,7 @@ class Domain(TimeStampedModel, DomainHelper): @Cache def registry_expiration_date(self) -> date: """Get or set the `ex_date` element from the registry. - Additionally, update the expiration date in the registrar""" + Additionally, _get_property updates the expiration date in the registrar""" try: return self._get_property("ex_date") except Exception as e: From c2cd8ce5b656361265d534c84bbf77c1af7d038a Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 27 Nov 2023 20:30:21 -0500 Subject: [PATCH 22/35] updated test for debugging --- src/registrar/tests/test_views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 8493a3e75..6922b1b9a 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1222,8 +1222,10 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest): def test_domain_detail_link_works(self): home_page = self.app.get("/") self.assertContains(home_page, "igorville.gov") + print(home_page) # click the "Edit" link detail_page = home_page.click("Manage", index=0) + print(detail_page) self.assertContains(detail_page, "igorville.gov") self.assertContains(detail_page, "Status") From 3ddc9260f2bd2c286175ca67c6e1df3f46071b0b Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 27 Nov 2023 20:38:11 -0500 Subject: [PATCH 23/35] updated test for debugging --- src/registrar/tests/test_views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 6922b1b9a..903027988 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -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") From 2a31c5410baa62900d7e6de3fa6c12319299e12c Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 27 Nov 2023 20:44:34 -0500 Subject: [PATCH 24/35] updated test for debugging --- src/registrar/tests/test_views.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 903027988..13b219f20 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1231,7 +1231,7 @@ class TestDomainDetail(TestDomainOverview): 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""" @@ -1243,7 +1243,7 @@ class TestDomainDetail(TestDomainOverview): 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") @@ -1252,7 +1252,7 @@ class TestDomainDetail(TestDomainOverview): 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") @@ -1263,7 +1263,7 @@ class TestDomainDetail(TestDomainOverview): 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") @@ -1279,7 +1279,7 @@ class TestDomainDetail(TestDomainOverview): 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 From 4a3d261de1bd1cc3fba5ef75d83176f219a5b883 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 27 Nov 2023 20:47:51 -0500 Subject: [PATCH 25/35] removed redundant tests; removed debugging from test --- src/registrar/tests/test_views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 13b219f20..88771ebab 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1224,10 +1224,8 @@ class TestDomainDetail(TestDomainOverview): def test_domain_detail_link_works(self): home_page = self.app.get("/") self.assertContains(home_page, "igorville.gov") - print(home_page) # click the "Edit" link detail_page = home_page.click("Manage", index=0) - print(detail_page) self.assertContains(detail_page, "igorville.gov") self.assertContains(detail_page, "Status") From 29962570a7b2936a3f420b2b82b763c14f1e8820 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 27 Nov 2023 21:02:31 -0500 Subject: [PATCH 26/35] fixed redundancy in test cases --- src/registrar/tests/test_views.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index d6bad9cdf..d2fdbc14f 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -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 From 7923c27fc24b07f79dec9895fb3679527578453e Mon Sep 17 00:00:00 2001 From: Michelle Rago <60157596+michelle-rago@users.noreply.github.com> Date: Tue, 28 Nov 2023 08:14:47 -0500 Subject: [PATCH 27/35] Change "Add another user" to "Add a domain manager" (#1386) * Change "Add another user" to "Add a domain manager" * Change "Add another user" to "Add a domain manager" * Update test_views.py to change "Add another user" to "Add a domain manager" --- src/registrar/templates/domain_add_user.html | 4 ++-- src/registrar/templates/domain_users.html | 2 +- src/registrar/tests/test_views.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/registrar/templates/domain_add_user.html b/src/registrar/templates/domain_add_user.html index 87b141570..d8cae576a 100644 --- a/src/registrar/templates/domain_add_user.html +++ b/src/registrar/templates/domain_add_user.html @@ -4,10 +4,10 @@ {% block title %}Add another user{% endblock %} {% block domain_content %} -

    Add another user

    +

    Add a domain manager

    You can add another user to help manage your domain. They will need to sign - into the .gov registrar with their Login.gov account. + in to the .gov registrar with their Login.gov account.

    diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index e1ea0b601..8ee837708 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -53,7 +53,7 @@ Add another user + Add a domain manager diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index d6bad9cdf..3b0287add 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1305,12 +1305,12 @@ class TestDomainManagers(TestDomainOverview): def test_domain_managers_add_link(self): """Button to get to user add page works.""" management_page = self.app.get(reverse("domain-users", kwargs={"pk": self.domain.id})) - add_page = management_page.click("Add another user") - self.assertContains(add_page, "Add another user") + add_page = management_page.click("Add a domain manager") + self.assertContains(add_page, "Add a domain manager") def test_domain_user_add(self): response = self.client.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - self.assertContains(response, "Add another user") + self.assertContains(response, "Add a domain manager") def test_domain_user_add_form(self): """Adding an existing user works.""" From 80880a4ca1d8f6cc9a440ffcb73c90d57a0e70d8 Mon Sep 17 00:00:00 2001 From: Michelle Rago <60157596+michelle-rago@users.noreply.github.com> Date: Tue, 28 Nov 2023 11:00:33 -0500 Subject: [PATCH 28/35] Update success message for org name and mailing address (#1387) Update domain.py --- src/registrar/views/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 3aac32531..4d91ddc66 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -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) From e24bf1ae58ab935211db2e4e4f0004fcc745b805 Mon Sep 17 00:00:00 2001 From: Michelle Rago <60157596+michelle-rago@users.noreply.github.com> Date: Tue, 28 Nov 2023 11:00:58 -0500 Subject: [PATCH 29/35] Change "Active users" to "Domain managers" on domain managers page (#1407) --- src/registrar/templates/domain_users.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index 8ee837708..0eecd35b3 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -25,8 +25,8 @@ {% if domain.permissions %}
    -

    Active users

    - +

    Domain managers

    + From bd6eabe9165f334183a1881b4e7f473365f87458 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 28 Nov 2023 12:17:47 -0500 Subject: [PATCH 30/35] Typo fixes in ops readme and rotate secrets --- docs/operations/README.md | 4 ++-- docs/operations/runbooks/rotate_application_secrets.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/operations/README.md b/docs/operations/README.md index 4c7f182bd..0629608dd 100644 --- a/docs/operations/README.md +++ b/docs/operations/README.md @@ -55,9 +55,9 @@ In the case where a bug fix or feature needs to be added outside of the normal c 1. Code will need to be branched NOT off of main, but off of the same commit as the most recent stable commit. This should be the one tagged with the most recent vX.XX.XX value. 2. After making the bug fix, the approved PR branch will not be merged yet, instead it will be tagged with a new release tag, incrementing the patch value from the last commit number. -3. If main and stable are on the the same commit then merge this branch into the staging using the staging release tag (staging-). +3. If main and stable are on the the same commit then merge this branch into staging using the staging release tag (staging-). 4. If staging is already ahead stable, you may need to create another branch that is based off of the current staging commit, merge in your code change and then tag that branch with the staging release. -5. Wait to merge your original branch until both deploys finish. Once they succeed then merge to main per the usual process. +5. Wait to merge your original branch until both deploys finish. Once they succeed then merge to main per the usual process. ## Serving static assets We are using [WhiteNoise](http://whitenoise.evans.io/en/stable/index.html) plugin to serve our static assets on cloud.gov. This plugin is added to the `MIDDLEWARE` list in our apps `settings.py`. diff --git a/docs/operations/runbooks/rotate_application_secrets.md b/docs/operations/runbooks/rotate_application_secrets.md index 78c402efe..a776e60b8 100644 --- a/docs/operations/runbooks/rotate_application_secrets.md +++ b/docs/operations/runbooks/rotate_application_secrets.md @@ -112,7 +112,7 @@ base64 -i client.key base64 -i client.crt ``` -You'll need to give the new certificate to the registry vendor _before_ rotating it in production. Once it has been accepted by the vender, make sure to update the kdbx file on Google Drive. +You'll need to give the new certificate to the registry vendor _before_ rotating it in production. Once it has been accepted by the vendor, make sure to update the kdbx file on Google Drive. ## REGISTRY_HOSTNAME From b6b251b287bd6b03c393ea428f0aaa697698026b Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 28 Nov 2023 13:32:48 -0500 Subject: [PATCH 31/35] more minor typo fixes --- .../decisions/0024-production-release-cadence.md | 6 +++--- docs/django-admin/roles.md | 6 +++--- docs/operations/runbooks/update_python_dependencies.md | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/architecture/decisions/0024-production-release-cadence.md b/docs/architecture/decisions/0024-production-release-cadence.md index 1020d3506..85bfcdfe2 100644 --- a/docs/architecture/decisions/0024-production-release-cadence.md +++ b/docs/architecture/decisions/0024-production-release-cadence.md @@ -15,9 +15,9 @@ Going into our first production launch we need a plan describing what our releas **Option 1:** Releasing to stable/staging once a sprint Releasing once a sprint would mean that we release the past sprint's work to stable at the end of the current sprint. At the same point, the current sprint's work would be pushed to staging, thus making staging a full sprint ahead of stable. While this is more straight forward, it means our users would have to wait longer to see changes that weren't deemed critical. **Option 2:** Releasing to stable/staging once a week -Releasing once a week would follow the same flow but with code being released to staging one week before the same code is released to stable. This would make stable only one week behind staging and would allow us to roll out minor bug fixes and faster with greater speed. The negative side is that we have less time to see if errors occur on staging +Releasing once a week would follow the same flow but with code being released to staging one week before the same code is released to stable. This would make stable only one week behind staging and would allow us to roll out minor bug fixes faster. The negative side is that we have less time to see if errors occur on staging. -In both of the above scenarios the release date would fall on the same day of the week that the sprint starts, which is currently a Wednesday. Additionally, in both scenarios the release commits would eventually be tagged with both a staging and stable tag. Furthermore, critical bugs or features would be exempt from these restrictions based on the product owner's discretion. +In both of the above scenarios, the release date would fall on the same day of the week that the sprint starts which is currently a Wednesday. Additionally, in both scenarios the release commits would eventually be tagged with both a staging and stable tag. Furthermore, critical bugs or features would be exempt from these restrictions based on the product owner's discretion. ## Decision @@ -25,6 +25,6 @@ We decided to go with option 2 and release once a week once in production. This ## Consequences -Work not completed by end of the sprint will have to wait to be added to stable. Also, making quick fixes for bugs that are found on stable will be a little more complicated to fix. +Work not completed by end of the sprint will have to wait to be added to stable. Also, making quick fixes for bugs that are found on stable will be a little more complicated. When first going into production, staging and stable will start with the same code base. The following week a new release will be made to staging, but not stable as no code will have been on staging long enough to warrant another release. Thus just at the start of launch stable will be essentially frozen for 2 weeks, not one. diff --git a/docs/django-admin/roles.md b/docs/django-admin/roles.md index 6fc0d385e..c527bbfa5 100644 --- a/docs/django-admin/roles.md +++ b/docs/django-admin/roles.md @@ -19,7 +19,7 @@ To do this, do the following: 3. Click on their username, then scroll down to the `User Permissions` section. 4. Under `User Permissions`, see the `Groups` table which has a column for `Available groups` and `Chosen groups`. Select the permission you want from the `Available groups` column and click the right arrow to move it to the `Chosen groups`. Note, if you want this user to be an analyst select `cisa_analysts_group`, otherwise select the `full_access_group`. 5. (Optional) If the user needs access to django admin (such as an analyst), then you will also need to make sure "Staff Status" is checked. This can be found in the same `User Permissions` section right below the checkbox for `Active`. -6. Click `Save` to apply all changes +6. Click `Save` to apply all changes. ## Removing a user group permission via django-admin @@ -30,7 +30,7 @@ If an employee was given the wrong permissions or has had a change in roles that 3. In this table, select the permission you want to remove from the `Chosen groups` and then click the left facing arrow to move the permission to `Available groups`. 4. Depending on the scenario you may now need to add the opposite permission group to the `Chosen groups` section, please see the section above for instructions on how to do that. 5. If the user should no longer see the admin page, you must ensure that under `User Permissions`, `Staff status` is NOT checked. -6. Click `Save` to apply all changes +6. Click `Save` to apply all changes. ## Editing group permissions through code @@ -40,4 +40,4 @@ We can edit and deploy new group permissions by: 2. Duplicating migration `0036_create_groups_01` and running migrations (append the name with a version number to help django detect the migration eg 0037_create_groups_02) -3. Making sure to update the dependency on the new migration with the previous migration \ No newline at end of file +3. Making sure to update the dependency on the new migration with the previous migration. \ No newline at end of file diff --git a/docs/operations/runbooks/update_python_dependencies.md b/docs/operations/runbooks/update_python_dependencies.md index 16475d3db..ea206bbde 100644 --- a/docs/operations/runbooks/update_python_dependencies.md +++ b/docs/operations/runbooks/update_python_dependencies.md @@ -3,7 +3,7 @@ 1. Check the [Pipfile](../../../src/Pipfile) for pinned dependencies and manually adjust the version numbers -1. Run +2. Run cd src docker-compose run app bash -c "pipenv lock && pipenv requirements > requirements.txt" @@ -14,6 +14,6 @@ The requirements.txt is used by Cloud.gov. It is needed to work around a bug in the CloudFoundry buildpack version of Pipenv that breaks on installing from a git repository. -1. (optional) Run `docker-compose stop` and `docker-compose build` to build a new image for local development with the updated dependencies. +3. (optional) Run `docker-compose stop` and `docker-compose build` to build a new image for local development with the updated dependencies. The reason for de-coupling the `build` and `lock` steps is to increase consistency between builds--a run of `build` will always get exactly the dependencies listed in `Pipfile.lock`, nothing more, nothing less. \ No newline at end of file From 89aa3534cf42192ee1cc249c91604031081f6e09 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 28 Nov 2023 17:10:20 -0500 Subject: [PATCH 32/35] fixed formatting of error message in javascript --- src/registrar/assets/js/get-gov.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 4ef4efbba..d069e8dc4 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -122,7 +122,7 @@ function inlineToast(el, id, style, msg) { } 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 { From f957c299b8e80ba8570535f54913880faa7eaf85 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 28 Nov 2023 17:12:12 -0500 Subject: [PATCH 33/35] added comment --- src/api/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api/views.py b/src/api/views.py index 5e5365e58..a9f8d7692 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -21,6 +21,8 @@ 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.", + # 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 isn’t available. " "" From be1f475add71255eca99e2ffebe58d9cd1a71747 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Tue, 28 Nov 2023 18:47:12 -0800 Subject: [PATCH 34/35] Add new developer sandbox 'backup' infrastructure --- .github/workflows/migrate.yaml | 1 + .github/workflows/reset-db.yaml | 1 + ops/manifests/manifest-backup.yaml | 32 ++++++++++++++++++++++++++++++ src/registrar/config/settings.py | 1 + 4 files changed, 35 insertions(+) create mode 100644 ops/manifests/manifest-backup.yaml diff --git a/.github/workflows/migrate.yaml b/.github/workflows/migrate.yaml index b1880c830..96078f932 100644 --- a/.github/workflows/migrate.yaml +++ b/.github/workflows/migrate.yaml @@ -16,6 +16,7 @@ on: - stable - staging - development + - backup - ky - es - nl diff --git a/.github/workflows/reset-db.yaml b/.github/workflows/reset-db.yaml index a28270a22..943175a87 100644 --- a/.github/workflows/reset-db.yaml +++ b/.github/workflows/reset-db.yaml @@ -16,6 +16,7 @@ on: options: - staging - development + - backup - ky - es - nl diff --git a/ops/manifests/manifest-backup.yaml b/ops/manifests/manifest-backup.yaml new file mode 100644 index 000000000..c4615d1d5 --- /dev/null +++ b/ops/manifests/manifest-backup.yaml @@ -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 diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 3487b1b66..7f20c8129 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -627,6 +627,7 @@ ALLOWED_HOSTS = [ "getgov-stable.app.cloud.gov", "getgov-staging.app.cloud.gov", "getgov-development.app.cloud.gov", + "getgov-backup.app.cloud.gov", "getgov-ky.app.cloud.gov", "getgov-es.app.cloud.gov", "getgov-nl.app.cloud.gov", From 056ef6f894985b01c1279c03991b7a989a0f4cbf Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Tue, 28 Nov 2023 18:51:37 -0800 Subject: [PATCH 35/35] added missing reference on deploy workflow --- .github/workflows/deploy-sandbox.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy-sandbox.yaml b/.github/workflows/deploy-sandbox.yaml index 583791fc1..ebbe4a376 100644 --- a/.github/workflows/deploy-sandbox.yaml +++ b/.github/workflows/deploy-sandbox.yaml @@ -21,6 +21,7 @@ jobs: || startsWith(github.head_ref, 'dk/') || startsWith(github.head_ref, 'es/') || startsWith(github.head_ref, 'ky/') + || startsWith(github.head_ref, 'backup/') outputs: environment: ${{ steps.var.outputs.environment}} runs-on: "ubuntu-latest"
    Domain usersDomain managers
    Email