diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 9de5f563c..c059e5674 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -736,7 +736,7 @@ class DomainAdmin(ListHeaderAdmin): search_help_text = "Search by domain name." change_form_template = "django/admin/domain_change_form.html" change_list_template = "django/admin/domain_change_list.html" - readonly_fields = ["state"] + readonly_fields = ["state", "expiration_date"] def export_data_type(self, request): # match the CSV example with all the fields diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 3bd2c0349..37e78ec6e 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -211,12 +211,56 @@ class Domain(TimeStampedModel, DomainHelper): @Cache def registry_expiration_date(self) -> date: - """Get or set the `ex_date` element from the registry.""" - return self._get_property("ex_date") + """Get or set the `ex_date` element from the registry. + Additionally, update the expiration date in the registrar""" + try: + self.expiration_date = self._get_property("ex_date") + self.save() + return self.expiration_date + except Exception as e: + # exception raised during the save to registrar + logger.error(f"error updating expiration date in registrar: {e}") + raise (e) @registry_expiration_date.setter # type: ignore def registry_expiration_date(self, ex_date: date): - pass + """ + Direct setting of the expiration date in the registry is not implemented. + + To update the expiration date, use renew_domain method.""" + raise NotImplementedError() + + def renew_domain(self, length: int = 1, unit: epp.Unit = epp.Unit.YEAR): + """ + Renew the domain to a length and unit of time relative to the current + expiration date. + + Default length and unit of time are 1 year. + """ + # if no expiration date from registry, set to today + try: + cur_exp_date = self.registry_expiration_date + except KeyError: + logger.warning("current expiration date not set; setting to today") + cur_exp_date = date.today() + + # create RenewDomain request + request = commands.RenewDomain(name=self.name, cur_exp_date=cur_exp_date, period=epp.Period(length, unit)) + + try: + # update expiration date in registry, and set the updated + # expiration date in the registrar, and in the cache + self._cache["ex_date"] = registry.send(request, cleaned=True).res_data[0].ex_date + self.expiration_date = self._cache["ex_date"] + self.save() + except RegistryError as err: + # if registry error occurs, log the error, and raise it as well + logger.error(f"registry error renewing domain: {err}") + raise (err) + except Exception as e: + # exception raised during the save to registrar + logger.error(f"error updating expiration date in registrar: {e}") + raise (e) @Cache def password(self) -> str: diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 2ad83dfff..9a062106f 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -556,6 +556,7 @@ class MockEppLib(TestCase): avail=..., addrs=..., registrant=..., + ex_date=..., ): self.auth_info = auth_info self.cr_date = cr_date @@ -565,6 +566,7 @@ class MockEppLib(TestCase): self.avail = avail # use for CheckDomain self.addrs = addrs self.registrant = registrant + self.ex_date = ex_date def dummyInfoContactResultData( self, @@ -811,6 +813,11 @@ class MockEppLib(TestCase): ], ) + mockRenewedDomainExpDate = fakedEppObject( + "fake.gov", + ex_date=datetime.date(2023, 5, 25), + ) + def _mockDomainName(self, _name, _avail=False): return MagicMock( res_data=[ @@ -870,6 +877,8 @@ class MockEppLib(TestCase): return self.mockCheckDomainCommand(_request, cleaned) case commands.DeleteDomain: return self.mockDeleteDomainCommands(_request, cleaned) + case commands.RenewDomain: + return self.mockRenewDomainCommand(_request, cleaned) case _: return MagicMock(res_data=[self.mockDataInfoHosts]) @@ -890,6 +899,15 @@ class MockEppLib(TestCase): raise RegistryError(code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION) return None + def mockRenewDomainCommand(self, _request, cleaned): + if getattr(_request, "name", None) == "fake-error.gov": + raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR) + else: + return MagicMock( + res_data=[self.mockRenewedDomainExpDate], + code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, + ) + def mockInfoDomainCommands(self, _request, cleaned): request_name = getattr(_request, "name", None) diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 2f2f6d962..c75b1b935 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -56,7 +56,7 @@ class TestDomainCache(MockEppLib): self.assertFalse("avail" in domain._cache.keys()) # using a setter should clear the cache - domain.registry_expiration_date = datetime.date.today() + domain.dnssecdata = [] self.assertEquals(domain._cache, {}) # send should have been called only once @@ -1953,6 +1953,41 @@ class TestRegistrantDNSSEC(MockEppLib): self.assertTrue(err.is_client_error() or err.is_session_error() or err.is_server_error()) +class TestExpirationDate(MockEppLib): + """User may renew expiration date by a number of units of time""" + + def setUp(self): + """ + Domain exists in registry + """ + super().setUp() + # for the tests, need a domain in the ready state + self.domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) + # for the test, need a domain that will raise an exception + self.domain_w_error, _ = Domain.objects.get_or_create(name="fake-error.gov", state=Domain.State.READY) + + def tearDown(self): + Domain.objects.all().delete() + super().tearDown() + + def test_expiration_date_setter_not_implemented(self): + """assert that the setter for expiration date is not implemented and will raise error""" + with self.assertRaises(NotImplementedError): + self.domain.registry_expiration_date = datetime.date.today() + + def test_renew_domain(self): + """assert that the renew_domain sets new expiration date in cache and saves to registrar""" + self.domain.renew_domain() + test_date = datetime.date(2023, 5, 25) + self.assertEquals(self.domain._cache["ex_date"], test_date) + self.assertEquals(self.domain.expiration_date, test_date) + + def test_renew_domain_error(self): + """assert that the renew_domain raises an exception when registry raises error""" + with self.assertRaises(RegistryError): + self.domain_w_error.renew_domain() + + class TestAnalystClientHold(MockEppLib): """Rule: Analysts may suspend or restore a domain by using client hold"""