Repository: cisagov/manage.get.gov

Issue:
Draft test cases for registry integration
Draft a set of test cases for automated testing, perhaps using Cucumber. These can use red, green, refactor, e.g. they can be failing at the time they are committed and make use of `@skip`.

These should illuminate the "shape" of the interface which we'll use to interact with the registry.

- [x] Activating a domain
- [x] Placing and removing client hold (suspending)
- [x] Placing and removing client X prohibited (locking)
- [x] Deleting a domain
- [x] CRUD actions on contacts
- [x] CRUD actions on nameservers
- [ ] CRUD actions on DNSSEC


Discussion:

Resolving patch:
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1,5 +1,5 @@
 import logging
-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
@@ -52,6 +52,34 @@ 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):
+        ACTION_BUTTON = "_place_client_hold"
+        if ACTION_BUTTON 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)
+
+
 class DomainApplicationAdmin(AuditedAdmin):
 
     """Customize the applications listing view."""
@@ -81,7 +109,7 @@ def save_model(self, request, obj, form, change):
 admin.site.register(models.Contact, AuditedAdmin)
 admin.site.register(models.DomainInvitation, 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
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -267,7 +267,7 @@ def renew(self):
 
     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."""


Test patch:
diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py
--- a/src/registrar/tests/test_models.py
+++ b/src/registrar/tests/test_models.py
@@ -12,7 +12,6 @@
     DomainInvitation,
     UserDomainRole,
 )
-from unittest import skip
 
 import boto3_mocking  # type: ignore
 from .common import MockSESClient, less_console_noise
@@ -213,55 +212,3 @@ def test_retrieve_on_first_login(self):
         """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
--- a/src/registrar/tests/test_models_domain.py
+++ b/src/registrar/tests/test_models_domain.py
@@ -1,54 +1,488 @@
+"""
+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 a property is accessed
+            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)
-
-    @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()
+
+    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")
+
+
+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 has not added a security contact email
+            Given `domain.security_contact` has not been set to anything
+            When the domain is created 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 dns data 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_lock(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_lock_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