mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-31 09:43:54 +02:00
Merge remote-tracking branch 'origin/main' into rjm/1027-groups-permissions-logging
This commit is contained in:
commit
3ea1f5aa77
16 changed files with 952 additions and 141 deletions
1
.github/workflows/deploy-sandbox.yaml
vendored
1
.github/workflows/deploy-sandbox.yaml
vendored
|
@ -19,6 +19,7 @@ jobs:
|
||||||
|| startsWith(github.head_ref, 'rh/')
|
|| startsWith(github.head_ref, 'rh/')
|
||||||
|| startsWith(github.head_ref, 'nl/')
|
|| startsWith(github.head_ref, 'nl/')
|
||||||
|| startsWith(github.head_ref, 'dk/')
|
|| startsWith(github.head_ref, 'dk/')
|
||||||
|
|| startsWith(github.head_ref, 'es/')
|
||||||
outputs:
|
outputs:
|
||||||
environment: ${{ steps.var.outputs.environment}}
|
environment: ${{ steps.var.outputs.environment}}
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
|
|
1
.github/workflows/migrate.yaml
vendored
1
.github/workflows/migrate.yaml
vendored
|
@ -15,6 +15,7 @@ on:
|
||||||
options:
|
options:
|
||||||
- stable
|
- stable
|
||||||
- staging
|
- staging
|
||||||
|
- es
|
||||||
- nl
|
- nl
|
||||||
- rh
|
- rh
|
||||||
- za
|
- za
|
||||||
|
|
1
.github/workflows/reset-db.yaml
vendored
1
.github/workflows/reset-db.yaml
vendored
|
@ -16,6 +16,7 @@ on:
|
||||||
options:
|
options:
|
||||||
- stable
|
- stable
|
||||||
- staging
|
- staging
|
||||||
|
- es
|
||||||
- nl
|
- nl
|
||||||
- rh
|
- rh
|
||||||
- za
|
- za
|
||||||
|
|
30
ops/manifests/manifest-es.yaml
Normal file
30
ops/manifests/manifest-es.yaml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
applications:
|
||||||
|
- name: getgov-es
|
||||||
|
buildpacks:
|
||||||
|
- python_buildpack
|
||||||
|
path: ../../src
|
||||||
|
instances: 1
|
||||||
|
memory: 512M
|
||||||
|
stack: cflinuxfs4
|
||||||
|
timeout: 180
|
||||||
|
command: ./run.sh
|
||||||
|
health-check-type: http
|
||||||
|
health-check-http-endpoint: /health
|
||||||
|
health-check-invocation-timeout: 40
|
||||||
|
env:
|
||||||
|
# Send stdout and stderr straight to the terminal without buffering
|
||||||
|
PYTHONUNBUFFERED: yup
|
||||||
|
# Tell Django where to find its configuration
|
||||||
|
DJANGO_SETTINGS_MODULE: registrar.config.settings
|
||||||
|
# Tell Django where it is being hosted
|
||||||
|
DJANGO_BASE_URL: https://getgov-es.app.cloud.gov
|
||||||
|
# Tell Django how much stuff to log
|
||||||
|
DJANGO_LOG_LEVEL: INFO
|
||||||
|
# default public site location
|
||||||
|
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||||
|
routes:
|
||||||
|
- route: getgov-es.app.cloud.gov
|
||||||
|
services:
|
||||||
|
- getgov-credentials
|
||||||
|
- getgov-es-database
|
2
src/Pipfile.lock
generated
2
src/Pipfile.lock
generated
|
@ -353,7 +353,7 @@
|
||||||
},
|
},
|
||||||
"fred-epplib": {
|
"fred-epplib": {
|
||||||
"git": "https://github.com/cisagov/epplib.git",
|
"git": "https://github.com/cisagov/epplib.git",
|
||||||
"ref": "f818cbf0b069a12f03e1d72e4b9f4900924b832d"
|
"ref": "d56d183f1664f34c40ca9716a3a9a345f0ef561c"
|
||||||
},
|
},
|
||||||
"furl": {
|
"furl": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
|
@ -39,7 +39,7 @@ class AvailableViewTest(TestCase):
|
||||||
self.assertIn("gsa.gov", domains)
|
self.assertIn("gsa.gov", domains)
|
||||||
# entries are all lowercase so GSA.GOV is not in the set
|
# entries are all lowercase so GSA.GOV is not in the set
|
||||||
self.assertNotIn("GSA.GOV", domains)
|
self.assertNotIn("GSA.GOV", domains)
|
||||||
self.assertNotIn("igorville.gov", domains)
|
self.assertNotIn("igorvilleremixed.gov", domains)
|
||||||
# all the entries have dots
|
# all the entries have dots
|
||||||
self.assertNotIn("gsa", domains)
|
self.assertNotIn("gsa", domains)
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ class AvailableViewTest(TestCase):
|
||||||
# input is lowercased so GSA.GOV should be found
|
# input is lowercased so GSA.GOV should be found
|
||||||
self.assertTrue(in_domains("GSA.GOV"))
|
self.assertTrue(in_domains("GSA.GOV"))
|
||||||
# This domain should not have been registered
|
# This domain should not have been registered
|
||||||
self.assertFalse(in_domains("igorville.gov"))
|
self.assertFalse(in_domains("igorvilleremixed.gov"))
|
||||||
|
|
||||||
def test_in_domains_dotgov(self):
|
def test_in_domains_dotgov(self):
|
||||||
"""Domain searches work without trailing .gov"""
|
"""Domain searches work without trailing .gov"""
|
||||||
|
@ -56,7 +56,7 @@ class AvailableViewTest(TestCase):
|
||||||
# input is lowercased so GSA.GOV should be found
|
# input is lowercased so GSA.GOV should be found
|
||||||
self.assertTrue(in_domains("GSA"))
|
self.assertTrue(in_domains("GSA"))
|
||||||
# This domain should not have been registered
|
# This domain should not have been registered
|
||||||
self.assertFalse(in_domains("igorville"))
|
self.assertFalse(in_domains("igorvilleremixed"))
|
||||||
|
|
||||||
def test_not_available_domain(self):
|
def test_not_available_domain(self):
|
||||||
"""gsa.gov is not available"""
|
"""gsa.gov is not available"""
|
||||||
|
@ -66,17 +66,17 @@ class AvailableViewTest(TestCase):
|
||||||
self.assertFalse(json.loads(response.content)["available"])
|
self.assertFalse(json.loads(response.content)["available"])
|
||||||
|
|
||||||
def test_available_domain(self):
|
def test_available_domain(self):
|
||||||
"""igorville.gov is still available"""
|
"""igorvilleremixed.gov is still available"""
|
||||||
request = self.factory.get(API_BASE_PATH + "igorville.gov")
|
request = self.factory.get(API_BASE_PATH + "igorvilleremixed.gov")
|
||||||
request.user = self.user
|
request.user = self.user
|
||||||
response = available(request, domain="igorville.gov")
|
response = available(request, domain="igorvilleremixed.gov")
|
||||||
self.assertTrue(json.loads(response.content)["available"])
|
self.assertTrue(json.loads(response.content)["available"])
|
||||||
|
|
||||||
def test_available_domain_dotgov(self):
|
def test_available_domain_dotgov(self):
|
||||||
"""igorville.gov is still available even without the .gov suffix"""
|
"""igorvilleremixed.gov is still available even without the .gov suffix"""
|
||||||
request = self.factory.get(API_BASE_PATH + "igorville")
|
request = self.factory.get(API_BASE_PATH + "igorvilleremixed")
|
||||||
request.user = self.user
|
request.user = self.user
|
||||||
response = available(request, domain="igorville")
|
response = available(request, domain="igorvilleremixed")
|
||||||
self.assertTrue(json.loads(response.content)["available"])
|
self.assertTrue(json.loads(response.content)["available"])
|
||||||
|
|
||||||
def test_error_handling(self):
|
def test_error_handling(self):
|
||||||
|
|
|
@ -11,6 +11,7 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
NAMESPACE = SimpleNamespace(
|
NAMESPACE = SimpleNamespace(
|
||||||
EPP="urn:ietf:params:xml:ns:epp-1.0",
|
EPP="urn:ietf:params:xml:ns:epp-1.0",
|
||||||
|
SEC_DNS="urn:ietf:params:xml:ns:secDNS-1.1",
|
||||||
XSI="http://www.w3.org/2001/XMLSchema-instance",
|
XSI="http://www.w3.org/2001/XMLSchema-instance",
|
||||||
FRED="noop",
|
FRED="noop",
|
||||||
NIC_CONTACT="urn:ietf:params:xml:ns:contact-1.0",
|
NIC_CONTACT="urn:ietf:params:xml:ns:contact-1.0",
|
||||||
|
@ -25,6 +26,7 @@ NAMESPACE = SimpleNamespace(
|
||||||
SCHEMA_LOCATION = SimpleNamespace(
|
SCHEMA_LOCATION = SimpleNamespace(
|
||||||
XSI="urn:ietf:params:xml:ns:epp-1.0 epp-1.0.xsd",
|
XSI="urn:ietf:params:xml:ns:epp-1.0 epp-1.0.xsd",
|
||||||
FRED="noop fred-1.5.0.xsd",
|
FRED="noop fred-1.5.0.xsd",
|
||||||
|
SEC_DNS="urn:ietf:params:xml:ns:secDNS-1.1 secDNS-1.1.xsd",
|
||||||
NIC_CONTACT="urn:ietf:params:xml:ns:contact-1.0 contact-1.0.xsd",
|
NIC_CONTACT="urn:ietf:params:xml:ns:contact-1.0 contact-1.0.xsd",
|
||||||
NIC_DOMAIN="urn:ietf:params:xml:ns:domain-1.0 domain-1.0.xsd",
|
NIC_DOMAIN="urn:ietf:params:xml:ns:domain-1.0 domain-1.0.xsd",
|
||||||
NIC_ENUMVAL="noop enumval-1.2.0.xsd",
|
NIC_ENUMVAL="noop enumval-1.2.0.xsd",
|
||||||
|
@ -45,6 +47,8 @@ try:
|
||||||
from .client import CLIENT, commands
|
from .client import CLIENT, commands
|
||||||
from .errors import RegistryError, ErrorCode
|
from .errors import RegistryError, ErrorCode
|
||||||
from epplib.models import common
|
from epplib.models import common
|
||||||
|
from epplib.responses import extensions
|
||||||
|
from epplib import responses
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -52,6 +56,8 @@ __all__ = [
|
||||||
"CLIENT",
|
"CLIENT",
|
||||||
"commands",
|
"commands",
|
||||||
"common",
|
"common",
|
||||||
|
"extensions",
|
||||||
|
"responses",
|
||||||
"ErrorCode",
|
"ErrorCode",
|
||||||
"RegistryError",
|
"RegistryError",
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,10 +7,13 @@ from django.contrib.auth.models import Group
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.http.response import HttpResponseRedirect
|
from django.http.response import HttpResponseRedirect
|
||||||
from django.urls import reverse
|
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 registrar.models.utility.admin_sort_fields import AdminSortFields
|
||||||
from . import models
|
from . import models
|
||||||
from auditlog.models import LogEntry # type: ignore
|
from auditlog.models import LogEntry # type: ignore
|
||||||
from auditlog.admin import LogEntryAdmin # type: ignore
|
from auditlog.admin import LogEntryAdmin # type: ignore
|
||||||
|
from django_fsm import TransitionNotAllowed # type: ignore
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -736,16 +739,61 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
return super().response_change(request, obj)
|
return super().response_change(request, obj)
|
||||||
|
|
||||||
def do_delete_domain(self, 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).
|
||||||
|
# We do not want to accidentally delete records.
|
||||||
|
self.message_user(request, "Object is not of type Domain", messages.ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
obj.deleted()
|
obj.deletedInEpp()
|
||||||
obj.save()
|
obj.save()
|
||||||
except Exception as err:
|
except RegistryError as err:
|
||||||
self.message_user(request, err, messages.ERROR)
|
# Using variables to get past the linter
|
||||||
|
message1 = f"Cannot delete Domain when in state {obj.state}"
|
||||||
|
message2 = "This subdomain is being used as a hostname on another domain"
|
||||||
|
# Human-readable mappings of ErrorCodes. Can be expanded.
|
||||||
|
error_messages = {
|
||||||
|
# noqa on these items as black wants to reformat to an invalid length
|
||||||
|
ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION: message1,
|
||||||
|
ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION: message2,
|
||||||
|
}
|
||||||
|
|
||||||
|
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:
|
||||||
|
if obj.state == Domain.State.DELETED:
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
"This domain is already deleted",
|
||||||
|
messages.INFO,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
"Error deleting this Domain: "
|
||||||
|
f"Can't switch from state '{obj.state}' to 'deleted'"
|
||||||
|
", must be either 'dns_needed' or 'on_hold'",
|
||||||
|
messages.ERROR,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
"Could not delete: An unspecified error occured",
|
||||||
|
messages.ERROR,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.message_user(
|
self.message_user(
|
||||||
request,
|
request,
|
||||||
("Domain %s Should now be deleted " ". Thanks!") % obj.name,
|
("Domain %s has been deleted. Thanks!") % obj.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
return HttpResponseRedirect(".")
|
return HttpResponseRedirect(".")
|
||||||
|
|
||||||
def do_get_status(self, request, obj):
|
def do_get_status(self, request, obj):
|
||||||
|
|
|
@ -570,6 +570,7 @@ SECURE_SSL_REDIRECT = True
|
||||||
ALLOWED_HOSTS = [
|
ALLOWED_HOSTS = [
|
||||||
"getgov-stable.app.cloud.gov",
|
"getgov-stable.app.cloud.gov",
|
||||||
"getgov-staging.app.cloud.gov",
|
"getgov-staging.app.cloud.gov",
|
||||||
|
"getgov-es.app.cloud.gov",
|
||||||
"getgov-nl.app.cloud.gov",
|
"getgov-nl.app.cloud.gov",
|
||||||
"getgov-rh.app.cloud.gov",
|
"getgov-rh.app.cloud.gov",
|
||||||
"getgov-za.app.cloud.gov",
|
"getgov-za.app.cloud.gov",
|
||||||
|
|
|
@ -2,7 +2,7 @@ import logging
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from string import digits
|
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
|
from django.db import models
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ from epplibwrapper import (
|
||||||
CLIENT as registry,
|
CLIENT as registry,
|
||||||
commands,
|
commands,
|
||||||
common as epp,
|
common as epp,
|
||||||
|
extensions,
|
||||||
RegistryError,
|
RegistryError,
|
||||||
ErrorCode,
|
ErrorCode,
|
||||||
)
|
)
|
||||||
|
@ -279,6 +280,27 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
logger.error("Error _create_host, code was %s error was %s" % (e.code, e))
|
logger.error("Error _create_host, code was %s error was %s" % (e.code, e))
|
||||||
return e.code
|
return e.code
|
||||||
|
|
||||||
|
@Cache
|
||||||
|
def dnssecdata(self) -> extensions.DNSSECExtension:
|
||||||
|
return self._get_property("dnssecdata")
|
||||||
|
|
||||||
|
@dnssecdata.setter # type: ignore
|
||||||
|
def dnssecdata(self, _dnssecdata: extensions.DNSSECExtension):
|
||||||
|
updateParams = {
|
||||||
|
"maxSigLife": _dnssecdata.get("maxSigLife", None),
|
||||||
|
"dsData": _dnssecdata.get("dsData", None),
|
||||||
|
"keyData": _dnssecdata.get("keyData", None),
|
||||||
|
"remAllDsKeyData": True,
|
||||||
|
}
|
||||||
|
request = commands.UpdateDomain(name=self.name)
|
||||||
|
extension = commands.UpdateDomainDNSSECExtension(**updateParams)
|
||||||
|
request.add_extension(extension)
|
||||||
|
try:
|
||||||
|
registry.send(request, cleaned=True)
|
||||||
|
except RegistryError as e:
|
||||||
|
logger.error("Error adding DNSSEC, code was %s error was %s" % (e.code, e))
|
||||||
|
raise e
|
||||||
|
|
||||||
@nameservers.setter # type: ignore
|
@nameservers.setter # type: ignore
|
||||||
def nameservers(self, hosts: list[tuple[str]]):
|
def nameservers(self, hosts: list[tuple[str]]):
|
||||||
"""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
|
||||||
|
@ -609,11 +631,6 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
"""
|
"""
|
||||||
return self.state == self.State.READY
|
return self.state == self.State.READY
|
||||||
|
|
||||||
def delete_request(self):
|
|
||||||
"""Delete from host. Possibly a duplicate of _delete_host?"""
|
|
||||||
# TODO fix in ticket #901
|
|
||||||
pass
|
|
||||||
|
|
||||||
def transfer(self):
|
def transfer(self):
|
||||||
"""Going somewhere. Not implemented."""
|
"""Going somewhere. Not implemented."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
@ -658,7 +675,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
"""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"""
|
||||||
request = commands.DeleteDomain(name=self.name)
|
request = commands.DeleteDomain(name=self.name)
|
||||||
registry.send(request)
|
registry.send(request, cleaned=True)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -725,9 +742,9 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
try:
|
try:
|
||||||
logger.info("Getting domain info from epp")
|
logger.info("Getting domain info from epp")
|
||||||
req = commands.InfoDomain(name=self.name)
|
req = commands.InfoDomain(name=self.name)
|
||||||
domainInfo = registry.send(req, cleaned=True).res_data[0]
|
domainInfoResponse = registry.send(req, cleaned=True)
|
||||||
exitEarly = True
|
exitEarly = True
|
||||||
return domainInfo
|
return domainInfoResponse
|
||||||
except RegistryError as e:
|
except RegistryError as e:
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
|
@ -804,16 +821,32 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
self._remove_client_hold()
|
self._remove_client_hold()
|
||||||
# TODO -on the client hold ticket any additional error handling here
|
# TODO -on the client hold ticket any additional error handling here
|
||||||
|
|
||||||
@transition(field="state", source=State.ON_HOLD, target=State.DELETED)
|
@transition(
|
||||||
def deleted(self):
|
field="state", source=[State.ON_HOLD, State.DNS_NEEDED], target=State.DELETED
|
||||||
"""domain is deleted in epp but is saved in our database"""
|
)
|
||||||
# TODO Domains may not be deleted if:
|
def deletedInEpp(self):
|
||||||
# a child host is being used by
|
"""Domain is deleted in epp but is saved in our database.
|
||||||
# another .gov domains. The host must be first removed
|
Error handling should be provided by the caller."""
|
||||||
# and/or renamed before the parent domain may be deleted.
|
# While we want to log errors, we want to preserve
|
||||||
logger.info("pendingCreate()-> inside pending create")
|
# that information when this function is called.
|
||||||
self._delete_domain()
|
# Human-readable errors are introduced at the admin.py level,
|
||||||
# TODO - delete ticket any additional error handling here
|
# as doing everything here would reduce reliablity.
|
||||||
|
try:
|
||||||
|
logger.info("deletedInEpp()-> inside _delete_domain")
|
||||||
|
self._delete_domain()
|
||||||
|
except RegistryError as err:
|
||||||
|
logger.error(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}"
|
||||||
|
)
|
||||||
|
raise err
|
||||||
|
else:
|
||||||
|
self._invalidate_cache()
|
||||||
|
|
||||||
@transition(
|
@transition(
|
||||||
field="state",
|
field="state",
|
||||||
|
@ -952,7 +985,8 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
"""Contact registry for info about a domain."""
|
"""Contact registry for info about a domain."""
|
||||||
try:
|
try:
|
||||||
# get info from registry
|
# get info from registry
|
||||||
data = self._get_or_create_domain()
|
dataResponse = self._get_or_create_domain()
|
||||||
|
data = dataResponse.res_data[0]
|
||||||
# extract properties from response
|
# extract properties from response
|
||||||
# (Ellipsis is used to mean "null")
|
# (Ellipsis is used to mean "null")
|
||||||
cache = {
|
cache = {
|
||||||
|
@ -974,75 +1008,46 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
# statuses can just be a list no need to keep the epp object
|
# statuses can just be a list no need to keep the epp object
|
||||||
if "statuses" in cleaned.keys():
|
if "statuses" in cleaned.keys():
|
||||||
cleaned["statuses"] = [status.state for status in cleaned["statuses"]]
|
cleaned["statuses"] = [status.state for status in cleaned["statuses"]]
|
||||||
|
|
||||||
|
# get extensions info, if there is any
|
||||||
|
# DNSSECExtension is one possible extension, make sure to handle
|
||||||
|
# only DNSSECExtension and not other type extensions
|
||||||
|
returned_extensions = dataResponse.extensions
|
||||||
|
cleaned["dnssecdata"] = None
|
||||||
|
for extension in returned_extensions:
|
||||||
|
if isinstance(extension, extensions.DNSSECExtension):
|
||||||
|
cleaned["dnssecdata"] = extension
|
||||||
|
# Capture and store old hosts and contacts from cache if they exist
|
||||||
|
old_cache_hosts = self._cache.get("hosts")
|
||||||
|
old_cache_contacts = self._cache.get("contacts")
|
||||||
|
|
||||||
# get contact info, if there are any
|
# get contact info, if there are any
|
||||||
if (
|
if (
|
||||||
# fetch_contacts and
|
fetch_contacts
|
||||||
"_contacts" in cleaned
|
and "_contacts" in cleaned
|
||||||
and isinstance(cleaned["_contacts"], list)
|
and isinstance(cleaned["_contacts"], list)
|
||||||
and len(cleaned["_contacts"])
|
and len(cleaned["_contacts"])
|
||||||
):
|
):
|
||||||
cleaned["contacts"] = []
|
cleaned["contacts"] = self._fetch_contacts(cleaned["_contacts"])
|
||||||
for domainContact in cleaned["_contacts"]:
|
# We're only getting contacts, so retain the old
|
||||||
# we do not use _get_or_create_* because we expect the object we
|
# hosts that existed in cache (if they existed)
|
||||||
# just asked the registry for still exists --
|
# and pass them along.
|
||||||
# if not, that's a problem
|
if old_cache_hosts is not None:
|
||||||
|
cleaned["hosts"] = old_cache_hosts
|
||||||
# TODO- discuss-should we check if contact is in public contacts
|
|
||||||
# and add it if not- this is really to keep in mine the transisiton
|
|
||||||
req = commands.InfoContact(id=domainContact.contact)
|
|
||||||
data = registry.send(req, cleaned=True).res_data[0]
|
|
||||||
|
|
||||||
# extract properties from response
|
|
||||||
# (Ellipsis is used to mean "null")
|
|
||||||
# convert this to use PublicContactInstead
|
|
||||||
contact = {
|
|
||||||
"id": domainContact.contact,
|
|
||||||
"type": domainContact.type,
|
|
||||||
"auth_info": getattr(data, "auth_info", ...),
|
|
||||||
"cr_date": getattr(data, "cr_date", ...),
|
|
||||||
"disclose": getattr(data, "disclose", ...),
|
|
||||||
"email": getattr(data, "email", ...),
|
|
||||||
"fax": getattr(data, "fax", ...),
|
|
||||||
"postal_info": getattr(data, "postal_info", ...),
|
|
||||||
"statuses": getattr(data, "statuses", ...),
|
|
||||||
"tr_date": getattr(data, "tr_date", ...),
|
|
||||||
"up_date": getattr(data, "up_date", ...),
|
|
||||||
"voice": getattr(data, "voice", ...),
|
|
||||||
}
|
|
||||||
|
|
||||||
cleaned["contacts"].append(
|
|
||||||
{k: v for k, v in contact.items() if v is not ...}
|
|
||||||
)
|
|
||||||
|
|
||||||
# get nameserver info, if there are any
|
# get nameserver info, if there are any
|
||||||
if (
|
if (
|
||||||
# fetch_hosts and
|
fetch_hosts
|
||||||
"_hosts" in cleaned
|
and "_hosts" in cleaned
|
||||||
and isinstance(cleaned["_hosts"], list)
|
and isinstance(cleaned["_hosts"], list)
|
||||||
and len(cleaned["_hosts"])
|
and len(cleaned["_hosts"])
|
||||||
):
|
):
|
||||||
# TODO- add elif in cache set it to be the old cache value
|
cleaned["hosts"] = self._fetch_hosts(cleaned["_hosts"])
|
||||||
# no point in removing
|
# We're only getting hosts, so retain the old
|
||||||
cleaned["hosts"] = []
|
# contacts that existed in cache (if they existed)
|
||||||
for name in cleaned["_hosts"]:
|
# and pass them along.
|
||||||
# we do not use _get_or_create_* because we expect the object we
|
if old_cache_contacts is not None:
|
||||||
# just asked the registry for still exists --
|
cleaned["contacts"] = old_cache_contacts
|
||||||
# if not, that's a problem
|
|
||||||
req = commands.InfoHost(name=name)
|
|
||||||
data = registry.send(req, cleaned=True).res_data[0]
|
|
||||||
# extract properties from response
|
|
||||||
# (Ellipsis is used to mean "null")
|
|
||||||
host = {
|
|
||||||
"name": name,
|
|
||||||
"addrs": getattr(data, "addrs", ...),
|
|
||||||
"cr_date": getattr(data, "cr_date", ...),
|
|
||||||
"statuses": getattr(data, "statuses", ...),
|
|
||||||
"tr_date": getattr(data, "tr_date", ...),
|
|
||||||
"up_date": getattr(data, "up_date", ...),
|
|
||||||
}
|
|
||||||
cleaned["hosts"].append(
|
|
||||||
{k: v for k, v in host.items() if v is not ...}
|
|
||||||
)
|
|
||||||
|
|
||||||
# replace the prior cache with new data
|
# replace the prior cache with new data
|
||||||
self._cache = cleaned
|
self._cache = cleaned
|
||||||
|
@ -1050,6 +1055,46 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
except RegistryError as e:
|
except RegistryError as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
|
|
||||||
|
def _fetch_contacts(self, contact_data):
|
||||||
|
"""Fetch contact info."""
|
||||||
|
contacts = []
|
||||||
|
for domainContact in contact_data:
|
||||||
|
req = commands.InfoContact(id=domainContact.contact)
|
||||||
|
data = registry.send(req, cleaned=True).res_data[0]
|
||||||
|
contact = {
|
||||||
|
"id": domainContact.contact,
|
||||||
|
"type": domainContact.type,
|
||||||
|
"auth_info": getattr(data, "auth_info", ...),
|
||||||
|
"cr_date": getattr(data, "cr_date", ...),
|
||||||
|
"disclose": getattr(data, "disclose", ...),
|
||||||
|
"email": getattr(data, "email", ...),
|
||||||
|
"fax": getattr(data, "fax", ...),
|
||||||
|
"postal_info": getattr(data, "postal_info", ...),
|
||||||
|
"statuses": getattr(data, "statuses", ...),
|
||||||
|
"tr_date": getattr(data, "tr_date", ...),
|
||||||
|
"up_date": getattr(data, "up_date", ...),
|
||||||
|
"voice": getattr(data, "voice", ...),
|
||||||
|
}
|
||||||
|
contacts.append({k: v for k, v in contact.items() if v is not ...})
|
||||||
|
return contacts
|
||||||
|
|
||||||
|
def _fetch_hosts(self, host_data):
|
||||||
|
"""Fetch host info."""
|
||||||
|
hosts = []
|
||||||
|
for name in host_data:
|
||||||
|
req = commands.InfoHost(name=name)
|
||||||
|
data = registry.send(req, cleaned=True).res_data[0]
|
||||||
|
host = {
|
||||||
|
"name": name,
|
||||||
|
"addrs": getattr(data, "addrs", ...),
|
||||||
|
"cr_date": getattr(data, "cr_date", ...),
|
||||||
|
"statuses": getattr(data, "statuses", ...),
|
||||||
|
"tr_date": getattr(data, "tr_date", ...),
|
||||||
|
"up_date": getattr(data, "up_date", ...),
|
||||||
|
}
|
||||||
|
hosts.append({k: v for k, v in host.items() if v is not ...})
|
||||||
|
return hosts
|
||||||
|
|
||||||
def _invalidate_cache(self):
|
def _invalidate_cache(self):
|
||||||
"""Remove cache data when updates are made."""
|
"""Remove cache data when updates are made."""
|
||||||
self._cache = {}
|
self._cache = {}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import logging
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django_fsm import FSMField, transition # type: ignore
|
from django_fsm import FSMField, transition # type: ignore
|
||||||
|
from registrar.models.domain import Domain
|
||||||
|
|
||||||
from .utility.time_stamped_model import TimeStampedModel
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
from ..utility.email import send_templated_email, EmailSendingError
|
from ..utility.email import send_templated_email, EmailSendingError
|
||||||
|
@ -610,9 +611,11 @@ class DomainApplication(TimeStampedModel):
|
||||||
|
|
||||||
As side effects this will delete the domain and domain_information
|
As side effects this will delete the domain and domain_information
|
||||||
(will cascade), and send an email notification."""
|
(will cascade), and send an email notification."""
|
||||||
|
|
||||||
if self.status == self.APPROVED:
|
if self.status == self.APPROVED:
|
||||||
self.approved_domain.delete_request()
|
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.delete()
|
self.approved_domain.delete()
|
||||||
self.approved_domain = None
|
self.approved_domain = None
|
||||||
|
|
||||||
|
@ -638,7 +641,10 @@ class DomainApplication(TimeStampedModel):
|
||||||
and domain_information (will cascade) when they exist."""
|
and domain_information (will cascade) when they exist."""
|
||||||
|
|
||||||
if self.status == self.APPROVED:
|
if self.status == self.APPROVED:
|
||||||
self.approved_domain.delete_request()
|
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.delete()
|
self.approved_domain.delete()
|
||||||
self.approved_domain = None
|
self.approved_domain = None
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,9 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input id="manageDomainSubmitButton" type="submit" value="Manage Domain" name="_edit_domain">
|
<input id="manageDomainSubmitButton" type="submit" value="Manage Domain" name="_edit_domain">
|
||||||
<input type="submit" value="get status" name="_get_status">
|
<input type="submit" value="get status" name="_get_status">
|
||||||
<input type="submit" value="EPP Delete Domain" name="_delete_domain">
|
{% if original.state != original.State.DELETED %}
|
||||||
|
<input type="submit" value="Delete Domain in Registry" name="_delete_domain">
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -619,6 +619,16 @@ class MockEppLib(TestCase):
|
||||||
# use this for when a contact is being updated
|
# use this for when a contact is being updated
|
||||||
# sets the second send() to fail
|
# sets the second send() to fail
|
||||||
raise RegistryError(code=ErrorCode.OBJECT_EXISTS)
|
raise RegistryError(code=ErrorCode.OBJECT_EXISTS)
|
||||||
|
elif (
|
||||||
|
isinstance(_request, commands.DeleteDomain)
|
||||||
|
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_ASSOCIATION_PROHIBITS_OPERATION
|
||||||
|
)
|
||||||
return MagicMock(res_data=[self.mockDataInfoHosts])
|
return MagicMock(res_data=[self.mockDataInfoHosts])
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
@ -49,6 +49,7 @@ class TestDomainAdmin(MockEppLib):
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
self.superuser = create_superuser()
|
self.superuser = create_superuser()
|
||||||
self.staffuser = create_user()
|
self.staffuser = create_user()
|
||||||
|
self.factory = RequestFactory()
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
@skip("Why did this test stop working, and is is a good test")
|
@skip("Why did this test stop working, and is is a good test")
|
||||||
|
@ -88,6 +89,155 @@ class TestDomainAdmin(MockEppLib):
|
||||||
self.assertContains(response, "Place hold")
|
self.assertContains(response, "Place hold")
|
||||||
self.assertNotContains(response, "Remove 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()
|
||||||
|
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, "Delete Domain in Registry")
|
||||||
|
|
||||||
|
# Test the info dialog
|
||||||
|
request = self.factory.post(
|
||||||
|
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||||
|
{"_delete_domain": "Delete Domain in 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):
|
||||||
|
"""
|
||||||
|
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()
|
||||||
|
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, "Delete Domain in Registry")
|
||||||
|
|
||||||
|
# Test the error
|
||||||
|
request = self.factory.post(
|
||||||
|
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||||
|
{"_delete_domain": "Delete Domain in 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)
|
||||||
|
|
||||||
|
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()
|
||||||
|
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, "Delete Domain in Registry")
|
||||||
|
|
||||||
|
# Test the info dialog
|
||||||
|
request = self.factory.post(
|
||||||
|
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||||
|
{"_delete_domain": "Delete Domain in 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)
|
||||||
|
|
||||||
|
# Try to delete it again
|
||||||
|
# Test the info dialog
|
||||||
|
request = self.factory.post(
|
||||||
|
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||||
|
{"_delete_domain": "Delete Domain in 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")
|
@skip("Waiting on epp lib to implement")
|
||||||
def test_place_and_remove_hold_epp(self):
|
def test_place_and_remove_hold_epp(self):
|
||||||
raise
|
raise
|
||||||
|
@ -139,8 +289,9 @@ class TestDomainApplicationAdminForm(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestDomainApplicationAdmin(TestCase):
|
class TestDomainApplicationAdmin(MockEppLib):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
self.site = AdminSite()
|
self.site = AdminSite()
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
self.admin = DomainApplicationAdmin(
|
self.admin = DomainApplicationAdmin(
|
||||||
|
@ -691,6 +842,7 @@ class TestDomainApplicationAdmin(TestCase):
|
||||||
domain_information.refresh_from_db()
|
domain_information.refresh_from_db()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
Domain.objects.all().delete()
|
Domain.objects.all().delete()
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
DomainApplication.objects.all().delete()
|
DomainApplication.objects.all().delete()
|
||||||
|
|
|
@ -3,9 +3,10 @@ Feature being tested: Registry Integration
|
||||||
|
|
||||||
This file tests the various ways in which the registrar interacts with the registry.
|
This file tests the various ways in which the registrar interacts with the registry.
|
||||||
"""
|
"""
|
||||||
|
from typing import Mapping, Any
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from unittest.mock import patch, call
|
from unittest.mock import MagicMock, patch, call
|
||||||
import datetime
|
import datetime
|
||||||
from registrar.models import Domain
|
from registrar.models import Domain
|
||||||
|
|
||||||
|
@ -16,10 +17,12 @@ from registrar.models.draft_domain import DraftDomain
|
||||||
from registrar.models.public_contact import PublicContact
|
from registrar.models.public_contact import PublicContact
|
||||||
from registrar.models.user import User
|
from registrar.models.user import User
|
||||||
from .common import MockEppLib
|
from .common import MockEppLib
|
||||||
|
from django_fsm import TransitionNotAllowed # type: ignore
|
||||||
from epplibwrapper import (
|
from epplibwrapper import (
|
||||||
commands,
|
commands,
|
||||||
common,
|
common,
|
||||||
|
extensions,
|
||||||
|
responses,
|
||||||
RegistryError,
|
RegistryError,
|
||||||
ErrorCode,
|
ErrorCode,
|
||||||
)
|
)
|
||||||
|
@ -51,8 +54,6 @@ class TestDomainCache(MockEppLib):
|
||||||
commands.InfoDomain(name="igorville.gov", auth_info=None),
|
commands.InfoDomain(name="igorville.gov", auth_info=None),
|
||||||
cleaned=True,
|
cleaned=True,
|
||||||
),
|
),
|
||||||
call(commands.InfoContact(id="123", auth_info=None), cleaned=True),
|
|
||||||
call(commands.InfoHost(name="fake.host.com"), cleaned=True),
|
|
||||||
],
|
],
|
||||||
any_order=False, # Ensure calls are in the specified order
|
any_order=False, # Ensure calls are in the specified order
|
||||||
)
|
)
|
||||||
|
@ -74,8 +75,6 @@ class TestDomainCache(MockEppLib):
|
||||||
call(
|
call(
|
||||||
commands.InfoDomain(name="igorville.gov", auth_info=None), cleaned=True
|
commands.InfoDomain(name="igorville.gov", auth_info=None), cleaned=True
|
||||||
),
|
),
|
||||||
call(commands.InfoContact(id="123", auth_info=None), cleaned=True),
|
|
||||||
call(commands.InfoHost(name="fake.host.com"), cleaned=True),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
self.mockedSendFunction.assert_has_calls(expectedCalls)
|
self.mockedSendFunction.assert_has_calls(expectedCalls)
|
||||||
|
@ -110,6 +109,19 @@ class TestDomainCache(MockEppLib):
|
||||||
# get and check hosts is set correctly
|
# get and check hosts is set correctly
|
||||||
domain._get_property("hosts")
|
domain._get_property("hosts")
|
||||||
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
|
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 contacts
|
||||||
|
domain._get_property("contacts")
|
||||||
|
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
|
||||||
|
self.assertEqual(domain._cache["contacts"], [expectedContactsDict])
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
def tearDown(self) -> None:
|
||||||
Domain.objects.all().delete()
|
Domain.objects.all().delete()
|
||||||
|
@ -170,8 +182,6 @@ class TestDomainCreation(MockEppLib):
|
||||||
commands.InfoDomain(name="beef-tongue.gov", auth_info=None),
|
commands.InfoDomain(name="beef-tongue.gov", auth_info=None),
|
||||||
cleaned=True,
|
cleaned=True,
|
||||||
),
|
),
|
||||||
call(commands.InfoContact(id="123", auth_info=None), cleaned=True),
|
|
||||||
call(commands.InfoHost(name="fake.host.com"), cleaned=True),
|
|
||||||
],
|
],
|
||||||
any_order=False, # Ensure calls are in the specified order
|
any_order=False, # Ensure calls are in the specified order
|
||||||
)
|
)
|
||||||
|
@ -221,8 +231,6 @@ class TestDomainStatuses(MockEppLib):
|
||||||
commands.InfoDomain(name="chicken-liver.gov", auth_info=None),
|
commands.InfoDomain(name="chicken-liver.gov", auth_info=None),
|
||||||
cleaned=True,
|
cleaned=True,
|
||||||
),
|
),
|
||||||
call(commands.InfoContact(id="123", auth_info=None), cleaned=True),
|
|
||||||
call(commands.InfoHost(name="fake.host.com"), cleaned=True),
|
|
||||||
],
|
],
|
||||||
any_order=False, # Ensure calls are in the specified order
|
any_order=False, # Ensure calls are in the specified order
|
||||||
)
|
)
|
||||||
|
@ -263,6 +271,114 @@ class TestDomainStatuses(MockEppLib):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
|
||||||
|
|
||||||
|
class TestDomainAvailable(MockEppLib):
|
||||||
|
"""Test Domain.available"""
|
||||||
|
|
||||||
|
# No SetUp or tearDown necessary for these tests
|
||||||
|
|
||||||
|
def test_domain_available(self):
|
||||||
|
"""
|
||||||
|
Scenario: Testing whether an available domain is available
|
||||||
|
Should return True
|
||||||
|
|
||||||
|
Mock response to mimic EPP Response
|
||||||
|
Validate CheckDomain command is called
|
||||||
|
Validate response given mock
|
||||||
|
"""
|
||||||
|
|
||||||
|
def side_effect(_request, cleaned):
|
||||||
|
return MagicMock(
|
||||||
|
res_data=[
|
||||||
|
responses.check.CheckDomainResultData(
|
||||||
|
name="available.gov", avail=True, reason=None
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
patcher = patch("registrar.models.domain.registry.send")
|
||||||
|
mocked_send = patcher.start()
|
||||||
|
mocked_send.side_effect = side_effect
|
||||||
|
|
||||||
|
available = Domain.available("available.gov")
|
||||||
|
mocked_send.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(
|
||||||
|
commands.CheckDomain(
|
||||||
|
["available.gov"],
|
||||||
|
),
|
||||||
|
cleaned=True,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertTrue(available)
|
||||||
|
patcher.stop()
|
||||||
|
|
||||||
|
def test_domain_unavailable(self):
|
||||||
|
"""
|
||||||
|
Scenario: Testing whether an unavailable domain is available
|
||||||
|
Should return False
|
||||||
|
|
||||||
|
Mock response to mimic EPP Response
|
||||||
|
Validate CheckDomain command is called
|
||||||
|
Validate response given mock
|
||||||
|
"""
|
||||||
|
|
||||||
|
def side_effect(_request, cleaned):
|
||||||
|
return MagicMock(
|
||||||
|
res_data=[
|
||||||
|
responses.check.CheckDomainResultData(
|
||||||
|
name="unavailable.gov", avail=False, reason="In Use"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
patcher = patch("registrar.models.domain.registry.send")
|
||||||
|
mocked_send = patcher.start()
|
||||||
|
mocked_send.side_effect = side_effect
|
||||||
|
|
||||||
|
available = Domain.available("unavailable.gov")
|
||||||
|
mocked_send.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(
|
||||||
|
commands.CheckDomain(
|
||||||
|
["unavailable.gov"],
|
||||||
|
),
|
||||||
|
cleaned=True,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertFalse(available)
|
||||||
|
patcher.stop()
|
||||||
|
|
||||||
|
def test_domain_available_with_value_error(self):
|
||||||
|
"""
|
||||||
|
Scenario: Testing whether an invalid domain is available
|
||||||
|
Should throw ValueError
|
||||||
|
|
||||||
|
Validate ValueError is raised
|
||||||
|
"""
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
Domain.available("invalid-string")
|
||||||
|
|
||||||
|
def test_domain_available_unsuccessful(self):
|
||||||
|
"""
|
||||||
|
Scenario: Testing behavior when registry raises a RegistryError
|
||||||
|
|
||||||
|
Validate RegistryError is raised
|
||||||
|
"""
|
||||||
|
|
||||||
|
def side_effect(_request, cleaned):
|
||||||
|
raise RegistryError(code=ErrorCode.COMMAND_SYNTAX_ERROR)
|
||||||
|
|
||||||
|
patcher = patch("registrar.models.domain.registry.send")
|
||||||
|
mocked_send = patcher.start()
|
||||||
|
mocked_send.side_effect = side_effect
|
||||||
|
|
||||||
|
with self.assertRaises(RegistryError):
|
||||||
|
Domain.available("raises-error.gov")
|
||||||
|
patcher.stop()
|
||||||
|
|
||||||
|
|
||||||
class TestRegistrantContacts(MockEppLib):
|
class TestRegistrantContacts(MockEppLib):
|
||||||
"""Rule: Registrants may modify their WHOIS data"""
|
"""Rule: Registrants may modify their WHOIS data"""
|
||||||
|
|
||||||
|
@ -664,44 +780,372 @@ class TestRegistrantNameservers(TestCase):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
class TestRegistrantDNSSEC(TestCase):
|
class TestRegistrantDNSSEC(MockEppLib):
|
||||||
"""Rule: Registrants may modify their secure DNS data"""
|
"""Rule: Registrants may modify their secure DNS data"""
|
||||||
|
|
||||||
|
# helper function to create UpdateDomainDNSSECExtention object for verification
|
||||||
|
def createUpdateExtension(self, dnssecdata: extensions.DNSSECExtension):
|
||||||
|
return commands.UpdateDomainDNSSECExtension(
|
||||||
|
maxSigLife=dnssecdata.maxSigLife,
|
||||||
|
dsData=dnssecdata.dsData,
|
||||||
|
keyData=dnssecdata.keyData,
|
||||||
|
remDsData=None,
|
||||||
|
remKeyData=None,
|
||||||
|
remAllDsKeyData=True,
|
||||||
|
)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
Background:
|
Background:
|
||||||
Given the registrant is logged in
|
Given the analyst is logged in
|
||||||
And the registrant is the admin on a domain
|
And a domain exists in the registry
|
||||||
"""
|
"""
|
||||||
pass
|
super().setUp()
|
||||||
|
# for the tests, need a domain in the unknown state
|
||||||
|
self.domain, _ = Domain.objects.get_or_create(name="fake.gov")
|
||||||
|
self.addDsData1 = {
|
||||||
|
"keyTag": 1234,
|
||||||
|
"alg": 3,
|
||||||
|
"digestType": 1,
|
||||||
|
"digest": "ec0bdd990b39feead889f0ba613db4adec0bdd99",
|
||||||
|
}
|
||||||
|
self.addDsData2 = {
|
||||||
|
"keyTag": 2345,
|
||||||
|
"alg": 3,
|
||||||
|
"digestType": 1,
|
||||||
|
"digest": "ec0bdd990b39feead889f0ba613db4adecb4adec",
|
||||||
|
}
|
||||||
|
self.keyDataDict = {
|
||||||
|
"flags": 257,
|
||||||
|
"protocol": 3,
|
||||||
|
"alg": 1,
|
||||||
|
"pubKey": "AQPJ////4Q==",
|
||||||
|
}
|
||||||
|
self.dnssecExtensionWithDsData: Mapping[str, Any] = {
|
||||||
|
"dsData": [common.DSData(**self.addDsData1)]
|
||||||
|
}
|
||||||
|
self.dnssecExtensionWithMultDsData: Mapping[str, Any] = {
|
||||||
|
"dsData": [
|
||||||
|
common.DSData(**self.addDsData1),
|
||||||
|
common.DSData(**self.addDsData2),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
self.dnssecExtensionWithKeyData: Mapping[str, Any] = {
|
||||||
|
"maxSigLife": 3215,
|
||||||
|
"keyData": [common.DNSSECKeyData(**self.keyDataDict)],
|
||||||
|
}
|
||||||
|
|
||||||
@skip("not implemented yet")
|
def tearDown(self):
|
||||||
def test_user_adds_dns_data(self):
|
Domain.objects.all().delete()
|
||||||
|
super().tearDown()
|
||||||
|
|
||||||
|
def test_user_adds_dnssec_data(self):
|
||||||
"""
|
"""
|
||||||
Scenario: Registrant adds DNS data
|
Scenario: Registrant adds DNSSEC data.
|
||||||
|
Verify that both the setter and getter are functioning properly
|
||||||
|
|
||||||
|
This test verifies:
|
||||||
|
1 - setter calls UpdateDomain command
|
||||||
|
2 - setter adds the UpdateDNSSECExtension extension to the command
|
||||||
|
3 - setter causes the getter to call info domain on next get from cache
|
||||||
|
4 - getter properly parses dnssecdata from InfoDomain response and sets to cache
|
||||||
|
|
||||||
"""
|
"""
|
||||||
raise
|
|
||||||
|
|
||||||
@skip("not implemented yet")
|
# make sure to stop any other patcher so there are no conflicts
|
||||||
|
self.mockSendPatch.stop()
|
||||||
|
|
||||||
|
def side_effect(_request, cleaned):
|
||||||
|
return MagicMock(
|
||||||
|
res_data=[self.mockDataInfoDomain],
|
||||||
|
extensions=[
|
||||||
|
extensions.DNSSECExtension(**self.dnssecExtensionWithDsData)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
patcher = patch("registrar.models.domain.registry.send")
|
||||||
|
mocked_send = patcher.start()
|
||||||
|
mocked_send.side_effect = side_effect
|
||||||
|
|
||||||
|
self.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 matches
|
||||||
|
self.assertEquals(
|
||||||
|
args[0].extensions[0],
|
||||||
|
self.createUpdateExtension(
|
||||||
|
extensions.DNSSECExtension(**self.dnssecExtensionWithDsData)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# test that the dnssecdata getter is functioning properly
|
||||||
|
dnssecdata_get = self.domain.dnssecdata
|
||||||
|
mocked_send.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(
|
||||||
|
commands.UpdateDomain(
|
||||||
|
name="fake.gov",
|
||||||
|
nsset=None,
|
||||||
|
keyset=None,
|
||||||
|
registrant=None,
|
||||||
|
auth_info=None,
|
||||||
|
),
|
||||||
|
cleaned=True,
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
commands.InfoDomain(
|
||||||
|
name="fake.gov",
|
||||||
|
),
|
||||||
|
cleaned=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEquals(
|
||||||
|
dnssecdata_get.dsData, self.dnssecExtensionWithDsData["dsData"]
|
||||||
|
)
|
||||||
|
|
||||||
|
patcher.stop()
|
||||||
|
|
||||||
def test_dnssec_is_idempotent(self):
|
def test_dnssec_is_idempotent(self):
|
||||||
"""
|
"""
|
||||||
Scenario: Registrant adds DNS data twice, due to a UI glitch
|
Scenario: Registrant adds DNS data twice, due to a UI glitch
|
||||||
|
|
||||||
"""
|
|
||||||
# implementation note: this requires seeing what happens when these are actually
|
# implementation note: this requires seeing what happens when these are actually
|
||||||
# sent like this, and then implementing appropriate mocks for any errors the
|
# sent like this, and then implementing appropriate mocks for any errors the
|
||||||
# registry normally sends in this case
|
# registry normally sends in this case
|
||||||
raise
|
|
||||||
|
|
||||||
@skip("not implemented yet")
|
This test verifies:
|
||||||
|
1 - UpdateDomain command called twice
|
||||||
|
2 - setter causes the getter to call info domain on next get from cache
|
||||||
|
3 - getter properly parses dnssecdata from InfoDomain response and sets to cache
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# make sure to stop any other patcher so there are no conflicts
|
||||||
|
self.mockSendPatch.stop()
|
||||||
|
|
||||||
|
def side_effect(_request, cleaned):
|
||||||
|
return MagicMock(
|
||||||
|
res_data=[self.mockDataInfoDomain],
|
||||||
|
extensions=[
|
||||||
|
extensions.DNSSECExtension(**self.dnssecExtensionWithDsData)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
patcher = patch("registrar.models.domain.registry.send")
|
||||||
|
mocked_send = patcher.start()
|
||||||
|
mocked_send.side_effect = side_effect
|
||||||
|
|
||||||
|
# set the dnssecdata once
|
||||||
|
self.domain.dnssecdata = self.dnssecExtensionWithDsData
|
||||||
|
# set the dnssecdata again
|
||||||
|
self.domain.dnssecdata = self.dnssecExtensionWithDsData
|
||||||
|
# test that the dnssecdata getter is functioning properly
|
||||||
|
dnssecdata_get = self.domain.dnssecdata
|
||||||
|
mocked_send.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(
|
||||||
|
commands.UpdateDomain(
|
||||||
|
name="fake.gov",
|
||||||
|
nsset=None,
|
||||||
|
keyset=None,
|
||||||
|
registrant=None,
|
||||||
|
auth_info=None,
|
||||||
|
),
|
||||||
|
cleaned=True,
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
commands.UpdateDomain(
|
||||||
|
name="fake.gov",
|
||||||
|
nsset=None,
|
||||||
|
keyset=None,
|
||||||
|
registrant=None,
|
||||||
|
auth_info=None,
|
||||||
|
),
|
||||||
|
cleaned=True,
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
commands.InfoDomain(
|
||||||
|
name="fake.gov",
|
||||||
|
),
|
||||||
|
cleaned=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEquals(
|
||||||
|
dnssecdata_get.dsData, self.dnssecExtensionWithDsData["dsData"]
|
||||||
|
)
|
||||||
|
|
||||||
|
patcher.stop()
|
||||||
|
|
||||||
|
def test_user_adds_dnssec_data_multiple_dsdata(self):
|
||||||
|
"""
|
||||||
|
Scenario: Registrant adds DNSSEC data with multiple DSData.
|
||||||
|
Verify that both the setter and getter are functioning properly
|
||||||
|
|
||||||
|
This test verifies:
|
||||||
|
1 - setter calls UpdateDomain command
|
||||||
|
2 - setter adds the UpdateDNSSECExtension extension to the command
|
||||||
|
3 - setter causes the getter to call info domain on next get from cache
|
||||||
|
4 - getter properly parses dnssecdata from InfoDomain response and sets to cache
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# make sure to stop any other patcher so there are no conflicts
|
||||||
|
self.mockSendPatch.stop()
|
||||||
|
|
||||||
|
def side_effect(_request, cleaned):
|
||||||
|
return MagicMock(
|
||||||
|
res_data=[self.mockDataInfoDomain],
|
||||||
|
extensions=[
|
||||||
|
extensions.DNSSECExtension(**self.dnssecExtensionWithMultDsData)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
patcher = patch("registrar.models.domain.registry.send")
|
||||||
|
mocked_send = patcher.start()
|
||||||
|
mocked_send.side_effect = side_effect
|
||||||
|
|
||||||
|
self.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(
|
||||||
|
extensions.DNSSECExtension(**self.dnssecExtensionWithMultDsData)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# test that the dnssecdata getter is functioning properly
|
||||||
|
dnssecdata_get = self.domain.dnssecdata
|
||||||
|
mocked_send.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(
|
||||||
|
commands.UpdateDomain(
|
||||||
|
name="fake.gov",
|
||||||
|
nsset=None,
|
||||||
|
keyset=None,
|
||||||
|
registrant=None,
|
||||||
|
auth_info=None,
|
||||||
|
),
|
||||||
|
cleaned=True,
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
commands.InfoDomain(
|
||||||
|
name="fake.gov",
|
||||||
|
),
|
||||||
|
cleaned=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEquals(
|
||||||
|
dnssecdata_get.dsData, self.dnssecExtensionWithMultDsData["dsData"]
|
||||||
|
)
|
||||||
|
|
||||||
|
patcher.stop()
|
||||||
|
|
||||||
|
def test_user_adds_dnssec_keydata(self):
|
||||||
|
"""
|
||||||
|
Scenario: Registrant adds DNSSEC data.
|
||||||
|
Verify that both the setter and getter are functioning properly
|
||||||
|
|
||||||
|
This test verifies:
|
||||||
|
1 - setter calls UpdateDomain command
|
||||||
|
2 - setter adds the UpdateDNSSECExtension extension to the command
|
||||||
|
3 - setter causes the getter to call info domain on next get from cache
|
||||||
|
4 - getter properly parses dnssecdata from InfoDomain response and sets to cache
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# make sure to stop any other patcher so there are no conflicts
|
||||||
|
self.mockSendPatch.stop()
|
||||||
|
|
||||||
|
def side_effect(_request, cleaned):
|
||||||
|
return MagicMock(
|
||||||
|
res_data=[self.mockDataInfoDomain],
|
||||||
|
extensions=[
|
||||||
|
extensions.DNSSECExtension(**self.dnssecExtensionWithKeyData)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
patcher = patch("registrar.models.domain.registry.send")
|
||||||
|
mocked_send = patcher.start()
|
||||||
|
mocked_send.side_effect = side_effect
|
||||||
|
|
||||||
|
self.domain.dnssecdata = self.dnssecExtensionWithKeyData
|
||||||
|
# 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(
|
||||||
|
extensions.DNSSECExtension(**self.dnssecExtensionWithKeyData)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# test that the dnssecdata getter is functioning properly
|
||||||
|
dnssecdata_get = self.domain.dnssecdata
|
||||||
|
mocked_send.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(
|
||||||
|
commands.UpdateDomain(
|
||||||
|
name="fake.gov",
|
||||||
|
nsset=None,
|
||||||
|
keyset=None,
|
||||||
|
registrant=None,
|
||||||
|
auth_info=None,
|
||||||
|
),
|
||||||
|
cleaned=True,
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
commands.InfoDomain(
|
||||||
|
name="fake.gov",
|
||||||
|
),
|
||||||
|
cleaned=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEquals(
|
||||||
|
dnssecdata_get.keyData, self.dnssecExtensionWithKeyData["keyData"]
|
||||||
|
)
|
||||||
|
|
||||||
|
patcher.stop()
|
||||||
|
|
||||||
def test_update_is_unsuccessful(self):
|
def test_update_is_unsuccessful(self):
|
||||||
"""
|
"""
|
||||||
Scenario: An update to the dns data is unsuccessful
|
Scenario: An update to the dns data is unsuccessful
|
||||||
When an error is returned from epplibwrapper
|
When an error is returned from epplibwrapper
|
||||||
Then a user-friendly error message is returned for displaying on the web
|
Then a user-friendly error message is returned for displaying on the web
|
||||||
"""
|
"""
|
||||||
raise
|
|
||||||
|
# make sure to stop any other patcher so there are no conflicts
|
||||||
|
self.mockSendPatch.stop()
|
||||||
|
|
||||||
|
def side_effect(_request, cleaned):
|
||||||
|
raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR)
|
||||||
|
|
||||||
|
patcher = patch("registrar.models.domain.registry.send")
|
||||||
|
mocked_send = patcher.start()
|
||||||
|
mocked_send.side_effect = side_effect
|
||||||
|
|
||||||
|
# if RegistryError is raised, view 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.dnssecdata = self.dnssecExtensionWithDsData
|
||||||
|
self.assertTrue(
|
||||||
|
err.is_client_error() or err.is_session_error() or err.is_server_error()
|
||||||
|
)
|
||||||
|
|
||||||
|
patcher.stop()
|
||||||
|
|
||||||
|
|
||||||
class TestAnalystClientHold(MockEppLib):
|
class TestAnalystClientHold(MockEppLib):
|
||||||
|
@ -940,7 +1384,7 @@ class TestAnalystLock(TestCase):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
class TestAnalystDelete(TestCase):
|
class TestAnalystDelete(MockEppLib):
|
||||||
"""Rule: Analysts may delete a domain"""
|
"""Rule: Analysts may delete a domain"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -949,35 +1393,99 @@ class TestAnalystDelete(TestCase):
|
||||||
Given the analyst is logged in
|
Given the analyst is logged in
|
||||||
And a domain exists in the registry
|
And a domain exists in the registry
|
||||||
"""
|
"""
|
||||||
pass
|
super().setUp()
|
||||||
|
self.domain, _ = Domain.objects.get_or_create(
|
||||||
|
name="fake.gov", state=Domain.State.READY
|
||||||
|
)
|
||||||
|
self.domain_on_hold, _ = Domain.objects.get_or_create(
|
||||||
|
name="fake-on-hold.gov", state=Domain.State.ON_HOLD
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
Domain.objects.all().delete()
|
||||||
|
super().tearDown()
|
||||||
|
|
||||||
@skip("not implemented yet")
|
|
||||||
def test_analyst_deletes_domain(self):
|
def test_analyst_deletes_domain(self):
|
||||||
"""
|
"""
|
||||||
Scenario: Analyst permanently deletes a domain
|
Scenario: Analyst permanently deletes a domain
|
||||||
When `domain.delete()` is called
|
When `domain.deletedInEpp()` is called
|
||||||
Then `commands.DeleteDomain` is sent to the registry
|
Then `commands.DeleteDomain` is sent to the registry
|
||||||
And `state` is set to `DELETED`
|
And `state` is set to `DELETED`
|
||||||
"""
|
"""
|
||||||
raise
|
# 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,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
@skip("not implemented yet")
|
# Domain itself should not be deleted
|
||||||
def test_analyst_deletes_domain_idempotent(self):
|
self.assertNotEqual(self.domain, None)
|
||||||
"""
|
|
||||||
Scenario: Analyst tries to delete an already deleted domain
|
# Domain should have the right state
|
||||||
Given `state` is already `DELETED`
|
self.assertEqual(self.domain.state, Domain.State.DELETED)
|
||||||
When `domain.delete()` is called
|
|
||||||
Then `commands.DeleteDomain` is sent to the registry
|
# Cache should be invalidated
|
||||||
And Domain returns normally (without error)
|
self.assertEqual(self.domain._cache, {})
|
||||||
"""
|
|
||||||
raise
|
|
||||||
|
|
||||||
@skip("not implemented yet")
|
|
||||||
def test_deletion_is_unsuccessful(self):
|
def test_deletion_is_unsuccessful(self):
|
||||||
"""
|
"""
|
||||||
Scenario: Domain deletion is unsuccessful
|
Scenario: Domain deletion is unsuccessful
|
||||||
When an error is returned from epplibwrapper
|
When a subdomain exists
|
||||||
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`
|
And `state` is not set to `DELETED`
|
||||||
"""
|
"""
|
||||||
raise
|
# 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()
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
Scenario: Domain deletion is unsuccessful due to FSM rules
|
||||||
|
Given state is 'ready'
|
||||||
|
When `domain.deletedInEpp()` is called
|
||||||
|
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.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)
|
||||||
|
|
|
@ -7,7 +7,7 @@ certifi==2023.7.22 ; python_version >= '3.6'
|
||||||
cfenv==0.5.3
|
cfenv==0.5.3
|
||||||
cffi==1.15.1
|
cffi==1.15.1
|
||||||
charset-normalizer==3.1.0 ; python_full_version >= '3.7.0'
|
charset-normalizer==3.1.0 ; python_full_version >= '3.7.0'
|
||||||
cryptography==41.0.3 ; python_version >= '3.7'
|
cryptography==41.0.4 ; 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'
|
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.0.0
|
dj-database-url==2.0.0
|
||||||
dj-email-url==1.0.6
|
dj-email-url==1.0.6
|
||||||
|
@ -22,7 +22,7 @@ django-phonenumber-field[phonenumberslite]==7.1.0
|
||||||
django-widget-tweaks==1.4.12
|
django-widget-tweaks==1.4.12
|
||||||
environs[django]==9.5.0
|
environs[django]==9.5.0
|
||||||
faker==18.10.0
|
faker==18.10.0
|
||||||
git+https://github.com/cisagov/epplib.git@f818cbf0b069a12f03e1d72e4b9f4900924b832d#egg=fred-epplib
|
git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c#egg=fred-epplib
|
||||||
furl==2.1.3
|
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'
|
future==0.18.3 ; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue