From ce280a5b59af7749189abfce5b0b6db77a316204 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 24 Jan 2024 10:02:49 -0700 Subject: [PATCH 001/119] Add readonly field --- src/registrar/admin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 3c1823f83..79fc93d95 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -621,6 +621,7 @@ class DomainInformationAdmin(ListHeaderAdmin): "type_of_work", "more_organization_information", "domain", + "domain_application", "submitter", "no_other_contacts_rationale", "anything_else", @@ -789,6 +790,7 @@ class DomainApplicationAdmin(ListHeaderAdmin): "creator", "about_your_organization", "requested_domain", + "approved_domain", "alternative_domains", "purpose", "submitter", From 9650143e968be27afc6d4113d4c35c81ddaec1d4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 24 Jan 2024 10:41:15 -0700 Subject: [PATCH 002/119] Add unit tests --- src/registrar/tests/test_admin.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index f7b1ef06e..1f179ba09 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -639,6 +639,7 @@ class TestDomainApplicationAdmin(MockEppLib): "creator", "about_your_organization", "requested_domain", + "approved_domain", "alternative_domains", "purpose", "submitter", @@ -1064,7 +1065,7 @@ class DomainInvitationAdminTest(TestCase): self.assertContains(response, retrieved_html, count=1) -class DomainInformationAdminTest(TestCase): +class TestDomainInformationAdmin(TestCase): def setUp(self): """Setup environment for a mock admin user""" self.site = AdminSite() @@ -1072,6 +1073,7 @@ class DomainInformationAdminTest(TestCase): self.admin = DomainInformationAdmin(model=DomainInformation, admin_site=self.site) self.client = Client(HTTP_HOST="localhost:8080") self.superuser = create_superuser() + self.staffuser = create_user() self.mock_data_generator = AuditedAdminMockData() self.test_helper = GenericTestHelper( @@ -1115,6 +1117,27 @@ class DomainInformationAdminTest(TestCase): Contact.objects.all().delete() User.objects.all().delete() + def test_readonly_fields_for_analyst(self): + """Ensures that analysts have their permissions setup correctly""" + request = self.factory.get("/") + request.user = self.staffuser + + readonly_fields = self.admin.get_readonly_fields(request) + + expected_fields = [ + "creator", + "type_of_work", + "more_organization_information", + "domain", + "domain_application", + "submitter", + "no_other_contacts_rationale", + "anything_else", + "is_policy_acknowledged", + ] + + self.assertEqual(readonly_fields, expected_fields) + def test_domain_sortable(self): """Tests if DomainInformation sorts by domain correctly""" p = "adminpass" From e2c965a9156855eb9db22d1675ff49c67047f7d9 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 24 Jan 2024 21:28:43 -0500 Subject: [PATCH 003/119] Reduce email sends to first-time transition for the applicable transitions --- src/registrar/models/domain_application.py | 73 +++++-- src/registrar/tests/test_admin.py | 227 +++++++++++---------- src/registrar/tests/test_models.py | 217 +++++++++++++++++++- 3 files changed, 382 insertions(+), 135 deletions(-) diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 196449bfa..e1c809058 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -1,4 +1,6 @@ from __future__ import annotations +from json import JSONDecodeError +import json from typing import Union import logging @@ -12,6 +14,7 @@ from registrar.models.domain import Domain from .utility.time_stamped_model import TimeStampedModel from ..utility.email import send_templated_email, EmailSendingError from itertools import chain +from auditlog.models import LogEntry # type: ignore logger = logging.getLogger(__name__) @@ -565,6 +568,25 @@ class DomainApplication(TimeStampedModel): except Exception: return "" + def has_previously_had_a_status_of(self, status): + """Return True if this request has previously had the status of {passed param}.""" + + log_entries = LogEntry.objects.get_for_object(self) + + for entry in log_entries: + try: + changes_dict = json.loads(entry.changes) + logger.info(changes_dict) + # changes_dict will look like {'status': ['withdrawn', 'submitted']}, + # henceforth the len(changes_dict.get('status', [])) == 2 + if len(changes_dict.get("status", [])) == 2 and changes_dict.get("status", [])[1] == status: + logger.info(f"found one instance where it had a status of {status}") + return True + except JSONDecodeError: + pass + + return False + def domain_is_not_active(self): if self.approved_domain: return not self.approved_domain.is_active() @@ -633,11 +655,13 @@ class DomainApplication(TimeStampedModel): self.submission_date = timezone.now().date() self.save() - self._send_status_update_email( - "submission confirmation", - "emails/submission_confirmation.txt", - "emails/submission_confirmation_subject.txt", - ) + # Limit email notifications for this transition to the first time the request transitions to this status + if not self.has_previously_had_a_status_of("submitted"): + self._send_status_update_email( + "submission confirmation", + "emails/submission_confirmation.txt", + "emails/submission_confirmation_subject.txt", + ) @transition( field="status", @@ -713,12 +737,14 @@ class DomainApplication(TimeStampedModel): user=self.creator, domain=created_domain, role=UserDomainRole.Roles.MANAGER ) - self._send_status_update_email( - "application approved", - "emails/status_change_approved.txt", - "emails/status_change_approved_subject.txt", - send_email, - ) + # Limit email notifications for this transition to the first time the request transitions to this status + if not self.has_previously_had_a_status_of("approved"): + self._send_status_update_email( + "application approved", + "emails/status_change_approved.txt", + "emails/status_change_approved_subject.txt", + send_email, + ) @transition( field="status", @@ -727,11 +753,14 @@ class DomainApplication(TimeStampedModel): ) def withdraw(self): """Withdraw an application that has been submitted.""" - self._send_status_update_email( - "withdraw", - "emails/domain_request_withdrawn.txt", - "emails/domain_request_withdrawn_subject.txt", - ) + + # Limit email notifications for this transition to the first time the request transitions to this status + if not self.has_previously_had_a_status_of("withdrawn"): + self._send_status_update_email( + "withdraw", + "emails/domain_request_withdrawn.txt", + "emails/domain_request_withdrawn_subject.txt", + ) @transition( field="status", @@ -757,11 +786,13 @@ class DomainApplication(TimeStampedModel): logger.error(err) logger.error("Can't query an approved domain while attempting a DA reject()") - self._send_status_update_email( - "action needed", - "emails/status_change_rejected.txt", - "emails/status_change_rejected_subject.txt", - ) + # Limit email notifications for this transition to the first time the request transitions to this status + if not self.has_previously_had_a_status_of("rejected"): + self._send_status_update_email( + "action needed", + "emails/status_change_rejected.txt", + "emails/status_change_rejected_subject.txt", + ) @transition( field="status", diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 7c9aa8fe4..7773cb60b 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -316,6 +316,7 @@ class TestDomainApplicationAdminForm(TestCase): ) +@boto3_mocking.patching class TestDomainApplicationAdmin(MockEppLib): def setUp(self): super().setUp() @@ -421,83 +422,143 @@ class TestDomainApplicationAdmin(MockEppLib): # Now let's make sure the long description does not exist self.assertNotContains(response, "Federal: an agency of the U.S. government") - @boto3_mocking.patching + def transition_state_and_send_email(self, application, status): + """Helper method for the email test cases.""" + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Create a mock request + request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) + + # Modify the application's property + application.status = status + + # Use the model admin's save_model method + self.admin.save_model(request, application, form=None, change=True) + + def assert_email_is_accurate(self, expected_string, email_index, email_address): + """Helper method for the email test cases. + email_index is the index of the email in mock_client.""" + + # Access the arguments passed to send_email + call_args = self.mock_client.EMAILS_SENT + kwargs = call_args[email_index]["kwargs"] + + # Retrieve the email details from the arguments + from_email = kwargs.get("FromEmailAddress") + to_email = kwargs["Destination"]["ToAddresses"][0] + email_content = kwargs["Content"] + email_body = email_content["Simple"]["Body"]["Text"]["Data"] + + # Assert or perform other checks on the email details + self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL) + self.assertEqual(to_email, email_address) + self.assertIn(expected_string, email_body) + def test_save_model_sends_submitted_email(self): - # make sure there is no user with this email + """When transitioning to submitted the first time (and the first time only) on a domain request, + an email is sent out.""" + + # Ensure there is no user with this email EMAIL = "mayor@igorville.gov" User.objects.filter(email=EMAIL).delete() - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Create a sample application - application = completed_application() - - # Create a mock request - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - - # Modify the application's property - application.status = DomainApplication.ApplicationStatus.SUBMITTED - - # Use the model admin's save_model method - self.admin.save_model(request, application, form=None, change=True) - - # Access the arguments passed to send_email - call_args = self.mock_client.EMAILS_SENT - kwargs = call_args[0]["kwargs"] - - # Retrieve the email details from the arguments - from_email = kwargs.get("FromEmailAddress") - to_email = kwargs["Destination"]["ToAddresses"][0] - email_content = kwargs["Content"] - email_body = email_content["Simple"]["Body"]["Text"]["Data"] - - # Assert or perform other checks on the email details - expected_string = "We received your .gov domain request." - self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL) - self.assertEqual(to_email, EMAIL) - self.assertIn(expected_string, email_body) + # Create a sample application + application = completed_application() + # Test Submitted Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) + self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - @boto3_mocking.patching + # Test Withdrawn Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN) + self.assert_email_is_accurate( + "Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + # Test Submitted Status Again (No new email should be sent) + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + def test_save_model_sends_approved_email(self): - # make sure there is no user with this email + """When transitioning to approved the first time (and the first time only) on a domain request, + an email is sent out.""" + + # Ensure there is no user with this email EMAIL = "mayor@igorville.gov" User.objects.filter(email=EMAIL).delete() - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - - # Create a mock request - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - - # Modify the application's property - application.status = DomainApplication.ApplicationStatus.APPROVED - - # Use the model admin's save_model method - self.admin.save_model(request, application, form=None, change=True) - - # Access the arguments passed to send_email - call_args = self.mock_client.EMAILS_SENT - kwargs = call_args[0]["kwargs"] - - # Retrieve the email details from the arguments - from_email = kwargs.get("FromEmailAddress") - to_email = kwargs["Destination"]["ToAddresses"][0] - email_content = kwargs["Content"] - email_body = email_content["Simple"]["Body"]["Text"]["Data"] - - # Assert or perform other checks on the email details - expected_string = "Congratulations! Your .gov domain request has been approved." - self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL) - self.assertEqual(to_email, EMAIL) - self.assertIn(expected_string, email_body) + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + # Test Submitted Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 0, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - @boto3_mocking.patching + # Test Withdrawn Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED) + self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + # Test Submitted Status Again (No new email should be sent) + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + def test_save_model_sends_rejected_email(self): + """When transitioning to rejected the first time (and the first time only) on a domain request, + an email is sent out.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + + # Test Submitted Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED) + self.assert_email_is_accurate("Your .gov domain request has been rejected.", 0, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Test Withdrawn Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + # Test Submitted Status Again (No new email should be sent) + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + def test_save_model_sends_withdrawn_email(self): + """When transitioning to withdrawn the first time (and the first time only) on a domain request, + an email is sent out.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample application + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + + # Test Submitted Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN) + self.assert_email_is_accurate( + "Your .gov domain request has been withdrawn and will not be reviewed by our team.", 0, EMAIL + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Test Withdrawn Status + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) + self.assert_email_is_accurate("We received your .gov domain request.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + # Test Submitted Status Again (No new email should be sent) + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + def test_save_model_sets_approved_domain(self): # make sure there is no user with this email EMAIL = "mayor@igorville.gov" @@ -520,45 +581,6 @@ class TestDomainApplicationAdmin(MockEppLib): # Test that approved domain exists and equals requested domain self.assertEqual(application.requested_domain.name, application.approved_domain.name) - @boto3_mocking.patching - def test_save_model_sends_rejected_email(self): - # make sure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Create a sample application - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - - # Create a mock request - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - - # Modify the application's property - application.status = DomainApplication.ApplicationStatus.REJECTED - - # Use the model admin's save_model method - self.admin.save_model(request, application, form=None, change=True) - - # Access the arguments passed to send_email - call_args = self.mock_client.EMAILS_SENT - kwargs = call_args[0]["kwargs"] - - # Retrieve the email details from the arguments - from_email = kwargs.get("FromEmailAddress") - to_email = kwargs["Destination"]["ToAddresses"][0] - email_content = kwargs["Content"] - email_body = email_content["Simple"]["Body"]["Text"]["Data"] - - # Assert or perform other checks on the email details - expected_string = "Your .gov domain request has been rejected." - self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL) - self.assertEqual(to_email, EMAIL) - self.assertIn(expected_string, email_body) - - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - - @boto3_mocking.patching def test_save_model_sets_restricted_status_on_user(self): # make sure there is no user with this email EMAIL = "mayor@igorville.gov" @@ -707,7 +729,6 @@ class TestDomainApplicationAdmin(MockEppLib): "Cannot edit an application with a restricted creator.", ) - @boto3_mocking.patching def test_error_when_saving_approved_to_rejected_and_domain_is_active(self): # Create an instance of the model application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index ef6522747..a1b22373d 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -1,6 +1,6 @@ from django.test import TestCase from django.db.utils import IntegrityError -from unittest.mock import patch +from unittest.mock import MagicMock, patch from registrar.models import ( Contact, @@ -154,17 +154,34 @@ class TestDomainApplication(TestCase): application.submit() self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED) + @patch("auditlog.models.LogEntry.objects.get_for_object") + def test_has_previously_had_a_status_of_returns_true(self, mock_get_for_object): + """Set up mock LogEntry.objects.get_for_object to return a log entry with the desired status""" + + log_entry_with_status = MagicMock(changes='{"status": ["previous_status", "desired_status"]}') + mock_get_for_object.return_value = [log_entry_with_status] + + result = self.started_application.has_previously_had_a_status_of("desired_status") + + self.assertTrue(result) + + @patch("auditlog.models.LogEntry.objects.get_for_object") + def test_has_previously_had_a_status_of_returns_false(self, mock_get_for_object): + """Set up mock LogEntry.objects.get_for_object to return a log entry + with a different status than the desired status""" + + log_entry_with_status = MagicMock(changes='{"status": ["previous_status", "different_status"]}') + mock_get_for_object.return_value = [log_entry_with_status] + + result = self.started_application.has_previously_had_a_status_of("desired_status") + + self.assertFalse(result) + def test_submit_sends_email(self): """Create an application and submit it and see if email was sent.""" - user, _ = User.objects.get_or_create(username="testy") - contact = Contact.objects.create(email="test@test.gov") - domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") - application = DomainApplication.objects.create( - creator=user, - requested_domain=domain, - submitter=contact, - ) - application.save() + + # submitter's email is mayor@igorville.gov + application = completed_application() with boto3_mocking.clients.handler_for("sesv2", self.mock_client): with less_console_noise(): @@ -176,7 +193,185 @@ class TestDomainApplication(TestCase): [ email for email in MockSESClient.EMAILS_SENT - if "test@test.gov" in email["kwargs"]["Destination"]["ToAddresses"] + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + ), + 0, + ) + + @patch("auditlog.models.LogEntry.objects.get_for_object") + def test_submit_does_not_send_email_if_submitted_previously(self, mock_get_for_object): + """Create an application, make it so it was submitted previously, submit it, + and see that an email was not sent.""" + + # submitter's email is mayor@igorville.gov + application = completed_application() + + # Mock the logs + log_entry_with_status = MagicMock(changes='{"status": ["started", "submitted"]}') + mock_get_for_object.return_value = [log_entry_with_status] + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.submit() + + # check to see if an email was sent + self.assertEqual( + len( + [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + ), + 0, + ) + + def test_approve_sends_email(self): + """Create an application and approve it and see if email was sent.""" + + # submitter's email is mayor@igorville.gov + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.approve() + + # check to see if an email was sent + self.assertGreater( + len( + [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + ), + 0, + ) + + @patch("auditlog.models.LogEntry.objects.get_for_object") + def test_approve_does_not_send_email_if_approved_previously(self, mock_get_for_object): + """Create an application, make it so it was approved previously, approve it, + and see that an email was not sent.""" + + # submitter's email is mayor@igorville.gov + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + + # Mock the logs + log_entry_with_status = MagicMock(changes='{"status": ["submitted", "approved"]}') + mock_get_for_object.return_value = [log_entry_with_status] + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.approve() + + # check to see if an email was sent + self.assertEqual( + len( + [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + ), + 0, + ) + + def test_withdraw_sends_email(self): + """Create an application and withdraw it and see if email was sent.""" + + # submitter's email is mayor@igorville.gov + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.withdraw() + + # check to see if an email was sent + self.assertGreater( + len( + [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + ), + 0, + ) + + @patch("auditlog.models.LogEntry.objects.get_for_object") + def test_withdraw_does_not_send_email_if_withdrawn_previously(self, mock_get_for_object): + """Create an application, make it so it was withdrawn previously, withdraw it, + and see that an email was not sent.""" + + # submitter's email is mayor@igorville.gov + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + + # Mock the logs + log_entry_with_status = MagicMock(changes='{"status": ["submitted", "withdrawn"]}') + mock_get_for_object.return_value = [log_entry_with_status] + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.withdraw() + + # check to see if an email was sent + self.assertEqual( + len( + [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + ), + 0, + ) + + def test_reject_sends_email(self): + """Create an application and reject it and see if email was sent.""" + + # submitter's email is mayor@igorville.gov + application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.reject() + + # check to see if an email was sent + self.assertGreater( + len( + [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + ), + 0, + ) + + @patch("auditlog.models.LogEntry.objects.get_for_object") + def test_reject_does_not_send_email_if_rejected_previously(self, mock_get_for_object): + """Create an application, make it so it was rejected previously, reject it, + and see that an email was not sent.""" + + # submitter's email is mayor@igorville.gov + application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) + + # Mock the logs + log_entry_with_status = MagicMock(changes='{"status": ["submitted", "rejected"]}') + mock_get_for_object.return_value = [log_entry_with_status] + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.reject() + + # check to see if an email was sent + self.assertEqual( + len( + [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] ] ), 0, From e04a8bde61e7f690943356b5131772998c51c560 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 24 Jan 2024 21:48:31 -0500 Subject: [PATCH 004/119] clean up --- src/registrar/models/domain_application.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index e1c809058..0a7bbcd0b 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -576,14 +576,14 @@ class DomainApplication(TimeStampedModel): for entry in log_entries: try: changes_dict = json.loads(entry.changes) - logger.info(changes_dict) # changes_dict will look like {'status': ['withdrawn', 'submitted']}, # henceforth the len(changes_dict.get('status', [])) == 2 if len(changes_dict.get("status", [])) == 2 and changes_dict.get("status", [])[1] == status: - logger.info(f"found one instance where it had a status of {status}") return True except JSONDecodeError: - pass + logger.warning( + "JSON decode error while parsing logs for domain requests in has_previously_had_a_status_of" + ) return False From abb4bd8a3f36bb53ba2b0342fde7dac2d76ab97a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 25 Jan 2024 15:02:09 -0700 Subject: [PATCH 005/119] Catch edge case --- src/registrar/templates/domain_detail.html | 2 +- src/registrar/views/domain.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index 09fc189e4..4dcef69ae 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -56,7 +56,7 @@ {% 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%} + {% if security_email is not None and security_email not in hidden_security_emails%} {% 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 editable=domain.is_editable %} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index b81d268b4..08db99e47 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -142,7 +142,7 @@ class DomainView(DomainBaseView): context = super().get_context_data(**kwargs) default_email = self.object.get_default_security_contact().email - context["default_security_email"] = default_email + context["hidden_security_emails"] = [default_email, "registrar@dotgov.gov"] security_email = self.object.get_security_email() if security_email is None or security_email == default_email: From a1f49329353c7a6b69cf5373203631b60a0bde1b Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 26 Jan 2024 16:00:32 -0500 Subject: [PATCH 006/119] Code cleanup --- src/registrar/models/domain_application.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 0a7bbcd0b..da801ce3d 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -578,7 +578,8 @@ class DomainApplication(TimeStampedModel): changes_dict = json.loads(entry.changes) # changes_dict will look like {'status': ['withdrawn', 'submitted']}, # henceforth the len(changes_dict.get('status', [])) == 2 - if len(changes_dict.get("status", [])) == 2 and changes_dict.get("status", [])[1] == status: + status_change = changes_dict.get("status", []) + if len(status_change) == 2 and status_change[1] == status: return True except JSONDecodeError: logger.warning( @@ -656,7 +657,7 @@ class DomainApplication(TimeStampedModel): self.save() # Limit email notifications for this transition to the first time the request transitions to this status - if not self.has_previously_had_a_status_of("submitted"): + if not self.has_previously_had_a_status_of(DomainApplication.ApplicationStatus.SUBMITTED): self._send_status_update_email( "submission confirmation", "emails/submission_confirmation.txt", @@ -738,7 +739,7 @@ class DomainApplication(TimeStampedModel): ) # Limit email notifications for this transition to the first time the request transitions to this status - if not self.has_previously_had_a_status_of("approved"): + if not self.has_previously_had_a_status_of(DomainApplication.ApplicationStatus.APPROVED): self._send_status_update_email( "application approved", "emails/status_change_approved.txt", @@ -755,7 +756,7 @@ class DomainApplication(TimeStampedModel): """Withdraw an application that has been submitted.""" # Limit email notifications for this transition to the first time the request transitions to this status - if not self.has_previously_had_a_status_of("withdrawn"): + if not self.has_previously_had_a_status_of(DomainApplication.ApplicationStatus.WITHDRAWN): self._send_status_update_email( "withdraw", "emails/domain_request_withdrawn.txt", @@ -787,7 +788,7 @@ class DomainApplication(TimeStampedModel): logger.error("Can't query an approved domain while attempting a DA reject()") # Limit email notifications for this transition to the first time the request transitions to this status - if not self.has_previously_had_a_status_of("rejected"): + if not self.has_previously_had_a_status_of(DomainApplication.ApplicationStatus.REJECTED): self._send_status_update_email( "action needed", "emails/status_change_rejected.txt", From 0546bd08e54b552c0683a3fdee67b37ecebec941 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 29 Jan 2024 18:22:35 -0500 Subject: [PATCH 007/119] suppressed logging from all test cases; made slight changes to handling of exceptions in connection pooling --- src/djangooidc/tests/test_views.py | 253 +- src/epplibwrapper/tests/common.py | 51 + src/epplibwrapper/tests/test_pool.py | 101 +- src/epplibwrapper/utility/pool.py | 13 + src/registrar/tests/common.py | 33 +- src/registrar/tests/test_admin.py | 424 ++- .../tests/test_management_scripts.py | 488 ++- src/registrar/tests/test_models.py | 243 +- src/registrar/tests/test_models_domain.py | 2757 ++++++++--------- src/registrar/tests/test_reports.py | 682 ++-- .../test_transition_domain_migrations.py | 887 +++--- src/registrar/tests/test_views.py | 379 +-- 12 files changed, 3145 insertions(+), 3166 deletions(-) create mode 100644 src/epplibwrapper/tests/common.py diff --git a/src/djangooidc/tests/test_views.py b/src/djangooidc/tests/test_views.py index 63b23df96..4193f723b 100644 --- a/src/djangooidc/tests/test_views.py +++ b/src/djangooidc/tests/test_views.py @@ -35,155 +35,155 @@ class ViewsTest(TestCase): pass def test_openid_sets_next(self, mock_client): - # setup - callback_url = reverse("openid_login_callback") - # mock - mock_client.create_authn_request.side_effect = self.say_hi - mock_client.get_default_acr_value.side_effect = self.create_acr - # test - response = self.client.get(reverse("login"), {"next": callback_url}) - # assert - session = mock_client.create_authn_request.call_args[0][0] - self.assertEqual(session["next"], callback_url) - self.assertEqual(response.status_code, 200) - self.assertContains(response, "Hi") + with less_console_noise(): + # setup + callback_url = reverse("openid_login_callback") + # mock + mock_client.create_authn_request.side_effect = self.say_hi + mock_client.get_default_acr_value.side_effect = self.create_acr + # test + response = self.client.get(reverse("login"), {"next": callback_url}) + # assert + session = mock_client.create_authn_request.call_args[0][0] + self.assertEqual(session["next"], callback_url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Hi") def test_openid_raises(self, mock_client): - # mock - mock_client.create_authn_request.side_effect = Exception("Test") - # test with less_console_noise(): + # mock + mock_client.create_authn_request.side_effect = Exception("Test") + # test response = self.client.get(reverse("login")) - # assert - self.assertEqual(response.status_code, 500) - self.assertTemplateUsed(response, "500.html") - self.assertIn("Server error", response.content.decode("utf-8")) + # assert + self.assertEqual(response.status_code, 500) + self.assertTemplateUsed(response, "500.html") + self.assertIn("Server error", response.content.decode("utf-8")) def test_callback_with_no_session_state(self, mock_client): """If the local session is None (ie the server restarted while user was logged out), we do not throw an exception. Rather, we attempt to login again.""" - # mock - mock_client.get_default_acr_value.side_effect = self.create_acr - mock_client.callback.side_effect = NoStateDefined() - # test with less_console_noise(): + # mock + mock_client.get_default_acr_value.side_effect = self.create_acr + mock_client.callback.side_effect = NoStateDefined() + # test response = self.client.get(reverse("openid_login_callback")) - # assert - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, "/") + # assert + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/") def test_login_callback_reads_next(self, mock_client): - # setup - session = self.client.session - session["next"] = reverse("logout") - session.save() - # mock - mock_client.callback.side_effect = self.user_info - # test - with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise(): - response = self.client.get(reverse("openid_login_callback")) - # assert - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, reverse("logout")) + with less_console_noise(): + # setup + session = self.client.session + session["next"] = reverse("logout") + session.save() + # mock + mock_client.callback.side_effect = self.user_info + # test + with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise(): + response = self.client.get(reverse("openid_login_callback")) + # assert + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, reverse("logout")) def test_login_callback_no_step_up_auth(self, mock_client): """Walk through login_callback when requires_step_up_auth returns False and assert that we have a redirect to /""" - # setup - session = self.client.session - session.save() - # mock - mock_client.callback.side_effect = self.user_info - # test - with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise(): - response = self.client.get(reverse("openid_login_callback")) - # assert - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, "/") + with less_console_noise(): + # setup + session = self.client.session + session.save() + # mock + mock_client.callback.side_effect = self.user_info + # test + with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise(): + response = self.client.get(reverse("openid_login_callback")) + # assert + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/") def test_requires_step_up_auth(self, mock_client): """Invoke login_callback passing it a request when requires_step_up_auth returns True and assert that session is updated and create_authn_request (mock) is called.""" - # Configure the mock to return an expected value for get_step_up_acr_value - mock_client.return_value.get_step_up_acr_value.return_value = "step_up_acr_value" - - # Create a mock request - request = self.factory.get("/some-url") - request.session = {"acr_value": ""} - - # Ensure that the CLIENT instance used in login_callback is the mock - # patch requires_step_up_auth to return True - with patch("djangooidc.views.requires_step_up_auth", return_value=True), patch( - "djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock() - ) as mock_create_authn_request: - login_callback(request) - - # create_authn_request only gets called when requires_step_up_auth is True - # and it changes this acr_value in request.session - - # Assert that acr_value is no longer empty string - self.assertNotEqual(request.session["acr_value"], "") - # And create_authn_request was called again - mock_create_authn_request.assert_called_once() + with less_console_noise(): + # Configure the mock to return an expected value for get_step_up_acr_value + mock_client.return_value.get_step_up_acr_value.return_value = "step_up_acr_value" + # Create a mock request + request = self.factory.get("/some-url") + request.session = {"acr_value": ""} + # Ensure that the CLIENT instance used in login_callback is the mock + # patch requires_step_up_auth to return True + with patch("djangooidc.views.requires_step_up_auth", return_value=True), patch( + "djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock() + ) as mock_create_authn_request: + login_callback(request) + # create_authn_request only gets called when requires_step_up_auth is True + # and it changes this acr_value in request.session + # Assert that acr_value is no longer empty string + self.assertNotEqual(request.session["acr_value"], "") + # And create_authn_request was called again + mock_create_authn_request.assert_called_once() def test_does_not_requires_step_up_auth(self, mock_client): """Invoke login_callback passing it a request when requires_step_up_auth returns False and assert that session is not updated and create_authn_request (mock) is not called. Possibly redundant with test_login_callback_requires_step_up_auth""" - # Create a mock request - request = self.factory.get("/some-url") - request.session = {"acr_value": ""} - - # Ensure that the CLIENT instance used in login_callback is the mock - # patch requires_step_up_auth to return False - with patch("djangooidc.views.requires_step_up_auth", return_value=False), patch( - "djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock() - ) as mock_create_authn_request: - login_callback(request) - - # create_authn_request only gets called when requires_step_up_auth is True - # and it changes this acr_value in request.session - - # Assert that acr_value is NOT updated by testing that it is still an empty string - self.assertEqual(request.session["acr_value"], "") - # Assert create_authn_request was not called - mock_create_authn_request.assert_not_called() + with less_console_noise(): + # Create a mock request + request = self.factory.get("/some-url") + request.session = {"acr_value": ""} + # Ensure that the CLIENT instance used in login_callback is the mock + # patch requires_step_up_auth to return False + with patch("djangooidc.views.requires_step_up_auth", return_value=False), patch( + "djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock() + ) as mock_create_authn_request: + login_callback(request) + # create_authn_request only gets called when requires_step_up_auth is True + # and it changes this acr_value in request.session + # Assert that acr_value is NOT updated by testing that it is still an empty string + self.assertEqual(request.session["acr_value"], "") + # Assert create_authn_request was not called + mock_create_authn_request.assert_not_called() @patch("djangooidc.views.authenticate") def test_login_callback_raises(self, mock_auth, mock_client): - # mock - mock_client.callback.side_effect = self.user_info - mock_auth.return_value = None - # test - with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise(): - response = self.client.get(reverse("openid_login_callback")) - # assert - self.assertEqual(response.status_code, 401) - self.assertTemplateUsed(response, "401.html") - self.assertIn("Unauthorized", response.content.decode("utf-8")) + with less_console_noise(): + # mock + mock_client.callback.side_effect = self.user_info + mock_auth.return_value = None + # test + with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise(): + response = self.client.get(reverse("openid_login_callback")) + # assert + self.assertEqual(response.status_code, 401) + self.assertTemplateUsed(response, "401.html") + self.assertIn("Unauthorized", response.content.decode("utf-8")) def test_logout_redirect_url(self, mock_client): - # setup - session = self.client.session - session["state"] = "TEST" # nosec B105 - session.save() - # mock - mock_client.callback.side_effect = self.user_info - mock_client.registration_response = {"post_logout_redirect_uris": ["http://example.com/back"]} - mock_client.provider_info = {"end_session_endpoint": "http://example.com/log_me_out"} - mock_client.client_id = "TEST" - # test with less_console_noise(): - response = self.client.get(reverse("logout")) - # assert - expected = ( - "http://example.com/log_me_out?client_id=TEST&state" - "=TEST&post_logout_redirect_uri=http%3A%2F%2Fexample.com%2Fback" - ) - actual = response.url - self.assertEqual(response.status_code, 302) - self.assertEqual(actual, expected) + # setup + session = self.client.session + session["state"] = "TEST" # nosec B105 + session.save() + # mock + mock_client.callback.side_effect = self.user_info + mock_client.registration_response = {"post_logout_redirect_uris": ["http://example.com/back"]} + mock_client.provider_info = {"end_session_endpoint": "http://example.com/log_me_out"} + mock_client.client_id = "TEST" + # test + with less_console_noise(): + response = self.client.get(reverse("logout")) + # assert + expected = ( + "http://example.com/log_me_out?client_id=TEST&state" + "=TEST&post_logout_redirect_uri=http%3A%2F%2Fexample.com%2Fback" + ) + actual = response.url + self.assertEqual(response.status_code, 302) + self.assertEqual(actual, expected) @patch("djangooidc.views.auth_logout") def test_logout_always_logs_out(self, mock_logout, _): @@ -194,12 +194,13 @@ class ViewsTest(TestCase): self.assertTrue(mock_logout.called) def test_logout_callback_redirects(self, _): - # setup - session = self.client.session - session["next"] = reverse("logout") - session.save() - # test - response = self.client.get(reverse("openid_logout_callback")) - # assert - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, reverse("logout")) + with less_console_noise(): + # setup + session = self.client.session + session["next"] = reverse("logout") + session.save() + # test + response = self.client.get(reverse("openid_logout_callback")) + # assert + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, reverse("logout")) diff --git a/src/epplibwrapper/tests/common.py b/src/epplibwrapper/tests/common.py new file mode 100644 index 000000000..122965ae8 --- /dev/null +++ b/src/epplibwrapper/tests/common.py @@ -0,0 +1,51 @@ +import os +import logging + +from contextlib import contextmanager + + +def get_handlers(): + """Obtain pointers to all StreamHandlers.""" + handlers = {} + + rootlogger = logging.getLogger() + for h in rootlogger.handlers: + if isinstance(h, logging.StreamHandler): + handlers[h.name] = h + + for logger in logging.Logger.manager.loggerDict.values(): + if not isinstance(logger, logging.PlaceHolder): + for h in logger.handlers: + if isinstance(h, logging.StreamHandler): + handlers[h.name] = h + + return handlers + + +@contextmanager +def less_console_noise(): + """ + Context manager to use in tests to silence console logging. + + This is helpful on tests which trigger console messages + (such as errors) which are normal and expected. + + It can easily be removed to debug a failing test. + """ + restore = {} + handlers = get_handlers() + devnull = open(os.devnull, "w") + + # redirect all the streams + for handler in handlers.values(): + prior = handler.setStream(devnull) + restore[handler.name] = prior + try: + # run the test + yield + finally: + # restore the streams + for handler in handlers.values(): + handler.setStream(restore[handler.name]) + # close the file we opened + devnull.close() diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index 1c36d26da..c602d4a06 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -9,7 +9,7 @@ from epplibwrapper.socket import Socket from epplibwrapper.utility.pool import EPPConnectionPool from registrar.models.domain import registry from contextlib import ExitStack - +from .common import less_console_noise import logging try: @@ -135,23 +135,26 @@ class TestConnectionPool(TestCase): stack.enter_context(patch.object(EPPConnectionPool, "kill_all_connections", do_nothing)) stack.enter_context(patch.object(SocketTransport, "send", self.fake_send)) stack.enter_context(patch.object(SocketTransport, "receive", fake_receive)) - # Restart the connection pool - registry.start_connection_pool() - # Pool should be running, and be the right size - self.assertEqual(registry.pool_status.connection_success, True) - self.assertEqual(registry.pool_status.pool_running, True) + with less_console_noise(): + # Restart the connection pool + registry.start_connection_pool() + # Pool should be running, and be the right size + self.assertEqual(registry.pool_status.connection_success, True) + self.assertEqual(registry.pool_status.pool_running, True) - # Send a command - result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) + # Send a command + result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) - # Should this ever fail, it either means that the schema has changed, - # or the pool is broken. - # If the schema has changed: Update the associated infoDomain.xml file - self.assertEqual(result.__dict__, expected_result) + # Should this ever fail, it either means that the schema has changed, + # or the pool is broken. + # If the schema has changed: Update the associated infoDomain.xml file + self.assertEqual(result.__dict__, expected_result) - # The number of open pools should match the number of requested ones. - # If it is 0, then they failed to open - self.assertEqual(len(registry._pool.conn), self.pool_options["size"]) + # The number of open pools should match the number of requested ones. + # If it is 0, then they failed to open + self.assertEqual(len(registry._pool.conn), self.pool_options["size"]) + # Kill the connection pool + registry.kill_pool() @patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success) def test_pool_restarts_on_send(self): @@ -198,51 +201,63 @@ class TestConnectionPool(TestCase): xml = (location).read_bytes() return xml + def do_nothing(command): + pass + # Mock what happens inside the "with" with ExitStack() as stack: stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket)) stack.enter_context(patch.object(Socket, "connect", self.fake_client)) + stack.enter_context(patch.object(EPPConnectionPool, "kill_all_connections", do_nothing)) stack.enter_context(patch.object(SocketTransport, "send", self.fake_send)) stack.enter_context(patch.object(SocketTransport, "receive", fake_receive)) - # Kill the connection pool - registry.kill_pool() + with less_console_noise(): + # Start the connection pool + registry.start_connection_pool() + # Kill the connection pool + registry.kill_pool() - self.assertEqual(registry.pool_status.connection_success, False) - self.assertEqual(registry.pool_status.pool_running, False) + self.assertEqual(registry.pool_status.pool_running, False) - # An exception should be raised as end user will be informed - # that they cannot connect to EPP - with self.assertRaises(RegistryError): - expected = "InfoDomain failed to execute due to a connection error." + # An exception should be raised as end user will be informed + # that they cannot connect to EPP + with self.assertRaises(RegistryError): + expected = "InfoDomain failed to execute due to a connection error." + result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) + self.assertEqual(result, expected) + + # A subsequent command should be successful, as the pool restarts result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) - self.assertEqual(result, expected) + # Should this ever fail, it either means that the schema has changed, + # or the pool is broken. + # If the schema has changed: Update the associated infoDomain.xml file + self.assertEqual(result.__dict__, expected_result) - # A subsequent command should be successful, as the pool restarts - result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) - # Should this ever fail, it either means that the schema has changed, - # or the pool is broken. - # If the schema has changed: Update the associated infoDomain.xml file - self.assertEqual(result.__dict__, expected_result) - - # The number of open pools should match the number of requested ones. - # If it is 0, then they failed to open - self.assertEqual(len(registry._pool.conn), self.pool_options["size"]) + # The number of open pools should match the number of requested ones. + # If it is 0, then they failed to open + self.assertEqual(len(registry._pool.conn), self.pool_options["size"]) + # Kill the connection pool + registry.kill_pool() @patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success) def test_raises_connection_error(self): """A .send is invoked on the pool, but registry connection is lost right as we send a command.""" - + with ExitStack() as stack: stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket)) stack.enter_context(patch.object(Socket, "connect", self.fake_client)) + with less_console_noise(): + # Start the connection pool + registry.start_connection_pool() - # Pool should be running - self.assertEqual(registry.pool_status.connection_success, True) - self.assertEqual(registry.pool_status.pool_running, True) + # Pool should be running + self.assertEqual(registry.pool_status.connection_success, True) + self.assertEqual(registry.pool_status.pool_running, True) - # Try to send a command out - should fail - with self.assertRaises(RegistryError): - expected = "InfoDomain failed to execute due to a connection error." - result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) - self.assertEqual(result, expected) + # Try to send a command out - should fail + with self.assertRaises(RegistryError): + expected = "InfoDomain failed to execute due to a connection error." + result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) + self.assertEqual(result, expected) + diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index 93edb2782..2c7de119f 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -85,6 +85,19 @@ class EPPConnectionPool(ConnectionPool): logger.error(message, exc_info=True) raise PoolError(code=PoolErrorCodes.KEEP_ALIVE_FAILED) from err + def _keepalive_periodic(self): + delay = float(self.keepalive) / self.size + while 1: + try: + with self.get() as c: + self._keepalive(c) + except PoolError as err: + logger.error(err.message, exc_info=True) + except self.exc_classes: + # Nothing to do, the pool will generate a new connection later + pass + gevent.sleep(delay) + def _create_socket(self, client, login) -> Socket: """Creates and returns a socket instance""" socket = Socket(client, login) diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 023e5319e..2865bf5c5 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -12,6 +12,7 @@ from typing import List, Dict from django.contrib.sessions.middleware import SessionMiddleware from django.conf import settings from django.contrib.auth import get_user_model, login +from django.utils.timezone import make_aware from registrar.models import ( Contact, @@ -643,7 +644,7 @@ class MockEppLib(TestCase): self, id, email, - cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), pw="thisisnotapassword", ): fake = info.InfoContactResultData( @@ -681,7 +682,7 @@ class MockEppLib(TestCase): mockDataInfoDomain = fakedEppObject( "fakePw", - cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)], hosts=["fake.host.com"], statuses=[ @@ -692,7 +693,7 @@ class MockEppLib(TestCase): ) mockDataExtensionDomain = fakedEppObject( "fakePw", - cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)], hosts=["fake.host.com"], statuses=[ @@ -706,7 +707,7 @@ class MockEppLib(TestCase): ) InfoDomainWithContacts = fakedEppObject( "fakepw", - cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), contacts=[ common.DomainContact( contact="securityContact", @@ -731,7 +732,7 @@ class MockEppLib(TestCase): InfoDomainWithDefaultSecurityContact = fakedEppObject( "fakepw", - cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), contacts=[ common.DomainContact( contact="defaultSec", @@ -750,7 +751,7 @@ class MockEppLib(TestCase): ) InfoDomainWithVerisignSecurityContact = fakedEppObject( "fakepw", - cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), contacts=[ common.DomainContact( contact="defaultVeri", @@ -766,7 +767,7 @@ class MockEppLib(TestCase): InfoDomainWithDefaultTechnicalContact = fakedEppObject( "fakepw", - cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), contacts=[ common.DomainContact( contact="defaultTech", @@ -791,14 +792,14 @@ class MockEppLib(TestCase): infoDomainNoContact = fakedEppObject( "security", - cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), contacts=[], hosts=["fake.host.com"], ) infoDomainThreeHosts = fakedEppObject( "my-nameserver.gov", - cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), contacts=[], hosts=[ "ns1.my-nameserver-1.com", @@ -809,25 +810,25 @@ class MockEppLib(TestCase): infoDomainNoHost = fakedEppObject( "my-nameserver.gov", - cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), contacts=[], hosts=[], ) infoDomainTwoHosts = fakedEppObject( "my-nameserver.gov", - cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), contacts=[], hosts=["ns1.my-nameserver-1.com", "ns1.my-nameserver-2.com"], ) mockDataInfoHosts = fakedEppObject( "lastPw", - cr_date=datetime.datetime(2023, 8, 25, 19, 45, 35), + cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)), addrs=[common.Ip(addr="1.2.3.4"), common.Ip(addr="2.3.4.5")], ) - mockDataHostChange = fakedEppObject("lastPw", cr_date=datetime.datetime(2023, 8, 25, 19, 45, 35)) + mockDataHostChange = fakedEppObject("lastPw", cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35))) addDsData1 = { "keyTag": 1234, "alg": 3, @@ -859,7 +860,7 @@ class MockEppLib(TestCase): infoDomainHasIP = fakedEppObject( "nameserverwithip.gov", - cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), contacts=[ common.DomainContact( contact="securityContact", @@ -884,7 +885,7 @@ class MockEppLib(TestCase): justNameserver = fakedEppObject( "justnameserver.com", - cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), contacts=[ common.DomainContact( contact="securityContact", @@ -907,7 +908,7 @@ class MockEppLib(TestCase): infoDomainCheckHostIPCombo = fakedEppObject( "nameserversubdomain.gov", - cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), contacts=[], hosts=[ "ns1.nameserversubdomain.gov", diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index ebf3dfed9..6344157d8 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -59,22 +59,22 @@ class TestDomainAdmin(MockEppLib): """ Make sure the short name is displaying in admin on the list page """ - self.client.force_login(self.superuser) - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - mock_client = MockSESClient() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): + with less_console_noise(): + self.client.force_login(self.superuser) + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): application.approve() - response = self.client.get("/admin/registrar/domain/") + response = self.client.get("/admin/registrar/domain/") - # There are 3 template references to Federal (3) plus one reference in the table - # for our actual application - self.assertContains(response, "Federal", count=4) - # This may be a bit more robust - self.assertContains(response, 'Federal', count=1) - # Now let's make sure the long description does not exist - self.assertNotContains(response, "Federal: an agency of the U.S. government") + # There are 3 template references to Federal (3) plus one reference in the table + # for our actual application + self.assertContains(response, "Federal", count=4) + # This may be a bit more robust + self.assertContains(response, 'Federal', count=1) + # Now let's make sure the long description does not exist + self.assertNotContains(response, "Federal: an agency of the U.S. government") @skip("Why did this test stop working, and is is a good test") def test_place_and_remove_hold(self): @@ -120,40 +120,37 @@ class TestDomainAdmin(MockEppLib): Then a user-friendly success message is returned for displaying on the web And `state` is et to `DELETED` """ - domain = create_ready_domain() - # Put in client hold - domain.place_client_hold() - p = "userpass" - self.client.login(username="staffuser", password=p) - - # Ensure everything is displaying correctly - response = self.client.get( - "/admin/registrar/domain/{}/change/".format(domain.pk), - follow=True, - ) - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - self.assertContains(response, "Remove from registry") - - # Test the info dialog - request = self.factory.post( - "/admin/registrar/domain/{}/change/".format(domain.pk), - {"_delete_domain": "Remove from registry", "name": domain.name}, - follow=True, - ) - request.user = self.client - - with patch("django.contrib.messages.add_message") as mock_add_message: - self.admin.do_delete_domain(request, domain) - mock_add_message.assert_called_once_with( - request, - messages.INFO, - "Domain city.gov has been deleted. Thanks!", - extra_tags="", - fail_silently=False, + with less_console_noise(): + domain = create_ready_domain() + # Put in client hold + domain.place_client_hold() + p = "userpass" + self.client.login(username="staffuser", password=p) + # Ensure everything is displaying correctly + response = self.client.get( + "/admin/registrar/domain/{}/change/".format(domain.pk), + follow=True, ) - - self.assertEqual(domain.state, Domain.State.DELETED) + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + self.assertContains(response, "Remove from registry") + # Test the info dialog + request = self.factory.post( + "/admin/registrar/domain/{}/change/".format(domain.pk), + {"_delete_domain": "Remove from registry", "name": domain.name}, + follow=True, + ) + request.user = self.client + with patch("django.contrib.messages.add_message") as mock_add_message: + self.admin.do_delete_domain(request, domain) + mock_add_message.assert_called_once_with( + request, + messages.INFO, + "Domain city.gov has been deleted. Thanks!", + extra_tags="", + fail_silently=False, + ) + self.assertEqual(domain.state, Domain.State.DELETED) def test_deletion_ready_fsm_failure(self): """ @@ -162,38 +159,36 @@ class TestDomainAdmin(MockEppLib): Then a user-friendly error message is returned for displaying on the web And `state` is not set to `DELETED` """ - domain = create_ready_domain() - p = "userpass" - self.client.login(username="staffuser", password=p) - - # Ensure everything is displaying correctly - response = self.client.get( - "/admin/registrar/domain/{}/change/".format(domain.pk), - follow=True, - ) - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - self.assertContains(response, "Remove from registry") - - # Test the error - request = self.factory.post( - "/admin/registrar/domain/{}/change/".format(domain.pk), - {"_delete_domain": "Remove from registry", "name": domain.name}, - follow=True, - ) - request.user = self.client - - with patch("django.contrib.messages.add_message") as mock_add_message: - self.admin.do_delete_domain(request, domain) - mock_add_message.assert_called_once_with( - request, - messages.ERROR, - "Error deleting this Domain: " - "Can't switch from state 'ready' to 'deleted'" - ", must be either 'dns_needed' or 'on_hold'", - extra_tags="", - fail_silently=False, + with less_console_noise(): + domain = create_ready_domain() + p = "userpass" + self.client.login(username="staffuser", password=p) + # Ensure everything is displaying correctly + response = self.client.get( + "/admin/registrar/domain/{}/change/".format(domain.pk), + follow=True, ) + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + self.assertContains(response, "Remove from registry") + # Test the error + request = self.factory.post( + "/admin/registrar/domain/{}/change/".format(domain.pk), + {"_delete_domain": "Remove from registry", "name": domain.name}, + follow=True, + ) + request.user = self.client + with patch("django.contrib.messages.add_message") as mock_add_message: + self.admin.do_delete_domain(request, domain) + mock_add_message.assert_called_once_with( + request, + messages.ERROR, + "Error deleting this Domain: " + "Can't switch from state 'ready' to 'deleted'" + ", must be either 'dns_needed' or 'on_hold'", + extra_tags="", + fail_silently=False, + ) self.assertEqual(domain.state, Domain.State.READY) @@ -205,62 +200,57 @@ class TestDomainAdmin(MockEppLib): Then `commands.DeleteDomain` is sent to the registry And Domain returns normally without an error dialog """ - domain = create_ready_domain() - # Put in client hold - domain.place_client_hold() - p = "userpass" - self.client.login(username="staffuser", password=p) - - # Ensure everything is displaying correctly - response = self.client.get( - "/admin/registrar/domain/{}/change/".format(domain.pk), - follow=True, - ) - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - self.assertContains(response, "Remove from registry") - - # Test the info dialog - request = self.factory.post( - "/admin/registrar/domain/{}/change/".format(domain.pk), - {"_delete_domain": "Remove from registry", "name": domain.name}, - follow=True, - ) - request.user = self.client - - # Delete it once - with patch("django.contrib.messages.add_message") as mock_add_message: - self.admin.do_delete_domain(request, domain) - mock_add_message.assert_called_once_with( - request, - messages.INFO, - "Domain city.gov has been deleted. Thanks!", - extra_tags="", - fail_silently=False, + with less_console_noise(): + domain = create_ready_domain() + # Put in client hold + domain.place_client_hold() + p = "userpass" + self.client.login(username="staffuser", password=p) + # Ensure everything is displaying correctly + response = self.client.get( + "/admin/registrar/domain/{}/change/".format(domain.pk), + follow=True, ) - - self.assertEqual(domain.state, Domain.State.DELETED) - - # Try to delete it again - # Test the info dialog - request = self.factory.post( - "/admin/registrar/domain/{}/change/".format(domain.pk), - {"_delete_domain": "Remove from registry", "name": domain.name}, - follow=True, - ) - request.user = self.client - - with patch("django.contrib.messages.add_message") as mock_add_message: - self.admin.do_delete_domain(request, domain) - mock_add_message.assert_called_once_with( - request, - messages.INFO, - "This domain is already deleted", - extra_tags="", - fail_silently=False, + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + self.assertContains(response, "Remove from registry") + # Test the info dialog + request = self.factory.post( + "/admin/registrar/domain/{}/change/".format(domain.pk), + {"_delete_domain": "Remove from registry", "name": domain.name}, + follow=True, ) + request.user = self.client + # Delete it once + with patch("django.contrib.messages.add_message") as mock_add_message: + self.admin.do_delete_domain(request, domain) + mock_add_message.assert_called_once_with( + request, + messages.INFO, + "Domain city.gov has been deleted. Thanks!", + extra_tags="", + fail_silently=False, + ) - self.assertEqual(domain.state, Domain.State.DELETED) + self.assertEqual(domain.state, Domain.State.DELETED) + # Try to delete it again + # Test the info dialog + request = self.factory.post( + "/admin/registrar/domain/{}/change/".format(domain.pk), + {"_delete_domain": "Remove from registry", "name": domain.name}, + follow=True, + ) + request.user = self.client + with patch("django.contrib.messages.add_message") as mock_add_message: + self.admin.do_delete_domain(request, domain) + mock_add_message.assert_called_once_with( + request, + messages.INFO, + "This domain is already deleted", + extra_tags="", + fail_silently=False, + ) + self.assertEqual(domain.state, Domain.State.DELETED) @skip("Waiting on epp lib to implement") def test_place_and_remove_hold_epp(self): @@ -1281,64 +1271,62 @@ class ListHeaderAdminTest(TestCase): self.superuser = create_superuser() def test_changelist_view(self): - # Have to get creative to get past linter - p = "adminpass" - self.client.login(username="superuser", password=p) - - # Mock a user - user = mock_user() - - # Make the request using the Client class - # which handles CSRF - # Follow=True handles the redirect - response = self.client.get( - "/admin/registrar/domainapplication/", - { - "status__exact": "started", - "investigator__id__exact": user.id, - "q": "Hello", - }, - follow=True, - ) - - # Assert that the filters and search_query are added to the extra_context - self.assertIn("filters", response.context) - self.assertIn("search_query", response.context) - # Assert the content of filters and search_query - filters = response.context["filters"] - search_query = response.context["search_query"] - self.assertEqual(search_query, "Hello") - self.assertEqual( - filters, - [ - {"parameter_name": "status", "parameter_value": "started"}, + with less_console_noise(): + # Have to get creative to get past linter + p = "adminpass" + self.client.login(username="superuser", password=p) + # Mock a user + user = mock_user() + # Make the request using the Client class + # which handles CSRF + # Follow=True handles the redirect + response = self.client.get( + "/admin/registrar/domainapplication/", { - "parameter_name": "investigator", - "parameter_value": user.first_name + " " + user.last_name, + "status__exact": "started", + "investigator__id__exact": user.id, + "q": "Hello", }, - ], - ) + follow=True, + ) + # Assert that the filters and search_query are added to the extra_context + self.assertIn("filters", response.context) + self.assertIn("search_query", response.context) + # Assert the content of filters and search_query + filters = response.context["filters"] + search_query = response.context["search_query"] + self.assertEqual(search_query, "Hello") + self.assertEqual( + filters, + [ + {"parameter_name": "status", "parameter_value": "started"}, + { + "parameter_name": "investigator", + "parameter_value": user.first_name + " " + user.last_name, + }, + ], + ) 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": "Jeff Lebowski", - "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": "Jeff Lebowski"}, - ], - ) + with less_console_noise(): + # Create a mock request object + request = self.factory.get("/admin/yourmodel/") + # Set the GET parameters for testing + request.GET = { + "status": "started", + "investigator": "Jeff Lebowski", + "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": "Jeff Lebowski"}, + ], + ) def tearDown(self): # delete any applications too @@ -1777,42 +1765,38 @@ class ContactAdminTest(TestCase): def test_change_view_for_joined_contact_five_or_more(self): """Create a contact, join it to 5 domain requests. The 6th join will be a user. Assert that the warning on the contact form lists 5 joins and a '1 more' ellispsis.""" - - self.client.force_login(self.superuser) - - # Create an instance of the model - # join it to 5 domain requests. The 6th join will be a user. - contact, _ = Contact.objects.get_or_create(user=self.staffuser) - application1 = completed_application(submitter=contact, name="city1.gov") - application2 = completed_application(submitter=contact, name="city2.gov") - application3 = completed_application(submitter=contact, name="city3.gov") - application4 = completed_application(submitter=contact, name="city4.gov") - application5 = completed_application(submitter=contact, name="city5.gov") - - with patch("django.contrib.messages.warning") as mock_warning: - # Use the test client to simulate the request - response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk])) - - logger.info(mock_warning) - - # Assert that the error message was called with the correct argument - # Note: The 6th join will be a user. - mock_warning.assert_called_once_with( - response.wsgi_request, - "" - "

And 1 more...

", - ) + with less_console_noise(): + self.client.force_login(self.superuser) + # Create an instance of the model + # join it to 5 domain requests. The 6th join will be a user. + contact, _ = Contact.objects.get_or_create(user=self.staffuser) + application1 = completed_application(submitter=contact, name="city1.gov") + application2 = completed_application(submitter=contact, name="city2.gov") + application3 = completed_application(submitter=contact, name="city3.gov") + application4 = completed_application(submitter=contact, name="city4.gov") + application5 = completed_application(submitter=contact, name="city5.gov") + with patch("django.contrib.messages.warning") as mock_warning: + # Use the test client to simulate the request + response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk])) + logger.debug(mock_warning) + # Assert that the error message was called with the correct argument + # Note: The 6th join will be a user. + mock_warning.assert_called_once_with( + response.wsgi_request, + "" + "

And 1 more...

", + ) def tearDown(self): DomainApplication.objects.all().delete() diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index 06886ba66..40cdce6d2 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -1,5 +1,6 @@ import copy -import datetime +from datetime import date, datetime, time +from django.utils import timezone from django.test import TestCase @@ -17,7 +18,7 @@ from django.core.management import call_command from unittest.mock import patch, call from epplibwrapper import commands, common -from .common import MockEppLib +from .common import MockEppLib, less_console_noise class TestPopulateFirstReady(TestCase): @@ -33,7 +34,9 @@ class TestPopulateFirstReady(TestCase): self.unknown_domain, _ = Domain.objects.get_or_create(name="fakeunknown.gov", state=Domain.State.UNKNOWN) # Set a ready_at date for testing purposes - self.ready_at_date = datetime.date(2022, 12, 31) + self.ready_at_date = date(2022, 12, 31) + _ready_at_datetime = datetime.combine(self.ready_at_date, time.min) + self.ready_at_date_tz_aware = timezone.make_aware(_ready_at_datetime, timezone=timezone.utc) def tearDown(self): """Deletes all DB objects related to migrations""" @@ -49,122 +52,103 @@ class TestPopulateFirstReady(TestCase): The 'call_command' function from Django's management framework is then used to execute the populate_first_ready command with the specified arguments. """ - with patch( - "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa - return_value=True, - ): - call_command("populate_first_ready") + with less_console_noise(): + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + call_command("populate_first_ready") def test_populate_first_ready_state_ready(self): """ Tests that the populate_first_ready works as expected for the state 'ready' """ - # Set the created at date - self.ready_domain.created_at = self.ready_at_date - self.ready_domain.save() - - desired_domain = copy.deepcopy(self.ready_domain) - - desired_domain.first_ready = self.ready_at_date - - # Run the expiration date script - self.run_populate_first_ready() - - self.assertEqual(desired_domain, self.ready_domain) - - # Explicitly test the first_ready date - first_ready = Domain.objects.filter(name="fakeready.gov").get().first_ready - self.assertEqual(first_ready, self.ready_at_date) + with less_console_noise(): + # Set the created at date + self.ready_domain.created_at = self.ready_at_date_tz_aware + self.ready_domain.save() + desired_domain = copy.deepcopy(self.ready_domain) + desired_domain.first_ready = self.ready_at_date + # Run the expiration date script + self.run_populate_first_ready() + self.assertEqual(desired_domain, self.ready_domain) + # Explicitly test the first_ready date + first_ready = Domain.objects.filter(name="fakeready.gov").get().first_ready + self.assertEqual(first_ready, self.ready_at_date) def test_populate_first_ready_state_deleted(self): """ Tests that the populate_first_ready works as expected for the state 'deleted' """ - # Set the created at date - self.deleted_domain.created_at = self.ready_at_date - self.deleted_domain.save() - - desired_domain = copy.deepcopy(self.deleted_domain) - - desired_domain.first_ready = self.ready_at_date - - # Run the expiration date script - self.run_populate_first_ready() - - self.assertEqual(desired_domain, self.deleted_domain) - - # Explicitly test the first_ready date - first_ready = Domain.objects.filter(name="fakedeleted.gov").get().first_ready - self.assertEqual(first_ready, self.ready_at_date) + with less_console_noise(): + # Set the created at date + self.deleted_domain.created_at = self.ready_at_date_tz_aware + self.deleted_domain.save() + desired_domain = copy.deepcopy(self.deleted_domain) + desired_domain.first_ready = self.ready_at_date + # Run the expiration date script + self.run_populate_first_ready() + self.assertEqual(desired_domain, self.deleted_domain) + # Explicitly test the first_ready date + first_ready = Domain.objects.filter(name="fakedeleted.gov").get().first_ready + self.assertEqual(first_ready, self.ready_at_date) def test_populate_first_ready_state_dns_needed(self): """ Tests that the populate_first_ready doesn't make changes when a domain's state is 'dns_needed' """ - # Set the created at date - self.dns_needed_domain.created_at = self.ready_at_date - self.dns_needed_domain.save() - - desired_domain = copy.deepcopy(self.dns_needed_domain) - - desired_domain.first_ready = None - - # Run the expiration date script - self.run_populate_first_ready() - - current_domain = self.dns_needed_domain - # The object should largely be unaltered (does not test first_ready) - self.assertEqual(desired_domain, current_domain) - - first_ready = Domain.objects.filter(name="fakedns.gov").get().first_ready - - # Explicitly test the first_ready date - self.assertNotEqual(first_ready, self.ready_at_date) - self.assertEqual(first_ready, None) + with less_console_noise(): + # Set the created at date + self.dns_needed_domain.created_at = self.ready_at_date_tz_aware + self.dns_needed_domain.save() + desired_domain = copy.deepcopy(self.dns_needed_domain) + desired_domain.first_ready = None + # Run the expiration date script + self.run_populate_first_ready() + current_domain = self.dns_needed_domain + # The object should largely be unaltered (does not test first_ready) + self.assertEqual(desired_domain, current_domain) + first_ready = Domain.objects.filter(name="fakedns.gov").get().first_ready + # Explicitly test the first_ready date + self.assertNotEqual(first_ready, self.ready_at_date) + self.assertEqual(first_ready, None) def test_populate_first_ready_state_on_hold(self): """ Tests that the populate_first_ready works as expected for the state 'on_hold' """ - self.hold_domain.created_at = self.ready_at_date - self.hold_domain.save() - - desired_domain = copy.deepcopy(self.hold_domain) - desired_domain.first_ready = self.ready_at_date - - # Run the update first ready_at script - self.run_populate_first_ready() - - current_domain = self.hold_domain - self.assertEqual(desired_domain, current_domain) - - # Explicitly test the first_ready date - first_ready = Domain.objects.filter(name="fakehold.gov").get().first_ready - self.assertEqual(first_ready, self.ready_at_date) + with less_console_noise(): + self.hold_domain.created_at = self.ready_at_date_tz_aware + self.hold_domain.save() + desired_domain = copy.deepcopy(self.hold_domain) + desired_domain.first_ready = self.ready_at_date + # Run the update first ready_at script + self.run_populate_first_ready() + current_domain = self.hold_domain + self.assertEqual(desired_domain, current_domain) + # Explicitly test the first_ready date + first_ready = Domain.objects.filter(name="fakehold.gov").get().first_ready + self.assertEqual(first_ready, self.ready_at_date) def test_populate_first_ready_state_unknown(self): """ Tests that the populate_first_ready works as expected for the state 'unknown' """ - # Set the created at date - self.unknown_domain.created_at = self.ready_at_date - self.unknown_domain.save() - - desired_domain = copy.deepcopy(self.unknown_domain) - desired_domain.first_ready = None - - # Run the expiration date script - self.run_populate_first_ready() - - current_domain = self.unknown_domain - - # The object should largely be unaltered (does not test first_ready) - self.assertEqual(desired_domain, current_domain) - - # Explicitly test the first_ready date - first_ready = Domain.objects.filter(name="fakeunknown.gov").get().first_ready - self.assertNotEqual(first_ready, self.ready_at_date) - self.assertEqual(first_ready, None) + with less_console_noise(): + # Set the created at date + self.unknown_domain.created_at = self.ready_at_date_tz_aware + self.unknown_domain.save() + desired_domain = copy.deepcopy(self.unknown_domain) + desired_domain.first_ready = None + # Run the expiration date script + self.run_populate_first_ready() + current_domain = self.unknown_domain + # The object should largely be unaltered (does not test first_ready) + self.assertEqual(desired_domain, current_domain) + # Explicitly test the first_ready date + first_ready = Domain.objects.filter(name="fakeunknown.gov").get().first_ready + self.assertNotEqual(first_ready, self.ready_at_date) + self.assertEqual(first_ready, None) class TestPatchAgencyInfo(TestCase): @@ -185,7 +169,8 @@ class TestPatchAgencyInfo(TestCase): @patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", return_value=True) def call_patch_federal_agency_info(self, mock_prompt): """Calls the patch_federal_agency_info command and mimics a keypress""" - call_command("patch_federal_agency_info", "registrar/tests/data/fake_current_full.csv", debug=True) + with less_console_noise(): + call_command("patch_federal_agency_info", "registrar/tests/data/fake_current_full.csv", debug=True) def test_patch_agency_info(self): """ @@ -194,17 +179,14 @@ class TestPatchAgencyInfo(TestCase): of a `DomainInformation` object when the corresponding `TransitionDomain` object has a valid `federal_agency`. """ - - # Ensure that the federal_agency is None - self.assertEqual(self.domain_info.federal_agency, None) - - self.call_patch_federal_agency_info() - - # Reload the domain_info object from the database - self.domain_info.refresh_from_db() - - # Check that the federal_agency field was updated - self.assertEqual(self.domain_info.federal_agency, "test agency") + with less_console_noise(): + # Ensure that the federal_agency is None + self.assertEqual(self.domain_info.federal_agency, None) + self.call_patch_federal_agency_info() + # Reload the domain_info object from the database + self.domain_info.refresh_from_db() + # Check that the federal_agency field was updated + self.assertEqual(self.domain_info.federal_agency, "test agency") def test_patch_agency_info_skip(self): """ @@ -213,21 +195,18 @@ class TestPatchAgencyInfo(TestCase): of a `DomainInformation` object when the corresponding `TransitionDomain` object does not exist. """ - # Set federal_agency to None to simulate a skip - self.transition_domain.federal_agency = None - self.transition_domain.save() - - with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context: - self.call_patch_federal_agency_info() - - # Check that the correct log message was output - self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0]) - - # Reload the domain_info object from the database - self.domain_info.refresh_from_db() - - # Check that the federal_agency field was not updated - self.assertIsNone(self.domain_info.federal_agency) + with less_console_noise(): + # Set federal_agency to None to simulate a skip + self.transition_domain.federal_agency = None + self.transition_domain.save() + with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context: + self.call_patch_federal_agency_info() + # Check that the correct log message was output + self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0]) + # Reload the domain_info object from the database + self.domain_info.refresh_from_db() + # Check that the federal_agency field was not updated + self.assertIsNone(self.domain_info.federal_agency) def test_patch_agency_info_skip_updates_data(self): """ @@ -235,25 +214,21 @@ class TestPatchAgencyInfo(TestCase): updates the DomainInformation object, because a record exists in the provided current-full.csv file. """ - # Set federal_agency to None to simulate a skip - self.transition_domain.federal_agency = None - self.transition_domain.save() - - # Change the domain name to something parsable in the .csv - self.domain.name = "cdomain1.gov" - self.domain.save() - - with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context: - self.call_patch_federal_agency_info() - - # Check that the correct log message was output - self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0]) - - # Reload the domain_info object from the database - self.domain_info.refresh_from_db() - - # Check that the federal_agency field was not updated - self.assertEqual(self.domain_info.federal_agency, "World War I Centennial Commission") + with less_console_noise(): + # Set federal_agency to None to simulate a skip + self.transition_domain.federal_agency = None + self.transition_domain.save() + # Change the domain name to something parsable in the .csv + self.domain.name = "cdomain1.gov" + self.domain.save() + with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context: + self.call_patch_federal_agency_info() + # Check that the correct log message was output + self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0]) + # Reload the domain_info object from the database + self.domain_info.refresh_from_db() + # Check that the federal_agency field was not updated + self.assertEqual(self.domain_info.federal_agency, "World War I Centennial Commission") def test_patch_agency_info_skips_valid_domains(self): """ @@ -261,20 +236,17 @@ class TestPatchAgencyInfo(TestCase): does not update the `federal_agency` field of a `DomainInformation` object """ - self.domain_info.federal_agency = "unchanged" - self.domain_info.save() - - with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="INFO") as context: - self.call_patch_federal_agency_info() - - # Check that the correct log message was output - self.assertIn("FINISHED", context.output[1]) - - # Reload the domain_info object from the database - self.domain_info.refresh_from_db() - - # Check that the federal_agency field was not updated - self.assertEqual(self.domain_info.federal_agency, "unchanged") + with less_console_noise(): + self.domain_info.federal_agency = "unchanged" + self.domain_info.save() + with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="INFO") as context: + self.call_patch_federal_agency_info() + # Check that the correct log message was output + self.assertIn("FINISHED", context.output[1]) + # Reload the domain_info object from the database + self.domain_info.refresh_from_db() + # Check that the federal_agency field was not updated + self.assertEqual(self.domain_info.federal_agency, "unchanged") class TestExtendExpirationDates(MockEppLib): @@ -283,39 +255,39 @@ class TestExtendExpirationDates(MockEppLib): super().setUp() # Create a valid domain that is updatable Domain.objects.get_or_create( - name="waterbutpurple.gov", state=Domain.State.READY, expiration_date=datetime.date(2023, 11, 15) + name="waterbutpurple.gov", state=Domain.State.READY, expiration_date=date(2023, 11, 15) ) TransitionDomain.objects.get_or_create( username="testytester@mail.com", domain_name="waterbutpurple.gov", - epp_expiration_date=datetime.date(2023, 11, 15), + epp_expiration_date=date(2023, 11, 15), ) # Create a domain with an invalid expiration date Domain.objects.get_or_create( - name="fake.gov", state=Domain.State.READY, expiration_date=datetime.date(2022, 5, 25) + name="fake.gov", state=Domain.State.READY, expiration_date=date(2022, 5, 25) ) TransitionDomain.objects.get_or_create( username="themoonisactuallycheese@mail.com", domain_name="fake.gov", - epp_expiration_date=datetime.date(2022, 5, 25), + epp_expiration_date=date(2022, 5, 25), ) # Create a domain with an invalid state Domain.objects.get_or_create( - name="fakeneeded.gov", state=Domain.State.DNS_NEEDED, expiration_date=datetime.date(2023, 11, 15) + name="fakeneeded.gov", state=Domain.State.DNS_NEEDED, expiration_date=date(2023, 11, 15) ) TransitionDomain.objects.get_or_create( username="fakeneeded@mail.com", domain_name="fakeneeded.gov", - epp_expiration_date=datetime.date(2023, 11, 15), + epp_expiration_date=date(2023, 11, 15), ) # Create a domain with a date greater than the maximum Domain.objects.get_or_create( - name="fakemaximum.gov", state=Domain.State.READY, expiration_date=datetime.date(2024, 12, 31) + name="fakemaximum.gov", state=Domain.State.READY, expiration_date=date(2024, 12, 31) ) TransitionDomain.objects.get_or_create( username="fakemaximum@mail.com", domain_name="fakemaximum.gov", - epp_expiration_date=datetime.date(2024, 12, 31), + epp_expiration_date=date(2024, 12, 31), ) def tearDown(self): @@ -338,83 +310,82 @@ class TestExtendExpirationDates(MockEppLib): The 'call_command' function from Django's management framework is then used to execute the extend_expiration_dates command with the specified arguments. """ - with patch( - "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa - return_value=True, - ): - call_command("extend_expiration_dates") + with less_console_noise(): + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + call_command("extend_expiration_dates") def test_extends_expiration_date_correctly(self): """ Tests that the extend_expiration_dates method extends dates as expected """ - desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get() - desired_domain.expiration_date = datetime.date(2024, 11, 15) - - # Run the expiration date script - self.run_extend_expiration_dates() - - current_domain = Domain.objects.filter(name="waterbutpurple.gov").get() - - self.assertEqual(desired_domain, current_domain) - # Explicitly test the expiration date - self.assertEqual(current_domain.expiration_date, datetime.date(2024, 11, 15)) + with less_console_noise(): + desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get() + desired_domain.expiration_date = date(2024, 11, 15) + # Run the expiration date script + self.run_extend_expiration_dates() + current_domain = Domain.objects.filter(name="waterbutpurple.gov").get() + self.assertEqual(desired_domain, current_domain) + # Explicitly test the expiration date + self.assertEqual(current_domain.expiration_date, date(2024, 11, 15)) def test_extends_expiration_date_skips_non_current(self): """ Tests that the extend_expiration_dates method correctly skips domains with an expiration date less than a certain threshold. """ - desired_domain = Domain.objects.filter(name="fake.gov").get() - desired_domain.expiration_date = datetime.date(2022, 5, 25) - - # Run the expiration date script - self.run_extend_expiration_dates() - - current_domain = Domain.objects.filter(name="fake.gov").get() - self.assertEqual(desired_domain, current_domain) - - # Explicitly test the expiration date. The extend_expiration_dates script - # will skip all dates less than date(2023, 11, 15), meaning that this domain - # should not be affected by the change. - self.assertEqual(current_domain.expiration_date, datetime.date(2022, 5, 25)) + with less_console_noise(): + desired_domain = Domain.objects.filter(name="fake.gov").get() + desired_domain.expiration_date = date(2022, 5, 25) + # Run the expiration date script + self.run_extend_expiration_dates() + current_domain = Domain.objects.filter(name="fake.gov").get() + self.assertEqual(desired_domain, current_domain) + # Explicitly test the expiration date. The extend_expiration_dates script + # will skip all dates less than date(2023, 11, 15), meaning that this domain + # should not be affected by the change. + self.assertEqual(current_domain.expiration_date, date(2022, 5, 25)) def test_extends_expiration_date_skips_maximum_date(self): """ Tests that the extend_expiration_dates method correctly skips domains with an expiration date more than a certain threshold. """ - desired_domain = Domain.objects.filter(name="fakemaximum.gov").get() - desired_domain.expiration_date = datetime.date(2024, 12, 31) + with less_console_noise(): + desired_domain = Domain.objects.filter(name="fakemaximum.gov").get() + desired_domain.expiration_date = date(2024, 12, 31) - # Run the expiration date script - self.run_extend_expiration_dates() + # Run the expiration date script + self.run_extend_expiration_dates() - current_domain = Domain.objects.filter(name="fakemaximum.gov").get() - self.assertEqual(desired_domain, current_domain) + current_domain = Domain.objects.filter(name="fakemaximum.gov").get() + self.assertEqual(desired_domain, current_domain) - # Explicitly test the expiration date. The extend_expiration_dates script - # will skip all dates less than date(2023, 11, 15), meaning that this domain - # should not be affected by the change. - self.assertEqual(current_domain.expiration_date, datetime.date(2024, 12, 31)) + # Explicitly test the expiration date. The extend_expiration_dates script + # will skip all dates less than date(2023, 11, 15), meaning that this domain + # should not be affected by the change. + self.assertEqual(current_domain.expiration_date, date(2024, 12, 31)) def test_extends_expiration_date_skips_non_ready(self): """ Tests that the extend_expiration_dates method correctly skips domains not in the state "ready" """ - desired_domain = Domain.objects.filter(name="fakeneeded.gov").get() - desired_domain.expiration_date = datetime.date(2023, 11, 15) + with less_console_noise(): + desired_domain = Domain.objects.filter(name="fakeneeded.gov").get() + desired_domain.expiration_date = date(2023, 11, 15) - # Run the expiration date script - self.run_extend_expiration_dates() + # Run the expiration date script + self.run_extend_expiration_dates() - current_domain = Domain.objects.filter(name="fakeneeded.gov").get() - self.assertEqual(desired_domain, current_domain) + current_domain = Domain.objects.filter(name="fakeneeded.gov").get() + self.assertEqual(desired_domain, current_domain) - # Explicitly test the expiration date. The extend_expiration_dates script - # will skip all dates less than date(2023, 11, 15), meaning that this domain - # should not be affected by the change. - self.assertEqual(current_domain.expiration_date, datetime.date(2023, 11, 15)) + # Explicitly test the expiration date. The extend_expiration_dates script + # will skip all dates less than date(2023, 11, 15), meaning that this domain + # should not be affected by the change. + self.assertEqual(current_domain.expiration_date, date(2023, 11, 15)) def test_extends_expiration_date_idempotent(self): """ @@ -423,26 +394,21 @@ class TestExtendExpirationDates(MockEppLib): Verifies that running the method multiple times does not change the expiration date of a domain beyond the initial extension. """ - desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get() - desired_domain.expiration_date = datetime.date(2024, 11, 15) - - # Run the expiration date script - self.run_extend_expiration_dates() - - current_domain = Domain.objects.filter(name="waterbutpurple.gov").get() - self.assertEqual(desired_domain, current_domain) - - # Explicitly test the expiration date - self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15)) - - # Run the expiration date script again - self.run_extend_expiration_dates() - - # The old domain shouldn't have changed - self.assertEqual(desired_domain, current_domain) - - # Explicitly test the expiration date - should be the same - self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15)) + with less_console_noise(): + desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get() + desired_domain.expiration_date = date(2024, 11, 15) + # Run the expiration date script + self.run_extend_expiration_dates() + current_domain = Domain.objects.filter(name="waterbutpurple.gov").get() + self.assertEqual(desired_domain, current_domain) + # Explicitly test the expiration date + self.assertEqual(desired_domain.expiration_date, date(2024, 11, 15)) + # Run the expiration date script again + self.run_extend_expiration_dates() + # The old domain shouldn't have changed + self.assertEqual(desired_domain, current_domain) + # Explicitly test the expiration date - should be the same + self.assertEqual(desired_domain.expiration_date, date(2024, 11, 15)) class TestDiscloseEmails(MockEppLib): @@ -461,39 +427,41 @@ class TestDiscloseEmails(MockEppLib): The 'call_command' function from Django's management framework is then used to execute the disclose_security_emails command. """ - with patch( - "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa - return_value=True, - ): - call_command("disclose_security_emails") + with less_console_noise(): + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + call_command("disclose_security_emails") def test_disclose_security_emails(self): """ Tests that command disclose_security_emails runs successfully with appropriate EPP calll to UpdateContact. """ - domain, _ = Domain.objects.get_or_create(name="testdisclose.gov", state=Domain.State.READY) - expectedSecContact = PublicContact.get_default_security() - expectedSecContact.domain = domain - expectedSecContact.email = "123@mail.gov" - # set domain security email to 123@mail.gov instead of default email - domain.security_contact = expectedSecContact - self.run_disclose_security_emails() + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="testdisclose.gov", state=Domain.State.READY) + expectedSecContact = PublicContact.get_default_security() + expectedSecContact.domain = domain + expectedSecContact.email = "123@mail.gov" + # set domain security email to 123@mail.gov instead of default email + domain.security_contact = expectedSecContact + self.run_disclose_security_emails() - # running disclose_security_emails sends EPP call UpdateContact with disclose - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.UpdateContact( - id=domain.security_contact.registry_id, - postal_info=domain._make_epp_contact_postal_info(contact=domain.security_contact), - email=domain.security_contact.email, - voice=domain.security_contact.voice, - fax=domain.security_contact.fax, - auth_info=common.ContactAuthInfo(pw="2fooBAR123fooBaz"), - disclose=domain._disclose_fields(contact=domain.security_contact), - ), - cleaned=True, - ) - ] - ) + # running disclose_security_emails sends EPP call UpdateContact with disclose + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.UpdateContact( + id=domain.security_contact.registry_id, + postal_info=domain._make_epp_contact_postal_info(contact=domain.security_contact), + email=domain.security_contact.email, + voice=domain.security_contact.voice, + fax=domain.security_contact.fax, + auth_info=common.ContactAuthInfo(pw="2fooBAR123fooBaz"), + disclose=domain._disclose_fields(contact=domain.security_contact), + ), + cleaned=True, + ) + ] + ) diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index ef6522747..d2210394b 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -60,127 +60,134 @@ class TestDomainApplication(TestCase): def assertNotRaises(self, exception_type): """Helper method for testing allowed transitions.""" - return self.assertRaises(Exception, None, exception_type) + with less_console_noise(): + return self.assertRaises(Exception, None, exception_type) def test_empty_create_fails(self): """Can't create a completely empty domain application. NOTE: something about theexception this test raises messes up with the atomic block in a custom tearDown method for the parent test class.""" - with self.assertRaisesRegex(IntegrityError, "creator"): - DomainApplication.objects.create() + with less_console_noise(): + with self.assertRaisesRegex(IntegrityError, "creator"): + DomainApplication.objects.create() def test_minimal_create(self): """Can create with just a creator.""" - user, _ = User.objects.get_or_create(username="testy") - application = DomainApplication.objects.create(creator=user) - self.assertEqual(application.status, DomainApplication.ApplicationStatus.STARTED) + with less_console_noise(): + user, _ = User.objects.get_or_create(username="testy") + application = DomainApplication.objects.create(creator=user) + self.assertEqual(application.status, DomainApplication.ApplicationStatus.STARTED) def test_full_create(self): """Can create with all fields.""" - user, _ = User.objects.get_or_create(username="testy") - contact = Contact.objects.create() - com_website, _ = Website.objects.get_or_create(website="igorville.com") - gov_website, _ = Website.objects.get_or_create(website="igorville.gov") - domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") - application = DomainApplication.objects.create( - creator=user, - investigator=user, - organization_type=DomainApplication.OrganizationChoices.FEDERAL, - federal_type=DomainApplication.BranchChoices.EXECUTIVE, - is_election_board=False, - organization_name="Test", - address_line1="100 Main St.", - address_line2="APT 1A", - state_territory="CA", - zipcode="12345-6789", - authorizing_official=contact, - requested_domain=domain, - submitter=contact, - purpose="Igorville rules!", - anything_else="All of Igorville loves the dotgov program.", - is_policy_acknowledged=True, - ) - application.current_websites.add(com_website) - application.alternative_domains.add(gov_website) - application.other_contacts.add(contact) - application.save() + with less_console_noise(): + user, _ = User.objects.get_or_create(username="testy") + contact = Contact.objects.create() + com_website, _ = Website.objects.get_or_create(website="igorville.com") + gov_website, _ = Website.objects.get_or_create(website="igorville.gov") + domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") + application = DomainApplication.objects.create( + creator=user, + investigator=user, + organization_type=DomainApplication.OrganizationChoices.FEDERAL, + federal_type=DomainApplication.BranchChoices.EXECUTIVE, + is_election_board=False, + organization_name="Test", + address_line1="100 Main St.", + address_line2="APT 1A", + state_territory="CA", + zipcode="12345-6789", + authorizing_official=contact, + requested_domain=domain, + submitter=contact, + purpose="Igorville rules!", + anything_else="All of Igorville loves the dotgov program.", + is_policy_acknowledged=True, + ) + application.current_websites.add(com_website) + application.alternative_domains.add(gov_website) + application.other_contacts.add(contact) + application.save() def test_domain_info(self): """Can create domain info with all fields.""" - user, _ = User.objects.get_or_create(username="testy") - contact = Contact.objects.create() - domain, _ = Domain.objects.get_or_create(name="igorville.gov") - information = DomainInformation.objects.create( - creator=user, - organization_type=DomainInformation.OrganizationChoices.FEDERAL, - federal_type=DomainInformation.BranchChoices.EXECUTIVE, - is_election_board=False, - organization_name="Test", - address_line1="100 Main St.", - address_line2="APT 1A", - state_territory="CA", - zipcode="12345-6789", - authorizing_official=contact, - submitter=contact, - purpose="Igorville rules!", - anything_else="All of Igorville loves the dotgov program.", - is_policy_acknowledged=True, - domain=domain, - ) - information.other_contacts.add(contact) - information.save() - self.assertEqual(information.domain.id, domain.id) - self.assertEqual(information.id, domain.domain_info.id) + with less_console_noise(): + user, _ = User.objects.get_or_create(username="testy") + contact = Contact.objects.create() + domain, _ = Domain.objects.get_or_create(name="igorville.gov") + information = DomainInformation.objects.create( + creator=user, + organization_type=DomainInformation.OrganizationChoices.FEDERAL, + federal_type=DomainInformation.BranchChoices.EXECUTIVE, + is_election_board=False, + organization_name="Test", + address_line1="100 Main St.", + address_line2="APT 1A", + state_territory="CA", + zipcode="12345-6789", + authorizing_official=contact, + submitter=contact, + purpose="Igorville rules!", + anything_else="All of Igorville loves the dotgov program.", + is_policy_acknowledged=True, + domain=domain, + ) + information.other_contacts.add(contact) + information.save() + self.assertEqual(information.domain.id, domain.id) + self.assertEqual(information.id, domain.domain_info.id) def test_status_fsm_submit_fail(self): - user, _ = User.objects.get_or_create(username="testy") - application = DomainApplication.objects.create(creator=user) + with less_console_noise(): + user, _ = User.objects.get_or_create(username="testy") + application = DomainApplication.objects.create(creator=user) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - with self.assertRaises(ValueError): - # can't submit an application with a null domain name - application.submit() + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + with self.assertRaises(ValueError): + # can't submit an application with a null domain name + application.submit() def test_status_fsm_submit_succeed(self): - user, _ = User.objects.get_or_create(username="testy") - site = DraftDomain.objects.create(name="igorville.gov") - application = DomainApplication.objects.create(creator=user, requested_domain=site) + with less_console_noise(): + user, _ = User.objects.get_or_create(username="testy") + site = DraftDomain.objects.create(name="igorville.gov") + application = DomainApplication.objects.create(creator=user, requested_domain=site) - # no submitter email so this emits a log warning + # no submitter email so this emits a log warning - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.submit() - self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.submit() + self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED) def test_submit_sends_email(self): """Create an application and submit it and see if email was sent.""" - user, _ = User.objects.get_or_create(username="testy") - contact = Contact.objects.create(email="test@test.gov") - domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") - application = DomainApplication.objects.create( - creator=user, - requested_domain=domain, - submitter=contact, - ) - application.save() + with less_console_noise(): + user, _ = User.objects.get_or_create(username="testy") + contact = Contact.objects.create(email="test@test.gov") + domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") + application = DomainApplication.objects.create( + creator=user, + requested_domain=domain, + submitter=contact, + ) + application.save() - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): application.submit() - # check to see if an email was sent - self.assertGreater( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "test@test.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) + # check to see if an email was sent + self.assertGreater( + len( + [ + email + for email in MockSESClient.EMAILS_SENT + if "test@test.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + ), + 0, + ) def test_submit_transition_allowed(self): """ @@ -268,13 +275,13 @@ class TestDomainApplication(TestCase): (self.rejected_application, TransitionNotAllowed), (self.ineligible_application, TransitionNotAllowed), ] - - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - try: - application.action_needed() - except TransitionNotAllowed: - self.fail("TransitionNotAllowed was raised, but it was not expected.") + with less_console_noise(): + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + try: + application.action_needed() + except TransitionNotAllowed: + self.fail("TransitionNotAllowed was raised, but it was not expected.") def test_action_needed_transition_not_allowed(self): """ @@ -286,11 +293,11 @@ class TestDomainApplication(TestCase): (self.action_needed_application, TransitionNotAllowed), (self.withdrawn_application, TransitionNotAllowed), ] - - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - with self.assertRaises(exception_type): - application.action_needed() + with less_console_noise(): + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + with self.assertRaises(exception_type): + application.action_needed() def test_approved_transition_allowed(self): """ @@ -499,25 +506,29 @@ class TestDomainApplication(TestCase): def test_has_rationale_returns_true(self): """has_rationale() returns true when an application has no_other_contacts_rationale""" - self.started_application.no_other_contacts_rationale = "You talkin' to me?" - self.started_application.save() - self.assertEquals(self.started_application.has_rationale(), True) + with less_console_noise(): + self.started_application.no_other_contacts_rationale = "You talkin' to me?" + self.started_application.save() + self.assertEquals(self.started_application.has_rationale(), True) def test_has_rationale_returns_false(self): """has_rationale() returns false when an application has no no_other_contacts_rationale""" - self.assertEquals(self.started_application.has_rationale(), False) + with less_console_noise(): + self.assertEquals(self.started_application.has_rationale(), False) def test_has_other_contacts_returns_true(self): """has_other_contacts() returns true when an application has other_contacts""" - # completed_application has other contacts by default - self.assertEquals(self.started_application.has_other_contacts(), True) + with less_console_noise(): + # completed_application has other contacts by default + self.assertEquals(self.started_application.has_other_contacts(), True) def test_has_other_contacts_returns_false(self): """has_other_contacts() returns false when an application has no other_contacts""" - application = completed_application( - status=DomainApplication.ApplicationStatus.STARTED, name="no-others.gov", has_other_contacts=False - ) - self.assertEquals(application.has_other_contacts(), False) + with less_console_noise(): + application = completed_application( + status=DomainApplication.ApplicationStatus.STARTED, name="no-others.gov", has_other_contacts=False + ) + self.assertEquals(application.has_other_contacts(), False) class TestPermissions(TestCase): diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 9026832cd..8cedb65e9 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -7,6 +7,7 @@ from django.test import TestCase from django.db.utils import IntegrityError from unittest.mock import MagicMock, patch, call import datetime +from django.utils.timezone import make_aware from registrar.models import Domain, Host, HostIP from unittest import skip @@ -46,158 +47,162 @@ class TestDomainCache(MockEppLib): def test_cache_sets_resets(self): """Cache should be set on getter and reset on setter calls""" - domain, _ = Domain.objects.get_or_create(name="igorville.gov") - # trigger getter - _ = domain.creation_date - domain._get_property("contacts") - # getter should set the domain cache with a InfoDomain object - # (see InfoDomainResult) - self.assertEquals(domain._cache["auth_info"], self.mockDataInfoDomain.auth_info) - self.assertEquals(domain._cache["cr_date"], self.mockDataInfoDomain.cr_date) - status_list = [status.state for status in self.mockDataInfoDomain.statuses] - self.assertEquals(domain._cache["statuses"], status_list) - self.assertFalse("avail" in domain._cache.keys()) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="igorville.gov") + # trigger getter + _ = domain.creation_date + domain._get_property("contacts") + # getter should set the domain cache with a InfoDomain object + # (see InfoDomainResult) + self.assertEquals(domain._cache["auth_info"], self.mockDataInfoDomain.auth_info) + self.assertEquals(domain._cache["cr_date"], self.mockDataInfoDomain.cr_date) + status_list = [status.state for status in self.mockDataInfoDomain.statuses] + self.assertEquals(domain._cache["statuses"], status_list) + self.assertFalse("avail" in domain._cache.keys()) - # using a setter should clear the cache - domain.dnssecdata = [] - self.assertEquals(domain._cache, {}) + # using a setter should clear the cache + domain.dnssecdata = [] + self.assertEquals(domain._cache, {}) - # send should have been called only once - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.InfoDomain(name="igorville.gov", auth_info=None), - cleaned=True, - ), - ], - any_order=False, # Ensure calls are in the specified order - ) + # send should have been called only once + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.InfoDomain(name="igorville.gov", auth_info=None), + cleaned=True, + ), + ], + any_order=False, # Ensure calls are in the specified order + ) def test_cache_used_when_avail(self): """Cache is pulled from if the object has already been accessed""" - domain, _ = Domain.objects.get_or_create(name="igorville.gov") - cr_date = domain.creation_date + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="igorville.gov") + cr_date = domain.creation_date - # repeat the getter call - cr_date = domain.creation_date + # repeat the getter call + cr_date = domain.creation_date - # value should still be set correctly - self.assertEqual(cr_date, self.mockDataInfoDomain.cr_date) - self.assertEqual(domain._cache["cr_date"], self.mockDataInfoDomain.cr_date) + # value should still be set correctly + self.assertEqual(cr_date, self.mockDataInfoDomain.cr_date) + self.assertEqual(domain._cache["cr_date"], self.mockDataInfoDomain.cr_date) - # send was only called once & not on the second getter call - expectedCalls = [ - call(commands.InfoDomain(name="igorville.gov", auth_info=None), cleaned=True), - ] + # send was only called once & not on the second getter call + expectedCalls = [ + call(commands.InfoDomain(name="igorville.gov", auth_info=None), cleaned=True), + ] - self.mockedSendFunction.assert_has_calls(expectedCalls) + self.mockedSendFunction.assert_has_calls(expectedCalls) def test_cache_nested_elements(self): """Cache works correctly with the nested objects cache and hosts""" - domain, _ = Domain.objects.get_or_create(name="igorville.gov") - # The contact list will initially contain objects of type 'DomainContact' - # this is then transformed into PublicContact, and cache should NOT - # hold onto the DomainContact object - expectedUnfurledContactsList = [ - common.DomainContact(contact="123", type="security"), - ] - expectedContactsDict = { - PublicContact.ContactTypeChoices.ADMINISTRATIVE: None, - PublicContact.ContactTypeChoices.SECURITY: "123", - PublicContact.ContactTypeChoices.TECHNICAL: None, - } - expectedHostsDict = { - "name": self.mockDataInfoDomain.hosts[0], - "addrs": [item.addr for item in self.mockDataInfoHosts.addrs], - "cr_date": self.mockDataInfoHosts.cr_date, - } + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="igorville.gov") + # The contact list will initially contain objects of type 'DomainContact' + # this is then transformed into PublicContact, and cache should NOT + # hold onto the DomainContact object + expectedUnfurledContactsList = [ + common.DomainContact(contact="123", type="security"), + ] + expectedContactsDict = { + PublicContact.ContactTypeChoices.ADMINISTRATIVE: None, + PublicContact.ContactTypeChoices.SECURITY: "123", + PublicContact.ContactTypeChoices.TECHNICAL: None, + } + expectedHostsDict = { + "name": self.mockDataInfoDomain.hosts[0], + "addrs": [item.addr for item in self.mockDataInfoHosts.addrs], + "cr_date": self.mockDataInfoHosts.cr_date, + } - # this can be changed when the getter for contacts is implemented - domain._get_property("contacts") + # this can be changed when the getter for contacts is implemented + domain._get_property("contacts") - # check domain info is still correct and not overridden - self.assertEqual(domain._cache["auth_info"], self.mockDataInfoDomain.auth_info) - self.assertEqual(domain._cache["cr_date"], self.mockDataInfoDomain.cr_date) + # check domain info is still correct and not overridden + self.assertEqual(domain._cache["auth_info"], self.mockDataInfoDomain.auth_info) + self.assertEqual(domain._cache["cr_date"], self.mockDataInfoDomain.cr_date) - # check contacts - self.assertEqual(domain._cache["_contacts"], self.mockDataInfoDomain.contacts) - # The contact list should not contain what is sent by the registry by default, - # as _fetch_cache will transform the type to PublicContact - self.assertNotEqual(domain._cache["contacts"], expectedUnfurledContactsList) - self.assertEqual(domain._cache["contacts"], expectedContactsDict) + # check contacts + self.assertEqual(domain._cache["_contacts"], self.mockDataInfoDomain.contacts) + # The contact list should not contain what is sent by the registry by default, + # as _fetch_cache will transform the type to PublicContact + self.assertNotEqual(domain._cache["contacts"], expectedUnfurledContactsList) + self.assertEqual(domain._cache["contacts"], expectedContactsDict) - # get and check hosts is set correctly - domain._get_property("hosts") - self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) - self.assertEqual(domain._cache["contacts"], expectedContactsDict) - # invalidate cache - domain._cache = {} + # get and check hosts is set correctly + domain._get_property("hosts") + self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) + self.assertEqual(domain._cache["contacts"], expectedContactsDict) + # invalidate cache + domain._cache = {} - # get host - domain._get_property("hosts") - self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) + # get host + domain._get_property("hosts") + self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) - # get contacts - domain._get_property("contacts") - self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) - self.assertEqual(domain._cache["contacts"], expectedContactsDict) + # get contacts + domain._get_property("contacts") + self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) + self.assertEqual(domain._cache["contacts"], expectedContactsDict) def test_map_epp_contact_to_public_contact(self): # Tests that the mapper is working how we expect - domain, _ = Domain.objects.get_or_create(name="registry.gov") - security = PublicContact.ContactTypeChoices.SECURITY - mapped = domain.map_epp_contact_to_public_contact( - self.mockDataInfoContact, - self.mockDataInfoContact.id, - security, - ) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="registry.gov") + security = PublicContact.ContactTypeChoices.SECURITY + mapped = domain.map_epp_contact_to_public_contact( + self.mockDataInfoContact, + self.mockDataInfoContact.id, + security, + ) - expected_contact = PublicContact( - domain=domain, - contact_type=security, - registry_id="123", - email="123@mail.gov", - voice="+1.8882820870", - fax="+1-212-9876543", - pw="lastPw", - name="Registry Customer Service", - org="Cybersecurity and Infrastructure Security Agency", - city="Arlington", - pc="22201", - cc="US", - sp="VA", - street1="4200 Wilson Blvd.", - ) + expected_contact = PublicContact( + domain=domain, + contact_type=security, + registry_id="123", + email="123@mail.gov", + voice="+1.8882820870", + fax="+1-212-9876543", + pw="lastPw", + name="Registry Customer Service", + org="Cybersecurity and Infrastructure Security Agency", + city="Arlington", + pc="22201", + cc="US", + sp="VA", + street1="4200 Wilson Blvd.", + ) - # Test purposes only, since we're comparing - # two duplicate objects. We would expect - # these not to have the same state. - expected_contact._state = mapped._state + # Test purposes only, since we're comparing + # two duplicate objects. We would expect + # these not to have the same state. + expected_contact._state = mapped._state - # Mapped object is what we expect - self.assertEqual(mapped.__dict__, expected_contact.__dict__) + # Mapped object is what we expect + self.assertEqual(mapped.__dict__, expected_contact.__dict__) - # The mapped object should correctly translate to a DB - # object. If not, something else went wrong. - db_object = domain._get_or_create_public_contact(mapped) - in_db = PublicContact.objects.filter( - registry_id=domain.security_contact.registry_id, - contact_type=security, - ).get() - # DB Object is the same as the mapped object - self.assertEqual(db_object, in_db) + # The mapped object should correctly translate to a DB + # object. If not, something else went wrong. + db_object = domain._get_or_create_public_contact(mapped) + in_db = PublicContact.objects.filter( + registry_id=domain.security_contact.registry_id, + contact_type=security, + ).get() + # DB Object is the same as the mapped object + self.assertEqual(db_object, in_db) - domain.security_contact = in_db - # Trigger the getter - _ = domain.security_contact - # Check to see that changes made - # to DB objects persist in cache correctly - in_db.email = "123test@mail.gov" - in_db.save() + domain.security_contact = in_db + # Trigger the getter + _ = domain.security_contact + # Check to see that changes made + # to DB objects persist in cache correctly + in_db.email = "123test@mail.gov" + in_db.save() - cached_contact = domain._cache["contacts"].get(security) - self.assertEqual(cached_contact, in_db.registry_id) - self.assertEqual(domain.security_contact.email, "123test@mail.gov") + cached_contact = domain._cache["contacts"].get(security) + self.assertEqual(cached_contact, in_db.registry_id) + self.assertEqual(domain.security_contact.email, "123test@mail.gov") def test_errors_map_epp_contact_to_public_contact(self): """ @@ -206,48 +211,49 @@ class TestDomainCache(MockEppLib): gets invalid data from EPPLib Then the function throws the expected ContactErrors """ - domain, _ = Domain.objects.get_or_create(name="registry.gov") - fakedEpp = self.fakedEppObject() - invalid_length = fakedEpp.dummyInfoContactResultData( - "Cymaticsisasubsetofmodalvibrationalphenomena", "lengthInvalid@mail.gov" - ) - valid_object = fakedEpp.dummyInfoContactResultData("valid", "valid@mail.gov") - - desired_error = ContactErrorCodes.CONTACT_ID_INVALID_LENGTH - with self.assertRaises(ContactError) as context: - domain.map_epp_contact_to_public_contact( - invalid_length, - invalid_length.id, - PublicContact.ContactTypeChoices.SECURITY, + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="registry.gov") + fakedEpp = self.fakedEppObject() + invalid_length = fakedEpp.dummyInfoContactResultData( + "Cymaticsisasubsetofmodalvibrationalphenomena", "lengthInvalid@mail.gov" ) - self.assertEqual(context.exception.code, desired_error) + valid_object = fakedEpp.dummyInfoContactResultData("valid", "valid@mail.gov") - desired_error = ContactErrorCodes.CONTACT_ID_NONE - with self.assertRaises(ContactError) as context: - domain.map_epp_contact_to_public_contact( - valid_object, - None, - PublicContact.ContactTypeChoices.SECURITY, - ) - self.assertEqual(context.exception.code, desired_error) + desired_error = ContactErrorCodes.CONTACT_ID_INVALID_LENGTH + with self.assertRaises(ContactError) as context: + domain.map_epp_contact_to_public_contact( + invalid_length, + invalid_length.id, + PublicContact.ContactTypeChoices.SECURITY, + ) + self.assertEqual(context.exception.code, desired_error) - desired_error = ContactErrorCodes.CONTACT_INVALID_TYPE - with self.assertRaises(ContactError) as context: - domain.map_epp_contact_to_public_contact( - "bad_object", - valid_object.id, - PublicContact.ContactTypeChoices.SECURITY, - ) - self.assertEqual(context.exception.code, desired_error) + desired_error = ContactErrorCodes.CONTACT_ID_NONE + with self.assertRaises(ContactError) as context: + domain.map_epp_contact_to_public_contact( + valid_object, + None, + PublicContact.ContactTypeChoices.SECURITY, + ) + self.assertEqual(context.exception.code, desired_error) - desired_error = ContactErrorCodes.CONTACT_TYPE_NONE - with self.assertRaises(ContactError) as context: - domain.map_epp_contact_to_public_contact( - valid_object, - valid_object.id, - None, - ) - self.assertEqual(context.exception.code, desired_error) + desired_error = ContactErrorCodes.CONTACT_INVALID_TYPE + with self.assertRaises(ContactError) as context: + domain.map_epp_contact_to_public_contact( + "bad_object", + valid_object.id, + PublicContact.ContactTypeChoices.SECURITY, + ) + self.assertEqual(context.exception.code, desired_error) + + desired_error = ContactErrorCodes.CONTACT_TYPE_NONE + with self.assertRaises(ContactError) as context: + domain.map_epp_contact_to_public_contact( + valid_object, + valid_object.id, + None, + ) + self.assertEqual(context.exception.code, desired_error) class TestDomainCreation(MockEppLib): @@ -346,42 +352,44 @@ class TestDomainStatuses(MockEppLib): def test_get_status(self): """Domain 'statuses' getter returns statuses by calling epp""" - domain, _ = Domain.objects.get_or_create(name="chicken-liver.gov") - # trigger getter - _ = domain.statuses - status_list = [status.state for status in self.mockDataInfoDomain.statuses] - self.assertEquals(domain._cache["statuses"], status_list) - # Called in _fetch_cache - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.InfoDomain(name="chicken-liver.gov", auth_info=None), - cleaned=True, - ), - ], - any_order=False, # Ensure calls are in the specified order - ) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="chicken-liver.gov") + # trigger getter + _ = domain.statuses + status_list = [status.state for status in self.mockDataInfoDomain.statuses] + self.assertEquals(domain._cache["statuses"], status_list) + # Called in _fetch_cache + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.InfoDomain(name="chicken-liver.gov", auth_info=None), + cleaned=True, + ), + ], + any_order=False, # Ensure calls are in the specified order + ) def test_get_status_returns_empty_list_when_value_error(self): """Domain 'statuses' getter returns an empty list when value error""" - domain, _ = Domain.objects.get_or_create(name="pig-knuckles.gov") + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="pig-knuckles.gov") - def side_effect(self): - raise KeyError + def side_effect(self): + raise KeyError - patcher = patch("registrar.models.domain.Domain._get_property") - mocked_get = patcher.start() - mocked_get.side_effect = side_effect + patcher = patch("registrar.models.domain.Domain._get_property") + mocked_get = patcher.start() + mocked_get.side_effect = side_effect - # trigger getter - _ = domain.statuses + # trigger getter + _ = domain.statuses - with self.assertRaises(KeyError): - _ = domain._cache["statuses"] - self.assertEquals(_, []) + with self.assertRaises(KeyError): + _ = domain._cache["statuses"] + self.assertEquals(_, []) - patcher.stop() + patcher.stop() @skip("not implemented yet") def test_place_client_hold_sets_status(self): @@ -398,28 +406,23 @@ class TestDomainStatuses(MockEppLib): first_ready is set when a domain is first transitioned to READY. It does not get overwritten in case the domain gets out of and back into READY. """ - domain, _ = Domain.objects.get_or_create(name="pig-knuckles.gov", state=Domain.State.DNS_NEEDED) - self.assertEqual(domain.first_ready, None) - - domain.ready() - - # check that status is READY - self.assertTrue(domain.is_active()) - self.assertNotEqual(domain.first_ready, None) - - # Capture the value of first_ready - first_ready = domain.first_ready - - # change domain status - domain.dns_needed() - self.assertFalse(domain.is_active()) - - # change back to READY - domain.ready() - self.assertTrue(domain.is_active()) - - # assert that the value of first_ready has not changed - self.assertEqual(domain.first_ready, first_ready) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="pig-knuckles.gov", state=Domain.State.DNS_NEEDED) + self.assertEqual(domain.first_ready, None) + domain.ready() + # check that status is READY + self.assertTrue(domain.is_active()) + self.assertNotEqual(domain.first_ready, None) + # Capture the value of first_ready + first_ready = domain.first_ready + # change domain status + domain.dns_needed() + self.assertFalse(domain.is_active()) + # change back to READY + domain.ready() + self.assertTrue(domain.is_active()) + # assert that the value of first_ready has not changed + self.assertEqual(domain.first_ready, first_ready) def tearDown(self) -> None: PublicContact.objects.all().delete() @@ -557,37 +560,32 @@ class TestRegistrantContacts(MockEppLib): Then the domain has a valid security contact with CISA defaults And disclose flags are set to keep the email address hidden """ - - # making a domain should make it domain - expectedSecContact = PublicContact.get_default_security() - expectedSecContact.domain = self.domain - - self.domain.dns_needed_from_unknown() - - self.assertEqual(self.mockedSendFunction.call_count, 8) - self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 4) - self.assertEqual( - PublicContact.objects.get( + with less_console_noise(): + # making a domain should make it domain + expectedSecContact = PublicContact.get_default_security() + expectedSecContact.domain = self.domain + self.domain.dns_needed_from_unknown() + self.assertEqual(self.mockedSendFunction.call_count, 8) + self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 4) + self.assertEqual( + PublicContact.objects.get( + domain=self.domain, + contact_type=PublicContact.ContactTypeChoices.SECURITY, + ).email, + expectedSecContact.email, + ) + id = PublicContact.objects.get( domain=self.domain, contact_type=PublicContact.ContactTypeChoices.SECURITY, - ).email, - expectedSecContact.email, - ) - - id = PublicContact.objects.get( - domain=self.domain, - contact_type=PublicContact.ContactTypeChoices.SECURITY, - ).registry_id - - expectedSecContact.registry_id = id - expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=False) - expectedUpdateDomain = commands.UpdateDomain( - name=self.domain.name, - add=[common.DomainContact(contact=expectedSecContact.registry_id, type="security")], - ) - - self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) - self.mockedSendFunction.assert_any_call(expectedUpdateDomain, cleaned=True) + ).registry_id + expectedSecContact.registry_id = id + expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=False) + expectedUpdateDomain = commands.UpdateDomain( + name=self.domain.name, + add=[common.DomainContact(contact=expectedSecContact.registry_id, type="security")], + ) + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + self.mockedSendFunction.assert_any_call(expectedUpdateDomain, cleaned=True) def test_user_adds_security_email(self): """ @@ -598,35 +596,31 @@ class TestRegistrantContacts(MockEppLib): And Domain sends `commands.UpdateDomain` to the registry with the newly created contact of type 'security' """ - # make a security contact that is a PublicContact - # make sure a security email already exists - self.domain.dns_needed_from_unknown() - expectedSecContact = PublicContact.get_default_security() - expectedSecContact.domain = self.domain - expectedSecContact.email = "newEmail@fake.com" - expectedSecContact.registry_id = "456" - expectedSecContact.name = "Fakey McFakerson" - - # calls the security contact setter as if you did - # self.domain.security_contact=expectedSecContact - expectedSecContact.save() - - # no longer the default email it should be disclosed - expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=True) - - expectedUpdateDomain = commands.UpdateDomain( - name=self.domain.name, - add=[common.DomainContact(contact=expectedSecContact.registry_id, type="security")], - ) - - # check that send has triggered the create command for the contact - receivedSecurityContact = PublicContact.objects.get( - domain=self.domain, contact_type=PublicContact.ContactTypeChoices.SECURITY - ) - - self.assertEqual(receivedSecurityContact, expectedSecContact) - self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) - self.mockedSendFunction.assert_any_call(expectedUpdateDomain, cleaned=True) + with less_console_noise(): + # make a security contact that is a PublicContact + # make sure a security email already exists + self.domain.dns_needed_from_unknown() + expectedSecContact = PublicContact.get_default_security() + expectedSecContact.domain = self.domain + expectedSecContact.email = "newEmail@fake.com" + expectedSecContact.registry_id = "456" + expectedSecContact.name = "Fakey McFakerson" + # calls the security contact setter as if you did + # self.domain.security_contact=expectedSecContact + expectedSecContact.save() + # no longer the default email it should be disclosed + expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=True) + expectedUpdateDomain = commands.UpdateDomain( + name=self.domain.name, + add=[common.DomainContact(contact=expectedSecContact.registry_id, type="security")], + ) + # check that send has triggered the create command for the contact + receivedSecurityContact = PublicContact.objects.get( + domain=self.domain, contact_type=PublicContact.ContactTypeChoices.SECURITY + ) + self.assertEqual(receivedSecurityContact, expectedSecContact) + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + self.mockedSendFunction.assert_any_call(expectedUpdateDomain, cleaned=True) def test_security_email_is_idempotent(self): """ @@ -635,26 +629,23 @@ class TestRegistrantContacts(MockEppLib): to the registry twice with identical data Then no errors are raised in Domain """ - - security_contact = self.domain.get_default_security_contact() - security_contact.registry_id = "fail" - security_contact.save() - - self.domain.security_contact = security_contact - - expectedCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=False) - - expectedUpdateDomain = commands.UpdateDomain( - name=self.domain.name, - add=[common.DomainContact(contact=security_contact.registry_id, type="security")], - ) - expected_calls = [ - call(expectedCreateCommand, cleaned=True), - call(expectedCreateCommand, cleaned=True), - call(expectedUpdateDomain, cleaned=True), - ] - self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True) - self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 1) + with less_console_noise(): + security_contact = self.domain.get_default_security_contact() + security_contact.registry_id = "fail" + security_contact.save() + self.domain.security_contact = security_contact + expectedCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=False) + expectedUpdateDomain = commands.UpdateDomain( + name=self.domain.name, + add=[common.DomainContact(contact=security_contact.registry_id, type="security")], + ) + expected_calls = [ + call(expectedCreateCommand, cleaned=True), + call(expectedCreateCommand, cleaned=True), + call(expectedUpdateDomain, cleaned=True), + ] + self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True) + self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 1) def test_user_deletes_security_email(self): """ @@ -667,51 +658,47 @@ class TestRegistrantContacts(MockEppLib): And the domain has a valid security contact with CISA defaults And disclose flags are set to keep the email address hidden """ - old_contact = self.domain.get_default_security_contact() - - old_contact.registry_id = "fail" - old_contact.email = "user.entered@email.com" - old_contact.save() - new_contact = self.domain.get_default_security_contact() - new_contact.registry_id = "fail" - new_contact.email = "" - self.domain.security_contact = new_contact - - firstCreateContactCall = self._convertPublicContactToEpp(old_contact, disclose_email=True) - updateDomainAddCall = commands.UpdateDomain( - name=self.domain.name, - add=[common.DomainContact(contact=old_contact.registry_id, type="security")], - ) - self.assertEqual( - PublicContact.objects.filter(domain=self.domain).get().email, - PublicContact.get_default_security().email, - ) - # this one triggers the fail - secondCreateContact = self._convertPublicContactToEpp(new_contact, disclose_email=True) - updateDomainRemCall = commands.UpdateDomain( - name=self.domain.name, - rem=[common.DomainContact(contact=old_contact.registry_id, type="security")], - ) - - defaultSecID = PublicContact.objects.filter(domain=self.domain).get().registry_id - default_security = PublicContact.get_default_security() - default_security.registry_id = defaultSecID - createDefaultContact = self._convertPublicContactToEpp(default_security, disclose_email=False) - updateDomainWDefault = commands.UpdateDomain( - name=self.domain.name, - add=[common.DomainContact(contact=defaultSecID, type="security")], - ) - - expected_calls = [ - call(firstCreateContactCall, cleaned=True), - call(updateDomainAddCall, cleaned=True), - call(secondCreateContact, cleaned=True), - call(updateDomainRemCall, cleaned=True), - call(createDefaultContact, cleaned=True), - call(updateDomainWDefault, cleaned=True), - ] - - self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True) + with less_console_noise(): + old_contact = self.domain.get_default_security_contact() + old_contact.registry_id = "fail" + old_contact.email = "user.entered@email.com" + old_contact.save() + new_contact = self.domain.get_default_security_contact() + new_contact.registry_id = "fail" + new_contact.email = "" + self.domain.security_contact = new_contact + firstCreateContactCall = self._convertPublicContactToEpp(old_contact, disclose_email=True) + updateDomainAddCall = commands.UpdateDomain( + name=self.domain.name, + add=[common.DomainContact(contact=old_contact.registry_id, type="security")], + ) + self.assertEqual( + PublicContact.objects.filter(domain=self.domain).get().email, + PublicContact.get_default_security().email, + ) + # this one triggers the fail + secondCreateContact = self._convertPublicContactToEpp(new_contact, disclose_email=True) + updateDomainRemCall = commands.UpdateDomain( + name=self.domain.name, + rem=[common.DomainContact(contact=old_contact.registry_id, type="security")], + ) + defaultSecID = PublicContact.objects.filter(domain=self.domain).get().registry_id + default_security = PublicContact.get_default_security() + default_security.registry_id = defaultSecID + createDefaultContact = self._convertPublicContactToEpp(default_security, disclose_email=False) + updateDomainWDefault = commands.UpdateDomain( + name=self.domain.name, + add=[common.DomainContact(contact=defaultSecID, type="security")], + ) + expected_calls = [ + call(firstCreateContactCall, cleaned=True), + call(updateDomainAddCall, cleaned=True), + call(secondCreateContact, cleaned=True), + call(updateDomainRemCall, cleaned=True), + call(createDefaultContact, cleaned=True), + call(updateDomainWDefault, cleaned=True), + ] + self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True) def test_updates_security_email(self): """ @@ -721,29 +708,28 @@ class TestRegistrantContacts(MockEppLib): security contact email Then Domain sends `commands.UpdateContact` to the registry """ - security_contact = self.domain.get_default_security_contact() - security_contact.email = "originalUserEmail@gmail.com" - security_contact.registry_id = "fail" - security_contact.save() - expectedCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=True) - - expectedUpdateDomain = commands.UpdateDomain( - name=self.domain.name, - add=[common.DomainContact(contact=security_contact.registry_id, type="security")], - ) - security_contact.email = "changedEmail@email.com" - security_contact.save() - expectedSecondCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=True) - updateContact = self._convertPublicContactToEpp(security_contact, disclose_email=True, createContact=False) - - expected_calls = [ - call(expectedCreateCommand, cleaned=True), - call(expectedUpdateDomain, cleaned=True), - call(expectedSecondCreateCommand, cleaned=True), - call(updateContact, cleaned=True), - ] - self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True) - self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 1) + with less_console_noise(): + security_contact = self.domain.get_default_security_contact() + security_contact.email = "originalUserEmail@gmail.com" + security_contact.registry_id = "fail" + security_contact.save() + expectedCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=True) + expectedUpdateDomain = commands.UpdateDomain( + name=self.domain.name, + add=[common.DomainContact(contact=security_contact.registry_id, type="security")], + ) + security_contact.email = "changedEmail@email.com" + security_contact.save() + expectedSecondCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=True) + updateContact = self._convertPublicContactToEpp(security_contact, disclose_email=True, createContact=False) + expected_calls = [ + call(expectedCreateCommand, cleaned=True), + call(expectedUpdateDomain, cleaned=True), + call(expectedSecondCreateCommand, cleaned=True), + call(updateContact, cleaned=True), + ] + self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True) + self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 1) def test_security_email_returns_on_registry_error(self): """ @@ -751,28 +737,24 @@ class TestRegistrantContacts(MockEppLib): Registry is unavailable and throws exception when attempting to build cache from registry. Security email retrieved from database. """ - # Use self.domain_contact which has been initialized with existing contacts, including securityContact - - # call get_security_email to initially set the security_contact_registry_id in the domain model - self.domain_contact.get_security_email() - # invalidate the cache so the next time get_security_email is called, it has to attempt to populate cache - self.domain_contact._invalidate_cache() - - # mock that registry throws an error on the EPP send - def side_effect(_request, cleaned): - raise RegistryError(code=ErrorCode.COMMAND_FAILED) - - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - # when get_security_email is called, the registry error will force the security contact - # to be retrieved using the security_contact_registry_id in the domain model - security_email = self.domain_contact.get_security_email() - - # assert that the proper security contact was retrieved by testing the email matches expected value - self.assertEqual(security_email, "security@mail.gov") - patcher.stop() + with less_console_noise(): + # Use self.domain_contact which has been initialized with existing contacts, including securityContact + # call get_security_email to initially set the security_contact_registry_id in the domain model + self.domain_contact.get_security_email() + # invalidate the cache so the next time get_security_email is called, it has to attempt to populate cache + self.domain_contact._invalidate_cache() + # mock that registry throws an error on the EPP send + def side_effect(_request, cleaned): + raise RegistryError(code=ErrorCode.COMMAND_FAILED) + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + # when get_security_email is called, the registry error will force the security contact + # to be retrieved using the security_contact_registry_id in the domain model + security_email = self.domain_contact.get_security_email() + # assert that the proper security contact was retrieved by testing the email matches expected value + self.assertEqual(security_email, "security@mail.gov") + patcher.stop() def test_security_email_stored_on_fetch_cache(self): """ @@ -781,13 +763,14 @@ class TestRegistrantContacts(MockEppLib): The mocked data for the EPP calls for the freeman.gov domain returns a security contact with registry id of securityContact when InfoContact is called """ - # Use self.domain_contact which has been initialized with existing contacts, including securityContact + with less_console_noise(): + # Use self.domain_contact which has been initialized with existing contacts, including securityContact - # force fetch_cache to be called, which will return above documented mocked hosts - self.domain_contact.get_security_email() + # force fetch_cache to be called, which will return above documented mocked hosts + self.domain_contact.get_security_email() - # assert that the security_contact_registry_id in the db matches "securityContact" - self.assertEqual(self.domain_contact.security_contact_registry_id, "securityContact") + # assert that the security_contact_registry_id in the db matches "securityContact" + self.assertEqual(self.domain_contact.security_contact_registry_id, "securityContact") def test_not_disclosed_on_other_contacts(self): """ @@ -798,113 +781,101 @@ class TestRegistrantContacts(MockEppLib): And the field `disclose` is set to false for DF.EMAIL on all fields except security """ - # Generates a domain with four existing contacts - domain, _ = Domain.objects.get_or_create(name="freeman.gov") - - # Contact setup - expected_admin = domain.get_default_administrative_contact() - expected_admin.email = self.mockAdministrativeContact.email - - expected_registrant = domain.get_default_registrant_contact() - expected_registrant.email = self.mockRegistrantContact.email - - expected_security = domain.get_default_security_contact() - expected_security.email = self.mockSecurityContact.email - - expected_tech = domain.get_default_technical_contact() - expected_tech.email = self.mockTechnicalContact.email - - domain.administrative_contact = expected_admin - domain.registrant_contact = expected_registrant - domain.security_contact = expected_security - domain.technical_contact = expected_tech - - contacts = [ - (expected_admin, domain.administrative_contact), - (expected_registrant, domain.registrant_contact), - (expected_security, domain.security_contact), - (expected_tech, domain.technical_contact), - ] - - # Test for each contact - for contact in contacts: - expected_contact = contact[0] - actual_contact = contact[1] - is_security = expected_contact.contact_type == "security" - - expectedCreateCommand = self._convertPublicContactToEpp(expected_contact, disclose_email=is_security) - - # Should only be disclosed if the type is security, as the email is valid - self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) - - # The emails should match on both items - self.assertEqual(expected_contact.email, actual_contact.email) + with less_console_noise(): + # Generates a domain with four existing contacts + domain, _ = Domain.objects.get_or_create(name="freeman.gov") + # Contact setup + expected_admin = domain.get_default_administrative_contact() + expected_admin.email = self.mockAdministrativeContact.email + expected_registrant = domain.get_default_registrant_contact() + expected_registrant.email = self.mockRegistrantContact.email + expected_security = domain.get_default_security_contact() + expected_security.email = self.mockSecurityContact.email + expected_tech = domain.get_default_technical_contact() + expected_tech.email = self.mockTechnicalContact.email + domain.administrative_contact = expected_admin + domain.registrant_contact = expected_registrant + domain.security_contact = expected_security + domain.technical_contact = expected_tech + contacts = [ + (expected_admin, domain.administrative_contact), + (expected_registrant, domain.registrant_contact), + (expected_security, domain.security_contact), + (expected_tech, domain.technical_contact), + ] + # Test for each contact + for contact in contacts: + expected_contact = contact[0] + actual_contact = contact[1] + is_security = expected_contact.contact_type == "security" + expectedCreateCommand = self._convertPublicContactToEpp(expected_contact, disclose_email=is_security) + # Should only be disclosed if the type is security, as the email is valid + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + # The emails should match on both items + self.assertEqual(expected_contact.email, actual_contact.email) def test_convert_public_contact_to_epp(self): - domain, _ = Domain.objects.get_or_create(name="freeman.gov") - dummy_contact = domain.get_default_security_contact() - test_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=True).__dict__ - test_not_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=False).__dict__ - - # Separated for linter - disclose_email_field = {common.DiscloseField.EMAIL} - expected_disclose = { - "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), - "disclose": common.Disclose(flag=True, fields=disclose_email_field, types=None), - "email": "dotgov@cisa.dhs.gov", - "extensions": [], - "fax": None, - "id": "ThIq2NcRIDN7PauO", - "ident": None, - "notify_email": None, - "postal_info": common.PostalInfo( - name="Registry Customer Service", - addr=common.ContactAddr( - street=["4200 Wilson Blvd.", None, None], - city="Arlington", - pc="22201", - cc="US", - sp="VA", + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="freeman.gov") + dummy_contact = domain.get_default_security_contact() + test_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=True).__dict__ + test_not_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=False).__dict__ + # Separated for linter + disclose_email_field = {common.DiscloseField.EMAIL} + expected_disclose = { + "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), + "disclose": common.Disclose(flag=True, fields=disclose_email_field, types=None), + "email": "dotgov@cisa.dhs.gov", + "extensions": [], + "fax": None, + "id": "ThIq2NcRIDN7PauO", + "ident": None, + "notify_email": None, + "postal_info": common.PostalInfo( + name="Registry Customer Service", + addr=common.ContactAddr( + street=["4200 Wilson Blvd.", None, None], + city="Arlington", + pc="22201", + cc="US", + sp="VA", + ), + org="Cybersecurity and Infrastructure Security Agency", + type="loc", ), - org="Cybersecurity and Infrastructure Security Agency", - type="loc", - ), - "vat": None, - "voice": "+1.8882820870", - } - - # Separated for linter - expected_not_disclose = { - "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), - "disclose": common.Disclose(flag=False, fields=disclose_email_field, types=None), - "email": "dotgov@cisa.dhs.gov", - "extensions": [], - "fax": None, - "id": "ThrECENCHI76PGLh", - "ident": None, - "notify_email": None, - "postal_info": common.PostalInfo( - name="Registry Customer Service", - addr=common.ContactAddr( - street=["4200 Wilson Blvd.", None, None], - city="Arlington", - pc="22201", - cc="US", - sp="VA", + "vat": None, + "voice": "+1.8882820870", + } + # Separated for linter + expected_not_disclose = { + "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), + "disclose": common.Disclose(flag=False, fields=disclose_email_field, types=None), + "email": "dotgov@cisa.dhs.gov", + "extensions": [], + "fax": None, + "id": "ThrECENCHI76PGLh", + "ident": None, + "notify_email": None, + "postal_info": common.PostalInfo( + name="Registry Customer Service", + addr=common.ContactAddr( + street=["4200 Wilson Blvd.", None, None], + city="Arlington", + pc="22201", + cc="US", + sp="VA", + ), + org="Cybersecurity and Infrastructure Security Agency", + type="loc", ), - org="Cybersecurity and Infrastructure Security Agency", - type="loc", - ), - "vat": None, - "voice": "+1.8882820870", - } - - # Set the ids equal, since this value changes - test_disclose["id"] = expected_disclose["id"] - test_not_disclose["id"] = expected_not_disclose["id"] - - self.assertEqual(test_disclose, expected_disclose) - self.assertEqual(test_not_disclose, expected_not_disclose) + "vat": None, + "voice": "+1.8882820870", + } + # Set the ids equal, since this value changes + test_disclose["id"] = expected_disclose["id"] + test_not_disclose["id"] = expected_not_disclose["id"] + self.assertEqual(test_disclose, expected_disclose) + self.assertEqual(test_not_disclose, expected_not_disclose) def test_not_disclosed_on_default_security_contact(self): """ @@ -913,17 +884,16 @@ class TestRegistrantContacts(MockEppLib): Then Domain sends `commands.CreateContact` to the registry And the field `disclose` is set to false for DF.EMAIL """ - domain, _ = Domain.objects.get_or_create(name="defaultsecurity.gov") - expectedSecContact = PublicContact.get_default_security() - expectedSecContact.domain = domain - expectedSecContact.registry_id = "defaultSec" - domain.security_contact = expectedSecContact - - expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=False) - - self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) - # Confirm that we are getting a default email - self.assertEqual(domain.security_contact.email, expectedSecContact.email) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="defaultsecurity.gov") + expectedSecContact = PublicContact.get_default_security() + expectedSecContact.domain = domain + expectedSecContact.registry_id = "defaultSec" + domain.security_contact = expectedSecContact + expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=False) + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + # Confirm that we are getting a default email + self.assertEqual(domain.security_contact.email, expectedSecContact.email) def test_not_disclosed_on_default_technical_contact(self): """ @@ -932,17 +902,16 @@ class TestRegistrantContacts(MockEppLib): Then Domain sends `commands.CreateContact` to the registry And the field `disclose` is set to false for DF.EMAIL """ - domain, _ = Domain.objects.get_or_create(name="defaulttechnical.gov") - expectedTechContact = PublicContact.get_default_technical() - expectedTechContact.domain = domain - expectedTechContact.registry_id = "defaultTech" - domain.technical_contact = expectedTechContact - - expectedCreateCommand = self._convertPublicContactToEpp(expectedTechContact, disclose_email=False) - - self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) - # Confirm that we are getting a default email - self.assertEqual(domain.technical_contact.email, expectedTechContact.email) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="defaulttechnical.gov") + expectedTechContact = PublicContact.get_default_technical() + expectedTechContact.domain = domain + expectedTechContact.registry_id = "defaultTech" + domain.technical_contact = expectedTechContact + expectedCreateCommand = self._convertPublicContactToEpp(expectedTechContact, disclose_email=False) + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + # Confirm that we are getting a default email + self.assertEqual(domain.technical_contact.email, expectedTechContact.email) def test_is_disclosed_on_security_contact(self): """ @@ -952,17 +921,16 @@ class TestRegistrantContacts(MockEppLib): Then Domain sends `commands.CreateContact` to the registry And the field `disclose` is set to true for DF.EMAIL """ - domain, _ = Domain.objects.get_or_create(name="igorville.gov") - expectedSecContact = PublicContact.get_default_security() - expectedSecContact.domain = domain - expectedSecContact.email = "123@mail.gov" - domain.security_contact = expectedSecContact - - expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=True) - - self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) - # Confirm that we are getting the desired email - self.assertEqual(domain.security_contact.email, expectedSecContact.email) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="igorville.gov") + expectedSecContact = PublicContact.get_default_security() + expectedSecContact.domain = domain + expectedSecContact.email = "123@mail.gov" + domain.security_contact = expectedSecContact + expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=True) + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + # Confirm that we are getting the desired email + self.assertEqual(domain.security_contact.email, expectedSecContact.email) @skip("not implemented yet") def test_update_is_unsuccessful(self): @@ -974,121 +942,112 @@ class TestRegistrantContacts(MockEppLib): raise def test_contact_getter_security(self): - security = PublicContact.ContactTypeChoices.SECURITY - # Create prexisting object - expected_contact = self.domain.map_epp_contact_to_public_contact( - self.mockSecurityContact, - contact_id="securityContact", - contact_type=security, - ) - - # Checks if we grabbed the correct PublicContact - self.assertEqual(self.domain_contact.security_contact.email, expected_contact.email) - - expected_contact_db = PublicContact.objects.filter( - registry_id=self.domain_contact.security_contact.registry_id, - contact_type=security, - ).get() - - self.assertEqual(self.domain_contact.security_contact, expected_contact_db) - - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.InfoContact(id="securityContact", auth_info=None), - cleaned=True, - ), - ] - ) - # Checks if we are receiving the cache we expect - cache = self.domain_contact._cache["contacts"] - self.assertEqual(cache.get(security), "securityContact") + with less_console_noise(): + security = PublicContact.ContactTypeChoices.SECURITY + # Create prexisting object + expected_contact = self.domain.map_epp_contact_to_public_contact( + self.mockSecurityContact, + contact_id="securityContact", + contact_type=security, + ) + # Checks if we grabbed the correct PublicContact + self.assertEqual(self.domain_contact.security_contact.email, expected_contact.email) + expected_contact_db = PublicContact.objects.filter( + registry_id=self.domain_contact.security_contact.registry_id, + contact_type=security, + ).get() + self.assertEqual(self.domain_contact.security_contact, expected_contact_db) + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.InfoContact(id="securityContact", auth_info=None), + cleaned=True, + ), + ] + ) + # Checks if we are receiving the cache we expect + cache = self.domain_contact._cache["contacts"] + self.assertEqual(cache.get(security), "securityContact") def test_contact_getter_technical(self): - technical = PublicContact.ContactTypeChoices.TECHNICAL - expected_contact = self.domain.map_epp_contact_to_public_contact( - self.mockTechnicalContact, - contact_id="technicalContact", - contact_type=technical, - ) - - self.assertEqual(self.domain_contact.technical_contact.email, expected_contact.email) - - # Checks if we grab the correct PublicContact - expected_contact_db = PublicContact.objects.filter( - registry_id=self.domain_contact.technical_contact.registry_id, - contact_type=technical, - ).get() - - # Checks if we grab the correct PublicContact - self.assertEqual(self.domain_contact.technical_contact, expected_contact_db) - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.InfoContact(id="technicalContact", auth_info=None), - cleaned=True, - ), - ] - ) - # Checks if we are receiving the cache we expect - cache = self.domain_contact._cache["contacts"] - self.assertEqual(cache.get(technical), "technicalContact") + with less_console_noise(): + technical = PublicContact.ContactTypeChoices.TECHNICAL + expected_contact = self.domain.map_epp_contact_to_public_contact( + self.mockTechnicalContact, + contact_id="technicalContact", + contact_type=technical, + ) + self.assertEqual(self.domain_contact.technical_contact.email, expected_contact.email) + # Checks if we grab the correct PublicContact + expected_contact_db = PublicContact.objects.filter( + registry_id=self.domain_contact.technical_contact.registry_id, + contact_type=technical, + ).get() + # Checks if we grab the correct PublicContact + self.assertEqual(self.domain_contact.technical_contact, expected_contact_db) + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.InfoContact(id="technicalContact", auth_info=None), + cleaned=True, + ), + ] + ) + # Checks if we are receiving the cache we expect + cache = self.domain_contact._cache["contacts"] + self.assertEqual(cache.get(technical), "technicalContact") def test_contact_getter_administrative(self): - administrative = PublicContact.ContactTypeChoices.ADMINISTRATIVE - expected_contact = self.domain.map_epp_contact_to_public_contact( - self.mockAdministrativeContact, - contact_id="adminContact", - contact_type=administrative, - ) - - self.assertEqual(self.domain_contact.administrative_contact.email, expected_contact.email) - - expected_contact_db = PublicContact.objects.filter( - registry_id=self.domain_contact.administrative_contact.registry_id, - contact_type=administrative, - ).get() - - # Checks if we grab the correct PublicContact - self.assertEqual(self.domain_contact.administrative_contact, expected_contact_db) - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.InfoContact(id="adminContact", auth_info=None), - cleaned=True, - ), - ] - ) - # Checks if we are receiving the cache we expect - cache = self.domain_contact._cache["contacts"] - self.assertEqual(cache.get(administrative), "adminContact") + with less_console_noise(): + administrative = PublicContact.ContactTypeChoices.ADMINISTRATIVE + expected_contact = self.domain.map_epp_contact_to_public_contact( + self.mockAdministrativeContact, + contact_id="adminContact", + contact_type=administrative, + ) + self.assertEqual(self.domain_contact.administrative_contact.email, expected_contact.email) + expected_contact_db = PublicContact.objects.filter( + registry_id=self.domain_contact.administrative_contact.registry_id, + contact_type=administrative, + ).get() + # Checks if we grab the correct PublicContact + self.assertEqual(self.domain_contact.administrative_contact, expected_contact_db) + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.InfoContact(id="adminContact", auth_info=None), + cleaned=True, + ), + ] + ) + # Checks if we are receiving the cache we expect + cache = self.domain_contact._cache["contacts"] + self.assertEqual(cache.get(administrative), "adminContact") def test_contact_getter_registrant(self): - expected_contact = self.domain.map_epp_contact_to_public_contact( - self.mockRegistrantContact, - contact_id="regContact", - contact_type=PublicContact.ContactTypeChoices.REGISTRANT, - ) - - self.assertEqual(self.domain_contact.registrant_contact.email, expected_contact.email) - - expected_contact_db = PublicContact.objects.filter( - registry_id=self.domain_contact.registrant_contact.registry_id, - contact_type=PublicContact.ContactTypeChoices.REGISTRANT, - ).get() - - # Checks if we grab the correct PublicContact - self.assertEqual(self.domain_contact.registrant_contact, expected_contact_db) - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.InfoContact(id="regContact", auth_info=None), - cleaned=True, - ), - ] - ) - # Checks if we are receiving the cache we expect. - self.assertEqual(self.domain_contact._cache["registrant"], expected_contact_db) + with less_console_noise(): + expected_contact = self.domain.map_epp_contact_to_public_contact( + self.mockRegistrantContact, + contact_id="regContact", + contact_type=PublicContact.ContactTypeChoices.REGISTRANT, + ) + self.assertEqual(self.domain_contact.registrant_contact.email, expected_contact.email) + expected_contact_db = PublicContact.objects.filter( + registry_id=self.domain_contact.registrant_contact.registry_id, + contact_type=PublicContact.ContactTypeChoices.REGISTRANT, + ).get() + # Checks if we grab the correct PublicContact + self.assertEqual(self.domain_contact.registrant_contact, expected_contact_db) + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.InfoContact(id="regContact", auth_info=None), + cleaned=True, + ), + ] + ) + # Checks if we are receiving the cache we expect. + self.assertEqual(self.domain_contact._cache["registrant"], expected_contact_db) class TestRegistrantNameservers(MockEppLib): @@ -1112,76 +1071,78 @@ class TestRegistrantNameservers(MockEppLib): def test_get_nameserver_changes_success_deleted_vals(self): """Testing only deleting and no other changes""" - self.domain._cache["hosts"] = [ - {"name": "ns1.example.com", "addrs": None}, - {"name": "ns2.example.com", "addrs": ["1.2.3.4"]}, - ] - newChanges = [ - ("ns1.example.com",), - ] - ( - deleted_values, - updated_values, - new_values, - oldNameservers, - ) = self.domain.getNameserverChanges(newChanges) + with less_console_noise(): + self.domain._cache["hosts"] = [ + {"name": "ns1.example.com", "addrs": None}, + {"name": "ns2.example.com", "addrs": ["1.2.3.4"]}, + ] + newChanges = [ + ("ns1.example.com",), + ] + ( + deleted_values, + updated_values, + new_values, + oldNameservers, + ) = self.domain.getNameserverChanges(newChanges) - self.assertEqual(deleted_values, ["ns2.example.com"]) - self.assertEqual(updated_values, []) - self.assertEqual(new_values, {}) - self.assertEqual( - oldNameservers, - {"ns1.example.com": None, "ns2.example.com": ["1.2.3.4"]}, - ) + self.assertEqual(deleted_values, ["ns2.example.com"]) + self.assertEqual(updated_values, []) + self.assertEqual(new_values, {}) + self.assertEqual( + oldNameservers, + {"ns1.example.com": None, "ns2.example.com": ["1.2.3.4"]}, + ) def test_get_nameserver_changes_success_updated_vals(self): """Testing only updating no other changes""" - self.domain._cache["hosts"] = [ - {"name": "ns3.my-nameserver.gov", "addrs": ["1.2.3.4"]}, - ] - newChanges = [ - ("ns3.my-nameserver.gov", ["1.2.4.5"]), - ] - ( - deleted_values, - updated_values, - new_values, - oldNameservers, - ) = self.domain.getNameserverChanges(newChanges) - - self.assertEqual(deleted_values, []) - self.assertEqual(updated_values, [("ns3.my-nameserver.gov", ["1.2.4.5"])]) - self.assertEqual(new_values, {}) - self.assertEqual( - oldNameservers, - {"ns3.my-nameserver.gov": ["1.2.3.4"]}, - ) + with less_console_noise(): + self.domain._cache["hosts"] = [ + {"name": "ns3.my-nameserver.gov", "addrs": ["1.2.3.4"]}, + ] + newChanges = [ + ("ns3.my-nameserver.gov", ["1.2.4.5"]), + ] + ( + deleted_values, + updated_values, + new_values, + oldNameservers, + ) = self.domain.getNameserverChanges(newChanges) + self.assertEqual(deleted_values, []) + self.assertEqual(updated_values, [("ns3.my-nameserver.gov", ["1.2.4.5"])]) + self.assertEqual(new_values, {}) + self.assertEqual( + oldNameservers, + {"ns3.my-nameserver.gov": ["1.2.3.4"]}, + ) def test_get_nameserver_changes_success_new_vals(self): - # Testing only creating no other changes - self.domain._cache["hosts"] = [ - {"name": "ns1.example.com", "addrs": None}, - ] - newChanges = [ - ("ns1.example.com",), - ("ns4.example.com",), - ] - ( - deleted_values, - updated_values, - new_values, - oldNameservers, - ) = self.domain.getNameserverChanges(newChanges) + with less_console_noise(): + # Testing only creating no other changes + self.domain._cache["hosts"] = [ + {"name": "ns1.example.com", "addrs": None}, + ] + newChanges = [ + ("ns1.example.com",), + ("ns4.example.com",), + ] + ( + deleted_values, + updated_values, + new_values, + oldNameservers, + ) = self.domain.getNameserverChanges(newChanges) - self.assertEqual(deleted_values, []) - self.assertEqual(updated_values, []) - self.assertEqual(new_values, {"ns4.example.com": None}) - self.assertEqual( - oldNameservers, - { - "ns1.example.com": None, - }, - ) + self.assertEqual(deleted_values, []) + self.assertEqual(updated_values, []) + self.assertEqual(new_values, {"ns4.example.com": None}) + self.assertEqual( + oldNameservers, + { + "ns1.example.com": None, + }, + ) def test_user_adds_one_nameserver(self): """ @@ -1193,32 +1154,27 @@ class TestRegistrantNameservers(MockEppLib): And `domain.is_active` returns False And domain.first_ready is null """ - - # set 1 nameserver - nameserver = "ns1.my-nameserver.com" - self.domain.nameservers = [(nameserver,)] - - # when we create a host, we should've updated at the same time - created_host = commands.CreateHost(nameserver) - update_domain_with_created = commands.UpdateDomain( - name=self.domain.name, - add=[common.HostObjSet([created_host.name])], - rem=[], - ) - - # checking if commands were sent (commands have to be sent in order) - expectedCalls = [ - call(created_host, cleaned=True), - call(update_domain_with_created, cleaned=True), - ] - - self.mockedSendFunction.assert_has_calls(expectedCalls) - - # check that status is still NOT READY - # as you have less than 2 nameservers - self.assertFalse(self.domain.is_active()) - - self.assertEqual(self.domain.first_ready, None) + with less_console_noise(): + # set 1 nameserver + nameserver = "ns1.my-nameserver.com" + self.domain.nameservers = [(nameserver,)] + # when we create a host, we should've updated at the same time + created_host = commands.CreateHost(nameserver) + update_domain_with_created = commands.UpdateDomain( + name=self.domain.name, + add=[common.HostObjSet([created_host.name])], + rem=[], + ) + # checking if commands were sent (commands have to be sent in order) + expectedCalls = [ + call(created_host, cleaned=True), + call(update_domain_with_created, cleaned=True), + ] + self.mockedSendFunction.assert_has_calls(expectedCalls) + # check that status is still NOT READY + # as you have less than 2 nameservers + self.assertFalse(self.domain.is_active()) + self.assertEqual(self.domain.first_ready, None) def test_user_adds_two_nameservers(self): """ @@ -1230,36 +1186,32 @@ class TestRegistrantNameservers(MockEppLib): And `domain.is_active` returns True And domain.first_ready is not null """ - - # set 2 nameservers - self.domain.nameservers = [(self.nameserver1,), (self.nameserver2,)] - - # when you create a host, you also have to update at same time - created_host1 = commands.CreateHost(self.nameserver1) - created_host2 = commands.CreateHost(self.nameserver2) - - update_domain_with_created = commands.UpdateDomain( - name=self.domain.name, - add=[ - common.HostObjSet([created_host1.name, created_host2.name]), - ], - rem=[], - ) - - infoDomain = commands.InfoDomain(name="my-nameserver.gov", auth_info=None) - # checking if commands were sent (commands have to be sent in order) - expectedCalls = [ - call(infoDomain, cleaned=True), - call(created_host1, cleaned=True), - call(created_host2, cleaned=True), - call(update_domain_with_created, cleaned=True), - ] - - self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) - self.assertEqual(4, self.mockedSendFunction.call_count) - # check that status is READY - self.assertTrue(self.domain.is_active()) - self.assertNotEqual(self.domain.first_ready, None) + with less_console_noise(): + # set 2 nameservers + self.domain.nameservers = [(self.nameserver1,), (self.nameserver2,)] + # when you create a host, you also have to update at same time + created_host1 = commands.CreateHost(self.nameserver1) + created_host2 = commands.CreateHost(self.nameserver2) + update_domain_with_created = commands.UpdateDomain( + name=self.domain.name, + add=[ + common.HostObjSet([created_host1.name, created_host2.name]), + ], + rem=[], + ) + infoDomain = commands.InfoDomain(name="my-nameserver.gov", auth_info=None) + # checking if commands were sent (commands have to be sent in order) + expectedCalls = [ + call(infoDomain, cleaned=True), + call(created_host1, cleaned=True), + call(created_host2, cleaned=True), + call(update_domain_with_created, cleaned=True), + ] + self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) + self.assertEqual(4, self.mockedSendFunction.call_count) + # check that status is READY + self.assertTrue(self.domain.is_active()) + self.assertNotEqual(self.domain.first_ready, None) def test_user_adds_too_many_nameservers(self): """ @@ -1268,43 +1220,41 @@ class TestRegistrantNameservers(MockEppLib): When `domain.nameservers` is set to an array of length 14 Then Domain raises a user-friendly error """ - - # set 13+ nameservers - nameserver1 = "ns1.cats-are-superior1.com" - nameserver2 = "ns1.cats-are-superior2.com" - nameserver3 = "ns1.cats-are-superior3.com" - nameserver4 = "ns1.cats-are-superior4.com" - nameserver5 = "ns1.cats-are-superior5.com" - nameserver6 = "ns1.cats-are-superior6.com" - nameserver7 = "ns1.cats-are-superior7.com" - nameserver8 = "ns1.cats-are-superior8.com" - nameserver9 = "ns1.cats-are-superior9.com" - nameserver10 = "ns1.cats-are-superior10.com" - nameserver11 = "ns1.cats-are-superior11.com" - nameserver12 = "ns1.cats-are-superior12.com" - nameserver13 = "ns1.cats-are-superior13.com" - nameserver14 = "ns1.cats-are-superior14.com" - - def _get_14_nameservers(): - self.domain.nameservers = [ - (nameserver1,), - (nameserver2,), - (nameserver3,), - (nameserver4,), - (nameserver5,), - (nameserver6,), - (nameserver7,), - (nameserver8,), - (nameserver9), - (nameserver10,), - (nameserver11,), - (nameserver12,), - (nameserver13,), - (nameserver14,), - ] - - self.assertRaises(NameserverError, _get_14_nameservers) - self.assertEqual(self.mockedSendFunction.call_count, 0) + with less_console_noise(): + # set 13+ nameservers + nameserver1 = "ns1.cats-are-superior1.com" + nameserver2 = "ns1.cats-are-superior2.com" + nameserver3 = "ns1.cats-are-superior3.com" + nameserver4 = "ns1.cats-are-superior4.com" + nameserver5 = "ns1.cats-are-superior5.com" + nameserver6 = "ns1.cats-are-superior6.com" + nameserver7 = "ns1.cats-are-superior7.com" + nameserver8 = "ns1.cats-are-superior8.com" + nameserver9 = "ns1.cats-are-superior9.com" + nameserver10 = "ns1.cats-are-superior10.com" + nameserver11 = "ns1.cats-are-superior11.com" + nameserver12 = "ns1.cats-are-superior12.com" + nameserver13 = "ns1.cats-are-superior13.com" + nameserver14 = "ns1.cats-are-superior14.com" + def _get_14_nameservers(): + self.domain.nameservers = [ + (nameserver1,), + (nameserver2,), + (nameserver3,), + (nameserver4,), + (nameserver5,), + (nameserver6,), + (nameserver7,), + (nameserver8,), + (nameserver9), + (nameserver10,), + (nameserver11,), + (nameserver12,), + (nameserver13,), + (nameserver14,), + ] + self.assertRaises(NameserverError, _get_14_nameservers) + self.assertEqual(self.mockedSendFunction.call_count, 0) def test_user_removes_some_nameservers(self): """ @@ -1315,37 +1265,36 @@ class TestRegistrantNameservers(MockEppLib): to the registry And `domain.is_active` returns True """ - - # Mock is set to return 3 nameservers on infodomain - self.domainWithThreeNS.nameservers = [(self.nameserver1,), (self.nameserver2,)] - expectedCalls = [ - # calls info domain, and info on all hosts - # to get past values - # then removes the single host and updates domain - call( - commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None), - cleaned=True, - ), - call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True), - call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True), - call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True), - call( - commands.UpdateDomain( - name=self.domainWithThreeNS.name, - add=[], - rem=[common.HostObjSet(hosts=["ns1.cats-are-superior3.com"])], - nsset=None, - keyset=None, - registrant=None, - auth_info=None, + with less_console_noise(): + # Mock is set to return 3 nameservers on infodomain + self.domainWithThreeNS.nameservers = [(self.nameserver1,), (self.nameserver2,)] + expectedCalls = [ + # calls info domain, and info on all hosts + # to get past values + # then removes the single host and updates domain + call( + commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None), + cleaned=True, ), - cleaned=True, - ), - call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True), - ] - - self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) - self.assertTrue(self.domainWithThreeNS.is_active()) + call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True), + call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True), + call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True), + call( + commands.UpdateDomain( + name=self.domainWithThreeNS.name, + add=[], + rem=[common.HostObjSet(hosts=["ns1.cats-are-superior3.com"])], + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, + ), + call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True), + ] + self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) + self.assertTrue(self.domainWithThreeNS.is_active()) def test_user_removes_too_many_nameservers(self): """ @@ -1357,41 +1306,40 @@ class TestRegistrantNameservers(MockEppLib): And `domain.is_active` returns False """ - - self.domainWithThreeNS.nameservers = [(self.nameserver1,)] - expectedCalls = [ - call( - commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None), - cleaned=True, - ), - call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True), - call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True), - call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True), - call(commands.DeleteHost(name="ns1.my-nameserver-2.com"), cleaned=True), - call( - commands.UpdateDomain( - name=self.domainWithThreeNS.name, - add=[], - rem=[ - common.HostObjSet( - hosts=[ - "ns1.my-nameserver-2.com", - "ns1.cats-are-superior3.com", - ] - ), - ], - nsset=None, - keyset=None, - registrant=None, - auth_info=None, + with less_console_noise(): + self.domainWithThreeNS.nameservers = [(self.nameserver1,)] + expectedCalls = [ + call( + commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None), + cleaned=True, ), - cleaned=True, - ), - call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True), - ] - - self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) - self.assertFalse(self.domainWithThreeNS.is_active()) + call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True), + call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True), + call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True), + call(commands.DeleteHost(name="ns1.my-nameserver-2.com"), cleaned=True), + call( + commands.UpdateDomain( + name=self.domainWithThreeNS.name, + add=[], + rem=[ + common.HostObjSet( + hosts=[ + "ns1.my-nameserver-2.com", + "ns1.cats-are-superior3.com", + ] + ), + ], + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, + ), + call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True), + ] + self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) + self.assertFalse(self.domainWithThreeNS.is_active()) def test_user_replaces_nameservers(self): """ @@ -1403,59 +1351,58 @@ class TestRegistrantNameservers(MockEppLib): And `commands.UpdateDomain` is sent to add #4 and #5 plus remove #2 and #3 And `commands.DeleteHost` is sent to delete #2 and #3 """ - self.domainWithThreeNS.nameservers = [ - (self.nameserver1,), - ("ns1.cats-are-superior1.com",), - ("ns1.cats-are-superior2.com",), - ] - - expectedCalls = [ - call( - commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None), - cleaned=True, - ), - call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True), - call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True), - call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True), - call(commands.DeleteHost(name="ns1.my-nameserver-2.com"), cleaned=True), - call( - commands.CreateHost(name="ns1.cats-are-superior1.com", addrs=[]), - cleaned=True, - ), - call( - commands.CreateHost(name="ns1.cats-are-superior2.com", addrs=[]), - cleaned=True, - ), - call( - commands.UpdateDomain( - name=self.domainWithThreeNS.name, - add=[ - common.HostObjSet( - hosts=[ - "ns1.cats-are-superior1.com", - "ns1.cats-are-superior2.com", - ] - ), - ], - rem=[ - common.HostObjSet( - hosts=[ - "ns1.my-nameserver-2.com", - "ns1.cats-are-superior3.com", - ] - ), - ], - nsset=None, - keyset=None, - registrant=None, - auth_info=None, + with less_console_noise(): + self.domainWithThreeNS.nameservers = [ + (self.nameserver1,), + ("ns1.cats-are-superior1.com",), + ("ns1.cats-are-superior2.com",), + ] + expectedCalls = [ + call( + commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None), + cleaned=True, ), - cleaned=True, - ), - ] - - self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) - self.assertTrue(self.domainWithThreeNS.is_active()) + call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True), + call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True), + call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True), + call(commands.DeleteHost(name="ns1.my-nameserver-2.com"), cleaned=True), + call( + commands.CreateHost(name="ns1.cats-are-superior1.com", addrs=[]), + cleaned=True, + ), + call( + commands.CreateHost(name="ns1.cats-are-superior2.com", addrs=[]), + cleaned=True, + ), + call( + commands.UpdateDomain( + name=self.domainWithThreeNS.name, + add=[ + common.HostObjSet( + hosts=[ + "ns1.cats-are-superior1.com", + "ns1.cats-are-superior2.com", + ] + ), + ], + rem=[ + common.HostObjSet( + hosts=[ + "ns1.my-nameserver-2.com", + "ns1.cats-are-superior3.com", + ] + ), + ], + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, + ), + ] + self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) + self.assertTrue(self.domainWithThreeNS.is_active()) def test_user_cannot_add_subordinate_without_ip(self): """ @@ -1465,11 +1412,10 @@ class TestRegistrantNameservers(MockEppLib): with a subdomain of the domain and no IP addresses Then Domain raises a user-friendly error """ - - dotgovnameserver = "my-nameserver.gov" - - with self.assertRaises(NameserverError): - self.domain.nameservers = [(dotgovnameserver,)] + with less_console_noise(): + dotgovnameserver = "my-nameserver.gov" + with self.assertRaises(NameserverError): + self.domain.nameservers = [(dotgovnameserver,)] def test_user_updates_ips(self): """ @@ -1480,46 +1426,45 @@ class TestRegistrantNameservers(MockEppLib): with a different IP address(es) Then `commands.UpdateHost` is sent to the registry """ - domain, _ = Domain.objects.get_or_create(name="nameserverwithip.gov", state=Domain.State.READY) - domain.nameservers = [ - ("ns1.nameserverwithip.gov", ["2.3.4.5", "1.2.3.4"]), - ( - "ns2.nameserverwithip.gov", - ["1.2.3.4", "2.3.4.5", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"], - ), - ("ns3.nameserverwithip.gov", ["2.3.4.5"]), - ] - - expectedCalls = [ - call( - commands.InfoDomain(name="nameserverwithip.gov", auth_info=None), - cleaned=True, - ), - call(commands.InfoHost(name="ns1.nameserverwithip.gov"), cleaned=True), - call(commands.InfoHost(name="ns2.nameserverwithip.gov"), cleaned=True), - call(commands.InfoHost(name="ns3.nameserverwithip.gov"), cleaned=True), - call( - commands.UpdateHost( - name="ns2.nameserverwithip.gov", - add=[common.Ip(addr="2001:0db8:85a3:0000:0000:8a2e:0370:7334", ip="v6")], - rem=[], - chg=None, + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="nameserverwithip.gov", state=Domain.State.READY) + domain.nameservers = [ + ("ns1.nameserverwithip.gov", ["2.3.4.5", "1.2.3.4"]), + ( + "ns2.nameserverwithip.gov", + ["1.2.3.4", "2.3.4.5", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"], ), - cleaned=True, - ), - call( - commands.UpdateHost( - name="ns3.nameserverwithip.gov", - add=[], - rem=[common.Ip(addr="1.2.3.4", ip=None)], - chg=None, + ("ns3.nameserverwithip.gov", ["2.3.4.5"]), + ] + expectedCalls = [ + call( + commands.InfoDomain(name="nameserverwithip.gov", auth_info=None), + cleaned=True, ), - cleaned=True, - ), - ] - - self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) - self.assertTrue(domain.is_active()) + call(commands.InfoHost(name="ns1.nameserverwithip.gov"), cleaned=True), + call(commands.InfoHost(name="ns2.nameserverwithip.gov"), cleaned=True), + call(commands.InfoHost(name="ns3.nameserverwithip.gov"), cleaned=True), + call( + commands.UpdateHost( + name="ns2.nameserverwithip.gov", + add=[common.Ip(addr="2001:0db8:85a3:0000:0000:8a2e:0370:7334", ip="v6")], + rem=[], + chg=None, + ), + cleaned=True, + ), + call( + commands.UpdateHost( + name="ns3.nameserverwithip.gov", + add=[], + rem=[common.Ip(addr="1.2.3.4", ip=None)], + chg=None, + ), + cleaned=True, + ), + ] + self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) + self.assertTrue(domain.is_active()) def test_user_cannot_add_non_subordinate_with_ip(self): """ @@ -1529,10 +1474,10 @@ class TestRegistrantNameservers(MockEppLib): which is not a subdomain of the domain and has IP addresses Then Domain raises a user-friendly error """ - dotgovnameserver = "mynameserverdotgov.gov" - - with self.assertRaises(NameserverError): - self.domain.nameservers = [(dotgovnameserver, ["1.2.3"])] + with less_console_noise(): + dotgovnameserver = "mynameserverdotgov.gov" + with self.assertRaises(NameserverError): + self.domain.nameservers = [(dotgovnameserver, ["1.2.3"])] def test_nameservers_are_idempotent(self): """ @@ -1541,60 +1486,60 @@ class TestRegistrantNameservers(MockEppLib): to the registry twice with identical data Then no errors are raised in Domain """ - - # Checking that it doesn't create or update even if out of order - self.domainWithThreeNS.nameservers = [ - (self.nameserver3,), - (self.nameserver1,), - (self.nameserver2,), - ] - - expectedCalls = [ - call( - commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None), - cleaned=True, - ), - call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True), - call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True), - call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True), - ] - - self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) - self.assertEqual(self.mockedSendFunction.call_count, 4) + with less_console_noise(): + # Checking that it doesn't create or update even if out of order + self.domainWithThreeNS.nameservers = [ + (self.nameserver3,), + (self.nameserver1,), + (self.nameserver2,), + ] + expectedCalls = [ + call( + commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None), + cleaned=True, + ), + call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True), + call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True), + call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True), + ] + self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) + self.assertEqual(self.mockedSendFunction.call_count, 4) def test_is_subdomain_with_no_ip(self): - domain, _ = Domain.objects.get_or_create(name="nameserversubdomain.gov", state=Domain.State.READY) - - with self.assertRaises(NameserverError): - domain.nameservers = [ - ("ns1.nameserversubdomain.gov",), - ("ns2.nameserversubdomain.gov",), - ] + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="nameserversubdomain.gov", state=Domain.State.READY) + with self.assertRaises(NameserverError): + domain.nameservers = [ + ("ns1.nameserversubdomain.gov",), + ("ns2.nameserversubdomain.gov",), + ] def test_not_subdomain_but_has_ip(self): - domain, _ = Domain.objects.get_or_create(name="nameserversubdomain.gov", state=Domain.State.READY) - - with self.assertRaises(NameserverError): - domain.nameservers = [ - ("ns1.cats-da-best.gov", ["1.2.3.4"]), - ("ns2.cats-da-best.gov", ["2.3.4.5"]), - ] + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="nameserversubdomain.gov", state=Domain.State.READY) + with self.assertRaises(NameserverError): + domain.nameservers = [ + ("ns1.cats-da-best.gov", ["1.2.3.4"]), + ("ns2.cats-da-best.gov", ["2.3.4.5"]), + ] def test_is_subdomain_but_ip_addr_not_valid(self): - domain, _ = Domain.objects.get_or_create(name="nameserversubdomain.gov", state=Domain.State.READY) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="nameserversubdomain.gov", state=Domain.State.READY) - with self.assertRaises(NameserverError): - domain.nameservers = [ - ("ns1.nameserversubdomain.gov", ["1.2.3"]), - ("ns2.nameserversubdomain.gov", ["2.3.4"]), - ] + with self.assertRaises(NameserverError): + domain.nameservers = [ + ("ns1.nameserversubdomain.gov", ["1.2.3"]), + ("ns2.nameserversubdomain.gov", ["2.3.4"]), + ] def test_setting_not_allowed(self): """Scenario: A domain state is not Ready or DNS needed then setting nameservers is not allowed""" - domain, _ = Domain.objects.get_or_create(name="onholdDomain.gov", state=Domain.State.ON_HOLD) - with self.assertRaises(ActionNotAllowed): - domain.nameservers = [self.nameserver1, self.nameserver2] + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="onholdDomain.gov", state=Domain.State.ON_HOLD) + with self.assertRaises(ActionNotAllowed): + domain.nameservers = [self.nameserver1, self.nameserver2] def test_nameserver_returns_on_registry_error(self): """ @@ -1602,28 +1547,23 @@ class TestRegistrantNameservers(MockEppLib): Registry is unavailable and throws exception when attempting to build cache from registry. Nameservers retrieved from database. """ - domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) - # set the host and host_ips directly in the database; this is normally handled through - # fetch_cache - host, _ = Host.objects.get_or_create(domain=domain, name="ns1.fake.gov") - host_ip, _ = HostIP.objects.get_or_create(host=host, address="1.1.1.1") - - # mock that registry throws an error on the InfoHost send - - def side_effect(_request, cleaned): - raise RegistryError(code=ErrorCode.COMMAND_FAILED) - - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - nameservers = domain.nameservers - - self.assertEqual(len(nameservers), 1) - self.assertEqual(nameservers[0][0], "ns1.fake.gov") - self.assertEqual(nameservers[0][1], ["1.1.1.1"]) - - patcher.stop() + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) + # set the host and host_ips directly in the database; this is normally handled through + # fetch_cache + host, _ = Host.objects.get_or_create(domain=domain, name="ns1.fake.gov") + host_ip, _ = HostIP.objects.get_or_create(host=host, address="1.1.1.1") + # mock that registry throws an error on the InfoHost send + def side_effect(_request, cleaned): + raise RegistryError(code=ErrorCode.COMMAND_FAILED) + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + nameservers = domain.nameservers + self.assertEqual(len(nameservers), 1) + self.assertEqual(nameservers[0][0], "ns1.fake.gov") + self.assertEqual(nameservers[0][1], ["1.1.1.1"]) + patcher.stop() def test_nameservers_stored_on_fetch_cache(self): """ @@ -1633,24 +1573,23 @@ class TestRegistrantNameservers(MockEppLib): of 'fake.host.com' from InfoDomain and an array of 2 IPs: 1.2.3.4 and 2.3.4.5 from InfoHost """ - domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) - - # mock the get_or_create methods for Host and HostIP - with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object( - HostIP.objects, "get_or_create" - ) as mock_host_ip_get_or_create: - # Set the return value for the mocks - mock_host_get_or_create.return_value = (Host(), True) - mock_host_ip_get_or_create.return_value = (HostIP(), True) - - # force fetch_cache to be called, which will return above documented mocked hosts - domain.nameservers - # assert that the mocks are called - mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.host.com") - # Retrieve the mocked_host from the return value of the mock - actual_mocked_host, _ = mock_host_get_or_create.return_value - mock_host_ip_get_or_create.assert_called_with(address="2.3.4.5", host=actual_mocked_host) - self.assertEqual(mock_host_ip_get_or_create.call_count, 2) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) + # mock the get_or_create methods for Host and HostIP + with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object( + HostIP.objects, "get_or_create" + ) as mock_host_ip_get_or_create: + # Set the return value for the mocks + mock_host_get_or_create.return_value = (Host(), True) + mock_host_ip_get_or_create.return_value = (HostIP(), True) + # force fetch_cache to be called, which will return above documented mocked hosts + domain.nameservers + # assert that the mocks are called + mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.host.com") + # Retrieve the mocked_host from the return value of the mock + actual_mocked_host, _ = mock_host_get_or_create.return_value + mock_host_ip_get_or_create.assert_called_with(address="2.3.4.5", host=actual_mocked_host) + self.assertEqual(mock_host_ip_get_or_create.call_count, 2) @skip("not implemented yet") def test_update_is_unsuccessful(self): @@ -1790,55 +1729,51 @@ class TestRegistrantDNSSEC(MockEppLib): ) else: return MagicMock(res_data=[self.mockDataInfoHosts]) - - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") - domain.dnssecdata = self.dnssecExtensionWithDsData - - # get the DNS SEC extension added to the UpdateDomain command and - # verify that it is properly sent - # args[0] is the _request sent to registry - args, _ = mocked_send.call_args - # assert that the extension on the update matches - self.assertEquals( - args[0].extensions[0], - self.createUpdateExtension(self.dnssecExtensionWithDsData), - ) - # test that the dnssecdata getter is functioning properly - dnssecdata_get = domain.dnssecdata - mocked_send.assert_has_calls( - [ - call( - commands.InfoDomain( - name="dnssec-dsdata.gov", + with less_console_noise(): + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") + domain.dnssecdata = self.dnssecExtensionWithDsData + # get the DNS SEC extension added to the UpdateDomain command and + # verify that it is properly sent + # args[0] is the _request sent to registry + args, _ = mocked_send.call_args + # assert that the extension on the update matches + self.assertEquals( + args[0].extensions[0], + self.createUpdateExtension(self.dnssecExtensionWithDsData), + ) + # test that the dnssecdata getter is functioning properly + dnssecdata_get = domain.dnssecdata + mocked_send.assert_has_calls( + [ + call( + commands.InfoDomain( + name="dnssec-dsdata.gov", + ), + cleaned=True, ), - cleaned=True, - ), - call( - commands.UpdateDomain( - name="dnssec-dsdata.gov", - nsset=None, - keyset=None, - registrant=None, - auth_info=None, + call( + commands.UpdateDomain( + name="dnssec-dsdata.gov", + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, ), - cleaned=True, - ), - call( - commands.InfoDomain( - name="dnssec-dsdata.gov", + call( + commands.InfoDomain( + name="dnssec-dsdata.gov", + ), + cleaned=True, ), - cleaned=True, - ), - ] - ) - - self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData) - - patcher.stop() + ] + ) + self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData) + patcher.stop() def test_dnssec_is_idempotent(self): """ @@ -1871,55 +1806,51 @@ class TestRegistrantDNSSEC(MockEppLib): ) else: return MagicMock(res_data=[self.mockDataInfoHosts]) - - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") - - # set the dnssecdata once - domain.dnssecdata = self.dnssecExtensionWithDsData - # set the dnssecdata again - domain.dnssecdata = self.dnssecExtensionWithDsData - # test that the dnssecdata getter is functioning properly - dnssecdata_get = domain.dnssecdata - mocked_send.assert_has_calls( - [ - call( - commands.InfoDomain( - name="dnssec-dsdata.gov", + with less_console_noise(): + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") + # set the dnssecdata once + domain.dnssecdata = self.dnssecExtensionWithDsData + # set the dnssecdata again + domain.dnssecdata = self.dnssecExtensionWithDsData + # test that the dnssecdata getter is functioning properly + dnssecdata_get = domain.dnssecdata + mocked_send.assert_has_calls( + [ + call( + commands.InfoDomain( + name="dnssec-dsdata.gov", + ), + cleaned=True, ), - cleaned=True, - ), - call( - commands.UpdateDomain( - name="dnssec-dsdata.gov", - nsset=None, - keyset=None, - registrant=None, - auth_info=None, + call( + commands.UpdateDomain( + name="dnssec-dsdata.gov", + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, ), - cleaned=True, - ), - call( - commands.InfoDomain( - name="dnssec-dsdata.gov", + call( + commands.InfoDomain( + name="dnssec-dsdata.gov", + ), + cleaned=True, ), - cleaned=True, - ), - call( - commands.InfoDomain( - name="dnssec-dsdata.gov", + call( + commands.InfoDomain( + name="dnssec-dsdata.gov", + ), + cleaned=True, ), - cleaned=True, - ), - ] - ) - - self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData) - - patcher.stop() + ] + ) + self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData) + patcher.stop() def test_user_adds_dnssec_data_multiple_dsdata(self): """ @@ -1948,49 +1879,45 @@ class TestRegistrantDNSSEC(MockEppLib): ) else: return MagicMock(res_data=[self.mockDataInfoHosts]) - - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - domain, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov") - - domain.dnssecdata = self.dnssecExtensionWithMultDsData - # get the DNS SEC extension added to the UpdateDomain command - # and verify that it is properly sent - # args[0] is the _request sent to registry - args, _ = mocked_send.call_args - # assert that the extension matches - self.assertEquals( - args[0].extensions[0], - self.createUpdateExtension(self.dnssecExtensionWithMultDsData), - ) - # test that the dnssecdata getter is functioning properly - dnssecdata_get = domain.dnssecdata - mocked_send.assert_has_calls( - [ - call( - commands.UpdateDomain( - name="dnssec-multdsdata.gov", - nsset=None, - keyset=None, - registrant=None, - auth_info=None, + with less_console_noise(): + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + domain, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov") + domain.dnssecdata = self.dnssecExtensionWithMultDsData + # get the DNS SEC extension added to the UpdateDomain command + # and verify that it is properly sent + # args[0] is the _request sent to registry + args, _ = mocked_send.call_args + # assert that the extension matches + self.assertEquals( + args[0].extensions[0], + self.createUpdateExtension(self.dnssecExtensionWithMultDsData), + ) + # test that the dnssecdata getter is functioning properly + dnssecdata_get = domain.dnssecdata + mocked_send.assert_has_calls( + [ + call( + commands.UpdateDomain( + name="dnssec-multdsdata.gov", + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, ), - cleaned=True, - ), - call( - commands.InfoDomain( - name="dnssec-multdsdata.gov", + call( + commands.InfoDomain( + name="dnssec-multdsdata.gov", + ), + cleaned=True, ), - cleaned=True, - ), - ] - ) - - self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithMultDsData.dsData) - - patcher.stop() + ] + ) + self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithMultDsData.dsData) + patcher.stop() def test_user_removes_dnssec_data(self): """ @@ -2020,66 +1947,64 @@ class TestRegistrantDNSSEC(MockEppLib): ) else: return MagicMock(res_data=[self.mockDataInfoHosts]) - - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") - # dnssecdata_get_initial = domain.dnssecdata # call to force initial mock - # domain._invalidate_cache() - domain.dnssecdata = self.dnssecExtensionWithDsData - domain.dnssecdata = self.dnssecExtensionRemovingDsData - # get the DNS SEC extension added to the UpdateDomain command and - # verify that it is properly sent - # args[0] is the _request sent to registry - args, _ = mocked_send.call_args - # assert that the extension on the update matches - self.assertEquals( - args[0].extensions[0], - self.createUpdateExtension( - self.dnssecExtensionWithDsData, - remove=True, - ), - ) - mocked_send.assert_has_calls( - [ - call( - commands.InfoDomain( - name="dnssec-dsdata.gov", - ), - cleaned=True, + with less_console_noise(): + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") + # dnssecdata_get_initial = domain.dnssecdata # call to force initial mock + # domain._invalidate_cache() + domain.dnssecdata = self.dnssecExtensionWithDsData + domain.dnssecdata = self.dnssecExtensionRemovingDsData + # get the DNS SEC extension added to the UpdateDomain command and + # verify that it is properly sent + # args[0] is the _request sent to registry + args, _ = mocked_send.call_args + # assert that the extension on the update matches + self.assertEquals( + args[0].extensions[0], + self.createUpdateExtension( + self.dnssecExtensionWithDsData, + remove=True, ), - call( - commands.UpdateDomain( - name="dnssec-dsdata.gov", - nsset=None, - keyset=None, - registrant=None, - auth_info=None, + ) + mocked_send.assert_has_calls( + [ + call( + commands.InfoDomain( + name="dnssec-dsdata.gov", + ), + cleaned=True, ), - cleaned=True, - ), - call( - commands.InfoDomain( - name="dnssec-dsdata.gov", + call( + commands.UpdateDomain( + name="dnssec-dsdata.gov", + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, ), - cleaned=True, - ), - call( - commands.UpdateDomain( - name="dnssec-dsdata.gov", - nsset=None, - keyset=None, - registrant=None, - auth_info=None, + call( + commands.InfoDomain( + name="dnssec-dsdata.gov", + ), + cleaned=True, ), - cleaned=True, - ), - ] - ) - - patcher.stop() + call( + commands.UpdateDomain( + name="dnssec-dsdata.gov", + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, + ), + ] + ) + patcher.stop() def test_update_is_unsuccessful(self): """ @@ -2087,12 +2012,11 @@ class TestRegistrantDNSSEC(MockEppLib): When an error is returned from epplibwrapper Then a user-friendly error message is returned for displaying on the web """ - - domain, _ = Domain.objects.get_or_create(name="dnssec-invalid.gov") - - with self.assertRaises(RegistryError) as err: - domain.dnssecdata = self.dnssecExtensionWithDsData - self.assertTrue(err.is_client_error() or err.is_session_error() or err.is_server_error()) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="dnssec-invalid.gov") + with self.assertRaises(RegistryError) as err: + domain.dnssecdata = self.dnssecExtensionWithDsData + self.assertTrue(err.is_client_error() or err.is_session_error() or err.is_server_error()) class TestExpirationDate(MockEppLib): @@ -2117,44 +2041,49 @@ class TestExpirationDate(MockEppLib): def test_expiration_date_setter_not_implemented(self): """assert that the setter for expiration date is not implemented and will raise error""" - with self.assertRaises(NotImplementedError): - self.domain.registry_expiration_date = datetime.date.today() + with less_console_noise(): + with self.assertRaises(NotImplementedError): + self.domain.registry_expiration_date = datetime.date.today() def test_renew_domain(self): """assert that the renew_domain sets new expiration date in cache and saves to registrar""" - self.domain.renew_domain() - test_date = datetime.date(2023, 5, 25) - self.assertEquals(self.domain._cache["ex_date"], test_date) - self.assertEquals(self.domain.expiration_date, test_date) + with less_console_noise(): + self.domain.renew_domain() + test_date = datetime.date(2023, 5, 25) + self.assertEquals(self.domain._cache["ex_date"], test_date) + self.assertEquals(self.domain.expiration_date, test_date) def test_renew_domain_error(self): """assert that the renew_domain raises an exception when registry raises error""" - with self.assertRaises(RegistryError): - self.domain_w_error.renew_domain() + with less_console_noise(): + with self.assertRaises(RegistryError): + self.domain_w_error.renew_domain() def test_is_expired(self): """assert that is_expired returns true for expiration_date in past""" - # force fetch_cache to be called - self.domain.statuses - self.assertTrue(self.domain.is_expired) + with less_console_noise(): + # force fetch_cache to be called + self.domain.statuses + self.assertTrue(self.domain.is_expired) def test_is_not_expired(self): """assert that is_expired returns false for expiration in future""" - # to do this, need to mock value returned from timezone.now - # set now to 2023-01-01 - mocked_datetime = datetime.datetime(2023, 1, 1, 12, 0, 0) - # force fetch_cache which sets the expiration date to 2023-05-25 - self.domain.statuses - - with patch("registrar.models.domain.timezone.now", return_value=mocked_datetime): - self.assertFalse(self.domain.is_expired()) + with less_console_noise(): + # to do this, need to mock value returned from timezone.now + # set now to 2023-01-01 + mocked_datetime = datetime.datetime(2023, 1, 1, 12, 0, 0) + # force fetch_cache which sets the expiration date to 2023-05-25 + self.domain.statuses + with patch("registrar.models.domain.timezone.now", return_value=mocked_datetime): + self.assertFalse(self.domain.is_expired()) 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) + with less_console_noise(): + # 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 TestCreationDate(MockEppLib): @@ -2169,7 +2098,7 @@ class TestCreationDate(MockEppLib): self.domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) # creation_date returned from mockDataInfoDomain with creation date: # cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35) - self.creation_date = datetime.datetime(2023, 5, 25, 19, 45, 35) + self.creation_date = make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)) def tearDown(self): Domain.objects.all().delete() @@ -2212,29 +2141,30 @@ class TestAnalystClientHold(MockEppLib): When `domain.place_client_hold()` is called Then `CLIENT_HOLD` is added to the domain's statuses """ - self.domain.place_client_hold() - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.UpdateDomain( - name="fake.gov", - add=[ - common.Status( - state=Domain.Status.CLIENT_HOLD, - description="", - lang="en", - ) - ], - nsset=None, - keyset=None, - registrant=None, - auth_info=None, - ), - cleaned=True, - ) - ] - ) - self.assertEquals(self.domain.state, Domain.State.ON_HOLD) + with less_console_noise(): + self.domain.place_client_hold() + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.UpdateDomain( + name="fake.gov", + add=[ + common.Status( + state=Domain.Status.CLIENT_HOLD, + description="", + lang="en", + ) + ], + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, + ) + ] + ) + self.assertEquals(self.domain.state, Domain.State.ON_HOLD) def test_analyst_places_client_hold_idempotent(self): """ @@ -2243,29 +2173,30 @@ class TestAnalystClientHold(MockEppLib): When `domain.place_client_hold()` is called Then Domain returns normally (without error) """ - self.domain_on_hold.place_client_hold() - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.UpdateDomain( - name="fake-on-hold.gov", - add=[ - common.Status( - state=Domain.Status.CLIENT_HOLD, - description="", - lang="en", - ) - ], - nsset=None, - keyset=None, - registrant=None, - auth_info=None, - ), - cleaned=True, - ) - ] - ) - self.assertEquals(self.domain_on_hold.state, Domain.State.ON_HOLD) + with less_console_noise(): + self.domain_on_hold.place_client_hold() + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.UpdateDomain( + name="fake-on-hold.gov", + add=[ + common.Status( + state=Domain.Status.CLIENT_HOLD, + description="", + lang="en", + ) + ], + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, + ) + ] + ) + self.assertEquals(self.domain_on_hold.state, Domain.State.ON_HOLD) def test_analyst_removes_client_hold(self): """ @@ -2274,29 +2205,30 @@ class TestAnalystClientHold(MockEppLib): When `domain.remove_client_hold()` is called Then `CLIENT_HOLD` is no longer in the domain's statuses """ - self.domain_on_hold.revert_client_hold() - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.UpdateDomain( - name="fake-on-hold.gov", - rem=[ - common.Status( - state=Domain.Status.CLIENT_HOLD, - description="", - lang="en", - ) - ], - nsset=None, - keyset=None, - registrant=None, - auth_info=None, - ), - cleaned=True, - ) - ] - ) - self.assertEquals(self.domain_on_hold.state, Domain.State.READY) + with less_console_noise(): + self.domain_on_hold.revert_client_hold() + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.UpdateDomain( + name="fake-on-hold.gov", + rem=[ + common.Status( + state=Domain.Status.CLIENT_HOLD, + description="", + lang="en", + ) + ], + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, + ) + ] + ) + self.assertEquals(self.domain_on_hold.state, Domain.State.READY) def test_analyst_removes_client_hold_idempotent(self): """ @@ -2305,29 +2237,30 @@ class TestAnalystClientHold(MockEppLib): When `domain.remove_client_hold()` is called Then Domain returns normally (without error) """ - self.domain.revert_client_hold() - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.UpdateDomain( - name="fake.gov", - rem=[ - common.Status( - state=Domain.Status.CLIENT_HOLD, - description="", - lang="en", - ) - ], - nsset=None, - keyset=None, - registrant=None, - auth_info=None, - ), - cleaned=True, - ) - ] - ) - self.assertEquals(self.domain.state, Domain.State.READY) + with less_console_noise(): + self.domain.revert_client_hold() + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.UpdateDomain( + name="fake.gov", + rem=[ + common.Status( + state=Domain.Status.CLIENT_HOLD, + description="", + lang="en", + ) + ], + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, + ) + ] + ) + self.assertEquals(self.domain.state, Domain.State.READY) def test_update_is_unsuccessful(self): """ @@ -2338,19 +2271,17 @@ class TestAnalystClientHold(MockEppLib): def side_effect(_request, cleaned): raise RegistryError(code=ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION) - - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - # if RegistryError is raised, admin formats user-friendly - # error message if error is_client_error, is_session_error, or - # is_server_error; so test for those conditions - with self.assertRaises(RegistryError) as err: - self.domain.place_client_hold() - self.assertTrue(err.is_client_error() or err.is_session_error() or err.is_server_error()) - - patcher.stop() + with less_console_noise(): + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + # if RegistryError is raised, admin formats user-friendly + # error message if error is_client_error, is_session_error, or + # is_server_error; so test for those conditions + with self.assertRaises(RegistryError) as err: + self.domain.place_client_hold() + self.assertTrue(err.is_client_error() or err.is_session_error() or err.is_server_error()) + patcher.stop() class TestAnalystLock(TestCase): @@ -2443,31 +2374,28 @@ class TestAnalystDelete(MockEppLib): The deleted date is set. """ - # Put the domain in client hold - self.domain.place_client_hold() - # Delete it... - self.domain.deletedInEpp() - self.domain.save() - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.DeleteDomain(name="fake.gov"), - cleaned=True, - ) - ] - ) - - # Domain itself should not be deleted - self.assertNotEqual(self.domain, None) - - # Domain should have the right state - self.assertEqual(self.domain.state, Domain.State.DELETED) - - # Domain should have a deleted - self.assertNotEqual(self.domain.deleted, None) - - # Cache should be invalidated - self.assertEqual(self.domain._cache, {}) + with less_console_noise(): + # Put the domain in client hold + self.domain.place_client_hold() + # Delete it... + self.domain.deletedInEpp() + self.domain.save() + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.DeleteDomain(name="fake.gov"), + cleaned=True, + ) + ] + ) + # Domain itself should not be deleted + self.assertNotEqual(self.domain, None) + # Domain should have the right state + self.assertEqual(self.domain.state, Domain.State.DELETED) + # Domain should have a deleted + self.assertNotEqual(self.domain.deleted, None) + # Cache should be invalidated + self.assertEqual(self.domain._cache, {}) def test_deletion_is_unsuccessful(self): """ @@ -2476,29 +2404,28 @@ class TestAnalystDelete(MockEppLib): Then a client error is returned of code 2305 And `state` is not set to `DELETED` """ - # Desired domain - domain, _ = Domain.objects.get_or_create(name="failDelete.gov", state=Domain.State.ON_HOLD) - # Put the domain in client hold - domain.place_client_hold() - - # Delete it - with self.assertRaises(RegistryError) as err: - domain.deletedInEpp() - domain.save() - self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION) - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.DeleteDomain(name="failDelete.gov"), - cleaned=True, - ) - ] - ) - - # Domain itself should not be deleted - self.assertNotEqual(domain, None) - # State should not have changed - self.assertEqual(domain.state, Domain.State.ON_HOLD) + with less_console_noise(): + # Desired domain + domain, _ = Domain.objects.get_or_create(name="failDelete.gov", state=Domain.State.ON_HOLD) + # Put the domain in client hold + domain.place_client_hold() + # Delete it + with self.assertRaises(RegistryError) as err: + domain.deletedInEpp() + domain.save() + self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION) + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.DeleteDomain(name="failDelete.gov"), + cleaned=True, + ) + ] + ) + # Domain itself should not be deleted + self.assertNotEqual(domain, None) + # State should not have changed + self.assertEqual(domain.state, Domain.State.ON_HOLD) def test_deletion_ready_fsm_failure(self): """ @@ -2511,15 +2438,15 @@ class TestAnalystDelete(MockEppLib): The deleted date is still null. """ - self.assertEqual(self.domain.state, Domain.State.READY) - with self.assertRaises(TransitionNotAllowed) as err: - self.domain.deletedInEpp() - self.domain.save() - self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION) - # Domain should not be deleted - self.assertNotEqual(self.domain, None) - # Domain should have the right state - self.assertEqual(self.domain.state, Domain.State.READY) - - # deleted should be null - self.assertEqual(self.domain.deleted, None) + with less_console_noise(): + self.assertEqual(self.domain.state, Domain.State.READY) + with self.assertRaises(TransitionNotAllowed) as err: + self.domain.deletedInEpp() + self.domain.save() + self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION) + # Domain should not be deleted + self.assertNotEqual(self.domain, None) + # Domain should have the right state + self.assertEqual(self.domain.state, Domain.State.READY) + # deleted should be null + self.assertEqual(self.domain.deleted, None) diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index a85fb5849..d74ebaffd 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -23,7 +23,7 @@ import boto3_mocking from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # type: ignore from datetime import date, datetime, timedelta from django.utils import timezone - +from .common import less_console_noise class CsvReportsTest(TestCase): """Tests to determine if we are uploading our reports correctly""" @@ -80,41 +80,43 @@ class CsvReportsTest(TestCase): @boto3_mocking.patching def test_generate_federal_report(self): """Ensures that we correctly generate current-federal.csv""" - mock_client = MagicMock() - fake_open = mock_open() - expected_file_content = [ - call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"), - call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"), - call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"), - ] - # We don't actually want to write anything for a test case, - # we just want to verify what is being written. - with boto3_mocking.clients.handler_for("s3", mock_client): - with patch("builtins.open", fake_open): - call_command("generate_current_federal_report", checkpath=False) - content = fake_open() + with less_console_noise(): + mock_client = MagicMock() + fake_open = mock_open() + expected_file_content = [ + call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"), + call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"), + call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"), + ] + # We don't actually want to write anything for a test case, + # we just want to verify what is being written. + with boto3_mocking.clients.handler_for("s3", mock_client): + with patch("builtins.open", fake_open): + call_command("generate_current_federal_report", checkpath=False) + content = fake_open() - content.write.assert_has_calls(expected_file_content) + content.write.assert_has_calls(expected_file_content) @boto3_mocking.patching def test_generate_full_report(self): """Ensures that we correctly generate current-full.csv""" - mock_client = MagicMock() - fake_open = mock_open() - expected_file_content = [ - call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"), - call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"), - call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"), - call("adomain2.gov,Interstate,,,,, \r\n"), - ] - # We don't actually want to write anything for a test case, - # we just want to verify what is being written. - with boto3_mocking.clients.handler_for("s3", mock_client): - with patch("builtins.open", fake_open): - call_command("generate_current_full_report", checkpath=False) - content = fake_open() + with less_console_noise(): + mock_client = MagicMock() + fake_open = mock_open() + expected_file_content = [ + call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"), + call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"), + call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"), + call("adomain2.gov,Interstate,,,,, \r\n"), + ] + # We don't actually want to write anything for a test case, + # we just want to verify what is being written. + with boto3_mocking.clients.handler_for("s3", mock_client): + with patch("builtins.open", fake_open): + call_command("generate_current_full_report", checkpath=False) + content = fake_open() - content.write.assert_has_calls(expected_file_content) + content.write.assert_has_calls(expected_file_content) @boto3_mocking.patching def test_not_found_full_report(self): @@ -122,20 +124,20 @@ class CsvReportsTest(TestCase): def side_effect(Bucket, Key): raise ClientError({"Error": {"Code": "NoSuchKey", "Message": "No such key"}}, "get_object") + with less_console_noise(): + mock_client = MagicMock() + mock_client.get_object.side_effect = side_effect - mock_client = MagicMock() - mock_client.get_object.side_effect = side_effect + response = None + with boto3_mocking.clients.handler_for("s3", mock_client): + with patch("boto3.client", return_value=mock_client): + with self.assertRaises(S3ClientError) as context: + response = self.client.get("/api/v1/get-report/current-full") + # Check that the response has status code 500 + self.assertEqual(response.status_code, 500) - response = None - with boto3_mocking.clients.handler_for("s3", mock_client): - with patch("boto3.client", return_value=mock_client): - with self.assertRaises(S3ClientError) as context: - response = self.client.get("/api/v1/get-report/current-full") - # Check that the response has status code 500 - self.assertEqual(response.status_code, 500) - - # Check that we get the right error back from the page - self.assertEqual(context.exception.code, S3ClientErrorCodes.FILE_NOT_FOUND_ERROR) + # Check that we get the right error back from the page + self.assertEqual(context.exception.code, S3ClientErrorCodes.FILE_NOT_FOUND_ERROR) @boto3_mocking.patching def test_not_found_federal_report(self): @@ -143,84 +145,86 @@ class CsvReportsTest(TestCase): def side_effect(Bucket, Key): raise ClientError({"Error": {"Code": "NoSuchKey", "Message": "No such key"}}, "get_object") + with less_console_noise(): + mock_client = MagicMock() + mock_client.get_object.side_effect = side_effect - mock_client = MagicMock() - mock_client.get_object.side_effect = side_effect + with boto3_mocking.clients.handler_for("s3", mock_client): + with patch("boto3.client", return_value=mock_client): + with self.assertRaises(S3ClientError) as context: + response = self.client.get("/api/v1/get-report/current-federal") + # Check that the response has status code 500 + self.assertEqual(response.status_code, 500) - with boto3_mocking.clients.handler_for("s3", mock_client): - with patch("boto3.client", return_value=mock_client): - with self.assertRaises(S3ClientError) as context: - response = self.client.get("/api/v1/get-report/current-federal") - # Check that the response has status code 500 - self.assertEqual(response.status_code, 500) - - # Check that we get the right error back from the page - self.assertEqual(context.exception.code, S3ClientErrorCodes.FILE_NOT_FOUND_ERROR) + # Check that we get the right error back from the page + self.assertEqual(context.exception.code, S3ClientErrorCodes.FILE_NOT_FOUND_ERROR) @boto3_mocking.patching def test_load_federal_report(self): """Tests the get_current_federal api endpoint""" - mock_client = MagicMock() - mock_client_instance = mock_client.return_value + with less_console_noise(): + mock_client = MagicMock() + mock_client_instance = mock_client.return_value - with open("registrar/tests/data/fake_current_federal.csv", "r") as file: - file_content = file.read() + with open("registrar/tests/data/fake_current_federal.csv", "r") as file: + file_content = file.read() - # Mock a recieved file - mock_client_instance.get_object.return_value = {"Body": io.BytesIO(file_content.encode())} - with boto3_mocking.clients.handler_for("s3", mock_client): - request = self.factory.get("/fake-path") - response = get_current_federal(request) + # Mock a recieved file + mock_client_instance.get_object.return_value = {"Body": io.BytesIO(file_content.encode())} + with boto3_mocking.clients.handler_for("s3", mock_client): + request = self.factory.get("/fake-path") + response = get_current_federal(request) - # Check that we are sending the correct calls. - # Ensures that we are decoding the file content recieved from AWS. - expected_call = [call.get_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key="current-federal.csv")] - mock_client_instance.assert_has_calls(expected_call) + # Check that we are sending the correct calls. + # Ensures that we are decoding the file content recieved from AWS. + expected_call = [call.get_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key="current-federal.csv")] + mock_client_instance.assert_has_calls(expected_call) - # Check that the response has status code 200 - self.assertEqual(response.status_code, 200) + # Check that the response has status code 200 + self.assertEqual(response.status_code, 200) - # Check that the response contains what we expect - expected_file_content = ( - "Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n" - "cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\n" - "ddomain3.gov,Federal,Armed Forces Retirement Home,,,," - ).encode() + # Check that the response contains what we expect + expected_file_content = ( + "Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n" + "cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\n" + "ddomain3.gov,Federal,Armed Forces Retirement Home,,,," + ).encode() - self.assertEqual(expected_file_content, response.content) + self.assertEqual(expected_file_content, response.content) @boto3_mocking.patching def test_load_full_report(self): """Tests the current-federal api link""" - mock_client = MagicMock() - mock_client_instance = mock_client.return_value + with less_console_noise(): + mock_client = MagicMock() + mock_client_instance = mock_client.return_value - with open("registrar/tests/data/fake_current_full.csv", "r") as file: - file_content = file.read() + with open("registrar/tests/data/fake_current_full.csv", "r") as file: + file_content = file.read() - # Mock a recieved file - mock_client_instance.get_object.return_value = {"Body": io.BytesIO(file_content.encode())} - with boto3_mocking.clients.handler_for("s3", mock_client): - request = self.factory.get("/fake-path") - response = get_current_full(request) + # Mock a recieved file + mock_client_instance.get_object.return_value = {"Body": io.BytesIO(file_content.encode())} + with boto3_mocking.clients.handler_for("s3", mock_client): + request = self.factory.get("/fake-path") + response = get_current_full(request) - # Check that we are sending the correct calls. - # Ensures that we are decoding the file content recieved from AWS. - expected_call = [call.get_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key="current-full.csv")] - mock_client_instance.assert_has_calls(expected_call) + # Check that we are sending the correct calls. + # Ensures that we are decoding the file content recieved from AWS. + expected_call = [call.get_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key="current-full.csv")] + mock_client_instance.assert_has_calls(expected_call) - # Check that the response has status code 200 - self.assertEqual(response.status_code, 200) + # Check that the response has status code 200 + self.assertEqual(response.status_code, 200) - # Check that the response contains what we expect - expected_file_content = ( - "Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n" - "cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\n" - "ddomain3.gov,Federal,Armed Forces Retirement Home,,,,\n" - "adomain2.gov,Interstate,,,,," - ).encode() + # Check that the response contains what we expect + expected_file_content = ( + "Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n" + "cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\n" + "ddomain3.gov,Federal,Armed Forces Retirement Home,,,,\n" + "adomain2.gov,Interstate,,,,," + ).encode() - self.assertEqual(expected_file_content, response.content) + self.assertEqual(expected_file_content, response.content) class ExportDataTest(MockEppLib): @@ -339,192 +343,170 @@ class ExportDataTest(MockEppLib): def test_export_domains_to_writer_security_emails(self): """Test that export_domains_to_writer returns the expected security email""" - - # Add security email information - self.domain_1.name = "defaultsecurity.gov" - self.domain_1.save() - - # Invoke setter - self.domain_1.security_contact - - # Invoke setter - self.domain_2.security_contact - - # Invoke setter - self.domain_3.security_contact - - # Create a CSV file in memory - csv_file = StringIO() - writer = csv.writer(csv_file) - - # Define columns, sort fields, and filter condition - columns = [ - "Domain name", - "Domain type", - "Agency", - "Organization name", - "City", - "State", - "AO", - "AO email", - "Security contact email", - "Status", - "Expiration date", - ] - sort_fields = ["domain__name"] - filter_condition = { - "domain__state__in": [ - Domain.State.READY, - Domain.State.DNS_NEEDED, - Domain.State.ON_HOLD, - ], - } - - self.maxDiff = None - # Call the export functions - write_header(writer, columns) - write_body(writer, columns, sort_fields, filter_condition) - - # Reset the CSV file's position to the beginning - csv_file.seek(0) - - # Read the content into a variable - csv_content = csv_file.read() - - # We expect READY domains, - # sorted alphabetially by domain name - expected_content = ( - "Domain name,Domain type,Agency,Organization name,City,State,AO," - "AO email,Security contact email,Status,Expiration date\n" - "adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n" - "adomain2.gov,Interstate,(blank),Dns needed\n" - "ddomain3.gov,Federal,Armed Forces Retirement Home,123@mail.gov,On hold,2023-05-25\n" - "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready" - ) - - # Normalize line endings and remove commas, - # spaces and leading/trailing whitespace - csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() - expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() - - self.assertEqual(csv_content, expected_content) + with less_console_noise(): + # Add security email information + self.domain_1.name = "defaultsecurity.gov" + self.domain_1.save() + # Invoke setter + self.domain_1.security_contact + # Invoke setter + self.domain_2.security_contact + # Invoke setter + self.domain_3.security_contact + # Create a CSV file in memory + csv_file = StringIO() + writer = csv.writer(csv_file) + # Define columns, sort fields, and filter condition + columns = [ + "Domain name", + "Domain type", + "Agency", + "Organization name", + "City", + "State", + "AO", + "AO email", + "Security contact email", + "Status", + "Expiration date", + ] + sort_fields = ["domain__name"] + filter_condition = { + "domain__state__in": [ + Domain.State.READY, + Domain.State.DNS_NEEDED, + Domain.State.ON_HOLD, + ], + } + self.maxDiff = None + # Call the export functions + write_header(writer, columns) + write_body(writer, columns, sort_fields, filter_condition) + # Reset the CSV file's position to the beginning + csv_file.seek(0) + # Read the content into a variable + csv_content = csv_file.read() + # We expect READY domains, + # sorted alphabetially by domain name + expected_content = ( + "Domain name,Domain type,Agency,Organization name,City,State,AO," + "AO email,Security contact email,Status,Expiration date\n" + "adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n" + "adomain2.gov,Interstate,(blank),Dns needed\n" + "ddomain3.gov,Federal,Armed Forces Retirement Home,123@mail.gov,On hold,2023-05-25\n" + "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready" + ) + # Normalize line endings and remove commas, + # spaces and leading/trailing whitespace + csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() + expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() + self.assertEqual(csv_content, expected_content) def test_write_body(self): """Test that write_body returns the existing domain, test that sort by domain name works, test that filter works""" - # Create a CSV file in memory - csv_file = StringIO() - writer = csv.writer(csv_file) + with less_console_noise(): + # Create a CSV file in memory + csv_file = StringIO() + writer = csv.writer(csv_file) - # Define columns, sort fields, and filter condition - columns = [ - "Domain name", - "Domain type", - "Agency", - "Organization name", - "City", - "State", - "AO", - "AO email", - "Submitter", - "Submitter title", - "Submitter email", - "Submitter phone", - "Security contact email", - "Status", - ] - sort_fields = ["domain__name"] - filter_condition = { - "domain__state__in": [ - Domain.State.READY, - Domain.State.DNS_NEEDED, - Domain.State.ON_HOLD, - ], - } - - # Call the export functions - write_header(writer, columns) - write_body(writer, columns, sort_fields, filter_condition) - - # Reset the CSV file's position to the beginning - csv_file.seek(0) - - # Read the content into a variable - csv_content = csv_file.read() - - # We expect READY domains, - # sorted alphabetially by domain name - expected_content = ( - "Domain name,Domain type,Agency,Organization name,City,State,AO," - "AO email,Submitter,Submitter title,Submitter email,Submitter phone," - "Security contact email,Status\n" - "adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n" - "adomain2.gov,Interstate,Dns needed\n" - "cdomain1.gov,Federal - Executive,World War I Centennial Commission,Ready\n" - "ddomain3.gov,Federal,Armed Forces Retirement Home,On hold\n" - ) - - # Normalize line endings and remove commas, - # spaces and leading/trailing whitespace - csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() - expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() - - self.assertEqual(csv_content, expected_content) + # Define columns, sort fields, and filter condition + columns = [ + "Domain name", + "Domain type", + "Agency", + "Organization name", + "City", + "State", + "AO", + "AO email", + "Submitter", + "Submitter title", + "Submitter email", + "Submitter phone", + "Security contact email", + "Status", + ] + sort_fields = ["domain__name"] + filter_condition = { + "domain__state__in": [ + Domain.State.READY, + Domain.State.DNS_NEEDED, + Domain.State.ON_HOLD, + ], + } + # Call the export functions + write_header(writer, columns) + write_body(writer, columns, sort_fields, filter_condition) + # Reset the CSV file's position to the beginning + csv_file.seek(0) + # Read the content into a variable + csv_content = csv_file.read() + # We expect READY domains, + # sorted alphabetially by domain name + expected_content = ( + "Domain name,Domain type,Agency,Organization name,City,State,AO," + "AO email,Submitter,Submitter title,Submitter email,Submitter phone," + "Security contact email,Status\n" + "adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n" + "adomain2.gov,Interstate,Dns needed\n" + "cdomain1.gov,Federal - Executive,World War I Centennial Commission,Ready\n" + "ddomain3.gov,Federal,Armed Forces Retirement Home,On hold\n" + ) + # Normalize line endings and remove commas, + # spaces and leading/trailing whitespace + csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() + expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() + self.assertEqual(csv_content, expected_content) def test_write_body_additional(self): """An additional test for filters and multi-column sort""" - # Create a CSV file in memory - csv_file = StringIO() - writer = csv.writer(csv_file) - - # Define columns, sort fields, and filter condition - columns = [ - "Domain name", - "Domain type", - "Agency", - "Organization name", - "City", - "State", - "Security contact email", - ] - sort_fields = ["domain__name", "federal_agency", "organization_type"] - filter_condition = { - "organization_type__icontains": "federal", - "domain__state__in": [ - Domain.State.READY, - Domain.State.DNS_NEEDED, - Domain.State.ON_HOLD, - ], - } - - # Call the export functions - write_header(writer, columns) - write_body(writer, columns, sort_fields, filter_condition) - - # Reset the CSV file's position to the beginning - csv_file.seek(0) - - # Read the content into a variable - csv_content = csv_file.read() - - # We expect READY domains, - # federal only - # sorted alphabetially by domain name - expected_content = ( - "Domain name,Domain type,Agency,Organization name,City," - "State,Security contact email\n" - "adomain10.gov,Federal,Armed Forces Retirement Home\n" - "cdomain1.gov,Federal - Executive,World War I Centennial Commission\n" - "ddomain3.gov,Federal,Armed Forces Retirement Home\n" - ) - - # Normalize line endings and remove commas, - # spaces and leading/trailing whitespace - csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() - expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() - - self.assertEqual(csv_content, expected_content) + with less_console_noise(): + # Create a CSV file in memory + csv_file = StringIO() + writer = csv.writer(csv_file) + # Define columns, sort fields, and filter condition + columns = [ + "Domain name", + "Domain type", + "Agency", + "Organization name", + "City", + "State", + "Security contact email", + ] + sort_fields = ["domain__name", "federal_agency", "organization_type"] + filter_condition = { + "organization_type__icontains": "federal", + "domain__state__in": [ + Domain.State.READY, + Domain.State.DNS_NEEDED, + Domain.State.ON_HOLD, + ], + } + # Call the export functions + write_header(writer, columns) + write_body(writer, columns, sort_fields, filter_condition) + # Reset the CSV file's position to the beginning + csv_file.seek(0) + # Read the content into a variable + csv_content = csv_file.read() + # We expect READY domains, + # federal only + # sorted alphabetially by domain name + expected_content = ( + "Domain name,Domain type,Agency,Organization name,City," + "State,Security contact email\n" + "adomain10.gov,Federal,Armed Forces Retirement Home\n" + "cdomain1.gov,Federal - Executive,World War I Centennial Commission\n" + "ddomain3.gov,Federal,Armed Forces Retirement Home\n" + ) + # Normalize line endings and remove commas, + # spaces and leading/trailing whitespace + csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() + expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() + self.assertEqual(csv_content, expected_content) def test_write_body_with_date_filter_pulls_domains_in_range(self): """Test that domains that are @@ -538,88 +520,88 @@ class ExportDataTest(MockEppLib): which are hard to mock. TODO: Simplify is created_at is not needed for the report.""" + with less_console_noise(): + # Create a CSV file in memory + csv_file = StringIO() + writer = csv.writer(csv_file) + # We use timezone.make_aware to sync to server time a datetime object with the current date (using date.today()) + # and a specific time (using datetime.min.time()). + end_date = timezone.make_aware(datetime.combine(date.today() + timedelta(days=2), datetime.min.time())) + start_date = timezone.make_aware(datetime.combine(date.today() - timedelta(days=2), datetime.min.time())) - # Create a CSV file in memory - csv_file = StringIO() - writer = csv.writer(csv_file) - # We use timezone.make_aware to sync to server time a datetime object with the current date (using date.today()) - # and a specific time (using datetime.min.time()). - end_date = timezone.make_aware(datetime.combine(date.today() + timedelta(days=2), datetime.min.time())) - start_date = timezone.make_aware(datetime.combine(date.today() - timedelta(days=2), datetime.min.time())) + # Define columns, sort fields, and filter condition + columns = [ + "Domain name", + "Domain type", + "Agency", + "Organization name", + "City", + "State", + "Status", + "Expiration date", + ] + sort_fields = [ + "created_at", + "domain__name", + ] + sort_fields_for_deleted_domains = [ + "domain__deleted", + "domain__name", + ] + filter_condition = { + "domain__state__in": [ + Domain.State.READY, + ], + "domain__first_ready__lte": end_date, + "domain__first_ready__gte": start_date, + } + filter_conditions_for_deleted_domains = { + "domain__state__in": [ + Domain.State.DELETED, + ], + "domain__deleted__lte": end_date, + "domain__deleted__gte": start_date, + } - # Define columns, sort fields, and filter condition - columns = [ - "Domain name", - "Domain type", - "Agency", - "Organization name", - "City", - "State", - "Status", - "Expiration date", - ] - sort_fields = [ - "created_at", - "domain__name", - ] - sort_fields_for_deleted_domains = [ - "domain__deleted", - "domain__name", - ] - filter_condition = { - "domain__state__in": [ - Domain.State.READY, - ], - "domain__first_ready__lte": end_date, - "domain__first_ready__gte": start_date, - } - filter_conditions_for_deleted_domains = { - "domain__state__in": [ - Domain.State.DELETED, - ], - "domain__deleted__lte": end_date, - "domain__deleted__gte": start_date, - } + # Call the export functions + write_header(writer, columns) + write_body( + writer, + columns, + sort_fields, + filter_condition, + ) + write_body( + writer, + columns, + sort_fields_for_deleted_domains, + filter_conditions_for_deleted_domains, + ) - # Call the export functions - write_header(writer, columns) - write_body( - writer, - columns, - sort_fields, - filter_condition, - ) - write_body( - writer, - columns, - sort_fields_for_deleted_domains, - filter_conditions_for_deleted_domains, - ) + # Reset the CSV file's position to the beginning + csv_file.seek(0) - # Reset the CSV file's position to the beginning - csv_file.seek(0) + # Read the content into a variable + csv_content = csv_file.read() - # Read the content into a variable - csv_content = csv_file.read() + # We expect READY domains first, created between today-2 and today+2, sorted by created_at then name + # and DELETED domains deleted between today-2 and today+2, sorted by deleted then name + expected_content = ( + "Domain name,Domain type,Agency,Organization name,City," + "State,Status,Expiration date\n" + "cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,\n" + "adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,\n" + "zdomain9.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n" + "sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n" + "xdomain7.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n" + ) - # We expect READY domains first, created between today-2 and today+2, sorted by created_at then name - # and DELETED domains deleted between today-2 and today+2, sorted by deleted then name - expected_content = ( - "Domain name,Domain type,Agency,Organization name,City," - "State,Status,Expiration date\n" - "cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,\n" - "adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,\n" - "zdomain9.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n" - "sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n" - "xdomain7.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n" - ) + # Normalize line endings and remove commas, + # spaces and leading/trailing whitespace + csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() + expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() - # Normalize line endings and remove commas, - # spaces and leading/trailing whitespace - csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() - expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() - - self.assertEqual(csv_content, expected_content) + self.assertEqual(csv_content, expected_content) class HelperFunctions(TestCase): diff --git a/src/registrar/tests/test_transition_domain_migrations.py b/src/registrar/tests/test_transition_domain_migrations.py index be4619e0b..d419e6fcd 100644 --- a/src/registrar/tests/test_transition_domain_migrations.py +++ b/src/registrar/tests/test_transition_domain_migrations.py @@ -20,7 +20,9 @@ from registrar.models.contact import Contact from .common import MockSESClient, less_console_noise import boto3_mocking # type: ignore +import logging +logger = logging.getLogger(__name__) class TestProcessedMigrations(TestCase): """This test case class is designed to verify the idempotency of migrations @@ -55,17 +57,18 @@ class TestProcessedMigrations(TestCase): The 'call_command' function from Django's management framework is then used to execute the load_transition_domain command with the specified arguments. """ - # noqa here because splitting this up makes it confusing. - # ES501 - with patch( - "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa - return_value=True, - ): - call_command( - "load_transition_domain", - self.migration_json_filename, - directory=self.test_data_file_location, - ) + with less_console_noise(): + # noqa here because splitting this up makes it confusing. + # ES501 + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + call_command( + "load_transition_domain", + self.migration_json_filename, + directory=self.test_data_file_location, + ) def run_transfer_domains(self): """ @@ -74,101 +77,104 @@ class TestProcessedMigrations(TestCase): The 'call_command' function from Django's management framework is then used to execute the load_transition_domain command with the specified arguments. """ - call_command("transfer_transition_domains_to_domains") + with less_console_noise(): + call_command("transfer_transition_domains_to_domains") def test_domain_idempotent(self): """ This test ensures that the domain transfer process is idempotent on Domain and DomainInformation. """ - unchanged_domain, _ = Domain.objects.get_or_create( - name="testdomain.gov", - state=Domain.State.READY, - expiration_date=datetime.date(2000, 1, 1), - ) - unchanged_domain_information, _ = DomainInformation.objects.get_or_create( - domain=unchanged_domain, organization_name="test org name", creator=self.user - ) - self.run_load_domains() + with less_console_noise(): + unchanged_domain, _ = Domain.objects.get_or_create( + name="testdomain.gov", + state=Domain.State.READY, + expiration_date=datetime.date(2000, 1, 1), + ) + unchanged_domain_information, _ = DomainInformation.objects.get_or_create( + domain=unchanged_domain, organization_name="test org name", creator=self.user + ) + self.run_load_domains() - # Test that a given TransitionDomain isn't set to "processed" - transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") - self.assertFalse(transition_domain_object.processed) + # Test that a given TransitionDomain isn't set to "processed" + transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") + self.assertFalse(transition_domain_object.processed) - self.run_transfer_domains() + self.run_transfer_domains() - # Test that old data isn't corrupted - actual_unchanged = Domain.objects.filter(name="testdomain.gov").get() - actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get() - self.assertEqual(unchanged_domain, actual_unchanged) - self.assertEqual(unchanged_domain_information, actual_unchanged_information) + # Test that old data isn't corrupted + actual_unchanged = Domain.objects.filter(name="testdomain.gov").get() + actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get() + self.assertEqual(unchanged_domain, actual_unchanged) + self.assertEqual(unchanged_domain_information, actual_unchanged_information) - # Test that a given TransitionDomain is set to "processed" after we transfer domains - transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") - self.assertTrue(transition_domain_object.processed) + # Test that a given TransitionDomain is set to "processed" after we transfer domains + transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") + self.assertTrue(transition_domain_object.processed) - # Manually change Domain/DomainInformation objects - changed_domain = Domain.objects.filter(name="fakewebsite3.gov").get() - changed_domain.expiration_date = datetime.date(1999, 1, 1) + # Manually change Domain/DomainInformation objects + changed_domain = Domain.objects.filter(name="fakewebsite3.gov").get() + changed_domain.expiration_date = datetime.date(1999, 1, 1) - changed_domain.save() + changed_domain.save() - changed_domain_information = DomainInformation.objects.filter(domain=changed_domain).get() - changed_domain_information.organization_name = "changed" + changed_domain_information = DomainInformation.objects.filter(domain=changed_domain).get() + changed_domain_information.organization_name = "changed" - changed_domain_information.save() + changed_domain_information.save() - # Rerun transfer domains - self.run_transfer_domains() + # Rerun transfer domains + self.run_transfer_domains() - # Test that old data isn't corrupted after running this twice - actual_unchanged = Domain.objects.filter(name="testdomain.gov").get() - actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get() - self.assertEqual(unchanged_domain, actual_unchanged) - self.assertEqual(unchanged_domain_information, actual_unchanged_information) + # Test that old data isn't corrupted after running this twice + actual_unchanged = Domain.objects.filter(name="testdomain.gov").get() + actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get() + self.assertEqual(unchanged_domain, actual_unchanged) + self.assertEqual(unchanged_domain_information, actual_unchanged_information) - # Ensure that domain hasn't changed - actual_domain = Domain.objects.filter(name="fakewebsite3.gov").get() - self.assertEqual(changed_domain, actual_domain) + # Ensure that domain hasn't changed + actual_domain = Domain.objects.filter(name="fakewebsite3.gov").get() + self.assertEqual(changed_domain, actual_domain) - # Ensure that DomainInformation hasn't changed - actual_domain_information = DomainInformation.objects.filter(domain=changed_domain).get() - self.assertEqual(changed_domain_information, actual_domain_information) + # Ensure that DomainInformation hasn't changed + actual_domain_information = DomainInformation.objects.filter(domain=changed_domain).get() + self.assertEqual(changed_domain_information, actual_domain_information) def test_transition_domain_is_processed(self): """ This test checks if a domain is correctly marked as processed in the transition. """ - old_transition_domain, _ = TransitionDomain.objects.get_or_create(domain_name="testdomain.gov") - # Asser that old records default to 'True' - self.assertTrue(old_transition_domain.processed) + with less_console_noise(): + old_transition_domain, _ = TransitionDomain.objects.get_or_create(domain_name="testdomain.gov") + # Asser that old records default to 'True' + self.assertTrue(old_transition_domain.processed) - unchanged_domain, _ = Domain.objects.get_or_create( - name="testdomain.gov", - state=Domain.State.READY, - expiration_date=datetime.date(2000, 1, 1), - ) - unchanged_domain_information, _ = DomainInformation.objects.get_or_create( - domain=unchanged_domain, organization_name="test org name", creator=self.user - ) - self.run_load_domains() + unchanged_domain, _ = Domain.objects.get_or_create( + name="testdomain.gov", + state=Domain.State.READY, + expiration_date=datetime.date(2000, 1, 1), + ) + unchanged_domain_information, _ = DomainInformation.objects.get_or_create( + domain=unchanged_domain, organization_name="test org name", creator=self.user + ) + self.run_load_domains() - # Test that a given TransitionDomain isn't set to "processed" - transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") - self.assertFalse(transition_domain_object.processed) + # Test that a given TransitionDomain isn't set to "processed" + transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") + self.assertFalse(transition_domain_object.processed) - self.run_transfer_domains() + self.run_transfer_domains() - # Test that old data isn't corrupted - actual_unchanged = Domain.objects.filter(name="testdomain.gov").get() - actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get() - self.assertEqual(unchanged_domain, actual_unchanged) - self.assertTrue(old_transition_domain.processed) - self.assertEqual(unchanged_domain_information, actual_unchanged_information) + # Test that old data isn't corrupted + actual_unchanged = Domain.objects.filter(name="testdomain.gov").get() + actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get() + self.assertEqual(unchanged_domain, actual_unchanged) + self.assertTrue(old_transition_domain.processed) + self.assertEqual(unchanged_domain_information, actual_unchanged_information) - # Test that a given TransitionDomain is set to "processed" after we transfer domains - transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") - self.assertTrue(transition_domain_object.processed) + # Test that a given TransitionDomain is set to "processed" after we transfer domains + transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") + self.assertTrue(transition_domain_object.processed) class TestOrganizationMigration(TestCase): @@ -200,17 +206,18 @@ class TestOrganizationMigration(TestCase): The 'call_command' function from Django's management framework is then used to execute the load_transition_domain command with the specified arguments. """ - # noqa here because splitting this up makes it confusing. - # ES501 - with patch( - "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa - return_value=True, - ): - call_command( - "load_transition_domain", - self.migration_json_filename, - directory=self.test_data_file_location, - ) + with less_console_noise(): + # noqa here because splitting this up makes it confusing. + # ES501 + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + call_command( + "load_transition_domain", + self.migration_json_filename, + directory=self.test_data_file_location, + ) def run_transfer_domains(self): """ @@ -219,7 +226,8 @@ class TestOrganizationMigration(TestCase): The 'call_command' function from Django's management framework is then used to execute the load_transition_domain command with the specified arguments. """ - call_command("transfer_transition_domains_to_domains") + with less_console_noise(): + call_command("transfer_transition_domains_to_domains") def run_load_organization_data(self): """ @@ -232,17 +240,18 @@ class TestOrganizationMigration(TestCase): The 'call_command' function from Django's management framework is then used to execute the load_organization_data command with the specified arguments. """ + with less_console_noise(): # noqa here (E501) because splitting this up makes it # confusing to read. - with patch( - "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa - return_value=True, - ): - call_command( - "load_organization_data", - self.migration_json_filename, - directory=self.test_data_file_location, - ) + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + call_command( + "load_organization_data", + self.migration_json_filename, + directory=self.test_data_file_location, + ) def compare_tables( self, @@ -256,7 +265,6 @@ class TestOrganizationMigration(TestCase): """Does a diff between the transition_domain and the following tables: domain, domain_information and the domain_invitation. Verifies that the data loaded correctly.""" - missing_domains = [] duplicate_domains = [] missing_domain_informations = [] @@ -301,58 +309,62 @@ class TestOrganizationMigration(TestCase): The expected result is a set of TransitionDomain objects with specific attributes. The test fetches the actual TransitionDomain objects from the database and compares them with the expected objects. - """ # noqa - E501 (harder to read) - # == First, parse all existing data == # - self.run_load_domains() - self.run_transfer_domains() + """ + with less_console_noise(): + # noqa - E501 (harder to read) + # == First, parse all existing data == # + self.run_load_domains() + self.run_transfer_domains() - # == Second, try adding org data to it == # - self.run_load_organization_data() + # == Second, try adding org data to it == # + self.run_load_organization_data() - # == Third, test that we've loaded data as we expect == # - transition_domains = TransitionDomain.objects.filter(domain_name="fakewebsite2.gov") + # == Third, test that we've loaded data as we expect == # + transition_domains = TransitionDomain.objects.filter(domain_name="fakewebsite2.gov") - # Should return three objects (three unique emails) - self.assertEqual(transition_domains.count(), 3) + # Should return three objects (three unique emails) + self.assertEqual(transition_domains.count(), 3) - # Lets test the first one - transition = transition_domains.first() - expected_transition_domain = TransitionDomain( - username="alexandra.bobbitt5@test.com", - domain_name="fakewebsite2.gov", - status="on hold", - email_sent=True, - organization_type="Federal", - organization_name="Fanoodle", - federal_type="Executive", - federal_agency="Department of Commerce", - epp_creation_date=datetime.date(2004, 5, 7), - epp_expiration_date=datetime.date(2023, 9, 30), - first_name="Seline", - middle_name="testmiddle2", - last_name="Tower", - title=None, - email="stower3@answers.com", - phone="151-539-6028", - address_line="93001 Arizona Drive", - city="Columbus", - state_territory="Oh", - zipcode="43268", - ) - expected_transition_domain.id = transition.id + # Lets test the first one + transition = transition_domains.first() + expected_transition_domain = TransitionDomain( + username="alexandra.bobbitt5@test.com", + domain_name="fakewebsite2.gov", + status="on hold", + email_sent=True, + organization_type="Federal", + organization_name="Fanoodle", + federal_type="Executive", + federal_agency="Department of Commerce", + epp_creation_date=datetime.date(2004, 5, 7), + epp_expiration_date=datetime.date(2023, 9, 30), + first_name="Seline", + middle_name="testmiddle2", + last_name="Tower", + title=None, + email="stower3@answers.com", + phone="151-539-6028", + address_line="93001 Arizona Drive", + city="Columbus", + state_territory="Oh", + zipcode="43268", + ) + expected_transition_domain.id = transition.id - self.assertEqual(transition, expected_transition_domain) + self.assertEqual(transition, expected_transition_domain) def test_transition_domain_status_unknown(self): """ Test that a domain in unknown status can be loaded - """ # noqa - E501 (harder to read) - # == First, parse all existing data == # - self.run_load_domains() - self.run_transfer_domains() + """ + with less_console_noise(): + # noqa - E501 (harder to read) + # == First, parse all existing data == # + self.run_load_domains() + self.run_transfer_domains() - domain_object = Domain.objects.get(name="fakewebsite3.gov") - self.assertEqual(domain_object.state, Domain.State.UNKNOWN) + domain_object = Domain.objects.get(name="fakewebsite3.gov") + self.assertEqual(domain_object.state, Domain.State.UNKNOWN) def test_load_organization_data_domain_information(self): """ @@ -367,35 +379,36 @@ class TestOrganizationMigration(TestCase): The test fetches the actual DomainInformation object from the database and compares it with the expected object. """ - # == First, parse all existing data == # - self.run_load_domains() - self.run_transfer_domains() + with less_console_noise(): + # == First, parse all existing data == # + self.run_load_domains() + self.run_transfer_domains() - # == Second, try adding org data to it == # - self.run_load_organization_data() + # == Second, try adding org data to it == # + self.run_load_organization_data() - # == Third, test that we've loaded data as we expect == # - _domain = Domain.objects.filter(name="fakewebsite2.gov").get() - domain_information = DomainInformation.objects.filter(domain=_domain).get() + # == Third, test that we've loaded data as we expect == # + _domain = Domain.objects.filter(name="fakewebsite2.gov").get() + domain_information = DomainInformation.objects.filter(domain=_domain).get() - expected_creator = User.objects.filter(username="System").get() - expected_ao = Contact.objects.filter(first_name="Seline", middle_name="testmiddle2", last_name="Tower").get() - expected_domain_information = DomainInformation( - creator=expected_creator, - organization_type="federal", - federal_agency="Department of Commerce", - federal_type="executive", - organization_name="Fanoodle", - address_line1="93001 Arizona Drive", - city="Columbus", - state_territory="Oh", - zipcode="43268", - authorizing_official=expected_ao, - domain=_domain, - ) - # Given that these are different objects, this needs to be set - expected_domain_information.id = domain_information.id - self.assertEqual(domain_information, expected_domain_information) + expected_creator = User.objects.filter(username="System").get() + expected_ao = Contact.objects.filter(first_name="Seline", middle_name="testmiddle2", last_name="Tower").get() + expected_domain_information = DomainInformation( + creator=expected_creator, + organization_type="federal", + federal_agency="Department of Commerce", + federal_type="executive", + organization_name="Fanoodle", + address_line1="93001 Arizona Drive", + city="Columbus", + state_territory="Oh", + zipcode="43268", + authorizing_official=expected_ao, + domain=_domain, + ) + # Given that these are different objects, this needs to be set + expected_domain_information.id = domain_information.id + self.assertEqual(domain_information, expected_domain_information) def test_load_organization_data_preserves_existing_data(self): """ @@ -410,44 +423,45 @@ class TestOrganizationMigration(TestCase): The expected result is that the DomainInformation object retains its pre-existing data after the load_organization_data method is run. """ - # == First, parse all existing data == # - self.run_load_domains() - self.run_transfer_domains() + with less_console_noise(): + # == First, parse all existing data == # + self.run_load_domains() + self.run_transfer_domains() - # == Second, try add prexisting fake data == # - _domain_old = Domain.objects.filter(name="fakewebsite2.gov").get() - domain_information_old = DomainInformation.objects.filter(domain=_domain_old).get() - domain_information_old.address_line1 = "93001 Galactic Way" - domain_information_old.city = "Olympus" - domain_information_old.state_territory = "MA" - domain_information_old.zipcode = "12345" - domain_information_old.save() + # == Second, try add prexisting fake data == # + _domain_old = Domain.objects.filter(name="fakewebsite2.gov").get() + domain_information_old = DomainInformation.objects.filter(domain=_domain_old).get() + domain_information_old.address_line1 = "93001 Galactic Way" + domain_information_old.city = "Olympus" + domain_information_old.state_territory = "MA" + domain_information_old.zipcode = "12345" + domain_information_old.save() - # == Third, try running the script == # - self.run_load_organization_data() + # == Third, try running the script == # + self.run_load_organization_data() - # == Fourth, test that no data is overwritten as we expect == # - _domain = Domain.objects.filter(name="fakewebsite2.gov").get() - domain_information = DomainInformation.objects.filter(domain=_domain).get() + # == Fourth, test that no data is overwritten as we expect == # + _domain = Domain.objects.filter(name="fakewebsite2.gov").get() + domain_information = DomainInformation.objects.filter(domain=_domain).get() - expected_creator = User.objects.filter(username="System").get() - expected_ao = Contact.objects.filter(first_name="Seline", middle_name="testmiddle2", last_name="Tower").get() - expected_domain_information = DomainInformation( - creator=expected_creator, - organization_type="federal", - federal_agency="Department of Commerce", - federal_type="executive", - organization_name="Fanoodle", - address_line1="93001 Galactic Way", - city="Olympus", - state_territory="MA", - zipcode="12345", - authorizing_official=expected_ao, - domain=_domain, - ) - # Given that these are different objects, this needs to be set - expected_domain_information.id = domain_information.id - self.assertEqual(domain_information, expected_domain_information) + expected_creator = User.objects.filter(username="System").get() + expected_ao = Contact.objects.filter(first_name="Seline", middle_name="testmiddle2", last_name="Tower").get() + expected_domain_information = DomainInformation( + creator=expected_creator, + organization_type="federal", + federal_agency="Department of Commerce", + federal_type="executive", + organization_name="Fanoodle", + address_line1="93001 Galactic Way", + city="Olympus", + state_territory="MA", + zipcode="12345", + authorizing_official=expected_ao, + domain=_domain, + ) + # Given that these are different objects, this needs to be set + expected_domain_information.id = domain_information.id + self.assertEqual(domain_information, expected_domain_information) def test_load_organization_data_integrity(self): """ @@ -462,29 +476,30 @@ class TestOrganizationMigration(TestCase): The expected result is that the counts of objects in the database match the expected counts, indicating that the data has not been corrupted. """ - # First, parse all existing data - self.run_load_domains() - self.run_transfer_domains() + with less_console_noise(): + # First, parse all existing data + self.run_load_domains() + self.run_transfer_domains() - # Second, try adding org data to it - self.run_load_organization_data() + # Second, try adding org data to it + self.run_load_organization_data() - # Third, test that we didn't corrupt any data - expected_total_transition_domains = 9 - expected_total_domains = 5 - expected_total_domain_informations = 5 + # Third, test that we didn't corrupt any data + expected_total_transition_domains = 9 + expected_total_domains = 5 + expected_total_domain_informations = 5 - expected_missing_domains = 0 - expected_duplicate_domains = 0 - expected_missing_domain_informations = 0 - self.compare_tables( - expected_total_transition_domains, - expected_total_domains, - expected_total_domain_informations, - expected_missing_domains, - expected_duplicate_domains, - expected_missing_domain_informations, - ) + expected_missing_domains = 0 + expected_duplicate_domains = 0 + expected_missing_domain_informations = 0 + self.compare_tables( + expected_total_transition_domains, + expected_total_domains, + expected_total_domain_informations, + expected_missing_domains, + expected_duplicate_domains, + expected_missing_domain_informations, + ) class TestMigrations(TestCase): @@ -521,39 +536,42 @@ class TestMigrations(TestCase): UserDomainRole.objects.all().delete() def run_load_domains(self): - # noqa here because splitting this up makes it confusing. - # ES501 - with patch( - "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa - return_value=True, - ): - call_command( - "load_transition_domain", - self.migration_json_filename, - directory=self.test_data_file_location, - ) - - def run_transfer_domains(self): - call_command("transfer_transition_domains_to_domains") - - def run_master_script(self): - # noqa here (E501) because splitting this up makes it - # confusing to read. - mock_client = MockSESClient() - with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + # noqa here because splitting this up makes it confusing. + # ES501 with patch( "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa return_value=True, ): - with patch("registrar.utility.email.send_templated_email", return_value=None): - call_command( - "master_domain_migrations", - runMigrations=True, - migrationDirectory=self.test_data_file_location, - migrationJSON=self.migration_json_filename, - disablePrompts=True, - ) - print(f"here: {mock_client.EMAILS_SENT}") + call_command( + "load_transition_domain", + self.migration_json_filename, + directory=self.test_data_file_location, + ) + + def run_transfer_domains(self): + with less_console_noise(): + call_command("transfer_transition_domains_to_domains") + + def run_master_script(self): + with less_console_noise(): + # noqa here (E501) because splitting this up makes it + # confusing to read. + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + with patch("registrar.utility.email.send_templated_email", return_value=None): + call_command( + "master_domain_migrations", + runMigrations=True, + migrationDirectory=self.test_data_file_location, + migrationJSON=self.migration_json_filename, + disablePrompts=True, + ) + logger.debug(f"here: {mock_client.EMAILS_SENT}") def compare_tables( self, @@ -607,7 +625,7 @@ class TestMigrations(TestCase): total_domain_informations = len(DomainInformation.objects.all()) total_domain_invitations = len(DomainInvitation.objects.all()) - print( + logger.debug( f""" total_missing_domains = {len(missing_domains)} total_duplicate_domains = {len(duplicate_domains)} @@ -636,225 +654,230 @@ class TestMigrations(TestCase): follow best practice of limiting the number of assertions per test. But for now, this will double-check that the script works as intended.""" + with less_console_noise(): + self.run_master_script() - self.run_master_script() + # STEP 2: (analyze the tables just like the + # migration script does, but add assert statements) + expected_total_transition_domains = 9 + expected_total_domains = 5 + expected_total_domain_informations = 5 + expected_total_domain_invitations = 8 - # STEP 2: (analyze the tables just like the - # migration script does, but add assert statements) - expected_total_transition_domains = 9 - expected_total_domains = 5 - expected_total_domain_informations = 5 - expected_total_domain_invitations = 8 - - expected_missing_domains = 0 - expected_duplicate_domains = 0 - expected_missing_domain_informations = 0 - # we expect 1 missing invite from anomaly.gov (an injected error) - expected_missing_domain_invitations = 1 - self.compare_tables( - expected_total_transition_domains, - expected_total_domains, - expected_total_domain_informations, - expected_total_domain_invitations, - expected_missing_domains, - expected_duplicate_domains, - expected_missing_domain_informations, - expected_missing_domain_invitations, - ) + expected_missing_domains = 0 + expected_duplicate_domains = 0 + expected_missing_domain_informations = 0 + # we expect 1 missing invite from anomaly.gov (an injected error) + expected_missing_domain_invitations = 1 + self.compare_tables( + expected_total_transition_domains, + expected_total_domains, + expected_total_domain_informations, + expected_total_domain_invitations, + expected_missing_domains, + expected_duplicate_domains, + expected_missing_domain_informations, + expected_missing_domain_invitations, + ) def test_load_empty_transition_domain(self): """Loads TransitionDomains without additional data""" - self.run_load_domains() + with less_console_noise(): + self.run_load_domains() - # STEP 2: (analyze the tables just like the migration - # script does, but add assert statements) - expected_total_transition_domains = 9 - expected_total_domains = 0 - expected_total_domain_informations = 0 - expected_total_domain_invitations = 0 + # STEP 2: (analyze the tables just like the migration + # script does, but add assert statements) + expected_total_transition_domains = 9 + expected_total_domains = 0 + expected_total_domain_informations = 0 + expected_total_domain_invitations = 0 - expected_missing_domains = 9 - expected_duplicate_domains = 0 - expected_missing_domain_informations = 9 - expected_missing_domain_invitations = 9 - self.compare_tables( - expected_total_transition_domains, - expected_total_domains, - expected_total_domain_informations, - expected_total_domain_invitations, - expected_missing_domains, - expected_duplicate_domains, - expected_missing_domain_informations, - expected_missing_domain_invitations, - ) + expected_missing_domains = 9 + expected_duplicate_domains = 0 + expected_missing_domain_informations = 9 + expected_missing_domain_invitations = 9 + self.compare_tables( + expected_total_transition_domains, + expected_total_domains, + expected_total_domain_informations, + expected_total_domain_invitations, + expected_missing_domains, + expected_duplicate_domains, + expected_missing_domain_informations, + expected_missing_domain_invitations, + ) def test_load_full_domain(self): - self.run_load_domains() - self.run_transfer_domains() + with less_console_noise(): + self.run_load_domains() + self.run_transfer_domains() - # Analyze the tables - expected_total_transition_domains = 9 - expected_total_domains = 5 - expected_total_domain_informations = 5 - expected_total_domain_invitations = 8 + # Analyze the tables + expected_total_transition_domains = 9 + expected_total_domains = 5 + expected_total_domain_informations = 5 + expected_total_domain_invitations = 8 - expected_missing_domains = 0 - expected_duplicate_domains = 0 - expected_missing_domain_informations = 0 - expected_missing_domain_invitations = 1 - self.compare_tables( - expected_total_transition_domains, - expected_total_domains, - expected_total_domain_informations, - expected_total_domain_invitations, - expected_missing_domains, - expected_duplicate_domains, - expected_missing_domain_informations, - expected_missing_domain_invitations, - ) + expected_missing_domains = 0 + expected_duplicate_domains = 0 + expected_missing_domain_informations = 0 + expected_missing_domain_invitations = 1 + self.compare_tables( + expected_total_transition_domains, + expected_total_domains, + expected_total_domain_informations, + expected_total_domain_invitations, + expected_missing_domains, + expected_duplicate_domains, + expected_missing_domain_informations, + expected_missing_domain_invitations, + ) - # Test created domains - anomaly_domains = Domain.objects.filter(name="anomaly.gov") - self.assertEqual(anomaly_domains.count(), 1) - anomaly = anomaly_domains.get() + # Test created domains + anomaly_domains = Domain.objects.filter(name="anomaly.gov") + self.assertEqual(anomaly_domains.count(), 1) + anomaly = anomaly_domains.get() - self.assertEqual(anomaly.expiration_date, datetime.date(2023, 3, 9)) + self.assertEqual(anomaly.expiration_date, datetime.date(2023, 3, 9)) - self.assertEqual(anomaly.name, "anomaly.gov") - self.assertEqual(anomaly.state, "ready") + self.assertEqual(anomaly.name, "anomaly.gov") + self.assertEqual(anomaly.state, "ready") - testdomain_domains = Domain.objects.filter(name="fakewebsite2.gov") - self.assertEqual(testdomain_domains.count(), 1) + testdomain_domains = Domain.objects.filter(name="fakewebsite2.gov") + self.assertEqual(testdomain_domains.count(), 1) - testdomain = testdomain_domains.get() + testdomain = testdomain_domains.get() - self.assertEqual(testdomain.expiration_date, datetime.date(2023, 9, 30)) - self.assertEqual(testdomain.name, "fakewebsite2.gov") - self.assertEqual(testdomain.state, "on hold") + self.assertEqual(testdomain.expiration_date, datetime.date(2023, 9, 30)) + self.assertEqual(testdomain.name, "fakewebsite2.gov") + self.assertEqual(testdomain.state, "on hold") def test_load_full_domain_information(self): - self.run_load_domains() - self.run_transfer_domains() + with less_console_noise(): + self.run_load_domains() + self.run_transfer_domains() - # Analyze the tables - expected_total_transition_domains = 9 - expected_total_domains = 5 - expected_total_domain_informations = 5 - expected_total_domain_invitations = 8 + # Analyze the tables + expected_total_transition_domains = 9 + expected_total_domains = 5 + expected_total_domain_informations = 5 + expected_total_domain_invitations = 8 - expected_missing_domains = 0 - expected_duplicate_domains = 0 - expected_missing_domain_informations = 0 - expected_missing_domain_invitations = 1 - self.compare_tables( - expected_total_transition_domains, - expected_total_domains, - expected_total_domain_informations, - expected_total_domain_invitations, - expected_missing_domains, - expected_duplicate_domains, - expected_missing_domain_informations, - expected_missing_domain_invitations, - ) + expected_missing_domains = 0 + expected_duplicate_domains = 0 + expected_missing_domain_informations = 0 + expected_missing_domain_invitations = 1 + self.compare_tables( + expected_total_transition_domains, + expected_total_domains, + expected_total_domain_informations, + expected_total_domain_invitations, + expected_missing_domains, + expected_duplicate_domains, + expected_missing_domain_informations, + expected_missing_domain_invitations, + ) - # Test created Domain Information objects - domain = Domain.objects.filter(name="anomaly.gov").get() - anomaly_domain_infos = DomainInformation.objects.filter(domain=domain) + # Test created Domain Information objects + domain = Domain.objects.filter(name="anomaly.gov").get() + anomaly_domain_infos = DomainInformation.objects.filter(domain=domain) - self.assertEqual(anomaly_domain_infos.count(), 1) + self.assertEqual(anomaly_domain_infos.count(), 1) - # This domain should be pretty barebones. Something isnt - # parsing right if we get a lot of data. - anomaly = anomaly_domain_infos.get() - self.assertEqual(anomaly.organization_name, "Flashdog") - self.assertEqual(anomaly.organization_type, None) - self.assertEqual(anomaly.federal_agency, None) - self.assertEqual(anomaly.federal_type, None) + # This domain should be pretty barebones. Something isnt + # parsing right if we get a lot of data. + anomaly = anomaly_domain_infos.get() + self.assertEqual(anomaly.organization_name, "Flashdog") + self.assertEqual(anomaly.organization_type, None) + self.assertEqual(anomaly.federal_agency, None) + self.assertEqual(anomaly.federal_type, None) - # Check for the "system" creator user - Users = User.objects.filter(username="System") - self.assertEqual(Users.count(), 1) - self.assertEqual(anomaly.creator, Users.get()) + # Check for the "system" creator user + Users = User.objects.filter(username="System") + self.assertEqual(Users.count(), 1) + self.assertEqual(anomaly.creator, Users.get()) - domain = Domain.objects.filter(name="fakewebsite2.gov").get() - fakewebsite_domain_infos = DomainInformation.objects.filter(domain=domain) - self.assertEqual(fakewebsite_domain_infos.count(), 1) + domain = Domain.objects.filter(name="fakewebsite2.gov").get() + fakewebsite_domain_infos = DomainInformation.objects.filter(domain=domain) + self.assertEqual(fakewebsite_domain_infos.count(), 1) - fakewebsite = fakewebsite_domain_infos.get() - self.assertEqual(fakewebsite.organization_name, "Fanoodle") - self.assertEqual(fakewebsite.organization_type, "federal") - self.assertEqual(fakewebsite.federal_agency, "Department of Commerce") - self.assertEqual(fakewebsite.federal_type, "executive") + fakewebsite = fakewebsite_domain_infos.get() + self.assertEqual(fakewebsite.organization_name, "Fanoodle") + self.assertEqual(fakewebsite.organization_type, "federal") + self.assertEqual(fakewebsite.federal_agency, "Department of Commerce") + self.assertEqual(fakewebsite.federal_type, "executive") - ao = fakewebsite.authorizing_official + ao = fakewebsite.authorizing_official - self.assertEqual(ao.first_name, "Seline") - self.assertEqual(ao.middle_name, "testmiddle2") - self.assertEqual(ao.last_name, "Tower") - self.assertEqual(ao.email, "stower3@answers.com") - self.assertEqual(ao.phone, "151-539-6028") + self.assertEqual(ao.first_name, "Seline") + self.assertEqual(ao.middle_name, "testmiddle2") + self.assertEqual(ao.last_name, "Tower") + self.assertEqual(ao.email, "stower3@answers.com") + self.assertEqual(ao.phone, "151-539-6028") - # Check for the "system" creator user - Users = User.objects.filter(username="System") - self.assertEqual(Users.count(), 1) - self.assertEqual(anomaly.creator, Users.get()) + # Check for the "system" creator user + Users = User.objects.filter(username="System") + self.assertEqual(Users.count(), 1) + self.assertEqual(anomaly.creator, Users.get()) def test_transfer_transition_domains_to_domains(self): - self.run_load_domains() - self.run_transfer_domains() + with less_console_noise(): + self.run_load_domains() + self.run_transfer_domains() - # Analyze the tables - expected_total_transition_domains = 9 - expected_total_domains = 5 - expected_total_domain_informations = 5 - expected_total_domain_invitations = 8 + # Analyze the tables + expected_total_transition_domains = 9 + expected_total_domains = 5 + expected_total_domain_informations = 5 + expected_total_domain_invitations = 8 - expected_missing_domains = 0 - expected_duplicate_domains = 0 - expected_missing_domain_informations = 0 - expected_missing_domain_invitations = 1 - self.compare_tables( - expected_total_transition_domains, - expected_total_domains, - expected_total_domain_informations, - expected_total_domain_invitations, - expected_missing_domains, - expected_duplicate_domains, - expected_missing_domain_informations, - expected_missing_domain_invitations, - ) + expected_missing_domains = 0 + expected_duplicate_domains = 0 + expected_missing_domain_informations = 0 + expected_missing_domain_invitations = 1 + self.compare_tables( + expected_total_transition_domains, + expected_total_domains, + expected_total_domain_informations, + expected_total_domain_invitations, + expected_missing_domains, + expected_duplicate_domains, + expected_missing_domain_informations, + expected_missing_domain_invitations, + ) def test_logins(self): - # TODO: setup manually instead of calling other scripts - self.run_load_domains() - self.run_transfer_domains() + with less_console_noise(): + # TODO: setup manually instead of calling other scripts + self.run_load_domains() + self.run_transfer_domains() - # Simluate Logins - for invite in DomainInvitation.objects.all(): - # get a user with this email address - user, user_created = User.objects.get_or_create(email=invite.email, username=invite.email) - user.on_each_login() + # Simluate Logins + for invite in DomainInvitation.objects.all(): + # get a user with this email address + user, user_created = User.objects.get_or_create(email=invite.email, username=invite.email) + user.on_each_login() - # Analyze the tables - expected_total_transition_domains = 9 - expected_total_domains = 5 - expected_total_domain_informations = 5 - expected_total_domain_invitations = 8 + # Analyze the tables + expected_total_transition_domains = 9 + expected_total_domains = 5 + expected_total_domain_informations = 5 + expected_total_domain_invitations = 8 - expected_missing_domains = 0 - expected_duplicate_domains = 0 - expected_missing_domain_informations = 0 - expected_missing_domain_invitations = 1 - self.compare_tables( - expected_total_transition_domains, - expected_total_domains, - expected_total_domain_informations, - expected_total_domain_invitations, - expected_missing_domains, - expected_duplicate_domains, - expected_missing_domain_informations, - expected_missing_domain_invitations, - ) + expected_missing_domains = 0 + expected_duplicate_domains = 0 + expected_missing_domain_informations = 0 + expected_missing_domain_invitations = 1 + self.compare_tables( + expected_total_transition_domains, + expected_total_domains, + expected_total_domain_informations, + expected_total_domain_invitations, + expected_missing_domains, + expected_duplicate_domains, + expected_missing_domain_informations, + expected_missing_domain_invitations, + ) @boto3_mocking.patching def test_send_domain_invitations_email(self): diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index f8373710c..6ab84eb4c 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -159,32 +159,33 @@ class LoggedInTests(TestWithUser): # Given that we are including a subset of items that can be deleted while excluding the rest, # subTest is appropriate here as otherwise we would need many duplicate tests for the same reason. - draft_domain = DraftDomain.objects.create(name="igorville.gov") - for status in DomainApplication.ApplicationStatus: - if status not in [ - DomainApplication.ApplicationStatus.STARTED, - DomainApplication.ApplicationStatus.WITHDRAWN, - ]: - with self.subTest(status=status): - application = DomainApplication.objects.create( - creator=self.user, requested_domain=draft_domain, status=status - ) + with less_console_noise(): + draft_domain = DraftDomain.objects.create(name="igorville.gov") + for status in DomainApplication.ApplicationStatus: + if status not in [ + DomainApplication.ApplicationStatus.STARTED, + DomainApplication.ApplicationStatus.WITHDRAWN, + ]: + with self.subTest(status=status): + application = DomainApplication.objects.create( + creator=self.user, requested_domain=draft_domain, status=status + ) - # Trigger the delete logic - response = self.client.post( - reverse("application-delete", kwargs={"pk": application.pk}), follow=True - ) + # Trigger the delete logic + response = self.client.post( + reverse("application-delete", kwargs={"pk": application.pk}), follow=True + ) - # Check for a 403 error - the end user should not be allowed to do this - self.assertEqual(response.status_code, 403) + # Check for a 403 error - the end user should not be allowed to do this + self.assertEqual(response.status_code, 403) - desired_application = DomainApplication.objects.filter(requested_domain=draft_domain) + desired_application = DomainApplication.objects.filter(requested_domain=draft_domain) - # Make sure the DomainApplication wasn't deleted - self.assertEqual(desired_application.count(), 1) + # Make sure the DomainApplication wasn't deleted + self.assertEqual(desired_application.count(), 1) - # clean up - application.delete() + # clean up + application.delete() def test_home_deletes_domain_application_and_orphans(self): """Tests if delete for DomainApplication deletes orphaned Contact objects""" @@ -329,7 +330,6 @@ class LoggedInTests(TestWithUser): with less_console_noise(): response = self.client.get("/request/", follow=True) - print(response.status_code) self.assertEqual(response.status_code, 403) @@ -2548,112 +2548,118 @@ class TestDomainDetail(TestDomainOverview): It shows as 'DNS needed'""" # At the time of this test's writing, there are 6 UNKNOWN domains inherited # from constructors. Let's reset. - Domain.objects.all().delete() - UserDomainRole.objects.all().delete() - self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") - home_page = self.app.get("/") - self.assertNotContains(home_page, "igorville.gov") - self.role, _ = UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER - ) - home_page = self.app.get("/") - self.assertContains(home_page, "igorville.gov") - igorville = Domain.objects.get(name="igorville.gov") - self.assertEquals(igorville.state, Domain.State.UNKNOWN) - self.assertNotContains(home_page, "Expired") - self.assertContains(home_page, "DNS needed") + with less_console_noise(): + Domain.objects.all().delete() + UserDomainRole.objects.all().delete() + self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") + home_page = self.app.get("/") + self.assertNotContains(home_page, "igorville.gov") + self.role, _ = UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER + ) + home_page = self.app.get("/") + self.assertContains(home_page, "igorville.gov") + igorville = Domain.objects.get(name="igorville.gov") + self.assertEquals(igorville.state, Domain.State.UNKNOWN) + self.assertNotContains(home_page, "Expired") + self.assertContains(home_page, "DNS needed") def test_unknown_domain_does_not_show_as_expired_on_detail_page(self): """An UNKNOWN domain does not show as expired on the detail page. It shows as 'DNS needed'""" # At the time of this test's writing, there are 6 UNKNOWN domains inherited # from constructors. Let's reset. - Domain.objects.all().delete() - UserDomainRole.objects.all().delete() + with less_console_noise(): + Domain.objects.all().delete() + UserDomainRole.objects.all().delete() - self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - self.role, _ = UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER - ) + self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + self.role, _ = UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER + ) - home_page = self.app.get("/") - self.assertContains(home_page, "igorville.gov") - igorville = Domain.objects.get(name="igorville.gov") - self.assertEquals(igorville.state, Domain.State.UNKNOWN) - detail_page = home_page.click("Manage", index=0) - self.assertNotContains(detail_page, "Expired") + home_page = self.app.get("/") + self.assertContains(home_page, "igorville.gov") + igorville = Domain.objects.get(name="igorville.gov") + self.assertEquals(igorville.state, Domain.State.UNKNOWN) + detail_page = home_page.click("Manage", index=0) + self.assertNotContains(detail_page, "Expired") - self.assertContains(detail_page, "DNS needed") + self.assertContains(detail_page, "DNS needed") 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""" - self.user.status = User.RESTRICTED - self.user.save() - home_page = self.app.get("/") - self.assertContains(home_page, "igorville.gov") with less_console_noise(): + self.user.status = User.RESTRICTED + self.user.save() + home_page = self.app.get("/") + self.assertContains(home_page, "igorville.gov") response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id})) self.assertEqual(response.status_code, 403) 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") + with less_console_noise(): + 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") + # 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_detail_see_just_nameserver(self): - home_page = self.app.get("/") - self.assertContains(home_page, "justnameserver.com") + with less_console_noise(): + home_page = self.app.get("/") + self.assertContains(home_page, "justnameserver.com") - # View nameserver on Domain Overview page - detail_page = self.app.get(reverse("domain", kwargs={"pk": self.domain_just_nameserver.id})) + # View nameserver on Domain Overview page + detail_page = self.app.get(reverse("domain", kwargs={"pk": self.domain_just_nameserver.id})) - self.assertContains(detail_page, "justnameserver.com") - self.assertContains(detail_page, "ns1.justnameserver.com") - self.assertContains(detail_page, "ns2.justnameserver.com") + self.assertContains(detail_page, "justnameserver.com") + self.assertContains(detail_page, "ns1.justnameserver.com") + self.assertContains(detail_page, "ns2.justnameserver.com") def test_domain_detail_see_nameserver_and_ip(self): - home_page = self.app.get("/") - self.assertContains(home_page, "nameserverwithip.gov") + with less_console_noise(): + home_page = self.app.get("/") + self.assertContains(home_page, "nameserverwithip.gov") - # View nameserver on Domain Overview page - detail_page = self.app.get(reverse("domain", kwargs={"pk": self.domain_with_ip.id})) + # View nameserver on Domain Overview page + detail_page = self.app.get(reverse("domain", kwargs={"pk": self.domain_with_ip.id})) - self.assertContains(detail_page, "nameserverwithip.gov") + self.assertContains(detail_page, "nameserverwithip.gov") - self.assertContains(detail_page, "ns1.nameserverwithip.gov") - self.assertContains(detail_page, "ns2.nameserverwithip.gov") - self.assertContains(detail_page, "ns3.nameserverwithip.gov") - # Splitting IP addresses bc there is odd whitespace and can't strip text - self.assertContains(detail_page, "(1.2.3.4,") - self.assertContains(detail_page, "2.3.4.5)") + self.assertContains(detail_page, "ns1.nameserverwithip.gov") + self.assertContains(detail_page, "ns2.nameserverwithip.gov") + self.assertContains(detail_page, "ns3.nameserverwithip.gov") + # Splitting IP addresses bc there is odd whitespace and can't strip text + self.assertContains(detail_page, "(1.2.3.4,") + self.assertContains(detail_page, "2.3.4.5)") 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 - staff_user = create_user() - # staff_user.save() - self.client.force_login(staff_user) + with less_console_noise(): + # have to use staff user for this test + staff_user = create_user() + # staff_user.save() + self.client.force_login(staff_user) - # need to set the analyst_action and analyst_action_location - # in the session to emulate user clicking Manage Domain - # in the admin interface - session = self.client.session - session["analyst_action"] = "foo" - session["analyst_action_location"] = self.domain_no_information.id - session.save() + # need to set the analyst_action and analyst_action_location + # in the session to emulate user clicking Manage Domain + # in the admin interface + session = self.client.session + session["analyst_action"] = "foo" + session["analyst_action_location"] = self.domain_no_information.id + session.save() - detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain_no_information.id})) + detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain_no_information.id})) - self.assertContains(detail_page, "noinformation.gov") - self.assertContains(detail_page, "Domain missing domain information") + self.assertContains(detail_page, "noinformation.gov") + self.assertContains(detail_page, "Domain missing domain information") class TestDomainManagers(TestDomainOverview): @@ -3430,124 +3436,121 @@ class TestDomainContactInformation(TestDomainOverview): class TestDomainSecurityEmail(TestDomainOverview): def test_domain_security_email_existing_security_contact(self): """Can load domain's security email page.""" - self.mockSendPatch = patch("registrar.models.domain.registry.send") - self.mockedSendFunction = self.mockSendPatch.start() - self.mockedSendFunction.side_effect = self.mockSend + with less_console_noise(): + self.mockSendPatch = patch("registrar.models.domain.registry.send") + self.mockedSendFunction = self.mockSendPatch.start() + self.mockedSendFunction.side_effect = self.mockSend - domain_contact, _ = Domain.objects.get_or_create(name="freeman.gov") - # Add current user to this domain - _ = UserDomainRole(user=self.user, domain=domain_contact, role="admin").save() - page = self.client.get(reverse("domain-security-email", kwargs={"pk": domain_contact.id})) + domain_contact, _ = Domain.objects.get_or_create(name="freeman.gov") + # Add current user to this domain + _ = UserDomainRole(user=self.user, domain=domain_contact, role="admin").save() + page = self.client.get(reverse("domain-security-email", kwargs={"pk": domain_contact.id})) - # Loads correctly - self.assertContains(page, "Security email") - self.assertContains(page, "security@mail.gov") - self.mockSendPatch.stop() + # Loads correctly + self.assertContains(page, "Security email") + self.assertContains(page, "security@mail.gov") + self.mockSendPatch.stop() def test_domain_security_email_no_security_contact(self): """Loads a domain with no defined security email. We should not show the default.""" - self.mockSendPatch = patch("registrar.models.domain.registry.send") - self.mockedSendFunction = self.mockSendPatch.start() - self.mockedSendFunction.side_effect = self.mockSend + with less_console_noise(): + self.mockSendPatch = patch("registrar.models.domain.registry.send") + self.mockedSendFunction = self.mockSendPatch.start() + self.mockedSendFunction.side_effect = self.mockSend - page = self.client.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) + page = self.client.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) - # Loads correctly - self.assertContains(page, "Security email") - self.assertNotContains(page, "dotgov@cisa.dhs.gov") - self.mockSendPatch.stop() + # Loads correctly + self.assertContains(page, "Security email") + self.assertNotContains(page, "dotgov@cisa.dhs.gov") + self.mockSendPatch.stop() def test_domain_security_email(self): """Can load domain's security email page.""" - page = self.client.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) - self.assertContains(page, "Security email") + with less_console_noise(): + page = self.client.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) + self.assertContains(page, "Security email") def test_domain_security_email_form(self): """Adding a security email works. Uses self.app WebTest because we need to interact with forms. """ - security_email_page = self.app.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - security_email_page.form["security_email"] = "mayor@igorville.gov" - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - mock_client = MagicMock() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): # swallow log warning message - result = security_email_page.form.submit() - self.assertEqual(result.status_code, 302) - self.assertEqual( - result["Location"], - reverse("domain-security-email", kwargs={"pk": self.domain.id}), - ) + with less_console_noise(): + security_email_page = self.app.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + security_email_page.form["security_email"] = "mayor@igorville.gov" + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + mock_client = MagicMock() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): # swallow log warning message + result = security_email_page.form.submit() + self.assertEqual(result.status_code, 302) + self.assertEqual( + result["Location"], + reverse("domain-security-email", kwargs={"pk": self.domain.id}), + ) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - success_page = result.follow() - self.assertContains(success_page, "The security email for this domain has been updated") + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + success_page = result.follow() + self.assertContains(success_page, "The security email for this domain has been updated") - def test_security_email_form_messages(self): + def test_domain_security_email_form_messages(self): """ Test against the success and error messages that are defined in the view """ - p = "adminpass" - self.client.login(username="superuser", password=p) - - form_data_registry_error = { - "security_email": "test@failCreate.gov", - } - - form_data_contact_error = { - "security_email": "test@contactError.gov", - } - - form_data_success = { - "security_email": "test@something.gov", - } - - test_cases = [ - ( - "RegistryError", - form_data_registry_error, - str(GenericError(code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY)), - ), - ( - "ContactError", - form_data_contact_error, - str(SecurityEmailError(code=SecurityEmailErrorCodes.BAD_DATA)), - ), - ( - "RegistrySuccess", - form_data_success, - "The security email for this domain has been updated.", - ), - # Add more test cases with different scenarios here - ] - - for test_name, data, expected_message in test_cases: - response = self.client.post( - reverse("domain-security-email", kwargs={"pk": self.domain.id}), - data=data, - follow=True, - ) - - # Check the response status code, content, or any other relevant assertions - self.assertEqual(response.status_code, 200) - - # Check if the expected message tag is set - if test_name == "RegistryError" or test_name == "ContactError": - message_tag = "error" - elif test_name == "RegistrySuccess": - message_tag = "success" - else: - # Handle other cases if needed - message_tag = "info" # Change to the appropriate default - - # Check the message tag - messages = list(response.context["messages"]) - self.assertEqual(len(messages), 1) - message = messages[0] - self.assertEqual(message.tags, message_tag) - self.assertEqual(message.message.strip(), expected_message.strip()) + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) + form_data_registry_error = { + "security_email": "test@failCreate.gov", + } + form_data_contact_error = { + "security_email": "test@contactError.gov", + } + form_data_success = { + "security_email": "test@something.gov", + } + test_cases = [ + ( + "RegistryError", + form_data_registry_error, + str(GenericError(code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY)), + ), + ( + "ContactError", + form_data_contact_error, + str(SecurityEmailError(code=SecurityEmailErrorCodes.BAD_DATA)), + ), + ( + "RegistrySuccess", + form_data_success, + "The security email for this domain has been updated.", + ), + # Add more test cases with different scenarios here + ] + for test_name, data, expected_message in test_cases: + response = self.client.post( + reverse("domain-security-email", kwargs={"pk": self.domain.id}), + data=data, + follow=True, + ) + # Check the response status code, content, or any other relevant assertions + self.assertEqual(response.status_code, 200) + # Check if the expected message tag is set + if test_name == "RegistryError" or test_name == "ContactError": + message_tag = "error" + elif test_name == "RegistrySuccess": + message_tag = "success" + else: + # Handle other cases if needed + message_tag = "info" # Change to the appropriate default + # Check the message tag + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + message = messages[0] + self.assertEqual(message.tags, message_tag) + self.assertEqual(message.message.strip(), expected_message.strip()) def test_domain_overview_blocked_for_ineligible_user(self): """We could easily duplicate this test for all domain management From 9da326be16bc4b03ae290a78ceee83c754cf65ae Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 30 Jan 2024 05:59:03 -0500 Subject: [PATCH 008/119] DJANGO_LOG_LEVEL can be passed through env to override value of DEBUG --- src/docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/docker-compose.yml b/src/docker-compose.yml index ba6530674..fdf069f56 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -25,6 +25,8 @@ services: - DJANGO_SECRET_KEY=really-long-random-string-BNPecI7+s8jMahQcGHZ3XQ5yUfRrSibdapVLIz0UemdktVPofDKcoy # Run Django in debug mode on local - DJANGO_DEBUG=True + # Set DJANGO_LOG_LEVEL in env + - DJANGO_LOG_LEVEL # Run Django without production flags - IS_PRODUCTION=False # Tell Django where it is being hosted From 023e150597831560972c4fbd4ded1ca8d97812a6 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 30 Jan 2024 10:05:18 -0700 Subject: [PATCH 009/119] Add enum for email defaults --- src/registrar/models/domain.py | 5 ++++- src/registrar/models/public_contact.py | 10 ++++++---- src/registrar/utility/csv_export.py | 4 +++- src/registrar/utility/enums.py | 12 ++++++++++++ src/registrar/views/domain.py | 3 ++- 5 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 1a581a4ec..6d8df525f 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -12,6 +12,7 @@ from django.utils import timezone from typing import Any from registrar.models.host import Host from registrar.models.host_ip import HostIP +from registrar.utility.enums import DefaultEmail from registrar.utility.errors import ( ActionNotAllowed, @@ -1400,7 +1401,9 @@ class Domain(TimeStampedModel, DomainHelper): is_security = contact.contact_type == contact.ContactTypeChoices.SECURITY DF = epp.DiscloseField fields = {DF.EMAIL} - disclose = is_security and contact.email != PublicContact.get_default_security().email + + hidden_security_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT, DefaultEmail.LEGACY_DEFAULT] + disclose = is_security and contact.email not in hidden_security_emails # Delete after testing on other devices logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose) # Will only disclose DF.EMAIL if its not the default diff --git a/src/registrar/models/public_contact.py b/src/registrar/models/public_contact.py index c49a66260..a7f3703a9 100644 --- a/src/registrar/models/public_contact.py +++ b/src/registrar/models/public_contact.py @@ -4,6 +4,8 @@ from string import ascii_uppercase, ascii_lowercase, digits from django.db import models +from registrar.utility.enums import DefaultEmail + from .utility.time_stamped_model import TimeStampedModel @@ -87,7 +89,7 @@ class PublicContact(TimeStampedModel): sp="VA", pc="20598-0645", cc="US", - email="dotgov@cisa.dhs.gov", + email=DefaultEmail.PUBLIC_CONTACT_DEFAULT, voice="+1.8882820870", pw="thisisnotapassword", ) @@ -104,7 +106,7 @@ class PublicContact(TimeStampedModel): sp="VA", pc="22201", cc="US", - email="dotgov@cisa.dhs.gov", + email=DefaultEmail.PUBLIC_CONTACT_DEFAULT, voice="+1.8882820870", pw="thisisnotapassword", ) @@ -121,7 +123,7 @@ class PublicContact(TimeStampedModel): sp="VA", pc="22201", cc="US", - email="dotgov@cisa.dhs.gov", + email=DefaultEmail.PUBLIC_CONTACT_DEFAULT, voice="+1.8882820870", pw="thisisnotapassword", ) @@ -138,7 +140,7 @@ class PublicContact(TimeStampedModel): sp="VA", pc="22201", cc="US", - email="dotgov@cisa.dhs.gov", + email=DefaultEmail.PUBLIC_CONTACT_DEFAULT, voice="+1.8882820870", pw="thisisnotapassword", ) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 3924c03c4..a6da9eea3 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -8,6 +8,8 @@ from django.db.models import Value from django.db.models.functions import Coalesce from django.utils import timezone +from registrar.utility.enums import DefaultEmail + logger = logging.getLogger(__name__) @@ -38,7 +40,7 @@ def write_row(writer, columns, domain_info): if security_contacts: security_email = security_contacts[0].email - invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"} + invalid_emails = {DefaultEmail.LEGACY_DEFAULT, DefaultEmail.PUBLIC_CONTACT_DEFAULT} # These are default emails that should not be displayed in the csv report if security_email is not None and security_email.lower() in invalid_emails: security_email = "(blank)" diff --git a/src/registrar/utility/enums.py b/src/registrar/utility/enums.py index 51f6523c5..7f99fb932 100644 --- a/src/registrar/utility/enums.py +++ b/src/registrar/utility/enums.py @@ -26,3 +26,15 @@ class LogCode(Enum): INFO = 3 DEBUG = 4 DEFAULT = 5 + + +class DefaultEmail(Enum): + """Stores the string values of default emails + + Overview of emails: + - PUBLIC_CONTACT_DEFAULT: "dotgov@cisa.dhs.gov" + - LEGACY_DEFAULT: "registrar@dotgov.gov" + """ + + PUBLIC_CONTACT_DEFAULT = "dotgov@cisa.dhs.gov" + LEGACY_DEFAULT = "registrar@dotgov.gov" diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 08db99e47..024045f58 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -22,6 +22,7 @@ from registrar.models import ( UserDomainRole, ) from registrar.models.public_contact import PublicContact +from registrar.utility.enums import DefaultEmail from registrar.utility.errors import ( GenericError, GenericErrorCodes, @@ -569,7 +570,7 @@ class DomainSecurityEmailView(DomainFormBaseView): initial = super().get_initial() security_contact = self.object.security_contact - invalid_emails = ["dotgov@cisa.dhs.gov", "registrar@dotgov.gov"] + invalid_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT, DefaultEmail.LEGACY_DEFAULT] if security_contact is None or security_contact.email in invalid_emails: initial["security_email"] = None return initial From e01fd547f1222faf859909fee669ebd0c3c1a2dd Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 30 Jan 2024 10:16:42 -0700 Subject: [PATCH 010/119] Add additional default --- src/registrar/views/domain.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 024045f58..08d8a923f 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -142,11 +142,12 @@ class DomainView(DomainBaseView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - default_email = self.object.get_default_security_contact().email - context["hidden_security_emails"] = [default_email, "registrar@dotgov.gov"] + default_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT, DefaultEmail.LEGACY_DEFAULT] + + context["hidden_security_emails"] = default_emails security_email = self.object.get_security_email() - if security_email is None or security_email == default_email: + if security_email is None or security_email in default_emails: context["security_email"] = None return context context["security_email"] = security_email From bcd44f42bfe8cd4f4062322952f9718d7fdda3d4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 30 Jan 2024 10:41:56 -0700 Subject: [PATCH 011/119] Add '.value' --- src/registrar/models/domain.py | 2 +- src/registrar/models/public_contact.py | 8 ++++---- src/registrar/utility/csv_export.py | 2 +- src/registrar/views/domain.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index b1ed5dd48..8a7f9c6e6 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1406,7 +1406,7 @@ class Domain(TimeStampedModel, DomainHelper): DF = epp.DiscloseField fields = {DF.EMAIL} - hidden_security_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT, DefaultEmail.LEGACY_DEFAULT] + hidden_security_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value] disclose = is_security and contact.email not in hidden_security_emails # Delete after testing on other devices logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose) diff --git a/src/registrar/models/public_contact.py b/src/registrar/models/public_contact.py index a7f3703a9..08891fa97 100644 --- a/src/registrar/models/public_contact.py +++ b/src/registrar/models/public_contact.py @@ -89,7 +89,7 @@ class PublicContact(TimeStampedModel): sp="VA", pc="20598-0645", cc="US", - email=DefaultEmail.PUBLIC_CONTACT_DEFAULT, + email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, voice="+1.8882820870", pw="thisisnotapassword", ) @@ -106,7 +106,7 @@ class PublicContact(TimeStampedModel): sp="VA", pc="22201", cc="US", - email=DefaultEmail.PUBLIC_CONTACT_DEFAULT, + email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, voice="+1.8882820870", pw="thisisnotapassword", ) @@ -123,7 +123,7 @@ class PublicContact(TimeStampedModel): sp="VA", pc="22201", cc="US", - email=DefaultEmail.PUBLIC_CONTACT_DEFAULT, + email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, voice="+1.8882820870", pw="thisisnotapassword", ) @@ -140,7 +140,7 @@ class PublicContact(TimeStampedModel): sp="VA", pc="22201", cc="US", - email=DefaultEmail.PUBLIC_CONTACT_DEFAULT, + email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, voice="+1.8882820870", pw="thisisnotapassword", ) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 3d025309a..e2e5f9c54 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -65,7 +65,7 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None security_email = _email if _email is not None else " " # These are default emails that should not be displayed in the csv report - invalid_emails = {DefaultEmail.LEGACY_DEFAULT, DefaultEmail.PUBLIC_CONTACT_DEFAULT} + invalid_emails = {DefaultEmail.LEGACY_DEFAULT.value, DefaultEmail.PUBLIC_CONTACT_DEFAULT.value} if security_email.lower() in invalid_emails: security_email = "(blank)" diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 08d8a923f..a5a4783a9 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -142,7 +142,7 @@ class DomainView(DomainBaseView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - default_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT, DefaultEmail.LEGACY_DEFAULT] + default_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value] context["hidden_security_emails"] = default_emails @@ -571,7 +571,7 @@ class DomainSecurityEmailView(DomainFormBaseView): initial = super().get_initial() security_contact = self.object.security_contact - invalid_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT, DefaultEmail.LEGACY_DEFAULT] + invalid_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value] if security_contact is None or security_contact.email in invalid_emails: initial["security_email"] = None return initial From 5b047039f20825fe02530e817e457360171f2dcb Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 30 Jan 2024 10:44:34 -0700 Subject: [PATCH 012/119] Linting --- src/registrar/utility/enums.py | 2 +- src/registrar/views/domain.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/utility/enums.py b/src/registrar/utility/enums.py index 7f99fb932..706eee1fc 100644 --- a/src/registrar/utility/enums.py +++ b/src/registrar/utility/enums.py @@ -30,7 +30,7 @@ class LogCode(Enum): class DefaultEmail(Enum): """Stores the string values of default emails - + Overview of emails: - PUBLIC_CONTACT_DEFAULT: "dotgov@cisa.dhs.gov" - LEGACY_DEFAULT: "registrar@dotgov.gov" diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index a5a4783a9..4dce2301c 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -143,7 +143,7 @@ class DomainView(DomainBaseView): context = super().get_context_data(**kwargs) default_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value] - + context["hidden_security_emails"] = default_emails security_email = self.object.get_security_email() From 114feafaf01bf06114ea3cee2b9eec4c1052c469 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 30 Jan 2024 14:26:11 -0500 Subject: [PATCH 013/119] Add a domain not ready FSM rule for the new transitions from approved, error handling and unit tests --- src/registrar/admin.py | 7 +- src/registrar/models/domain_application.py | 58 ++++---- src/registrar/tests/test_admin.py | 153 ++++++--------------- src/registrar/tests/test_models.py | 40 ++++++ 4 files changed, 118 insertions(+), 140 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 325081575..9a75d7d72 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -882,14 +882,11 @@ class DomainApplicationAdmin(ListHeaderAdmin): if ( obj and original_obj.status == models.DomainApplication.ApplicationStatus.APPROVED - and ( - obj.status == models.DomainApplication.ApplicationStatus.REJECTED - or obj.status == models.DomainApplication.ApplicationStatus.INELIGIBLE - ) + and obj.status != models.DomainApplication.ApplicationStatus.APPROVED and not obj.domain_is_not_active() ): # If an admin tried to set an approved application to - # rejected or ineligible and the related domain is already + # another status and the related domain is already # active, shortcut the action and throw a friendly # error message. This action would still not go through # shortcut or not as the rules are duplicated on the model, diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 30def9cfc..5f61c9385 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -572,6 +572,19 @@ class DomainApplication(TimeStampedModel): return not self.approved_domain.is_active() return True + def delete_and_clean_up_domain(self, called_from): + try: + domain_state = self.approved_domain.state + # Only reject if it exists on EPP + if domain_state != Domain.State.UNKNOWN: + self.approved_domain.deletedInEpp() + self.approved_domain.save() + self.approved_domain.delete() + self.approved_domain = None + except Exception as err: + logger.error(err) + logger.error(f"Can't query an approved domain while attempting {called_from}") + def _send_status_update_email(self, new_status, email_template, email_template_subject, send_email=True): """Send a status update email to the submitter. @@ -651,11 +664,19 @@ class DomainApplication(TimeStampedModel): ApplicationStatus.INELIGIBLE, ], target=ApplicationStatus.IN_REVIEW, + conditions=[domain_is_not_active], ) def in_review(self): """Investigate an application that has been submitted. - This action is logged.""" + This action is logged. + + As side effects this will delete the domain and domain_information + (will cascade) when they exist.""" + + if self.status == self.ApplicationStatus.APPROVED: + self.delete_and_clean_up_domain("in_review") + literal = DomainApplication.ApplicationStatus.IN_REVIEW # Check if the tuple exists, then grab its value in_review = literal if literal is not None else "In Review" @@ -670,11 +691,19 @@ class DomainApplication(TimeStampedModel): ApplicationStatus.INELIGIBLE, ], target=ApplicationStatus.ACTION_NEEDED, + conditions=[domain_is_not_active], ) def action_needed(self): """Send back an application that is under investigation or rejected. - This action is logged.""" + This action is logged. + + As side effects this will delete the domain and domain_information + (will cascade) when they exist.""" + + if self.status == self.ApplicationStatus.APPROVED: + self.delete_and_clean_up_domain("reject_with_prejudice") + literal = DomainApplication.ApplicationStatus.ACTION_NEEDED # Check if the tuple is setup correctly, then grab its value action_needed = literal if literal is not None else "Action Needed" @@ -746,18 +775,9 @@ class DomainApplication(TimeStampedModel): As side effects this will delete the domain and domain_information (will cascade), and send an email notification.""" + if self.status == self.ApplicationStatus.APPROVED: - try: - domain_state = self.approved_domain.state - # Only reject if it exists on EPP - if domain_state != Domain.State.UNKNOWN: - self.approved_domain.deletedInEpp() - self.approved_domain.save() - self.approved_domain.delete() - self.approved_domain = None - except Exception as err: - logger.error(err) - logger.error("Can't query an approved domain while attempting a DA reject()") + self.delete_and_clean_up_domain("reject") self._send_status_update_email( "action needed", @@ -786,17 +806,7 @@ class DomainApplication(TimeStampedModel): and domain_information (will cascade) when they exist.""" if self.status == self.ApplicationStatus.APPROVED: - try: - domain_state = self.approved_domain.state - # Only reject if it exists on EPP - if domain_state != Domain.State.UNKNOWN: - self.approved_domain.deletedInEpp() - self.approved_domain.save() - self.approved_domain.delete() - self.approved_domain = None - except Exception as err: - logger.error(err) - logger.error("Can't query an approved domain while attempting a DA reject_with_prejudice()") + self.delete_and_clean_up_domain("reject_with_prejudice") self.creator.restrict_user() diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index f88e25c2f..6b0c16b5b 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -707,41 +707,13 @@ class TestDomainApplicationAdmin(MockEppLib): "Cannot edit an application with a restricted creator.", ) - @boto3_mocking.patching - def test_error_when_saving_approved_to_rejected_and_domain_is_active(self): - # Create an instance of the model - application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - domain = Domain.objects.create(name=application.requested_domain.name) - application.approved_domain = domain - application.save() + def trigger_saving_approved_to_another_state(self, domain_is_active, another_state): + """Helper method that triggers domain request state changes from approved to another state, + with an associated domain that can be either active (READY) or not. - # Create a request object with a superuser - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - request.user = self.superuser + Used to test errors when saving a change with an active domain, also used to test side effects + when saving a change goes through.""" - # Define a custom implementation for is_active - def custom_is_active(self): - return True # Override to return True - - # Use ExitStack to combine patch contexts - with ExitStack() as stack: - # Patch Domain.is_active and django.contrib.messages.error simultaneously - stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) - stack.enter_context(patch.object(messages, "error")) - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Simulate saving the model - application.status = DomainApplication.ApplicationStatus.REJECTED - self.admin.save_model(request, application, None, True) - - # Assert that the error message was called with the correct argument - messages.error.assert_called_once_with( - request, - "This action is not permitted. The domain " + "is already active.", - ) - - def test_side_effects_when_saving_approved_to_rejected(self): # Create an instance of the model application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) domain = Domain.objects.create(name=application.requested_domain.name) @@ -755,101 +727,60 @@ class TestDomainApplicationAdmin(MockEppLib): # Define a custom implementation for is_active def custom_is_active(self): - return False # Override to return False + return domain_is_active # Override to return True # Use ExitStack to combine patch contexts with ExitStack() as stack: # Patch Domain.is_active and django.contrib.messages.error simultaneously stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) stack.enter_context(patch.object(messages, "error")) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Simulate saving the model - application.status = DomainApplication.ApplicationStatus.REJECTED - self.admin.save_model(request, application, None, True) - # Assert that the error message was never called - messages.error.assert_not_called() + application.status = another_state + self.admin.save_model(request, application, None, True) - self.assertEqual(application.approved_domain, None) + # Assert that the error message was called with the correct argument + if domain_is_active: + messages.error.assert_called_once_with( + request, + "This action is not permitted. The domain " + "is already active.", + ) + else: + # Assert that the error message was never called + messages.error.assert_not_called() - # Assert that Domain got Deleted - with self.assertRaises(Domain.DoesNotExist): - domain.refresh_from_db() + self.assertEqual(application.approved_domain, None) - # Assert that DomainInformation got Deleted - with self.assertRaises(DomainInformation.DoesNotExist): - domain_information.refresh_from_db() + # Assert that Domain got Deleted + with self.assertRaises(Domain.DoesNotExist): + domain.refresh_from_db() + + # Assert that DomainInformation got Deleted + with self.assertRaises(DomainInformation.DoesNotExist): + domain_information.refresh_from_db() + + def test_error_when_saving_approved_to_in_review_and_domain_is_active(self): + self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.IN_REVIEW) + + def test_error_when_saving_approved_to_action_needed_and_domain_is_active(self): + self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.ACTION_NEEDED) + + def test_error_when_saving_approved_to_rejected_and_domain_is_active(self): + self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.REJECTED) def test_error_when_saving_approved_to_ineligible_and_domain_is_active(self): - # Create an instance of the model - application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - domain = Domain.objects.create(name=application.requested_domain.name) - application.approved_domain = domain - application.save() + self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.INELIGIBLE) - # Create a request object with a superuser - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - request.user = self.superuser + def test_side_effects_when_saving_approved_to_in_review(self): + self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.IN_REVIEW) - # Define a custom implementation for is_active - def custom_is_active(self): - return True # Override to return True + def test_side_effects_when_saving_approved_to_action_needed(self): + self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.ACTION_NEEDED) - # Use ExitStack to combine patch contexts - with ExitStack() as stack: - # Patch Domain.is_active and django.contrib.messages.error simultaneously - stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) - stack.enter_context(patch.object(messages, "error")) - - # Simulate saving the model - application.status = DomainApplication.ApplicationStatus.INELIGIBLE - self.admin.save_model(request, application, None, True) - - # Assert that the error message was called with the correct argument - messages.error.assert_called_once_with( - request, - "This action is not permitted. The domain " + "is already active.", - ) + def test_side_effects_when_saving_approved_to_rejected(self): + self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.REJECTED) def test_side_effects_when_saving_approved_to_ineligible(self): - # Create an instance of the model - application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - domain = Domain.objects.create(name=application.requested_domain.name) - domain_information = DomainInformation.objects.create(creator=self.superuser, domain=domain) - application.approved_domain = domain - application.save() - - # Create a request object with a superuser - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - request.user = self.superuser - - # Define a custom implementation for is_active - def custom_is_active(self): - return False # Override to return False - - # Use ExitStack to combine patch contexts - with ExitStack() as stack: - # Patch Domain.is_active and django.contrib.messages.error simultaneously - stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) - stack.enter_context(patch.object(messages, "error")) - - # Simulate saving the model - application.status = DomainApplication.ApplicationStatus.INELIGIBLE - self.admin.save_model(request, application, None, True) - - # Assert that the error message was never called - messages.error.assert_not_called() - - self.assertEqual(application.approved_domain, None) - - # Assert that Domain got Deleted - with self.assertRaises(Domain.DoesNotExist): - domain.refresh_from_db() - - # Assert that DomainInformation got Deleted - with self.assertRaises(DomainInformation.DoesNotExist): - domain_information.refresh_from_db() + self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.INELIGIBLE) def test_has_correct_filters(self): """ diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index d0005cbd5..d2db53817 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -457,6 +457,46 @@ class TestDomainApplication(TestCase): with self.assertRaises(exception_type): application.reject_with_prejudice() + def test_transition_not_allowed_approved_in_review_when_domain_is_active(self): + """Create an application with status approved, create a matching domain that + is active, and call in_review against transition rules""" + + domain = Domain.objects.create(name=self.approved_application.requested_domain.name) + self.approved_application.approved_domain = domain + self.approved_application.save() + + # Define a custom implementation for is_active + def custom_is_active(self): + return True # Override to return True + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Use patch to temporarily replace is_active with the custom implementation + with patch.object(Domain, "is_active", custom_is_active): + # Now, when you call is_active on Domain, it will return True + with self.assertRaises(TransitionNotAllowed): + self.approved_application.in_review() + + def test_transition_not_allowed_approved_action_needed_when_domain_is_active(self): + """Create an application with status approved, create a matching domain that + is active, and call action_needed against transition rules""" + + domain = Domain.objects.create(name=self.approved_application.requested_domain.name) + self.approved_application.approved_domain = domain + self.approved_application.save() + + # Define a custom implementation for is_active + def custom_is_active(self): + return True # Override to return True + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Use patch to temporarily replace is_active with the custom implementation + with patch.object(Domain, "is_active", custom_is_active): + # Now, when you call is_active on Domain, it will return True + with self.assertRaises(TransitionNotAllowed): + self.approved_application.action_needed() + def test_transition_not_allowed_approved_rejected_when_domain_is_active(self): """Create an application with status approved, create a matching domain that is active, and call reject against transition rules""" From 9cd6d2f7b470fc1d8e72502085e9bf9766656a55 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 30 Jan 2024 14:13:56 -0700 Subject: [PATCH 014/119] Add basic icon with tooltip --- src/registrar/models/domain.py | 25 +++++++++++++++++++++++++ src/registrar/templates/home.html | 9 +++++++++ 2 files changed, 34 insertions(+) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 27a8364bc..e84221a3f 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1397,6 +1397,31 @@ class Domain(TimeStampedModel, DomainHelper): logger.info("Changing to DNS_NEEDED state") logger.info("able to transition to DNS_NEEDED state") + def get_state_help_text(self) -> str: + """Returns a str containing additional information about a given state""" + help_text = "" + print(f"self state is {self.state}") + match self.state: + case self.State.DNS_NEEDED | self.State.UNKNOWN: + help_text = ( + "Before this domain can be used, " + "you’ll need to add name server addresses." + ) + case self.State.READY: + help_text = "This domain has name servers and is ready for use." + case self.State.ON_HOLD: + help_text = ( + "This domain is administratively paused, " + "so it can’t be edited and won’t resolve in DNS. " + "Contact help@get.gov for details." + ) + case self.State.DELETED: + help_text = ( + "This domain has been removed and " + "is no longer registered to your organization." + ) + return help_text + def _disclose_fields(self, contact: PublicContact): """creates a disclose object that can be added to a contact Create using .disclose= on the command before sending. diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index c7a005f97..0b0553d8b 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -53,6 +53,15 @@ {% else %} {{ domain.state|capfirst }} {% endif %} + + + From 6f891d82883d3ab2cdf6312546673a5dd7d1a0df Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 30 Jan 2024 14:26:53 -0700 Subject: [PATCH 015/119] Increase hover area --- src/registrar/templates/home.html | 40 ++++++++++++++++++------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 0b0553d8b..1afb167cf 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -45,23 +45,29 @@ {{ domain.expiration_date|date }} - {# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #} - {% if domain.is_expired and domain.state != domain.State.UNKNOWN %} - Expired - {% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %} - DNS needed - {% else %} - {{ domain.state|capfirst }} - {% endif %} - - - + + {# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #} + {% if domain.is_expired and domain.state != domain.State.UNKNOWN %} + Expired + {% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %} + DNS needed + {% else %} + {{ domain.state|capfirst }} + {% endif %} + {# TODO: this tooltip should trigger on click, not hover. Better for access and works better button wise #} + + + + From 248dd4adfc1d66739fbaf6b5e74ea7bf43791758 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 30 Jan 2024 16:42:59 -0500 Subject: [PATCH 016/119] changes for linter --- src/epplibwrapper/tests/test_pool.py | 7 +++---- src/epplibwrapper/utility/pool.py | 2 +- src/registrar/tests/test_management_scripts.py | 4 +--- src/registrar/tests/test_models_domain.py | 11 +++++++++++ src/registrar/tests/test_reports.py | 7 +++++-- .../tests/test_transition_domain_migrations.py | 18 ++++++++++++------ 6 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index c602d4a06..f8e556445 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -135,7 +135,7 @@ class TestConnectionPool(TestCase): stack.enter_context(patch.object(EPPConnectionPool, "kill_all_connections", do_nothing)) stack.enter_context(patch.object(SocketTransport, "send", self.fake_send)) stack.enter_context(patch.object(SocketTransport, "receive", fake_receive)) - with less_console_noise(): + with less_console_noise(): # Restart the connection pool registry.start_connection_pool() # Pool should be running, and be the right size @@ -211,7 +211,7 @@ class TestConnectionPool(TestCase): stack.enter_context(patch.object(EPPConnectionPool, "kill_all_connections", do_nothing)) stack.enter_context(patch.object(SocketTransport, "send", self.fake_send)) stack.enter_context(patch.object(SocketTransport, "receive", fake_receive)) - with less_console_noise(): + with less_console_noise(): # Start the connection pool registry.start_connection_pool() # Kill the connection pool @@ -243,7 +243,7 @@ class TestConnectionPool(TestCase): def test_raises_connection_error(self): """A .send is invoked on the pool, but registry connection is lost right as we send a command.""" - + with ExitStack() as stack: stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket)) stack.enter_context(patch.object(Socket, "connect", self.fake_client)) @@ -260,4 +260,3 @@ class TestConnectionPool(TestCase): expected = "InfoDomain failed to execute due to a connection error." result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) self.assertEqual(result, expected) - diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index 2c7de119f..56ed46143 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -97,7 +97,7 @@ class EPPConnectionPool(ConnectionPool): # Nothing to do, the pool will generate a new connection later pass gevent.sleep(delay) - + def _create_socket(self, client, login) -> Socket: """Creates and returns a socket instance""" socket = Socket(client, login) diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index 40cdce6d2..34178e262 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -263,9 +263,7 @@ class TestExtendExpirationDates(MockEppLib): epp_expiration_date=date(2023, 11, 15), ) # Create a domain with an invalid expiration date - Domain.objects.get_or_create( - name="fake.gov", state=Domain.State.READY, expiration_date=date(2022, 5, 25) - ) + Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY, expiration_date=date(2022, 5, 25)) TransitionDomain.objects.get_or_create( username="themoonisactuallycheese@mail.com", domain_name="fake.gov", diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 8cedb65e9..ca0a5e8d8 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -743,9 +743,11 @@ class TestRegistrantContacts(MockEppLib): self.domain_contact.get_security_email() # invalidate the cache so the next time get_security_email is called, it has to attempt to populate cache self.domain_contact._invalidate_cache() + # mock that registry throws an error on the EPP send def side_effect(_request, cleaned): raise RegistryError(code=ErrorCode.COMMAND_FAILED) + patcher = patch("registrar.models.domain.registry.send") mocked_send = patcher.start() mocked_send.side_effect = side_effect @@ -1236,6 +1238,7 @@ class TestRegistrantNameservers(MockEppLib): nameserver12 = "ns1.cats-are-superior12.com" nameserver13 = "ns1.cats-are-superior13.com" nameserver14 = "ns1.cats-are-superior14.com" + def _get_14_nameservers(): self.domain.nameservers = [ (nameserver1,), @@ -1253,6 +1256,7 @@ class TestRegistrantNameservers(MockEppLib): (nameserver13,), (nameserver14,), ] + self.assertRaises(NameserverError, _get_14_nameservers) self.assertEqual(self.mockedSendFunction.call_count, 0) @@ -1553,9 +1557,11 @@ class TestRegistrantNameservers(MockEppLib): # fetch_cache host, _ = Host.objects.get_or_create(domain=domain, name="ns1.fake.gov") host_ip, _ = HostIP.objects.get_or_create(host=host, address="1.1.1.1") + # mock that registry throws an error on the InfoHost send def side_effect(_request, cleaned): raise RegistryError(code=ErrorCode.COMMAND_FAILED) + patcher = patch("registrar.models.domain.registry.send") mocked_send = patcher.start() mocked_send.side_effect = side_effect @@ -1729,6 +1735,7 @@ class TestRegistrantDNSSEC(MockEppLib): ) else: return MagicMock(res_data=[self.mockDataInfoHosts]) + with less_console_noise(): patcher = patch("registrar.models.domain.registry.send") mocked_send = patcher.start() @@ -1806,6 +1813,7 @@ class TestRegistrantDNSSEC(MockEppLib): ) else: return MagicMock(res_data=[self.mockDataInfoHosts]) + with less_console_noise(): patcher = patch("registrar.models.domain.registry.send") mocked_send = patcher.start() @@ -1879,6 +1887,7 @@ class TestRegistrantDNSSEC(MockEppLib): ) else: return MagicMock(res_data=[self.mockDataInfoHosts]) + with less_console_noise(): patcher = patch("registrar.models.domain.registry.send") mocked_send = patcher.start() @@ -1947,6 +1956,7 @@ class TestRegistrantDNSSEC(MockEppLib): ) else: return MagicMock(res_data=[self.mockDataInfoHosts]) + with less_console_noise(): patcher = patch("registrar.models.domain.registry.send") mocked_send = patcher.start() @@ -2271,6 +2281,7 @@ class TestAnalystClientHold(MockEppLib): def side_effect(_request, cleaned): raise RegistryError(code=ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION) + with less_console_noise(): patcher = patch("registrar.models.domain.registry.send") mocked_send = patcher.start() diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index d74ebaffd..630904218 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -25,6 +25,7 @@ from datetime import date, datetime, timedelta from django.utils import timezone from .common import less_console_noise + class CsvReportsTest(TestCase): """Tests to determine if we are uploading our reports correctly""" @@ -124,6 +125,7 @@ class CsvReportsTest(TestCase): def side_effect(Bucket, Key): raise ClientError({"Error": {"Code": "NoSuchKey", "Message": "No such key"}}, "get_object") + with less_console_noise(): mock_client = MagicMock() mock_client.get_object.side_effect = side_effect @@ -145,6 +147,7 @@ class CsvReportsTest(TestCase): def side_effect(Bucket, Key): raise ClientError({"Error": {"Code": "NoSuchKey", "Message": "No such key"}}, "get_object") + with less_console_noise(): mock_client = MagicMock() mock_client.get_object.side_effect = side_effect @@ -524,8 +527,8 @@ class ExportDataTest(MockEppLib): # Create a CSV file in memory csv_file = StringIO() writer = csv.writer(csv_file) - # We use timezone.make_aware to sync to server time a datetime object with the current date (using date.today()) - # and a specific time (using datetime.min.time()). + # We use timezone.make_aware to sync to server time a datetime object with the current date + # (using date.today()) and a specific time (using datetime.min.time()). end_date = timezone.make_aware(datetime.combine(date.today() + timedelta(days=2), datetime.min.time())) start_date = timezone.make_aware(datetime.combine(date.today() - timedelta(days=2), datetime.min.time())) diff --git a/src/registrar/tests/test_transition_domain_migrations.py b/src/registrar/tests/test_transition_domain_migrations.py index d419e6fcd..e9453bd03 100644 --- a/src/registrar/tests/test_transition_domain_migrations.py +++ b/src/registrar/tests/test_transition_domain_migrations.py @@ -24,6 +24,7 @@ import logging logger = logging.getLogger(__name__) + class TestProcessedMigrations(TestCase): """This test case class is designed to verify the idempotency of migrations related to domain transitions in the application.""" @@ -241,8 +242,8 @@ class TestOrganizationMigration(TestCase): execute the load_organization_data command with the specified arguments. """ with less_console_noise(): - # noqa here (E501) because splitting this up makes it - # confusing to read. + # noqa here (E501) because splitting this up makes it + # confusing to read. with patch( "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa return_value=True, @@ -308,8 +309,9 @@ class TestOrganizationMigration(TestCase): 3. Checks that the data has been loaded as expected. The expected result is a set of TransitionDomain objects with specific attributes. - The test fetches the actual TransitionDomain objects from the database and compares them with the expected objects. - """ + The test fetches the actual TransitionDomain objects from the database and compares them with + the expected objects. + """ with less_console_noise(): # noqa - E501 (harder to read) # == First, parse all existing data == # @@ -392,7 +394,9 @@ class TestOrganizationMigration(TestCase): domain_information = DomainInformation.objects.filter(domain=_domain).get() expected_creator = User.objects.filter(username="System").get() - expected_ao = Contact.objects.filter(first_name="Seline", middle_name="testmiddle2", last_name="Tower").get() + expected_ao = Contact.objects.filter( + first_name="Seline", middle_name="testmiddle2", last_name="Tower" + ).get() expected_domain_information = DomainInformation( creator=expected_creator, organization_type="federal", @@ -445,7 +449,9 @@ class TestOrganizationMigration(TestCase): domain_information = DomainInformation.objects.filter(domain=_domain).get() expected_creator = User.objects.filter(username="System").get() - expected_ao = Contact.objects.filter(first_name="Seline", middle_name="testmiddle2", last_name="Tower").get() + expected_ao = Contact.objects.filter( + first_name="Seline", middle_name="testmiddle2", last_name="Tower" + ).get() expected_domain_information = DomainInformation( creator=expected_creator, organization_type="federal", From 60d1c2f1ed76cf68bff7a0d972354c5396a9f200 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 30 Jan 2024 14:53:46 -0700 Subject: [PATCH 017/119] CSS changes --- src/registrar/assets/sass/_theme/_buttons.scss | 8 ++++++++ src/registrar/templates/home.html | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 02089ec6d..6aa2995fd 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -132,3 +132,11 @@ a.usa-button--unstyled:visited { margin-left: units(2); } } + +// TODO: find another file for this +.info-button span.usa-tooltip svg.usa-icon { + svg.usa-icon{ + transform: translateY(2px) !important; + background: transparent; + } +} diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 1afb167cf..60aa1cd55 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -45,7 +45,7 @@ {{ domain.expiration_date|date }} - Date: Tue, 30 Jan 2024 19:16:56 -0500 Subject: [PATCH 018/119] Initial solution and loggers --- src/registrar/views/application.py | 77 ++++++++++++++++++++- src/registrar/views/utility/steps_helper.py | 15 ++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index a15f36ccc..e57a5436d 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -224,8 +224,10 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): if request.path_info == self.NEW_URL_NAME: return render(request, "application_intro.html") else: + logger.info('get calling self.steps.first') return self.goto(self.steps.first) + logger.info('get setting current step') self.steps.current = current_url context = self.get_context_data() context["forms"] = self.get_forms() @@ -254,6 +256,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): All arguments (**kwargs) are passed directly to `get_forms`. """ + logger.info('get_all_forms gettig steps in self.steps') nested = (self.get_forms(step=step, **kwargs) for step in self.steps) flattened = [form for lst in nested for form in lst] return flattened @@ -269,6 +272,8 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): and from the database if `use_db` is True (provided that record exists). An empty form will be provided if neither of those are true. """ + + logger.info('get_forms setting prefix to self.steps.current') kwargs = { "files": files, "prefix": self.steps.current, @@ -328,6 +333,66 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): DomainApplication.ApplicationStatus.ACTION_NEEDED, ] return DomainApplication.objects.filter(creator=self.request.user, status__in=check_statuses) + + def db_check_for_unlocking_steps(self): + unlocked_steps = {} + + if self.application.organization_type: + unlocked_steps["organization_type"] = True + + if self.application.tribe_name: + unlocked_steps["tribal_government"] = True + + if self.application.federal_agency: + unlocked_steps["organization_federal"] = True + + if self.application.is_election_board: + unlocked_steps["organization_election"] = True + + if ( + self.application.organization_name + or self.application.address_line1 + or self.application.city + or self.application.state_territory + or self.application.zipcode + or self.application.urbanization + ): + unlocked_steps["organization_contact"] = True + + if self.application.about_your_organization: + unlocked_steps["about_your_organization"] = True + + if self.application.authorizing_official: + unlocked_steps["authorizing_official"] = True + + # Since this field is optional, test to see if the next step has been touched + if self.application.current_websites.exists() or self.application.requested_domain: + unlocked_steps["current_sites"] = True + + if self.application.requested_domain: + unlocked_steps["dotgov_domain"] = True + + if self.application.purpose: + unlocked_steps["purpose"] = True + + if self.application.submitter: + unlocked_steps["your_contact"] = True + + if self.application.other_contacts.exists() or self.application.no_other_contacts_rationale: + unlocked_steps["other_contacts"] = True + + # Since this field is optional, test to see if the next step has been touched + if self.application.anything_else or self.application.is_policy_acknowledged: + unlocked_steps["anything_else"] = True + + if self.application.is_policy_acknowledged: + unlocked_steps["requirements"] = True + + if self.application.submission_date: + unlocked_steps["review"] = True + + return unlocked_steps + def get_context_data(self): """Define context for access on all wizard pages.""" @@ -338,11 +403,17 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): modal_heading = "You are about to submit a domain request for " + str(self.application.requested_domain) else: modal_heading = "You are about to submit an incomplete request" + + logger.info(f'get_context_data returning value for cisited equals to: {self.storage.get("step_history", [])}') + + unlocked_steps_list = list(self.db_check_for_unlocking_steps().keys()) + + return { "form_titles": self.TITLES, "steps": self.steps, # Add information about which steps should be unlocked - "visited": self.storage.get("step_history", []), + "visited": unlocked_steps_list, "is_federal": self.application.is_federal(), "modal_button": modal_button, "modal_heading": modal_heading, @@ -360,6 +431,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): return step_list def goto(self, step): + logger.info(f'goto sets self.steps.current to passed {step}') self.steps.current = step return redirect(reverse(f"{self.URL_NAMESPACE}:{step}")) @@ -368,6 +440,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): next = self.steps.next if next: self.steps.current = next + logger.info(f'goto sets self.goto_next_step.current to passed {self.steps.next}') return self.goto(next) else: raise Http404() @@ -387,6 +460,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): if button == "intro_acknowledge": if request.path_info == self.NEW_URL_NAME: del self.storage + logger.info(f'post calling goto with {self.steps.first}') return self.goto(self.steps.first) # if accessing this class directly, redirect to the first step @@ -406,6 +480,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): # return them to the page they were already on if button == "save": messages.success(request, "Your progress has been saved!") + logger.info(f'post calling goto with {self.steps.current}') return self.goto(self.steps.current) # if user opted to save progress and return, # return them to the home page diff --git a/src/registrar/views/utility/steps_helper.py b/src/registrar/views/utility/steps_helper.py index f5eca2b55..55f481263 100644 --- a/src/registrar/views/utility/steps_helper.py +++ b/src/registrar/views/utility/steps_helper.py @@ -44,28 +44,35 @@ class StepsHelper: """ def __init__(self, wizard): + logger.info(f"steps_helper __init__") self._wizard = wizard def __dir__(self): + logger.info(f"steps_helper __dir__ {self.all}") return self.all def __len__(self): + logger.info(f"steps_helper __len__ {self.count}") return self.count def __repr__(self): + logger.info(f"steps_helper __repr__ {self._wizard} {self.all}") return "" % (self._wizard, self.all) def __getitem__(self, step): + logger.info(f"steps_helper __getitem__ {self.all[step]}") return self.all[step] @property def all(self): """Returns the names of all steps.""" + logger.info(f"steps_helper all {self._wizard.get_step_list()}") return self._wizard.get_step_list() @property def count(self): """Returns the total number of steps/forms in this the wizard.""" + logger.info(f"steps_helper count {len(self.all)}") return len(self.all) @property @@ -79,12 +86,14 @@ class StepsHelper: current_url = resolve(self._wizard.request.path_info).url_name step = current_url if current_url in self.all else self.first self._wizard.storage["current_step"] = step + logger.info(f"steps_helper current getter {step}") return step @current.setter def current(self, step: str): """Sets the current step. Updates step history.""" if step in self.all: + logger.info(f"steps_helper current setter {step}") self._wizard.storage["current_step"] = step else: logger.debug("Invalid step name %s given to StepHelper" % str(step)) @@ -97,11 +106,13 @@ class StepsHelper: @property def first(self): """Returns the name of the first step.""" + logger.info(f"steps_helper first {self.all[0]}") return self.all[0] @property def last(self): """Returns the name of the last step.""" + logger.info(f"steps_helper last {self.all[-1]}") return self.all[-1] @property @@ -110,6 +121,7 @@ class StepsHelper: steps = self.all index = steps.index(self.current) + 1 if index < self.count: + logger.info(f"steps_helper next {steps[index]}") return steps[index] return None @@ -119,6 +131,7 @@ class StepsHelper: steps = self.all key = steps.index(self.current) - 1 if key >= 0: + logger.info(f"steps_helper prev {steps[key]}") return steps[key] return None @@ -127,10 +140,12 @@ class StepsHelper: """Returns the index for the current step.""" steps = self.all if self.current in steps: + logger.info(f"steps_helper index {steps.index(self)}") return steps.index(self) return None @property def history(self): """Returns the list of already visited steps.""" + logger.info(f"steps_helper history {self._wizard.storage.setdefault('step_history', [])}") return self._wizard.storage.setdefault("step_history", []) From da73edefb47044d41e8f8309e17374cadb2da741 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 31 Jan 2024 10:31:43 -0700 Subject: [PATCH 019/119] Change style --- .../assets/sass/_theme/_buttons.scss | 11 +++++----- src/registrar/templates/home.html | 22 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 6aa2995fd..81ae2f0d7 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -133,10 +133,9 @@ a.usa-button--unstyled:visited { } } -// TODO: find another file for this -.info-button span.usa-tooltip svg.usa-icon { - svg.usa-icon{ - transform: translateY(2px) !important; - background: transparent; - } +// TODO: find another file for this +.info-tooltip { + transform: translateY(4px) !important; + display: inline-block; + background: transparent; } diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 60aa1cd55..dd7e70fbb 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -45,7 +45,7 @@ {{ domain.expiration_date|date }} - - - + + + + + From e2e4ad9d02075ae2f268d5c52d8f281598f6ef78 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 31 Jan 2024 11:29:11 -0700 Subject: [PATCH 020/119] Align icon --- .../assets/sass/_theme/_buttons.scss | 7 ------- src/registrar/templates/home.html | 20 +++++++++---------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 81ae2f0d7..02089ec6d 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -132,10 +132,3 @@ a.usa-button--unstyled:visited { margin-left: units(2); } } - -// TODO: find another file for this -.info-tooltip { - transform: translateY(4px) !important; - display: inline-block; - background: transparent; -} diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index dd7e70fbb..f8d1332ef 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -58,17 +58,15 @@ {% else %} {{ domain.state|capfirst }} {% endif %} - - - - - + + + From 5debd67a897c6e050cf0d3bdf568466d6ba9e638 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 31 Jan 2024 11:46:14 -0700 Subject: [PATCH 021/119] Add color --- src/registrar/templates/home.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index f8d1332ef..50fba8cc5 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -59,7 +59,7 @@ {{ domain.state|capfirst }} {% endif %} Date: Wed, 31 Jan 2024 11:55:27 -0700 Subject: [PATCH 022/119] Styles --- src/registrar/assets/sass/_theme/_buttons.scss | 4 ++-- src/registrar/assets/sass/_theme/_tables.scss | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 2f4121399..ef8635b95 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -53,8 +53,8 @@ a.usa-button--unstyled.disabled-link:focus { } .usa-button--unstyled.disabled-button, -.usa-button--unstyled.disabled-link:hover, -.usa-button--unstyled.disabled-link:focus { +.usa-button--unstyled.disabled-button:hover, +.usa-button--unstyled.disabled-button:focus { cursor: not-allowed !important; outline: none !important; text-decoration: none !important; diff --git a/src/registrar/assets/sass/_theme/_tables.scss b/src/registrar/assets/sass/_theme/_tables.scss index 84c4791e5..1ba24baba 100644 --- a/src/registrar/assets/sass/_theme/_tables.scss +++ b/src/registrar/assets/sass/_theme/_tables.scss @@ -26,6 +26,13 @@ padding-bottom: units(2px); } + td { + .no-click-outline, .no-click-outline:hover, .no-click-outline:focus{ + cursor: default !important; + outline: none !important; + } + } + // Ticket #1510 // @include at-media('desktop') { // th:first-child { From 340e395c0013325c34173dcf5b3407f07237c33e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:54:32 -0700 Subject: [PATCH 023/119] Outline styling, refactor help text logic --- src/registrar/assets/sass/_theme/_tables.scss | 6 ++- src/registrar/models/domain.py | 49 +++++++++++-------- src/registrar/templates/home.html | 4 +- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_tables.scss b/src/registrar/assets/sass/_theme/_tables.scss index 1ba24baba..45e47cee3 100644 --- a/src/registrar/assets/sass/_theme/_tables.scss +++ b/src/registrar/assets/sass/_theme/_tables.scss @@ -28,8 +28,10 @@ td { .no-click-outline, .no-click-outline:hover, .no-click-outline:focus{ - cursor: default !important; - outline: none !important; + outline: none; + } + .cursor-help{ + cursor: help; } } diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index e84221a3f..542d063ed 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -139,6 +139,33 @@ class Domain(TimeStampedModel, DomainHelper): # previously existed but has been deleted from the registry DELETED = "deleted", "Deleted" + @classmethod + def get_help_text(cls, state) -> str: + """Returns a help message for a desired state. If none is found, an empty string is returned""" + help_texts = { + # For now, unknown has the same message as DNS_NEEDED + cls.UNKNOWN:( + "Before this domain can be used, " + "you’ll need to add name server addresses." + ), + cls.DNS_NEEDED: ( + "Before this domain can be used, " + "you’ll need to add name server addresses." + ), + cls.READY: "This domain has name servers and is ready for use.", + cls.ON_HOLD: ( + "This domain is administratively paused, " + "so it can’t be edited and won’t resolve in DNS. " + "Contact help@get.gov for details." + ), + cls.DELETED: ( + "This domain has been removed and " + "is no longer registered to your organization." + ) + } + + return help_texts.get(state, "") + class Cache(property): """ Python descriptor to turn class methods into properties. @@ -1399,27 +1426,7 @@ class Domain(TimeStampedModel, DomainHelper): def get_state_help_text(self) -> str: """Returns a str containing additional information about a given state""" - help_text = "" - print(f"self state is {self.state}") - match self.state: - case self.State.DNS_NEEDED | self.State.UNKNOWN: - help_text = ( - "Before this domain can be used, " - "you’ll need to add name server addresses." - ) - case self.State.READY: - help_text = "This domain has name servers and is ready for use." - case self.State.ON_HOLD: - help_text = ( - "This domain is administratively paused, " - "so it can’t be edited and won’t resolve in DNS. " - "Contact help@get.gov for details." - ) - case self.State.DELETED: - help_text = ( - "This domain has been removed and " - "is no longer registered to your organization." - ) + help_text = Domain.State.get_help_text(self.state) return help_text def _disclose_fields(self, contact: PublicContact): diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 8f77748a0..9a1d550a4 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -48,7 +48,7 @@ {{ domain.expiration_date|date }} - Date: Wed, 31 Jan 2024 13:31:54 -0700 Subject: [PATCH 024/119] Add max width to tooltip --- src/registrar/assets/sass/_theme/_base.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index b6d13cee3..afaf0c1df 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -129,3 +129,8 @@ abbr[title] { .flex-end { align-items: flex-end; } + +.usa-tooltip__body { + max-width: 320px; + white-space: normal; +} From 6572ee8d0ccd4fc56578331bcd55019d919e0038 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 31 Jan 2024 13:48:38 -0700 Subject: [PATCH 025/119] Test --- src/registrar/assets/sass/_theme/_base.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index afaf0c1df..6bd690188 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -131,6 +131,7 @@ abbr[title] { } .usa-tooltip__body { - max-width: 320px; + min-width: 320px; + max-width: 350px; white-space: normal; } From 4de78042471cc2577b98a81f21c7cced51d8e674 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 31 Jan 2024 14:12:49 -0700 Subject: [PATCH 026/119] Disable pointer events on svg --- src/registrar/assets/sass/_theme/_base.scss | 6 ++++++ src/registrar/templates/home.html | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 6bd690188..fa1f0e210 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -135,3 +135,9 @@ abbr[title] { max-width: 350px; white-space: normal; } + +// USWDS has weird interactions with SVGs regarding tooltips, +// and other components. In this event, we need to disable pointer interactions. +.disable-pointer-events { + pointer-events: none; +} diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 9a1d550a4..566a2d102 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -68,7 +68,7 @@ title="{{domain.get_state_help_text}}" focusable="true" > - + From 3fe47296a92427b25dab3ea922c5d80bb55772a4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 1 Feb 2024 10:02:52 -0700 Subject: [PATCH 027/119] Update run.sh --- src/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/run.sh b/src/run.sh index 487c54591..d8dabe0cf 100755 --- a/src/run.sh +++ b/src/run.sh @@ -6,4 +6,4 @@ set -o pipefail # Make sure that django's `collectstatic` has been run locally before pushing up to any environment, # so that the styles and static assets to show up correctly on any environment. -gunicorn registrar.config.wsgi -t 60 +gunicorn --worker-class=gevent --worker-connections=1000 --workers=3 registrar.config.wsgi -t 60 From cfc9a2d1695c2c6c65920225bef4ac7a52daeb72 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 1 Feb 2024 11:55:35 -0700 Subject: [PATCH 028/119] Test one worker --- src/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/run.sh b/src/run.sh index d8dabe0cf..e7512b28d 100755 --- a/src/run.sh +++ b/src/run.sh @@ -6,4 +6,4 @@ set -o pipefail # Make sure that django's `collectstatic` has been run locally before pushing up to any environment, # so that the styles and static assets to show up correctly on any environment. -gunicorn --worker-class=gevent --worker-connections=1000 --workers=3 registrar.config.wsgi -t 60 +gunicorn --worker-class=gevent --worker-connections=1000 --workers=1 registrar.config.wsgi -t 60 From 0653ee838512cbdaa0971deed6061c5dd1a9355b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 1 Feb 2024 12:09:51 -0700 Subject: [PATCH 029/119] Add sleep to mimic --- src/registrar/views/domain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 313762ef1..48d537562 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -6,7 +6,7 @@ inherit from `DomainPermissionView` (or DomainInvitationPermissionDeleteView). """ import logging - +import time from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin from django.db import IntegrityError @@ -150,6 +150,7 @@ class DomainView(DomainBaseView): context["security_email"] = None return context context["security_email"] = security_email + time.sleep(100) return context def in_editable_state(self, pk): From 0f41d64e0e342098598b4023e98dccc60a09787b Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 1 Feb 2024 12:01:49 -0800 Subject: [PATCH 030/119] Update order of the column headers --- src/registrar/utility/csv_export.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index f9608f553..1ea9131e0 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -77,6 +77,8 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None # create a dictionary of fields which can be included in output FIELDS = { "Domain name": domain.name, + "Status": domain.get_state_display(), + "Expiration date": domain.expiration_date, "Domain type": domain_type, "Agency": domain_info.federal_agency, "Organization name": domain_info.organization_name, @@ -85,8 +87,6 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None "AO": domain_info.ao, # type: ignore "AO email": domain_info.authorizing_official.email if domain_info.authorizing_official else " ", "Security contact email": security_email, - "Status": domain.get_state_display(), - "Expiration date": domain.expiration_date, "Created at": domain.created_at, "First ready": domain.first_ready, "Deleted": domain.deleted, @@ -152,6 +152,8 @@ def export_data_type_to_csv(csv_file): # define columns to include in export columns = [ "Domain name", + "Status", + "Expiration date", "Domain type", "Agency", "Organization name", @@ -160,8 +162,6 @@ def export_data_type_to_csv(csv_file): "AO", "AO email", "Security contact email", - "Status", - "Expiration date", ] # Coalesce is used to replace federal_type of None with ZZZZZ sort_fields = [ From 1130b5db8eaec794e6b2643e24a0e083b1ad5f2b Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 1 Feb 2024 17:23:09 -0500 Subject: [PATCH 031/119] unit tests --- .../templates/application_sidebar.html | 29 ++-- src/registrar/tests/test_views.py | 148 +++++++++++++++++- src/registrar/views/application.py | 108 +++++-------- src/registrar/views/utility/steps_helper.py | 15 -- 4 files changed, 200 insertions(+), 100 deletions(-) diff --git a/src/registrar/templates/application_sidebar.html b/src/registrar/templates/application_sidebar.html index 318bea366..da55a623e 100644 --- a/src/registrar/templates/application_sidebar.html +++ b/src/registrar/templates/application_sidebar.html @@ -4,24 +4,27 @@ - \ No newline at end of file + From 24954ebdce62e02e7d8c8d263722d01f12523f2e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 2 Feb 2024 10:57:11 -0700 Subject: [PATCH 041/119] Add text for expired --- src/registrar/models/domain.py | 13 +++++++++++-- src/registrar/templates/domain_detail.html | 7 ++++++- src/registrar/tests/test_views.py | 8 ++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 542d063ed..70b3ac2e5 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1425,8 +1425,17 @@ class Domain(TimeStampedModel, DomainHelper): logger.info("able to transition to DNS_NEEDED state") def get_state_help_text(self) -> str: - """Returns a str containing additional information about a given state""" - help_text = Domain.State.get_help_text(self.state) + """Returns a str containing additional information about a given state. + Returns custom content for when the domain itself is expired.""" + if not self.is_expired(): + help_text = Domain.State.get_help_text(self.state) + else: + # Given expired is not a physical state, but it is displayed as such, + # We need custom logic to determine this message. + help_text = ( + "This domain has expired, but it is still online. " + "To renew this domain, contact help@get.gov." + ) return help_text def _disclose_fields(self, contact: PublicContact): diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index 09fc189e4..fe9062a23 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -6,7 +6,7 @@
@@ -25,6 +25,11 @@ {% else %} {{ domain.state|title }} {% endif %} + {% if domain.get_state_help_text %} +
+ {{ domain.get_state_help_text }} +
+ {% endif %}

diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 0531d81d5..ba28a7a56 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -128,6 +128,10 @@ class LoggedInTests(TestWithUser): "This domain has been removed and " "is no longer registered to your organization." ) + expired_text = ( + "This domain has expired, but it is still online. " + "To renew this domain, contact help@get.gov." + ) # Generate a mapping of domain names, the state, and expected messages for the subtest test_cases = [ ("deleted.gov", Domain.State.DELETED, deleted_text), @@ -135,12 +139,16 @@ class LoggedInTests(TestWithUser): ("unknown.gov", Domain.State.UNKNOWN, dns_needed_text), ("onhold.gov", Domain.State.ON_HOLD, on_hold_text), ("ready.gov", Domain.State.READY, ready_text), + ("expired.gov", Domain.State.READY, expired_text) ] for domain_name, state, expected_message in test_cases: with self.subTest(domain_name=domain_name, state=state, expected_message=expected_message): # Create a domain and a UserRole with the given params test_domain, _ = Domain.objects.get_or_create(name=domain_name, state=state) + if domain_name == "expired.gov": + test_domain.expiration_date = date(2011, 10, 10) + test_domain.save() user_role, _ = UserDomainRole.objects.get_or_create( user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER ) From c0e866011b2e39c483a64a63eda481c331e2a766 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 2 Feb 2024 11:52:08 -0700 Subject: [PATCH 042/119] Changes --- src/registrar/public/sass/_theme/_base.scss | 141 ++++++++++++++++++++ src/registrar/templates/home.html | 1 - 2 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 src/registrar/public/sass/_theme/_base.scss diff --git a/src/registrar/public/sass/_theme/_base.scss b/src/registrar/public/sass/_theme/_base.scss new file mode 100644 index 000000000..a7a4bf8a9 --- /dev/null +++ b/src/registrar/public/sass/_theme/_base.scss @@ -0,0 +1,141 @@ +@use "uswds-core" as *; + +/* Styles for making visible to screen reader / AT users only. */ +.sr-only { + @include sr-only; +} + +.clear-both { + clear: both; +} + +* { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +#wrapper { + flex-grow: 1; + padding-top: units(3); + padding-bottom: units(6) * 2 ; //Workaround because USWDS units jump from 10 to 15 +} + +#wrapper.dashboard { + background-color: color('primary-lightest'); + padding-top: units(5); +} + +.usa-logo { + @include at-media(desktop) { + margin-top: units(2); + } +} + +.usa-logo__text { + @include typeset('sans', 'xl', 2); + color: color('primary-darker'); +} + +.usa-nav__primary { + margin-top:units(1); +} + +.section--outlined { + background-color: color('white'); + border: 1px solid color('base-lighter'); + border-radius: 4px; + padding: 0 units(2) units(3); + margin-top: units(3); + + h2 { + color: color('primary-dark'); + margin-top: units(2); + margin-bottom: units(2); + } + + p { + margin-bottom: 0; + } + + @include at-media(mobile-lg) { + margin-top: units(5); + + h2 { + margin-bottom: 0; + } + } +} + +.break-word { + word-break: break-word; +} + +.dotgov-status-box { + background-color: color('primary-lightest'); + border-color: color('accent-cool-lighter'); +} + +.dotgov-status-box--action-need { + background-color: color('warning-lighter'); + border-color: color('warning'); +} + +footer { + border-top: 1px solid color('primary-darker'); +} + +.usa-footer__secondary-section { + background-color: color('primary-lightest'); +} + +.usa-footer__secondary-section a { + color: color('primary'); +} + +.usa-identifier__logo { + height: units(7); +} + +abbr[title] { + // workaround for underlining abbr element + border-bottom: none; + text-decoration: none; +} + +@include at-media(tablet) { + .float-right-tablet { + float: right; + } + .float-left-tablet { + float: left; + } +} + +@include at-media(desktop) { + .float-right-desktop { + float: right; + } + .float-left-desktop { + float: left; + } +} + +.flex-end { + align-items: flex-end; +} + +.usa-tooltip__body { + min-width: 320px; +} + +// USWDS has weird interactions with SVGs regarding tooltips, +// and other components. In this event, we need to disable pointer interactions. +.disable-pointer-events { + pointer-events: none; +} diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index b5dab701b..fecb05540 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -49,7 +49,6 @@ {{ domain.expiration_date|date }} {# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #} From a50462353fccc90983a864fdea6629def69a84e0 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 2 Feb 2024 11:53:40 -0700 Subject: [PATCH 043/119] Remove accidental addition --- src/registrar/assets/sass/_theme/_base.scss | 2 - src/registrar/public/sass/_theme/_base.scss | 141 -------------------- 2 files changed, 143 deletions(-) delete mode 100644 src/registrar/public/sass/_theme/_base.scss diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index fa1f0e210..a7a4bf8a9 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -132,8 +132,6 @@ abbr[title] { .usa-tooltip__body { min-width: 320px; - max-width: 350px; - white-space: normal; } // USWDS has weird interactions with SVGs regarding tooltips, diff --git a/src/registrar/public/sass/_theme/_base.scss b/src/registrar/public/sass/_theme/_base.scss deleted file mode 100644 index a7a4bf8a9..000000000 --- a/src/registrar/public/sass/_theme/_base.scss +++ /dev/null @@ -1,141 +0,0 @@ -@use "uswds-core" as *; - -/* Styles for making visible to screen reader / AT users only. */ -.sr-only { - @include sr-only; -} - -.clear-both { - clear: both; -} - -* { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -#wrapper { - flex-grow: 1; - padding-top: units(3); - padding-bottom: units(6) * 2 ; //Workaround because USWDS units jump from 10 to 15 -} - -#wrapper.dashboard { - background-color: color('primary-lightest'); - padding-top: units(5); -} - -.usa-logo { - @include at-media(desktop) { - margin-top: units(2); - } -} - -.usa-logo__text { - @include typeset('sans', 'xl', 2); - color: color('primary-darker'); -} - -.usa-nav__primary { - margin-top:units(1); -} - -.section--outlined { - background-color: color('white'); - border: 1px solid color('base-lighter'); - border-radius: 4px; - padding: 0 units(2) units(3); - margin-top: units(3); - - h2 { - color: color('primary-dark'); - margin-top: units(2); - margin-bottom: units(2); - } - - p { - margin-bottom: 0; - } - - @include at-media(mobile-lg) { - margin-top: units(5); - - h2 { - margin-bottom: 0; - } - } -} - -.break-word { - word-break: break-word; -} - -.dotgov-status-box { - background-color: color('primary-lightest'); - border-color: color('accent-cool-lighter'); -} - -.dotgov-status-box--action-need { - background-color: color('warning-lighter'); - border-color: color('warning'); -} - -footer { - border-top: 1px solid color('primary-darker'); -} - -.usa-footer__secondary-section { - background-color: color('primary-lightest'); -} - -.usa-footer__secondary-section a { - color: color('primary'); -} - -.usa-identifier__logo { - height: units(7); -} - -abbr[title] { - // workaround for underlining abbr element - border-bottom: none; - text-decoration: none; -} - -@include at-media(tablet) { - .float-right-tablet { - float: right; - } - .float-left-tablet { - float: left; - } -} - -@include at-media(desktop) { - .float-right-desktop { - float: right; - } - .float-left-desktop { - float: left; - } -} - -.flex-end { - align-items: flex-end; -} - -.usa-tooltip__body { - min-width: 320px; -} - -// USWDS has weird interactions with SVGs regarding tooltips, -// and other components. In this event, we need to disable pointer interactions. -.disable-pointer-events { - pointer-events: none; -} From 0b661f37141d780ed236ed971754f3a9fe1915a2 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 2 Feb 2024 14:10:45 -0500 Subject: [PATCH 044/119] added a comment --- src/epplibwrapper/utility/pool.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index 56ed46143..4f54e14ce 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -86,6 +86,8 @@ class EPPConnectionPool(ConnectionPool): raise PoolError(code=PoolErrorCodes.KEEP_ALIVE_FAILED) from err def _keepalive_periodic(self): + """Overriding _keepalive_periodic from geventconnpool so that PoolErrors + are properly handled, as opposed to printing to stdout""" delay = float(self.keepalive) / self.size while 1: try: From bb3e71380fd46da032f6f53a62e0b9e5072c5b66 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 2 Feb 2024 12:23:32 -0700 Subject: [PATCH 045/119] Fix test cases --- src/registrar/assets/sass/_theme/_base.scss | 10 ++++-- src/registrar/templates/home.html | 4 +-- src/registrar/tests/test_views.py | 35 ++++++++++++++++----- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index a7a4bf8a9..ca4c03de6 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -130,10 +130,14 @@ abbr[title] { align-items: flex-end; } -.usa-tooltip__body { - min-width: 320px; +// Only apply this custom wrapping to desktop +@media (min-width: 768px) { + .usa-tooltip__body { + min-width: 320px; + max-width: 350px; + white-space: normal; + } } - // USWDS has weird interactions with SVGs regarding tooltips, // and other components. In this event, we need to disable pointer interactions. .disable-pointer-events { diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index fecb05540..44023fc7d 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -48,9 +48,7 @@ {{ domain.expiration_date|date }} - + {# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #} {% if domain.is_expired and domain.state != domain.State.UNKNOWN %} Expired diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index ba28a7a56..7dc11f9ec 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -128,10 +128,6 @@ class LoggedInTests(TestWithUser): "This domain has been removed and " "is no longer registered to your organization." ) - expired_text = ( - "This domain has expired, but it is still online. " - "To renew this domain, contact help@get.gov." - ) # Generate a mapping of domain names, the state, and expected messages for the subtest test_cases = [ ("deleted.gov", Domain.State.DELETED, deleted_text), @@ -139,16 +135,15 @@ class LoggedInTests(TestWithUser): ("unknown.gov", Domain.State.UNKNOWN, dns_needed_text), ("onhold.gov", Domain.State.ON_HOLD, on_hold_text), ("ready.gov", Domain.State.READY, ready_text), - ("expired.gov", Domain.State.READY, expired_text) ] for domain_name, state, expected_message in test_cases: with self.subTest(domain_name=domain_name, state=state, expected_message=expected_message): # Create a domain and a UserRole with the given params test_domain, _ = Domain.objects.get_or_create(name=domain_name, state=state) - if domain_name == "expired.gov": - test_domain.expiration_date = date(2011, 10, 10) - test_domain.save() + test_domain.expiration_date = date.today() + test_domain.save() + user_role, _ = UserDomainRole.objects.get_or_create( user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER ) @@ -167,6 +162,30 @@ class LoggedInTests(TestWithUser): user_role.delete() test_domain.delete() + def test_state_help_text_expired(self): + """Tests if each domain state has help text when expired""" + expired_text = ( + "This domain has expired, but it is still online. " + "To renew this domain, contact help@get.gov." + ) + test_domain, _ = Domain.objects.get_or_create(name="expired.gov", state=Domain.State.READY) + test_domain.expiration_date = date(2011, 10, 10) + test_domain.save() + + UserDomainRole.objects.get_or_create( + user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER + ) + + # Grab the home page + response = self.client.get("/") + + # Make sure the user can actually see the domain. + # We expect two instances because of SR content. + self.assertContains(response, "expired.gov", count=2) + + # Check that we have the right text content. + self.assertContains(response, expired_text, count=1) + def test_home_deletes_withdrawn_domain_application(self): """Tests if the user can delete a DomainApplication in the 'withdrawn' status""" From 6dd846f8b68c73898d3e19e875816d8c0039f5f3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 2 Feb 2024 12:30:52 -0700 Subject: [PATCH 046/119] Linting --- src/registrar/models/domain.py | 20 +++++-------------- src/registrar/tests/test_views.py | 33 +++++++++---------------------- 2 files changed, 14 insertions(+), 39 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 70b3ac2e5..9fd5d5490 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -144,24 +144,15 @@ class Domain(TimeStampedModel, DomainHelper): """Returns a help message for a desired state. If none is found, an empty string is returned""" help_texts = { # For now, unknown has the same message as DNS_NEEDED - cls.UNKNOWN:( - "Before this domain can be used, " - "you’ll need to add name server addresses." - ), - cls.DNS_NEEDED: ( - "Before this domain can be used, " - "you’ll need to add name server addresses." - ), + cls.UNKNOWN: ("Before this domain can be used, " "you’ll need to add name server addresses."), + cls.DNS_NEEDED: ("Before this domain can be used, " "you’ll need to add name server addresses."), cls.READY: "This domain has name servers and is ready for use.", cls.ON_HOLD: ( - "This domain is administratively paused, " + "This domain is administratively paused, " "so it can’t be edited and won’t resolve in DNS. " "Contact help@get.gov for details." ), - cls.DELETED: ( - "This domain has been removed and " - "is no longer registered to your organization." - ) + cls.DELETED: ("This domain has been removed and " "is no longer registered to your organization."), } return help_texts.get(state, "") @@ -1433,8 +1424,7 @@ class Domain(TimeStampedModel, DomainHelper): # Given expired is not a physical state, but it is displayed as such, # We need custom logic to determine this message. help_text = ( - "This domain has expired, but it is still online. " - "To renew this domain, contact help@get.gov." + "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov." ) return help_text diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 7dc11f9ec..ad7c54bd7 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -6,7 +6,7 @@ from django.test import Client, TestCase from django.urls import reverse from django.contrib.auth import get_user_model -from .common import AuditedAdminMockData, MockEppLib, MockSESClient, completed_application, create_user, generic_domain_object # type: ignore +from .common import MockEppLib, MockSESClient, completed_application, create_user # type: ignore from django_webtest import WebTest # type: ignore import boto3_mocking # type: ignore @@ -105,29 +105,20 @@ class LoggedInTests(TestWithUser): # clean up application.delete() - + def test_state_help_text(self): """Tests if each domain state has help text""" - + # Get the expected text content of each state - deleted_text = ( - "Before this domain can be used, " - "you’ll need to add name server addresses." - ) - dns_needed_text = ( - "Before this domain can be used, " - "you’ll need to add name server addresses." - ) + deleted_text = "Before this domain can be used, " "you’ll need to add name server addresses." + dns_needed_text = "Before this domain can be used, " "you’ll need to add name server addresses." ready_text = "This domain has name servers and is ready for use." on_hold_text = ( - "This domain is administratively paused, " + "This domain is administratively paused, " "so it can’t be edited and won’t resolve in DNS. " "Contact help@get.gov for details." ) - deleted_text = ( - "This domain has been removed and " - "is no longer registered to your organization." - ) + deleted_text = "This domain has been removed and " "is no longer registered to your organization." # Generate a mapping of domain names, the state, and expected messages for the subtest test_cases = [ ("deleted.gov", Domain.State.DELETED, deleted_text), @@ -138,7 +129,6 @@ class LoggedInTests(TestWithUser): ] for domain_name, state, expected_message in test_cases: with self.subTest(domain_name=domain_name, state=state, expected_message=expected_message): - # Create a domain and a UserRole with the given params test_domain, _ = Domain.objects.get_or_create(name=domain_name, state=state) test_domain.expiration_date = date.today() @@ -164,17 +154,12 @@ class LoggedInTests(TestWithUser): def test_state_help_text_expired(self): """Tests if each domain state has help text when expired""" - expired_text = ( - "This domain has expired, but it is still online. " - "To renew this domain, contact help@get.gov." - ) + expired_text = "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov." test_domain, _ = Domain.objects.get_or_create(name="expired.gov", state=Domain.State.READY) test_domain.expiration_date = date(2011, 10, 10) test_domain.save() - UserDomainRole.objects.get_or_create( - user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER - ) + UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER) # Grab the home page response = self.client.get("/") From e27054cd259b5246891faa9655eaae04c3ecdbcc Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 2 Feb 2024 13:02:52 -0700 Subject: [PATCH 047/119] Add styles for desktop and mobile --- src/registrar/assets/sass/_theme/_base.scss | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index ca4c03de6..b5f4c5c6f 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -131,11 +131,21 @@ abbr[title] { } // Only apply this custom wrapping to desktop -@media (min-width: 768px) { +@include at-media(desktop) { .usa-tooltip__body { - min-width: 320px; + min-width: 350px; max-width: 350px; white-space: normal; + text-align: center; + } +} + +@media (min-width: 768px) { + .usa-tooltip__body { + min-width: 250px; + max-width: 250px; + white-space: normal; + text-align: center; } } // USWDS has weird interactions with SVGs regarding tooltips, From ad6d080ad94ef7726d88d38adaa58b02cbbecc5e Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 2 Feb 2024 15:11:22 -0500 Subject: [PATCH 048/119] Add non-prod banner --- src/registrar/assets/sass/_theme/_alerts.scss | 5 +- src/registrar/templates/admin/base_site.html | 79 ++++++++++++------- src/registrar/templates/base.html | 4 + .../includes/non-production-alert.html | 5 ++ .../test_environment_variables_effects.py | 29 +++++++ 5 files changed, 93 insertions(+), 29 deletions(-) create mode 100644 src/registrar/templates/includes/non-production-alert.html create mode 100644 src/registrar/tests/test_environment_variables_effects.py diff --git a/src/registrar/assets/sass/_theme/_alerts.scss b/src/registrar/assets/sass/_theme/_alerts.scss index 9ee28a357..163f243d3 100644 --- a/src/registrar/assets/sass/_theme/_alerts.scss +++ b/src/registrar/assets/sass/_theme/_alerts.scss @@ -17,5 +17,8 @@ .usa-alert__body::before { left: 1rem !important; } - } + } + .usa-alert__body.margin-left-1 { + margin-left: 0.5rem!important; + } } diff --git a/src/registrar/templates/admin/base_site.html b/src/registrar/templates/admin/base_site.html index c0884c912..f9ff23455 100644 --- a/src/registrar/templates/admin/base_site.html +++ b/src/registrar/templates/admin/base_site.html @@ -24,34 +24,57 @@ {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} {% block extrastyle %}{{ block.super }} - + {% endblock %} -{% block branding %} -

.gov admin

-{% if user.is_anonymous %} - {% include "admin/color_theme_toggle.html" %} -{% endif %} +{% block header %} + {% if not IS_PRODUCTION %} + {% with add_body_class="margin-left-1" %} + {% include "includes/non-production-alert.html" %} + {% endwith %} + {% endif %} + + {# Djando update: this div will change to header #} + {% endblock %} -{% comment %} - This was copied from the 'userlinks' template, with a few minor changes. - You can find that here: - https://github.com/django/django/blob/d25f3892114466d689fd6936f79f3bd9a9acc30e/django/contrib/admin/templates/admin/base.html#L59 -{% endcomment %} -{% block userlinks %} - {% if site_url %} - {% translate 'View site' %} / - {% endif %} - {% if user.is_active and user.is_staff %} - {% url 'django-admindocs-docroot' as docsroot %} - {% if docsroot %} - {% translate 'Documentation' %} / - {% endif %} - {% endif %} - {% if user.has_usable_password %} - {% translate 'Change password' %} / - {% endif %} - {% translate 'Log out' %} - {% include "admin/color_theme_toggle.html" %} - {% endblock %} -{% block nav-global %}{% endblock %} \ No newline at end of file diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index 2786cca22..c0702e78f 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -70,6 +70,10 @@ Skip to main content + {% if not IS_PRODUCTION %} + {% include "includes/non-production-alert.html" %} + {% endif %} +
diff --git a/src/registrar/templates/includes/non-production-alert.html b/src/registrar/templates/includes/non-production-alert.html new file mode 100644 index 000000000..e4ce21437 --- /dev/null +++ b/src/registrar/templates/includes/non-production-alert.html @@ -0,0 +1,5 @@ +
+
+ You are not on production. +
+
diff --git a/src/registrar/tests/test_environment_variables_effects.py b/src/registrar/tests/test_environment_variables_effects.py new file mode 100644 index 000000000..03706f179 --- /dev/null +++ b/src/registrar/tests/test_environment_variables_effects.py @@ -0,0 +1,29 @@ +from django.test import Client, TestCase, override_settings +from django.contrib.auth import get_user_model + + +class MyTestCase(TestCase): + def setUp(self): + self.client = Client() + username = "test_user" + first_name = "First" + last_name = "Last" + email = "info@example.com" + self.user = get_user_model().objects.create( + username=username, first_name=first_name, last_name=last_name, email=email + ) + self.client.force_login(self.user) + + def tearDown(self): + super().tearDown() + self.user.delete() + + @override_settings(IS_PRODUCTION=True) + def test_production_environment(self): + home_page = self.client.get("/") + self.assertNotContains(home_page, "You are not on production.") + + @override_settings(IS_PRODUCTION=False) + def test_non_production_environment(self): + home_page = self.client.get("/") + self.assertContains(home_page, "You are not on production.") From fa8122b2179e85a0b1038ce2e98cb2b60678bd4e Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 2 Feb 2024 15:16:38 -0500 Subject: [PATCH 049/119] Add test defs --- src/registrar/tests/test_environment_variables_effects.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/registrar/tests/test_environment_variables_effects.py b/src/registrar/tests/test_environment_variables_effects.py index 03706f179..9ef065aeb 100644 --- a/src/registrar/tests/test_environment_variables_effects.py +++ b/src/registrar/tests/test_environment_variables_effects.py @@ -20,10 +20,12 @@ class MyTestCase(TestCase): @override_settings(IS_PRODUCTION=True) def test_production_environment(self): + """No banner on prod.""" home_page = self.client.get("/") self.assertNotContains(home_page, "You are not on production.") @override_settings(IS_PRODUCTION=False) def test_non_production_environment(self): + """Banner on non-prod.""" home_page = self.client.get("/") self.assertContains(home_page, "You are not on production.") From 02d147ae114349fc1ea5295bb31a450f51ce467e Mon Sep 17 00:00:00 2001 From: rachidatecs <107004823+rachidatecs@users.noreply.github.com> Date: Mon, 5 Feb 2024 11:56:46 -0500 Subject: [PATCH 050/119] Update src/registrar/templates/includes/non-production-alert.html Co-authored-by: zandercymatics <141044360+zandercymatics@users.noreply.github.com> --- src/registrar/templates/includes/non-production-alert.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/includes/non-production-alert.html b/src/registrar/templates/includes/non-production-alert.html index e4ce21437..4f8aaeac0 100644 --- a/src/registrar/templates/includes/non-production-alert.html +++ b/src/registrar/templates/includes/non-production-alert.html @@ -1,5 +1,5 @@
- You are not on production. + WARNING: You are not on production.
From eac9e1ab6ef7859b65568fc711f916b95e00c4d4 Mon Sep 17 00:00:00 2001 From: Kristina Yin <140533113+kristinacyin@users.noreply.github.com> Date: Mon, 5 Feb 2024 10:19:36 -0800 Subject: [PATCH 051/119] Issue #1471 - Update tab title on request pages (#1724) * change tab title on request pages * Change HTML page title to list the current page first --------- Co-authored-by: Michelle Rago <60157596+michelle-rago@users.noreply.github.com> --- src/registrar/templates/application_form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/application_form.html b/src/registrar/templates/application_form.html index 524045fbe..965967072 100644 --- a/src/registrar/templates/application_form.html +++ b/src/registrar/templates/application_form.html @@ -1,7 +1,7 @@ {% extends 'base.html' %} {% load static form_helpers url_helpers %} -{% block title %}Request a .gov domain | {{form_titles|get_item:steps.current}} | {% endblock %} +{% block title %}{{form_titles|get_item:steps.current}} | Request a .gov | {% endblock %} {% block content %}
From 0a921e14424fa91f30ad435275b5568bb167eb13 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 5 Feb 2024 12:00:31 -0700 Subject: [PATCH 052/119] Fix bug with unknown and add test for it --- src/registrar/models/domain.py | 2 +- src/registrar/tests/test_views.py | 35 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 9fd5d5490..233e619b3 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1418,7 +1418,7 @@ class Domain(TimeStampedModel, DomainHelper): def get_state_help_text(self) -> str: """Returns a str containing additional information about a given state. Returns custom content for when the domain itself is expired.""" - if not self.is_expired(): + if not self.is_expired() and self.state != self.State.UNKNOWN: help_text = Domain.State.get_help_text(self.state) else: # Given expired is not a physical state, but it is displayed as such, diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index ad7c54bd7..ab1dfc87e 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -170,6 +170,41 @@ class LoggedInTests(TestWithUser): # Check that we have the right text content. self.assertContains(response, expired_text, count=1) + + def test_state_help_text_no_expiration_date(self): + """Tests if each domain state has help text when expiration date is None""" + + # == Test a expiration of None for state ready. This should be expired. == # + expired_text = "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov." + test_domain, _ = Domain.objects.get_or_create(name="expired.gov", state=Domain.State.READY) + + UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER) + + # Grab the home page + response = self.client.get("/") + + # Make sure the user can actually see the domain. + # We expect two instances because of SR content. + self.assertContains(response, "expired.gov", count=2) + + # Check that we have the right text content. + self.assertContains(response, expired_text, count=1) + + # == Test a expiration of None for state unknown. This should not display expired text. == # + unknown_text = "Before this domain can be used, " "you’ll need to add name server addresses." + test_domain_2, _ = Domain.objects.get_or_create(name="notexpired.gov", state=Domain.State.UNKNOWN) + + UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain_2, role=UserDomainRole.Roles.MANAGER) + + # Grab the home page + response = self.client.get("/") + + # Make sure the user can actually see the domain. + # We expect two instances because of SR content. + self.assertContains(response, "notexpired.gov", count=2) + + # Check that we have the right text content. + self.assertContains(response, unknown_text, count=1) def test_home_deletes_withdrawn_domain_application(self): """Tests if the user can delete a DomainApplication in the 'withdrawn' status""" From 3888b24af8a3a5ff023bcd85ae8457abf941f06d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 5 Feb 2024 12:02:25 -0700 Subject: [PATCH 053/119] Modify test slightly --- src/registrar/tests/test_views.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index ab1dfc87e..6afe834a1 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -176,7 +176,9 @@ class LoggedInTests(TestWithUser): # == Test a expiration of None for state ready. This should be expired. == # expired_text = "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov." - test_domain, _ = Domain.objects.get_or_create(name="expired.gov", state=Domain.State.READY) + test_domain, _ = Domain.objects.get_or_create(name="imexpired.gov", state=Domain.State.READY) + test_domain.expiration_date = None + test_domain.save() UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER) @@ -185,7 +187,10 @@ class LoggedInTests(TestWithUser): # Make sure the user can actually see the domain. # We expect two instances because of SR content. - self.assertContains(response, "expired.gov", count=2) + self.assertContains(response, "imexpired.gov", count=2) + + # Make sure the expiration date is None + self.assertEqual(test_domain.expiration_date, None) # Check that we have the right text content. self.assertContains(response, expired_text, count=1) @@ -193,6 +198,8 @@ class LoggedInTests(TestWithUser): # == Test a expiration of None for state unknown. This should not display expired text. == # unknown_text = "Before this domain can be used, " "you’ll need to add name server addresses." test_domain_2, _ = Domain.objects.get_or_create(name="notexpired.gov", state=Domain.State.UNKNOWN) + test_domain_2.expiration_date = None + test_domain_2.save() UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain_2, role=UserDomainRole.Roles.MANAGER) @@ -203,6 +210,9 @@ class LoggedInTests(TestWithUser): # We expect two instances because of SR content. self.assertContains(response, "notexpired.gov", count=2) + # Make sure the expiration date is None + self.assertEqual(test_domain_2.expiration_date, None) + # Check that we have the right text content. self.assertContains(response, unknown_text, count=1) From 19af42ca49ce8fd668743c0068453bd05630ee30 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 5 Feb 2024 12:06:46 -0700 Subject: [PATCH 054/119] Fix unit test --- src/registrar/tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 6afe834a1..666b3c962 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -110,7 +110,7 @@ class LoggedInTests(TestWithUser): """Tests if each domain state has help text""" # Get the expected text content of each state - deleted_text = "Before this domain can be used, " "you’ll need to add name server addresses." + deleted_text = "This domain has been removed and " "is no longer registered to your organization." dns_needed_text = "Before this domain can be used, " "you’ll need to add name server addresses." ready_text = "This domain has name servers and is ready for use." on_hold_text = ( From 4110b37865f5cf80b64348aca5c1923281e55b8f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 5 Feb 2024 12:25:19 -0700 Subject: [PATCH 055/119] Linting, fix domain.py --- src/registrar/models/domain.py | 8 +++++--- src/registrar/tests/test_views.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 233e619b3..ca1ba541a 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1418,14 +1418,16 @@ class Domain(TimeStampedModel, DomainHelper): def get_state_help_text(self) -> str: """Returns a str containing additional information about a given state. Returns custom content for when the domain itself is expired.""" - if not self.is_expired() and self.state != self.State.UNKNOWN: - help_text = Domain.State.get_help_text(self.state) - else: + + if self.is_expired() and self.state != self.State.UNKNOWN: # Given expired is not a physical state, but it is displayed as such, # We need custom logic to determine this message. help_text = ( "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov." ) + else: + help_text = Domain.State.get_help_text(self.state) + return help_text def _disclose_fields(self, contact: PublicContact): diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 666b3c962..2ba762b0e 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -170,7 +170,7 @@ class LoggedInTests(TestWithUser): # Check that we have the right text content. self.assertContains(response, expired_text, count=1) - + def test_state_help_text_no_expiration_date(self): """Tests if each domain state has help text when expiration date is None""" From 4437b315b32eca484a024fb3bf1145595edbd0e0 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 5 Feb 2024 12:43:27 -0700 Subject: [PATCH 056/119] Consolidate css --- src/registrar/assets/sass/_theme/_base.scss | 13 +++---------- src/registrar/assets/sass/_theme/_tables.scss | 9 ++++++--- src/registrar/templates/home.html | 4 ++-- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index b5f4c5c6f..347b132a3 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -133,23 +133,16 @@ abbr[title] { // Only apply this custom wrapping to desktop @include at-media(desktop) { .usa-tooltip__body { - min-width: 350px; - max-width: 350px; + width: 350px; white-space: normal; text-align: center; } } -@media (min-width: 768px) { +@media (tablet) { .usa-tooltip__body { - min-width: 250px; - max-width: 250px; + width: 250px; white-space: normal; text-align: center; } } -// USWDS has weird interactions with SVGs regarding tooltips, -// and other components. In this event, we need to disable pointer interactions. -.disable-pointer-events { - pointer-events: none; -} diff --git a/src/registrar/assets/sass/_theme/_tables.scss b/src/registrar/assets/sass/_theme/_tables.scss index 45e47cee3..7b13656e7 100644 --- a/src/registrar/assets/sass/_theme/_tables.scss +++ b/src/registrar/assets/sass/_theme/_tables.scss @@ -27,11 +27,14 @@ } td { - .no-click-outline, .no-click-outline:hover, .no-click-outline:focus{ + .no-click-outline-and-cursor-help{ outline: none; - } - .cursor-help{ cursor: help; + use { + // USWDS has weird interactions with SVGs regarding tooltips, + // and other components. In this event, we need to disable pointer interactions. + pointer-events: none; + } } } diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 44023fc7d..699a82c61 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -58,13 +58,13 @@ {{ domain.state|capfirst }} {% endif %} - + From c8c5fc8134a07b80e85f117a67cc0de874a81d84 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 5 Feb 2024 14:19:39 -0700 Subject: [PATCH 057/119] CSS change --- src/registrar/assets/sass/_theme/_base.scss | 2 +- src/registrar/templates/home.html | 36 ++++++++++----------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 347b132a3..0a62009e7 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -139,7 +139,7 @@ abbr[title] { } } -@media (tablet) { +@include at-media(tablet) { .usa-tooltip__body { width: 250px; white-space: normal; diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 699a82c61..0747c9985 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -48,25 +48,23 @@ {{ domain.expiration_date|date }} - - {# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #} - {% if domain.is_expired and domain.state != domain.State.UNKNOWN %} - Expired - {% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %} - DNS needed - {% else %} - {{ domain.state|capfirst }} - {% endif %} - - - - + {# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #} + {% if domain.is_expired and domain.state != domain.State.UNKNOWN %} + Expired + {% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %} + DNS needed + {% else %} + {{ domain.state|capfirst }} + {% endif %} + + + From 81c8fe97fad0da26faa745536753e3100e5d0956 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 5 Feb 2024 17:24:39 -0500 Subject: [PATCH 058/119] solidify the bool checks on db_check_for_unlocking_steps --- src/registrar/tests/test_views.py | 4 +-- src/registrar/utility/csv_export.py | 16 ++++++++++ src/registrar/views/application.py | 46 +++++++++++++++-------------- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 8c992036f..66ed27bbc 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -2431,7 +2431,7 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest): self.assertContains(detail_page, "link_usa-checked", count=11) else: - self.fail("Expected a redirect, but got a different response") + self.fail(f"Expected a redirect, but got a different response: {response}") def test_unlocked_steps_partial_application(self): """Test when some fields in the application are filled.""" @@ -2498,7 +2498,7 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest): self.assertContains(detail_page, "link_usa-checked", count=5) else: - self.fail("Expected a redirect, but got a different response") + self.fail(f"Expected a redirect, but got a different response: {response}") class TestWithDomainPermissions(TestWithUser): diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index f9608f553..0046058e6 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -92,6 +92,12 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None "Deleted": domain.deleted, } + # user_emails = [user.email for user in domain.permissions] + + # Dynamically add user emails to the FIELDS dictionary + # for i, user_email in enumerate(user_emails, start=1): + # FIELDS[f"User{i} email"] = user_email + row = [FIELDS.get(column, "") for column in columns] return row @@ -127,6 +133,16 @@ def write_body( else: logger.warning("csv_export -> Domain was none for PublicContact") + # all_user_nums = 0 + # for domain_info in all_domain_infos: + # user_num = len(domain_info.domain.permissions) + # all_user_nums.append(user_num) + + # if user_num > highest_user_nums: + # highest_user_nums = user_num + + # Build the header here passing to it highest_user_nums + # Reduce the memory overhead when performing the write operation paginator = Paginator(all_domain_infos, 1000) for page_num in paginator.page_range: diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index a6b9134d4..b71018d81 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -338,35 +338,37 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): def db_check_for_unlocking_steps(self): """Helper for get_context_data - Queries the DB for an application and returns a dict for unlocked steps.""" + Queries the DB for an application and returns a list of unlocked steps.""" history_dict = { - "organization_type": bool(self.application.organization_type), - "tribal_government": bool(self.application.tribe_name), - "organization_federal": bool(self.application.federal_type), - "organization_election": bool(self.application.is_election_board), + "organization_type": self.application.organization_type is not None, + "tribal_government": self.application.tribe_name is not None, + "organization_federal": self.application.federal_type is not None, + "organization_election": self.application.is_election_board is not None, "organization_contact": ( - bool(self.application.federal_agency) - or bool(self.application.organization_name) - or bool(self.application.address_line1) - or bool(self.application.city) - or bool(self.application.state_territory) - or bool(self.application.zipcode) - or bool(self.application.urbanization) + self.application.federal_agency is not None + or self.application.organization_name is not None + or self.application.address_line1 is not None + or self.application.city is not None + or self.application.state_territory is not None + or self.application.zipcode is not None + or self.application.urbanization is not None ), - "about_your_organization": bool(self.application.about_your_organization), - "authorizing_official": bool(self.application.authorizing_official), + "about_your_organization": self.application.about_your_organization is not None, + "authorizing_official": self.application.authorizing_official is not None, "current_sites": ( - bool(self.application.current_websites.exists()) or bool(self.application.requested_domain) + self.application.current_websites.exists() or self.application.requested_domain is not None ), - "dotgov_domain": bool(self.application.requested_domain), - "purpose": bool(self.application.purpose), - "your_contact": bool(self.application.submitter), + "dotgov_domain": self.application.requested_domain is not None, + "purpose": self.application.purpose is not None, + "your_contact": self.application.submitter is not None, "other_contacts": ( - bool(self.application.other_contacts.exists()) or bool(self.application.no_other_contacts_rationale) + self.application.other_contacts.exists() or self.application.no_other_contacts_rationale is not None ), - "anything_else": (bool(self.application.anything_else) or bool(self.application.is_policy_acknowledged)), - "requirements": bool(self.application.is_policy_acknowledged), - "review": bool(self.application.is_policy_acknowledged), + "anything_else": ( + self.application.anything_else is not None or self.application.is_policy_acknowledged is not None + ), + "requirements": self.application.is_policy_acknowledged is not None, + "review": self.application.is_policy_acknowledged is not None, } return [key for key, value in history_dict.items() if value] From 19764e81511f40fafeb17dff34181788f044fb48 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 5 Feb 2024 19:14:09 -0700 Subject: [PATCH 059/119] Added fake submitted dates to fixtures --- src/registrar/fixtures_applications.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/fixtures_applications.py b/src/registrar/fixtures_applications.py index 92094b876..9519dfce9 100644 --- a/src/registrar/fixtures_applications.py +++ b/src/registrar/fixtures_applications.py @@ -1,3 +1,4 @@ +import datetime import logging import random from faker import Faker @@ -104,7 +105,7 @@ class DomainApplicationFixture: # Random choice of agency for selects, used as placeholders for testing. else random.choice(DomainApplication.AGENCIES) # nosec ) - + da.submission_date = fake.date() da.federal_type = ( app["federal_type"] if "federal_type" in app From 8e4e41ce35c2855d515ce5860b1aa82f118df039 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 6 Feb 2024 09:32:07 -0700 Subject: [PATCH 060/119] Change import order on tests test_views seems is bugging out for some reason --- src/registrar/tests/test_views.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 2ba762b0e..d4e7fbcb7 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1,14 +1,15 @@ +import logging +import boto3_mocking # type: ignore from unittest import skip from unittest.mock import MagicMock, ANY, patch +from datetime import date, datetime, timedelta +from django.utils import timezone from django.conf import settings from django.test import Client, TestCase from django.urls import reverse from django.contrib.auth import get_user_model - -from .common import MockEppLib, MockSESClient, completed_application, create_user # type: ignore from django_webtest import WebTest # type: ignore -import boto3_mocking # type: ignore from registrar.utility.errors import ( NameserverError, @@ -22,8 +23,8 @@ from registrar.utility.errors import ( ) from registrar.models import ( - DomainApplication, Domain, + DomainApplication, DomainInformation, DraftDomain, DomainInvitation, @@ -36,11 +37,8 @@ from registrar.models import ( User, ) from registrar.views.application import ApplicationWizard, Step -from datetime import date, datetime, timedelta -from django.utils import timezone +from .common import MockEppLib, MockSESClient, completed_application, create_user, less_console_noise # type: ignore -from .common import less_console_noise -import logging logger = logging.getLogger(__name__) @@ -119,13 +117,18 @@ class LoggedInTests(TestWithUser): "Contact help@get.gov for details." ) deleted_text = "This domain has been removed and " "is no longer registered to your organization." + + # Seperated here for the linter. This works locally but fails through github actions? + # No idea why. Linter and test cases think this doesn't exist. + domain_state = Domain.State # noqa: F821 + # Generate a mapping of domain names, the state, and expected messages for the subtest test_cases = [ - ("deleted.gov", Domain.State.DELETED, deleted_text), - ("dnsneeded.gov", Domain.State.DNS_NEEDED, dns_needed_text), - ("unknown.gov", Domain.State.UNKNOWN, dns_needed_text), - ("onhold.gov", Domain.State.ON_HOLD, on_hold_text), - ("ready.gov", Domain.State.READY, ready_text), + ("deleted.gov", domain_state.DELETED, deleted_text), + ("dnsneeded.gov", domain_state.DNS_NEEDED, dns_needed_text), + ("unknown.gov", domain_state.UNKNOWN, dns_needed_text), + ("onhold.gov", domain_state.ON_HOLD, on_hold_text), + ("ready.gov", domain_state.READY, ready_text), ] for domain_name, state, expected_message in test_cases: with self.subTest(domain_name=domain_name, state=state, expected_message=expected_message): From 980864f48eab407a52e9e569268bd52dd92bbcd3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 6 Feb 2024 09:32:42 -0700 Subject: [PATCH 061/119] Revert "Change import order on tests" This reverts commit 8e4e41ce35c2855d515ce5860b1aa82f118df039. --- src/registrar/tests/test_views.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index d4e7fbcb7..2ba762b0e 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1,15 +1,14 @@ -import logging -import boto3_mocking # type: ignore from unittest import skip from unittest.mock import MagicMock, ANY, patch -from datetime import date, datetime, timedelta -from django.utils import timezone from django.conf import settings from django.test import Client, TestCase from django.urls import reverse from django.contrib.auth import get_user_model + +from .common import MockEppLib, MockSESClient, completed_application, create_user # type: ignore from django_webtest import WebTest # type: ignore +import boto3_mocking # type: ignore from registrar.utility.errors import ( NameserverError, @@ -23,8 +22,8 @@ from registrar.utility.errors import ( ) from registrar.models import ( - Domain, DomainApplication, + Domain, DomainInformation, DraftDomain, DomainInvitation, @@ -37,8 +36,11 @@ from registrar.models import ( User, ) from registrar.views.application import ApplicationWizard, Step -from .common import MockEppLib, MockSESClient, completed_application, create_user, less_console_noise # type: ignore +from datetime import date, datetime, timedelta +from django.utils import timezone +from .common import less_console_noise +import logging logger = logging.getLogger(__name__) @@ -117,18 +119,13 @@ class LoggedInTests(TestWithUser): "Contact help@get.gov for details." ) deleted_text = "This domain has been removed and " "is no longer registered to your organization." - - # Seperated here for the linter. This works locally but fails through github actions? - # No idea why. Linter and test cases think this doesn't exist. - domain_state = Domain.State # noqa: F821 - # Generate a mapping of domain names, the state, and expected messages for the subtest test_cases = [ - ("deleted.gov", domain_state.DELETED, deleted_text), - ("dnsneeded.gov", domain_state.DNS_NEEDED, dns_needed_text), - ("unknown.gov", domain_state.UNKNOWN, dns_needed_text), - ("onhold.gov", domain_state.ON_HOLD, on_hold_text), - ("ready.gov", domain_state.READY, ready_text), + ("deleted.gov", Domain.State.DELETED, deleted_text), + ("dnsneeded.gov", Domain.State.DNS_NEEDED, dns_needed_text), + ("unknown.gov", Domain.State.UNKNOWN, dns_needed_text), + ("onhold.gov", Domain.State.ON_HOLD, on_hold_text), + ("ready.gov", Domain.State.READY, ready_text), ] for domain_name, state, expected_message in test_cases: with self.subTest(domain_name=domain_name, state=state, expected_message=expected_message): From 70bdfd4589a881ae9533bef75465219f997a1dfe Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 6 Feb 2024 09:49:20 -0700 Subject: [PATCH 062/119] Fix test cases --- src/registrar/tests/test_views.py | 359 ----------------- src/registrar/tests/test_views_application.py | 364 ++++++++++++++++++ 2 files changed, 364 insertions(+), 359 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 194b4a8e5..3cfeeeedb 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -55,362 +55,3 @@ class TestWithUser(MockEppLib): DomainApplication.objects.all().delete() DomainInformation.objects.all().delete() self.user.delete() - - -class LoggedInTests(TestWithUser): - def setUp(self): - super().setUp() - self.client.force_login(self.user) - - def tearDown(self): - super().tearDown() - Contact.objects.all().delete() - - def test_home_lists_domain_applications(self): - response = self.client.get("/") - self.assertNotContains(response, "igorville.gov") - site = DraftDomain.objects.create(name="igorville.gov") - application = DomainApplication.objects.create(creator=self.user, requested_domain=site) - response = self.client.get("/") - - # count = 7 because of screenreader content - self.assertContains(response, "igorville.gov", count=7) - - # clean up - application.delete() - - def test_state_help_text(self): - """Tests if each domain state has help text""" - - # Get the expected text content of each state - deleted_text = "This domain has been removed and " "is no longer registered to your organization." - dns_needed_text = "Before this domain can be used, " "you’ll need to add name server addresses." - ready_text = "This domain has name servers and is ready for use." - on_hold_text = ( - "This domain is administratively paused, " - "so it can’t be edited and won’t resolve in DNS. " - "Contact help@get.gov for details." - ) - deleted_text = "This domain has been removed and " "is no longer registered to your organization." - # Generate a mapping of domain names, the state, and expected messages for the subtest - test_cases = [ - ("deleted.gov", Domain.State.DELETED, deleted_text), - ("dnsneeded.gov", Domain.State.DNS_NEEDED, dns_needed_text), - ("unknown.gov", Domain.State.UNKNOWN, dns_needed_text), - ("onhold.gov", Domain.State.ON_HOLD, on_hold_text), - ("ready.gov", Domain.State.READY, ready_text), - ] - for domain_name, state, expected_message in test_cases: - with self.subTest(domain_name=domain_name, state=state, expected_message=expected_message): - # Create a domain and a UserRole with the given params - test_domain, _ = Domain.objects.get_or_create(name=domain_name, state=state) - test_domain.expiration_date = date.today() - test_domain.save() - - user_role, _ = UserDomainRole.objects.get_or_create( - user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER - ) - - # Grab the home page - response = self.client.get("/") - - # Make sure the user can actually see the domain. - # We expect two instances because of SR content. - self.assertContains(response, domain_name, count=2) - - # Check that we have the right text content. - self.assertContains(response, expected_message, count=1) - - # Delete the role and domain to ensure we're testing in isolation - user_role.delete() - test_domain.delete() - - def test_state_help_text_expired(self): - """Tests if each domain state has help text when expired""" - expired_text = "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov." - test_domain, _ = Domain.objects.get_or_create(name="expired.gov", state=Domain.State.READY) - test_domain.expiration_date = date(2011, 10, 10) - test_domain.save() - - UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER) - - # Grab the home page - response = self.client.get("/") - - # Make sure the user can actually see the domain. - # We expect two instances because of SR content. - self.assertContains(response, "expired.gov", count=2) - - # Check that we have the right text content. - self.assertContains(response, expired_text, count=1) - - def test_state_help_text_no_expiration_date(self): - """Tests if each domain state has help text when expiration date is None""" - - # == Test a expiration of None for state ready. This should be expired. == # - expired_text = "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov." - test_domain, _ = Domain.objects.get_or_create(name="imexpired.gov", state=Domain.State.READY) - test_domain.expiration_date = None - test_domain.save() - - UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER) - - # Grab the home page - response = self.client.get("/") - - # Make sure the user can actually see the domain. - # We expect two instances because of SR content. - self.assertContains(response, "imexpired.gov", count=2) - - # Make sure the expiration date is None - self.assertEqual(test_domain.expiration_date, None) - - # Check that we have the right text content. - self.assertContains(response, expired_text, count=1) - - # == Test a expiration of None for state unknown. This should not display expired text. == # - unknown_text = "Before this domain can be used, " "you’ll need to add name server addresses." - test_domain_2, _ = Domain.objects.get_or_create(name="notexpired.gov", state=Domain.State.UNKNOWN) - test_domain_2.expiration_date = None - test_domain_2.save() - - UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain_2, role=UserDomainRole.Roles.MANAGER) - - # Grab the home page - response = self.client.get("/") - - # Make sure the user can actually see the domain. - # We expect two instances because of SR content. - self.assertContains(response, "notexpired.gov", count=2) - - # Make sure the expiration date is None - self.assertEqual(test_domain_2.expiration_date, None) - - # Check that we have the right text content. - self.assertContains(response, unknown_text, count=1) - - def test_home_deletes_withdrawn_domain_application(self): - """Tests if the user can delete a DomainApplication in the 'withdrawn' status""" - - site = DraftDomain.objects.create(name="igorville.gov") - application = DomainApplication.objects.create( - creator=self.user, requested_domain=site, status=DomainApplication.ApplicationStatus.WITHDRAWN - ) - - # Ensure that igorville.gov exists on the page - home_page = self.client.get("/") - self.assertContains(home_page, "igorville.gov") - - # Check if the delete button exists. We can do this by checking for its id and text content. - self.assertContains(home_page, "Delete") - self.assertContains(home_page, "button-toggle-delete-domain-alert-1") - - # Trigger the delete logic - response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True) - - self.assertNotContains(response, "igorville.gov") - - # clean up - application.delete() - - def test_home_deletes_started_domain_application(self): - """Tests if the user can delete a DomainApplication in the 'started' status""" - - site = DraftDomain.objects.create(name="igorville.gov") - application = DomainApplication.objects.create( - creator=self.user, requested_domain=site, status=DomainApplication.ApplicationStatus.STARTED - ) - - # Ensure that igorville.gov exists on the page - home_page = self.client.get("/") - self.assertContains(home_page, "igorville.gov") - - # Check if the delete button exists. We can do this by checking for its id and text content. - self.assertContains(home_page, "Delete") - self.assertContains(home_page, "button-toggle-delete-domain-alert-1") - - # Trigger the delete logic - response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True) - - self.assertNotContains(response, "igorville.gov") - - # clean up - application.delete() - - def test_home_doesnt_delete_other_domain_applications(self): - """Tests to ensure the user can't delete Applications not in the status of STARTED or WITHDRAWN""" - - # Given that we are including a subset of items that can be deleted while excluding the rest, - # subTest is appropriate here as otherwise we would need many duplicate tests for the same reason. - with less_console_noise(): - draft_domain = DraftDomain.objects.create(name="igorville.gov") - for status in DomainApplication.ApplicationStatus: - if status not in [ - DomainApplication.ApplicationStatus.STARTED, - DomainApplication.ApplicationStatus.WITHDRAWN, - ]: - with self.subTest(status=status): - application = DomainApplication.objects.create( - creator=self.user, requested_domain=draft_domain, status=status - ) - - # Trigger the delete logic - response = self.client.post( - reverse("application-delete", kwargs={"pk": application.pk}), follow=True - ) - - # Check for a 403 error - the end user should not be allowed to do this - self.assertEqual(response.status_code, 403) - - desired_application = DomainApplication.objects.filter(requested_domain=draft_domain) - - # Make sure the DomainApplication wasn't deleted - self.assertEqual(desired_application.count(), 1) - - # clean up - application.delete() - - def test_home_deletes_domain_application_and_orphans(self): - """Tests if delete for DomainApplication deletes orphaned Contact objects""" - - # Create the site and contacts to delete (orphaned) - contact = Contact.objects.create( - first_name="Henry", - last_name="Mcfakerson", - ) - contact_shared = Contact.objects.create( - first_name="Relative", - last_name="Aether", - ) - - # Create two non-orphaned contacts - contact_2 = Contact.objects.create( - first_name="Saturn", - last_name="Mars", - ) - - # Attach a user object to a contact (should not be deleted) - contact_user, _ = Contact.objects.get_or_create(user=self.user) - - site = DraftDomain.objects.create(name="igorville.gov") - application = DomainApplication.objects.create( - creator=self.user, - requested_domain=site, - status=DomainApplication.ApplicationStatus.WITHDRAWN, - authorizing_official=contact, - submitter=contact_user, - ) - application.other_contacts.set([contact_2]) - - # Create a second application to attach contacts to - site_2 = DraftDomain.objects.create(name="teaville.gov") - application_2 = DomainApplication.objects.create( - creator=self.user, - requested_domain=site_2, - status=DomainApplication.ApplicationStatus.STARTED, - authorizing_official=contact_2, - submitter=contact_shared, - ) - application_2.other_contacts.set([contact_shared]) - - # Ensure that igorville.gov exists on the page - home_page = self.client.get("/") - self.assertContains(home_page, "igorville.gov") - - # Trigger the delete logic - response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True) - - # igorville is now deleted - self.assertNotContains(response, "igorville.gov") - - # Check if the orphaned contact was deleted - orphan = Contact.objects.filter(id=contact.id) - self.assertFalse(orphan.exists()) - - # All non-orphan contacts should still exist and are unaltered - try: - current_user = Contact.objects.filter(id=contact_user.id).get() - except Contact.DoesNotExist: - self.fail("contact_user (a non-orphaned contact) was deleted") - - self.assertEqual(current_user, contact_user) - try: - edge_case = Contact.objects.filter(id=contact_2.id).get() - except Contact.DoesNotExist: - self.fail("contact_2 (a non-orphaned contact) was deleted") - - self.assertEqual(edge_case, contact_2) - - def test_home_deletes_domain_application_and_shared_orphans(self): - """Test the edge case for an object that will become orphaned after a delete - (but is not an orphan at the time of deletion)""" - - # Create the site and contacts to delete (orphaned) - contact = Contact.objects.create( - first_name="Henry", - last_name="Mcfakerson", - ) - contact_shared = Contact.objects.create( - first_name="Relative", - last_name="Aether", - ) - - # Create two non-orphaned contacts - contact_2 = Contact.objects.create( - first_name="Saturn", - last_name="Mars", - ) - - # Attach a user object to a contact (should not be deleted) - contact_user, _ = Contact.objects.get_or_create(user=self.user) - - site = DraftDomain.objects.create(name="igorville.gov") - application = DomainApplication.objects.create( - creator=self.user, - requested_domain=site, - status=DomainApplication.ApplicationStatus.WITHDRAWN, - authorizing_official=contact, - submitter=contact_user, - ) - application.other_contacts.set([contact_2]) - - # Create a second application to attach contacts to - site_2 = DraftDomain.objects.create(name="teaville.gov") - application_2 = DomainApplication.objects.create( - creator=self.user, - requested_domain=site_2, - status=DomainApplication.ApplicationStatus.STARTED, - authorizing_official=contact_2, - submitter=contact_shared, - ) - application_2.other_contacts.set([contact_shared]) - - home_page = self.client.get("/") - self.assertContains(home_page, "teaville.gov") - - # Trigger the delete logic - response = self.client.post(reverse("application-delete", kwargs={"pk": application_2.pk}), follow=True) - - self.assertNotContains(response, "teaville.gov") - - # Check if the orphaned contact was deleted - orphan = Contact.objects.filter(id=contact_shared.id) - self.assertFalse(orphan.exists()) - - def test_application_form_view(self): - response = self.client.get("/request/", follow=True) - self.assertContains( - response, - "You’re about to start your .gov domain request.", - ) - - def test_domain_application_form_with_ineligible_user(self): - """Application form not accessible for an ineligible user. - This test should be solid enough since all application wizard - views share the same permissions class""" - self.user.status = User.RESTRICTED - self.user.save() - - with less_console_noise(): - response = self.client.get("/request/", follow=True) - self.assertEqual(response.status_code, 403) diff --git a/src/registrar/tests/test_views_application.py b/src/registrar/tests/test_views_application.py index 62485fa86..733eaf578 100644 --- a/src/registrar/tests/test_views_application.py +++ b/src/registrar/tests/test_views_application.py @@ -2,6 +2,7 @@ from unittest import skip from django.conf import settings from django.urls import reverse +from datetime import date from .common import MockSESClient, completed_application # type: ignore from django_webtest import WebTest # type: ignore @@ -14,6 +15,8 @@ from registrar.models import ( Contact, User, Website, + UserDomainRole, + DraftDomain, ) from registrar.views.application import ApplicationWizard, Step @@ -2197,3 +2200,364 @@ class DomainApplicationTestDifferentStatuses(TestWithUser, WebTest): # domain object, so we do not expect to see 'city.gov' # in either the Domains or Requests tables. self.assertNotContains(home_page, "city.gov") + + +class HomeTests(TestWithUser): + """A series of tests that target the two tables on home.html""" + + def setUp(self): + super().setUp() + self.client.force_login(self.user) + + def tearDown(self): + super().tearDown() + Contact.objects.all().delete() + + def test_home_lists_domain_applications(self): + response = self.client.get("/") + self.assertNotContains(response, "igorville.gov") + site = DraftDomain.objects.create(name="igorville.gov") + application = DomainApplication.objects.create(creator=self.user, requested_domain=site) + response = self.client.get("/") + + # count = 7 because of screenreader content + self.assertContains(response, "igorville.gov", count=7) + + # clean up + application.delete() + + def test_state_help_text(self): + """Tests if each domain state has help text""" + + # Get the expected text content of each state + deleted_text = "This domain has been removed and " "is no longer registered to your organization." + dns_needed_text = "Before this domain can be used, " "you’ll need to add name server addresses." + ready_text = "This domain has name servers and is ready for use." + on_hold_text = ( + "This domain is administratively paused, " + "so it can’t be edited and won’t resolve in DNS. " + "Contact help@get.gov for details." + ) + deleted_text = "This domain has been removed and " "is no longer registered to your organization." + # Generate a mapping of domain names, the state, and expected messages for the subtest + test_cases = [ + ("deleted.gov", Domain.State.DELETED, deleted_text), + ("dnsneeded.gov", Domain.State.DNS_NEEDED, dns_needed_text), + ("unknown.gov", Domain.State.UNKNOWN, dns_needed_text), + ("onhold.gov", Domain.State.ON_HOLD, on_hold_text), + ("ready.gov", Domain.State.READY, ready_text), + ] + for domain_name, state, expected_message in test_cases: + with self.subTest(domain_name=domain_name, state=state, expected_message=expected_message): + # Create a domain and a UserRole with the given params + test_domain, _ = Domain.objects.get_or_create(name=domain_name, state=state) + test_domain.expiration_date = date.today() + test_domain.save() + + user_role, _ = UserDomainRole.objects.get_or_create( + user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER + ) + + # Grab the home page + response = self.client.get("/") + + # Make sure the user can actually see the domain. + # We expect two instances because of SR content. + self.assertContains(response, domain_name, count=2) + + # Check that we have the right text content. + self.assertContains(response, expected_message, count=1) + + # Delete the role and domain to ensure we're testing in isolation + user_role.delete() + test_domain.delete() + + def test_state_help_text_expired(self): + """Tests if each domain state has help text when expired""" + expired_text = "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov." + test_domain, _ = Domain.objects.get_or_create(name="expired.gov", state=Domain.State.READY) + test_domain.expiration_date = date(2011, 10, 10) + test_domain.save() + + UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER) + + # Grab the home page + response = self.client.get("/") + + # Make sure the user can actually see the domain. + # We expect two instances because of SR content. + self.assertContains(response, "expired.gov", count=2) + + # Check that we have the right text content. + self.assertContains(response, expired_text, count=1) + + def test_state_help_text_no_expiration_date(self): + """Tests if each domain state has help text when expiration date is None""" + + # == Test a expiration of None for state ready. This should be expired. == # + expired_text = "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov." + test_domain, _ = Domain.objects.get_or_create(name="imexpired.gov", state=Domain.State.READY) + test_domain.expiration_date = None + test_domain.save() + + UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER) + + # Grab the home page + response = self.client.get("/") + + # Make sure the user can actually see the domain. + # We expect two instances because of SR content. + self.assertContains(response, "imexpired.gov", count=2) + + # Make sure the expiration date is None + self.assertEqual(test_domain.expiration_date, None) + + # Check that we have the right text content. + self.assertContains(response, expired_text, count=1) + + # == Test a expiration of None for state unknown. This should not display expired text. == # + unknown_text = "Before this domain can be used, " "you’ll need to add name server addresses." + test_domain_2, _ = Domain.objects.get_or_create(name="notexpired.gov", state=Domain.State.UNKNOWN) + test_domain_2.expiration_date = None + test_domain_2.save() + + UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain_2, role=UserDomainRole.Roles.MANAGER) + + # Grab the home page + response = self.client.get("/") + + # Make sure the user can actually see the domain. + # We expect two instances because of SR content. + self.assertContains(response, "notexpired.gov", count=2) + + # Make sure the expiration date is None + self.assertEqual(test_domain_2.expiration_date, None) + + # Check that we have the right text content. + self.assertContains(response, unknown_text, count=1) + + def test_home_deletes_withdrawn_domain_application(self): + """Tests if the user can delete a DomainApplication in the 'withdrawn' status""" + + site = DraftDomain.objects.create(name="igorville.gov") + application = DomainApplication.objects.create( + creator=self.user, requested_domain=site, status=DomainApplication.ApplicationStatus.WITHDRAWN + ) + + # Ensure that igorville.gov exists on the page + home_page = self.client.get("/") + self.assertContains(home_page, "igorville.gov") + + # Check if the delete button exists. We can do this by checking for its id and text content. + self.assertContains(home_page, "Delete") + self.assertContains(home_page, "button-toggle-delete-domain-alert-1") + + # Trigger the delete logic + response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True) + + self.assertNotContains(response, "igorville.gov") + + # clean up + application.delete() + + def test_home_deletes_started_domain_application(self): + """Tests if the user can delete a DomainApplication in the 'started' status""" + + site = DraftDomain.objects.create(name="igorville.gov") + application = DomainApplication.objects.create( + creator=self.user, requested_domain=site, status=DomainApplication.ApplicationStatus.STARTED + ) + + # Ensure that igorville.gov exists on the page + home_page = self.client.get("/") + self.assertContains(home_page, "igorville.gov") + + # Check if the delete button exists. We can do this by checking for its id and text content. + self.assertContains(home_page, "Delete") + self.assertContains(home_page, "button-toggle-delete-domain-alert-1") + + # Trigger the delete logic + response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True) + + self.assertNotContains(response, "igorville.gov") + + # clean up + application.delete() + + def test_home_doesnt_delete_other_domain_applications(self): + """Tests to ensure the user can't delete Applications not in the status of STARTED or WITHDRAWN""" + + # Given that we are including a subset of items that can be deleted while excluding the rest, + # subTest is appropriate here as otherwise we would need many duplicate tests for the same reason. + with less_console_noise(): + draft_domain = DraftDomain.objects.create(name="igorville.gov") + for status in DomainApplication.ApplicationStatus: + if status not in [ + DomainApplication.ApplicationStatus.STARTED, + DomainApplication.ApplicationStatus.WITHDRAWN, + ]: + with self.subTest(status=status): + application = DomainApplication.objects.create( + creator=self.user, requested_domain=draft_domain, status=status + ) + + # Trigger the delete logic + response = self.client.post( + reverse("application-delete", kwargs={"pk": application.pk}), follow=True + ) + + # Check for a 403 error - the end user should not be allowed to do this + self.assertEqual(response.status_code, 403) + + desired_application = DomainApplication.objects.filter(requested_domain=draft_domain) + + # Make sure the DomainApplication wasn't deleted + self.assertEqual(desired_application.count(), 1) + + # clean up + application.delete() + + def test_home_deletes_domain_application_and_orphans(self): + """Tests if delete for DomainApplication deletes orphaned Contact objects""" + + # Create the site and contacts to delete (orphaned) + contact = Contact.objects.create( + first_name="Henry", + last_name="Mcfakerson", + ) + contact_shared = Contact.objects.create( + first_name="Relative", + last_name="Aether", + ) + + # Create two non-orphaned contacts + contact_2 = Contact.objects.create( + first_name="Saturn", + last_name="Mars", + ) + + # Attach a user object to a contact (should not be deleted) + contact_user, _ = Contact.objects.get_or_create(user=self.user) + + site = DraftDomain.objects.create(name="igorville.gov") + application = DomainApplication.objects.create( + creator=self.user, + requested_domain=site, + status=DomainApplication.ApplicationStatus.WITHDRAWN, + authorizing_official=contact, + submitter=contact_user, + ) + application.other_contacts.set([contact_2]) + + # Create a second application to attach contacts to + site_2 = DraftDomain.objects.create(name="teaville.gov") + application_2 = DomainApplication.objects.create( + creator=self.user, + requested_domain=site_2, + status=DomainApplication.ApplicationStatus.STARTED, + authorizing_official=contact_2, + submitter=contact_shared, + ) + application_2.other_contacts.set([contact_shared]) + + # Ensure that igorville.gov exists on the page + home_page = self.client.get("/") + self.assertContains(home_page, "igorville.gov") + + # Trigger the delete logic + response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True) + + # igorville is now deleted + self.assertNotContains(response, "igorville.gov") + + # Check if the orphaned contact was deleted + orphan = Contact.objects.filter(id=contact.id) + self.assertFalse(orphan.exists()) + + # All non-orphan contacts should still exist and are unaltered + try: + current_user = Contact.objects.filter(id=contact_user.id).get() + except Contact.DoesNotExist: + self.fail("contact_user (a non-orphaned contact) was deleted") + + self.assertEqual(current_user, contact_user) + try: + edge_case = Contact.objects.filter(id=contact_2.id).get() + except Contact.DoesNotExist: + self.fail("contact_2 (a non-orphaned contact) was deleted") + + self.assertEqual(edge_case, contact_2) + + def test_home_deletes_domain_application_and_shared_orphans(self): + """Test the edge case for an object that will become orphaned after a delete + (but is not an orphan at the time of deletion)""" + + # Create the site and contacts to delete (orphaned) + contact = Contact.objects.create( + first_name="Henry", + last_name="Mcfakerson", + ) + contact_shared = Contact.objects.create( + first_name="Relative", + last_name="Aether", + ) + + # Create two non-orphaned contacts + contact_2 = Contact.objects.create( + first_name="Saturn", + last_name="Mars", + ) + + # Attach a user object to a contact (should not be deleted) + contact_user, _ = Contact.objects.get_or_create(user=self.user) + + site = DraftDomain.objects.create(name="igorville.gov") + application = DomainApplication.objects.create( + creator=self.user, + requested_domain=site, + status=DomainApplication.ApplicationStatus.WITHDRAWN, + authorizing_official=contact, + submitter=contact_user, + ) + application.other_contacts.set([contact_2]) + + # Create a second application to attach contacts to + site_2 = DraftDomain.objects.create(name="teaville.gov") + application_2 = DomainApplication.objects.create( + creator=self.user, + requested_domain=site_2, + status=DomainApplication.ApplicationStatus.STARTED, + authorizing_official=contact_2, + submitter=contact_shared, + ) + application_2.other_contacts.set([contact_shared]) + + home_page = self.client.get("/") + self.assertContains(home_page, "teaville.gov") + + # Trigger the delete logic + response = self.client.post(reverse("application-delete", kwargs={"pk": application_2.pk}), follow=True) + + self.assertNotContains(response, "teaville.gov") + + # Check if the orphaned contact was deleted + orphan = Contact.objects.filter(id=contact_shared.id) + self.assertFalse(orphan.exists()) + + def test_application_form_view(self): + response = self.client.get("/request/", follow=True) + self.assertContains( + response, + "You’re about to start your .gov domain request.", + ) + + def test_domain_application_form_with_ineligible_user(self): + """Application form not accessible for an ineligible user. + This test should be solid enough since all application wizard + views share the same permissions class""" + self.user.status = User.RESTRICTED + self.user.save() + + with less_console_noise(): + response = self.client.get("/request/", follow=True) + self.assertEqual(response.status_code, 403) From 76809a75a8438154b3b03296dfb716b32c15dac9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 6 Feb 2024 09:57:46 -0700 Subject: [PATCH 063/119] Linting --- src/registrar/assets/sass/_theme/_base.scss | 2 +- src/registrar/assets/sass/_theme/_tables.scss | 16 +++++++--------- src/registrar/tests/test_views.py | 5 ----- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 0a62009e7..983af3a01 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -139,7 +139,7 @@ abbr[title] { } } -@include at-media(tablet) { +@media (min-width: 768px) { .usa-tooltip__body { width: 250px; white-space: normal; diff --git a/src/registrar/assets/sass/_theme/_tables.scss b/src/registrar/assets/sass/_theme/_tables.scss index 7b13656e7..0d58b5878 100644 --- a/src/registrar/assets/sass/_theme/_tables.scss +++ b/src/registrar/assets/sass/_theme/_tables.scss @@ -26,15 +26,13 @@ padding-bottom: units(2px); } - td { - .no-click-outline-and-cursor-help{ - outline: none; - cursor: help; - use { - // USWDS has weird interactions with SVGs regarding tooltips, - // and other components. In this event, we need to disable pointer interactions. - pointer-events: none; - } + td .no-click-outline-and-cursor-help { + outline: none; + cursor: help; + use { + // USWDS has weird interactions with SVGs regarding tooltips, + // and other components. In this event, we need to disable pointer interactions. + pointer-events: none; } } diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 3cfeeeedb..8469071f8 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1,5 +1,4 @@ from django.test import Client, TestCase -from django.urls import reverse from django.contrib.auth import get_user_model from .common import MockEppLib # type: ignore @@ -8,11 +7,7 @@ from .common import MockEppLib # type: ignore from registrar.models import ( DomainApplication, DomainInformation, - DraftDomain, - Contact, - User, ) -from .common import less_console_noise import logging logger = logging.getLogger(__name__) From 09b0ebb8922f9c0a28326b169d18a6deffcb8652 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 6 Feb 2024 10:13:00 -0700 Subject: [PATCH 064/119] Revert "Change manifest health check" This reverts commit 2c3efb4cbce24696f069a5f4034789f0b9c11084. --- ops/manifests/manifest-za.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ops/manifests/manifest-za.yaml b/ops/manifests/manifest-za.yaml index 54cdc0262..271f49da9 100644 --- a/ops/manifests/manifest-za.yaml +++ b/ops/manifests/manifest-za.yaml @@ -11,7 +11,7 @@ applications: command: ./run.sh health-check-type: http health-check-http-endpoint: /health - health-check-invocation-timeout: 1 + health-check-invocation-timeout: 40 env: # Send stdout and stderr straight to the terminal without buffering PYTHONUNBUFFERED: yup From a9302a8428a40a3126e74db7d1699e914d404132 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 6 Feb 2024 11:08:42 -0800 Subject: [PATCH 065/119] Get all emails for domain managers and matching header title to be dynamic --- src/registrar/utility/csv_export.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 1ea9131e0..c6e278254 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) def write_header(writer, columns): """ Receives params from the parent methods and outputs a CSV with a header row. - Works with write_header as longas the same writer object is passed. + Works with write_header as long as the same writer object is passed. """ writer.writerow(columns) @@ -92,6 +92,13 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None "Deleted": domain.deleted, } + # Get each domain managers email and add to list + dm_emails = [dm.email for dm in domain.permissions] + + # Matching header for domain managers to be dynamic + for i, dm_email in enumerate(dm_emails, start=1): + FIELDS[f"Domain Manager email {i}":dm_email] + row = [FIELDS.get(column, "") for column in columns] return row From 175c721025a0847d205d5c0ac6d10c3948144a8e Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 6 Feb 2024 12:30:11 -0700 Subject: [PATCH 066/119] linted --- src/registrar/fixtures_applications.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/fixtures_applications.py b/src/registrar/fixtures_applications.py index 9519dfce9..659a3040e 100644 --- a/src/registrar/fixtures_applications.py +++ b/src/registrar/fixtures_applications.py @@ -1,4 +1,3 @@ -import datetime import logging import random from faker import Faker From 7f7e61ccc1d8a5200612bb4d516f707a932c11f5 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 6 Feb 2024 11:52:04 -0800 Subject: [PATCH 067/119] Add logic for domain manager title in header --- src/registrar/utility/csv_export.py | 30 ++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index c6e278254..9fe62bfcc 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -14,11 +14,16 @@ from registrar.models.public_contact import PublicContact logger = logging.getLogger(__name__) -def write_header(writer, columns): +def write_header(writer, columns, max_dm_count): """ Receives params from the parent methods and outputs a CSV with a header row. Works with write_header as long as the same writer object is passed. """ + + for i in range(1, max_dm_count + 1): + columns.append(f"Domain manager email {i}") + + writer.writerow("hello") writer.writerow(columns) @@ -134,12 +139,22 @@ def write_body( else: logger.warning("csv_export -> Domain was none for PublicContact") + # The maximum amount of domain managers an account has + # We get the max so we can set the column header accurately + max_dm_count = 0 + paginator_ran = False + # Reduce the memory overhead when performing the write operation paginator = Paginator(all_domain_infos, 1000) for page_num in paginator.page_range: page = paginator.page(page_num) rows = [] for domain_info in page.object_list: + # Get count of all the domain managers for an account + dm_count = len(domain_info.domain.permissions) + if dm_count > max_dm_count: + max_dm_count = dm_count + try: row = parse_row(columns, domain_info, security_emails_dict) rows.append(row) @@ -149,7 +164,12 @@ def write_body( logger.error("csv_export -> Error when parsing row, domain was None") continue + # We only want this to run once just for the column header + if paginator_ran is False: + write_header(writer, columns, max_dm_count) + writer.writerows(rows) + paginator_ran = True def export_data_type_to_csv(csv_file): @@ -184,7 +204,7 @@ def export_data_type_to_csv(csv_file): Domain.State.ON_HOLD, ], } - write_header(writer, columns) + # write_header(writer, columns) write_body(writer, columns, sort_fields, filter_condition) @@ -216,7 +236,7 @@ def export_data_full_to_csv(csv_file): Domain.State.ON_HOLD, ], } - write_header(writer, columns) + # write_header(writer, columns) write_body(writer, columns, sort_fields, filter_condition) @@ -249,7 +269,7 @@ def export_data_federal_to_csv(csv_file): Domain.State.ON_HOLD, ], } - write_header(writer, columns) + # write_header(writer, columns) write_body(writer, columns, sort_fields, filter_condition) @@ -317,6 +337,6 @@ def export_data_growth_to_csv(csv_file, start_date, end_date): "domain__deleted__gte": start_date_formatted, } - write_header(writer, columns) + # write_header(writer, columns) write_body(writer, columns, sort_fields, filter_condition) write_body(writer, columns, sort_fields_for_deleted_domains, filter_condition_for_deleted_domains) From 029f28aaf40ab72b1ab6c2066152629659a434bf Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 6 Feb 2024 13:18:05 -0800 Subject: [PATCH 068/119] Testing chanegs to trigger sandbox --- src/registrar/templates/home.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index c7a005f97..21cb1e301 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -10,7 +10,7 @@ {# the entire logged in page goes here #}
-

Manage your domains

+

Manage your domains - Test Trigger Here

Date: Tue, 6 Feb 2024 13:22:08 -0800 Subject: [PATCH 069/119] limit changes to just gevent --- src/registrar/views/domain.py | 2 -- src/run.sh | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 48d537562..094ad86da 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -6,7 +6,6 @@ inherit from `DomainPermissionView` (or DomainInvitationPermissionDeleteView). """ import logging -import time from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin from django.db import IntegrityError @@ -150,7 +149,6 @@ class DomainView(DomainBaseView): context["security_email"] = None return context context["security_email"] = security_email - time.sleep(100) return context def in_editable_state(self, pk): diff --git a/src/run.sh b/src/run.sh index e7512b28d..04987c154 100755 --- a/src/run.sh +++ b/src/run.sh @@ -6,4 +6,4 @@ set -o pipefail # Make sure that django's `collectstatic` has been run locally before pushing up to any environment, # so that the styles and static assets to show up correctly on any environment. -gunicorn --worker-class=gevent --worker-connections=1000 --workers=1 registrar.config.wsgi -t 60 +gunicorn --worker-class=gevent registrar.config.wsgi -t 60 \ No newline at end of file From 239b704a920696b9f96f89bd4a9cd9a3ae8d51d5 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Tue, 6 Feb 2024 13:40:11 -0800 Subject: [PATCH 070/119] fixed newlines --- src/registrar/views/domain.py | 1 + src/run.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 094ad86da..313762ef1 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -6,6 +6,7 @@ inherit from `DomainPermissionView` (or DomainInvitationPermissionDeleteView). """ import logging + from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin from django.db import IntegrityError diff --git a/src/run.sh b/src/run.sh index 04987c154..1d35cd617 100755 --- a/src/run.sh +++ b/src/run.sh @@ -6,4 +6,4 @@ set -o pipefail # Make sure that django's `collectstatic` has been run locally before pushing up to any environment, # so that the styles and static assets to show up correctly on any environment. -gunicorn --worker-class=gevent registrar.config.wsgi -t 60 \ No newline at end of file +gunicorn --worker-class=gevent registrar.config.wsgi -t 60 From 28d37fc8699daa0bc2c4eb24dcc32466f6c55ee7 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 6 Feb 2024 14:04:43 -0800 Subject: [PATCH 071/119] Test an open column title --- src/registrar/utility/csv_export.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index fcbce470d..64afd2d06 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -189,7 +189,14 @@ def export_data_type_to_csv(csv_file): "AO", "AO email", "Security contact email", + "Domain Manager email", ] + + # STUCK HERE + + # So the problem is we don't even have access to domains or a count here. + # We could pass it in, but it's messy. Maybe helper function? Seems repetitive + # Coalesce is used to replace federal_type of None with ZZZZZ sort_fields = [ "organization_type", From 80ca25339d09dae9d8acf42869a21991f9a76ff6 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 6 Feb 2024 20:34:57 -0500 Subject: [PATCH 072/119] debugging for oidc --- src/djangooidc/oidc.py | 5 +++++ src/djangooidc/views.py | 4 +++- src/registrar/config/settings.py | 6 +++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/djangooidc/oidc.py b/src/djangooidc/oidc.py index bff766bb4..ac28988e0 100644 --- a/src/djangooidc/oidc.py +++ b/src/djangooidc/oidc.py @@ -27,6 +27,7 @@ class Client(oic.Client): """Step 1: Configure the OpenID Connect client.""" logger.debug("Initializing the OpenID Connect client...") try: + logger.debug("__init__ first try") provider = settings.OIDC_PROVIDERS[op] verify_ssl = getattr(settings, "OIDC_VERIFY_SSL", True) except Exception as err: @@ -35,6 +36,7 @@ class Client(oic.Client): raise o_e.InternalError() try: + logger.debug("__init__ second try") # prepare private key for authentication method of private_key_jwt key_bundle = keyio.KeyBundle() rsa_key = importKey(provider["client_registration"]["sp_private_key"]) @@ -51,6 +53,7 @@ class Client(oic.Client): raise o_e.InternalError() try: + logger.debug("__init__ third try") # create the oic client instance super().__init__( client_id=None, @@ -70,6 +73,7 @@ class Client(oic.Client): raise o_e.InternalError() try: + logger.debug("__init__ fourth try") # discover and store the provider (OP) urls, etc self.provider_config(provider["srv_discovery_url"]) self.store_registration_info(RegistrationResponse(**provider["client_registration"])) @@ -80,6 +84,7 @@ class Client(oic.Client): provider["srv_discovery_url"], ) raise o_e.InternalError() + logger.debug("__init__ finished initializing") def create_authn_request( self, diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index 2fc2a0363..7af59f0bc 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -16,6 +16,7 @@ from registrar.models import User logger = logging.getLogger(__name__) try: + logger.debug("oidc views initializing provider") # Initialize provider using pyOICD OP = getattr(settings, "OIDC_ACTIVE_PROVIDER") CLIENT = Client(OP) @@ -55,7 +56,7 @@ def error_page(request, error): def openid(request): """Redirect the user to an authentication provider (OP).""" - + logger.debug("in openid") # If the session reset because of a server restart, attempt to login again request.session["acr_value"] = CLIENT.get_default_acr_value() @@ -69,6 +70,7 @@ def openid(request): def login_callback(request): """Analyze the token returned by the authentication provider (OP).""" + logger.debug("in login_callback") try: query = parse_qs(request.GET.urlencode()) userinfo = CLIENT.callback(query, request.session) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 372434887..b54979dde 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -469,19 +469,19 @@ LOGGING = { # Django's runserver requests "django.request": { "handlers": ["django.server"], - "level": "INFO", + "level": "DEBUG", "propagate": False, }, # OpenID Connect logger "oic": { "handlers": ["console"], - "level": "INFO", + "level": "DEBUG", "propagate": False, }, # Django wrapper for OpenID Connect "djangooidc": { "handlers": ["console"], - "level": "INFO", + "level": "DEBUG", "propagate": False, }, # Our app! From f05f0c76a8a12626ccfc59ba7be815d15ae60c73 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 7 Feb 2024 08:51:26 -0500 Subject: [PATCH 073/119] debugging on health check view --- src/registrar/views/health.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/registrar/views/health.py b/src/registrar/views/health.py index 9a4b449a5..620839dd2 100644 --- a/src/registrar/views/health.py +++ b/src/registrar/views/health.py @@ -1,10 +1,13 @@ +import logging from django.http import HttpResponse from login_required import login_not_required +logger = logging.getLogger(__name__) # the health check endpoint needs to be globally available so that the # PaaS orchestrator can make sure the app has come up properly @login_not_required def health(request): + logger.debug("in health check view") return HttpResponse('OK - Get.govOK') From 0eac1db3ad6f2e9b6d463c496383d94893d07c04 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 7 Feb 2024 11:00:21 -0500 Subject: [PATCH 074/119] Revise banner color and copy --- src/registrar/assets/sass/_theme/_uswds-theme.scss | 4 ++++ src/registrar/templates/includes/non-production-alert.html | 4 ++-- src/registrar/tests/test_environment_variables_effects.py | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_uswds-theme.scss b/src/registrar/assets/sass/_theme/_uswds-theme.scss index 0cdf6675e..a26f23508 100644 --- a/src/registrar/assets/sass/_theme/_uswds-theme.scss +++ b/src/registrar/assets/sass/_theme/_uswds-theme.scss @@ -116,6 +116,10 @@ in the form $setting: value, $theme-color-success-light: $dhs-green-30, $theme-color-success-lighter: $dhs-green-15, + /*--------------------------- + ## Emergency state + ----------------------------*/ + $theme-color-emergency: #FFC3F9, /*--------------------------- # Input settings diff --git a/src/registrar/templates/includes/non-production-alert.html b/src/registrar/templates/includes/non-production-alert.html index 4f8aaeac0..811e1f9de 100644 --- a/src/registrar/templates/includes/non-production-alert.html +++ b/src/registrar/templates/includes/non-production-alert.html @@ -1,5 +1,5 @@ -

+
- WARNING: You are not on production. + WARNING: You are on a test site.
diff --git a/src/registrar/tests/test_environment_variables_effects.py b/src/registrar/tests/test_environment_variables_effects.py index 9ef065aeb..3a838c2a2 100644 --- a/src/registrar/tests/test_environment_variables_effects.py +++ b/src/registrar/tests/test_environment_variables_effects.py @@ -22,10 +22,10 @@ class MyTestCase(TestCase): def test_production_environment(self): """No banner on prod.""" home_page = self.client.get("/") - self.assertNotContains(home_page, "You are not on production.") + self.assertNotContains(home_page, "You are on a test site.") @override_settings(IS_PRODUCTION=False) def test_non_production_environment(self): """Banner on non-prod.""" home_page = self.client.get("/") - self.assertContains(home_page, "You are not on production.") + self.assertContains(home_page, "You are on a test site.") From 7b64929d607190b35984a52bc28eff2b42ece7c1 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 7 Feb 2024 11:08:40 -0500 Subject: [PATCH 075/119] @no_login_required not working, so added to LOGIN_REQUIRED_IGNORE_PATHS --- src/registrar/config/settings.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index b54979dde..617d8beea 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -437,7 +437,7 @@ LOGGING = { "formatter": "verbose", }, "django.server": { - "level": "INFO", + "level": "DEBUG", "class": "logging.StreamHandler", "formatter": "django.server", }, @@ -451,19 +451,19 @@ LOGGING = { # Django's generic logger "django": { "handlers": ["console"], - "level": "INFO", + "level": "DEBUG", "propagate": False, }, # Django's template processor "django.template": { "handlers": ["console"], - "level": "INFO", + "level": "DEBUG", "propagate": False, }, # Django's runserver "django.server": { "handlers": ["django.server"], - "level": "INFO", + "level": "DEBUG", "propagate": False, }, # Django's runserver requests @@ -516,6 +516,7 @@ LOGIN_URL = "/openid/login" # the initial login requests without erroring. LOGIN_REQUIRED_IGNORE_PATHS = [ r"/openid/(.+)$", + r"/health(.*)$", ] # where to go after logging out From 8f1ab863ee8e22678a38ef1cf2206fd025d1c17c Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 7 Feb 2024 11:44:09 -0500 Subject: [PATCH 076/119] Change warning to attention --- src/registrar/templates/includes/non-production-alert.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/includes/non-production-alert.html b/src/registrar/templates/includes/non-production-alert.html index 811e1f9de..8e40892bc 100644 --- a/src/registrar/templates/includes/non-production-alert.html +++ b/src/registrar/templates/includes/non-production-alert.html @@ -1,5 +1,5 @@
- WARNING: You are on a test site. + Attention: You are on a test site.
From 2a676260c6b86636e07a84dab84602331d1c3155 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 7 Feb 2024 13:08:26 -0500 Subject: [PATCH 077/119] health check to /health, updated testing of /health, cleaned up extraneous debug logging --- src/djangooidc/oidc.py | 5 ----- src/djangooidc/views.py | 3 --- src/registrar/config/settings.py | 15 +++++++------- src/registrar/config/urls.py | 2 +- src/registrar/tests/test_url_auth.py | 31 ++++++++++++++++++++++++++++ src/registrar/views/health.py | 3 --- 6 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/djangooidc/oidc.py b/src/djangooidc/oidc.py index ac28988e0..bff766bb4 100644 --- a/src/djangooidc/oidc.py +++ b/src/djangooidc/oidc.py @@ -27,7 +27,6 @@ class Client(oic.Client): """Step 1: Configure the OpenID Connect client.""" logger.debug("Initializing the OpenID Connect client...") try: - logger.debug("__init__ first try") provider = settings.OIDC_PROVIDERS[op] verify_ssl = getattr(settings, "OIDC_VERIFY_SSL", True) except Exception as err: @@ -36,7 +35,6 @@ class Client(oic.Client): raise o_e.InternalError() try: - logger.debug("__init__ second try") # prepare private key for authentication method of private_key_jwt key_bundle = keyio.KeyBundle() rsa_key = importKey(provider["client_registration"]["sp_private_key"]) @@ -53,7 +51,6 @@ class Client(oic.Client): raise o_e.InternalError() try: - logger.debug("__init__ third try") # create the oic client instance super().__init__( client_id=None, @@ -73,7 +70,6 @@ class Client(oic.Client): raise o_e.InternalError() try: - logger.debug("__init__ fourth try") # discover and store the provider (OP) urls, etc self.provider_config(provider["srv_discovery_url"]) self.store_registration_info(RegistrationResponse(**provider["client_registration"])) @@ -84,7 +80,6 @@ class Client(oic.Client): provider["srv_discovery_url"], ) raise o_e.InternalError() - logger.debug("__init__ finished initializing") def create_authn_request( self, diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index 7af59f0bc..3d824c8e3 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -16,7 +16,6 @@ from registrar.models import User logger = logging.getLogger(__name__) try: - logger.debug("oidc views initializing provider") # Initialize provider using pyOICD OP = getattr(settings, "OIDC_ACTIVE_PROVIDER") CLIENT = Client(OP) @@ -56,7 +55,6 @@ def error_page(request, error): def openid(request): """Redirect the user to an authentication provider (OP).""" - logger.debug("in openid") # If the session reset because of a server restart, attempt to login again request.session["acr_value"] = CLIENT.get_default_acr_value() @@ -70,7 +68,6 @@ def openid(request): def login_callback(request): """Analyze the token returned by the authentication provider (OP).""" - logger.debug("in login_callback") try: query = parse_qs(request.GET.urlencode()) userinfo = CLIENT.callback(query, request.session) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 617d8beea..372434887 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -437,7 +437,7 @@ LOGGING = { "formatter": "verbose", }, "django.server": { - "level": "DEBUG", + "level": "INFO", "class": "logging.StreamHandler", "formatter": "django.server", }, @@ -451,37 +451,37 @@ LOGGING = { # Django's generic logger "django": { "handlers": ["console"], - "level": "DEBUG", + "level": "INFO", "propagate": False, }, # Django's template processor "django.template": { "handlers": ["console"], - "level": "DEBUG", + "level": "INFO", "propagate": False, }, # Django's runserver "django.server": { "handlers": ["django.server"], - "level": "DEBUG", + "level": "INFO", "propagate": False, }, # Django's runserver requests "django.request": { "handlers": ["django.server"], - "level": "DEBUG", + "level": "INFO", "propagate": False, }, # OpenID Connect logger "oic": { "handlers": ["console"], - "level": "DEBUG", + "level": "INFO", "propagate": False, }, # Django wrapper for OpenID Connect "djangooidc": { "handlers": ["console"], - "level": "DEBUG", + "level": "INFO", "propagate": False, }, # Our app! @@ -516,7 +516,6 @@ LOGIN_URL = "/openid/login" # the initial login requests without erroring. LOGIN_REQUIRED_IGNORE_PATHS = [ r"/openid/(.+)$", - r"/health(.*)$", ] # where to go after logging out diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index f6378b555..4bd7b4baf 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -74,7 +74,7 @@ urlpatterns = [ views.ApplicationWithdrawn.as_view(), name="application-withdrawn", ), - path("health/", views.health), + path("health", views.health, name="health"), path("openid/", include("djangooidc.urls")), path("request/", include((application_urls, APPLICATION_NAMESPACE))), path("api/v1/available/", available, name="available"), diff --git a/src/registrar/tests/test_url_auth.py b/src/registrar/tests/test_url_auth.py index 3e0514a85..d4d343e18 100644 --- a/src/registrar/tests/test_url_auth.py +++ b/src/registrar/tests/test_url_auth.py @@ -114,6 +114,13 @@ class TestURLAuth(TestCase): "/api/v1/available/", "/api/v1/get-report/current-federal", "/api/v1/get-report/current-full", + "/health", + ] + + # We will test that the following URLs are not protected by auth + # and that the url returns a 200 response + NO_AUTH_URLS = [ + "/health", ] def assertURLIsProtectedByAuth(self, url): @@ -146,6 +153,27 @@ class TestURLAuth(TestCase): raise AssertionError( f"GET {url} returned HTTP {code}, but should redirect to login or deny access", ) + + def assertURLIsNotProtectedByAuth(self, url): + """ + Make a GET request to the given URL, and ensure that it returns 200. + """ + + try: + with less_console_noise(): + response = self.client.get(url) + except Exception as e: + # It'll be helpful to provide information on what URL was being + # accessed at the time the exception occurred. Python 3 will + # also include a full traceback of the original exception, so + # we don't need to worry about hiding the original cause. + raise AssertionError(f'Accessing {url} raised "{e}"', e) + + code = response.status_code + if code != 200: + raise AssertionError( + f"GET {url} returned HTTP {code}, but should return 200 OK", + ) def test_login_required_all_urls(self): """All URLs redirect to the login view.""" @@ -153,3 +181,6 @@ class TestURLAuth(TestCase): if url not in self.IGNORE_URLS: with self.subTest(viewname=viewname): self.assertURLIsProtectedByAuth(url) + elif url in self.NO_AUTH_URLS: + with self.subTest(viewname=viewname): + self.assertURLIsNotProtectedByAuth(url) diff --git a/src/registrar/views/health.py b/src/registrar/views/health.py index 620839dd2..9a4b449a5 100644 --- a/src/registrar/views/health.py +++ b/src/registrar/views/health.py @@ -1,13 +1,10 @@ -import logging from django.http import HttpResponse from login_required import login_not_required -logger = logging.getLogger(__name__) # the health check endpoint needs to be globally available so that the # PaaS orchestrator can make sure the app has come up properly @login_not_required def health(request): - logger.debug("in health check view") return HttpResponse('OK - Get.govOK') From d4c47cc3651b364e1111558f39cb1419022ce3de Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 7 Feb 2024 13:21:13 -0500 Subject: [PATCH 078/119] minor fixes to tests --- src/registrar/tests/test_url_auth.py | 2 +- src/registrar/tests/test_views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_url_auth.py b/src/registrar/tests/test_url_auth.py index d4d343e18..39ca00f4d 100644 --- a/src/registrar/tests/test_url_auth.py +++ b/src/registrar/tests/test_url_auth.py @@ -153,7 +153,7 @@ class TestURLAuth(TestCase): raise AssertionError( f"GET {url} returned HTTP {code}, but should redirect to login or deny access", ) - + def assertURLIsNotProtectedByAuth(self, url): """ Make a GET request to the given URL, and ensure that it returns 200. diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 450993e5c..1942a3839 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -23,7 +23,7 @@ class TestViews(TestCase): self.client = Client() def test_health_check_endpoint(self): - response = self.client.get("/health/") + response = self.client.get("/health") self.assertContains(response, "OK", status_code=200) def test_home_page(self): From 515c3711b38a92f203b3e0e07f7d45a5e154c909 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 7 Feb 2024 13:30:13 -0700 Subject: [PATCH 079/119] Add aria information --- src/registrar/assets/sass/_theme/_base.scss | 16 ++++++++++++---- src/registrar/templates/home.html | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 983af3a01..127db5589 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -139,10 +139,18 @@ abbr[title] { } } -@media (min-width: 768px) { +@include at-media(tablet) { .usa-tooltip__body { - width: 250px; - white-space: normal; - text-align: center; + width: 250px !important; + white-space: normal !important; + text-align: center !important; + } +} + +@include at-media(mobile) { + .usa-tooltip__body { + width: 250px !important; + white-space: normal !important; + text-align: center !important; } } diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 0747c9985..b5e8ca5a4 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -58,10 +58,10 @@ {% endif %} From 7bcb1e2e4f7bd3148dbbb14385967ea175ab138c Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 7 Feb 2024 16:14:25 -0500 Subject: [PATCH 080/119] New acs: skip emails for transitions from action needed and in review to submitted --- src/registrar/models/domain_application.py | 65 +++----- src/registrar/tests/test_admin.py | 51 +++++-- src/registrar/tests/test_models.py | 165 ++++++--------------- 3 files changed, 103 insertions(+), 178 deletions(-) diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index da801ce3d..f96ec1040 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -1,6 +1,4 @@ from __future__ import annotations -from json import JSONDecodeError -import json from typing import Union import logging @@ -14,7 +12,6 @@ from registrar.models.domain import Domain from .utility.time_stamped_model import TimeStampedModel from ..utility.email import send_templated_email, EmailSendingError from itertools import chain -from auditlog.models import LogEntry # type: ignore logger = logging.getLogger(__name__) @@ -568,26 +565,6 @@ class DomainApplication(TimeStampedModel): except Exception: return "" - def has_previously_had_a_status_of(self, status): - """Return True if this request has previously had the status of {passed param}.""" - - log_entries = LogEntry.objects.get_for_object(self) - - for entry in log_entries: - try: - changes_dict = json.loads(entry.changes) - # changes_dict will look like {'status': ['withdrawn', 'submitted']}, - # henceforth the len(changes_dict.get('status', [])) == 2 - status_change = changes_dict.get("status", []) - if len(status_change) == 2 and status_change[1] == status: - return True - except JSONDecodeError: - logger.warning( - "JSON decode error while parsing logs for domain requests in has_previously_had_a_status_of" - ) - - return False - def domain_is_not_active(self): if self.approved_domain: return not self.approved_domain.is_active() @@ -656,8 +633,8 @@ class DomainApplication(TimeStampedModel): self.submission_date = timezone.now().date() self.save() - # Limit email notifications for this transition to the first time the request transitions to this status - if not self.has_previously_had_a_status_of(DomainApplication.ApplicationStatus.SUBMITTED): + # Limit email notifications to transitions from Started and Withdrawn + if self.status == self.ApplicationStatus.STARTED or self.status == self.ApplicationStatus.WITHDRAWN: self._send_status_update_email( "submission confirmation", "emails/submission_confirmation.txt", @@ -738,14 +715,12 @@ class DomainApplication(TimeStampedModel): user=self.creator, domain=created_domain, role=UserDomainRole.Roles.MANAGER ) - # Limit email notifications for this transition to the first time the request transitions to this status - if not self.has_previously_had_a_status_of(DomainApplication.ApplicationStatus.APPROVED): - self._send_status_update_email( - "application approved", - "emails/status_change_approved.txt", - "emails/status_change_approved_subject.txt", - send_email, - ) + self._send_status_update_email( + "application approved", + "emails/status_change_approved.txt", + "emails/status_change_approved_subject.txt", + send_email, + ) @transition( field="status", @@ -755,13 +730,11 @@ class DomainApplication(TimeStampedModel): def withdraw(self): """Withdraw an application that has been submitted.""" - # Limit email notifications for this transition to the first time the request transitions to this status - if not self.has_previously_had_a_status_of(DomainApplication.ApplicationStatus.WITHDRAWN): - self._send_status_update_email( - "withdraw", - "emails/domain_request_withdrawn.txt", - "emails/domain_request_withdrawn_subject.txt", - ) + self._send_status_update_email( + "withdraw", + "emails/domain_request_withdrawn.txt", + "emails/domain_request_withdrawn_subject.txt", + ) @transition( field="status", @@ -787,13 +760,11 @@ class DomainApplication(TimeStampedModel): logger.error(err) logger.error("Can't query an approved domain while attempting a DA reject()") - # Limit email notifications for this transition to the first time the request transitions to this status - if not self.has_previously_had_a_status_of(DomainApplication.ApplicationStatus.REJECTED): - self._send_status_update_email( - "action needed", - "emails/status_change_rejected.txt", - "emails/status_change_rejected_subject.txt", - ) + self._send_status_update_email( + "action needed", + "emails/status_change_rejected.txt", + "emails/status_change_rejected_subject.txt", + ) @transition( field="status", diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 7773cb60b..83f777189 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -456,8 +456,11 @@ class TestDomainApplicationAdmin(MockEppLib): self.assertIn(expected_string, email_body) def test_save_model_sends_submitted_email(self): - """When transitioning to submitted the first time (and the first time only) on a domain request, - an email is sent out.""" + """When transitioning to submitted from started or withdrawn on a domain request, + an email is sent out. + + When transitioning to submitted from dns needed or in review on a domain request, + no email is sent out.""" # Ensure there is no user with this email EMAIL = "mayor@igorville.gov" @@ -466,7 +469,7 @@ class TestDomainApplicationAdmin(MockEppLib): # Create a sample application application = completed_application() - # Test Submitted Status + # Test Submitted Status from started self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL) self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) @@ -478,13 +481,33 @@ class TestDomainApplicationAdmin(MockEppLib): ) self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - # Test Submitted Status Again (No new email should be sent) + # Test Submitted Status Again (from withdrawn) self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Move it to IN_REVIEW + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.IN_REVIEW) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Test Submitted Status Again from in IN_REVIEW, no new email should be sent + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Move it to IN_REVIEW + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.IN_REVIEW) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Move it to ACTION_NEEDED + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.ACTION_NEEDED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Test Submitted Status Again from in ACTION_NEEDED, no new email should be sent + self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) def test_save_model_sends_approved_email(self): - """When transitioning to approved the first time (and the first time only) on a domain request, - an email is sent out.""" + """When transitioning to approved on a domain request, + an email is sent out every time.""" # Ensure there is no user with this email EMAIL = "mayor@igorville.gov" @@ -505,11 +528,11 @@ class TestDomainApplicationAdmin(MockEppLib): # Test Submitted Status Again (No new email should be sent) self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) def test_save_model_sends_rejected_email(self): - """When transitioning to rejected the first time (and the first time only) on a domain request, - an email is sent out.""" + """When transitioning to rejected on a domain request, + an email is sent out every time.""" # Ensure there is no user with this email EMAIL = "mayor@igorville.gov" @@ -530,11 +553,11 @@ class TestDomainApplicationAdmin(MockEppLib): # Test Submitted Status Again (No new email should be sent) self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) def test_save_model_sends_withdrawn_email(self): - """When transitioning to withdrawn the first time (and the first time only) on a domain request, - an email is sent out.""" + """When transitioning to withdrawn on a domain request, + an email is sent out every time.""" # Ensure there is no user with this email EMAIL = "mayor@igorville.gov" @@ -557,7 +580,7 @@ class TestDomainApplicationAdmin(MockEppLib): # Test Submitted Status Again (No new email should be sent) self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) def test_save_model_sets_approved_domain(self): # make sure there is no user with this email diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index a1b22373d..66c8d04f8 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -1,6 +1,6 @@ from django.test import TestCase from django.db.utils import IntegrityError -from unittest.mock import MagicMock, patch +from unittest.mock import patch from registrar.models import ( Contact, @@ -154,30 +154,7 @@ class TestDomainApplication(TestCase): application.submit() self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED) - @patch("auditlog.models.LogEntry.objects.get_for_object") - def test_has_previously_had_a_status_of_returns_true(self, mock_get_for_object): - """Set up mock LogEntry.objects.get_for_object to return a log entry with the desired status""" - - log_entry_with_status = MagicMock(changes='{"status": ["previous_status", "desired_status"]}') - mock_get_for_object.return_value = [log_entry_with_status] - - result = self.started_application.has_previously_had_a_status_of("desired_status") - - self.assertTrue(result) - - @patch("auditlog.models.LogEntry.objects.get_for_object") - def test_has_previously_had_a_status_of_returns_false(self, mock_get_for_object): - """Set up mock LogEntry.objects.get_for_object to return a log entry - with a different status than the desired status""" - - log_entry_with_status = MagicMock(changes='{"status": ["previous_status", "different_status"]}') - mock_get_for_object.return_value = [log_entry_with_status] - - result = self.started_application.has_previously_had_a_status_of("desired_status") - - self.assertFalse(result) - - def test_submit_sends_email(self): + def test_submit_from_started_sends_email(self): """Create an application and submit it and see if email was sent.""" # submitter's email is mayor@igorville.gov @@ -199,17 +176,55 @@ class TestDomainApplication(TestCase): 0, ) - @patch("auditlog.models.LogEntry.objects.get_for_object") - def test_submit_does_not_send_email_if_submitted_previously(self, mock_get_for_object): - """Create an application, make it so it was submitted previously, submit it, - and see that an email was not sent.""" + def test_submit_from_withdrawn_sends_email(self): + """Create a withdrawn application and submit it and see if email was sent.""" # submitter's email is mayor@igorville.gov - application = completed_application() + application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN) - # Mock the logs - log_entry_with_status = MagicMock(changes='{"status": ["started", "submitted"]}') - mock_get_for_object.return_value = [log_entry_with_status] + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.submit() + + # check to see if an email was sent + self.assertGreater( + len( + [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + ), + 0, + ) + + def test_submit_from_action_needed_does_not_send_email(self): + """Create a withdrawn application and submit it and see if email was sent.""" + + # submitter's email is mayor@igorville.gov + application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED) + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.submit() + + # check to see if an email was sent + self.assertEqual( + len( + [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + ), + 0, + ) + + def test_submit_from_in_review_does_not_send_email(self): + """Create a withdrawn application and submit it and see if email was sent.""" + + # submitter's email is mayor@igorville.gov + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) with boto3_mocking.clients.handler_for("sesv2", self.mock_client): with less_console_noise(): @@ -249,34 +264,6 @@ class TestDomainApplication(TestCase): 0, ) - @patch("auditlog.models.LogEntry.objects.get_for_object") - def test_approve_does_not_send_email_if_approved_previously(self, mock_get_for_object): - """Create an application, make it so it was approved previously, approve it, - and see that an email was not sent.""" - - # submitter's email is mayor@igorville.gov - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - - # Mock the logs - log_entry_with_status = MagicMock(changes='{"status": ["submitted", "approved"]}') - mock_get_for_object.return_value = [log_entry_with_status] - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.approve() - - # check to see if an email was sent - self.assertEqual( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) - def test_withdraw_sends_email(self): """Create an application and withdraw it and see if email was sent.""" @@ -299,34 +286,6 @@ class TestDomainApplication(TestCase): 0, ) - @patch("auditlog.models.LogEntry.objects.get_for_object") - def test_withdraw_does_not_send_email_if_withdrawn_previously(self, mock_get_for_object): - """Create an application, make it so it was withdrawn previously, withdraw it, - and see that an email was not sent.""" - - # submitter's email is mayor@igorville.gov - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - - # Mock the logs - log_entry_with_status = MagicMock(changes='{"status": ["submitted", "withdrawn"]}') - mock_get_for_object.return_value = [log_entry_with_status] - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.withdraw() - - # check to see if an email was sent - self.assertEqual( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) - def test_reject_sends_email(self): """Create an application and reject it and see if email was sent.""" @@ -349,34 +308,6 @@ class TestDomainApplication(TestCase): 0, ) - @patch("auditlog.models.LogEntry.objects.get_for_object") - def test_reject_does_not_send_email_if_rejected_previously(self, mock_get_for_object): - """Create an application, make it so it was rejected previously, reject it, - and see that an email was not sent.""" - - # submitter's email is mayor@igorville.gov - application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - - # Mock the logs - log_entry_with_status = MagicMock(changes='{"status": ["submitted", "rejected"]}') - mock_get_for_object.return_value = [log_entry_with_status] - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.reject() - - # check to see if an email was sent - self.assertEqual( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) - def test_submit_transition_allowed(self): """ Test that calling submit from allowable statuses does raises TransitionNotAllowed. From 3318892d6827f5f50b17a1c570f8cf56ff87c1b9 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 7 Feb 2024 16:29:16 -0500 Subject: [PATCH 081/119] fix indent error --- src/registrar/tests/test_models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index d0782c359..7f866ad8b 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -167,8 +167,8 @@ class TestDomainApplication(TestCase): # submitter's email is mayor@igorville.gov application = completed_application() - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - application.submit() + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + application.submit() # check to see if an email was sent self.assertGreater( From 0a942e4183fe0201338fa7f16ffee16992f6357e Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 7 Feb 2024 16:43:51 -0500 Subject: [PATCH 082/119] remove duplicate test def --- src/registrar/tests/test_admin.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index a5ea89072..f90b18584 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -744,13 +744,6 @@ class TestDomainApplicationAdmin(MockEppLib): "Cannot edit an application with a restricted creator.", ) - def test_error_when_saving_approved_to_rejected_and_domain_is_active(self): - # Create an instance of the model - application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - domain = Domain.objects.create(name=application.requested_domain.name) - application.approved_domain = domain - application.save() - def trigger_saving_approved_to_another_state(self, domain_is_active, another_state): """Helper method that triggers domain request state changes from approved to another state, with an associated domain that can be either active (READY) or not. From 823f134687296cd351a152da7767a8d84b7ff5f4 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 7 Feb 2024 14:54:28 -0700 Subject: [PATCH 083/119] Test update to Django 4.2.3 --- src/Pipfile | 2 +- src/Pipfile.lock | 1139 ++++++++++++++++++++---------------------- src/requirements.txt | 46 +- 3 files changed, 570 insertions(+), 617 deletions(-) diff --git a/src/Pipfile b/src/Pipfile index d4a9bbafb..a0ada646a 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -django = "*" +django = "4.2.3" cfenv = "*" django-cors-headers = "*" pycryptodomex = "*" diff --git a/src/Pipfile.lock b/src/Pipfile.lock index d265123de..3ac2fad0e 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a0ea45d8f77132e22b8c3437e90903240af1bb29f9994f6ce4ce1e5b8d06b6ed" + "sha256": "8db8d1bdb9c343c50771fc8bd001c555037dc3606a48dcafb3800d743fb95f3e" }, "pipfile-spec": 6, "requires": {}, @@ -32,20 +32,20 @@ }, "boto3": { "hashes": [ - "sha256:d12467fb3a64d359b0bda0570a8163a5859fcac13e786f2a3db0392523178556", - "sha256:eed0f7df91066b6ac63a53d16459ac082458d57061bedf766135d9e1c2b75a6b" + "sha256:65acfe7f1cf2a9b7df3d4edb87c8022e02685825bd1957e7bb678cc0d09f5e5f", + "sha256:73f5ec89cb3ddb3ed577317889fd2f2df783f66b6502a9a4239979607e33bf74" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==1.33.7" + "markers": "python_version >= '3.8'", + "version": "==1.34.37" }, "botocore": { "hashes": [ - "sha256:71ec0e85b996cf9def3dd8f4ca6cb4a9fd3a614aa4c9c7cbf33f2f68e1d0649a", - "sha256:b2299bc13bb8c0928edc98bf4594deb14cba2357536120f63772027a16ce7374" + "sha256:2a5bf33aacd2d970afd3d492e179e06ea98a5469030d5cfe7a2ad9995f7bb2ef", + "sha256:3c46ddb1679e6ef45ca78b48665398636bda532a07cd476e4b500697d13d9a99" ], - "markers": "python_version >= '3.7'", - "version": "==1.33.7" + "markers": "python_version >= '3.8'", + "version": "==1.34.37" }, "cachetools": { "hashes": [ @@ -58,11 +58,11 @@ }, "certifi": { "hashes": [ - "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", - "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" + "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", + "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" ], "markers": "python_version >= '3.6'", - "version": "==2023.11.17" + "version": "==2024.2.2" }, "cfenv": { "hashes": [ @@ -127,7 +127,7 @@ "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" ], - "markers": "python_version >= '3.8'", + "markers": "platform_python_implementation != 'PyPy'", "version": "==1.16.0" }, "charset-normalizer": { @@ -228,32 +228,41 @@ }, "cryptography": { "hashes": [ - "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960", - "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a", - "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc", - "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a", - "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf", - "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1", - "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39", - "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406", - "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a", - "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a", - "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c", - "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be", - "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15", - "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2", - "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d", - "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157", - "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003", - "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248", - "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a", - "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec", - "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309", - "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7", - "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d" + "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380", + "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589", + "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea", + "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65", + "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a", + "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3", + "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008", + "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1", + "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2", + "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635", + "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2", + "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90", + "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee", + "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a", + "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242", + "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12", + "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2", + "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d", + "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be", + "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee", + "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6", + "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529", + "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929", + "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1", + "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6", + "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a", + "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446", + "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9", + "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888", + "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4", + "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33", + "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f" ], "markers": "python_version >= '3.7'", - "version": "==41.0.7" + "version": "==42.0.2" }, "defusedxml": { "hashes": [ @@ -279,12 +288,12 @@ }, "django": { "hashes": [ - "sha256:8e0f1c2c2786b5c0e39fe1afce24c926040fad47c8ea8ad30aaf1188df29fc41", - "sha256:e1d37c51ad26186de355cbcec16613ebdabfa9689bbade9c538835205a8abbe9" + "sha256:45a747e1c5b3d6df1b141b1481e193b033fd1fdbda3ff52677dc81afdaacbaed", + "sha256:f7c7852a5ac5a3da5a8d5b35cc6168f31b605971441798dac845f17ca8028039" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.2.7" + "version": "==4.2.3" }, "django-allow-cidr": { "hashes": [ @@ -347,11 +356,11 @@ "phonenumberslite" ], "hashes": [ - "sha256:16778f2717ea2aecc6178beb0d6bc431c78c6a8b0474e1fa8face040efeb6e9e", - "sha256:20c7c5c449e33eed5fd45ef8d3dc668faabaeff3277eddd1892b262d686ba381" + "sha256:bc6eaa49d1f9d870944f5280258db511e3a1ba5e2fbbed255488dceacae45d06", + "sha256:f9cdb3de085f99c249328293a3b93d4e5fa440c0c8e3b99eb0d0f54748629797" ], "markers": "python_version >= '3.8'", - "version": "==7.2.0" + "version": "==7.3.0" }, "django-widget-tweaks": { "hashes": [ @@ -367,19 +376,20 @@ "django" ], "hashes": [ - "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124", - "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9" + "sha256:cc421ddb143fa30183568164755aa113a160e555cd19e97e664c478662032c24", + "sha256:feeaf28f17fd0499f9cd7c0fcf408c6d82c308e69e335eb92d09322fc9ed8138" ], - "markers": "python_version >= '3.6'", - "version": "==9.5.0" + "markers": "python_version >= '3.8'", + "version": "==10.3.0" }, "faker": { "hashes": [ - "sha256:562a3a09c3ed3a1a7b20e13d79f904dfdfc5e740f72813ecf95e4cf71e5a2f52", - "sha256:aeb3e26742863d1e387f9d156f1c36e14af63bf5e6f36fb39b8c27f6a903be38" + "sha256:60e89e5c0b584e285a7db05eceba35011a241954afdab2853cb246c8a56700a2", + "sha256:b7f76bb1b2ac4cdc54442d955e36e477c387000f31ce46887fb9722a041be60b" ], + "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==20.1.0" + "version": "==23.1.0" }, "fred-epplib": { "git": "https://github.com/cisagov/epplib.git", @@ -448,71 +458,72 @@ }, "geventconnpool": { "git": "https://github.com/rasky/geventconnpool.git", - "ref": "1bbb93a714a331a069adf27265fe582d9ba7ecd4" + "ref": null }, "greenlet": { "hashes": [ - "sha256:0a02d259510b3630f330c86557331a3b0e0c79dac3d166e449a39363beaae174", - "sha256:0b6f9f8ca7093fd4433472fd99b5650f8a26dcd8ba410e14094c1e44cd3ceddd", - "sha256:100f78a29707ca1525ea47388cec8a049405147719f47ebf3895e7509c6446aa", - "sha256:1757936efea16e3f03db20efd0cd50a1c86b06734f9f7338a90c4ba85ec2ad5a", - "sha256:19075157a10055759066854a973b3d1325d964d498a805bb68a1f9af4aaef8ec", - "sha256:19bbdf1cce0346ef7341705d71e2ecf6f41a35c311137f29b8a2dc2341374565", - "sha256:20107edf7c2c3644c67c12205dc60b1bb11d26b2610b276f97d666110d1b511d", - "sha256:22f79120a24aeeae2b4471c711dcf4f8c736a2bb2fabad2a67ac9a55ea72523c", - "sha256:2847e5d7beedb8d614186962c3d774d40d3374d580d2cbdab7f184580a39d234", - "sha256:28e89e232c7593d33cac35425b58950789962011cc274aa43ef8865f2e11f46d", - "sha256:329c5a2e5a0ee942f2992c5e3ff40be03e75f745f48847f118a3cfece7a28546", - "sha256:337322096d92808f76ad26061a8f5fccb22b0809bea39212cd6c406f6a7060d2", - "sha256:3fcc780ae8edbb1d050d920ab44790201f027d59fdbd21362340a85c79066a74", - "sha256:41bdeeb552d814bcd7fb52172b304898a35818107cc8778b5101423c9017b3de", - "sha256:4eddd98afc726f8aee1948858aed9e6feeb1758889dfd869072d4465973f6bfd", - "sha256:52e93b28db27ae7d208748f45d2db8a7b6a380e0d703f099c949d0f0d80b70e9", - "sha256:55d62807f1c5a1682075c62436702aaba941daa316e9161e4b6ccebbbf38bda3", - "sha256:5805e71e5b570d490938d55552f5a9e10f477c19400c38bf1d5190d760691846", - "sha256:599daf06ea59bfedbec564b1692b0166a0045f32b6f0933b0dd4df59a854caf2", - "sha256:60d5772e8195f4e9ebf74046a9121bbb90090f6550f81d8956a05387ba139353", - "sha256:696d8e7d82398e810f2b3622b24e87906763b6ebfd90e361e88eb85b0e554dc8", - "sha256:6e6061bf1e9565c29002e3c601cf68569c450be7fc3f7336671af7ddb4657166", - "sha256:80ac992f25d10aaebe1ee15df45ca0d7571d0f70b645c08ec68733fb7a020206", - "sha256:816bd9488a94cba78d93e1abb58000e8266fa9cc2aa9ccdd6eb0696acb24005b", - "sha256:85d2b77e7c9382f004b41d9c72c85537fac834fb141b0296942d52bf03fe4a3d", - "sha256:87c8ceb0cf8a5a51b8008b643844b7f4a8264a2c13fcbcd8a8316161725383fe", - "sha256:89ee2e967bd7ff85d84a2de09df10e021c9b38c7d91dead95b406ed6350c6997", - "sha256:8bef097455dea90ffe855286926ae02d8faa335ed8e4067326257cb571fc1445", - "sha256:8d11ebbd679e927593978aa44c10fc2092bc454b7d13fdc958d3e9d508aba7d0", - "sha256:91e6c7db42638dc45cf2e13c73be16bf83179f7859b07cfc139518941320be96", - "sha256:97e7ac860d64e2dcba5c5944cfc8fa9ea185cd84061c623536154d5a89237884", - "sha256:990066bff27c4fcf3b69382b86f4c99b3652bab2a7e685d968cd4d0cfc6f67c6", - "sha256:9fbc5b8f3dfe24784cee8ce0be3da2d8a79e46a276593db6868382d9c50d97b1", - "sha256:ac4a39d1abae48184d420aa8e5e63efd1b75c8444dd95daa3e03f6c6310e9619", - "sha256:b2c02d2ad98116e914d4f3155ffc905fd0c025d901ead3f6ed07385e19122c94", - "sha256:b2d3337dcfaa99698aa2377c81c9ca72fcd89c07e7eb62ece3f23a3fe89b2ce4", - "sha256:b489c36d1327868d207002391f662a1d163bdc8daf10ab2e5f6e41b9b96de3b1", - "sha256:b641161c302efbb860ae6b081f406839a8b7d5573f20a455539823802c655f63", - "sha256:b8ba29306c5de7717b5761b9ea74f9c72b9e2b834e24aa984da99cbfc70157fd", - "sha256:b9934adbd0f6e476f0ecff3c94626529f344f57b38c9a541f87098710b18af0a", - "sha256:ce85c43ae54845272f6f9cd8320d034d7a946e9773c693b27d620edec825e376", - "sha256:cf868e08690cb89360eebc73ba4be7fb461cfbc6168dd88e2fbbe6f31812cd57", - "sha256:d2905ce1df400360463c772b55d8e2518d0e488a87cdea13dd2c71dcb2a1fa16", - "sha256:d57e20ba591727da0c230ab2c3f200ac9d6d333860d85348816e1dca4cc4792e", - "sha256:d6a8c9d4f8692917a3dc7eb25a6fb337bff86909febe2f793ec1928cd97bedfc", - "sha256:d923ff276f1c1f9680d32832f8d6c040fe9306cbfb5d161b0911e9634be9ef0a", - "sha256:daa7197b43c707462f06d2c693ffdbb5991cbb8b80b5b984007de431493a319c", - "sha256:dbd4c177afb8a8d9ba348d925b0b67246147af806f0b104af4d24f144d461cd5", - "sha256:dc4d815b794fd8868c4d67602692c21bf5293a75e4b607bb92a11e821e2b859a", - "sha256:e9d21aaa84557d64209af04ff48e0ad5e28c5cca67ce43444e939579d085da72", - "sha256:ea6b8aa9e08eea388c5f7a276fabb1d4b6b9d6e4ceb12cc477c3d352001768a9", - "sha256:eabe7090db68c981fca689299c2d116400b553f4b713266b130cfc9e2aa9c5a9", - "sha256:f2f6d303f3dee132b322a14cd8765287b8f86cdc10d2cb6a6fae234ea488888e", - "sha256:f33f3258aae89da191c6ebaa3bc517c6c4cbc9b9f689e5d8452f7aedbb913fa8", - "sha256:f7bfb769f7efa0eefcd039dd19d843a4fbfbac52f1878b1da2ed5793ec9b1a65", - "sha256:f89e21afe925fcfa655965ca8ea10f24773a1791400989ff32f467badfe4a064", - "sha256:fa24255ae3c0ab67e613556375a4341af04a084bd58764731972bcbc8baeba36" + "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67", + "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6", + "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257", + "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4", + "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676", + "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61", + "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc", + "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca", + "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7", + "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728", + "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305", + "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6", + "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379", + "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414", + "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04", + "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a", + "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf", + "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491", + "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559", + "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e", + "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274", + "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb", + "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b", + "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9", + "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b", + "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be", + "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506", + "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405", + "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113", + "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f", + "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5", + "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230", + "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d", + "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f", + "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a", + "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e", + "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61", + "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6", + "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d", + "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71", + "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22", + "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2", + "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3", + "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067", + "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc", + "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881", + "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3", + "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e", + "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac", + "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53", + "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0", + "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b", + "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83", + "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41", + "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c", + "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf", + "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", + "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==3.0.1" + "version": "==3.0.3" }, "gunicorn": { "hashes": [ @@ -541,183 +552,169 @@ }, "lxml": { "hashes": [ - "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3", - "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d", - "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a", - "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120", - "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305", - "sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287", - "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23", - "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52", - "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f", - "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4", - "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584", - "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f", - "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693", - "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef", - "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5", - "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02", - "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc", - "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7", - "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da", - "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a", - "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40", - "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8", - "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd", - "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601", - "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c", - "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be", - "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2", - "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c", - "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129", - "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc", - "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2", - "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1", - "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7", - "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d", - "sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477", - "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d", - "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e", - "sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7", - "sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2", - "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574", - "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf", - "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b", - "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98", - "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12", - "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42", - "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35", - "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d", - "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce", - "sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d", - "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f", - "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db", - "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4", - "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694", - "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac", - "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2", - "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7", - "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96", - "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d", - "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b", - "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a", - "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13", - "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340", - "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6", - "sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458", - "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c", - "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c", - "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9", - "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432", - "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991", - "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69", - "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf", - "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb", - "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b", - "sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833", - "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76", - "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85", - "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e", - "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50", - "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8", - "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4", - "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b", - "sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5", - "sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190", - "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7", - "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa", - "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0", - "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9", - "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0", - "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b", - "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5", - "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7", - "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4" + "sha256:13521a321a25c641b9ea127ef478b580b5ec82aa2e9fc076c86169d161798b01", + "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f", + "sha256:16018f7099245157564d7148165132c70adb272fb5a17c048ba70d9cc542a1a1", + "sha256:16dd953fb719f0ffc5bc067428fc9e88f599e15723a85618c45847c96f11f431", + "sha256:19a1bc898ae9f06bccb7c3e1dfd73897ecbbd2c96afe9095a6026016e5ca97b8", + "sha256:1ad17c20e3666c035db502c78b86e58ff6b5991906e55bdbef94977700c72623", + "sha256:22b7ee4c35f374e2c20337a95502057964d7e35b996b1c667b5c65c567d2252a", + "sha256:24ef5a4631c0b6cceaf2dbca21687e29725b7c4e171f33a8f8ce23c12558ded1", + "sha256:25663d6e99659544ee8fe1b89b1a8c0aaa5e34b103fab124b17fa958c4a324a6", + "sha256:262bc5f512a66b527d026518507e78c2f9c2bd9eb5c8aeeb9f0eb43fcb69dc67", + "sha256:280f3edf15c2a967d923bcfb1f8f15337ad36f93525828b40a0f9d6c2ad24890", + "sha256:2ad3a8ce9e8a767131061a22cd28fdffa3cd2dc193f399ff7b81777f3520e372", + "sha256:2befa20a13f1a75c751f47e00929fb3433d67eb9923c2c0b364de449121f447c", + "sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb", + "sha256:304128394c9c22b6569eba2a6d98392b56fbdfbad58f83ea702530be80d0f9df", + "sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84", + "sha256:3aeca824b38ca78d9ee2ab82bd9883083d0492d9d17df065ba3b94e88e4d7ee6", + "sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45", + "sha256:3e3898ae2b58eeafedfe99e542a17859017d72d7f6a63de0f04f99c2cb125936", + "sha256:3eea6ed6e6c918e468e693c41ef07f3c3acc310b70ddd9cc72d9ef84bc9564ca", + "sha256:3f14a4fb1c1c402a22e6a341a24c1341b4a3def81b41cd354386dcb795f83897", + "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a", + "sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d", + "sha256:49a9b4af45e8b925e1cd6f3b15bbba2c81e7dba6dce170c677c9cda547411e14", + "sha256:4f8b0c78e7aac24979ef09b7f50da871c2de2def043d468c4b41f512d831e912", + "sha256:52427a7eadc98f9e62cb1368a5079ae826f94f05755d2d567d93ee1bc3ceb354", + "sha256:5e53d7e6a98b64fe54775d23a7c669763451340c3d44ad5e3a3b48a1efbdc96f", + "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c", + "sha256:601f4a75797d7a770daed8b42b97cd1bb1ba18bd51a9382077a6a247a12aa38d", + "sha256:61c5a7edbd7c695e54fca029ceb351fc45cd8860119a0f83e48be44e1c464862", + "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969", + "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e", + "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8", + "sha256:704f5572ff473a5f897745abebc6df40f22d4133c1e0a1f124e4f2bd3330ff7e", + "sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa", + "sha256:7cfced4a069003d8913408e10ca8ed092c49a7f6cefee9bb74b6b3e860683b45", + "sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a", + "sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147", + "sha256:82cd34f1081ae4ea2ede3d52f71b7be313756e99b4b5f829f89b12da552d3aa3", + "sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3", + "sha256:877efb968c3d7eb2dad540b6cabf2f1d3c0fbf4b2d309a3c141f79c7e0061324", + "sha256:8b9f19df998761babaa7f09e6bc169294eefafd6149aaa272081cbddc7ba4ca3", + "sha256:8cf5877f7ed384dabfdcc37922c3191bf27e55b498fecece9fd5c2c7aaa34c33", + "sha256:8d2900b7f5318bc7ad8631d3d40190b95ef2aa8cc59473b73b294e4a55e9f30f", + "sha256:8d7b4beebb178e9183138f552238f7e6613162a42164233e2bda00cb3afac58f", + "sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764", + "sha256:98f3f020a2b736566c707c8e034945c02aa94e124c24f77ca097c446f81b01f1", + "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114", + "sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581", + "sha256:9bcf86dfc8ff3e992fed847c077bd875d9e0ba2fa25d859c3a0f0f76f07f0c8d", + "sha256:9bd0ae7cc2b85320abd5e0abad5ccee5564ed5f0cc90245d2f9a8ef330a8deae", + "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da", + "sha256:9e5ac3437746189a9b4121db2a7b86056ac8786b12e88838696899328fc44bb2", + "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e", + "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda", + "sha256:a96f02ba1bcd330807fc060ed91d1f7a20853da6dd449e5da4b09bfcc08fdcf5", + "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa", + "sha256:ae15347a88cf8af0949a9872b57a320d2605ae069bcdf047677318bc0bba45b1", + "sha256:af8920ce4a55ff41167ddbc20077f5698c2e710ad3353d32a07d3264f3a2021e", + "sha256:afd825e30f8d1f521713a5669b63657bcfe5980a916c95855060048b88e1adb7", + "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1", + "sha256:b4b68c961b5cc402cbd99cca5eb2547e46ce77260eb705f4d117fd9c3f932b95", + "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93", + "sha256:b9e240ae0ba96477682aa87899d94ddec1cc7926f9df29b1dd57b39e797d5ab5", + "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b", + "sha256:bf8443781533b8d37b295016a4b53c1494fa9a03573c09ca5104550c138d5c05", + "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5", + "sha256:c3cd1fc1dc7c376c54440aeaaa0dcc803d2126732ff5c6b68ccd619f2e64be4f", + "sha256:c7257171bb8d4432fe9d6fdde4d55fdbe663a63636a17f7f9aaba9bcb3153ad7", + "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8", + "sha256:d74fcaf87132ffc0447b3c685a9f862ffb5b43e70ea6beec2fb8057d5d2a1fea", + "sha256:d8c1d679df4361408b628f42b26a5d62bd3e9ba7f0c0e7969f925021554755aa", + "sha256:e856c1c7255c739434489ec9c8aa9cdf5179785d10ff20add308b5d673bed5cd", + "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b", + "sha256:ed7326563024b6e91fef6b6c7a1a2ff0a71b97793ac33dbbcf38f6005e51ff6e", + "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4", + "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204", + "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==4.9.3" + "markers": "python_version >= '3.6'", + "version": "==5.1.0" }, "mako": { "hashes": [ - "sha256:57d4e997349f1a92035aa25c17ace371a4213f2ca42f99bee9a602500cfd54d9", - "sha256:e3a9d388fd00e87043edbe8792f45880ac0114e9c4adc69f6e9bfb2c55e3b11b" + "sha256:2a0c8ad7f6274271b3bb7467dd37cf9cc6dab4bc19cb69a4ef10669402de698e", + "sha256:32a99d70754dfce237019d17ffe4a282d2d3351b9c476e90d8a60e63f133b80c" ], "markers": "python_version >= '3.8'", - "version": "==1.3.0" + "version": "==1.3.2" }, "markupsafe": { "hashes": [ - "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", - "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", - "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", - "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", - "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c", - "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", - "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", - "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb", - "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939", - "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", - "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", - "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", - "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", - "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", - "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", - "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", - "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd", - "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", - "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", - "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", - "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", - "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", - "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", - "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", - "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", - "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007", - "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", - "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", - "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", - "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", - "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", - "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", - "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", - "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1", - "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", - "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", - "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c", - "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", - "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823", - "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", - "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", - "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", - "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", - "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", - "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", - "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", - "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", - "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", - "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", - "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", - "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", - "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", - "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", - "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", - "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", - "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", - "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", - "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc", - "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2", - "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11" + "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", + "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", + "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", + "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", + "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", + "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", + "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", + "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", + "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", + "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", + "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", + "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", + "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", + "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", + "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", + "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", + "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", + "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", + "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", + "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", + "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", + "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", + "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", + "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", + "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", + "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", + "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", + "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", + "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", + "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", + "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", + "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", + "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", + "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", + "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", + "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", + "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", + "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", + "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", + "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", + "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", + "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", + "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", + "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", + "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", + "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", + "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", + "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", + "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", + "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", + "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", + "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", + "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", + "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", + "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", + "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", + "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", + "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", + "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", + "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" ], "markers": "python_version >= '3.7'", - "version": "==2.1.3" + "version": "==2.1.5" }, "marshmallow": { "hashes": [ - "sha256:5d2371bbe42000f2b3fb5eaa065224df7d8f8597bc19a1bbfa5bfe7fba8da889", - "sha256:684939db93e80ad3561392f47be0230743131560a41c5110684c16e21ade0a5c" + "sha256:4c1daff273513dc5eb24b219a8035559dc573c8f322558ef85f5438ddd1236dd", + "sha256:c21d4b98fee747c130e6bc8f45c4b3199ea66bc00c12ee1f639f0aeca034d5e9" ], "markers": "python_version >= '3.8'", - "version": "==3.20.1" + "version": "==3.20.2" }, "oic": { "hashes": [ @@ -745,10 +742,10 @@ }, "phonenumberslite": { "hashes": [ - "sha256:305736b1b489e2bc6831710a2f34a9324f2bf96a1e77c8e0b3136dfaf9ca7753", - "sha256:6356f2728fa1d2c2bc9e79c3bfcfedc91a36537df7a134f150731a821a469a96" + "sha256:2b04a53401d01ab42564c1abc762fc9808ad398e71dacfa3b38d4321e112ecb3", + "sha256:74e3ee63dfa2bb562ce2e6ce74ce76ae74a2f81472005b80343235fb43426db4" ], - "version": "==8.13.26" + "version": "==8.13.29" }, "psycopg2-binary": { "hashes": [ @@ -838,161 +835,135 @@ }, "pycryptodomex": { "hashes": [ - "sha256:09c9401dc06fb3d94cb1ec23b4ea067a25d1f4c6b7b118ff5631d0b5daaab3cc", - "sha256:0b2f1982c5bc311f0aab8c293524b861b485d76f7c9ab2c3ac9a25b6f7655975", - "sha256:136b284e9246b4ccf4f752d435c80f2c44fc2321c198505de1d43a95a3453b3c", - "sha256:1789d89f61f70a4cd5483d4dfa8df7032efab1118f8b9894faae03c967707865", - "sha256:2126bc54beccbede6eade00e647106b4f4c21e5201d2b0a73e9e816a01c50905", - "sha256:258c4233a3fe5a6341780306a36c6fb072ef38ce676a6d41eec3e591347919e8", - "sha256:263de9a96d2fcbc9f5bd3a279f14ea0d5f072adb68ebd324987576ec25da084d", - "sha256:50cb18d4dd87571006fd2447ccec85e6cec0136632a550aa29226ba075c80644", - "sha256:5b883e1439ab63af976656446fb4839d566bb096f15fc3c06b5a99cde4927188", - "sha256:5d73e9fa3fe830e7b6b42afc49d8329b07a049a47d12e0ef9225f2fd220f19b2", - "sha256:61056a1fd3254f6f863de94c233b30dd33bc02f8c935b2000269705f1eeeffa4", - "sha256:67c8eb79ab33d0fbcb56842992298ddb56eb6505a72369c20f60bc1d2b6fb002", - "sha256:6e45bb4635b3c4e0a00ca9df75ef6295838c85c2ac44ad882410cb631ed1eeaa", - "sha256:7cb51096a6a8d400724104db8a7e4f2206041a1f23e58924aa3d8d96bcb48338", - "sha256:800a2b05cfb83654df80266692f7092eeefe2a314fa7901dcefab255934faeec", - "sha256:8df69e41f7e7015a90b94d1096ec3d8e0182e73449487306709ec27379fff761", - "sha256:917033016ecc23c8933205585a0ab73e20020fdf671b7cd1be788a5c4039840b", - "sha256:a12144d785518f6491ad334c75ccdc6ad52ea49230b4237f319dbb7cef26f464", - "sha256:a3866d68e2fc345162b1b9b83ef80686acfe5cec0d134337f3b03950a0a8bf56", - "sha256:a588a1cb7781da9d5e1c84affd98c32aff9c89771eac8eaa659d2760666f7139", - "sha256:a77b79852175064c822b047fee7cf5a1f434f06ad075cc9986aa1c19a0c53eb0", - "sha256:af83a554b3f077564229865c45af0791be008ac6469ef0098152139e6bd4b5b6", - "sha256:b801216c48c0886742abf286a9a6b117e248ca144d8ceec1f931ce2dd0c9cb40", - "sha256:bfb040b5dda1dff1e197d2ef71927bd6b8bfcb9793bc4dfe0bb6df1e691eaacb", - "sha256:c01678aee8ac0c1a461cbc38ad496f953f9efcb1fa19f5637cbeba7544792a53", - "sha256:c74eb1f73f788facece7979ce91594dc177e1a9b5d5e3e64697dd58299e5cb4d", - "sha256:c9a68a2f7bd091ccea54ad3be3e9d65eded813e6d79fdf4cc3604e26cdd6384f", - "sha256:d4dd3b381ff5a5907a3eb98f5f6d32c64d319a840278ceea1dcfcc65063856f3", - "sha256:e8e5ecbd4da4157889fce8ba49da74764dd86c891410bfd6b24969fa46edda51", - "sha256:eb2fc0ec241bf5e5ef56c8fbec4a2634d631e4c4f616a59b567947a0f35ad83c", - "sha256:edbe083c299835de7e02c8aa0885cb904a75087d35e7bab75ebe5ed336e8c3e2", - "sha256:ff64fd720def623bf64d8776f8d0deada1cc1bf1ec3c1f9d6f5bb5bd098d034f" + "sha256:0daad007b685db36d977f9de73f61f8da2a7104e20aca3effd30752fd56f73e1", + "sha256:108e5f1c1cd70ffce0b68739c75734437c919d2eaec8e85bffc2c8b4d2794305", + "sha256:19764605feea0df966445d46533729b645033f134baeb3ea26ad518c9fdf212c", + "sha256:1be97461c439a6af4fe1cf8bf6ca5936d3db252737d2f379cc6b2e394e12a458", + "sha256:25cd61e846aaab76d5791d006497134602a9e451e954833018161befc3b5b9ed", + "sha256:2a47bcc478741b71273b917232f521fd5704ab4b25d301669879e7273d3586cc", + "sha256:59af01efb011b0e8b686ba7758d59cf4a8263f9ad35911bfe3f416cee4f5c08c", + "sha256:5dcac11031a71348faaed1f403a0debd56bf5404232284cf8c761ff918886ebc", + "sha256:62a5ec91388984909bb5398ea49ee61b68ecb579123694bffa172c3b0a107079", + "sha256:645bd4ca6f543685d643dadf6a856cc382b654cc923460e3a10a49c1b3832aeb", + "sha256:653b29b0819605fe0898829c8ad6400a6ccde096146730c2da54eede9b7b8baa", + "sha256:69138068268127cd605e03438312d8f271135a33140e2742b417d027a0539427", + "sha256:6e186342cfcc3aafaad565cbd496060e5a614b441cacc3995ef0091115c1f6c5", + "sha256:76bd15bb65c14900d98835fcd10f59e5e0435077431d3a394b60b15864fddd64", + "sha256:7805830e0c56d88f4d491fa5ac640dfc894c5ec570d1ece6ed1546e9df2e98d6", + "sha256:7a710b79baddd65b806402e14766c721aee8fb83381769c27920f26476276c1e", + "sha256:7a7a8f33a1f1fb762ede6cc9cbab8f2a9ba13b196bfaf7bc6f0b39d2ba315a43", + "sha256:82ee7696ed8eb9a82c7037f32ba9b7c59e51dda6f105b39f043b6ef293989cb3", + "sha256:88afd7a3af7ddddd42c2deda43d53d3dfc016c11327d0915f90ca34ebda91499", + "sha256:8af1a451ff9e123d0d8bd5d5e60f8e3315c3a64f3cdd6bc853e26090e195cdc8", + "sha256:8ee606964553c1a0bc74057dd8782a37d1c2bc0f01b83193b6f8bb14523b877b", + "sha256:91852d4480a4537d169c29a9d104dda44094c78f1f5b67bca76c29a91042b623", + "sha256:9c682436c359b5ada67e882fec34689726a09c461efd75b6ea77b2403d5665b7", + "sha256:bc3ee1b4d97081260d92ae813a83de4d2653206967c4a0a017580f8b9548ddbc", + "sha256:bca649483d5ed251d06daf25957f802e44e6bb6df2e8f218ae71968ff8f8edc4", + "sha256:c39778fd0548d78917b61f03c1fa8bfda6cfcf98c767decf360945fe6f97461e", + "sha256:cbe71b6712429650e3883dc81286edb94c328ffcd24849accac0a4dbcc76958a", + "sha256:d00fe8596e1cc46b44bf3907354e9377aa030ec4cd04afbbf6e899fc1e2a7781", + "sha256:d3584623e68a5064a04748fb6d76117a21a7cb5eaba20608a41c7d0c61721794", + "sha256:e48217c7901edd95f9f097feaa0388da215ed14ce2ece803d3f300b4e694abea", + "sha256:f2e497413560e03421484189a6b65e33fe800d3bd75590e6d78d4dfdb7accf3b", + "sha256:ff5c9a67f8a4fba4aed887216e32cbc48f2a6fb2673bb10a99e43be463e15913" ], "index": "pypi", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.19.0" + "version": "==3.20.0" }, "pydantic": { "hashes": [ - "sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0", - "sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd" + "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f", + "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9" ], - "markers": "python_version >= '3.7'", - "version": "==2.5.2" + "markers": "python_version >= '3.8'", + "version": "==2.6.1" }, "pydantic-core": { "hashes": [ - "sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b", - "sha256:074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b", - "sha256:079206491c435b60778cf2b0ee5fd645e61ffd6e70c47806c9ed51fc75af078d", - "sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8", - "sha256:0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124", - "sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189", - "sha256:16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c", - "sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d", - "sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f", - "sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520", - "sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4", - "sha256:3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6", - "sha256:3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955", - "sha256:34708cc82c330e303f4ce87758828ef6e457681b58ce0e921b6e97937dd1e2a3", - "sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b", - "sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a", - "sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68", - "sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3", - "sha256:45e95333b8418ded64745f14574aa9bfc212cb4fbeed7a687b0c6e53b5e188cd", - "sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de", - "sha256:49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b", - "sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634", - "sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7", - "sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459", - "sha256:4e47a76848f92529879ecfc417ff88a2806438f57be4a6a8bf2961e8f9ca9ec7", - "sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3", - "sha256:531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331", - "sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf", - "sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d", - "sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36", - "sha256:5c7d5b5005f177764e96bd584d7bf28d6e26e96f2a541fdddb934c486e36fd59", - "sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937", - "sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc", - "sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093", - "sha256:652c1988019752138b974c28f43751528116bcceadad85f33a258869e641d753", - "sha256:6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706", - "sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca", - "sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260", - "sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997", - "sha256:6c327e9cd849b564b234da821236e6bcbe4f359a42ee05050dc79d8ed2a91588", - "sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71", - "sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb", - "sha256:6e4d090e73e0725b2904fdbdd8d73b8802ddd691ef9254577b708d413bf3006e", - "sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69", - "sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5", - "sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07", - "sha256:77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1", - "sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0", - "sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd", - "sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8", - "sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944", - "sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26", - "sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda", - "sha256:88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4", - "sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9", - "sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00", - "sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe", - "sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6", - "sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada", - "sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4", - "sha256:a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7", - "sha256:a717aef6971208f0851a2420b075338e33083111d92041157bbe0e2713b37325", - "sha256:a71891847f0a73b1b9eb86d089baee301477abef45f7eaf303495cd1473613e4", - "sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b", - "sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88", - "sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04", - "sha256:abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863", - "sha256:af36f36538418f3806048f3b242a1777e2540ff9efaa667c27da63d2749dbce0", - "sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911", - "sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b", - "sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e", - "sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144", - "sha256:ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5", - "sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720", - "sha256:c0b97ec434041827935044bbbe52b03d6018c2897349670ff8fe11ed24d1d4ab", - "sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d", - "sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789", - "sha256:c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec", - "sha256:c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2", - "sha256:c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db", - "sha256:c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f", - "sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef", - "sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3", - "sha256:ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209", - "sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc", - "sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651", - "sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8", - "sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e", - "sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66", - "sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7", - "sha256:d81e6987b27bc7d101c8597e1cd2bcaa2fee5e8e0f356735c7ed34368c471550", - "sha256:dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd", - "sha256:de790a3b5aa2124b8b78ae5faa033937a72da8efe74b9231698b5a1dd9be3405", - "sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27", - "sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093", - "sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077", - "sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113", - "sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3", - "sha256:ec1e72d6412f7126eb7b2e3bfca42b15e6e389e1bc88ea0069d0cc1742f477c6", - "sha256:ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf", - "sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed", - "sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88", - "sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe", - "sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18", - "sha256:fe0a5a1025eb797752136ac8b4fa21aa891e3d74fd340f864ff982d649691867" + "sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379", + "sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06", + "sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05", + "sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7", + "sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753", + "sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a", + "sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731", + "sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc", + "sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380", + "sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3", + "sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c", + "sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11", + "sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990", + "sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a", + "sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2", + "sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8", + "sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97", + "sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a", + "sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8", + "sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef", + "sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77", + "sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33", + "sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82", + "sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5", + "sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b", + "sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55", + "sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e", + "sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b", + "sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7", + "sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec", + "sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc", + "sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469", + "sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b", + "sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20", + "sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e", + "sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d", + "sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f", + "sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b", + "sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039", + "sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e", + "sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2", + "sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f", + "sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b", + "sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc", + "sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8", + "sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522", + "sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e", + "sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784", + "sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a", + "sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890", + "sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485", + "sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545", + "sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f", + "sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943", + "sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878", + "sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f", + "sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17", + "sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7", + "sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286", + "sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c", + "sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb", + "sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646", + "sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978", + "sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8", + "sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15", + "sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272", + "sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2", + "sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55", + "sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf", + "sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545", + "sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4", + "sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a", + "sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804", + "sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4", + "sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0", + "sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a", + "sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113", + "sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d", + "sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25" ], - "markers": "python_version >= '3.7'", - "version": "==2.14.5" + "markers": "python_version >= '3.8'", + "version": "==2.16.2" }, "pydantic-settings": { "hashes": [ @@ -1019,11 +990,11 @@ }, "python-dotenv": { "hashes": [ - "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba", - "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a" + "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", + "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a" ], "markers": "python_version >= '3.8'", - "version": "==1.0.0" + "version": "==1.0.1" }, "requests": { "hashes": [ @@ -1036,19 +1007,19 @@ }, "s3transfer": { "hashes": [ - "sha256:368ac6876a9e9ed91f6bc86581e319be08188dc60d50e0d56308ed5765446283", - "sha256:c9e56cbe88b28d8e197cf841f1f0c130f246595e77ae5b5a05b69fe7cb83de76" + "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e", + "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b" ], - "markers": "python_version >= '3.7'", - "version": "==0.8.2" + "markers": "python_version >= '3.8'", + "version": "==0.10.0" }, "setuptools": { "hashes": [ - "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2", - "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6" + "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05", + "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78" ], "markers": "python_version >= '3.8'", - "version": "==69.0.2" + "version": "==69.0.3" }, "six": { "hashes": [ @@ -1068,12 +1039,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", - "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" + "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.8.0" + "version": "==4.9.0" }, "urllib3": { "hashes": [ @@ -1154,45 +1125,49 @@ }, "bandit": { "hashes": [ - "sha256:75665181dc1e0096369112541a056c59d1c5f66f9bb74a8d686c3c362b83f549", - "sha256:bdfc739baa03b880c2d15d0431b31c658ffc348e907fe197e54e0389dd59e11e" - ], - "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==1.7.5" - }, - "beautifulsoup4": { - "hashes": [ - "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da", - "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a" - ], - "markers": "python_full_version >= '3.6.0'", - "version": "==4.12.2" - }, - "black": { - "hashes": [ - "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4", - "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b", - "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f", - "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07", - "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187", - "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6", - "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05", - "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06", - "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e", - "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5", - "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244", - "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f", - "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221", - "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055", - "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479", - "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394", - "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911", - "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142" + "sha256:17e60786a7ea3c9ec84569fd5aee09936d116cb0cb43151023258340dbffb7ed", + "sha256:527906bec6088cb499aae31bc962864b4e77569e9d529ee51df3a93b4b8ab28a" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==23.11.0" + "version": "==1.7.7" + }, + "beautifulsoup4": { + "hashes": [ + "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", + "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed" + ], + "markers": "python_full_version >= '3.6.0'", + "version": "==4.12.3" + }, + "black": { + "hashes": [ + "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8", + "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6", + "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62", + "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445", + "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c", + "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a", + "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9", + "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2", + "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6", + "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b", + "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4", + "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168", + "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d", + "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5", + "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024", + "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e", + "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b", + "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161", + "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717", + "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8", + "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac", + "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==24.1.1" }, "blinker": { "hashes": [ @@ -1204,12 +1179,12 @@ }, "boto3": { "hashes": [ - "sha256:d12467fb3a64d359b0bda0570a8163a5859fcac13e786f2a3db0392523178556", - "sha256:eed0f7df91066b6ac63a53d16459ac082458d57061bedf766135d9e1c2b75a6b" + "sha256:65acfe7f1cf2a9b7df3d4edb87c8022e02685825bd1957e7bb678cc0d09f5e5f", + "sha256:73f5ec89cb3ddb3ed577317889fd2f2df783f66b6502a9a4239979607e33bf74" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==1.33.7" + "markers": "python_version >= '3.8'", + "version": "==1.34.37" }, "boto3-mocking": { "hashes": [ @@ -1222,28 +1197,28 @@ }, "boto3-stubs": { "hashes": [ - "sha256:0461f6fec92d96aa2ea3a207329bd020a62a7aaa86f284e5cf054d9b0c7f03c2", - "sha256:449b91060cd953e08980d76a3b67d7eb4246e663b37ecba4ec625b54619e1c22" + "sha256:97b5ca3d3145385acde5af46ca2da3fc74f433545034c36183f389e99771516e", + "sha256:c6618c7126bac0337c05e161e9c428febc57d6a24d7ff62de46e67761f402c57" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==1.33.7" + "markers": "python_version >= '3.8'", + "version": "==1.34.37" }, "botocore": { "hashes": [ - "sha256:71ec0e85b996cf9def3dd8f4ca6cb4a9fd3a614aa4c9c7cbf33f2f68e1d0649a", - "sha256:b2299bc13bb8c0928edc98bf4594deb14cba2357536120f63772027a16ce7374" + "sha256:2a5bf33aacd2d970afd3d492e179e06ea98a5469030d5cfe7a2ad9995f7bb2ef", + "sha256:3c46ddb1679e6ef45ca78b48665398636bda532a07cd476e4b500697d13d9a99" ], - "markers": "python_version >= '3.7'", - "version": "==1.33.7" + "markers": "python_version >= '3.8'", + "version": "==1.34.37" }, "botocore-stubs": { "hashes": [ - "sha256:ca5de1ad4dc384f919387bb96eececb70900fda9219acfcf4b473b35a4834ec9", - "sha256:f73e4728a4a391f0407cd9403f6935343aac5687fb1e0eab7c3351d3419e853b" + "sha256:087cd42973edcb5527dc97eec87fa29fffecc39691249486e02045677d4a2dbe", + "sha256:d6bcea8a6872aa46d389027dc5c022241fd0a2047a8b858aa5005e6151ed30a7" ], - "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==1.33.7" + "markers": "python_version >= '3.8' and python_version < '4.0'", + "version": "==1.34.37" }, "click": { "hashes": [ @@ -1255,21 +1230,21 @@ }, "django": { "hashes": [ - "sha256:8e0f1c2c2786b5c0e39fe1afce24c926040fad47c8ea8ad30aaf1188df29fc41", - "sha256:e1d37c51ad26186de355cbcec16613ebdabfa9689bbade9c538835205a8abbe9" + "sha256:45a747e1c5b3d6df1b141b1481e193b033fd1fdbda3ff52677dc81afdaacbaed", + "sha256:f7c7852a5ac5a3da5a8d5b35cc6168f31b605971441798dac845f17ca8028039" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.2.7" + "version": "==4.2.3" }, "django-debug-toolbar": { "hashes": [ - "sha256:af99128c06e8e794479e65ab62cc6c7d1e74e1c19beb44dcbf9bad7a9c017327", - "sha256:bc7fdaafafcdedefcc67a4a5ad9dac96efd6e41db15bc74d402a54a2ba4854dc" + "sha256:0b0dddee5ea29b9cb678593bc0d7a6d76b21d7799cb68e091a2148341a80f3c4", + "sha256:e09b7dcb8417b743234dfc57c95a7c1d1d87a88844abd13b4c5387f807b31bf6" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.2.0" + "version": "==4.3.0" }, "django-model2puml": { "hashes": [ @@ -1280,20 +1255,20 @@ }, "django-stubs": { "hashes": [ - "sha256:2fcd257884a68dfa02de41ee5410ec805264d9b07d9b5b119e4dea82c7b8345e", - "sha256:e60b43de662a199db4b15c803c06669e0ac5035614af291cbd3b91591f7dcc94" + "sha256:4cf4de258fa71adc6f2799e983091b9d46cfc67c6eebc68fe111218c9a62b3b8", + "sha256:8ccd2ff4ee5adf22b9e3b7b1a516d2e1c2191e9d94e672c35cc2bc3dd61e0f6b" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.2.6" + "version": "==4.2.7" }, "django-stubs-ext": { "hashes": [ - "sha256:8c4d1fb5f68419b3b2474c659681a189803e27d6a5e5abf5aa0da57601b58633", - "sha256:921cd7ae4614e74c234bc0fe86ee75537d163addfe1fc6f134bf03e29d86c01e" + "sha256:45a5d102417a412e3606e3c358adb4744988a92b7b58ccf3fd64bddd5d04d14c", + "sha256:519342ac0849cda1559746c9a563f03ff99f636b0ebe7c14b75e816a00dfddc3" ], "markers": "python_version >= '3.8'", - "version": "==4.2.5" + "version": "==4.2.7" }, "django-webtest": { "hashes": [ @@ -1305,28 +1280,12 @@ }, "flake8": { "hashes": [ - "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23", - "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5" + "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132", + "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3" ], "index": "pypi", "markers": "python_full_version >= '3.8.1'", - "version": "==6.1.0" - }, - "gitdb": { - "hashes": [ - "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4", - "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b" - ], - "markers": "python_version >= '3.7'", - "version": "==4.0.11" - }, - "gitpython": { - "hashes": [ - "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4", - "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a" - ], - "markers": "python_version >= '3.7'", - "version": "==3.1.40" + "version": "==7.0.0" }, "jmespath": { "hashes": [ @@ -1362,37 +1321,37 @@ }, "mypy": { "hashes": [ - "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340", - "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49", - "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82", - "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce", - "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb", - "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51", - "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5", - "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e", - "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7", - "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33", - "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9", - "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1", - "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6", - "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a", - "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe", - "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7", - "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200", - "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7", - "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a", - "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28", - "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea", - "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120", - "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d", - "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42", - "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea", - "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2", - "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a" + "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6", + "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d", + "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02", + "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d", + "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3", + "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3", + "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3", + "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66", + "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259", + "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835", + "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd", + "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d", + "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8", + "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07", + "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b", + "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e", + "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6", + "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae", + "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9", + "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d", + "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a", + "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592", + "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218", + "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817", + "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4", + "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410", + "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.7.1" + "version": "==1.8.0" }, "mypy-extensions": { "hashes": [ @@ -1420,11 +1379,11 @@ }, "pathspec": { "hashes": [ - "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20", - "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3" + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" ], - "markers": "python_version >= '3.7'", - "version": "==0.11.2" + "markers": "python_version >= '3.8'", + "version": "==0.12.1" }, "pbr": { "hashes": [ @@ -1436,11 +1395,11 @@ }, "platformdirs": { "hashes": [ - "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", - "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" + "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", + "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" ], "markers": "python_version >= '3.8'", - "version": "==4.1.0" + "version": "==4.2.0" }, "pycodestyle": { "hashes": [ @@ -1452,11 +1411,11 @@ }, "pyflakes": { "hashes": [ - "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774", - "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc" + "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", + "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a" ], "markers": "python_version >= '3.8'", - "version": "==3.1.0" + "version": "==3.2.0" }, "pygments": { "hashes": [ @@ -1505,6 +1464,7 @@ "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", @@ -1540,11 +1500,11 @@ }, "s3transfer": { "hashes": [ - "sha256:368ac6876a9e9ed91f6bc86581e319be08188dc60d50e0d56308ed5765446283", - "sha256:c9e56cbe88b28d8e197cf841f1f0c130f246595e77ae5b5a05b69fe7cb83de76" + "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e", + "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b" ], - "markers": "python_version >= '3.7'", - "version": "==0.8.2" + "markers": "python_version >= '3.8'", + "version": "==0.10.0" }, "six": { "hashes": [ @@ -1554,14 +1514,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, - "smmap": { - "hashes": [ - "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62", - "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da" - ], - "markers": "python_version >= '3.7'", - "version": "==5.0.1" - }, "soupsieve": { "hashes": [ "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690", @@ -1596,11 +1548,11 @@ }, "types-awscrt": { "hashes": [ - "sha256:850d5ad95d8f337b15fb154790f39af077faf5c08d43758fd750f379a87d5f73", - "sha256:a577c4d60a7fb7e21b436a73207a66f6ba50329d578b347934c5d99d4d612901" + "sha256:06a859189a329ca8e66d56ceeef2391488e39b878fbd2141f115eab4d416fe22", + "sha256:f61a120d3e98ee1387bc5ca4b93437f258cc5c2af1f55f8634ec4cee5729f178" ], "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==0.19.19" + "version": "==0.20.3" }, "types-cachetools": { "hashes": [ @@ -1613,10 +1565,11 @@ }, "types-pytz": { "hashes": [ - "sha256:1999a123a3dc0e39a2ef6d19f3f8584211de9e6a77fe7a0259f04a524e90a5cf", - "sha256:cc23d0192cd49c8f6bba44ee0c81e4586a8f30204970fc0894d209a6b08dab9a" + "sha256:9679eef0365db3af91ef7722c199dbb75ee5c1b67e3c4dd7bfbeb1b8a71c21a3", + "sha256:c93751ee20dfc6e054a0148f8f5227b9a00b79c90a4d3c9f464711a73179c89e" ], - "version": "==2023.3.1.1" + "markers": "python_version >= '3.8'", + "version": "==2024.1.0.20240203" }, "types-pyyaml": { "hashes": [ @@ -1627,29 +1580,29 @@ }, "types-requests": { "hashes": [ - "sha256:b32b9a86beffa876c0c3ac99a4cd3b8b51e973fb8e3bd4e0a6bb32c7efad80fc", - "sha256:dc5852a76f1eaf60eafa81a2e50aefa3d1f015c34cf0cba130930866b1b22a92" - ], - "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==2.31.0.10" - }, - "types-s3transfer": { - "hashes": [ - "sha256:2e41756fcf94775a9949afa856489ac4570308609b0493dfbd7b4d333eb423e6", - "sha256:5e084ebcf2704281c71b19d5da6e1544b50859367d034b50080d5316a76a9418" - ], - "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==0.8.2" - }, - "typing-extensions": { - "hashes": [ - "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", - "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" + "sha256:03a28ce1d7cd54199148e043b2079cdded22d6795d19a2c2a6791a4b2b5e2eb5", + "sha256:9592a9a4cb92d6d75d9b491a41477272b710e021011a2a3061157e2fb1f1a5d1" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.8.0" + "version": "==2.31.0.20240125" + }, + "types-s3transfer": { + "hashes": [ + "sha256:35e4998c25df7f8985ad69dedc8e4860e8af3b43b7615e940d53c00d413bdc69", + "sha256:44fcdf0097b924a9aab1ee4baa1179081a9559ca62a88c807e2b256893ce688f" + ], + "markers": "python_version >= '3.7' and python_version < '4.0'", + "version": "==0.10.0" + }, + "typing-extensions": { + "hashes": [ + "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==4.9.0" }, "urllib3": { "hashes": [ @@ -1661,11 +1614,11 @@ }, "waitress": { "hashes": [ - "sha256:7500c9625927c8ec60f54377d590f67b30c8e70ef4b8894214ac6e4cad233d2a", - "sha256:780a4082c5fbc0fde6a2fcfe5e26e6efc1e8f425730863c04085769781f51eba" + "sha256:005da479b04134cdd9dd602d1ee7c49d79de0537610d653674cc6cbde222b8a1", + "sha256:2a06f242f4ba0cc563444ca3d1998959447477363a2d7e9b8b4d75d35cfd1669" ], - "markers": "python_full_version >= '3.7.0'", - "version": "==2.1.2" + "markers": "python_full_version >= '3.8.0'", + "version": "==3.0.0" }, "webob": { "hashes": [ diff --git a/src/requirements.txt b/src/requirements.txt index b203f87bb..a748e4b96 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,18 +1,18 @@ -i https://pypi.python.org/simple annotated-types==0.6.0; python_version >= '3.8' asgiref==3.7.2; python_version >= '3.7' -boto3==1.33.7; python_version >= '3.7' -botocore==1.33.7; python_version >= '3.7' +boto3==1.34.37; python_version >= '3.8' +botocore==1.34.37; python_version >= '3.8' cachetools==5.3.2; python_version >= '3.7' -certifi==2023.11.17; python_version >= '3.6' +certifi==2024.2.2; python_version >= '3.6' cfenv==0.5.3 -cffi==1.16.0; python_version >= '3.8' +cffi==1.16.0; platform_python_implementation != 'PyPy' charset-normalizer==3.3.2; python_full_version >= '3.7.0' -cryptography==41.0.7; python_version >= '3.7' +cryptography==42.0.2; python_version >= '3.7' defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' dj-database-url==2.1.0 dj-email-url==1.0.6 -django==4.2.7; python_version >= '3.8' +django==4.2.3; python_version >= '3.8' django-allow-cidr==0.7.1 django-auditlog==2.3.0; python_version >= '3.7' django-cache-url==3.4.5 @@ -20,42 +20,42 @@ django-cors-headers==4.3.1; python_version >= '3.8' django-csp==3.7 django-fsm==2.8.1 django-login-required-middleware==0.9.0 -django-phonenumber-field[phonenumberslite]==7.2.0; python_version >= '3.8' +django-phonenumber-field[phonenumberslite]==7.3.0; python_version >= '3.8' django-widget-tweaks==1.5.0; python_version >= '3.8' -environs[django]==9.5.0; python_version >= '3.6' -faker==20.1.0; python_version >= '3.8' +environs[django]==10.3.0; python_version >= '3.8' +faker==23.1.0; python_version >= '3.8' fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c furl==2.1.3 future==0.18.3; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' gevent==23.9.1; python_version >= '3.8' -geventconnpool@ git+https://github.com/rasky/geventconnpool.git@1bbb93a714a331a069adf27265fe582d9ba7ecd4 -greenlet==3.0.1; python_version >= '3.7' +geventconnpool@ git+https://github.com/rasky/geventconnpool.git +greenlet==3.0.3; python_version >= '3.7' gunicorn==21.2.0; python_version >= '3.5' idna==3.6; python_version >= '3.5' jmespath==1.0.1; python_version >= '3.7' -lxml==4.9.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' -mako==1.3.0; python_version >= '3.8' -markupsafe==2.1.3; python_version >= '3.7' -marshmallow==3.20.1; python_version >= '3.8' +lxml==5.1.0; python_version >= '3.6' +mako==1.3.2; python_version >= '3.8' +markupsafe==2.1.5; python_version >= '3.7' +marshmallow==3.20.2; python_version >= '3.8' oic==1.6.1; python_version ~= '3.7' orderedmultidict==1.0.1 packaging==23.2; python_version >= '3.7' -phonenumberslite==8.13.26 +phonenumberslite==8.13.29 psycopg2-binary==2.9.9; python_version >= '3.7' pycparser==2.21 -pycryptodomex==3.19.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' -pydantic==2.5.2; python_version >= '3.7' -pydantic-core==2.14.5; python_version >= '3.7' +pycryptodomex==3.20.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' +pydantic==2.6.1; python_version >= '3.8' +pydantic-core==2.16.2; python_version >= '3.8' pydantic-settings==2.1.0; python_version >= '3.8' pyjwkest==1.4.2 python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -python-dotenv==1.0.0; python_version >= '3.8' +python-dotenv==1.0.1; python_version >= '3.8' requests==2.31.0; python_version >= '3.7' -s3transfer==0.8.2; python_version >= '3.7' -setuptools==69.0.2; python_version >= '3.8' +s3transfer==0.10.0; python_version >= '3.8' +setuptools==69.0.3; python_version >= '3.8' six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' sqlparse==0.4.4; python_version >= '3.5' -typing-extensions==4.8.0; python_version >= '3.8' +typing-extensions==4.9.0; python_version >= '3.8' urllib3==2.0.7; python_version >= '3.7' whitenoise==6.6.0; python_version >= '3.8' zope.event==5.0; python_version >= '3.7' From 9613f14c0144e3742073db7e9f8738095ca16f94 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 7 Feb 2024 15:46:33 -0700 Subject: [PATCH 084/119] revert geventconnpool --- src/Pipfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 3ac2fad0e..528408952 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -458,7 +458,7 @@ }, "geventconnpool": { "git": "https://github.com/rasky/geventconnpool.git", - "ref": null + "ref": "1bbb93a714a331a069adf27265fe582d9ba7ecd4" }, "greenlet": { "hashes": [ From 497d11c67cb98f4db93666173a204f0273542616 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 7 Feb 2024 16:43:16 -0700 Subject: [PATCH 085/119] revert geventconnpool in requirements.txt --- src/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/requirements.txt b/src/requirements.txt index a748e4b96..7c79cf8a3 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -28,7 +28,7 @@ fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a furl==2.1.3 future==0.18.3; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' gevent==23.9.1; python_version >= '3.8' -geventconnpool@ git+https://github.com/rasky/geventconnpool.git +geventconnpool@ git+https://github.com/rasky/geventconnpool.git@1bbb93a714a331a069adf27265fe582d9ba7ecd4 greenlet==3.0.3; python_version >= '3.7' gunicorn==21.2.0; python_version >= '3.5' idna==3.6; python_version >= '3.5' From 39dfb41c2f2db16d7559746226c17911a9ca6ade Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Wed, 7 Feb 2024 16:15:44 -0800 Subject: [PATCH 086/119] ran lint --- src/api/tests/test_available.py | 2 -- src/api/views.py | 1 + src/registrar/admin.py | 1 - src/registrar/apps.py | 1 - src/registrar/config/settings.py | 1 + src/registrar/fixtures_applications.py | 1 - src/registrar/management/commands/cat_files_into_getgov.py | 1 + .../management/commands/generate_current_federal_report.py | 1 + .../management/commands/generate_current_full_report.py | 1 + .../management/commands/patch_federal_agency_info.py | 1 + .../management/commands/utility/epp_data_containers.py | 1 + .../commands/utility/extra_transition_domain_helper.py | 1 + src/registrar/models/contact.py | 1 - src/registrar/models/domain_application.py | 3 --- src/registrar/models/domain_information.py | 1 - src/registrar/models/user_domain_role.py | 2 -- src/registrar/models/verified_by_staff.py | 1 - src/registrar/models/website.py | 1 - src/registrar/no_cache_middleware.py | 1 - src/registrar/templatetags/field_helpers.py | 1 + src/registrar/tests/test_models.py | 2 -- src/registrar/tests/test_models_domain.py | 1 + src/registrar/tests/test_views_application.py | 1 - src/registrar/tests/test_views_domain.py | 1 - src/registrar/utility/email.py | 1 - src/registrar/views/domain.py | 1 - src/registrar/views/utility/mixins.py | 7 ------- src/registrar/views/utility/permission_views.py | 7 ------- 28 files changed, 10 insertions(+), 35 deletions(-) diff --git a/src/api/tests/test_available.py b/src/api/tests/test_available.py index fa9dadcd4..b85ea6335 100644 --- a/src/api/tests/test_available.py +++ b/src/api/tests/test_available.py @@ -19,7 +19,6 @@ API_BASE_PATH = "/api/v1/available/?domain=" class AvailableViewTest(MockEppLib): - """Test that the view function works as expected.""" def setUp(self): @@ -123,7 +122,6 @@ class AvailableViewTest(MockEppLib): class AvailableAPITest(MockEppLib): - """Test that the API can be called as expected.""" def setUp(self): diff --git a/src/api/views.py b/src/api/views.py index f9fa2d1ea..2199e15ac 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -1,4 +1,5 @@ """Internal API views""" + from django.apps import apps from django.views.decorators.http import require_http_methods from django.http import HttpResponse diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 4034bf35b..c5f5be276 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -739,7 +739,6 @@ class DomainApplicationAdminForm(forms.ModelForm): class DomainApplicationAdmin(ListHeaderAdmin): - """Custom domain applications admin class.""" class InvestigatorFilter(admin.SimpleListFilter): diff --git a/src/registrar/apps.py b/src/registrar/apps.py index 9f1b186ad..fcb5c17fd 100644 --- a/src/registrar/apps.py +++ b/src/registrar/apps.py @@ -2,7 +2,6 @@ from django.apps import AppConfig class RegistrarConfig(AppConfig): - """Configure signal handling for our registrar Django application.""" name = "registrar" diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 372434887..009baa1c6 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -16,6 +16,7 @@ $ docker-compose exec app python manage.py shell ``` """ + import environs from base64 import b64decode from cfenv import AppEnv # type: ignore diff --git a/src/registrar/fixtures_applications.py b/src/registrar/fixtures_applications.py index 659a3040e..3e4e0e362 100644 --- a/src/registrar/fixtures_applications.py +++ b/src/registrar/fixtures_applications.py @@ -201,7 +201,6 @@ class DomainApplicationFixture: class DomainFixture(DomainApplicationFixture): - """Create one domain and permissions on it for each user.""" @classmethod diff --git a/src/registrar/management/commands/cat_files_into_getgov.py b/src/registrar/management/commands/cat_files_into_getgov.py index 4ccb1301b..4fb7ad5b8 100644 --- a/src/registrar/management/commands/cat_files_into_getgov.py +++ b/src/registrar/management/commands/cat_files_into_getgov.py @@ -1,4 +1,5 @@ """Loads files from /tmp into our sandboxes""" + import glob import logging diff --git a/src/registrar/management/commands/generate_current_federal_report.py b/src/registrar/management/commands/generate_current_federal_report.py index 1a123bf5b..6516bf99b 100644 --- a/src/registrar/management/commands/generate_current_federal_report.py +++ b/src/registrar/management/commands/generate_current_federal_report.py @@ -1,4 +1,5 @@ """Generates current-full.csv and current-federal.csv then uploads them to the desired URL.""" + import logging import os diff --git a/src/registrar/management/commands/generate_current_full_report.py b/src/registrar/management/commands/generate_current_full_report.py index 80c031605..be810ee10 100644 --- a/src/registrar/management/commands/generate_current_full_report.py +++ b/src/registrar/management/commands/generate_current_full_report.py @@ -1,4 +1,5 @@ """Generates current-full.csv and current-federal.csv then uploads them to the desired URL.""" + import logging import os diff --git a/src/registrar/management/commands/patch_federal_agency_info.py b/src/registrar/management/commands/patch_federal_agency_info.py index 35642c1bf..b286f1516 100644 --- a/src/registrar/management/commands/patch_federal_agency_info.py +++ b/src/registrar/management/commands/patch_federal_agency_info.py @@ -1,4 +1,5 @@ """Loops through each valid DomainInformation object and updates its agency value""" + import argparse import csv import logging diff --git a/src/registrar/management/commands/utility/epp_data_containers.py b/src/registrar/management/commands/utility/epp_data_containers.py index 1f370dca7..9e5769751 100644 --- a/src/registrar/management/commands/utility/epp_data_containers.py +++ b/src/registrar/management/commands/utility/epp_data_containers.py @@ -5,6 +5,7 @@ Regarding our dataclasses: Not intended to be used as models but rather as an alternative to storing as a dictionary. By keeping it as a dataclass instead of a dictionary, we can maintain data consistency. """ # noqa + from dataclasses import dataclass, field from datetime import date from enum import Enum diff --git a/src/registrar/management/commands/utility/extra_transition_domain_helper.py b/src/registrar/management/commands/utility/extra_transition_domain_helper.py index c082552eb..5c3573fb1 100644 --- a/src/registrar/management/commands/utility/extra_transition_domain_helper.py +++ b/src/registrar/management/commands/utility/extra_transition_domain_helper.py @@ -1,4 +1,5 @@ """""" + import csv from dataclasses import dataclass from datetime import datetime diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index ff7389780..d316cde4c 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -6,7 +6,6 @@ from .utility.time_stamped_model import TimeStampedModel class Contact(TimeStampedModel): - """Contact information follows a similar pattern for each contact.""" user = models.OneToOneField( diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index c37fc19b5..307115112 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -17,7 +17,6 @@ logger = logging.getLogger(__name__) class DomainApplication(TimeStampedModel): - """A registrant's application for a new domain.""" # Constants for choice fields @@ -97,7 +96,6 @@ class DomainApplication(TimeStampedModel): ARMED_FORCES_AP = "AP", "Armed Forces Pacific (AP)" class OrganizationChoices(models.TextChoices): - """ Primary organization choices: For use in django admin @@ -114,7 +112,6 @@ class DomainApplication(TimeStampedModel): SCHOOL_DISTRICT = "school_district", "School district" class OrganizationChoicesVerbose(models.TextChoices): - """ Secondary organization choices For use in the application form and on the templates diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 65d099e5a..acaa330bb 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -14,7 +14,6 @@ logger = logging.getLogger(__name__) class DomainInformation(TimeStampedModel): - """A registrant's domain information for that domain, exported from DomainApplication. We use these field from DomainApplication with few exceptions which are 'removed' via pop at the bottom of this file. Most of design for domain diff --git a/src/registrar/models/user_domain_role.py b/src/registrar/models/user_domain_role.py index 479f75089..6e915e4af 100644 --- a/src/registrar/models/user_domain_role.py +++ b/src/registrar/models/user_domain_role.py @@ -4,11 +4,9 @@ from .utility.time_stamped_model import TimeStampedModel class UserDomainRole(TimeStampedModel): - """This is a linking table that connects a user with a role on a domain.""" class Roles(models.TextChoices): - """The possible roles are listed here. Implementation of the named roles for allowing particular operations happens diff --git a/src/registrar/models/verified_by_staff.py b/src/registrar/models/verified_by_staff.py index 4c9e76e9d..a6d861504 100644 --- a/src/registrar/models/verified_by_staff.py +++ b/src/registrar/models/verified_by_staff.py @@ -4,7 +4,6 @@ from .utility.time_stamped_model import TimeStampedModel class VerifiedByStaff(TimeStampedModel): - """emails that get added to this table will bypass ial2 on login.""" email = models.EmailField( diff --git a/src/registrar/models/website.py b/src/registrar/models/website.py index d21564531..0b337e397 100644 --- a/src/registrar/models/website.py +++ b/src/registrar/models/website.py @@ -4,7 +4,6 @@ from .utility.time_stamped_model import TimeStampedModel class Website(TimeStampedModel): - """Keep domain names in their own table so that applications can refer to many of them.""" diff --git a/src/registrar/no_cache_middleware.py b/src/registrar/no_cache_middleware.py index 6f509b9d6..5edfca20e 100644 --- a/src/registrar/no_cache_middleware.py +++ b/src/registrar/no_cache_middleware.py @@ -6,7 +6,6 @@ better caching responses. class NoCacheMiddleware: - """Middleware to add a single header to every response.""" def __init__(self, get_response): diff --git a/src/registrar/templatetags/field_helpers.py b/src/registrar/templatetags/field_helpers.py index bc296753e..811897908 100644 --- a/src/registrar/templatetags/field_helpers.py +++ b/src/registrar/templatetags/field_helpers.py @@ -1,4 +1,5 @@ """Custom field helpers for our inputs.""" + import re from django import template diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index d7c8960f6..41fa75f1d 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -600,7 +600,6 @@ class TestPermissions(TestCase): class TestDomainInformation(TestCase): - """Test the DomainInformation model, when approved or otherwise""" def setUp(self): @@ -653,7 +652,6 @@ class TestDomainInformation(TestCase): class TestInvitations(TestCase): - """Test the retrieval of invitations.""" def setUp(self): diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index ca0a5e8d8..1c4d2521e 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -3,6 +3,7 @@ Feature being tested: Registry Integration This file tests the various ways in which the registrar interacts with the registry. """ + from django.test import TestCase from django.db.utils import IntegrityError from unittest.mock import MagicMock, patch, call diff --git a/src/registrar/tests/test_views_application.py b/src/registrar/tests/test_views_application.py index 02fe5ff76..2b08d8d74 100644 --- a/src/registrar/tests/test_views_application.py +++ b/src/registrar/tests/test_views_application.py @@ -28,7 +28,6 @@ logger = logging.getLogger(__name__) class DomainApplicationTests(TestWithUser, WebTest): - """Webtests for domain application to test filling and submitting.""" # Doesn't work with CSRF checking diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index c9422e700..2c8e796ac 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -1236,7 +1236,6 @@ class TestDomainSecurityEmail(TestDomainOverview): class TestDomainDNSSEC(TestDomainOverview): - """MockEPPLib is already inherited.""" def test_dnssec_page_refreshes_enable_button(self): diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index d56c02cbf..461637f23 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -10,7 +10,6 @@ logger = logging.getLogger(__name__) class EmailSendingError(RuntimeError): - """Local error for handling all failures when sending email.""" pass diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 04fe1ce3a..d5f8f67b4 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -135,7 +135,6 @@ class DomainFormBaseView(DomainBaseView, FormMixin): class DomainView(DomainBaseView): - """Domain detail overview page.""" template_name = "domain_detail.html" diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index b2c4cb364..8de75e151 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -146,7 +146,6 @@ class OrderableFieldsMixin: class PermissionsLoginMixin(PermissionRequiredMixin): - """Mixin that redirects to login page if not logged in, otherwise 403.""" def handle_no_permission(self): @@ -155,7 +154,6 @@ class PermissionsLoginMixin(PermissionRequiredMixin): class DomainPermission(PermissionsLoginMixin): - """Permission mixin that redirects to domain if user has access, otherwise 403""" @@ -264,7 +262,6 @@ class DomainPermission(PermissionsLoginMixin): class DomainApplicationPermission(PermissionsLoginMixin): - """Permission mixin that redirects to domain application if user has access, otherwise 403""" @@ -287,7 +284,6 @@ class DomainApplicationPermission(PermissionsLoginMixin): class UserDeleteDomainRolePermission(PermissionsLoginMixin): - """Permission mixin for UserDomainRole if user has access, otherwise 403""" @@ -324,7 +320,6 @@ class UserDeleteDomainRolePermission(PermissionsLoginMixin): class DomainApplicationPermissionWithdraw(PermissionsLoginMixin): - """Permission mixin that redirects to withdraw action on domain application if user has access, otherwise 403""" @@ -347,7 +342,6 @@ class DomainApplicationPermissionWithdraw(PermissionsLoginMixin): class ApplicationWizardPermission(PermissionsLoginMixin): - """Permission mixin that redirects to start or edit domain application if user has access, otherwise 403""" @@ -365,7 +359,6 @@ class ApplicationWizardPermission(PermissionsLoginMixin): class DomainInvitationPermission(PermissionsLoginMixin): - """Permission mixin that redirects to domain invitation if user has access, otherwise 403" diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index 54c96d602..02d3db96d 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -20,7 +20,6 @@ logger = logging.getLogger(__name__) class DomainPermissionView(DomainPermission, DetailView, abc.ABC): - """Abstract base view for domains that enforces permissions. This abstract view cannot be instantiated. Actual views must specify @@ -58,7 +57,6 @@ class DomainPermissionView(DomainPermission, DetailView, abc.ABC): class DomainApplicationPermissionView(DomainApplicationPermission, DetailView, abc.ABC): - """Abstract base view for domain applications that enforces permissions This abstract view cannot be instantiated. Actual views must specify @@ -78,7 +76,6 @@ class DomainApplicationPermissionView(DomainApplicationPermission, DetailView, a class DomainApplicationPermissionWithdrawView(DomainApplicationPermissionWithdraw, DetailView, abc.ABC): - """Abstract base view for domain application withdraw function This abstract view cannot be instantiated. Actual views must specify @@ -98,7 +95,6 @@ class DomainApplicationPermissionWithdrawView(DomainApplicationPermissionWithdra class ApplicationWizardPermissionView(ApplicationWizardPermission, TemplateView, abc.ABC): - """Abstract base view for the application form that enforces permissions This abstract view cannot be instantiated. Actual views must specify @@ -113,7 +109,6 @@ class ApplicationWizardPermissionView(ApplicationWizardPermission, TemplateView, class DomainInvitationPermissionDeleteView(DomainInvitationPermission, DeleteView, abc.ABC): - """Abstract view for deleting a domain invitation. This one is fairly specialized, but this is the only thing that we do @@ -127,7 +122,6 @@ class DomainInvitationPermissionDeleteView(DomainInvitationPermission, DeleteVie class DomainApplicationPermissionDeleteView(DomainApplicationPermission, DeleteView, abc.ABC): - """Abstract view for deleting a DomainApplication.""" model = DomainApplication @@ -135,7 +129,6 @@ class DomainApplicationPermissionDeleteView(DomainApplicationPermission, DeleteV class UserDomainRolePermissionDeleteView(UserDeleteDomainRolePermission, DeleteView, abc.ABC): - """Abstract base view for deleting a UserDomainRole. This abstract view cannot be instantiated. Actual views must specify From 1e5f97720817356e7d3b3def66efdbd97a60747c Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 7 Feb 2024 18:17:48 -0700 Subject: [PATCH 087/119] updated django to 4.2.10 & linted --- src/Pipfile | 2 +- src/Pipfile.lock | 14 +++++++------- src/api/tests/test_available.py | 2 -- src/api/views.py | 1 + src/registrar/admin.py | 1 - src/registrar/apps.py | 1 - src/registrar/config/settings.py | 1 + src/registrar/fixtures_applications.py | 1 - .../management/commands/cat_files_into_getgov.py | 1 + .../commands/generate_current_federal_report.py | 1 + .../commands/generate_current_full_report.py | 1 + .../commands/patch_federal_agency_info.py | 1 + .../commands/utility/epp_data_containers.py | 1 + .../utility/extra_transition_domain_helper.py | 1 + src/registrar/models/contact.py | 1 - src/registrar/models/domain_application.py | 3 --- src/registrar/models/domain_information.py | 1 - src/registrar/models/user_domain_role.py | 2 -- src/registrar/models/verified_by_staff.py | 1 - src/registrar/models/website.py | 1 - src/registrar/no_cache_middleware.py | 1 - src/registrar/templatetags/field_helpers.py | 1 + src/registrar/tests/test_models.py | 2 -- src/registrar/tests/test_models_domain.py | 1 + src/registrar/tests/test_views_application.py | 1 - src/registrar/tests/test_views_domain.py | 1 - src/registrar/utility/email.py | 1 - src/registrar/views/domain.py | 1 - src/registrar/views/utility/mixins.py | 7 ------- src/registrar/views/utility/permission_views.py | 7 ------- src/requirements.txt | 2 +- 31 files changed, 19 insertions(+), 44 deletions(-) diff --git a/src/Pipfile b/src/Pipfile index a0ada646a..51417d578 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -django = "4.2.3" +django = "4.2.10" cfenv = "*" django-cors-headers = "*" pycryptodomex = "*" diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 528408952..7d511a0e5 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8db8d1bdb9c343c50771fc8bd001c555037dc3606a48dcafb3800d743fb95f3e" + "sha256": "a672aeb8951fd850e90ad87c6f03cf71e2fc2b387d56fd3942361cb0b45bb449" }, "pipfile-spec": 6, "requires": {}, @@ -288,12 +288,12 @@ }, "django": { "hashes": [ - "sha256:45a747e1c5b3d6df1b141b1481e193b033fd1fdbda3ff52677dc81afdaacbaed", - "sha256:f7c7852a5ac5a3da5a8d5b35cc6168f31b605971441798dac845f17ca8028039" + "sha256:a2d4c4d4ea0b6f0895acde632071aff6400bfc331228fc978b05452a0ff3e9f1", + "sha256:b1260ed381b10a11753c73444408e19869f3241fc45c985cd55a30177c789d13" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.2.3" + "version": "==4.2.10" }, "django-allow-cidr": { "hashes": [ @@ -1230,12 +1230,12 @@ }, "django": { "hashes": [ - "sha256:45a747e1c5b3d6df1b141b1481e193b033fd1fdbda3ff52677dc81afdaacbaed", - "sha256:f7c7852a5ac5a3da5a8d5b35cc6168f31b605971441798dac845f17ca8028039" + "sha256:a2d4c4d4ea0b6f0895acde632071aff6400bfc331228fc978b05452a0ff3e9f1", + "sha256:b1260ed381b10a11753c73444408e19869f3241fc45c985cd55a30177c789d13" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.2.3" + "version": "==4.2.10" }, "django-debug-toolbar": { "hashes": [ diff --git a/src/api/tests/test_available.py b/src/api/tests/test_available.py index fa9dadcd4..b85ea6335 100644 --- a/src/api/tests/test_available.py +++ b/src/api/tests/test_available.py @@ -19,7 +19,6 @@ API_BASE_PATH = "/api/v1/available/?domain=" class AvailableViewTest(MockEppLib): - """Test that the view function works as expected.""" def setUp(self): @@ -123,7 +122,6 @@ class AvailableViewTest(MockEppLib): class AvailableAPITest(MockEppLib): - """Test that the API can be called as expected.""" def setUp(self): diff --git a/src/api/views.py b/src/api/views.py index f9fa2d1ea..2199e15ac 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -1,4 +1,5 @@ """Internal API views""" + from django.apps import apps from django.views.decorators.http import require_http_methods from django.http import HttpResponse diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 4034bf35b..c5f5be276 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -739,7 +739,6 @@ class DomainApplicationAdminForm(forms.ModelForm): class DomainApplicationAdmin(ListHeaderAdmin): - """Custom domain applications admin class.""" class InvestigatorFilter(admin.SimpleListFilter): diff --git a/src/registrar/apps.py b/src/registrar/apps.py index 9f1b186ad..fcb5c17fd 100644 --- a/src/registrar/apps.py +++ b/src/registrar/apps.py @@ -2,7 +2,6 @@ from django.apps import AppConfig class RegistrarConfig(AppConfig): - """Configure signal handling for our registrar Django application.""" name = "registrar" diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 372434887..009baa1c6 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -16,6 +16,7 @@ $ docker-compose exec app python manage.py shell ``` """ + import environs from base64 import b64decode from cfenv import AppEnv # type: ignore diff --git a/src/registrar/fixtures_applications.py b/src/registrar/fixtures_applications.py index 659a3040e..3e4e0e362 100644 --- a/src/registrar/fixtures_applications.py +++ b/src/registrar/fixtures_applications.py @@ -201,7 +201,6 @@ class DomainApplicationFixture: class DomainFixture(DomainApplicationFixture): - """Create one domain and permissions on it for each user.""" @classmethod diff --git a/src/registrar/management/commands/cat_files_into_getgov.py b/src/registrar/management/commands/cat_files_into_getgov.py index 4ccb1301b..4fb7ad5b8 100644 --- a/src/registrar/management/commands/cat_files_into_getgov.py +++ b/src/registrar/management/commands/cat_files_into_getgov.py @@ -1,4 +1,5 @@ """Loads files from /tmp into our sandboxes""" + import glob import logging diff --git a/src/registrar/management/commands/generate_current_federal_report.py b/src/registrar/management/commands/generate_current_federal_report.py index 1a123bf5b..6516bf99b 100644 --- a/src/registrar/management/commands/generate_current_federal_report.py +++ b/src/registrar/management/commands/generate_current_federal_report.py @@ -1,4 +1,5 @@ """Generates current-full.csv and current-federal.csv then uploads them to the desired URL.""" + import logging import os diff --git a/src/registrar/management/commands/generate_current_full_report.py b/src/registrar/management/commands/generate_current_full_report.py index 80c031605..be810ee10 100644 --- a/src/registrar/management/commands/generate_current_full_report.py +++ b/src/registrar/management/commands/generate_current_full_report.py @@ -1,4 +1,5 @@ """Generates current-full.csv and current-federal.csv then uploads them to the desired URL.""" + import logging import os diff --git a/src/registrar/management/commands/patch_federal_agency_info.py b/src/registrar/management/commands/patch_federal_agency_info.py index 35642c1bf..b286f1516 100644 --- a/src/registrar/management/commands/patch_federal_agency_info.py +++ b/src/registrar/management/commands/patch_federal_agency_info.py @@ -1,4 +1,5 @@ """Loops through each valid DomainInformation object and updates its agency value""" + import argparse import csv import logging diff --git a/src/registrar/management/commands/utility/epp_data_containers.py b/src/registrar/management/commands/utility/epp_data_containers.py index 1f370dca7..9e5769751 100644 --- a/src/registrar/management/commands/utility/epp_data_containers.py +++ b/src/registrar/management/commands/utility/epp_data_containers.py @@ -5,6 +5,7 @@ Regarding our dataclasses: Not intended to be used as models but rather as an alternative to storing as a dictionary. By keeping it as a dataclass instead of a dictionary, we can maintain data consistency. """ # noqa + from dataclasses import dataclass, field from datetime import date from enum import Enum diff --git a/src/registrar/management/commands/utility/extra_transition_domain_helper.py b/src/registrar/management/commands/utility/extra_transition_domain_helper.py index c082552eb..5c3573fb1 100644 --- a/src/registrar/management/commands/utility/extra_transition_domain_helper.py +++ b/src/registrar/management/commands/utility/extra_transition_domain_helper.py @@ -1,4 +1,5 @@ """""" + import csv from dataclasses import dataclass from datetime import datetime diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index ff7389780..d316cde4c 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -6,7 +6,6 @@ from .utility.time_stamped_model import TimeStampedModel class Contact(TimeStampedModel): - """Contact information follows a similar pattern for each contact.""" user = models.OneToOneField( diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index c37fc19b5..307115112 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -17,7 +17,6 @@ logger = logging.getLogger(__name__) class DomainApplication(TimeStampedModel): - """A registrant's application for a new domain.""" # Constants for choice fields @@ -97,7 +96,6 @@ class DomainApplication(TimeStampedModel): ARMED_FORCES_AP = "AP", "Armed Forces Pacific (AP)" class OrganizationChoices(models.TextChoices): - """ Primary organization choices: For use in django admin @@ -114,7 +112,6 @@ class DomainApplication(TimeStampedModel): SCHOOL_DISTRICT = "school_district", "School district" class OrganizationChoicesVerbose(models.TextChoices): - """ Secondary organization choices For use in the application form and on the templates diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 65d099e5a..acaa330bb 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -14,7 +14,6 @@ logger = logging.getLogger(__name__) class DomainInformation(TimeStampedModel): - """A registrant's domain information for that domain, exported from DomainApplication. We use these field from DomainApplication with few exceptions which are 'removed' via pop at the bottom of this file. Most of design for domain diff --git a/src/registrar/models/user_domain_role.py b/src/registrar/models/user_domain_role.py index 479f75089..6e915e4af 100644 --- a/src/registrar/models/user_domain_role.py +++ b/src/registrar/models/user_domain_role.py @@ -4,11 +4,9 @@ from .utility.time_stamped_model import TimeStampedModel class UserDomainRole(TimeStampedModel): - """This is a linking table that connects a user with a role on a domain.""" class Roles(models.TextChoices): - """The possible roles are listed here. Implementation of the named roles for allowing particular operations happens diff --git a/src/registrar/models/verified_by_staff.py b/src/registrar/models/verified_by_staff.py index 4c9e76e9d..a6d861504 100644 --- a/src/registrar/models/verified_by_staff.py +++ b/src/registrar/models/verified_by_staff.py @@ -4,7 +4,6 @@ from .utility.time_stamped_model import TimeStampedModel class VerifiedByStaff(TimeStampedModel): - """emails that get added to this table will bypass ial2 on login.""" email = models.EmailField( diff --git a/src/registrar/models/website.py b/src/registrar/models/website.py index d21564531..0b337e397 100644 --- a/src/registrar/models/website.py +++ b/src/registrar/models/website.py @@ -4,7 +4,6 @@ from .utility.time_stamped_model import TimeStampedModel class Website(TimeStampedModel): - """Keep domain names in their own table so that applications can refer to many of them.""" diff --git a/src/registrar/no_cache_middleware.py b/src/registrar/no_cache_middleware.py index 6f509b9d6..5edfca20e 100644 --- a/src/registrar/no_cache_middleware.py +++ b/src/registrar/no_cache_middleware.py @@ -6,7 +6,6 @@ better caching responses. class NoCacheMiddleware: - """Middleware to add a single header to every response.""" def __init__(self, get_response): diff --git a/src/registrar/templatetags/field_helpers.py b/src/registrar/templatetags/field_helpers.py index bc296753e..811897908 100644 --- a/src/registrar/templatetags/field_helpers.py +++ b/src/registrar/templatetags/field_helpers.py @@ -1,4 +1,5 @@ """Custom field helpers for our inputs.""" + import re from django import template diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index d7c8960f6..41fa75f1d 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -600,7 +600,6 @@ class TestPermissions(TestCase): class TestDomainInformation(TestCase): - """Test the DomainInformation model, when approved or otherwise""" def setUp(self): @@ -653,7 +652,6 @@ class TestDomainInformation(TestCase): class TestInvitations(TestCase): - """Test the retrieval of invitations.""" def setUp(self): diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index ca0a5e8d8..1c4d2521e 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -3,6 +3,7 @@ Feature being tested: Registry Integration This file tests the various ways in which the registrar interacts with the registry. """ + from django.test import TestCase from django.db.utils import IntegrityError from unittest.mock import MagicMock, patch, call diff --git a/src/registrar/tests/test_views_application.py b/src/registrar/tests/test_views_application.py index 02fe5ff76..2b08d8d74 100644 --- a/src/registrar/tests/test_views_application.py +++ b/src/registrar/tests/test_views_application.py @@ -28,7 +28,6 @@ logger = logging.getLogger(__name__) class DomainApplicationTests(TestWithUser, WebTest): - """Webtests for domain application to test filling and submitting.""" # Doesn't work with CSRF checking diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index c9422e700..2c8e796ac 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -1236,7 +1236,6 @@ class TestDomainSecurityEmail(TestDomainOverview): class TestDomainDNSSEC(TestDomainOverview): - """MockEPPLib is already inherited.""" def test_dnssec_page_refreshes_enable_button(self): diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index d56c02cbf..461637f23 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -10,7 +10,6 @@ logger = logging.getLogger(__name__) class EmailSendingError(RuntimeError): - """Local error for handling all failures when sending email.""" pass diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 04fe1ce3a..d5f8f67b4 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -135,7 +135,6 @@ class DomainFormBaseView(DomainBaseView, FormMixin): class DomainView(DomainBaseView): - """Domain detail overview page.""" template_name = "domain_detail.html" diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index b2c4cb364..8de75e151 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -146,7 +146,6 @@ class OrderableFieldsMixin: class PermissionsLoginMixin(PermissionRequiredMixin): - """Mixin that redirects to login page if not logged in, otherwise 403.""" def handle_no_permission(self): @@ -155,7 +154,6 @@ class PermissionsLoginMixin(PermissionRequiredMixin): class DomainPermission(PermissionsLoginMixin): - """Permission mixin that redirects to domain if user has access, otherwise 403""" @@ -264,7 +262,6 @@ class DomainPermission(PermissionsLoginMixin): class DomainApplicationPermission(PermissionsLoginMixin): - """Permission mixin that redirects to domain application if user has access, otherwise 403""" @@ -287,7 +284,6 @@ class DomainApplicationPermission(PermissionsLoginMixin): class UserDeleteDomainRolePermission(PermissionsLoginMixin): - """Permission mixin for UserDomainRole if user has access, otherwise 403""" @@ -324,7 +320,6 @@ class UserDeleteDomainRolePermission(PermissionsLoginMixin): class DomainApplicationPermissionWithdraw(PermissionsLoginMixin): - """Permission mixin that redirects to withdraw action on domain application if user has access, otherwise 403""" @@ -347,7 +342,6 @@ class DomainApplicationPermissionWithdraw(PermissionsLoginMixin): class ApplicationWizardPermission(PermissionsLoginMixin): - """Permission mixin that redirects to start or edit domain application if user has access, otherwise 403""" @@ -365,7 +359,6 @@ class ApplicationWizardPermission(PermissionsLoginMixin): class DomainInvitationPermission(PermissionsLoginMixin): - """Permission mixin that redirects to domain invitation if user has access, otherwise 403" diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index 54c96d602..02d3db96d 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -20,7 +20,6 @@ logger = logging.getLogger(__name__) class DomainPermissionView(DomainPermission, DetailView, abc.ABC): - """Abstract base view for domains that enforces permissions. This abstract view cannot be instantiated. Actual views must specify @@ -58,7 +57,6 @@ class DomainPermissionView(DomainPermission, DetailView, abc.ABC): class DomainApplicationPermissionView(DomainApplicationPermission, DetailView, abc.ABC): - """Abstract base view for domain applications that enforces permissions This abstract view cannot be instantiated. Actual views must specify @@ -78,7 +76,6 @@ class DomainApplicationPermissionView(DomainApplicationPermission, DetailView, a class DomainApplicationPermissionWithdrawView(DomainApplicationPermissionWithdraw, DetailView, abc.ABC): - """Abstract base view for domain application withdraw function This abstract view cannot be instantiated. Actual views must specify @@ -98,7 +95,6 @@ class DomainApplicationPermissionWithdrawView(DomainApplicationPermissionWithdra class ApplicationWizardPermissionView(ApplicationWizardPermission, TemplateView, abc.ABC): - """Abstract base view for the application form that enforces permissions This abstract view cannot be instantiated. Actual views must specify @@ -113,7 +109,6 @@ class ApplicationWizardPermissionView(ApplicationWizardPermission, TemplateView, class DomainInvitationPermissionDeleteView(DomainInvitationPermission, DeleteView, abc.ABC): - """Abstract view for deleting a domain invitation. This one is fairly specialized, but this is the only thing that we do @@ -127,7 +122,6 @@ class DomainInvitationPermissionDeleteView(DomainInvitationPermission, DeleteVie class DomainApplicationPermissionDeleteView(DomainApplicationPermission, DeleteView, abc.ABC): - """Abstract view for deleting a DomainApplication.""" model = DomainApplication @@ -135,7 +129,6 @@ class DomainApplicationPermissionDeleteView(DomainApplicationPermission, DeleteV class UserDomainRolePermissionDeleteView(UserDeleteDomainRolePermission, DeleteView, abc.ABC): - """Abstract base view for deleting a UserDomainRole. This abstract view cannot be instantiated. Actual views must specify diff --git a/src/requirements.txt b/src/requirements.txt index 7c79cf8a3..a6130a3bf 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -12,7 +12,7 @@ cryptography==42.0.2; python_version >= '3.7' defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' dj-database-url==2.1.0 dj-email-url==1.0.6 -django==4.2.3; python_version >= '3.8' +django==4.2.10; python_version >= '3.8' django-allow-cidr==0.7.1 django-auditlog==2.3.0; python_version >= '3.7' django-cache-url==3.4.5 From 5abee70d2ea306f8fa04d305c50abc88ab972d48 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 8 Feb 2024 15:23:13 -0700 Subject: [PATCH 088/119] 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 04fe1ce3a..e6af77b6c 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -555,7 +555,7 @@ class DomainYourContactInformationView(DomainFormBaseView): # Post to DB using values from the form form.save() - messages.success(self.request, "Your contact information has been updated.") + messages.success(self.request, "Your contact information for all your domains has been updated.") # superclass has the redirect return super().form_valid(form) From 1df7dc48df354d4f00171b9c26e54242338a0bcf Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 9 Feb 2024 08:35:30 -0500 Subject: [PATCH 089/119] oidc error on init logs error; oidc re-inits on request if in error state --- src/djangooidc/tests/test_views.py | 24 ++++++++-------- src/djangooidc/views.py | 46 ++++++++++++++++++++++-------- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/src/djangooidc/tests/test_views.py b/src/djangooidc/tests/test_views.py index 4193f723b..057ed6f6b 100644 --- a/src/djangooidc/tests/test_views.py +++ b/src/djangooidc/tests/test_views.py @@ -82,14 +82,14 @@ class ViewsTest(TestCase): # mock mock_client.callback.side_effect = self.user_info # test - with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise(): + with patch("djangooidc.views._requires_step_up_auth", return_value=False), less_console_noise(): response = self.client.get(reverse("openid_login_callback")) # assert self.assertEqual(response.status_code, 302) self.assertEqual(response.url, reverse("logout")) def test_login_callback_no_step_up_auth(self, mock_client): - """Walk through login_callback when requires_step_up_auth returns False + """Walk through login_callback when _requires_step_up_auth returns False and assert that we have a redirect to /""" with less_console_noise(): # setup @@ -98,14 +98,14 @@ class ViewsTest(TestCase): # mock mock_client.callback.side_effect = self.user_info # test - with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise(): + with patch("djangooidc.views._requires_step_up_auth", return_value=False), less_console_noise(): response = self.client.get(reverse("openid_login_callback")) # assert self.assertEqual(response.status_code, 302) self.assertEqual(response.url, "/") def test_requires_step_up_auth(self, mock_client): - """Invoke login_callback passing it a request when requires_step_up_auth returns True + """Invoke login_callback passing it a request when _requires_step_up_auth returns True and assert that session is updated and create_authn_request (mock) is called.""" with less_console_noise(): # Configure the mock to return an expected value for get_step_up_acr_value @@ -114,12 +114,12 @@ class ViewsTest(TestCase): request = self.factory.get("/some-url") request.session = {"acr_value": ""} # Ensure that the CLIENT instance used in login_callback is the mock - # patch requires_step_up_auth to return True - with patch("djangooidc.views.requires_step_up_auth", return_value=True), patch( + # patch _requires_step_up_auth to return True + with patch("djangooidc.views._requires_step_up_auth", return_value=True), patch( "djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock() ) as mock_create_authn_request: login_callback(request) - # create_authn_request only gets called when requires_step_up_auth is True + # create_authn_request only gets called when _requires_step_up_auth is True # and it changes this acr_value in request.session # Assert that acr_value is no longer empty string self.assertNotEqual(request.session["acr_value"], "") @@ -127,7 +127,7 @@ class ViewsTest(TestCase): mock_create_authn_request.assert_called_once() def test_does_not_requires_step_up_auth(self, mock_client): - """Invoke login_callback passing it a request when requires_step_up_auth returns False + """Invoke login_callback passing it a request when _requires_step_up_auth returns False and assert that session is not updated and create_authn_request (mock) is not called. Possibly redundant with test_login_callback_requires_step_up_auth""" @@ -136,12 +136,12 @@ class ViewsTest(TestCase): request = self.factory.get("/some-url") request.session = {"acr_value": ""} # Ensure that the CLIENT instance used in login_callback is the mock - # patch requires_step_up_auth to return False - with patch("djangooidc.views.requires_step_up_auth", return_value=False), patch( + # patch _requires_step_up_auth to return False + with patch("djangooidc.views._requires_step_up_auth", return_value=False), patch( "djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock() ) as mock_create_authn_request: login_callback(request) - # create_authn_request only gets called when requires_step_up_auth is True + # create_authn_request only gets called when _requires_step_up_auth is True # and it changes this acr_value in request.session # Assert that acr_value is NOT updated by testing that it is still an empty string self.assertEqual(request.session["acr_value"], "") @@ -155,7 +155,7 @@ class ViewsTest(TestCase): mock_client.callback.side_effect = self.user_info mock_auth.return_value = None # test - with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise(): + with patch("djangooidc.views._requires_step_up_auth", return_value=False), less_console_noise(): response = self.client.get(reverse("openid_login_callback")) # assert self.assertEqual(response.status_code, 401) diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index 3d824c8e3..af933b7ff 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -15,15 +15,28 @@ from registrar.models import User logger = logging.getLogger(__name__) -try: +CLIENT = None + + +def _initialize_client(): + """Initialize the OIDC client. Exceptions are allowed to raise + and will need to be caught.""" + global CLIENT # Initialize provider using pyOICD OP = getattr(settings, "OIDC_ACTIVE_PROVIDER") CLIENT = Client(OP) - logger.debug("client initialized %s" % CLIENT) + logger.debug("Client initialized: %s" % CLIENT) + + +# Initialize CLIENT +try: + _initialize_client() except Exception as err: - CLIENT = None # type: ignore - logger.warning(err) - logger.warning("Unable to configure OpenID Connect provider. Users cannot log in.") + # In the event of an exception, log the error and allow the app load to continue + # without the OIDC Client. Subsequent login attempts will attempt to initialize + # again if Client is None + logger.error(err) + logger.error("Unable to configure OpenID Connect provider. Users cannot log in.") def error_page(request, error): @@ -55,12 +68,14 @@ def error_page(request, error): def openid(request): """Redirect the user to an authentication provider (OP).""" - # If the session reset because of a server restart, attempt to login again - request.session["acr_value"] = CLIENT.get_default_acr_value() - - request.session["next"] = request.GET.get("next", "/") - + global CLIENT try: + # If the CLIENT is none, attempt to reinitialize before handling the request + if CLIENT is None: + _initialize_client() + request.session["acr_value"] = CLIENT.get_default_acr_value() + request.session["next"] = request.GET.get("next", "/") + # Create the authentication request return CLIENT.create_authn_request(request.session) except Exception as err: return error_page(request, err) @@ -68,12 +83,16 @@ def openid(request): def login_callback(request): """Analyze the token returned by the authentication provider (OP).""" + global CLIENT try: + # If the CLIENT is none, attempt to reinitialize before handling the request + if CLIENT is None: + _initialize_client() query = parse_qs(request.GET.urlencode()) userinfo = CLIENT.callback(query, request.session) # test for need for identity verification and if it is satisfied # if not satisfied, redirect user to login with stepped up acr_value - if requires_step_up_auth(userinfo): + if _requires_step_up_auth(userinfo): # add acr_value to request.session request.session["acr_value"] = CLIENT.get_step_up_acr_value() return CLIENT.create_authn_request(request.session) @@ -86,13 +105,16 @@ def login_callback(request): else: raise o_e.BannedUser() except o_e.NoStateDefined as nsd_err: + # In the event that a user is in the middle of a login when the app is restarted, + # their session state will no longer be available, so redirect the user to the + # beginning of login process without raising an error to the user. logger.warning(f"No State Defined: {nsd_err}") return redirect(request.session.get("next", "/")) except Exception as err: return error_page(request, err) -def requires_step_up_auth(userinfo): +def _requires_step_up_auth(userinfo): """if User.needs_identity_verification and step_up_acr_value not in ial returned from callback, return True""" step_up_acr_value = CLIENT.get_step_up_acr_value() From 7e37b93fc702f1eb5a750cf2888fdd42a7e26128 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 9 Feb 2024 11:53:19 -0500 Subject: [PATCH 090/119] Refactor model tests for less repetition --- src/registrar/models/domain_application.py | 4 +- src/registrar/tests/test_models.py | 167 +++++---------------- 2 files changed, 39 insertions(+), 132 deletions(-) diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 0399a039e..f048bdb89 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -655,7 +655,9 @@ class DomainApplication(TimeStampedModel): self.save() # Limit email notifications to transitions from Started and Withdrawn - if self.status == self.ApplicationStatus.STARTED or self.status == self.ApplicationStatus.WITHDRAWN: + limited_statuses = [self.ApplicationStatus.STARTED, self.ApplicationStatus.WITHDRAWN] + + if self.status in limited_statuses: self._send_status_update_email( "submission confirmation", "emails/submission_confirmation.txt", diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 7f866ad8b..0cb050f41 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -161,158 +161,63 @@ class TestDomainApplication(TestCase): application.submit() self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED) + def check_email_sent(self, application, msg, action, expected_count): + """Check if an email was sent after performing an action.""" + + with self.subTest(msg=msg, action=action): + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Perform the specified action + action_method = getattr(application, action) + action_method() + + # Check if an email was sent + sent_emails = [ + email + for email in MockSESClient.EMAILS_SENT + if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] + ] + self.assertEqual(len(sent_emails), expected_count) + def test_submit_from_started_sends_email(self): - """Create an application and submit it and see if email was sent.""" - - # submitter's email is mayor@igorville.gov + msg = "Create an application and submit it and see if email was sent." application = completed_application() - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - application.submit() - - # check to see if an email was sent - self.assertGreater( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) + self.check_email_sent(application, msg, "submit", 1) def test_submit_from_withdrawn_sends_email(self): - """Create a withdrawn application and submit it and see if email was sent.""" - - # submitter's email is mayor@igorville.gov + msg = "Create a withdrawn application and submit it and see if email was sent." application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN) - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.submit() - - # check to see if an email was sent - self.assertGreater( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) + self.check_email_sent(application, msg, "submit", 1) def test_submit_from_action_needed_does_not_send_email(self): - """Create a withdrawn application and submit it and see if email was sent.""" - - # submitter's email is mayor@igorville.gov + msg = "Create an application with ACTION_NEEDED status and submit it, check if email was not sent." application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED) - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.submit() - - # check to see if an email was sent - self.assertEqual( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) + self.check_email_sent(application, msg, "submit", 0) def test_submit_from_in_review_does_not_send_email(self): - """Create a withdrawn application and submit it and see if email was sent.""" - - # submitter's email is mayor@igorville.gov + msg = "Create a withdrawn application and submit it and see if email was sent." application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.submit() - - # check to see if an email was sent - self.assertEqual( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) + self.check_email_sent(application, msg, "submit", 0) def test_approve_sends_email(self): - """Create an application and approve it and see if email was sent.""" - - # submitter's email is mayor@igorville.gov + msg = "Create an application and approve it and see if email was sent." application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.approve() - - # check to see if an email was sent - self.assertGreater( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) + self.check_email_sent(application, msg, "approve", 1) def test_withdraw_sends_email(self): - """Create an application and withdraw it and see if email was sent.""" - - # submitter's email is mayor@igorville.gov + msg = "Create an application and withdraw it and see if email was sent." application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.withdraw() - - # check to see if an email was sent - self.assertGreater( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) + self.check_email_sent(application, msg, "withdraw", 1) def test_reject_sends_email(self): - """Create an application and reject it and see if email was sent.""" - - # submitter's email is mayor@igorville.gov + msg = "Create an application and reject it and see if email was sent." application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) + self.check_email_sent(application, msg, "reject", 1) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - application.reject() - - # check to see if an email was sent - self.assertGreater( - len( - [ - email - for email in MockSESClient.EMAILS_SENT - if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"] - ] - ), - 0, - ) + def test_reject_with_prejudice_does_not_send_email(self): + msg = "Create an application and reject it with prejudice and see if email was sent." + application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) + self.check_email_sent(application, msg, "reject_with_prejudice", 0) def test_submit_transition_allowed(self): """ From 6820456286102217f1e3c0d41b2308116863d1f6 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 9 Feb 2024 14:25:11 -0500 Subject: [PATCH 091/119] openid tests --- src/djangooidc/tests/test_views.py | 171 ++++++++++++++++++++++------- src/djangooidc/views.py | 8 +- 2 files changed, 140 insertions(+), 39 deletions(-) diff --git a/src/djangooidc/tests/test_views.py b/src/djangooidc/tests/test_views.py index 057ed6f6b..9784ae0bd 100644 --- a/src/djangooidc/tests/test_views.py +++ b/src/djangooidc/tests/test_views.py @@ -4,13 +4,13 @@ from django.http import HttpResponse from django.test import Client, TestCase, RequestFactory from django.urls import reverse -from djangooidc.exceptions import NoStateDefined -from ..views import login_callback +from djangooidc.exceptions import NoStateDefined, InternalError +from ..views import login_callback, CLIENT from .common import less_console_noise -@patch("djangooidc.views.CLIENT", autospec=True) +@patch("djangooidc.views.CLIENT", new_callable=MagicMock) class ViewsTest(TestCase): def setUp(self): self.client = Client() @@ -35,56 +35,135 @@ class ViewsTest(TestCase): pass def test_openid_sets_next(self, mock_client): + """ Test that the openid method properly sets next in the session.""" with less_console_noise(): - # setup + # SETUP + # set up the callback url that will be tested in assertions against + # session[next] callback_url = reverse("openid_login_callback") - # mock + # MOCK + # when login is called, response from create_authn_request should + # be returned to user, so let's mock it and test it mock_client.create_authn_request.side_effect = self.say_hi + # in this case, we need to mock the get_default_acr_value so that + # openid method will execute properly, but the acr_value itself + # is not important for this test mock_client.get_default_acr_value.side_effect = self.create_acr - # test + # TEST + # test the login url, passing a callback url response = self.client.get(reverse("login"), {"next": callback_url}) - # assert + # ASSERTIONS session = mock_client.create_authn_request.call_args[0][0] + # assert the session[next] is set to the callback_url self.assertEqual(session["next"], callback_url) + # assert that openid returned properly the response from + # create_authn_request self.assertEqual(response.status_code, 200) self.assertContains(response, "Hi") def test_openid_raises(self, mock_client): + """Test that errors in openid raise 500 error for the user. + This test specifically tests for any exceptions that might be raised from + create_authn_request. This includes scenarios where CLIENT exists, but + is no longer functioning properly.""" with less_console_noise(): - # mock + # MOCK + # when login is called, exception thrown from create_authn_request + # should present 500 error page to user mock_client.create_authn_request.side_effect = Exception("Test") - # test + # TEST + # test when login url is called response = self.client.get(reverse("login")) - # assert + # ASSERTIONS + # assert that the 500 error page is raised self.assertEqual(response.status_code, 500) self.assertTemplateUsed(response, "500.html") self.assertIn("Server error", response.content.decode("utf-8")) - def test_callback_with_no_session_state(self, mock_client): + def test_openid_raises_when_client_is_none_and_cant_init(self, mock_client): + """Test that errors in openid raise 500 error for the user. + This test specifically tests for the condition where the CLIENT + is None and the client initialization attempt raises an exception.""" + with less_console_noise(): + # MOCK + # mock that CLIENT is None + # mock that Client() raises an exception (by mocking _initialize_client) + # Patch CLIENT to None for this specific test + with patch("djangooidc.views.CLIENT", None): + # Patch _initialize_client() to raise an exception + with patch("djangooidc.views._initialize_client") as mock_init: + mock_init.side_effect = InternalError + # TEST + # test when login url is called + response = self.client.get(reverse("login")) + # ASSERTIONS + # assert that the 500 error page is raised + self.assertEqual(response.status_code, 500) + self.assertTemplateUsed(response, "500.html") + self.assertIn("Server error", response.content.decode("utf-8")) + + def test_openid_initializes_client_and_calls_create_authn_request(self, mock_client): + """Test that openid re-initializes the client when the client had not + been previously initiated.""" + with less_console_noise(): + # MOCK + # response from create_authn_request should + # be returned to user, so let's mock it and test it + mock_client.create_authn_request.side_effect = self.say_hi + # in this case, we need to mock the get_default_acr_value so that + # openid method will execute properly, but the acr_value itself + # is not important for this test + mock_client.get_default_acr_value.side_effect = self.create_acr + with patch("djangooidc.views._initialize_client") as mock_init_client: + with patch("djangooidc.views._client_is_none") as mock_client_is_none: + # mock the client to initially be None + mock_client_is_none.return_value = True + # TEST + # test when login url is called + response = self.client.get(reverse("login")) + # ASSERTIONS + # assert that _initialize_client was called + mock_init_client.assert_called_once() + # assert that the response is the mocked response from create_authn_request + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Hi") + + def test_login_callback_with_no_session_state(self, mock_client): """If the local session is None (ie the server restarted while user was logged out), we do not throw an exception. Rather, we attempt to login again.""" with less_console_noise(): - # mock + # MOCK + # mock the acr_value to some string + # mock the callback function to raise the NoStateDefined Exception mock_client.get_default_acr_value.side_effect = self.create_acr mock_client.callback.side_effect = NoStateDefined() - # test + # TEST + # test the login callback response = self.client.get(reverse("openid_login_callback")) - # assert + # ASSERTIONS + # assert that the user is redirected to the start of the login process self.assertEqual(response.status_code, 302) self.assertEqual(response.url, "/") def test_login_callback_reads_next(self, mock_client): + """If the next value is set in the session, test that login_callback returns + a redirect to the 'next' url.""" with less_console_noise(): - # setup + # SETUP session = self.client.session + # set 'next' to the logout url session["next"] = reverse("logout") session.save() - # mock + # MOCK + # mock that callback returns user_info; this is the expected behavior mock_client.callback.side_effect = self.user_info - # test - with patch("djangooidc.views._requires_step_up_auth", return_value=False), less_console_noise(): + # patch that the request does not require step up auth + # TEST + # test the login callback url + with patch("djangooidc.views._requires_step_up_auth", return_value=False): response = self.client.get(reverse("openid_login_callback")) - # assert + # ASSERTIONS + # assert the redirect url is the same as the 'next' value set in session self.assertEqual(response.status_code, 302) self.assertEqual(response.url, reverse("logout")) @@ -92,15 +171,19 @@ class ViewsTest(TestCase): """Walk through login_callback when _requires_step_up_auth returns False and assert that we have a redirect to /""" with less_console_noise(): - # setup + # SETUP session = self.client.session session.save() - # mock + # MOCK + # mock that callback returns user_info; this is the expected behavior mock_client.callback.side_effect = self.user_info - # test - with patch("djangooidc.views._requires_step_up_auth", return_value=False), less_console_noise(): + # patch that the request does not require step up auth + # TEST + # test the login callback url + with patch("djangooidc.views._requires_step_up_auth", return_value=False): response = self.client.get(reverse("openid_login_callback")) - # assert + # ASSERTIONS + # assert that redirect is to / when no 'next' is set self.assertEqual(response.status_code, 302) self.assertEqual(response.url, "/") @@ -108,6 +191,7 @@ class ViewsTest(TestCase): """Invoke login_callback passing it a request when _requires_step_up_auth returns True and assert that session is updated and create_authn_request (mock) is called.""" with less_console_noise(): + # MOCK # Configure the mock to return an expected value for get_step_up_acr_value mock_client.return_value.get_step_up_acr_value.return_value = "step_up_acr_value" # Create a mock request @@ -118,7 +202,10 @@ class ViewsTest(TestCase): with patch("djangooidc.views._requires_step_up_auth", return_value=True), patch( "djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock() ) as mock_create_authn_request: + # TEST + # test the login callback login_callback(request) + # ASSERTIONS # create_authn_request only gets called when _requires_step_up_auth is True # and it changes this acr_value in request.session # Assert that acr_value is no longer empty string @@ -132,6 +219,7 @@ class ViewsTest(TestCase): Possibly redundant with test_login_callback_requires_step_up_auth""" with less_console_noise(): + # MOCK # Create a mock request request = self.factory.get("/some-url") request.session = {"acr_value": ""} @@ -140,7 +228,10 @@ class ViewsTest(TestCase): with patch("djangooidc.views._requires_step_up_auth", return_value=False), patch( "djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock() ) as mock_create_authn_request: + # TEST + # test the login callback login_callback(request) + # ASSERTIONS # create_authn_request only gets called when _requires_step_up_auth is True # and it changes this acr_value in request.session # Assert that acr_value is NOT updated by testing that it is still an empty string @@ -150,33 +241,36 @@ class ViewsTest(TestCase): @patch("djangooidc.views.authenticate") def test_login_callback_raises(self, mock_auth, mock_client): + """Test that login callback raises a 401 when user is unauthorized""" with less_console_noise(): - # mock + # MOCK + # mock that callback returns user_info; this is the expected behavior mock_client.callback.side_effect = self.user_info mock_auth.return_value = None - # test - with patch("djangooidc.views._requires_step_up_auth", return_value=False), less_console_noise(): + # TEST + with patch("djangooidc.views._requires_step_up_auth", return_value=False): response = self.client.get(reverse("openid_login_callback")) - # assert + # ASSERTIONS self.assertEqual(response.status_code, 401) self.assertTemplateUsed(response, "401.html") self.assertIn("Unauthorized", response.content.decode("utf-8")) def test_logout_redirect_url(self, mock_client): + """Test that logout redirects to the configured post_logout_redirect_uris.""" with less_console_noise(): - # setup + # SETUP session = self.client.session session["state"] = "TEST" # nosec B105 session.save() - # mock + # MOCK mock_client.callback.side_effect = self.user_info mock_client.registration_response = {"post_logout_redirect_uris": ["http://example.com/back"]} mock_client.provider_info = {"end_session_endpoint": "http://example.com/log_me_out"} mock_client.client_id = "TEST" - # test + # TEST with less_console_noise(): response = self.client.get(reverse("logout")) - # assert + # ASSERTIONS expected = ( "http://example.com/log_me_out?client_id=TEST&state" "=TEST&post_logout_redirect_uri=http%3A%2F%2Fexample.com%2Fback" @@ -187,20 +281,23 @@ class ViewsTest(TestCase): @patch("djangooidc.views.auth_logout") def test_logout_always_logs_out(self, mock_logout, _): - # Without additional mocking, logout will always fail. - # Here we test that auth_logout is called regardless + """Without additional mocking, logout will always fail. + Here we test that auth_logout is called regardless""" + # TEST with less_console_noise(): self.client.get(reverse("logout")) + # ASSERTIONS self.assertTrue(mock_logout.called) def test_logout_callback_redirects(self, _): + """Test that the logout_callback redirects properly""" with less_console_noise(): - # setup + # SETUP session = self.client.session session["next"] = reverse("logout") session.save() - # test + # TEST response = self.client.get(reverse("openid_logout_callback")) - # assert + # ASSERTIONS self.assertEqual(response.status_code, 302) self.assertEqual(response.url, reverse("logout")) diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index af933b7ff..0ba75b2e2 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -27,6 +27,10 @@ def _initialize_client(): CLIENT = Client(OP) logger.debug("Client initialized: %s" % CLIENT) +def _client_is_none(): + """ Return if the CLIENT is currently None.""" + global CLIENT + return CLIENT is None # Initialize CLIENT try: @@ -71,7 +75,7 @@ def openid(request): global CLIENT try: # If the CLIENT is none, attempt to reinitialize before handling the request - if CLIENT is None: + if _client_is_none(): _initialize_client() request.session["acr_value"] = CLIENT.get_default_acr_value() request.session["next"] = request.GET.get("next", "/") @@ -86,7 +90,7 @@ def login_callback(request): global CLIENT try: # If the CLIENT is none, attempt to reinitialize before handling the request - if CLIENT is None: + if _client_is_none(): _initialize_client() query = parse_qs(request.GET.urlencode()) userinfo = CLIENT.callback(query, request.session) From 9dff75d392cc0e71a2f37db7089609db8395e310 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 9 Feb 2024 15:28:03 -0500 Subject: [PATCH 092/119] tests for login_callback --- src/djangooidc/tests/test_views.py | 56 +++++++++++++++++++++++++++--- src/djangooidc/views.py | 4 ++- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/djangooidc/tests/test_views.py b/src/djangooidc/tests/test_views.py index 9784ae0bd..4cd2241e3 100644 --- a/src/djangooidc/tests/test_views.py +++ b/src/djangooidc/tests/test_views.py @@ -5,7 +5,7 @@ from django.test import Client, TestCase, RequestFactory from django.urls import reverse from djangooidc.exceptions import NoStateDefined, InternalError -from ..views import login_callback, CLIENT +from ..views import login_callback from .common import less_console_noise @@ -35,7 +35,7 @@ class ViewsTest(TestCase): pass def test_openid_sets_next(self, mock_client): - """ Test that the openid method properly sets next in the session.""" + """Test that the openid method properly sets next in the session.""" with less_console_noise(): # SETUP # set up the callback url that will be tested in assertions against @@ -167,6 +167,54 @@ class ViewsTest(TestCase): self.assertEqual(response.status_code, 302) self.assertEqual(response.url, reverse("logout")) + def test_login_callback_raises_when_client_is_none_and_cant_init(self, mock_client): + """Test that errors in login_callback raise 500 error for the user. + This test specifically tests for the condition where the CLIENT + is None and the client initialization attempt raises an exception.""" + with less_console_noise(): + # MOCK + # mock that CLIENT is None + # mock that Client() raises an exception (by mocking _initialize_client) + # Patch CLIENT to None for this specific test + with patch("djangooidc.views.CLIENT", None): + # Patch _initialize_client() to raise an exception + with patch("djangooidc.views._initialize_client") as mock_init: + mock_init.side_effect = InternalError + # TEST + # test the login callback url + response = self.client.get(reverse("openid_login_callback")) + # ASSERTIONS + # assert that the 500 error page is raised + self.assertEqual(response.status_code, 500) + self.assertTemplateUsed(response, "500.html") + self.assertIn("Server error", response.content.decode("utf-8")) + + def test_login_callback_initializes_client_and_succeeds(self, mock_client): + """Test that openid re-initializes the client when the client had not + been previously initiated.""" + with less_console_noise(): + # SETUP + session = self.client.session + session.save() + # MOCK + # mock that callback returns user_info; this is the expected behavior + mock_client.callback.side_effect = self.user_info + # patch that the request does not require step up auth + with patch("djangooidc.views._requires_step_up_auth", return_value=False): + with patch("djangooidc.views._initialize_client") as mock_init_client: + with patch("djangooidc.views._client_is_none") as mock_client_is_none: + # mock the client to initially be None + mock_client_is_none.return_value = True + # TEST + # test the login callback url + response = self.client.get(reverse("openid_login_callback")) + # ASSERTIONS + # assert that _initialize_client was called + mock_init_client.assert_called_once() + # assert that redirect is to / when no 'next' is set + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/") + def test_login_callback_no_step_up_auth(self, mock_client): """Walk through login_callback when _requires_step_up_auth returns False and assert that we have a redirect to /""" @@ -187,7 +235,7 @@ class ViewsTest(TestCase): self.assertEqual(response.status_code, 302) self.assertEqual(response.url, "/") - def test_requires_step_up_auth(self, mock_client): + def test_login_callback_requires_step_up_auth(self, mock_client): """Invoke login_callback passing it a request when _requires_step_up_auth returns True and assert that session is updated and create_authn_request (mock) is called.""" with less_console_noise(): @@ -213,7 +261,7 @@ class ViewsTest(TestCase): # And create_authn_request was called again mock_create_authn_request.assert_called_once() - def test_does_not_requires_step_up_auth(self, mock_client): + def test_login_callback_does_not_requires_step_up_auth(self, mock_client): """Invoke login_callback passing it a request when _requires_step_up_auth returns False and assert that session is not updated and create_authn_request (mock) is not called. diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index 0ba75b2e2..0f5da01bf 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -27,11 +27,13 @@ def _initialize_client(): CLIENT = Client(OP) logger.debug("Client initialized: %s" % CLIENT) + def _client_is_none(): - """ Return if the CLIENT is currently None.""" + """Return if the CLIENT is currently None.""" global CLIENT return CLIENT is None + # Initialize CLIENT try: _initialize_client() From 77d88dd82f9c8b2819033a72795cb425506234ce Mon Sep 17 00:00:00 2001 From: Michelle Rago <60157596+michelle-rago@users.noreply.github.com> Date: Fri, 9 Feb 2024 16:39:06 -0500 Subject: [PATCH 093/119] Change "Add user" to "Add a domain manager" (#1761) --- src/registrar/templates/domain_add_user.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_add_user.html b/src/registrar/templates/domain_add_user.html index d67c343a6..65290832d 100644 --- a/src/registrar/templates/domain_add_user.html +++ b/src/registrar/templates/domain_add_user.html @@ -18,7 +18,7 @@ + >Add a domain manager {% endblock %} {# domain_content #} From 8ed88aa137b5093b323cd1bc4102e7ee14d9af12 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 12 Feb 2024 10:02:55 -0800 Subject: [PATCH 094/119] Add conditionals for domain managers --- src/registrar/utility/csv_export.py | 53 ++++++++++++++++------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 64afd2d06..fc3faae05 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -14,14 +14,14 @@ from registrar.utility.enums import DefaultEmail logger = logging.getLogger(__name__) -def write_header(writer, columns, max_dm_count): +def write_header(writer, columns, max_dm_count, get_domain_managers): """ Receives params from the parent methods and outputs a CSV with a header row. Works with write_header as long as the same writer object is passed. """ - - for i in range(1, max_dm_count + 1): - columns.append(f"Domain manager email {i}") + if get_domain_managers: + for i in range(1, max_dm_count + 1): + columns.append(f"Domain manager email {i}") writer.writerow("hello") writer.writerow(columns) @@ -48,7 +48,7 @@ def get_domain_infos(filter_condition, sort_fields): return domain_infos_cleaned -def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None): +def parse_row(columns, domain_info: DomainInformation, get_domain_managers, security_emails_dict=None): """Given a set of columns, generate a new row from cleaned column data""" # Domain should never be none when parsing this information @@ -97,12 +97,13 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None "Deleted": domain.deleted, } - # Get each domain managers email and add to list - dm_emails = [dm.email for dm in domain.permissions] + if get_domain_managers: + # Get each domain managers email and add to list + dm_emails = [dm.email for dm in domain.permissions] - # Matching header for domain managers to be dynamic - for i, dm_email in enumerate(dm_emails, start=1): - FIELDS[f"Domain Manager email {i}":dm_email] + # Matching header for domain managers to be dynamic + for i, dm_email in enumerate(dm_emails, start=1): + FIELDS[f"Domain Manager email {i}":dm_email] row = [FIELDS.get(column, "") for column in columns] return row @@ -113,12 +114,17 @@ def write_body( columns, sort_fields, filter_condition, + get_domain_managers=False, ): """ Receives params from the parent methods and outputs a CSV with fltered and sorted domains. Works with write_header as longas the same writer object is passed. """ + # We only want to write the domain manager information for export_thing_here so we have to make it conditional + + # Trying to make the domain managers logic conditional + # Get the domainInfos all_domain_infos = get_domain_infos(filter_condition, sort_fields) @@ -151,12 +157,12 @@ def write_body( rows = [] for domain_info in page.object_list: # Get count of all the domain managers for an account - dm_count = len(domain_info.domain.permissions) - if dm_count > max_dm_count: - max_dm_count = dm_count - + if get_domain_managers: + dm_count = len(domain_info.domain.permissions) + if dm_count > max_dm_count: + max_dm_count = dm_count try: - row = parse_row(columns, domain_info, security_emails_dict) + row = parse_row(columns, domain_info, security_emails_dict, get_domain_managers) rows.append(row) except ValueError: # This should not happen. If it does, just skip this row. @@ -165,8 +171,8 @@ def write_body( continue # We only want this to run once just for the column header - if paginator_ran is False: - write_header(writer, columns, max_dm_count) + if paginator_ran is False and "Domain name" in columns: + write_header(writer, columns, max_dm_count, get_domain_managers) writer.writerows(rows) paginator_ran = True @@ -189,14 +195,9 @@ def export_data_type_to_csv(csv_file): "AO", "AO email", "Security contact email", - "Domain Manager email", + # For domain manager we are pass it in as a parameter below in write_body ] - # STUCK HERE - - # So the problem is we don't even have access to domains or a count here. - # We could pass it in, but it's messy. Maybe helper function? Seems repetitive - # Coalesce is used to replace federal_type of None with ZZZZZ sort_fields = [ "organization_type", @@ -212,7 +213,7 @@ def export_data_type_to_csv(csv_file): ], } # write_header(writer, columns) - write_body(writer, columns, sort_fields, filter_condition) + write_body(writer, columns, sort_fields, filter_condition, True) def export_data_full_to_csv(csv_file): @@ -345,5 +346,9 @@ def export_data_growth_to_csv(csv_file, start_date, end_date): } # write_header(writer, columns) + # Domains that got created write_body(writer, columns, sort_fields, filter_condition) + # Domains that got deleted + # Have a way to skip the header for this one + write_body(writer, columns, sort_fields_for_deleted_domains, filter_condition_for_deleted_domains) From ee76372a19ee90aed70cfa47745527037d99de26 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 12 Feb 2024 10:16:45 -0800 Subject: [PATCH 095/119] Add comment to jumpstart deploy --- src/registrar/utility/csv_export.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index fa252112d..9ffbb057c 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -19,11 +19,13 @@ def write_header(writer, columns, max_dm_count, get_domain_managers): Receives params from the parent methods and outputs a CSV with a header row. Works with write_header as long as the same writer object is passed. """ + + # If we have domain managers, set column title dynamically here if get_domain_managers: for i in range(1, max_dm_count + 1): columns.append(f"Domain manager email {i}") - writer.writerow("hello") + writer.writerow("hellotesting123") writer.writerow(columns) @@ -96,7 +98,7 @@ def parse_row(columns, domain_info: DomainInformation, get_domain_managers, secu "First ready": domain.first_ready, "Deleted": domain.deleted, } - + if get_domain_managers: # Get each domain managers email and add to list dm_emails = [dm.email for dm in domain.permissions] From eb89ceb9f3f62f25cb15de72a66ee2528ed16490 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 12 Feb 2024 13:13:24 -0700 Subject: [PATCH 096/119] Update domain.py --- src/registrar/views/domain.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 04fe1ce3a..42db3b677 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -791,10 +791,11 @@ class DomainInvitationDeleteView(DomainInvitationPermissionDeleteView, SuccessMe object: DomainInvitation # workaround for type mismatch in DeleteView def get_success_url(self): + messages.success(self.request, self.get_success_message()) return reverse("domain-users", kwargs={"pk": self.object.domain.id}) - def get_success_message(self, cleaned_data): - return f"Successfully canceled invitation for {self.object.email}." + def get_success_message(self): + return f"Canceled invitation to {self.object.email}." class DomainDeleteUserView(UserDomainRolePermissionDeleteView): From b3abe0b031568d1b1915c3cbe11b640ff7f9d581 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 12 Feb 2024 14:00:20 -0700 Subject: [PATCH 097/119] PR suggestions --- src/registrar/templates/domain_detail.html | 4 +++- src/registrar/templates/home.html | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index ba891da57..2b2d45695 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -17,6 +17,7 @@ Status: + {# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #} {% if domain.is_expired and domain.state != domain.State.UNKNOWN %} Expired @@ -25,8 +26,9 @@ {% else %} {{ domain.state|title }} {% endif %} + {% if domain.get_state_help_text %} -
+
{{ domain.get_state_help_text }}
{% endif %} diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index b5e8ca5a4..b4f3b99f8 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -62,6 +62,7 @@ title="{{domain.get_state_help_text}}" focusable="true" aria-label="Status Information" + labelledby="Status Information" > From 3a595cf01ff419042ddf61d22912a2fa30c01a80 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 12 Feb 2024 14:34:44 -0700 Subject: [PATCH 098/119] VO changes --- src/registrar/templates/home.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index b4f3b99f8..a79065f50 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -62,9 +62,9 @@ title="{{domain.get_state_help_text}}" focusable="true" aria-label="Status Information" - labelledby="Status Information" + role="tooltip" > - + From 5412933d4d9158db0c51ea866f71041391e2e3e5 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 12 Feb 2024 15:24:55 -0700 Subject: [PATCH 099/119] Update domain.py --- src/registrar/views/domain.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 42db3b677..c7981f617 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -787,14 +787,13 @@ class DomainAddUserView(DomainFormBaseView): return redirect(self.get_success_url()) -class DomainInvitationDeleteView(DomainInvitationPermissionDeleteView, SuccessMessageMixin): +class DomainInvitationDeleteView(SuccessMessageMixin, DomainInvitationPermissionDeleteView): object: DomainInvitation # workaround for type mismatch in DeleteView def get_success_url(self): - messages.success(self.request, self.get_success_message()) return reverse("domain-users", kwargs={"pk": self.object.domain.id}) - def get_success_message(self): + def get_success_message(self, cleaned_data): return f"Canceled invitation to {self.object.email}." From f1f91669d7f64bd43b60e566ce504dc61b492339 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 12 Feb 2024 15:02:36 -0800 Subject: [PATCH 100/119] Fix logic --- src/registrar/utility/csv_export.py | 53 +++++++++++------------------ 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 9ffbb057c..242c997de 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -14,18 +14,12 @@ from registrar.utility.enums import DefaultEmail logger = logging.getLogger(__name__) -def write_header(writer, columns, max_dm_count, get_domain_managers): +def write_header(writer, columns): """ Receives params from the parent methods and outputs a CSV with a header row. Works with write_header as long as the same writer object is passed. """ - # If we have domain managers, set column title dynamically here - if get_domain_managers: - for i in range(1, max_dm_count + 1): - columns.append(f"Domain manager email {i}") - - writer.writerow("hellotesting123") writer.writerow(columns) @@ -50,7 +44,7 @@ def get_domain_infos(filter_condition, sort_fields): return domain_infos_cleaned -def parse_row(columns, domain_info: DomainInformation, get_domain_managers, security_emails_dict=None): +def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None, get_domain_managers=False): """Given a set of columns, generate a new row from cleaned column data""" # Domain should never be none when parsing this information @@ -101,11 +95,11 @@ def parse_row(columns, domain_info: DomainInformation, get_domain_managers, secu if get_domain_managers: # Get each domain managers email and add to list - dm_emails = [dm.email for dm in domain.permissions] + dm_emails = [dm.user.email for dm in domain.permissions.all()] - # Matching header for domain managers to be dynamic + # This is the row fields for i, dm_email in enumerate(dm_emails, start=1): - FIELDS[f"Domain Manager email {i}":dm_email] + FIELDS[f"Domain manager email {i}"] = dm_email row = [FIELDS.get(column, "") for column in columns] return row @@ -117,6 +111,7 @@ def write_body( sort_fields, filter_condition, get_domain_managers=False, + should_write_header=True, ): """ Receives params from the parent methods and outputs a CSV with fltered and sorted domains. @@ -151,18 +146,19 @@ def write_body( # We get the max so we can set the column header accurately max_dm_count = 0 paginator_ran = False - # Reduce the memory overhead when performing the write operation paginator = Paginator(all_domain_infos, 1000) for page_num in paginator.page_range: page = paginator.page(page_num) rows = [] for domain_info in page.object_list: - # Get count of all the domain managers for an account if get_domain_managers: - dm_count = len(domain_info.domain.permissions) + dm_count = len(domain_info.domain.permissions.all()) if dm_count > max_dm_count: max_dm_count = dm_count + for i in range(1, max_dm_count + 1): + if f"Domain manager email {i}" not in columns: + columns.append(f"Domain manager email {i}") try: row = parse_row(columns, domain_info, security_emails_dict, get_domain_managers) rows.append(row) @@ -171,13 +167,12 @@ def write_body( # It indicates that DomainInformation.domain is None. logger.error("csv_export -> Error when parsing row, domain was None") continue + # We only want this to run once just for the column header + if paginator_ran is False and should_write_header: + write_header(writer, columns) - # We only want this to run once just for the column header - if paginator_ran is False and "Domain name" in columns: - write_header(writer, columns, max_dm_count, get_domain_managers) - - writer.writerows(rows) - paginator_ran = True + writer.writerows(rows) + paginator_ran = True def export_data_type_to_csv(csv_file): @@ -214,8 +209,7 @@ def export_data_type_to_csv(csv_file): Domain.State.ON_HOLD, ], } - # write_header(writer, columns) - write_body(writer, columns, sort_fields, filter_condition, True) + write_body(writer, columns, sort_fields, filter_condition, True, True) def export_data_full_to_csv(csv_file): @@ -246,8 +240,7 @@ def export_data_full_to_csv(csv_file): Domain.State.ON_HOLD, ], } - # write_header(writer, columns) - write_body(writer, columns, sort_fields, filter_condition) + write_body(writer, columns, sort_fields, filter_condition, False, True) def export_data_federal_to_csv(csv_file): @@ -279,8 +272,7 @@ def export_data_federal_to_csv(csv_file): Domain.State.ON_HOLD, ], } - # write_header(writer, columns) - write_body(writer, columns, sort_fields, filter_condition) + write_body(writer, columns, sort_fields, filter_condition, False, True) def get_default_start_date(): @@ -347,10 +339,5 @@ def export_data_growth_to_csv(csv_file, start_date, end_date): "domain__deleted__gte": start_date_formatted, } - # write_header(writer, columns) - # Domains that got created - write_body(writer, columns, sort_fields, filter_condition) - # Domains that got deleted - # Have a way to skip the header for this one - - write_body(writer, columns, sort_fields_for_deleted_domains, filter_condition_for_deleted_domains) + write_body(writer, columns, sort_fields, filter_condition, False, True) + write_body(writer, columns, sort_fields_for_deleted_domains, filter_condition_for_deleted_domains, False, False) From 3c48fb9f7e54de29a603211bae8abe7faaa49041 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 12 Feb 2024 15:26:31 -0800 Subject: [PATCH 101/119] Refactor parts of the code --- src/registrar/utility/csv_export.py | 66 +++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 242c997de..1c8f365cf 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -105,6 +105,51 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None return row +# def _check_domain_managers(domain_info, columns): +# max_dm_count = 0 + +# dm_count = len(domain_info.domain.permissions.all()) +# if dm_count > max_dm_count: +# max_dm_count = dm_count +# for i in range(1, max_dm_count + 1): +# if f"Domain manager email {i}" not in columns: +# columns.append(f"Domain manager email {i}") + +# return columns + + +def _get_security_emails(sec_contact_ids): + """ + Retrieve security contact emails for the given security contact IDs. + """ + security_emails_dict = {} + public_contacts = ( + PublicContact.objects.only("email", "domain__name") + .select_related("domain") + .filter(registry_id__in=sec_contact_ids) + ) + + # Populate a dictionary of domain names and their security contacts + for contact in public_contacts: + domain: Domain = contact.domain + if domain is not None and domain.name not in security_emails_dict: + security_emails_dict[domain.name] = contact.email + else: + logger.warning("csv_export -> Domain was none for PublicContact") + + return security_emails_dict + + +def update_columns_with_domain_managers(columns, max_dm_count): + """ + Update the columns list to include "Domain manager email" headers + based on the maximum domain manager count. + """ + for i in range(1, max_dm_count + 1): + if f"Domain manager email {i}" not in columns: + columns.append(f"Domain manager email {i}") + + def write_body( writer, columns, @@ -127,24 +172,12 @@ def write_body( # Store all security emails to avoid epp calls or excessive filters sec_contact_ids = all_domain_infos.values_list("domain__security_contact_registry_id", flat=True) - security_emails_dict = {} - public_contacts = ( - PublicContact.objects.only("email", "domain__name") - .select_related("domain") - .filter(registry_id__in=sec_contact_ids) - ) - # Populate a dictionary of domain names and their security contacts - for contact in public_contacts: - domain: Domain = contact.domain - if domain is not None and domain.name not in security_emails_dict: - security_emails_dict[domain.name] = contact.email - else: - logger.warning("csv_export -> Domain was none for PublicContact") + security_emails_dict = _get_security_emails(sec_contact_ids) # The maximum amount of domain managers an account has - # We get the max so we can set the column header accurately max_dm_count = 0 + # We get the max so we can set the column header accurately paginator_ran = False # Reduce the memory overhead when performing the write operation paginator = Paginator(all_domain_infos, 1000) @@ -153,12 +186,11 @@ def write_body( rows = [] for domain_info in page.object_list: if get_domain_managers: + # _check_domain_managers(domain_info, columns) dm_count = len(domain_info.domain.permissions.all()) if dm_count > max_dm_count: max_dm_count = dm_count - for i in range(1, max_dm_count + 1): - if f"Domain manager email {i}" not in columns: - columns.append(f"Domain manager email {i}") + update_columns_with_domain_managers(columns, max_dm_count) try: row = parse_row(columns, domain_info, security_emails_dict, get_domain_managers) rows.append(row) From 8ea135b5a4b8aff3baf15dfe21cf640506976e3e Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 12 Feb 2024 16:48:33 -0800 Subject: [PATCH 102/119] Update unit tests --- src/registrar/tests/test_reports.py | 84 +++++++++++++++++++++++++---- src/registrar/utility/csv_export.py | 14 ----- 2 files changed, 75 insertions(+), 23 deletions(-) diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index 630904218..3ea6b3695 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -7,13 +7,14 @@ from registrar.models.domain import Domain from registrar.models.public_contact import PublicContact from registrar.models.user import User from django.contrib.auth import get_user_model +from registrar.models.user_domain_role import UserDomainRole from registrar.tests.common import MockEppLib from registrar.utility.csv_export import ( - write_header, write_body, get_default_start_date, get_default_end_date, ) + from django.core.management import call_command from unittest.mock import MagicMock, call, mock_open, patch from api.views import get_current_federal, get_current_full @@ -336,11 +337,28 @@ class ExportDataTest(MockEppLib): federal_agency="Armed Forces Retirement Home", ) + meoward_user = get_user_model().objects.create( + username="meoward_username", first_name="first_meoward", last_name="last_meoward", email="meoward@rocks.com" + ) + + _, created = UserDomainRole.objects.get_or_create( + user=meoward_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER + ) + + _, created = UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER + ) + + _, created = UserDomainRole.objects.get_or_create( + user=meoward_user, domain=self.domain_2, role=UserDomainRole.Roles.MANAGER + ) + def tearDown(self): PublicContact.objects.all().delete() Domain.objects.all().delete() DomainInformation.objects.all().delete() User.objects.all().delete() + UserDomainRole.objects.all().delete() super().tearDown() def test_export_domains_to_writer_security_emails(self): @@ -383,7 +401,6 @@ class ExportDataTest(MockEppLib): } self.maxDiff = None # Call the export functions - write_header(writer, columns) write_body(writer, columns, sort_fields, filter_condition) # Reset the CSV file's position to the beginning csv_file.seek(0) @@ -440,7 +457,6 @@ class ExportDataTest(MockEppLib): ], } # Call the export functions - write_header(writer, columns) write_body(writer, columns, sort_fields, filter_condition) # Reset the CSV file's position to the beginning csv_file.seek(0) @@ -489,7 +505,6 @@ class ExportDataTest(MockEppLib): ], } # Call the export functions - write_header(writer, columns) write_body(writer, columns, sort_fields, filter_condition) # Reset the CSV file's position to the beginning csv_file.seek(0) @@ -567,7 +582,6 @@ class ExportDataTest(MockEppLib): } # Call the export functions - write_header(writer, columns) write_body( writer, columns, @@ -575,10 +589,7 @@ class ExportDataTest(MockEppLib): filter_condition, ) write_body( - writer, - columns, - sort_fields_for_deleted_domains, - filter_conditions_for_deleted_domains, + writer, columns, sort_fields_for_deleted_domains, filter_conditions_for_deleted_domains, False, False ) # Reset the CSV file's position to the beginning @@ -606,6 +617,61 @@ class ExportDataTest(MockEppLib): self.assertEqual(csv_content, expected_content) + def test_export_domains_to_writer_domain_managers(self): + """Test that export_domains_to_writer returns the + expected domain managers""" + with less_console_noise(): + # Create a CSV file in memory + csv_file = StringIO() + writer = csv.writer(csv_file) + # Define columns, sort fields, and filter condition + + columns = [ + "Domain name", + "Status", + "Expiration date", + "Domain type", + "Agency", + "Organization name", + "City", + "State", + "AO", + "AO email", + "Security contact email", + ] + sort_fields = ["domain__name"] + filter_condition = { + "domain__state__in": [ + Domain.State.READY, + Domain.State.DNS_NEEDED, + Domain.State.ON_HOLD, + ], + } + self.maxDiff = None + # Call the export functions + write_body(writer, columns, sort_fields, filter_condition, True, True) + # Reset the CSV file's position to the beginning + csv_file.seek(0) + # Read the content into a variable + csv_content = csv_file.read() + # We expect READY domains, + # sorted alphabetially by domain name + expected_content = ( + "Domain name,Status,Expiration date,Domain type,Agency," + "Organization name,City,State,AO,AO email," + "Security contact email,Domain manager email 1,Domain manager email 2,\n" + "adomain10.gov,Ready,,Federal,Armed Forces Retirement Home,,,, , ,\n" + "adomain2.gov,Dns needed,,Interstate,,,,, , , ,meoward@rocks.com\n" + "cdomain1.gov,Ready,,Federal - Executive,World War I Centennial Commission,,," + ", , , ,meoward@rocks.com,info@example.com\n" + "ddomain3.gov,On hold,,Federal,Armed Forces Retirement Home,,,, , , ,,\n" + ) + # Normalize line endings and remove commas, + # spaces and leading/trailing whitespace + csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() + expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() + self.assertEqual(csv_content, expected_content) + class HelperFunctions(TestCase): """This asserts that 1=1. Its limited usefulness lies in making sure the helper methods stay healthy.""" diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 1c8f365cf..806e72952 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -105,19 +105,6 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None return row -# def _check_domain_managers(domain_info, columns): -# max_dm_count = 0 - -# dm_count = len(domain_info.domain.permissions.all()) -# if dm_count > max_dm_count: -# max_dm_count = dm_count -# for i in range(1, max_dm_count + 1): -# if f"Domain manager email {i}" not in columns: -# columns.append(f"Domain manager email {i}") - -# return columns - - def _get_security_emails(sec_contact_ids): """ Retrieve security contact emails for the given security contact IDs. @@ -186,7 +173,6 @@ def write_body( rows = [] for domain_info in page.object_list: if get_domain_managers: - # _check_domain_managers(domain_info, columns) dm_count = len(domain_info.domain.permissions.all()) if dm_count > max_dm_count: max_dm_count = dm_count From d329ae6c8f1167f0100e832858e6c63ed17d489a Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Mon, 12 Feb 2024 17:46:24 -0800 Subject: [PATCH 103/119] Update comments --- src/registrar/templates/home.html | 2 +- src/registrar/utility/csv_export.py | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 2f4f6d2e9..9dd093de9 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -13,7 +13,7 @@ {% block messages %} {% include "includes/form_messages.html" %} {% endblock %} -

Manage your domains - TEST TEST 123

+

Manage your domains

diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 806e72952..af4162489 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -97,7 +97,7 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None # Get each domain managers email and add to list dm_emails = [dm.user.email for dm in domain.permissions.all()] - # This is the row fields + # Set up the "matching header" + row field data for i, dm_email in enumerate(dm_emails, start=1): FIELDS[f"Domain manager email {i}"] = dm_email @@ -129,7 +129,7 @@ def _get_security_emails(sec_contact_ids): def update_columns_with_domain_managers(columns, max_dm_count): """ - Update the columns list to include "Domain manager email" headers + Update the columns list to include "Domain manager email {#}" headers based on the maximum domain manager count. """ for i in range(1, max_dm_count + 1): @@ -148,13 +148,10 @@ def write_body( """ Receives params from the parent methods and outputs a CSV with fltered and sorted domains. Works with write_header as longas the same writer object is passed. + get_domain_managers: Conditional bc we only use domain manager info for export_data_full_to_csv + should_write_header: Conditional bc export_data_growth_to_csv calls write_body twice """ - # We only want to write the domain manager information for export_thing_here so we have to make it conditional - - # Trying to make the domain managers logic conditional - - # Get the domainInfos all_domain_infos = get_domain_infos(filter_condition, sort_fields) # Store all security emails to avoid epp calls or excessive filters @@ -163,8 +160,9 @@ def write_body( security_emails_dict = _get_security_emails(sec_contact_ids) # The maximum amount of domain managers an account has - max_dm_count = 0 # We get the max so we can set the column header accurately + max_dm_count = 0 + # Flag bc we don't want to set header every loop paginator_ran = False # Reduce the memory overhead when performing the write operation paginator = Paginator(all_domain_infos, 1000) @@ -185,7 +183,6 @@ def write_body( # It indicates that DomainInformation.domain is None. logger.error("csv_export -> Error when parsing row, domain was None") continue - # We only want this to run once just for the column header if paginator_ran is False and should_write_header: write_header(writer, columns) From 144140c2fdb18925030b53675560d5f34497ac6a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 13 Feb 2024 08:03:17 -0700 Subject: [PATCH 104/119] Linting --- src/registrar/tests/test_views_application.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/tests/test_views_application.py b/src/registrar/tests/test_views_application.py index 0a596a148..593b8d31e 100644 --- a/src/registrar/tests/test_views_application.py +++ b/src/registrar/tests/test_views_application.py @@ -18,7 +18,6 @@ from registrar.models import ( User, Website, UserDomainRole, - DraftDomain, ) from registrar.views.application import ApplicationWizard, Step From 69f6f4db0cc5f82ea3f76e12a07c9fede819c7f8 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 13 Feb 2024 10:37:34 -0500 Subject: [PATCH 105/119] added a debug message when client needs to be re-initialized --- src/djangooidc/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index 0f5da01bf..444b8b950 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -78,6 +78,7 @@ def openid(request): try: # If the CLIENT is none, attempt to reinitialize before handling the request if _client_is_none(): + logger.debug("OIDC client is None, attempting to initialize") _initialize_client() request.session["acr_value"] = CLIENT.get_default_acr_value() request.session["next"] = request.GET.get("next", "/") @@ -93,6 +94,7 @@ def login_callback(request): try: # If the CLIENT is none, attempt to reinitialize before handling the request if _client_is_none(): + logger.debug("OIDC client is None, attempting to initialize") _initialize_client() query = parse_qs(request.GET.urlencode()) userinfo = CLIENT.callback(query, request.session) From 166135a2362a9b8bf3d74873f1a2f802d3e5a3a1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 13 Feb 2024 10:35:38 -0700 Subject: [PATCH 106/119] Add comment --- src/registrar/views/domain.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index c7981f617..653eb8661 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -786,7 +786,9 @@ class DomainAddUserView(DomainFormBaseView): return redirect(self.get_success_url()) - +# The order of the superclasses matters here. BaseDeleteView has a bug where the +# "form_valid" function does not call super, so it cannot use SuccessMessageMixin. +# The workaround is to use SuccessMessageMixin first. class DomainInvitationDeleteView(SuccessMessageMixin, DomainInvitationPermissionDeleteView): object: DomainInvitation # workaround for type mismatch in DeleteView From 5cf18eb5991d8a4eab45395b3ebfa8c3ad97b68a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 13 Feb 2024 10:39:18 -0700 Subject: [PATCH 107/119] Lint --- src/registrar/views/domain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 653eb8661..25625dd82 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -786,7 +786,8 @@ class DomainAddUserView(DomainFormBaseView): return redirect(self.get_success_url()) -# The order of the superclasses matters here. BaseDeleteView has a bug where the + +# The order of the superclasses matters here. BaseDeleteView has a bug where the # "form_valid" function does not call super, so it cannot use SuccessMessageMixin. # The workaround is to use SuccessMessageMixin first. class DomainInvitationDeleteView(SuccessMessageMixin, DomainInvitationPermissionDeleteView): From 6e062e841fc9f085d6f05a9a6c9743a237551e52 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 13 Feb 2024 10:25:41 -0800 Subject: [PATCH 108/119] Update test comments --- src/registrar/tests/test_reports.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index 3ea6b3695..386acb253 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -341,6 +341,7 @@ class ExportDataTest(MockEppLib): username="meoward_username", first_name="first_meoward", last_name="last_meoward", email="meoward@rocks.com" ) + # Test for more than 1 domain manager _, created = UserDomainRole.objects.get_or_create( user=meoward_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER ) @@ -349,6 +350,7 @@ class ExportDataTest(MockEppLib): user=self.user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER ) + # Test for just 1 domain manager _, created = UserDomainRole.objects.get_or_create( user=meoward_user, domain=self.domain_2, role=UserDomainRole.Roles.MANAGER ) From 3387ec032bf0ed715236865996d2b7cd8edc7be6 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 14 Feb 2024 12:09:27 -0500 Subject: [PATCH 109/119] handle logout when no session is present --- src/djangooidc/tests/test_views.py | 20 ++++++++++++++++++++ src/djangooidc/views.py | 6 +++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/djangooidc/tests/test_views.py b/src/djangooidc/tests/test_views.py index 4cd2241e3..462917947 100644 --- a/src/djangooidc/tests/test_views.py +++ b/src/djangooidc/tests/test_views.py @@ -327,6 +327,26 @@ class ViewsTest(TestCase): self.assertEqual(response.status_code, 302) self.assertEqual(actual, expected) + def test_logout_redirect_url_with_no_session_state(self, mock_client): + """Test that logout redirects to the configured post_logout_redirect_uris.""" + with less_console_noise(): + # MOCK + mock_client.callback.side_effect = self.user_info + mock_client.registration_response = {"post_logout_redirect_uris": ["http://example.com/back"]} + mock_client.provider_info = {"end_session_endpoint": "http://example.com/log_me_out"} + mock_client.client_id = "TEST" + # TEST + with less_console_noise(): + response = self.client.get(reverse("logout")) + # ASSERTIONS + expected = ( + "http://example.com/log_me_out?client_id=TEST" + "&post_logout_redirect_uri=http%3A%2F%2Fexample.com%2Fback" + ) + actual = response.url + self.assertEqual(response.status_code, 302) + self.assertEqual(actual, expected) + @patch("djangooidc.views.auth_logout") def test_logout_always_logs_out(self, mock_logout, _): """Without additional mocking, logout will always fail. diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index 444b8b950..2d3c842d2 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -145,8 +145,12 @@ def logout(request, next_page=None): user = request.user request_args = { "client_id": CLIENT.client_id, - "state": request.session["state"], } + # if state is not in request session, still redirect to the identity + # provider's logout url, but don't include the state in the url; this + # will successfully log out of the identity provider + if "state" in request.session: + request_args["state"] = request.session["state"] if ( "post_logout_redirect_uris" in CLIENT.registration_response.keys() and len(CLIENT.registration_response["post_logout_redirect_uris"]) > 0 From 2ad8b2e2ec72e5fefacb47bdeb058c7f7b83fdcc Mon Sep 17 00:00:00 2001 From: Michelle Rago <60157596+michelle-rago@users.noreply.github.com> Date: Wed, 14 Feb 2024 12:51:42 -0500 Subject: [PATCH 110/119] Update button text for adding a domain manager (#1776) --- src/registrar/templates/domain_add_user.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_add_user.html b/src/registrar/templates/domain_add_user.html index 65290832d..b2f9fef24 100644 --- a/src/registrar/templates/domain_add_user.html +++ b/src/registrar/templates/domain_add_user.html @@ -18,7 +18,7 @@ + >Add domain manager {% endblock %} {# domain_content #} From a8857ef18e917b9b81b7eefe23921d3751ec4669 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 14 Feb 2024 13:09:25 -0800 Subject: [PATCH 111/119] Address refactor feedback --- src/registrar/utility/csv_export.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index af4162489..1c2b64f3d 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -133,8 +133,7 @@ def update_columns_with_domain_managers(columns, max_dm_count): based on the maximum domain manager count. """ for i in range(1, max_dm_count + 1): - if f"Domain manager email {i}" not in columns: - columns.append(f"Domain manager email {i}") + columns.append(f"Domain manager email {i}") def write_body( @@ -159,22 +158,19 @@ def write_body( security_emails_dict = _get_security_emails(sec_contact_ids) - # The maximum amount of domain managers an account has - # We get the max so we can set the column header accurately - max_dm_count = 0 - # Flag bc we don't want to set header every loop - paginator_ran = False # Reduce the memory overhead when performing the write operation paginator = Paginator(all_domain_infos, 1000) + + if get_domain_managers: + # We want to get the max amont of domain managers an + # account has to set the column header dynamically + max_dm_count = max(len(domain_info.domain.permissions.all()) for domain_info in all_domain_infos) + update_columns_with_domain_managers(columns, max_dm_count) + for page_num in paginator.page_range: page = paginator.page(page_num) rows = [] for domain_info in page.object_list: - if get_domain_managers: - dm_count = len(domain_info.domain.permissions.all()) - if dm_count > max_dm_count: - max_dm_count = dm_count - update_columns_with_domain_managers(columns, max_dm_count) try: row = parse_row(columns, domain_info, security_emails_dict, get_domain_managers) rows.append(row) @@ -183,11 +179,10 @@ def write_body( # It indicates that DomainInformation.domain is None. logger.error("csv_export -> Error when parsing row, domain was None") continue - if paginator_ran is False and should_write_header: + if should_write_header: write_header(writer, columns) writer.writerows(rows) - paginator_ran = True def export_data_type_to_csv(csv_file): @@ -222,6 +217,7 @@ def export_data_type_to_csv(csv_file): Domain.State.READY, Domain.State.DNS_NEEDED, Domain.State.ON_HOLD, + Domain.State.UNKNOWN, # REMOVE ], } write_body(writer, columns, sort_fields, filter_condition, True, True) From 2dbf9cfa84d7a1847d0e8a9567ee215d5e3be866 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 14 Feb 2024 13:33:41 -0800 Subject: [PATCH 112/119] Remove extra test line --- src/registrar/utility/csv_export.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 1c2b64f3d..85bc00e33 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -217,7 +217,6 @@ def export_data_type_to_csv(csv_file): Domain.State.READY, Domain.State.DNS_NEEDED, Domain.State.ON_HOLD, - Domain.State.UNKNOWN, # REMOVE ], } write_body(writer, columns, sort_fields, filter_condition, True, True) From 22cefd6ed8caf85f7709f029da0955b3ab470568 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 14 Feb 2024 13:45:33 -0800 Subject: [PATCH 113/119] Add parameter logic --- src/registrar/tests/test_reports.py | 28 ++++++++++++++++++++++------ src/registrar/utility/csv_export.py | 17 ++++++++++++----- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index 386acb253..fa5acc96d 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -403,7 +403,10 @@ class ExportDataTest(MockEppLib): } self.maxDiff = None # Call the export functions - write_body(writer, columns, sort_fields, filter_condition) + write_body( + writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True + ) + # Reset the CSV file's position to the beginning csv_file.seek(0) # Read the content into a variable @@ -459,7 +462,9 @@ class ExportDataTest(MockEppLib): ], } # Call the export functions - write_body(writer, columns, sort_fields, filter_condition) + write_body( + writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True + ) # Reset the CSV file's position to the beginning csv_file.seek(0) # Read the content into a variable @@ -507,7 +512,9 @@ class ExportDataTest(MockEppLib): ], } # Call the export functions - write_body(writer, columns, sort_fields, filter_condition) + write_body( + writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True + ) # Reset the CSV file's position to the beginning csv_file.seek(0) # Read the content into a variable @@ -589,11 +596,17 @@ class ExportDataTest(MockEppLib): columns, sort_fields, filter_condition, + get_domain_managers=False, + should_write_header=True, ) write_body( - writer, columns, sort_fields_for_deleted_domains, filter_conditions_for_deleted_domains, False, False + writer, + columns, + sort_fields_for_deleted_domains, + filter_conditions_for_deleted_domains, + get_domain_managers=False, + should_write_header=False, ) - # Reset the CSV file's position to the beginning csv_file.seek(0) @@ -651,7 +664,10 @@ class ExportDataTest(MockEppLib): } self.maxDiff = None # Call the export functions - write_body(writer, columns, sort_fields, filter_condition, True, True) + write_body( + writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True + ) + # Reset the CSV file's position to the beginning csv_file.seek(0) # Read the content into a variable diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 85bc00e33..1e4895f80 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -219,7 +219,7 @@ def export_data_type_to_csv(csv_file): Domain.State.ON_HOLD, ], } - write_body(writer, columns, sort_fields, filter_condition, True, True) + write_body(writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True) def export_data_full_to_csv(csv_file): @@ -250,7 +250,7 @@ def export_data_full_to_csv(csv_file): Domain.State.ON_HOLD, ], } - write_body(writer, columns, sort_fields, filter_condition, False, True) + write_body(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True) def export_data_federal_to_csv(csv_file): @@ -282,7 +282,7 @@ def export_data_federal_to_csv(csv_file): Domain.State.ON_HOLD, ], } - write_body(writer, columns, sort_fields, filter_condition, False, True) + write_body(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True) def get_default_start_date(): @@ -349,5 +349,12 @@ def export_data_growth_to_csv(csv_file, start_date, end_date): "domain__deleted__gte": start_date_formatted, } - write_body(writer, columns, sort_fields, filter_condition, False, True) - write_body(writer, columns, sort_fields_for_deleted_domains, filter_condition_for_deleted_domains, False, False) + write_body(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True) + write_body( + writer, + columns, + sort_fields_for_deleted_domains, + filter_condition_for_deleted_domains, + get_domain_managers=False, + should_write_header=False, + ) From da30c2dda04ae7ae4fb00d318d9c306cd8cf9103 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 14 Feb 2024 16:45:50 -0500 Subject: [PATCH 114/119] removed help_text from fields in contact model --- ...email_alter_contact_first_name_and_more.py | 45 +++++++++++++++++++ src/registrar/models/contact.py | 6 --- 2 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 src/registrar/migrations/0069_alter_contact_email_alter_contact_first_name_and_more.py diff --git a/src/registrar/migrations/0069_alter_contact_email_alter_contact_first_name_and_more.py b/src/registrar/migrations/0069_alter_contact_email_alter_contact_first_name_and_more.py new file mode 100644 index 000000000..5869e6fae --- /dev/null +++ b/src/registrar/migrations/0069_alter_contact_email_alter_contact_first_name_and_more.py @@ -0,0 +1,45 @@ +# Generated by Django 4.2.7 on 2024-02-14 21:45 + +from django.db import migrations, models +import phonenumber_field.modelfields + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0068_domainapplication_notes_domaininformation_notes"), + ] + + operations = [ + migrations.AlterField( + model_name="contact", + name="email", + field=models.EmailField(blank=True, db_index=True, max_length=254, null=True), + ), + migrations.AlterField( + model_name="contact", + name="first_name", + field=models.TextField(blank=True, db_index=True, null=True, verbose_name="first name / given name"), + ), + migrations.AlterField( + model_name="contact", + name="last_name", + field=models.TextField(blank=True, db_index=True, null=True, verbose_name="last name / family name"), + ), + migrations.AlterField( + model_name="contact", + name="middle_name", + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name="contact", + name="phone", + field=phonenumber_field.modelfields.PhoneNumberField( + blank=True, db_index=True, max_length=128, null=True, region=None + ), + ), + migrations.AlterField( + model_name="contact", + name="title", + field=models.TextField(blank=True, null=True, verbose_name="title or role in your organization"), + ), + ] diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index ff7389780..ca1ff74c4 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -19,38 +19,32 @@ class Contact(TimeStampedModel): first_name = models.TextField( null=True, blank=True, - help_text="First name", verbose_name="first name / given name", db_index=True, ) middle_name = models.TextField( null=True, blank=True, - help_text="Middle name (optional)", ) last_name = models.TextField( null=True, blank=True, - help_text="Last name", verbose_name="last name / family name", db_index=True, ) title = models.TextField( null=True, blank=True, - help_text="Title", verbose_name="title or role in your organization", ) email = models.EmailField( null=True, blank=True, - help_text="Email", db_index=True, ) phone = PhoneNumberField( null=True, blank=True, - help_text="Phone", db_index=True, ) From f5a1348ccb51546dad370c3ab91c58eb4fba705c Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 14 Feb 2024 17:11:55 -0500 Subject: [PATCH 115/119] updated comment --- src/djangooidc/tests/test_views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/djangooidc/tests/test_views.py b/src/djangooidc/tests/test_views.py index 462917947..0f734b80d 100644 --- a/src/djangooidc/tests/test_views.py +++ b/src/djangooidc/tests/test_views.py @@ -339,6 +339,7 @@ class ViewsTest(TestCase): with less_console_noise(): response = self.client.get(reverse("logout")) # ASSERTIONS + # Assert redirect code and url are accurate expected = ( "http://example.com/log_me_out?client_id=TEST" "&post_logout_redirect_uri=http%3A%2F%2Fexample.com%2Fback" From e5a8bb031b5f9d4f92d0d6bf07f91d0d41d25012 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 14 Feb 2024 15:40:09 -0700 Subject: [PATCH 116/119] Added migration step to deploy-stable and deploy-staging --- .github/workflows/deploy-stable.yaml | 8 ++++++++ .github/workflows/deploy-staging.yaml | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/.github/workflows/deploy-stable.yaml b/.github/workflows/deploy-stable.yaml index 1e643ef9a..0ded4a3a6 100644 --- a/.github/workflows/deploy-stable.yaml +++ b/.github/workflows/deploy-stable.yaml @@ -37,3 +37,11 @@ jobs: cf_org: cisa-dotgov cf_space: stable cf_manifest: "ops/manifests/manifest-stable.yaml" + - name: Run Django migrations + uses: cloud-gov/cg-cli-tools@main + with: + cf_username: ${{ secrets.CF_STABLE_USERNAME }} + cf_password: ${{ secrets.CF_STABLE_PASSWORD }} + cf_org: cisa-dotgov + cf_space: stable + cf_command: "run-task getgov-stable --command 'python manage.py migrate' --name migrate" \ No newline at end of file diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml index fa4543637..1df08f412 100644 --- a/.github/workflows/deploy-staging.yaml +++ b/.github/workflows/deploy-staging.yaml @@ -37,3 +37,11 @@ jobs: cf_org: cisa-dotgov cf_space: staging cf_manifest: "ops/manifests/manifest-staging.yaml" + - name: Run Django migrations + uses: cloud-gov/cg-cli-tools@main + with: + cf_username: ${{ secrets.CF_STAGING_USERNAME }} + cf_password: ${{ secrets.CF_STAGING_PASSWORD }} + cf_org: cisa-dotgov + cf_space: staging + cf_command: "run-task getgov-staging --command 'python manage.py migrate' --name migrate" From a66fb36432f3c028cfe5b92f342bd55973ee0103 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 15 Feb 2024 10:52:14 -0800 Subject: [PATCH 117/119] Update function naming and length check --- src/registrar/tests/test_reports.py | 16 ++++++++-------- src/registrar/utility/csv_export.py | 15 ++++++++------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index fa5acc96d..011c60b93 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -10,7 +10,7 @@ from django.contrib.auth import get_user_model from registrar.models.user_domain_role import UserDomainRole from registrar.tests.common import MockEppLib from registrar.utility.csv_export import ( - write_body, + write_csv, get_default_start_date, get_default_end_date, ) @@ -403,7 +403,7 @@ class ExportDataTest(MockEppLib): } self.maxDiff = None # Call the export functions - write_body( + write_csv( writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True ) @@ -427,7 +427,7 @@ class ExportDataTest(MockEppLib): expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() self.assertEqual(csv_content, expected_content) - def test_write_body(self): + def test_write_csv(self): """Test that write_body returns the existing domain, test that sort by domain name works, test that filter works""" @@ -462,7 +462,7 @@ class ExportDataTest(MockEppLib): ], } # Call the export functions - write_body( + write_csv( writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True ) # Reset the CSV file's position to the beginning @@ -512,7 +512,7 @@ class ExportDataTest(MockEppLib): ], } # Call the export functions - write_body( + write_csv( writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True ) # Reset the CSV file's position to the beginning @@ -591,7 +591,7 @@ class ExportDataTest(MockEppLib): } # Call the export functions - write_body( + write_csv( writer, columns, sort_fields, @@ -599,7 +599,7 @@ class ExportDataTest(MockEppLib): get_domain_managers=False, should_write_header=True, ) - write_body( + write_csv( writer, columns, sort_fields_for_deleted_domains, @@ -664,7 +664,7 @@ class ExportDataTest(MockEppLib): } self.maxDiff = None # Call the export functions - write_body( + write_csv( writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True ) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 1e4895f80..90e80f551 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -136,7 +136,7 @@ def update_columns_with_domain_managers(columns, max_dm_count): columns.append(f"Domain manager email {i}") -def write_body( +def write_csv( writer, columns, sort_fields, @@ -161,7 +161,7 @@ def write_body( # Reduce the memory overhead when performing the write operation paginator = Paginator(all_domain_infos, 1000) - if get_domain_managers: + if get_domain_managers and len(all_domain_infos) > 0: # We want to get the max amont of domain managers an # account has to set the column header dynamically max_dm_count = max(len(domain_info.domain.permissions.all()) for domain_info in all_domain_infos) @@ -179,6 +179,7 @@ def write_body( # It indicates that DomainInformation.domain is None. logger.error("csv_export -> Error when parsing row, domain was None") continue + if should_write_header: write_header(writer, columns) @@ -219,7 +220,7 @@ def export_data_type_to_csv(csv_file): Domain.State.ON_HOLD, ], } - write_body(writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True) + write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True) def export_data_full_to_csv(csv_file): @@ -250,7 +251,7 @@ def export_data_full_to_csv(csv_file): Domain.State.ON_HOLD, ], } - write_body(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True) + write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True) def export_data_federal_to_csv(csv_file): @@ -282,7 +283,7 @@ def export_data_federal_to_csv(csv_file): Domain.State.ON_HOLD, ], } - write_body(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True) + write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True) def get_default_start_date(): @@ -349,8 +350,8 @@ def export_data_growth_to_csv(csv_file, start_date, end_date): "domain__deleted__gte": start_date_formatted, } - write_body(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True) - write_body( + write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True) + write_csv( writer, columns, sort_fields_for_deleted_domains, From 0e23946cf85ad74b42b14e4c84d82a8b6aa27527 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 20 Feb 2024 10:45:49 -0700 Subject: [PATCH 118/119] Bug fix --- src/registrar/models/domain_information.py | 6 ++++++ src/registrar/models/utility/domain_helper.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index acaa330bb..b35f41ee4 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -255,6 +255,12 @@ class DomainInformation(TimeStampedModel): else: da_many_to_many_dict[field] = getattr(domain_application, field).all() + # This will not happen in normal code flow, but having some redundancy doesn't hurt. + # da_dict should not have "id" under any circumstances. + if "id" in da_dict: + logger.warning("create_from_da() -> Found attribute 'id' when trying to create") + da_dict.pop("id", None) + # Create a placeholder DomainInformation object domain_info = DomainInformation(**da_dict) diff --git a/src/registrar/models/utility/domain_helper.py b/src/registrar/models/utility/domain_helper.py index 230b23e16..9e3559676 100644 --- a/src/registrar/models/utility/domain_helper.py +++ b/src/registrar/models/utility/domain_helper.py @@ -180,8 +180,8 @@ class DomainHelper: """ # Get a list of the existing fields on model_1 and model_2 - model_1_fields = set(field.name for field in model_1._meta.get_fields() if field != "id") - model_2_fields = set(field.name for field in model_2._meta.get_fields() if field != "id") + model_1_fields = set(field.name for field in model_1._meta.get_fields() if field.name != "id") + model_2_fields = set(field.name for field in model_2._meta.get_fields() if field.name != "id") # Get the fields that exist on both DomainApplication and DomainInformation common_fields = model_1_fields & model_2_fields From 0d23007fcd316f8c4093e7e5bbf9b99b84c5fa76 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 20 Feb 2024 11:13:09 -0700 Subject: [PATCH 119/119] Add some additional information --- src/registrar/models/domain_information.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index b35f41ee4..1a50efe2c 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -257,6 +257,8 @@ class DomainInformation(TimeStampedModel): # This will not happen in normal code flow, but having some redundancy doesn't hurt. # da_dict should not have "id" under any circumstances. + # If it does have it, then this indicates that common_fields is overzealous in the data + # that it is returning. Try looking in DomainHelper.get_common_fields. if "id" in da_dict: logger.warning("create_from_da() -> Found attribute 'id' when trying to create") da_dict.pop("id", None)