mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-16 17:47:02 +02:00
Commit for #901
This commit is contained in:
parent
c6baf9c98b
commit
59b095beab
7 changed files with 226 additions and 115 deletions
|
@ -7,6 +7,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||
from django.http.response import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from epplibwrapper.errors import ErrorCode, RegistryError
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.models.utility.admin_sort_fields import AdminSortFields
|
||||
from . import models
|
||||
from auditlog.models import LogEntry # type: ignore
|
||||
|
@ -717,51 +718,47 @@ class DomainAdmin(ListHeaderAdmin):
|
|||
return super().response_change(request, obj)
|
||||
|
||||
def do_delete_domain(self, request, obj):
|
||||
if not isinstance(obj, Domain):
|
||||
# Could be problematic if the type is similar,
|
||||
# but not the same (same field/func names) so we err out.
|
||||
# We do not want to accidentally delete records.
|
||||
raise ValueError("Object is not of type Domain")
|
||||
try:
|
||||
obj.deletedInEpp()
|
||||
obj.save()
|
||||
except RegistryError as err:
|
||||
if err.is_connection_error():
|
||||
self.message_user(
|
||||
request,
|
||||
"Error connecting to the registry",
|
||||
messages.ERROR,
|
||||
)
|
||||
elif err.code == ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION:
|
||||
self.message_user(
|
||||
request,
|
||||
"Error deleting this Domain: "
|
||||
# Human-readable mappings of ErrorCodes. Can be expanded.
|
||||
error_messages = {
|
||||
ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION:
|
||||
f"Cannot delete Domain when in status {obj.status}",
|
||||
messages.ERROR,
|
||||
ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION:
|
||||
"This subdomain is being used as a hostname on another domain"
|
||||
}
|
||||
|
||||
message = "Cannot connect to the registry"
|
||||
if not err.is_connection_error():
|
||||
# If nothing is found, will default to returned err
|
||||
message = error_messages.get(err.code, err)
|
||||
self.message_user(request, f"Error deleting this Domain: {message}", messages.ERROR)
|
||||
except TransitionNotAllowed as err:
|
||||
if obj.state == Domain.State.DELETED:
|
||||
self.message_user(
|
||||
request,
|
||||
f"This domain is already deleted",
|
||||
messages.INFO,
|
||||
)
|
||||
elif err.code == ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION:
|
||||
else:
|
||||
self.message_user(
|
||||
request,
|
||||
"Error deleting this Domain: "
|
||||
f" This subdomain is being used as a hostname on another domain",
|
||||
messages.ERROR,
|
||||
)
|
||||
elif err.code:
|
||||
self.message_user(
|
||||
request,
|
||||
f"Error deleting this Domain: {err}",
|
||||
messages.ERROR,
|
||||
)
|
||||
else:
|
||||
# all other type error messages, display the error
|
||||
self.message_user(request, err, messages.ERROR)
|
||||
except ValueError as err:
|
||||
self.message_user(request, err, messages.ERROR)
|
||||
except TransitionNotAllowed
|
||||
self.message_user(
|
||||
request,
|
||||
f"Error deleting this Domain: {err}",
|
||||
f"Can't switch from state '{obj.state}' to 'deleted'"
|
||||
,
|
||||
messages.ERROR,
|
||||
)
|
||||
else:
|
||||
self.message_user(
|
||||
request,
|
||||
("Domain %s Should now be deleted " ". Thanks!") % obj.name,
|
||||
("Domain %s has been deleted. Thanks!") % obj.name,
|
||||
)
|
||||
return HttpResponseRedirect(".")
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import logging
|
|||
|
||||
from datetime import date
|
||||
from string import digits
|
||||
from django_fsm import FSMField, transition # type: ignore
|
||||
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
@ -802,20 +802,14 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
self._remove_client_hold()
|
||||
# TODO -on the client hold ticket any additional error handling here
|
||||
|
||||
@transition(field="state", source=State.ON_HOLD, target=State.DELETED)
|
||||
@transition(field="state", source=[State.ON_HOLD, State.DNS_NEEDED], target=State.DELETED)
|
||||
def deletedInEpp(self):
|
||||
"""domain is deleted in epp but is saved in our database.
|
||||
Returns the request_code"""
|
||||
valid_delete_states = [
|
||||
self.State.ON_HOLD,
|
||||
self.State.DNS_NEEDED
|
||||
]
|
||||
# Check that the domain contacts a valid status
|
||||
if (self.state not in valid_delete_states):
|
||||
raise ValueError(
|
||||
f"Invalid domain state of {self.state}. Cannot delete."
|
||||
)
|
||||
|
||||
"""Domain is deleted in epp but is saved in our database.
|
||||
Error handling should be provided by the caller."""
|
||||
# While we want to log errors, we want to preserve
|
||||
# that information when this function is called.
|
||||
# Human-readable errors are introduced at the admin.py level,
|
||||
# as doing everything here would reduce reliablity.
|
||||
try:
|
||||
logger.info("deletedInEpp()-> inside _delete_domain")
|
||||
self._delete_domain()
|
||||
|
@ -824,6 +818,11 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
f"Could not delete domain. Registry returned error: {err}"
|
||||
)
|
||||
raise err
|
||||
except TransitionNotAllowed as err:
|
||||
logger.error(
|
||||
"Could not delete domain. FSM failure: {err}"
|
||||
)
|
||||
raise err
|
||||
except Exception as err:
|
||||
logger.error(
|
||||
f"Could not delete domain. An unspecified error occured: {err}"
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
|
||||
{% block field_sets %}
|
||||
<div class="submit-row">
|
||||
{% if original.state == original.State.READY %}
|
||||
{% if original.state == original.State.READY%}
|
||||
<input type="submit" value="Place hold" name="_place_client_hold">
|
||||
{% elif original.state == original.State.ON_HOLD %}
|
||||
{% elif original.state == original.State.ON_HOLD%}
|
||||
<input type="submit" value="Remove hold" name="_remove_client_hold">
|
||||
{% endif %}
|
||||
<input id="manageDomainSubmitButton" type="submit" value="Manage Domain" name="_edit_domain">
|
||||
|
|
|
@ -40,6 +40,9 @@
|
|||
{{ domain.name }}
|
||||
</th>
|
||||
<td data-sort-value="{{ domain.created_time|date:"U" }}" data-label="Date created">{{ domain.created_time|date }}</td>
|
||||
{% comment %} Should this be domain.status?
|
||||
<td data-label="Status">{{ domain.status|title }}</td>
|
||||
{% endcomment %}
|
||||
<td data-label="Status">{{ domain.application_status|title }}</td>
|
||||
<td>
|
||||
<a href="{% url "domain" pk=domain.pk %}">
|
||||
|
|
|
@ -606,10 +606,13 @@ class MockEppLib(TestCase):
|
|||
raise RegistryError(code=ErrorCode.OBJECT_EXISTS)
|
||||
elif (
|
||||
isinstance(_request, commands.DeleteDomain)
|
||||
and getattr(_request, "name", None) == "fail.gov"
|
||||
and 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_STATUS_PROHIBITS_OPERATION
|
||||
code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION
|
||||
)
|
||||
return MagicMock(res_data=[self.mockDataInfoHosts])
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ class TestDomainAdmin(MockEppLib):
|
|||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.superuser = create_superuser()
|
||||
self.staffuser = create_user()
|
||||
self.factory = RequestFactory()
|
||||
super().setUp()
|
||||
|
||||
def test_place_and_remove_hold(self):
|
||||
|
@ -87,6 +88,150 @@ class TestDomainAdmin(MockEppLib):
|
|||
self.assertContains(response, "Place hold")
|
||||
self.assertNotContains(response, "Remove hold")
|
||||
|
||||
def test_deletion_is_successful(self):
|
||||
"""
|
||||
Scenario: Domain deletion is unsuccessful
|
||||
When the domain is deleted
|
||||
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()
|
||||
self.client.login(username="staffuser", password="userpass")
|
||||
|
||||
# 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, "EPP Delete Domain")
|
||||
|
||||
# Test the info dialog
|
||||
request = self.factory.post(
|
||||
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||
{"_delete_domain": "Epp Delete Domain", "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):
|
||||
"""
|
||||
Scenario: Domain deletion is unsuccessful
|
||||
When an error is returned from epplibwrapper
|
||||
Then a user-friendly error message is returned for displaying on the web
|
||||
And `state` is not set to `DELETED`
|
||||
"""
|
||||
domain = create_ready_domain()
|
||||
self.client.login(username="staffuser", password="userpass")
|
||||
|
||||
# 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, "EPP Delete Domain")
|
||||
|
||||
# Test the error
|
||||
request = self.factory.post(
|
||||
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||
{"_delete_domain": "Epp Delete Domain", "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'",
|
||||
extra_tags='',
|
||||
fail_silently=False
|
||||
)
|
||||
|
||||
self.assertEqual(domain.state, Domain.State.READY)
|
||||
|
||||
def test_analyst_deletes_domain_idempotent(self):
|
||||
"""
|
||||
Scenario: Analyst tries to delete an already deleted domain
|
||||
Given `state` is already `DELETED`
|
||||
When `domain.deletedInEpp()` is called
|
||||
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()
|
||||
self.client.login(username="staffuser", password="userpass")
|
||||
|
||||
# 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, "EPP Delete Domain")
|
||||
|
||||
# Test the info dialog
|
||||
request = self.factory.post(
|
||||
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||
{"_delete_domain": "Epp Delete Domain", "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)
|
||||
|
||||
# Try to delete it again
|
||||
# Test the info dialog
|
||||
request = self.factory.post(
|
||||
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||
{"_delete_domain": "Epp Delete Domain", "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):
|
||||
raise
|
||||
|
|
|
@ -16,7 +16,7 @@ from registrar.models.draft_domain import DraftDomain
|
|||
from registrar.models.public_contact import PublicContact
|
||||
from registrar.models.user import User
|
||||
from .common import MockEppLib
|
||||
|
||||
from django_fsm import TransitionNotAllowed # type: ignore
|
||||
from epplibwrapper import (
|
||||
commands,
|
||||
common,
|
||||
|
@ -983,71 +983,37 @@ class TestAnalystDelete(MockEppLib):
|
|||
self.assertNotEqual(self.domain, None)
|
||||
# Domain should have the right state
|
||||
self.assertEqual(self.domain.state, Domain.State.DELETED)
|
||||
|
||||
def test_analyst_deletes_domain_idempotent(self):
|
||||
"""
|
||||
Scenario: Analyst tries to delete an already deleted domain
|
||||
Given `state` is already `DELETED`
|
||||
When `domain.deletedInEpp()` is called
|
||||
Then `commands.DeleteDomain` is sent to the registry
|
||||
And Domain returns normally (without error)
|
||||
"""
|
||||
# Put the domain in client hold
|
||||
self.domain.place_client_hold()
|
||||
# Delete it...
|
||||
self.domain.deletedInEpp()
|
||||
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)
|
||||
|
||||
# Delete it again - monitoring for errors
|
||||
try:
|
||||
self.domain.deletedInEpp()
|
||||
except Exception as err:
|
||||
self.fail("deletedInEpp() threw an error")
|
||||
raise err
|
||||
|
||||
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)
|
||||
# Cache should be invalidated
|
||||
self.assertEqual(self.domain._cache, {})
|
||||
|
||||
def test_deletion_is_unsuccessful(self):
|
||||
"""
|
||||
Scenario: Domain deletion is unsuccessful
|
||||
When an error is returned from epplibwrapper
|
||||
Then a user-friendly error message is returned for displaying on the web
|
||||
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="fail.gov", state=Domain.State.ON_HOLD
|
||||
name="failDelete.gov", state=Domain.State.ON_HOLD
|
||||
)
|
||||
# Put the domain in client hold
|
||||
domain.place_client_hold()
|
||||
# Delete it...
|
||||
|
||||
# Delete it...
|
||||
with self.assertRaises(RegistryError) as err:
|
||||
domain.deletedInEpp()
|
||||
self.assertTrue(
|
||||
err.is_client_error()
|
||||
and err.code == ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION
|
||||
and err.code == ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION
|
||||
)
|
||||
self.mockedSendFunction.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.DeleteDomain(name="failDelete.gov"),
|
||||
cleaned=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
# TODO - check UI for error
|
||||
# Domain itself should not be deleted
|
||||
|
@ -1055,25 +1021,23 @@ class TestAnalystDelete(MockEppLib):
|
|||
# State should not have changed
|
||||
self.assertEqual(domain.state, Domain.State.ON_HOLD)
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_deletion_ready_fsm_failure(self):
|
||||
"""
|
||||
Scenario: Domain deletion is unsuccessful due to FSM rules
|
||||
Given state is 'ready'
|
||||
When `domain.deletedInEpp()` is called
|
||||
Then a user-friendly error message is returned for displaying on the web
|
||||
and domain is of `state` is `READY`
|
||||
Then an FSM error is returned
|
||||
And `state` is not set to `DELETED`
|
||||
"""
|
||||
self.assertEqual(self.domain.state, Domain.State.READY)
|
||||
with self.assertRaises(TransitionNotAllowed) as err:
|
||||
self.domain.deletedInEpp()
|
||||
self.mockedSendFunction.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.DeleteDomain(name="fake.gov", auth_info=None),
|
||||
cleaned=True,
|
||||
)
|
||||
]
|
||||
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, "DELETED")
|
||||
self.assertEqual(self.domain.state, Domain.State.READY)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue