diff --git a/src/registrar/management/commands/update_default_public_contacts.py b/src/registrar/management/commands/update_default_public_contacts.py index cab208d69..b59ab35a5 100644 --- a/src/registrar/management/commands/update_default_public_contacts.py +++ b/src/registrar/management/commands/update_default_public_contacts.py @@ -73,5 +73,5 @@ class Command(BaseCommand, PopulateScriptTemplate): f"Details: DB value - {record_field}, expected value(s) - {expected_values}" ) TerminalHelper.colorful_logger("WARNING", "YELLOW", message) - return False - return True + return True + return False diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index b21576c83..24eca1988 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -870,8 +870,7 @@ class Domain(TimeStampedModel, DomainHelper): logger.error("Error changing to new registrant error code is %s, error is %s" % (e.code, e)) # TODO-error handling better here? - @classmethod - def _set_singleton_contact(cls, contact: PublicContact, expectedType: str): # noqa + def _set_singleton_contact(self, contact: PublicContact, expectedType: str): # noqa """Sets the contacts by adding them to the registry as new contacts, updates the contact if it is already in epp, deletes any additional contacts of the matching type for this domain @@ -880,6 +879,7 @@ class Domain(TimeStampedModel, DomainHelper): which inturn call this function) Will throw error if contact type is not the same as expectType Raises ValueError if expected type doesn't match the contact type""" + if expectedType != contact.contact_type: raise ValueError("Cannot set a contact with a different contact type, expected type was %s" % expectedType) @@ -890,12 +890,11 @@ class Domain(TimeStampedModel, DomainHelper): # domain and type but a different id # like in highlander where there can only be one duplicate_contacts = PublicContact.objects.exclude(registry_id=contact.registry_id).filter( - domain=cls, contact_type=contact.contact_type + domain=self, contact_type=contact.contact_type ) - # if no record exists with this contact type # make contact in registry, duplicate and errors handled there - errorCode = cls._make_contact_in_registry(contact) + errorCode = self._make_contact_in_registry(contact) # contact is already added to the domain, but something may have changed on it alreadyExistsInRegistry = errorCode == ErrorCode.OBJECT_EXISTS @@ -916,11 +915,11 @@ class Domain(TimeStampedModel, DomainHelper): if isRegistrant: # send update domain only for registant contacts existing_contact.delete() - cls._add_registrant_to_existing_domain(contact) + self._add_registrant_to_existing_domain(contact) else: # remove the old contact and add a new one try: - cls._update_domain_with_contact(contact=existing_contact, rem=True) + self._update_domain_with_contact(contact=existing_contact, rem=True) existing_contact.delete() except Exception as err: logger.error("Raising error after removing and adding a new contact") @@ -929,13 +928,13 @@ class Domain(TimeStampedModel, DomainHelper): # update domain with contact or update the contact itself if not isEmptySecurity: if not alreadyExistsInRegistry and not isRegistrant: - cls._update_domain_with_contact(contact=contact, rem=False) + self._update_domain_with_contact(contact=contact, rem=False) # if already exists just update elif alreadyExistsInRegistry: current_contact = PublicContact.objects.filter(registry_id=contact.registry_id).get() if current_contact.email != contact.email: - cls._update_epp_contact(contact=contact) + self._update_epp_contact(contact=contact) else: logger.info("removing security contact and setting default again") @@ -945,10 +944,10 @@ class Domain(TimeStampedModel, DomainHelper): # don't let user delete the default without adding a new email if current_contact.email != PublicContact.get_default_security().email: # remove the contact - cls._update_domain_with_contact(contact=current_contact, rem=True) + self._update_domain_with_contact(contact=current_contact, rem=True) current_contact.delete() # add new contact - security_contact = cls.get_default_security_contact() + security_contact = self.get_default_security_contact() security_contact.save() @security_contact.setter # type: ignore diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 101be51ef..daa231e9d 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -1910,7 +1910,14 @@ class MockEppLib(TestCase): return MagicMock(res_data=[mocked_result]) def mockCreateContactCommands(self, _request, cleaned): - if getattr(_request, "id", None) == "fail" and self.mockedSendFunction.call_count == 3: + ids_to_throw_already_exists = [ + "failAdmin", + "failTech", + "failSec", + "failReg", + "fail" + ] + if getattr(_request, "id", None) in ids_to_throw_already_exists and self.mockedSendFunction.call_count == 3: # use this for when a contact is being updated # sets the second send() to fail raise RegistryError(code=ErrorCode.OBJECT_EXISTS) @@ -1925,7 +1932,14 @@ class MockEppLib(TestCase): return MagicMock(res_data=[self.mockDataInfoHosts]) def mockDeleteContactCommands(self, _request, cleaned): - if getattr(_request, "id", None) == "fail": + ids_to_throw_already_exists = [ + "failAdmin", + "failTech", + "failSec", + "failReg", + "fail" + ] + if getattr(_request, "id", None) in ids_to_throw_already_exists: raise RegistryError(code=ErrorCode.OBJECT_EXISTS) else: return MagicMock( diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index 110feea85..2f6bf95c3 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -32,6 +32,7 @@ from registrar.models import ( Portfolio, Suborganization, ) +from registrar.utility.enums import DefaultEmail import tablib from unittest.mock import patch, call, MagicMock, mock_open from epplibwrapper import commands, common @@ -2506,3 +2507,197 @@ class TestRemovePortfolios(TestCase): # Check that the portfolio was deleted self.assertFalse(Portfolio.objects.filter(organization_name="Test with suborg").exists()) + + +class TestUpdateDefaultPublicContacts(MockEppLib): + """Tests for the update_default_public_contacts management command.""" + + @less_console_noise_decorator + def setUp(self): + """Setup test data with PublicContact records.""" + super().setUp() + self.domain_request = completed_domain_request( + name="testdomain.gov", + status=DomainRequest.DomainRequestStatus.IN_REVIEW, + ) + self.domain_request.approve() + self.domain = self.domain_request.approved_domain + + # 1. PublicContact with all old default values + self.old_default_contact = PublicContact.get_default_administrative() + self.old_default_contact.registry_id = "failAdmin" + self.old_default_contact.name = "CSD/CB – ATTN: Cameron Dixon" + self.old_default_contact.street1 = "CISA – NGR STOP 0645" + self.old_default_contact.pc = "20598-0645" + self.old_default_contact.email = DefaultEmail.OLD_PUBLIC_CONTACT_DEFAULT + self.old_default_contact.domain = self.domain + self.old_default_contact.save() + + # 2. PublicContact with current default email but old values for other fields + self.mixed_default_contact = PublicContact.get_default_technical() + self.mixed_default_contact.registry_id = "failTech" + self.mixed_default_contact.name = "registry customer service" + self.mixed_default_contact.street1 = "4200 Wilson Blvd." + self.mixed_default_contact.pc = "22201" + self.mixed_default_contact.email = DefaultEmail.PUBLIC_CONTACT_DEFAULT + self.mixed_default_contact.domain = self.domain + self.mixed_default_contact.save(skip_epp_save=True) + + # 3. PublicContact with non-default values + self.non_default_contact = PublicContact.get_default_security() + self.non_default_contact.registry_id = "failSec" + self.non_default_contact.name = "Hotdogs" + self.non_default_contact.street1 = "123 hotdog town" + self.non_default_contact.pc = "22111" + self.non_default_contact.email = "thehotdogman@igorville.gov" + self.non_default_contact.domain = self.domain + self.non_default_contact.save(skip_epp_save=True) + + # 4. Create a contact using the default helper function but with old email + self.default_registrant_old_email = PublicContact.get_default_registrant() + self.default_registrant_old_email.registry_id = "failReg" + self.default_registrant_old_email.domain = self.domain + self.default_registrant_old_email.email = DefaultEmail.LEGACY_DEFAULT + self.default_registrant_old_email.save(skip_epp_save=True) + + def tearDown(self): + """Clean up test data.""" + super().tearDown() + PublicContact.objects.all().delete() + Domain.objects.all().delete() + DomainRequest.objects.all().delete() + DomainInformation.objects.all().delete() + User.objects.all().delete() + + @patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", return_value=True) + @less_console_noise_decorator + def run_update_default_public_contacts(self, mock_prompt, **kwargs): + """Execute the update_default_public_contacts command with options.""" + call_command("update_default_public_contacts", **kwargs) + + @less_console_noise_decorator + def test_updates_old_default_contact(self): + """ + Test that contacts with old default values are updated to new default values. + Also tests for string normalization. + """ + self.run_update_default_public_contacts() + self.old_default_contact.refresh_from_db() + + # Verify updates occurred + self.assertEqual(self.old_default_contact.name, "CSD/CB – Attn: .gov TLD") + self.assertEqual(self.old_default_contact.street1, "1110 N. Glebe Rd") + self.assertEqual(self.old_default_contact.pc, "22201") + self.assertEqual(self.old_default_contact.email, DefaultEmail.PUBLIC_CONTACT_DEFAULT) + + # Verify EPP create/update calls were made + expected_update = self._convertPublicContactToEpp( + self.old_default_contact, + disclose_email=True, + createContact=False, + disclose_fields={"email", "voice", "addr"}, + disclose_types={"addr": "loc"}, + ) + self.mockedSendFunction.assert_any_call(expected_update, cleaned=True) + + @less_console_noise_decorator + def test_updates_with_default_contact_values(self): + """ + Test that contacts created from the default helper function with old email are updated. + """ + self.run_update_default_public_contacts() + self.default_registrant_old_email.refresh_from_db() + + # Verify updates occurred + self.assertEqual(self.default_registrant_old_email.name, "CSD/CB – Attn: .gov TLD") + self.assertEqual(self.default_registrant_old_email.street1, "1110 N. Glebe Rd") + self.assertEqual(self.default_registrant_old_email.pc, "22201") + self.assertEqual(self.default_registrant_old_email.email, DefaultEmail.PUBLIC_CONTACT_DEFAULT) + + # Verify values match the default + default_admin = PublicContact.get_default_administrative() + self.assertEqual(self.default_registrant_old_email.name, default_admin.name) + self.assertEqual(self.default_registrant_old_email.street1, default_admin.street1) + self.assertEqual(self.default_registrant_old_email.pc, default_admin.pc) + self.assertEqual(self.default_registrant_old_email.email, default_admin.email) + + # Verify EPP create/update calls were made + expected_update = self._convertPublicContactToEpp( + self.default_registrant_old_email, + disclose_email=False, + createContact=False, + disclose_fields={} + ) + self.mockedSendFunction.assert_any_call(expected_update, cleaned=True) + + @less_console_noise_decorator + def test_skips_non_default_contacts(self): + """ + Test that contacts with non-default values are skipped. + """ + original_name = self.non_default_contact.name + original_street1 = self.non_default_contact.street1 + original_pc = self.non_default_contact.pc + original_email = self.non_default_contact.email + + self.run_update_default_public_contacts() + self.non_default_contact.refresh_from_db() + + # Verify no updates occurred + self.assertEqual(self.non_default_contact.name, original_name) + self.assertEqual(self.non_default_contact.street1, original_street1) + self.assertEqual(self.non_default_contact.pc, original_pc) + self.assertEqual(self.non_default_contact.email, original_email) + + # Ensure that the update is still skipped even with the override flag + self.run_update_default_public_contacts(overwrite_updated_contacts=True) + self.non_default_contact.refresh_from_db() + + # Verify no updates occurred + self.assertEqual(self.non_default_contact.name, original_name) + self.assertEqual(self.non_default_contact.street1, original_street1) + self.assertEqual(self.non_default_contact.pc, original_pc) + self.assertEqual(self.non_default_contact.email, original_email) + + + @less_console_noise_decorator + def test_skips_contacts_with_current_default_email_by_default(self): + """ + Test that contacts with the current default email are skipped when not using the override flag. + """ + # Get original values + original_name = self.mixed_default_contact.name + original_street1 = self.mixed_default_contact.street1 + + self.run_update_default_public_contacts() + self.mixed_default_contact.refresh_from_db() + + # Verify no updates occurred + self.assertEqual(self.mixed_default_contact.name, original_name) + self.assertEqual(self.mixed_default_contact.street1, original_street1) + self.assertEqual(self.mixed_default_contact.email, DefaultEmail.PUBLIC_CONTACT_DEFAULT) + + @less_console_noise_decorator + def test_updates_with_overwrite_flag(self): + """ + Test that contacts with the current default email are updated when using the override flag. + """ + # Run the command with the override flag + self.run_update_default_public_contacts(overwrite_updated_contacts=True) + self.mixed_default_contact.refresh_from_db() + + # Verify updates occurred + self.assertEqual(self.mixed_default_contact.name, "CSD/CB – Attn: .gov TLD") + self.assertEqual(self.mixed_default_contact.street1, "1110 N. Glebe Rd") + self.assertEqual(self.mixed_default_contact.pc, "22201") + self.assertEqual(self.mixed_default_contact.email, DefaultEmail.PUBLIC_CONTACT_DEFAULT) + + # Verify EPP create/update calls were made + expected_update = self._convertPublicContactToEpp( + self.mixed_default_contact, + disclose_email=False, + createContact=False, + disclose_fields={} + ) + print(f"call args: {self.mockedSendFunction.call_args_list}") + self.mockedSendFunction.assert_any_call(expected_update, cleaned=True)