Merge pull request #3185 from cisagov/ms/2823-update-delete-domain-process

#2823: update delete domain process - [MS]
This commit is contained in:
Matt-Spence 2024-12-16 14:04:47 -05:00 committed by GitHub
commit c02b036301
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 335 additions and 75 deletions

View file

@ -20,7 +20,7 @@ applications:
# Tell Django where it is being hosted # Tell Django where it is being hosted
DJANGO_BASE_URL: https://getgov-ms.app.cloud.gov DJANGO_BASE_URL: https://getgov-ms.app.cloud.gov
# Tell Django how much stuff to log # Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO DJANGO_LOG_LEVEL: DEBUG
# default public site location # default public site location
GETGOV_PUBLIC_SITE_URL: https://get.gov GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments # Flag to disable/enable features in prod environments

View file

@ -62,9 +62,11 @@ class RegistryError(Exception):
- 2501 - 2502 Something malicious or abusive may have occurred - 2501 - 2502 Something malicious or abusive may have occurred
""" """
def __init__(self, *args, code=None, **kwargs): def __init__(self, *args, code=None, note="", **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.code = code self.code = code
# note is a string that can be used to provide additional context
self.note = note
def should_retry(self): def should_retry(self):
return self.code == ErrorCode.COMMAND_FAILED return self.code == ErrorCode.COMMAND_FAILED

View file

@ -3334,7 +3334,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
except RegistryError as err: except RegistryError as err:
# Using variables to get past the linter # Using variables to get past the linter
message1 = f"Cannot delete Domain when in state {obj.state}" message1 = f"Cannot delete Domain when in state {obj.state}"
message2 = "This subdomain is being used as a hostname on another domain" message2 = f"This subdomain is being used as a hostname on another domain: {err.note}"
# Human-readable mappings of ErrorCodes. Can be expanded. # Human-readable mappings of ErrorCodes. Can be expanded.
error_messages = { error_messages = {
# noqa on these items as black wants to reformat to an invalid length # noqa on these items as black wants to reformat to an invalid length

View file

@ -230,6 +230,12 @@ class Domain(TimeStampedModel, DomainHelper):
"""Called during delete. Example: `del domain.registrant`.""" """Called during delete. Example: `del domain.registrant`."""
super().__delete__(obj) super().__delete__(obj)
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
# If the domain is deleted we don't want the expiration date to be set
if self.state == self.State.DELETED and self.expiration_date:
self.expiration_date = None
super().save(force_insert, force_update, using, update_fields)
@classmethod @classmethod
def available(cls, domain: str) -> bool: def available(cls, domain: str) -> bool:
"""Check if a domain is available. """Check if a domain is available.
@ -253,7 +259,7 @@ class Domain(TimeStampedModel, DomainHelper):
return not cls.available(domain) return not cls.available(domain)
@Cache @Cache
def contacts(self) -> dict[str, str]: def registry_contacts(self) -> dict[str, str]:
""" """
Get a dictionary of registry IDs for the contacts for this domain. Get a dictionary of registry IDs for the contacts for this domain.
@ -706,7 +712,7 @@ class Domain(TimeStampedModel, DomainHelper):
raise e raise e
@nameservers.setter # type: ignore @nameservers.setter # type: ignore
def nameservers(self, hosts: list[tuple[str, list]]): def nameservers(self, hosts: list[tuple[str, list]]): # noqa
"""Host should be a tuple of type str, str,... where the elements are """Host should be a tuple of type str, str,... where the elements are
Fully qualified host name, addresses associated with the host Fully qualified host name, addresses associated with the host
example: [(ns1.okay.gov, [127.0.0.1, others ips])]""" example: [(ns1.okay.gov, [127.0.0.1, others ips])]"""
@ -743,7 +749,12 @@ class Domain(TimeStampedModel, DomainHelper):
successTotalNameservers = len(oldNameservers) - deleteCount + addToDomainCount successTotalNameservers = len(oldNameservers) - deleteCount + addToDomainCount
try:
self._delete_hosts_if_not_used(hostsToDelete=deleted_values) self._delete_hosts_if_not_used(hostsToDelete=deleted_values)
except Exception as e:
# we don't need this part to succeed in order to continue.
logger.error("Failed to delete nameserver hosts: %s", e)
if successTotalNameservers < 2: if successTotalNameservers < 2:
try: try:
self.dns_needed() self.dns_needed()
@ -1029,6 +1040,47 @@ class Domain(TimeStampedModel, DomainHelper):
def _delete_domain(self): def _delete_domain(self):
"""This domain should be deleted from the registry """This domain should be deleted from the registry
may raises RegistryError, should be caught or handled correctly by caller""" may raises RegistryError, should be caught or handled correctly by caller"""
logger.info("Deleting subdomains for %s", self.name)
# check if any subdomains are in use by another domain
hosts = Host.objects.filter(name__regex=r".+{}".format(self.name))
for host in hosts:
if host.domain != self:
logger.error("Unable to delete host: %s is in use by another domain: %s", host.name, host.domain)
raise RegistryError(
code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION,
note=f"Host {host.name} is in use by {host.domain}",
)
(
deleted_values,
updated_values,
new_values,
oldNameservers,
) = self.getNameserverChanges(hosts=[])
_ = self._update_host_values(updated_values, oldNameservers) # returns nothing, just need to be run and errors
addToDomainList, _ = self.createNewHostList(new_values)
deleteHostList, _ = self.createDeleteHostList(deleted_values)
responseCode = self.addAndRemoveHostsFromDomain(hostsToAdd=addToDomainList, hostsToDelete=deleteHostList)
# if unable to update domain raise error and stop
if responseCode != ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY:
raise NameserverError(code=nsErrorCodes.BAD_DATA)
# addAndRemoveHostsFromDomain removes the hosts from the domain object,
# but we still need to delete the object themselves
self._delete_hosts_if_not_used(hostsToDelete=deleted_values)
logger.debug("Deleting non-registrant contacts for %s", self.name)
contacts = PublicContact.objects.filter(domain=self)
for contact in contacts:
if contact.contact_type != PublicContact.ContactTypeChoices.REGISTRANT:
self._update_domain_with_contact(contact, rem=True)
request = commands.DeleteContact(contact.registry_id)
registry.send(request, cleaned=True)
logger.info("Deleting domain %s", self.name)
request = commands.DeleteDomain(name=self.name) request = commands.DeleteDomain(name=self.name)
registry.send(request, cleaned=True) registry.send(request, cleaned=True)
@ -1096,7 +1148,7 @@ class Domain(TimeStampedModel, DomainHelper):
Returns True if expired, False otherwise. Returns True if expired, False otherwise.
""" """
if self.expiration_date is None: if self.expiration_date is None:
return True return self.state != self.State.DELETED
now = timezone.now().date() now = timezone.now().date()
return self.expiration_date < now return self.expiration_date < now
@ -1430,6 +1482,8 @@ class Domain(TimeStampedModel, DomainHelper):
@transition(field="state", source=[State.ON_HOLD, State.DNS_NEEDED], target=State.DELETED) @transition(field="state", source=[State.ON_HOLD, State.DNS_NEEDED], target=State.DELETED)
def deletedInEpp(self): def deletedInEpp(self):
"""Domain is deleted in epp but is saved in our database. """Domain is deleted in epp but is saved in our database.
Subdomains will be deleted first if not in use by another domain.
Contacts for this domain will also be deleted.
Error handling should be provided by the caller.""" Error handling should be provided by the caller."""
# While we want to log errors, we want to preserve # While we want to log errors, we want to preserve
# that information when this function is called. # that information when this function is called.
@ -1439,8 +1493,9 @@ class Domain(TimeStampedModel, DomainHelper):
logger.info("deletedInEpp()-> inside _delete_domain") logger.info("deletedInEpp()-> inside _delete_domain")
self._delete_domain() self._delete_domain()
self.deleted = timezone.now() self.deleted = timezone.now()
self.expiration_date = None
except RegistryError as err: except RegistryError as err:
logger.error(f"Could not delete domain. Registry returned error: {err}") logger.error(f"Could not delete domain. Registry returned error: {err}. {err.note}")
raise err raise err
except TransitionNotAllowed as err: except TransitionNotAllowed as err:
logger.error("Could not delete domain. FSM failure: {err}") logger.error("Could not delete domain. FSM failure: {err}")
@ -1745,7 +1800,6 @@ class Domain(TimeStampedModel, DomainHelper):
"""delete the host object in registry, """delete the host object in registry,
will only delete the host object, if it's not being used by another domain will only delete the host object, if it's not being used by another domain
Performs just the DeleteHost epp call Performs just the DeleteHost epp call
Supresses regstry error, as registry can disallow delete for various reasons
Args: Args:
hostsToDelete (list[str])- list of nameserver/host names to remove hostsToDelete (list[str])- list of nameserver/host names to remove
Returns: Returns:
@ -1764,6 +1818,8 @@ class Domain(TimeStampedModel, DomainHelper):
else: else:
logger.error("Error _delete_hosts_if_not_used, code was %s error was %s" % (e.code, e)) logger.error("Error _delete_hosts_if_not_used, code was %s error was %s" % (e.code, e))
raise e
def _fix_unknown_state(self, cleaned): def _fix_unknown_state(self, cleaned):
""" """
_fix_unknown_state: Calls _add_missing_contacts_if_unknown _fix_unknown_state: Calls _add_missing_contacts_if_unknown

View file

@ -1232,6 +1232,7 @@ class MockEppLib(TestCase):
common.Status(state="serverTransferProhibited", description="", lang="en"), common.Status(state="serverTransferProhibited", description="", lang="en"),
common.Status(state="inactive", description="", lang="en"), common.Status(state="inactive", description="", lang="en"),
], ],
registrant="regContact",
ex_date=date(2023, 5, 25), ex_date=date(2023, 5, 25),
) )
@ -1394,6 +1395,15 @@ class MockEppLib(TestCase):
hosts=["fake.host.com"], hosts=["fake.host.com"],
) )
infoDomainSharedHost = fakedEppObject(
"sharedHost.gov",
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
contacts=[],
hosts=[
"ns1.sharedhost.com",
],
)
infoDomainThreeHosts = fakedEppObject( infoDomainThreeHosts = fakedEppObject(
"my-nameserver.gov", "my-nameserver.gov",
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)), cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
@ -1604,6 +1614,8 @@ class MockEppLib(TestCase):
return self.mockInfoContactCommands(_request, cleaned) return self.mockInfoContactCommands(_request, cleaned)
case commands.CreateContact: case commands.CreateContact:
return self.mockCreateContactCommands(_request, cleaned) return self.mockCreateContactCommands(_request, cleaned)
case commands.DeleteContact:
return self.mockDeleteContactCommands(_request, cleaned)
case commands.UpdateDomain: case commands.UpdateDomain:
return self.mockUpdateDomainCommands(_request, cleaned) return self.mockUpdateDomainCommands(_request, cleaned)
case commands.CreateHost: case commands.CreateHost:
@ -1611,10 +1623,7 @@ class MockEppLib(TestCase):
case commands.UpdateHost: case commands.UpdateHost:
return self.mockUpdateHostCommands(_request, cleaned) return self.mockUpdateHostCommands(_request, cleaned)
case commands.DeleteHost: case commands.DeleteHost:
return MagicMock( return self.mockDeleteHostCommands(_request, cleaned)
res_data=[self.mockDataHostChange],
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
)
case commands.CheckDomain: case commands.CheckDomain:
return self.mockCheckDomainCommand(_request, cleaned) return self.mockCheckDomainCommand(_request, cleaned)
case commands.DeleteDomain: case commands.DeleteDomain:
@ -1667,6 +1676,15 @@ class MockEppLib(TestCase):
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
) )
def mockDeleteHostCommands(self, _request, cleaned):
host = getattr(_request, "name", None)
if "sharedhost.com" in host:
raise RegistryError(code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION, note="ns1.sharedhost.com")
return MagicMock(
res_data=[self.mockDataHostChange],
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
)
def mockUpdateDomainCommands(self, _request, cleaned): def mockUpdateDomainCommands(self, _request, cleaned):
if getattr(_request, "name", None) == "dnssec-invalid.gov": if getattr(_request, "name", None) == "dnssec-invalid.gov":
raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR) raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR)
@ -1678,9 +1696,6 @@ class MockEppLib(TestCase):
def mockDeleteDomainCommands(self, _request, cleaned): def mockDeleteDomainCommands(self, _request, cleaned):
if getattr(_request, "name", None) == "failDelete.gov": if getattr(_request, "name", None) == "failDelete.gov":
name = getattr(_request, "name", None)
fake_nameserver = "ns1.failDelete.gov"
if name in fake_nameserver:
raise RegistryError(code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION) raise RegistryError(code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION)
return None return None
@ -1721,6 +1736,7 @@ class MockEppLib(TestCase):
# Define a dictionary to map request names to data and extension values # Define a dictionary to map request names to data and extension values
request_mappings = { request_mappings = {
"fake.gov": (self.mockDataInfoDomain, None),
"security.gov": (self.infoDomainNoContact, None), "security.gov": (self.infoDomainNoContact, None),
"dnssec-dsdata.gov": ( "dnssec-dsdata.gov": (
self.mockDataInfoDomain, self.mockDataInfoDomain,
@ -1751,6 +1767,7 @@ class MockEppLib(TestCase):
"subdomainwoip.gov": (self.mockDataInfoDomainSubdomainNoIP, None), "subdomainwoip.gov": (self.mockDataInfoDomainSubdomainNoIP, None),
"ddomain3.gov": (self.InfoDomainWithContacts, None), "ddomain3.gov": (self.InfoDomainWithContacts, None),
"igorville.gov": (self.InfoDomainWithContacts, None), "igorville.gov": (self.InfoDomainWithContacts, None),
"sharingiscaring.gov": (self.infoDomainSharedHost, None),
} }
# Retrieve the corresponding values from the dictionary # Retrieve the corresponding values from the dictionary
@ -1801,6 +1818,15 @@ class MockEppLib(TestCase):
raise ContactError(code=ContactErrorCodes.CONTACT_TYPE_NONE) raise ContactError(code=ContactErrorCodes.CONTACT_TYPE_NONE)
return MagicMock(res_data=[self.mockDataInfoHosts]) return MagicMock(res_data=[self.mockDataInfoHosts])
def mockDeleteContactCommands(self, _request, cleaned):
if getattr(_request, "id", None) == "fail":
raise RegistryError(code=ErrorCode.OBJECT_EXISTS)
else:
return MagicMock(
res_data=[self.mockDataInfoContact],
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
)
def setUp(self): def setUp(self):
"""mock epp send function as this will fail locally""" """mock epp send function as this will fail locally"""
self.mockSendPatch = patch("registrar.models.domain.registry.send") self.mockSendPatch = patch("registrar.models.domain.registry.send")

View file

@ -16,6 +16,7 @@ from registrar.models import (
Host, Host,
Portfolio, Portfolio,
) )
from registrar.models.public_contact import PublicContact
from registrar.models.user_domain_role import UserDomainRole from registrar.models.user_domain_role import UserDomainRole
from .common import ( from .common import (
MockSESClient, MockSESClient,
@ -59,6 +60,7 @@ class TestDomainAdminAsStaff(MockEppLib):
def tearDown(self): def tearDown(self):
super().tearDown() super().tearDown()
Host.objects.all().delete() Host.objects.all().delete()
PublicContact.objects.all().delete()
Domain.objects.all().delete() Domain.objects.all().delete()
DomainInformation.objects.all().delete() DomainInformation.objects.all().delete()
DomainRequest.objects.all().delete() DomainRequest.objects.all().delete()
@ -170,7 +172,7 @@ class TestDomainAdminAsStaff(MockEppLib):
@less_console_noise_decorator @less_console_noise_decorator
def test_deletion_is_successful(self): def test_deletion_is_successful(self):
""" """
Scenario: Domain deletion is unsuccessful Scenario: Domain deletion is successful
When the domain is deleted When the domain is deleted
Then a user-friendly success message is returned for displaying on the web Then a user-friendly success message is returned for displaying on the web
And `state` is set to `DELETED` And `state` is set to `DELETED`
@ -221,6 +223,55 @@ class TestDomainAdminAsStaff(MockEppLib):
self.assertEqual(domain.state, Domain.State.DELETED) self.assertEqual(domain.state, Domain.State.DELETED)
# @less_console_noise_decorator
def test_deletion_is_unsuccessful(self):
"""
Scenario: Domain deletion is unsuccessful
When the domain is deleted and has shared subdomains
Then a user-friendly success message is returned for displaying on the web
And `state` is not set to `DELETED`
"""
domain, _ = Domain.objects.get_or_create(name="sharingiscaring.gov", state=Domain.State.ON_HOLD)
# Put in client hold
domain.place_client_hold()
# 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")
# The contents of the modal should exist before and after the post.
# Check for the header
self.assertContains(response, "Are you sure you want to remove this domain from the registry?")
# Check for some of its body
self.assertContains(response, "When a domain is removed from the registry:")
# Check for some of the button content
self.assertContains(response, "Yes, 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.ERROR,
"Error deleting this Domain: This subdomain is being used as a hostname on another domain: ns1.sharedhost.com", # noqa
extra_tags="",
fail_silently=False,
)
self.assertEqual(domain.state, Domain.State.ON_HOLD)
@less_console_noise_decorator @less_console_noise_decorator
def test_deletion_ready_fsm_failure(self): def test_deletion_ready_fsm_failure(self):
""" """

View file

@ -9,6 +9,7 @@ from django.db.utils import IntegrityError
from unittest.mock import MagicMock, patch, call from unittest.mock import MagicMock, patch, call
import datetime import datetime
from django.utils.timezone import make_aware from django.utils.timezone import make_aware
from api.tests.common import less_console_noise_decorator
from registrar.models import Domain, Host, HostIP from registrar.models import Domain, Host, HostIP
from unittest import skip from unittest import skip
@ -1454,6 +1455,7 @@ class TestRegistrantNameservers(MockEppLib):
), ),
call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True), call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True),
] ]
self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True)
self.assertFalse(self.domainWithThreeNS.is_active()) self.assertFalse(self.domainWithThreeNS.is_active())
@ -2582,12 +2584,32 @@ class TestAnalystDelete(MockEppLib):
""" """
super().setUp() super().setUp()
self.domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) self.domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
self.domain_with_contacts, _ = Domain.objects.get_or_create(name="freeman.gov", state=Domain.State.READY)
self.domain_on_hold, _ = Domain.objects.get_or_create(name="fake-on-hold.gov", state=Domain.State.ON_HOLD) self.domain_on_hold, _ = Domain.objects.get_or_create(name="fake-on-hold.gov", state=Domain.State.ON_HOLD)
Host.objects.create(name="ns1.sharingiscaring.gov", domain=self.domain_on_hold)
PublicContact.objects.create(
registry_id="regContact",
contact_type=PublicContact.ContactTypeChoices.REGISTRANT,
domain=self.domain_with_contacts,
)
PublicContact.objects.create(
registry_id="adminContact",
contact_type=PublicContact.ContactTypeChoices.ADMINISTRATIVE,
domain=self.domain_with_contacts,
)
PublicContact.objects.create(
registry_id="techContact",
contact_type=PublicContact.ContactTypeChoices.TECHNICAL,
domain=self.domain_with_contacts,
)
def tearDown(self): def tearDown(self):
Host.objects.all().delete()
PublicContact.objects.all().delete()
Domain.objects.all().delete() Domain.objects.all().delete()
super().tearDown() super().tearDown()
@less_console_noise_decorator
def test_analyst_deletes_domain(self): def test_analyst_deletes_domain(self):
""" """
Scenario: Analyst permanently deletes a domain Scenario: Analyst permanently deletes a domain
@ -2597,7 +2619,6 @@ class TestAnalystDelete(MockEppLib):
The deleted date is set. The deleted date is set.
""" """
with less_console_noise():
# Put the domain in client hold # Put the domain in client hold
self.domain.place_client_hold() self.domain.place_client_hold()
# Delete it... # Delete it...
@ -2620,36 +2641,141 @@ class TestAnalystDelete(MockEppLib):
# Cache should be invalidated # Cache should be invalidated
self.assertEqual(self.domain._cache, {}) self.assertEqual(self.domain._cache, {})
@less_console_noise_decorator
def test_deletion_is_unsuccessful(self): def test_deletion_is_unsuccessful(self):
""" """
Scenario: Domain deletion is unsuccessful Scenario: Domain deletion is unsuccessful
When a subdomain exists When a subdomain exists that is in use by another domain
Then a client error is returned of code 2305 Then a client error is returned of code 2305
And `state` is not set to `DELETED` And `state` is not set to `DELETED`
""" """
with less_console_noise():
# Desired domain # Desired domain
domain, _ = Domain.objects.get_or_create(name="failDelete.gov", state=Domain.State.ON_HOLD) domain, _ = Domain.objects.get_or_create(name="sharingiscaring.gov", state=Domain.State.ON_HOLD)
# Put the domain in client hold # Put the domain in client hold
domain.place_client_hold() domain.place_client_hold()
# Delete it # Delete it
with self.assertRaises(RegistryError) as err: with self.assertRaises(RegistryError) as err:
domain.deletedInEpp() domain.deletedInEpp()
domain.save() domain.save()
self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION)
self.mockedSendFunction.assert_has_calls( self.assertTrue(err.code == ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION)
[ self.assertEqual(err.msg, "Host ns1.sharingiscaring.gov is in use by: fake-on-hold.gov")
call(
commands.DeleteDomain(name="failDelete.gov"),
cleaned=True,
)
]
)
# Domain itself should not be deleted # Domain itself should not be deleted
self.assertNotEqual(domain, None) self.assertNotEqual(domain, None)
# State should not have changed # State should not have changed
self.assertEqual(domain.state, Domain.State.ON_HOLD) self.assertEqual(domain.state, Domain.State.ON_HOLD)
@less_console_noise_decorator
def test_deletion_with_host_and_contacts(self):
"""
Scenario: Domain with related Host and Contacts is Deleted
When a contact and host exists that is tied to this domain
Then all the needed commands are sent to the registry
And `state` is set to `DELETED`
"""
# Put the domain in client hold
self.domain_with_contacts.place_client_hold()
# Delete it
self.domain_with_contacts.deletedInEpp()
self.domain_with_contacts.save()
# Check that the host and contacts are deleted
self.mockedSendFunction.assert_has_calls(
[
call(
commands.UpdateDomain(
name="freeman.gov",
add=[common.Status(state=Domain.Status.CLIENT_HOLD, description="", lang="en")],
rem=[],
nsset=None,
keyset=None,
registrant=None,
auth_info=None,
),
cleaned=True,
),
]
)
self.mockedSendFunction.assert_has_calls(
[
call(
commands.InfoDomain(name="freeman.gov", auth_info=None),
cleaned=True,
),
call(
commands.InfoHost(name="fake.host.com"),
cleaned=True,
),
call(
commands.UpdateDomain(
name="freeman.gov",
add=[],
rem=[common.HostObjSet(hosts=["fake.host.com"])],
nsset=None,
keyset=None,
registrant=None,
auth_info=None,
),
cleaned=True,
),
]
)
self.mockedSendFunction.assert_has_calls(
[
call(
commands.DeleteHost(name="fake.host.com"),
cleaned=True,
),
call(
commands.UpdateDomain(
name="freeman.gov",
add=[],
rem=[common.DomainContact(contact="adminContact", type="admin")],
nsset=None,
keyset=None,
registrant=None,
auth_info=None,
),
cleaned=True,
),
call(
commands.DeleteContact(id="adminContact"),
cleaned=True,
),
call(
commands.UpdateDomain(
name="freeman.gov",
add=[],
rem=[common.DomainContact(contact="techContact", type="tech")],
nsset=None,
keyset=None,
registrant=None,
auth_info=None,
),
cleaned=True,
),
call(
commands.DeleteContact(id="techContact"),
cleaned=True,
),
],
any_order=True,
)
self.mockedSendFunction.assert_has_calls(
[
call(
commands.DeleteDomain(name="freeman.gov"),
cleaned=True,
),
],
)
# Domain itself should not be deleted
self.assertNotEqual(self.domain_with_contacts, None)
# State should have changed
self.assertEqual(self.domain_with_contacts.state, Domain.State.DELETED)
@less_console_noise_decorator
def test_deletion_ready_fsm_failure(self): def test_deletion_ready_fsm_failure(self):
""" """
Scenario: Domain deletion is unsuccessful due to FSM rules Scenario: Domain deletion is unsuccessful due to FSM rules
@ -2661,7 +2787,6 @@ class TestAnalystDelete(MockEppLib):
The deleted date is still null. The deleted date is still null.
""" """
with less_console_noise():
self.assertEqual(self.domain.state, Domain.State.READY) self.assertEqual(self.domain.state, Domain.State.READY)
with self.assertRaises(TransitionNotAllowed) as err: with self.assertRaises(TransitionNotAllowed) as err:
self.domain.deletedInEpp() self.domain.deletedInEpp()