diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 439dfd9f9..bfd8e7fd6 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1,4 +1,4 @@
-from django.contrib import admin
+from django.contrib import admin, messages
from django.contrib.auth.admin import UserAdmin
from django.contrib.contenttypes.models import ContentType
from django.http.response import HttpResponseRedirect
@@ -50,13 +50,40 @@ class MyHostAdmin(AuditedAdmin):
inlines = [HostIPInline]
+class DomainAdmin(AuditedAdmin):
+
+ """Custom domain admin class to add extra buttons."""
+
+ change_form_template = "django/admin/domain_change_form.html"
+ readonly_fields = ["state"]
+
+ def response_change(self, request, obj):
+ if "_place_client_hold" in request.POST:
+ try:
+ obj.place_client_hold()
+ except Exception as err:
+ self.message_user(request, err, messages.ERROR)
+ else:
+ self.message_user(
+ request,
+ (
+ "%s is in client hold. This domain is no longer accessible on"
+ " the public internet."
+ )
+ % obj.name,
+ )
+ return HttpResponseRedirect(".")
+
+ return super().response_change(request, obj)
+
+
admin.site.register(models.User, MyUserAdmin)
admin.site.register(models.UserDomainRole, AuditedAdmin)
admin.site.register(models.Contact, AuditedAdmin)
admin.site.register(models.DomainInvitation, AuditedAdmin)
admin.site.register(models.DomainApplication, AuditedAdmin)
admin.site.register(models.DomainInformation, AuditedAdmin)
-admin.site.register(models.Domain, AuditedAdmin)
+admin.site.register(models.Domain, DomainAdmin)
admin.site.register(models.Host, MyHostAdmin)
admin.site.register(models.Nameserver, MyHostAdmin)
admin.site.register(models.Website, AuditedAdmin)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index c286cd940..fe5da4b73 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -267,7 +267,7 @@ class Domain(TimeStampedModel, DomainHelper):
def place_client_hold(self):
"""This domain should not be active."""
- raise NotImplementedError()
+ raise NotImplementedError("This is not implemented yet.")
def remove_client_hold(self):
"""This domain is okay to be active."""
diff --git a/src/registrar/templates/django/admin/domain_change_form.html b/src/registrar/templates/django/admin/domain_change_form.html
new file mode 100644
index 000000000..5fa89f20a
--- /dev/null
+++ b/src/registrar/templates/django/admin/domain_change_form.html
@@ -0,0 +1,8 @@
+{% extends 'admin/change_form.html' %}
+
+{% block field_sets %}
+
+
+
+ {{ block.super }}
+{% endblock %}
\ No newline at end of file
diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py
index 97fe51a92..65d2c8d11 100644
--- a/src/registrar/tests/test_models.py
+++ b/src/registrar/tests/test_models.py
@@ -12,7 +12,6 @@ from registrar.models import (
DomainInvitation,
UserDomainRole,
)
-from unittest import skip
import boto3_mocking # type: ignore
from .common import MockSESClient, less_console_noise
@@ -213,55 +212,3 @@ class TestInvitations(TestCase):
"""A new user's first_login callback retrieves their invitations."""
self.user.first_login()
self.assertTrue(UserDomainRole.objects.get(user=self.user, domain=self.domain))
-
-
-@skip("Not implemented yet.")
-class TestDomainApplicationLifeCycle(TestCase):
- def test_application_approval(self):
- # DomainApplication is created
- # test: Domain is created and is inactive
- # analyst approves DomainApplication
- # test: Domain is activated
- pass
-
- def test_application_rejection(self):
- # DomainApplication is created
- # test: Domain is created and is inactive
- # analyst rejects DomainApplication
- # test: Domain remains inactive
- pass
-
- def test_application_deleted_before_approval(self):
- # DomainApplication is created
- # test: Domain is created and is inactive
- # admin deletes DomainApplication
- # test: Domain is deleted; Hosts, HostIps and Nameservers are deleted
- pass
-
- def test_application_deleted_following_approval(self):
- # DomainApplication is created
- # test: Domain is created and is inactive
- # analyst approves DomainApplication
- # admin deletes DomainApplication
- # test: DomainApplication foreign key field on Domain is set to null
- pass
-
- def test_application_approval_with_conflicting_name(self):
- # DomainApplication #1 is created
- # test: Domain #1 is created and is inactive
- # analyst approves DomainApplication #1
- # test: Domain #1 is activated
- # DomainApplication #2 is created, with the same domain name string
- # test: Domain #2 is created and is inactive
- # analyst approves DomainApplication #2
- # test: error is raised
- # test: DomainApplication #1 remains approved
- # test: Domain #1 remains active
- # test: DomainApplication #2 remains in investigating
- # test: Domain #2 remains inactive
- pass
-
- def test_application_approval_with_network_errors(self):
- # TODO: scenario wherein application is approved,
- # but attempts to contact the registry to activate the domain fail
- pass
diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py
index 25b7be2d2..23b65f697 100644
--- a/src/registrar/tests/test_models_domain.py
+++ b/src/registrar/tests/test_models_domain.py
@@ -1,54 +1,487 @@
+"""
+Feature being tested: Registry Integration
+
+This file tests the various ways in which the registrar interacts with the registry.
+"""
from django.test import TestCase
from django.db.utils import IntegrityError
from registrar.models import (
- DomainApplication,
- User,
Domain,
)
from unittest import skip
-class TestDomain(TestCase):
- def test_empty_create_fails(self):
+class TestDomainCreation(TestCase):
+ """Rule: An approved domain application must result in a domain"""
+
+ def setUp(self):
+ """
+ Background:
+ Given that a valid domain application exists
+ """
+ pass
+
+ @skip("not implemented yet")
+ def test_approved_application_creates_domain_locally(self):
+ """
+ Scenario: Analyst approves a domain application
+ When the DomainApplication transitions to approved
+ Then a Domain exists in the database with the same `name`
+ But a domain object does not exist in the registry
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_accessing_domain_properties_creates_domain_in_registry(self):
+ """
+ Scenario: A registrant checks the status of a newly approved domain
+ Given that no domain object exists in the registry
+ When `domain.is_active()` is called
+ Then Domain sends `commands.CreateDomain` to the registry
+ And `domain.state` is set to `CREATED`
+ And `domain.is_active()` returns False
+ """
+ raise
+
+ def test_empty_domain_creation(self):
"""Can't create a completely empty domain."""
with self.assertRaisesRegex(IntegrityError, "name"):
Domain.objects.create()
- def test_minimal_create(self):
+ def test_minimal_creation(self):
"""Can create with just a name."""
Domain.objects.create(name="igorville.gov")
- # this assertion will not work -- for now, the fact that the
- # above command didn't error out is proof enough
- # self.assertEquals(domain.state, Domain.State.DRAFTED)
- @skip("cannot activate a domain without mock registry")
- def test_get_status(self):
- """Returns proper status based on `state`."""
- domain = Domain.objects.create(name="igorville.gov")
- domain.save()
- self.assertEqual(None, domain.status)
- domain.activate()
- domain.save()
- self.assertIn("ok", domain.status)
+ def test_duplicate_creation(self):
+ """Can't create domain if name is not unique."""
+ Domain.objects.create(name="igorville.gov")
+ with self.assertRaisesRegex(IntegrityError, "name"):
+ Domain.objects.create(name="igorville.gov")
- @skip("cannot activate a domain without mock registry")
- def test_fsm_activate_fail_unique(self):
- """Can't activate domain if name is not unique."""
- d1, _ = Domain.objects.get_or_create(name="igorville.gov")
- d2, _ = Domain.objects.get_or_create(name="igorville.gov")
- d1.activate()
- d1.save()
- with self.assertRaises(ValueError):
- d2.activate()
- @skip("cannot activate a domain without mock registry")
- def test_fsm_activate_fail_unapproved(self):
- """Can't activate domain if application isn't approved."""
- d1, _ = Domain.objects.get_or_create(name="igorville.gov")
- user, _ = User.objects.get_or_create()
- application = DomainApplication.objects.create(creator=user)
- d1.domain_application = application
- d1.save()
- with self.assertRaises(ValueError):
- d1.activate()
+class TestRegistrantContacts(TestCase):
+ """Rule: Registrants may modify their WHOIS data"""
+
+ def setUp(self):
+ """
+ Background:
+ Given the registrant is logged in
+ And the registrant is the admin on a domain
+ """
+ pass
+
+ @skip("not implemented yet")
+ def test_no_security_email(self):
+ """
+ Scenario: Registrant declines to add a security contact email
+ Given the domain exists in the registry
+ Then the domain has a valid security contact with CISA defaults
+ And disclose flags are set to keep the email address hidden
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_user_adds_security_email(self):
+ """
+ Scenario: Registrant adds a security contact email
+ When `domain.security_contact` is set equal to a PublicContact with the
+ chosen security contact email
+ Then Domain sends `commands.CreateContact` to the registry
+ And Domain sends `commands.UpdateDomain` to the registry with the newly
+ created contact of type 'security'
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_security_email_is_idempotent(self):
+ """
+ Scenario: Registrant adds a security contact email twice, due to a UI glitch
+ When `commands.CreateContact` and `commands.UpdateDomain` are sent
+ to the registry twice with identical data
+ Then no errors are raised in Domain
+ """
+ # implementation note: this requires seeing what happens when these are actually
+ # sent like this, and then implementing appropriate mocks for any errors the
+ # registry normally sends in this case
+ raise
+
+ @skip("not implemented yet")
+ def test_user_deletes_security_email(self):
+ """
+ Scenario: Registrant clears out an existing security contact email
+ Given a domain exists in the registry with a user-added security email
+ When `domain.security_contact` is set equal to a PublicContact with an empty
+ security contact email
+ Then Domain sends `commands.UpdateDomain` and `commands.DeleteContact`
+ to the registry
+ And the domain has a valid security contact with CISA defaults
+ And disclose flags are set to keep the email address hidden
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_updates_security_email(self):
+ """
+ Scenario: Registrant replaces one valid security contact email with another
+ Given a domain exists in the registry with a user-added security email
+ When `domain.security_contact` is set equal to a PublicContact with a new
+ security contact email
+ Then Domain sends `commands.UpdateContact` to the registry
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_update_is_unsuccessful(self):
+ """
+ Scenario: An update to the security contact is unsuccessful
+ When an error is returned from epplibwrapper
+ Then a user-friendly error message is returned for displaying on the web
+ """
+ raise
+
+
+class TestRegistrantNameservers(TestCase):
+ """Rule: Registrants may modify their nameservers"""
+
+ def setUp(self):
+ """
+ Background:
+ Given the registrant is logged in
+ And the registrant is the admin on a domain
+ """
+ pass
+
+ @skip("not implemented yet")
+ def test_user_adds_one_nameserver(self):
+ """
+ Scenario: Registrant adds a single nameserver
+ Given the domain has zero nameservers
+ When `domain.nameservers` is set to an array of length 1
+ Then `commands.CreateHost` and `commands.UpdateDomain` is sent
+ to the registry
+ And `domain.is_active` returns False
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_user_adds_two_nameservers(self):
+ """
+ Scenario: Registrant adds 2 or more nameservers, thereby activating the domain
+ Given the domain has zero nameservers
+ When `domain.nameservers` is set to an array of length 2
+ Then `commands.CreateHost` and `commands.UpdateDomain` is sent
+ to the registry
+ And `domain.is_active` returns True
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_user_adds_too_many_nameservers(self):
+ """
+ Scenario: Registrant adds 14 or more nameservers
+ Given the domain has zero nameservers
+ When `domain.nameservers` is set to an array of length 14
+ Then Domain raises a user-friendly error
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_user_removes_some_nameservers(self):
+ """
+ Scenario: Registrant removes some nameservers, while keeping at least 2
+ Given the domain has 3 nameservers
+ When `domain.nameservers` is set to an array containing nameserver #1 and #2
+ Then `commands.UpdateDomain` and `commands.DeleteHost` is sent
+ to the registry
+ And `domain.is_active` returns True
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_user_removes_too_many_nameservers(self):
+ """
+ Scenario: Registrant removes some nameservers, bringing the total to less than 2
+ Given the domain has 3 nameservers
+ When `domain.nameservers` is set to an array containing nameserver #1
+ Then `commands.UpdateDomain` and `commands.DeleteHost` is sent
+ to the registry
+ And `domain.is_active` returns False
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_user_replaces_nameservers(self):
+ """
+ Scenario: Registrant simultaneously adds and removes some nameservers
+ Given the domain has 3 nameservers
+ When `domain.nameservers` is set to an array containing nameserver #1 plus
+ two new nameservers
+ Then `commands.CreateHost` is sent to create #4 and #5
+ And `commands.UpdateDomain` is sent to add #4 and #5 plus remove #2 and #3
+ And `commands.DeleteHost` is sent to delete #2 and #3
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_user_cannot_add_subordinate_without_ip(self):
+ """
+ Scenario: Registrant adds a nameserver which is a subdomain of their .gov
+ Given the domain exists in the registry
+ When `domain.nameservers` is set to an array containing an entry
+ with a subdomain of the domain and no IP addresses
+ Then Domain raises a user-friendly error
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_user_updates_ips(self):
+ """
+ Scenario: Registrant changes IP addresses for a nameserver
+ Given the domain exists in the registry
+ And has a subordinate nameserver
+ When `domain.nameservers` is set to an array containing that nameserver
+ with a different IP address(es)
+ Then `commands.UpdateHost` is sent to the registry
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_user_cannot_add_non_subordinate_with_ip(self):
+ """
+ Scenario: Registrant adds a nameserver which is NOT a subdomain of their .gov
+ Given the domain exists in the registry
+ When `domain.nameservers` is set to an array containing an entry
+ which is not a subdomain of the domain and has IP addresses
+ Then Domain raises a user-friendly error
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_nameservers_are_idempotent(self):
+ """
+ Scenario: Registrant adds a set of nameservers twice, due to a UI glitch
+ When `commands.CreateHost` and `commands.UpdateDomain` are sent
+ to the registry twice with identical data
+ Then no errors are raised in Domain
+ """
+ # implementation note: this requires seeing what happens when these are actually
+ # sent like this, and then implementing appropriate mocks for any errors the
+ # registry normally sends in this case
+ raise
+
+ @skip("not implemented yet")
+ def test_update_is_unsuccessful(self):
+ """
+ Scenario: An update to the nameservers is unsuccessful
+ When an error is returned from epplibwrapper
+ Then a user-friendly error message is returned for displaying on the web
+ """
+ raise
+
+
+class TestRegistrantDNSSEC(TestCase):
+ """Rule: Registrants may modify their secure DNS data"""
+
+ def setUp(self):
+ """
+ Background:
+ Given the registrant is logged in
+ And the registrant is the admin on a domain
+ """
+ pass
+
+ @skip("not implemented yet")
+ def test_user_adds_dns_data(self):
+ """
+ Scenario: Registrant adds DNS data
+ ...
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_dnssec_is_idempotent(self):
+ """
+ Scenario: Registrant adds DNS data twice, due to a UI glitch
+ ...
+ """
+ # implementation note: this requires seeing what happens when these are actually
+ # sent like this, and then implementing appropriate mocks for any errors the
+ # registry normally sends in this case
+ raise
+
+ @skip("not implemented yet")
+ def test_update_is_unsuccessful(self):
+ """
+ Scenario: An update to the security contact is unsuccessful
+ When an error is returned from epplibwrapper
+ Then a user-friendly error message is returned for displaying on the web
+ """
+ raise
+
+
+class TestAnalystClientHold(TestCase):
+ """Rule: Analysts may suspend or restore a domain by using client hold"""
+
+ def setUp(self):
+ """
+ Background:
+ Given the analyst is logged in
+ And a domain exists in the registry
+ """
+ pass
+
+ @skip("not implemented yet")
+ def test_analyst_places_client_hold(self):
+ """
+ Scenario: Analyst takes a domain off the internet
+ When `domain.place_client_hold()` is called
+ Then `CLIENT_HOLD` is added to the domain's statuses
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_analyst_places_client_hold_idempotent(self):
+ """
+ Scenario: Analyst tries to place client hold twice
+ Given `CLIENT_HOLD` is already in the domain's statuses
+ When `domain.place_client_hold()` is called
+ Then Domain returns normally (without error)
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_analyst_removes_client_hold(self):
+ """
+ Scenario: Analyst restores a suspended domain
+ Given `CLIENT_HOLD` is in the domain's statuses
+ When `domain.remove_client_hold()` is called
+ Then `CLIENT_HOLD` is no longer in the domain's statuses
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_analyst_removes_client_hold_idempotent(self):
+ """
+ Scenario: Analyst tries to remove client hold twice
+ Given `CLIENT_HOLD` is not in the domain's statuses
+ When `domain.remove_client_hold()` is called
+ Then Domain returns normally (without error)
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_update_is_unsuccessful(self):
+ """
+ Scenario: An update to place or remove client hold is unsuccessful
+ When an error is returned from epplibwrapper
+ Then a user-friendly error message is returned for displaying on the web
+ """
+ raise
+
+
+class TestAnalystLock(TestCase):
+ """Rule: Analysts may lock or unlock a domain to prevent or allow updates"""
+
+ def setUp(self):
+ """
+ Background:
+ Given the analyst is logged in
+ And a domain exists in the registry
+ """
+ pass
+
+ @skip("not implemented yet")
+ def test_analyst_locks_domain(self):
+ """
+ Scenario: Analyst locks a domain to prevent edits or deletion
+ When `domain.lock()` is called
+ Then `CLIENT_DELETE_PROHIBITED` is added to the domain's statuses
+ And `CLIENT_TRANSFER_PROHIBITED` is added to the domain's statuses
+ And `CLIENT_UPDATE_PROHIBITED` is added to the domain's statuses
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_analyst_locks_domain_idempotent(self):
+ """
+ Scenario: Analyst tries to lock a domain twice
+ Given `CLIENT_*_PROHIBITED` is already in the domain's statuses
+ When `domain.lock()` is called
+ Then Domain returns normally (without error)
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_analyst_removes_client_hold(self):
+ """
+ Scenario: Analyst unlocks a domain to allow deletion or edits
+ Given `CLIENT_*_PROHIBITED` is in the domain's statuses
+ When `domain.unlock()` is called
+ Then `CLIENT_DELETE_PROHIBITED` is no longer in the domain's statuses
+ And `CLIENT_TRANSFER_PROHIBITED` is no longer in the domain's statuses
+ And `CLIENT_UPDATE_PROHIBITED` is no longer in the domain's statuses
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_analyst_removes_client_hold_idempotent(self):
+ """
+ Scenario: Analyst tries to unlock a domain twice
+ Given `CLIENT_*_PROHIBITED` is not in the domain's statuses
+ When `domain.unlock()` is called
+ Then Domain returns normally (without error)
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_update_is_unsuccessful(self):
+ """
+ Scenario: An update to lock or unlock a domain is unsuccessful
+ When an error is returned from epplibwrapper
+ Then a user-friendly error message is returned for displaying on the web
+ """
+ raise
+
+
+class TestAnalystDelete(TestCase):
+ """Rule: Analysts may delete a domain"""
+
+ def setUp(self):
+ """
+ Background:
+ Given the analyst is logged in
+ And a domain exists in the registry
+ """
+ pass
+
+ @skip("not implemented yet")
+ def test_analyst_deletes_domain(self):
+ """
+ Scenario: Analyst permanently deletes a domain
+ When `domain.delete()` is called
+ Then `commands.DeleteDomain` is sent to the registry
+ And `state` is set to `DELETED`
+ """
+ raise
+
+ @skip("not implemented yet")
+ def test_analyst_deletes_domain_idempotent(self):
+ """
+ Scenario: Analyst tries to delete an already deleted domain
+ Given `state` is already `DELETED`
+ When `domain.delete()` is called
+ Then `commands.DeleteDomain` is sent to the registry
+ And Domain returns normally (without error)
+ """
+ raise
+
+ @skip("not implemented yet")
+ 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
+ And `state` is not set to `DELETED`
+ """
+ raise