diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 2b0e7e9ab..6265a5b95 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -284,30 +284,21 @@ class Domain(TimeStampedModel, DomainHelper): def dnssecdata(self) -> extensions.DNSSECExtension: return self._get_property("dnssecdata") - @dnssecdata.setter - def dnssecdata( - self, - _dnssecdata: extensions.DNSSECExtension - ): + @dnssecdata.setter # type: ignore + def dnssecdata(self, _dnssecdata: extensions.DNSSECExtension): updateParams = { - "maxSigLife": _dnssecdata.maxSigLife, - "dsData": _dnssecdata.dsData, - "keyData": _dnssecdata.keyData, + "maxSigLife": _dnssecdata.get("maxSigLife", None), + "dsData": _dnssecdata.get("dsData", None), + "keyData": _dnssecdata.get("keyData", None), "remAllDsKeyData": True, } - request = commands.UpdateDomain( - name=self.name - ) + 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) - ) + logger.error("Error adding DNSSEC, code was %s error was %s" % (e.code, e)) raise e @nameservers.setter # type: ignore @@ -1010,10 +1001,10 @@ class Domain(TimeStampedModel, DomainHelper): # get extensions info, if there is any # DNSSECExtension is one possible extension, make sure to handle # only DNSSECExtension and not other type extensions - extensions = dataResponse.extensions + returned_extensions = dataResponse.extensions cleaned["dnssecdata"] = None - for extension in extensions: - if isinstance(extension,extensions.DNSSECExtension): + for extension in returned_extensions: + if isinstance(extension, extensions.DNSSECExtension): cleaned["dnssecdata"] = extension # get contact info, if there are any diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 54045bb32..e32cc2d42 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -3,9 +3,10 @@ Feature being tested: Registry Integration 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.db.utils import IntegrityError -from unittest.mock import patch, call +from unittest.mock import MagicMock, patch, call import datetime from registrar.models import Domain @@ -20,6 +21,7 @@ from .common import MockEppLib from epplibwrapper import ( commands, common, + extensions, RegistryError, ErrorCode, ) @@ -664,44 +666,357 @@ class TestRegistrantNameservers(TestCase): raise -class TestRegistrantDNSSEC(TestCase): +class TestRegistrantDNSSEC(MockEppLib): """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): """ Background: - Given the registrant is logged in - And the registrant is the admin on a domain + Given the analyst is logged in + 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 test_user_adds_dns_data(self): + def tearDown(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") + 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): """ 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") + 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 + + """ + + 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 + + """ + + 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 + + """ + + 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): """ 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 + + 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):