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