diff --git a/.github/ISSUE_TEMPLATE/issue-default.yml b/.github/ISSUE_TEMPLATE/issue-default.yml index 27ec10415..3a34b2943 100644 --- a/.github/ISSUE_TEMPLATE/issue-default.yml +++ b/.github/ISSUE_TEMPLATE/issue-default.yml @@ -6,13 +6,13 @@ body: id: title-help attributes: value: | - > Titles should be short, descriptive, and compelling. + > Titles should be short, descriptive, and compelling. Use sentence case. - type: textarea id: issue-description attributes: - label: Issue description and context + label: Issue description description: | - Describe the issue so that someone who wasn't present for its discovery can understand the problem and why it matters. Use full sentences, plain language, and good [formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). Share desired outcomes or potential next steps. Images or links to other content/context (like documents or Slack discussions) are welcome. + Describe the issue so that someone who wasn't present for its discovery can understand why it matters. Use full sentences, plain language, and good [formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). validations: required: true - type: textarea @@ -20,16 +20,22 @@ body: attributes: label: Acceptance criteria description: "If known, share 1-3 statements that would need to be true for this issue to be considered resolved. Use a [task list](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/about-task-lists#creating-task-lists) if appropriate." - placeholder: "- [ ] The button does the thing." + placeholder: "- [ ]" + - type: textarea + id: additional-context + attributes: + label: Additional context + description: "Share any other thoughts, like how this might be implemented or fixed. Screenshots and links to documents/discussions are welcome." - type: textarea id: links-to-other-issues attributes: label: Links to other issues description: | - Add the issue #number of other issues this relates to and how (e.g., 🚧 Blocks, ⛔️ Is blocked by, 🔄 Relates to). + "Add issue #numbers this relates to and how (e.g., 🚧 :construction: Blocks, ⛔️ :no_entry: Is blocked by, 🔄 :repeat: Relates to)." placeholder: 🔄 Relates to... - type: markdown id: note attributes: value: | - > We may edit this issue's text to document our understanding and clarify the product work. + > We may edit the text in this issue to document our understanding and clarify the product work. + diff --git a/docs/developer/user-permissions.md b/docs/developer/user-permissions.md index 31b69d3b3..f7c41492d 100644 --- a/docs/developer/user-permissions.md +++ b/docs/developer/user-permissions.md @@ -42,7 +42,7 @@ as health checks used by our platform). ## Adding roles The current MVP design uses only a single role called -`UserDomainRole.Roles.ADMIN` that has all access on a domain. As such, the +`UserDomainRole.Roles.MANAGER` that has all access on a domain. As such, the permission mixin doesn't need to examine the `role` field carefully. In the future, as we add additional roles that our product vision calls for (read-only? editing only some information?), we need to add conditional diff --git a/src/api/tests/test_available.py b/src/api/tests/test_available.py index 0bbe01f03..9eab17bf7 100644 --- a/src/api/tests/test_available.py +++ b/src/api/tests/test_available.py @@ -3,19 +3,27 @@ import json from django.contrib.auth import get_user_model -from django.test import TestCase, RequestFactory +from django.test import RequestFactory -from ..views import available, _domains, in_domains +from ..views import available, in_domains from .common import less_console_noise +from registrar.tests.common import MockEppLib +from unittest.mock import call + +from epplibwrapper import ( + commands, + RegistryError, +) API_BASE_PATH = "/api/v1/available/" -class AvailableViewTest(TestCase): +class AvailableViewTest(MockEppLib): """Test that the view function works as expected.""" def setUp(self): + super().setUp() self.user = get_user_model().objects.create(username="username") self.factory = RequestFactory() @@ -29,26 +37,37 @@ class AvailableViewTest(TestCase): response_object = json.loads(response.content) self.assertIn("available", response_object) - def test_domain_list(self): - """Test the domain list that is returned from Github. + def test_in_domains_makes_calls_(self): + """Domain searches successfully make correct mock EPP calls""" + gsa_available = in_domains("gsa.gov") + igorville_available = in_domains("igorvilleremixed.gov") - This does not mock out the external file, it is actually fetched from - the internet. - """ - domains = _domains() - self.assertIn("gsa.gov", domains) - # entries are all lowercase so GSA.GOV is not in the set - self.assertNotIn("GSA.GOV", domains) - self.assertNotIn("igorvilleremixed.gov", domains) - # all the entries have dots - self.assertNotIn("gsa", domains) + """Domain searches successfully make mock EPP calls""" + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.CheckDomain( + ["gsa.gov"], + ), + cleaned=True, + ), + call( + commands.CheckDomain( + ["igorvilleremixed.gov"], + ), + cleaned=True, + ), + ] + ) + """Domain searches return correct availability results""" + self.assertTrue(gsa_available) + self.assertFalse(igorville_available) - def test_in_domains(self): + def test_in_domains_capitalized(self): + """Domain searches work without case sensitivity""" self.assertTrue(in_domains("gsa.gov")) # input is lowercased so GSA.GOV should be found - self.assertTrue(in_domains("GSA.GOV")) - # This domain should not have been registered - self.assertFalse(in_domains("igorvilleremixed.gov")) + self.assertTrue(in_domains("GSA.gov")) def test_in_domains_dotgov(self): """Domain searches work without trailing .gov""" @@ -86,13 +105,18 @@ class AvailableViewTest(TestCase): request.user = self.user response = available(request, domain=bad_string) self.assertFalse(json.loads(response.content)["available"]) + # domain set to raise error successfully raises error + with self.assertRaises(RegistryError): + error_domain_available = available(request, "errordomain.gov") + self.assertFalse(json.loads(error_domain_available.content)["available"]) -class AvailableAPITest(TestCase): +class AvailableAPITest(MockEppLib): """Test that the API can be called as expected.""" def setUp(self): + super().setUp() self.user = get_user_model().objects.create(username="username") def test_available_get(self): diff --git a/src/api/views.py b/src/api/views.py index e19e060ef..e8b8431de 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -3,8 +3,6 @@ from django.apps import apps from django.views.decorators.http import require_http_methods from django.http import JsonResponse -from django.contrib.auth.decorators import login_required - import requests from cachetools.func import ttl_cache @@ -59,16 +57,15 @@ def in_domains(domain): given domain doesn't end with .gov, ".gov" is added when looking for a match. """ - domain = domain.lower() + Domain = apps.get_model("registrar.Domain") if domain.endswith(".gov"): - return domain.lower() in _domains() + return Domain.available(domain) else: # domain search string doesn't end with .gov, add it on here - return (domain + ".gov") in _domains() + return Domain.available(domain + ".gov") @require_http_methods(["GET"]) -@login_required def available(request, domain=""): """Is a given domain available or not. diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 718bd5792..0857ec603 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -22,15 +22,15 @@ a.breadcrumb__back { } } -a.usa-button { +a.usa-button:not(.usa-button--unstyled, .usa-button--outline) { text-decoration: none; color: color('white'); } -a.usa-button:visited, -a.usa-button:hover, -a.usa-button:focus, -a.usa-button:active { +a.usa-button:not(.usa-button--unstyled, .usa-button--outline):visited, +a.usa-button:not(.usa-button--unstyled, .usa-button--outline):hover, +a.usa-button:not(.usa-button--unstyled, .usa-button--outline):focus, +a.usa-button:not(.usa-button--unstyled, .usa-button--outline):active { color: color('white'); } diff --git a/src/registrar/migrations/0040_alter_userdomainrole_role.py b/src/registrar/migrations/0040_alter_userdomainrole_role.py new file mode 100644 index 000000000..39e539f55 --- /dev/null +++ b/src/registrar/migrations/0040_alter_userdomainrole_role.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.1 on 2023-10-20 15:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0039_alter_transitiondomain_status"), + ] + + operations = [ + migrations.AlterField( + model_name="userdomainrole", + name="role", + field=models.TextField(choices=[("manager", "Manager")]), + ), + ] diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index ad1fbebd6..c7d786426 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1410,18 +1410,16 @@ class Domain(TimeStampedModel, DomainHelper): """creates a disclose object that can be added to a contact Create using .disclose= on the command before sending. if item is security email then make sure email is visable""" - isSecurity = contact.contact_type == contact.ContactTypeChoices.SECURITY + is_security = contact.contact_type == contact.ContactTypeChoices.SECURITY DF = epp.DiscloseField - fields = {DF.FAX, DF.VOICE, DF.ADDR} - - if not isSecurity or ( - isSecurity and contact.email == PublicContact.get_default_security().email - ): - fields.add(DF.EMAIL) + fields = {DF.EMAIL} + disclose = ( + is_security and contact.email != PublicContact.get_default_security().email + ) + # Will only disclose DF.EMAIL if its not the default return epp.Disclose( - flag=False, + flag=disclose, fields=fields, - types={DF.ADDR: "loc"}, ) def _make_epp_contact_postal_info(self, contact: PublicContact): # type: ignore diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index a4752aa88..68fbfab0d 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -612,7 +612,7 @@ class DomainApplication(TimeStampedModel): # create the permission for the user UserDomainRole = apps.get_model("registrar.UserDomainRole") UserDomainRole.objects.get_or_create( - user=self.creator, domain=created_domain, role=UserDomainRole.Roles.ADMIN + user=self.creator, domain=created_domain, role=UserDomainRole.Roles.MANAGER ) self._send_status_update_email( diff --git a/src/registrar/models/domain_invitation.py b/src/registrar/models/domain_invitation.py index 7cc2a5432..dff03fb87 100644 --- a/src/registrar/models/domain_invitation.py +++ b/src/registrar/models/domain_invitation.py @@ -63,7 +63,7 @@ class DomainInvitation(TimeStampedModel): # and create a role for that user on this domain _, created = UserDomainRole.objects.get_or_create( - user=user, domain=self.domain, role=UserDomainRole.Roles.ADMIN + user=user, domain=self.domain, role=UserDomainRole.Roles.MANAGER ) if not created: # something strange happened and this role already existed when diff --git a/src/registrar/models/user_domain_role.py b/src/registrar/models/user_domain_role.py index e5cb01cc1..7b1f550d3 100644 --- a/src/registrar/models/user_domain_role.py +++ b/src/registrar/models/user_domain_role.py @@ -15,7 +15,7 @@ class UserDomainRole(TimeStampedModel): elsewhere. """ - ADMIN = "manager" + MANAGER = "manager" user = models.ForeignKey( "registrar.User", diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index e0d672093..4ddbd673a 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -52,7 +52,7 @@ {% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url %} {% endif %} {% url 'domain-users' pk=domain.id as url %} - {% include "includes/summary_item.html" with title='User management' users='true' list=True value=domain.permissions.all edit_link=url %} + {% include "includes/summary_item.html" with title='Domain managers' users='true' list=True value=domain.permissions.all edit_link=url %} {% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index 1acd87eeb..ac45ad04c 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -100,7 +100,7 @@ - User management + Domain managers diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index 22b9d18d1..f66eef5a6 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -1,10 +1,23 @@ {% extends "domain_base.html" %} -{% load static %} +{% load static url_helpers %} -{% block title %}User management | {{ domain.name }} | {% endblock %} +{% block title %}Domain managers | {{ domain.name }} | {% endblock %} {% block domain_content %} -

User management

+

Domain managers

+ +

+ Domain managers can update all information related to a domain within the + .gov registrar, including contact details, authorizing official, security + email, and DNS name servers. +

+ + {% if domain.permissions %}
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index a9f38db03..8cd5fd6ba 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -31,6 +31,7 @@ from epplibwrapper import ( info, RegistryError, ErrorCode, + responses, ) from registrar.models.utility.contact_error import ContactError, ContactErrorCodes @@ -669,6 +670,44 @@ class MockEppLib(TestCase): registrant="regContact", ) + InfoDomainWithDefaultSecurityContact = fakedEppObject( + "fakepw", + cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + contacts=[ + common.DomainContact( + contact="defaultSec", + type=PublicContact.ContactTypeChoices.SECURITY, + ) + ], + hosts=["fake.host.com"], + statuses=[ + common.Status(state="serverTransferProhibited", description="", lang="en"), + common.Status(state="inactive", description="", lang="en"), + ], + ) + + InfoDomainWithDefaultTechnicalContact = fakedEppObject( + "fakepw", + cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + contacts=[ + common.DomainContact( + contact="defaultTech", + type=PublicContact.ContactTypeChoices.TECHNICAL, + ) + ], + hosts=["fake.host.com"], + statuses=[ + common.Status(state="serverTransferProhibited", description="", lang="en"), + common.Status(state="inactive", description="", lang="en"), + ], + ) + + mockDefaultTechnicalContact = InfoDomainWithContacts.dummyInfoContactResultData( + "defaultTech", "dotgov@cisa.dhs.gov" + ) + mockDefaultSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData( + "defaultSec", "dotgov@cisa.dhs.gov" + ) mockSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData( "securityContact", "security@mail.gov" ) @@ -784,45 +823,63 @@ class MockEppLib(TestCase): ], ) + def _mockDomainName(self, _name, _avail=False): + return MagicMock( + res_data=[ + responses.check.CheckDomainResultData( + name=_name, avail=_avail, reason=None + ), + ] + ) + + def mockCheckDomainCommand(self, _request, cleaned): + if "gsa.gov" in getattr(_request, "names", None): + return self._mockDomainName("gsa.gov", True) + elif "GSA.gov" in getattr(_request, "names", None): + return self._mockDomainName("GSA.gov", True) + elif "igorvilleremixed.gov" in getattr(_request, "names", None): + return self._mockDomainName("igorvilleremixed.gov", False) + elif "errordomain.gov" in getattr(_request, "names", None): + raise RegistryError("Registry cannot find domain availability.") + else: + return self._mockDomainName("domainnotfound.gov", False) + def mockSend(self, _request, cleaned): """Mocks the registry.send function used inside of domain.py registry is imported from epplibwrapper returns objects that simulate what would be in a epp response but only relevant pieces for tests""" - if isinstance(_request, commands.InfoDomain): - return self.mockInfoDomainCommands(_request, cleaned) - elif isinstance(_request, commands.InfoContact): - return self.mockInfoContactCommands(_request, cleaned) - elif isinstance(_request, commands.UpdateDomain): - return self.mockUpdateDomainCommands(_request, cleaned) - elif isinstance(_request, commands.CreateContact): - return self.mockCreateContactCommands(_request, cleaned) - elif isinstance(_request, commands.CreateHost): - return MagicMock( - res_data=[self.mockDataHostChange], - code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, - ) - elif isinstance(_request, commands.UpdateHost): - return MagicMock( - res_data=[self.mockDataHostChange], - code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, - ) - elif isinstance(_request, commands.DeleteHost): - return MagicMock( - res_data=[self.mockDataHostChange], - code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, - ) - elif ( - isinstance(_request, commands.DeleteDomain) - and getattr(_request, "name", None) == "failDelete.gov" - ): - name = getattr(_request, "name", None) - fake_nameserver = "ns1.failDelete.gov" - if name in fake_nameserver: - raise RegistryError( - code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION + + match type(_request): + case commands.InfoDomain: + return self.mockInfoDomainCommands(_request, cleaned) + case commands.InfoContact: + return self.mockInfoContactCommands(_request, cleaned) + case commands.CreateContact: + return self.mockCreateContactCommands(_request, cleaned) + case commands.UpdateDomain: + return self.mockUpdateDomainCommands(_request, cleaned) + case commands.CreateHost: + return MagicMock( + res_data=[self.mockDataHostChange], + code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, ) - return MagicMock(res_data=[self.mockDataInfoHosts]) + case commands.UpdateHost: + return MagicMock( + res_data=[self.mockDataHostChange], + code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, + ) + case commands.DeleteHost: + return MagicMock( + res_data=[self.mockDataHostChange], + code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, + ) + case commands.CheckDomain: + return self.mockCheckDomainCommand(_request, cleaned) + case commands.DeleteDomain: + return self.mockDeleteDomainCommands(_request, cleaned) + case _: + return MagicMock(res_data=[self.mockDataInfoHosts]) def mockUpdateDomainCommands(self, _request, cleaned): if getattr(_request, "name", None) == "dnssec-invalid.gov": @@ -833,6 +890,16 @@ class MockEppLib(TestCase): code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, ) + def mockDeleteDomainCommands(self, _request, cleaned): + if getattr(_request, "name", None) == "failDelete.gov": + name = getattr(_request, "name", None) + fake_nameserver = "ns1.failDelete.gov" + if name in fake_nameserver: + raise RegistryError( + code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION + ) + return None + def mockInfoDomainCommands(self, _request, cleaned): request_name = getattr(_request, "name", None) @@ -862,6 +929,8 @@ class MockEppLib(TestCase): "namerserversubdomain.gov": (self.infoDomainCheckHostIPCombo, None), "freeman.gov": (self.InfoDomainWithContacts, None), "threenameserversDomain.gov": (self.infoDomainThreeHosts, None), + "defaultsecurity.gov": (self.InfoDomainWithDefaultSecurityContact, None), + "defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None), } # Retrieve the corresponding values from the dictionary @@ -887,6 +956,10 @@ class MockEppLib(TestCase): mocked_result = self.mockAdministrativeContact case "regContact": mocked_result = self.mockRegistrantContact + case "defaultSec": + mocked_result = self.mockDefaultSecurityContact + case "defaultTech": + mocked_result = self.mockDefaultTechnicalContact case _: # Default contact return mocked_result = self.mockDataInfoContact @@ -921,15 +994,11 @@ class MockEppLib(TestCase): self, contact: PublicContact, disclose_email=False, createContact=True ): DF = common.DiscloseField - fields = {DF.FAX, DF.VOICE, DF.ADDR} - - if not disclose_email: - fields.add(DF.EMAIL) + fields = {DF.EMAIL} di = common.Disclose( - flag=False, + flag=disclose_email, fields=fields, - types={DF.ADDR: "loc"}, ) # check docs here looks like we may have more than one address field but diff --git a/src/registrar/tests/test_forms.py b/src/registrar/tests/test_forms.py index 95be195ba..4b1aeb12c 100644 --- a/src/registrar/tests/test_forms.py +++ b/src/registrar/tests/test_forms.py @@ -1,6 +1,6 @@ """Test form validation requirements.""" -from django.test import TestCase +from django.test import TestCase, RequestFactory from registrar.forms.application_wizard import ( CurrentSitesForm, @@ -16,9 +16,16 @@ from registrar.forms.application_wizard import ( AboutYourOrganizationForm, ) from registrar.forms.domain import ContactForm +from registrar.tests.common import MockEppLib +from django.contrib.auth import get_user_model -class TestFormValidation(TestCase): +class TestFormValidation(MockEppLib): + def setUp(self): + super().setUp() + self.user = get_user_model().objects.create(username="username") + self.factory = RequestFactory() + def test_org_contact_zip_invalid(self): form = OrganizationContactForm(data={"zipcode": "nah"}) self.assertEqual( diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 2c6f78ef5..e76dea035 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -601,7 +601,7 @@ class TestInvitations(TestCase): def test_retrieve_existing_role_no_error(self): # make the overlapping role UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain, role=UserDomainRole.Roles.ADMIN + user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER ) # this is not an error but does produce a console warning with less_console_noise(): diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index ef3084f9c..5759df1be 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -19,7 +19,7 @@ from registrar.utility.errors import ActionNotAllowed, NameserverError from registrar.models.utility.contact_error import ContactError, ContactErrorCodes -from .common import MockEppLib + from django_fsm import TransitionNotAllowed # type: ignore from epplibwrapper import ( commands, @@ -29,6 +29,7 @@ from epplibwrapper import ( RegistryError, ErrorCode, ) +from .common import MockEppLib import logging logger = logging.getLogger(__name__) @@ -760,6 +761,198 @@ class TestRegistrantContacts(MockEppLib): self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True) self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 1) + def test_not_disclosed_on_other_contacts(self): + """ + Scenario: Registrant creates a new domain with multiple contacts + When `domain` has registrant, admin, technical, + and security contacts + Then Domain sends `commands.CreateContact` to the registry + And the field `disclose` is set to false for DF.EMAIL + on all fields except security + """ + # Generates a domain with four existing contacts + domain, _ = Domain.objects.get_or_create(name="freeman.gov") + + # Contact setup + expected_admin = domain.get_default_administrative_contact() + expected_admin.email = self.mockAdministrativeContact.email + + expected_registrant = domain.get_default_registrant_contact() + expected_registrant.email = self.mockRegistrantContact.email + + expected_security = domain.get_default_security_contact() + expected_security.email = self.mockSecurityContact.email + + expected_tech = domain.get_default_technical_contact() + expected_tech.email = self.mockTechnicalContact.email + + domain.administrative_contact = expected_admin + domain.registrant_contact = expected_registrant + domain.security_contact = expected_security + domain.technical_contact = expected_tech + + contacts = [ + (expected_admin, domain.administrative_contact), + (expected_registrant, domain.registrant_contact), + (expected_security, domain.security_contact), + (expected_tech, domain.technical_contact), + ] + + # Test for each contact + for contact in contacts: + expected_contact = contact[0] + actual_contact = contact[1] + is_security = expected_contact.contact_type == "security" + + expectedCreateCommand = self._convertPublicContactToEpp( + expected_contact, disclose_email=is_security + ) + + # Should only be disclosed if the type is security, as the email is valid + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + + # The emails should match on both items + self.assertEqual(expected_contact.email, actual_contact.email) + + def test_convert_public_contact_to_epp(self): + self.maxDiff = None + domain, _ = Domain.objects.get_or_create(name="freeman.gov") + dummy_contact = domain.get_default_security_contact() + test_disclose = self._convertPublicContactToEpp( + dummy_contact, disclose_email=True + ).__dict__ + test_not_disclose = self._convertPublicContactToEpp( + dummy_contact, disclose_email=False + ).__dict__ + + # Separated for linter + disclose_email_field = {common.DiscloseField.EMAIL} + expected_disclose = { + "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), + "disclose": common.Disclose( + flag=True, fields=disclose_email_field, types=None + ), + "email": "dotgov@cisa.dhs.gov", + "extensions": [], + "fax": None, + "id": "ThIq2NcRIDN7PauO", + "ident": None, + "notify_email": None, + "postal_info": common.PostalInfo( + name="Registry Customer Service", + addr=common.ContactAddr( + street=["4200 Wilson Blvd.", None, None], + city="Arlington", + pc="22201", + cc="US", + sp="VA", + ), + org="Cybersecurity and Infrastructure Security Agency", + type="loc", + ), + "vat": None, + "voice": "+1.8882820870", + } + + # Separated for linter + expected_not_disclose = { + "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), + "disclose": common.Disclose( + flag=False, fields=disclose_email_field, types=None + ), + "email": "dotgov@cisa.dhs.gov", + "extensions": [], + "fax": None, + "id": "ThrECENCHI76PGLh", + "ident": None, + "notify_email": None, + "postal_info": common.PostalInfo( + name="Registry Customer Service", + addr=common.ContactAddr( + street=["4200 Wilson Blvd.", None, None], + city="Arlington", + pc="22201", + cc="US", + sp="VA", + ), + org="Cybersecurity and Infrastructure Security Agency", + type="loc", + ), + "vat": None, + "voice": "+1.8882820870", + } + + # Set the ids equal, since this value changes + test_disclose["id"] = expected_disclose["id"] + test_not_disclose["id"] = expected_not_disclose["id"] + + self.assertEqual(test_disclose, expected_disclose) + self.assertEqual(test_not_disclose, expected_not_disclose) + + def test_not_disclosed_on_default_security_contact(self): + """ + Scenario: Registrant creates a new domain with no security email + When `domain.security_contact.email` is equal to the default + Then Domain sends `commands.CreateContact` to the registry + And the field `disclose` is set to false for DF.EMAIL + """ + domain, _ = Domain.objects.get_or_create(name="defaultsecurity.gov") + expectedSecContact = PublicContact.get_default_security() + expectedSecContact.domain = domain + expectedSecContact.registry_id = "defaultSec" + domain.security_contact = expectedSecContact + + expectedCreateCommand = self._convertPublicContactToEpp( + expectedSecContact, disclose_email=False + ) + + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + # Confirm that we are getting a default email + self.assertEqual(domain.security_contact.email, expectedSecContact.email) + + def test_not_disclosed_on_default_technical_contact(self): + """ + Scenario: Registrant creates a new domain with no technical contact + When `domain.technical_contact.email` is equal to the default + Then Domain sends `commands.CreateContact` to the registry + And the field `disclose` is set to false for DF.EMAIL + """ + domain, _ = Domain.objects.get_or_create(name="defaulttechnical.gov") + expectedTechContact = PublicContact.get_default_technical() + expectedTechContact.domain = domain + expectedTechContact.registry_id = "defaultTech" + domain.technical_contact = expectedTechContact + + expectedCreateCommand = self._convertPublicContactToEpp( + expectedTechContact, disclose_email=False + ) + + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + # Confirm that we are getting a default email + self.assertEqual(domain.technical_contact.email, expectedTechContact.email) + + def test_is_disclosed_on_security_contact(self): + """ + Scenario: Registrant creates a new domain with a security email + When `domain.security_contact.email` is set to a valid email + and is not the default + Then Domain sends `commands.CreateContact` to the registry + And the field `disclose` is set to true for DF.EMAIL + """ + domain, _ = Domain.objects.get_or_create(name="igorville.gov") + expectedSecContact = PublicContact.get_default_security() + expectedSecContact.domain = domain + expectedSecContact.email = "123@mail.gov" + domain.security_contact = expectedSecContact + + expectedCreateCommand = self._convertPublicContactToEpp( + expectedSecContact, disclose_email=True + ) + + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + # Confirm that we are getting the desired email + self.assertEqual(domain.security_contact.email, expectedSecContact.email) + @skip("not implemented yet") def test_update_is_unsuccessful(self): """ diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index f262d081b..1c215ec05 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -89,7 +89,7 @@ class LoggedInTests(TestWithUser): domain, _ = Domain.objects.get_or_create(name="igorville.gov") self.assertNotContains(response, "igorville.gov") role, _ = UserDomainRole.objects.get_or_create( - user=self.user, domain=domain, role=UserDomainRole.Roles.ADMIN + user=self.user, domain=domain, role=UserDomainRole.Roles.MANAGER ) response = self.client.get("/") # count = 2 because it is also in screenreader content @@ -1121,23 +1121,25 @@ class TestWithDomainPermissions(TestWithUser): creator=self.user, domain=self.domain_dnssec_none ) self.role, _ = UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain, role=UserDomainRole.Roles.ADMIN + user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER ) UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain_dsdata, role=UserDomainRole.Roles.ADMIN + user=self.user, domain=self.domain_dsdata, role=UserDomainRole.Roles.MANAGER ) UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain_multdsdata, - role=UserDomainRole.Roles.ADMIN, + role=UserDomainRole.Roles.MANAGER, ) UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain_keydata, role=UserDomainRole.Roles.ADMIN + user=self.user, + domain=self.domain_keydata, + role=UserDomainRole.Roles.MANAGER, ) UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain_dnssec_none, - role=UserDomainRole.Roles.ADMIN, + role=UserDomainRole.Roles.MANAGER, ) def tearDown(self): @@ -1223,14 +1225,14 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest): self.assertEqual(response.status_code, 403) -class TestDomainUserManagement(TestDomainOverview): - def test_domain_user_management(self): +class TestDomainManagers(TestDomainOverview): + def test_domain_managers(self): response = self.client.get( reverse("domain-users", kwargs={"pk": self.domain.id}) ) - self.assertContains(response, "User management") + self.assertContains(response, "Domain managers") - def test_domain_user_management_add_link(self): + def test_domain_managers_add_link(self): """Button to get to user add page works.""" management_page = self.app.get( reverse("domain-users", kwargs={"pk": self.domain.id}) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index aa71a7551..2d6070bca 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -656,7 +656,7 @@ class DomainSecurityEmailView(DomainFormBaseView): class DomainUsersView(DomainBaseView): - """User management page in the domain details.""" + """Domain managers page in the domain details.""" template_name = "domain_users.html" @@ -736,7 +736,9 @@ class DomainAddUserView(DomainFormBaseView): try: UserDomainRole.objects.create( - user=requested_user, domain=self.object, role=UserDomainRole.Roles.ADMIN + user=requested_user, + domain=self.object, + role=UserDomainRole.Roles.MANAGER, ) except IntegrityError: # User already has the desired role! Do nothing??