diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 55006f53f..449c4c4bb 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -438,7 +438,6 @@ class Domain(TimeStampedModel, DomainHelper): raise NameserverError(code=nsErrorCodes.INVALID_HOST, nameserver=nameserver) elif cls.isSubdomain(name, nameserver) and (ip is None or ip == []): raise NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=nameserver) - elif not cls.isSubdomain(name, nameserver) and (ip is not None and ip != []): raise NameserverError(code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED, nameserver=nameserver, ip=ip) elif ip is not None and ip != []: @@ -1789,6 +1788,10 @@ class Domain(TimeStampedModel, DomainHelper): for cleaned_host in cleaned_hosts: # Check if the cleaned_host already exists host_in_db, host_created = Host.objects.get_or_create(domain=self, name=cleaned_host["name"]) + # Check if the nameserver is a subdomain of the current domain + # If it is NOT a subdomain, we remove the IP address + if not Domain.isSubdomain(self.name, cleaned_host["name"]): + cleaned_host["addrs"] = [] # Get cleaned list of ips for update cleaned_ips = cleaned_host["addrs"] if not host_created: diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 17833d689..ee1ab8b68 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -692,6 +692,56 @@ class MockEppLib(TestCase): ], ex_date=datetime.date(2023, 5, 25), ) + + mockDataInfoDomainSubdomain = fakedEppObject( + "fakePw", + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), + contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)], + hosts=["fake.meoward.gov"], + statuses=[ + common.Status(state="serverTransferProhibited", description="", lang="en"), + common.Status(state="inactive", description="", lang="en"), + ], + ex_date=datetime.date(2023, 5, 25), + ) + + mockDataInfoDomainSubdomainAndIPAddress = fakedEppObject( + "fakePw", + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), + contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)], + hosts=["fake.meow.gov"], + statuses=[ + common.Status(state="serverTransferProhibited", description="", lang="en"), + common.Status(state="inactive", description="", lang="en"), + ], + ex_date=datetime.date(2023, 5, 25), + addrs=[common.Ip(addr="2.0.0.8")], + ) + + mockDataInfoDomainNotSubdomainNoIP = fakedEppObject( + "fakePw", + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), + contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)], + hosts=["fake.meow.com"], + statuses=[ + common.Status(state="serverTransferProhibited", description="", lang="en"), + common.Status(state="inactive", description="", lang="en"), + ], + ex_date=datetime.date(2023, 5, 25), + ) + + mockDataInfoDomainSubdomainNoIP = fakedEppObject( + "fakePw", + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), + contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)], + hosts=["fake.subdomainwoip.gov"], + statuses=[ + common.Status(state="serverTransferProhibited", description="", lang="en"), + common.Status(state="inactive", description="", lang="en"), + ], + ex_date=datetime.date(2023, 5, 25), + ) + mockDataExtensionDomain = fakedEppObject( "fakePw", cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), @@ -829,6 +879,24 @@ class MockEppLib(TestCase): addrs=[common.Ip(addr="1.2.3.4"), common.Ip(addr="2.3.4.5")], ) + mockDataInfoHosts1IP = fakedEppObject( + "lastPw", + cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)), + addrs=[common.Ip(addr="2.0.0.8")], + ) + + mockDataInfoHostsNotSubdomainNoIP = fakedEppObject( + "lastPw", + cr_date=make_aware(datetime.datetime(2023, 8, 26, 19, 45, 35)), + addrs=[], + ) + + mockDataInfoHostsSubdomainNoIP = fakedEppObject( + "lastPw", + cr_date=make_aware(datetime.datetime(2023, 8, 27, 19, 45, 35)), + addrs=[], + ) + mockDataHostChange = fakedEppObject("lastPw", cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35))) addDsData1 = { "keyTag": 1234, @@ -995,6 +1063,8 @@ class MockEppLib(TestCase): return self.mockDeleteDomainCommands(_request, cleaned) case commands.RenewDomain: return self.mockRenewDomainCommand(_request, cleaned) + case commands.InfoHost: + return self.mockInfoHostCommmands(_request, cleaned) case _: return MagicMock(res_data=[self.mockDataInfoHosts]) @@ -1009,6 +1079,25 @@ class MockEppLib(TestCase): code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, ) + def mockInfoHostCommmands(self, _request, cleaned): + request_name = getattr(_request, "name", None) + + # Define a dictionary to map request names to data and extension values + request_mappings = { + "fake.meow.gov": (self.mockDataInfoHosts1IP, None), # is subdomain and has ip + "fake.meow.com": (self.mockDataInfoHostsNotSubdomainNoIP, None), # not subdomain w no ip + "fake.subdomainwoip.gov": (self.mockDataInfoHostsSubdomainNoIP, None), # subdomain w no ip + } + + # Retrieve the corresponding values from the dictionary + default_mapping = (self.mockDataInfoHosts, None) + res_data, extensions = request_mappings.get(request_name, default_mapping) + + return MagicMock( + res_data=[res_data], + extensions=[extensions] if extensions is not None else [], + ) + def mockUpdateHostCommands(self, _request, cleaned): test_ws_ip = common.Ip(addr="1.1. 1.1") addrs_submitted = getattr(_request, "addrs", []) @@ -1097,6 +1186,10 @@ class MockEppLib(TestCase): "adomain2.gov": (self.InfoDomainWithVerisignSecurityContact, None), "defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None), "justnameserver.com": (self.justNameserver, None), + "meoward.gov": (self.mockDataInfoDomainSubdomain, None), + "meow.gov": (self.mockDataInfoDomainSubdomainAndIPAddress, None), + "fakemeow.gov": (self.mockDataInfoDomainNotSubdomainNoIP, None), + "subdomainwoip.gov": (self.mockDataInfoDomainSubdomainNoIP, None), } # Retrieve the corresponding values from the dictionary diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 2bd581734..647d0ff47 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -96,7 +96,7 @@ class TestDomainCache(MockEppLib): self.mockedSendFunction.assert_has_calls(expectedCalls) - def test_cache_nested_elements(self): + def test_cache_nested_elements_not_subdomain(self): """Cache works correctly with the nested objects cache and hosts""" with less_console_noise(): domain, _ = Domain.objects.get_or_create(name="igorville.gov") @@ -113,7 +113,7 @@ class TestDomainCache(MockEppLib): } expectedHostsDict = { "name": self.mockDataInfoDomain.hosts[0], - "addrs": [item.addr for item in self.mockDataInfoHosts.addrs], + "addrs": [], # should return empty bc fake.host.com is not a subdomain of igorville.gov "cr_date": self.mockDataInfoHosts.cr_date, } @@ -138,6 +138,59 @@ class TestDomainCache(MockEppLib): # invalidate cache domain._cache = {} + # get host + domain._get_property("hosts") + # Should return empty bc fake.host.com is not a subdomain of igorville.gov + self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) + + # get contacts + domain._get_property("contacts") + self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) + self.assertEqual(domain._cache["contacts"], expectedContactsDict) + + def test_cache_nested_elements_is_subdomain(self): + """Cache works correctly with the nested objects cache and hosts""" + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="meoward.gov") + + # The contact list will initially contain objects of type 'DomainContact' + # this is then transformed into PublicContact, and cache should NOT + # hold onto the DomainContact object + expectedUnfurledContactsList = [ + common.DomainContact(contact="123", type="security"), + ] + expectedContactsDict = { + PublicContact.ContactTypeChoices.ADMINISTRATIVE: None, + PublicContact.ContactTypeChoices.SECURITY: "123", + PublicContact.ContactTypeChoices.TECHNICAL: None, + } + expectedHostsDict = { + "name": self.mockDataInfoDomainSubdomain.hosts[0], + "addrs": [item.addr for item in self.mockDataInfoHosts.addrs], + "cr_date": self.mockDataInfoHosts.cr_date, + } + + # this can be changed when the getter for contacts is implemented + domain._get_property("contacts") + + # check domain info is still correct and not overridden + self.assertEqual(domain._cache["auth_info"], self.mockDataInfoDomainSubdomain.auth_info) + self.assertEqual(domain._cache["cr_date"], self.mockDataInfoDomainSubdomain.cr_date) + + # check contacts + self.assertEqual(domain._cache["_contacts"], self.mockDataInfoDomainSubdomain.contacts) + # The contact list should not contain what is sent by the registry by default, + # as _fetch_cache will transform the type to PublicContact + self.assertNotEqual(domain._cache["contacts"], expectedUnfurledContactsList) + self.assertEqual(domain._cache["contacts"], expectedContactsDict) + + # get and check hosts is set correctly + domain._get_property("hosts") + self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) + self.assertEqual(domain._cache["contacts"], expectedContactsDict) + # invalidate cache + domain._cache = {} + # get host domain._get_property("hosts") self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) @@ -1582,31 +1635,100 @@ class TestRegistrantNameservers(MockEppLib): self.assertEqual(nameservers[0][1], ["1.1.1.1"]) patcher.stop() - def test_nameservers_stored_on_fetch_cache(self): + def test_nameservers_stored_on_fetch_cache_a_subdomain_with_ip(self): + """ + #1: Nameserver is a subdomain, and has an IP address + referenced by mockDataInfoDomainSubdomainAndIPAddress + """ + with less_console_noise(): + # make the domain + domain, _ = Domain.objects.get_or_create(name="meow.gov", state=Domain.State.READY) + + # mock the get_or_create methods for Host and HostIP + with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object( + HostIP.objects, "get_or_create" + ) as mock_host_ip_get_or_create: + mock_host_get_or_create.return_value = (Host(domain=domain), True) + mock_host_ip_get_or_create.return_value = (HostIP(), True) + + # force fetch_cache to be called, which will return above documented mocked hosts + domain.nameservers + + mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.meow.gov") + # Retrieve the mocked_host from the return value of the mock + actual_mocked_host, _ = mock_host_get_or_create.return_value + mock_host_ip_get_or_create.assert_called_with(address="2.0.0.8", host=actual_mocked_host) + self.assertEqual(mock_host_ip_get_or_create.call_count, 1) + + def test_nameservers_stored_on_fetch_cache_a_subdomain_without_ip(self): + """ + #2: Nameserver is a subdomain, but doesn't have an IP address associated + referenced by mockDataInfoDomainSubdomainNoIP + """ + with less_console_noise(): + # make the domain + domain, _ = Domain.objects.get_or_create(name="subdomainwoip.gov", state=Domain.State.READY) + + # mock the get_or_create methods for Host and HostIP + with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object( + HostIP.objects, "get_or_create" + ) as mock_host_ip_get_or_create: + mock_host_get_or_create.return_value = (Host(domain=domain), True) + mock_host_ip_get_or_create.return_value = (HostIP(), True) + + # force fetch_cache to be called, which will return above documented mocked hosts + domain.nameservers + + mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.subdomainwoip.gov") + mock_host_ip_get_or_create.assert_not_called() + self.assertEqual(mock_host_ip_get_or_create.call_count, 0) + + def test_nameservers_stored_on_fetch_cache_not_subdomain_with_ip(self): """ Scenario: Nameservers are stored in db when they are retrieved from fetch_cache. Verify the success of this by asserting get_or_create calls to db. The mocked data for the EPP calls returns a host name of 'fake.host.com' from InfoDomain and an array of 2 IPs: 1.2.3.4 and 2.3.4.5 from InfoHost + + #3: Nameserver is not a subdomain, but it does have an IP address returned + due to how we set up our defaults """ with less_console_noise(): domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) - # mock the get_or_create methods for Host and HostIP + with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object( HostIP.objects, "get_or_create" ) as mock_host_ip_get_or_create: - # Set the return value for the mocks - mock_host_get_or_create.return_value = (Host(), True) + mock_host_get_or_create.return_value = (Host(domain=domain), True) mock_host_ip_get_or_create.return_value = (HostIP(), True) + # force fetch_cache to be called, which will return above documented mocked hosts domain.nameservers - # assert that the mocks are called + mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.host.com") - # Retrieve the mocked_host from the return value of the mock - actual_mocked_host, _ = mock_host_get_or_create.return_value - mock_host_ip_get_or_create.assert_called_with(address="2.3.4.5", host=actual_mocked_host) - self.assertEqual(mock_host_ip_get_or_create.call_count, 2) + mock_host_ip_get_or_create.assert_not_called() + self.assertEqual(mock_host_ip_get_or_create.call_count, 0) + + def test_nameservers_stored_on_fetch_cache_not_subdomain_without_ip(self): + """ + #4: Nameserver is not a subdomain and doesn't have an associated IP address + referenced by self.mockDataInfoDomainNotSubdomainNoIP + """ + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="fakemeow.gov", state=Domain.State.READY) + + with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object( + HostIP.objects, "get_or_create" + ) as mock_host_ip_get_or_create: + mock_host_get_or_create.return_value = (Host(domain=domain), True) + mock_host_ip_get_or_create.return_value = (HostIP(), True) + + # force fetch_cache to be called, which will return above documented mocked hosts + domain.nameservers + mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.meow.com") + mock_host_ip_get_or_create.assert_not_called() + self.assertEqual(mock_host_ip_get_or_create.call_count, 0) @skip("not implemented yet") def test_update_is_unsuccessful(self): diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index 2c8e796ac..59b5faaa9 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -821,14 +821,15 @@ class TestDomainNameservers(TestDomainOverview): nameserver1 = "ns1.igorville.gov" nameserver2 = "ns2.igorville.gov" valid_ip = "1.1. 1.1" - # initial nameservers page has one server with two ips + valid_ip_2 = "2.2. 2.2" # have to throw an error in order to test that the whitespace has been stripped from ip nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) # attempt to submit the form without one host and an ip with whitespace nameservers_page.form["form-0-server"] = nameserver1 - nameservers_page.form["form-1-ip"] = valid_ip + nameservers_page.form["form-0-ip"] = valid_ip + nameservers_page.form["form-1-ip"] = valid_ip_2 nameservers_page.form["form-1-server"] = nameserver2 with less_console_noise(): # swallow log warning message result = nameservers_page.form.submit() @@ -937,15 +938,14 @@ class TestDomainNameservers(TestDomainOverview): nameserver1 = "ns1.igorville.gov" nameserver2 = "ns2.igorville.gov" valid_ip = "127.0.0.1" - # initial nameservers page has one server with two ips + valid_ip_2 = "128.0.0.2" nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form without two hosts, both subdomains, - # only one has ips nameservers_page.form["form-0-server"] = nameserver1 + nameservers_page.form["form-0-ip"] = valid_ip nameservers_page.form["form-1-server"] = nameserver2 - nameservers_page.form["form-1-ip"] = valid_ip + nameservers_page.form["form-1-ip"] = valid_ip_2 with less_console_noise(): # swallow log warning message result = nameservers_page.form.submit() # form submission was a successful post, response should be a 302