diff --git a/src/epplibwrapper/tests/test_client.py b/src/epplibwrapper/tests/test_client.py index d30ec4865..57c99a05f 100644 --- a/src/epplibwrapper/tests/test_client.py +++ b/src/epplibwrapper/tests/test_client.py @@ -3,10 +3,10 @@ from dateutil.tz import tzlocal # type: ignore from unittest.mock import MagicMock, patch from pathlib import Path from django.test import TestCase +from api.tests.common import less_console_noise_decorator from gevent.exceptions import ConcurrentObjectUseError from epplibwrapper.client import EPPLibWrapper from epplibwrapper.errors import RegistryError, LoginError -from .common import less_console_noise import logging try: @@ -24,99 +24,101 @@ logger = logging.getLogger(__name__) class TestClient(TestCase): """Test the EPPlibwrapper client""" + @less_console_noise_decorator def fake_result(self, code, msg): """Helper function to create a fake Result object""" return Result(code=code, msg=msg, res_data=[], cl_tr_id="cl_tr_id", sv_tr_id="sv_tr_id") + @less_console_noise_decorator @patch("epplibwrapper.client.Client") def test_initialize_client_success(self, mock_client): """Test when the initialize_client is successful""" - with less_console_noise(): - # Mock the Client instance and its methods - mock_connect = MagicMock() - # Create a mock Result instance - mock_result = MagicMock(spec=Result) - mock_result.code = 200 - mock_result.msg = "Success" - mock_result.res_data = ["data1", "data2"] - mock_result.cl_tr_id = "client_id" - mock_result.sv_tr_id = "server_id" - mock_send = MagicMock(return_value=mock_result) - mock_client.return_value.connect = mock_connect - mock_client.return_value.send = mock_send + # Mock the Client instance and its methods + mock_connect = MagicMock() + # Create a mock Result instance + mock_result = MagicMock(spec=Result) + mock_result.code = 200 + mock_result.msg = "Success" + mock_result.res_data = ["data1", "data2"] + mock_result.cl_tr_id = "client_id" + mock_result.sv_tr_id = "server_id" + mock_send = MagicMock(return_value=mock_result) + mock_client.return_value.connect = mock_connect + mock_client.return_value.send = mock_send - # Create EPPLibWrapper instance and initialize client - wrapper = EPPLibWrapper() + # Create EPPLibWrapper instance and initialize client + wrapper = EPPLibWrapper() - # Assert that connect method is called once - mock_connect.assert_called_once() - # Assert that _client is not None after initialization - self.assertIsNotNone(wrapper._client) + # Assert that connect method is called once + mock_connect.assert_called_once() + # Assert that _client is not None after initialization + self.assertIsNotNone(wrapper._client) + @less_console_noise_decorator @patch("epplibwrapper.client.Client") def test_initialize_client_transport_error(self, mock_client): """Test when the send(login) step of initialize_client raises a TransportError.""" - with less_console_noise(): - # Mock the Client instance and its methods - mock_connect = MagicMock() - mock_send = MagicMock(side_effect=TransportError("Transport error")) - mock_client.return_value.connect = mock_connect - mock_client.return_value.send = mock_send + # Mock the Client instance and its methods + mock_connect = MagicMock() + mock_send = MagicMock(side_effect=TransportError("Transport error")) + mock_client.return_value.connect = mock_connect + mock_client.return_value.send = mock_send - with self.assertRaises(RegistryError): - # Create EPPLibWrapper instance and initialize client - # if functioning as expected, initial __init__ should except - # and log any Exception raised - wrapper = EPPLibWrapper() - # so call _initialize_client a second time directly to test - # the raised exception - wrapper._initialize_client() + with self.assertRaises(RegistryError): + # Create EPPLibWrapper instance and initialize client + # if functioning as expected, initial __init__ should except + # and log any Exception raised + wrapper = EPPLibWrapper() + # so call _initialize_client a second time directly to test + # the raised exception + wrapper._initialize_client() + @less_console_noise_decorator @patch("epplibwrapper.client.Client") def test_initialize_client_login_error(self, mock_client): """Test when the send(login) step of initialize_client returns (2400) comamnd failed code.""" - with less_console_noise(): - # Mock the Client instance and its methods - mock_connect = MagicMock() - # Create a mock Result instance - mock_result = MagicMock(spec=Result) - mock_result.code = 2400 - mock_result.msg = "Login failed" - mock_result.res_data = ["data1", "data2"] - mock_result.cl_tr_id = "client_id" - mock_result.sv_tr_id = "server_id" - mock_send = MagicMock(return_value=mock_result) - mock_client.return_value.connect = mock_connect - mock_client.return_value.send = mock_send + # Mock the Client instance and its methods + mock_connect = MagicMock() + # Create a mock Result instance + mock_result = MagicMock(spec=Result) + mock_result.code = 2400 + mock_result.msg = "Login failed" + mock_result.res_data = ["data1", "data2"] + mock_result.cl_tr_id = "client_id" + mock_result.sv_tr_id = "server_id" + mock_send = MagicMock(return_value=mock_result) + mock_client.return_value.connect = mock_connect + mock_client.return_value.send = mock_send - with self.assertRaises(LoginError): - # Create EPPLibWrapper instance and initialize client - # if functioning as expected, initial __init__ should except - # and log any Exception raised - wrapper = EPPLibWrapper() - # so call _initialize_client a second time directly to test - # the raised exception - wrapper._initialize_client() + with self.assertRaises(LoginError): + # Create EPPLibWrapper instance and initialize client + # if functioning as expected, initial __init__ should except + # and log any Exception raised + wrapper = EPPLibWrapper() + # so call _initialize_client a second time directly to test + # the raised exception + wrapper._initialize_client() + @less_console_noise_decorator @patch("epplibwrapper.client.Client") def test_initialize_client_unknown_exception(self, mock_client): """Test when the send(login) step of initialize_client raises an unexpected Exception.""" - with less_console_noise(): - # Mock the Client instance and its methods - mock_connect = MagicMock() - mock_send = MagicMock(side_effect=Exception("Unknown exception")) - mock_client.return_value.connect = mock_connect - mock_client.return_value.send = mock_send + # Mock the Client instance and its methods + mock_connect = MagicMock() + mock_send = MagicMock(side_effect=Exception("Unknown exception")) + mock_client.return_value.connect = mock_connect + mock_client.return_value.send = mock_send - with self.assertRaises(RegistryError): - # Create EPPLibWrapper instance and initialize client - # if functioning as expected, initial __init__ should except - # and log any Exception raised - wrapper = EPPLibWrapper() - # so call _initialize_client a second time directly to test - # the raised exception - wrapper._initialize_client() + with self.assertRaises(RegistryError): + # Create EPPLibWrapper instance and initialize client + # if functioning as expected, initial __init__ should except + # and log any Exception raised + wrapper = EPPLibWrapper() + # so call _initialize_client a second time directly to test + # the raised exception + wrapper._initialize_client() + @less_console_noise_decorator @patch("epplibwrapper.client.Client") def test_initialize_client_fails_recovers_with_send_command(self, mock_client): """Test when the initialize_client fails on the connect() step. And then a subsequent @@ -126,56 +128,56 @@ class TestClient(TestCase): Initialization step fails at app init Send command fails (with 2400 code) prompting retry Client closes and re-initializes, and command is sent successfully""" - with less_console_noise(): - # Mock the Client instance and its methods - # close() should return successfully - mock_close = MagicMock() - mock_client.return_value.close = mock_close - # Create success and failure results - command_success_result = self.fake_result(1000, "Command completed successfully") - command_failure_result = self.fake_result(2400, "Command failed") - # side_effect for the connect() calls - # first connect() should raise an Exception - # subsequent connect() calls should return success - connect_call_count = 0 + # Mock the Client instance and its methods + # close() should return successfully + mock_close = MagicMock() + mock_client.return_value.close = mock_close + # Create success and failure results + command_success_result = self.fake_result(1000, "Command completed successfully") + command_failure_result = self.fake_result(2400, "Command failed") + # side_effect for the connect() calls + # first connect() should raise an Exception + # subsequent connect() calls should return success + connect_call_count = 0 - def connect_side_effect(*args, **kwargs): - nonlocal connect_call_count - connect_call_count += 1 - if connect_call_count == 1: - raise Exception("Connection failed") - else: - return command_success_result + def connect_side_effect(*args, **kwargs): + nonlocal connect_call_count + connect_call_count += 1 + if connect_call_count == 1: + raise Exception("Connection failed") + else: + return command_success_result - mock_connect = MagicMock(side_effect=connect_side_effect) - mock_client.return_value.connect = mock_connect - # side_effect for the send() calls - # first send will be the send("InfoDomainCommand") and should fail - # subsequend send() calls should return success - send_call_count = 0 + mock_connect = MagicMock(side_effect=connect_side_effect) + mock_client.return_value.connect = mock_connect + # side_effect for the send() calls + # first send will be the send("InfoDomainCommand") and should fail + # subsequend send() calls should return success + send_call_count = 0 - def send_side_effect(*args, **kwargs): - nonlocal send_call_count - send_call_count += 1 - if send_call_count == 1: - return command_failure_result - else: - return command_success_result + def send_side_effect(*args, **kwargs): + nonlocal send_call_count + send_call_count += 1 + if send_call_count == 1: + return command_failure_result + else: + return command_success_result - mock_send = MagicMock(side_effect=send_side_effect) - mock_client.return_value.send = mock_send - # Create EPPLibWrapper instance and call send command - wrapper = EPPLibWrapper() - wrapper.send("InfoDomainCommand", cleaned=True) - # two connect() calls should be made, the initial failed connect() - # and the successful connect() during retry() - self.assertEquals(mock_connect.call_count, 2) - # close() should only be called once, during retry() - mock_close.assert_called_once() - # send called 4 times: failed send("InfoDomainCommand"), passed send(logout), - # passed send(login), passed send("InfoDomainCommand") - self.assertEquals(mock_send.call_count, 4) + mock_send = MagicMock(side_effect=send_side_effect) + mock_client.return_value.send = mock_send + # Create EPPLibWrapper instance and call send command + wrapper = EPPLibWrapper() + wrapper.send("InfoDomainCommand", cleaned=True) + # two connect() calls should be made, the initial failed connect() + # and the successful connect() during retry() + self.assertEquals(mock_connect.call_count, 2) + # close() should only be called once, during retry() + mock_close.assert_called_once() + # send called 4 times: failed send("InfoDomainCommand"), passed send(logout), + # passed send(login), passed send("InfoDomainCommand") + self.assertEquals(mock_send.call_count, 4) + @less_console_noise_decorator @patch("epplibwrapper.client.Client") def test_send_command_failed_retries_and_fails_again(self, mock_client): """Test when the send("InfoDomainCommand) call fails with a 2400, prompting a retry @@ -185,42 +187,42 @@ class TestClient(TestCase): Initialization succeeds Send command fails (with 2400 code) prompting retry Client closes and re-initializes, and command fails again with 2400""" - with less_console_noise(): - # Mock the Client instance and its methods - # connect() and close() should succeed throughout - mock_connect = MagicMock() - mock_close = MagicMock() - # Create a mock Result instance - send_command_success_result = self.fake_result(1000, "Command completed successfully") - send_command_failure_result = self.fake_result(2400, "Command failed") + # Mock the Client instance and its methods + # connect() and close() should succeed throughout + mock_connect = MagicMock() + mock_close = MagicMock() + # Create a mock Result instance + send_command_success_result = self.fake_result(1000, "Command completed successfully") + send_command_failure_result = self.fake_result(2400, "Command failed") - # side_effect for send command, passes for all other sends (login, logout), but - # fails for send("InfoDomainCommand") - def side_effect(*args, **kwargs): - if args[0] == "InfoDomainCommand": - return send_command_failure_result - else: - return send_command_success_result + # side_effect for send command, passes for all other sends (login, logout), but + # fails for send("InfoDomainCommand") + def side_effect(*args, **kwargs): + if args[0] == "InfoDomainCommand": + return send_command_failure_result + else: + return send_command_success_result - mock_send = MagicMock(side_effect=side_effect) - mock_client.return_value.connect = mock_connect - mock_client.return_value.close = mock_close - mock_client.return_value.send = mock_send + mock_send = MagicMock(side_effect=side_effect) + mock_client.return_value.connect = mock_connect + mock_client.return_value.close = mock_close + mock_client.return_value.send = mock_send - with self.assertRaises(RegistryError): - # Create EPPLibWrapper instance and initialize client - wrapper = EPPLibWrapper() - # call send, which should throw a RegistryError (after retry) - wrapper.send("InfoDomainCommand", cleaned=True) - # connect() should be called twice, once during initialization, second time - # during retry - self.assertEquals(mock_connect.call_count, 2) - # close() is called once during retry - mock_close.assert_called_once() - # send() is called 5 times: send(login), send(command) fails, send(logout) - # send(login), send(command) - self.assertEquals(mock_send.call_count, 5) + with self.assertRaises(RegistryError): + # Create EPPLibWrapper instance and initialize client + wrapper = EPPLibWrapper() + # call send, which should throw a RegistryError (after retry) + wrapper.send("InfoDomainCommand", cleaned=True) + # connect() should be called twice, once during initialization, second time + # during retry + self.assertEquals(mock_connect.call_count, 2) + # close() is called once during retry + mock_close.assert_called_once() + # send() is called 5 times: send(login), send(command) fails, send(logout) + # send(login), send(command) + self.assertEquals(mock_send.call_count, 5) + @less_console_noise_decorator @patch("epplibwrapper.client.Client") def test_send_command_failure_prompts_successful_retry(self, mock_client): """Test when the send("InfoDomainCommand) call fails with a 2400, prompting a retry @@ -229,40 +231,40 @@ class TestClient(TestCase): Initialization succeeds Send command fails (with 2400 code) prompting retry Client closes and re-initializes, and command succeeds""" - with less_console_noise(): - # Mock the Client instance and its methods - # connect() and close() should succeed throughout - mock_connect = MagicMock() - mock_close = MagicMock() - # create success and failure result messages - send_command_success_result = self.fake_result(1000, "Command completed successfully") - send_command_failure_result = self.fake_result(2400, "Command failed") - # side_effect for send call, initial send(login) succeeds during initialization, next send(command) - # fails, subsequent sends (logout, login, command) all succeed - send_call_count = 0 + # Mock the Client instance and its methods + # connect() and close() should succeed throughout + mock_connect = MagicMock() + mock_close = MagicMock() + # create success and failure result messages + send_command_success_result = self.fake_result(1000, "Command completed successfully") + send_command_failure_result = self.fake_result(2400, "Command failed") + # side_effect for send call, initial send(login) succeeds during initialization, next send(command) + # fails, subsequent sends (logout, login, command) all succeed + send_call_count = 0 - def side_effect(*args, **kwargs): - nonlocal send_call_count - send_call_count += 1 - if send_call_count == 2: - return send_command_failure_result - else: - return send_command_success_result + def side_effect(*args, **kwargs): + nonlocal send_call_count + send_call_count += 1 + if send_call_count == 2: + return send_command_failure_result + else: + return send_command_success_result - mock_send = MagicMock(side_effect=side_effect) - mock_client.return_value.connect = mock_connect - mock_client.return_value.close = mock_close - mock_client.return_value.send = mock_send - # Create EPPLibWrapper instance and initialize client - wrapper = EPPLibWrapper() - wrapper.send("InfoDomainCommand", cleaned=True) - # connect() is called twice, once during initialization of app, once during retry - self.assertEquals(mock_connect.call_count, 2) - # close() is called once, during retry - mock_close.assert_called_once() - # send() is called 5 times: send(login), send(command) fail, send(logout), send(login), send(command) - self.assertEquals(mock_send.call_count, 5) + mock_send = MagicMock(side_effect=side_effect) + mock_client.return_value.connect = mock_connect + mock_client.return_value.close = mock_close + mock_client.return_value.send = mock_send + # Create EPPLibWrapper instance and initialize client + wrapper = EPPLibWrapper() + wrapper.send("InfoDomainCommand", cleaned=True) + # connect() is called twice, once during initialization of app, once during retry + self.assertEquals(mock_connect.call_count, 2) + # close() is called once, during retry + mock_close.assert_called_once() + # send() is called 5 times: send(login), send(command) fail, send(logout), send(login), send(command) + self.assertEquals(mock_send.call_count, 5) + @less_console_noise_decorator def fake_failure_send_concurrent_threads(self, command=None, cleaned=None): """ Raises a ConcurrentObjectUseError, which gevent throws when accessing @@ -277,6 +279,7 @@ class TestClient(TestCase): """ pass # noqa + @less_console_noise_decorator def fake_success_send(self, command=None, cleaned=None): """ Simulates receiving a success response from EPP. @@ -292,6 +295,7 @@ class TestClient(TestCase): ) return mock + @less_console_noise_decorator def fake_info_domain_received(self, command=None, cleaned=None): """ Simulates receiving a response by reading from a predefined XML file. @@ -300,6 +304,7 @@ class TestClient(TestCase): xml = (location).read_bytes() return xml + @less_console_noise_decorator def get_fake_epp_result(self): """Mimics a return from EPP by returning a dictionary in the same format""" result = { @@ -338,6 +343,7 @@ class TestClient(TestCase): } return result + @less_console_noise_decorator def test_send_command_close_failure_recovers(self): """ Validates the resilience of the connection handling mechanism @@ -350,7 +356,6 @@ class TestClient(TestCase): - Subsequently, the client re-initializes the connection. - A retry of the command execution post-reinitialization succeeds. """ - expected_result = self.get_fake_epp_result() wrapper = None # Trigger a retry diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 7a50c3da0..9f8a0cbb6 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -72,6 +72,29 @@ body { } } +.section--outlined__header--no-portfolio { + .section--outlined__search, + .section--outlined__utility-button { + margin-top: units(2); + } + + @include at-media(tablet) { + display: flex; + column-gap: units(3); + + .section--outlined__search, + .section--outlined__utility-button { + margin-top: 0; + } + .section--outlined__search { + flex-grow: 4; + // Align right + max-width: 383px; + margin-left: auto; + } + } +} + .break-word { word-break: break-word; } diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 8ec43705f..7fa379c0b 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -204,4 +204,13 @@ a.usa-button--unstyled:visited { box-shadow: none; } } - \ No newline at end of file + +.usa-icon.usa-icon--big { + margin: 0; + height: 1.5em; + width: 1.5em; +} + +.margin-right-neg-4px { + margin-right: -4px; +} diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index b91e0980c..7f9db0e41 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -9,7 +9,7 @@ from django.urls import include, path from django.views.generic import RedirectView from registrar import views -from registrar.views.admin_views import ( +from registrar.views.report_views import ( ExportDataDomainsGrowth, ExportDataFederal, ExportDataFull, @@ -19,6 +19,7 @@ from registrar.views.admin_views import ( ExportDataUnmanagedDomains, AnalyticsView, ExportDomainRequestDataFull, + ExportDataTypeUser, ) from registrar.views.domain_request import Step @@ -124,6 +125,11 @@ urlpatterns = [ name="analytics", ), path("admin/", admin.site.urls), + path( + "reports/export_data_type_user/", + ExportDataTypeUser.as_view(), + name="export_data_type_user", + ), path( "domain-request//edit/", views.DomainRequestWizard.as_view(), diff --git a/src/registrar/forms/__init__.py b/src/registrar/forms/__init__.py index a0c71d48c..374b3102f 100644 --- a/src/registrar/forms/__init__.py +++ b/src/registrar/forms/__init__.py @@ -10,3 +10,6 @@ from .domain import ( DomainDsdataFormset, DomainDsdataForm, ) +from .portfolio import ( + PortfolioOrgAddressForm, +) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 9b8f1b7fc..02a0724d1 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -458,7 +458,7 @@ class DomainOrgNameAddressForm(forms.ModelForm): # the database fields have blank=True so ModelForm doesn't create # required fields by default. Use this list in __init__ to mark each # of these fields as required - required = ["organization_name", "address_line1", "city", "zipcode"] + required = ["organization_name", "address_line1", "city", "state_territory", "zipcode"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py new file mode 100644 index 000000000..9362c7bbd --- /dev/null +++ b/src/registrar/forms/portfolio.py @@ -0,0 +1,69 @@ +"""Forms for portfolio.""" + +import logging +from django import forms +from django.core.validators import RegexValidator + +from ..models import DomainInformation, Portfolio + +logger = logging.getLogger(__name__) + + +class PortfolioOrgAddressForm(forms.ModelForm): + """Form for updating the portfolio org mailing address.""" + + zipcode = forms.CharField( + label="Zip code", + validators=[ + RegexValidator( + "^[0-9]{5}(?:-[0-9]{4})?$|^$", + message="Enter a zip code in the required format, like 12345 or 12345-6789.", + ) + ], + ) + + class Meta: + model = Portfolio + fields = [ + "address_line1", + "address_line2", + "city", + "state_territory", + "zipcode", + # "urbanization", + ] + error_messages = { + "address_line1": {"required": "Enter the street address of your organization."}, + "city": {"required": "Enter the city where your organization is located."}, + "state_territory": { + "required": "Select the state, territory, or military post where your organization is located." + }, + } + widgets = { + # We need to set the required attributed for State/territory + # because for this fields we are creating an individual + # instance of the Select. For the other fields we use the for loop to set + # the class's required attribute to true. + "address_line1": forms.TextInput, + "address_line2": forms.TextInput, + "city": forms.TextInput, + "state_territory": forms.Select( + attrs={ + "required": True, + }, + choices=DomainInformation.StateTerritoryChoices.choices, + ), + # "urbanization": forms.TextInput, + } + + # the database fields have blank=True so ModelForm doesn't create + # required fields by default. Use this list in __init__ to mark each + # of these fields as required + required = ["address_line1", "city", "state_territory", "zipcode"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field_name in self.required: + self.fields[field_name].required = True + self.fields["state_territory"].widget.attrs.pop("maxlength", None) + self.fields["zipcode"].widget.attrs.pop("maxlength", None) diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index 7d0500e19..06b01e672 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -6,11 +6,6 @@ from registrar.models.federal_agency import FederalAgency from .utility.time_stamped_model import TimeStampedModel -# def get_default_federal_agency(): -# """returns non-federal agency""" -# return FederalAgency.objects.filter(agency="Non-Federal Agency").first() - - class Portfolio(TimeStampedModel): """ Portfolio is used for organizing domains/domain-requests into diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py index f9d4303c4..4f74b4163 100644 --- a/src/registrar/models/utility/generic_helper.py +++ b/src/registrar/models/utility/generic_helper.py @@ -170,18 +170,11 @@ class CreateOrUpdateOrganizationTypeHelper: # There is no avenue for this to occur in the UI, # as such - this can only occur if the object is initialized in this way. # Or if there are pre-existing data. - logger.debug( - "create_or_update_organization_type() -> is_election_board " - f"cannot exist for {generic_org_type}. Setting to None." - ) self.instance.is_election_board = None self.instance.organization_type = generic_org_type else: # This can only happen with manual data tinkering, which causes these to be out of sync. if self.instance.is_election_board is None: - logger.warning( - "create_or_update_organization_type() -> is_election_board is out of sync. Updating value." - ) self.instance.is_election_board = False if self.instance.is_election_board: @@ -218,10 +211,6 @@ class CreateOrUpdateOrganizationTypeHelper: # There is no avenue for this to occur in the UI, # as such - this can only occur if the object is initialized in this way. # Or if there are pre-existing data. - logger.warning( - "create_or_update_organization_type() -> is_election_board " - f"cannot exist for {current_org_type}. Setting to None." - ) self.instance.is_election_board = None else: # if self.instance.organization_type is set to None, then this means diff --git a/src/registrar/templates/includes/domains_table.html b/src/registrar/templates/includes/domains_table.html index cef3c61a4..4fea482ac 100644 --- a/src/registrar/templates/includes/domains_table.html +++ b/src/registrar/templates/includes/domains_table.html @@ -1,16 +1,14 @@ {% load static %}
-
+
{% if portfolio is None %} -
-

Domains

-
- +

Domains

+ {% endif %} -
-
+ +
{% if portfolio %} -
+
Filter by
diff --git a/src/registrar/templates/portfolio_organization.html b/src/registrar/templates/portfolio_organization.html index c7eae7130..0dede3c32 100644 --- a/src/registrar/templates/portfolio_organization.html +++ b/src/registrar/templates/portfolio_organization.html @@ -1,8 +1,64 @@ {% extends 'portfolio_base.html' %} +{% load static field_helpers%} + +{% block title %}Organization mailing address | {{ portfolio.name }} | {% endblock %} {% load static %} {% block portfolio_content %} -

Organization

+
+
+

+ Portfolio name: {{ portfolio }} +

+ {% include 'portfolio_organization_sidebar.html' %} +
+ +
+ +

Organization

+ +

The name of your federal agency will be publicly listed as the domain registrant.

+ +

+ The federal agency for your organization can’t be updated here. + To suggest an update, email help@get.gov. +

+ + {% include "includes/form_errors.html" with form=form %} + + {% include "includes/required_fields.html" %} + +
+ {% csrf_token %} + +

+ Federal agency + {{ portfolio }} +

+ + {% input_with_errors form.address_line1 %} + + {% input_with_errors form.address_line2 %} + + {% input_with_errors form.city %} + + {% input_with_errors form.state_territory %} + + {% with add_class="usa-input--small" %} + {% input_with_errors form.zipcode %} + {% endwith %} + + +
+
+
{% endblock %} diff --git a/src/registrar/templates/portfolio_organization_sidebar.html b/src/registrar/templates/portfolio_organization_sidebar.html new file mode 100644 index 000000000..6199c2476 --- /dev/null +++ b/src/registrar/templates/portfolio_organization_sidebar.html @@ -0,0 +1,23 @@ +{% load static url_helpers %} + +
+ +
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index b005211a6..a4c3e2ef4 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -41,6 +41,8 @@ from registrar.models.user_domain_role import UserDomainRole from registrar.models.utility.contact_error import ContactError, ContactErrorCodes +from api.tests.common import less_console_noise_decorator + logger = logging.getLogger(__name__) @@ -525,230 +527,230 @@ class AuditedAdminMockData: class MockDb(TestCase): - """Hardcoded mocks make test case assertions straightforward.""" - def setUp(self): - super().setUp() + @classmethod + @less_console_noise_decorator + def sharedSetUp(cls): username = "test_user" first_name = "First" last_name = "Last" email = "info@example.com" - self.user = get_user_model().objects.create( + cls.user = get_user_model().objects.create( username=username, first_name=first_name, last_name=last_name, email=email ) current_date = get_time_aware_date(datetime(2024, 4, 2)) # Create start and end dates using timedelta - self.end_date = current_date + timedelta(days=2) - self.start_date = current_date - timedelta(days=2) + cls.end_date = current_date + timedelta(days=2) + cls.start_date = current_date - timedelta(days=2) - self.federal_agency_1, _ = FederalAgency.objects.get_or_create(agency="World War I Centennial Commission") - self.federal_agency_2, _ = FederalAgency.objects.get_or_create(agency="Armed Forces Retirement Home") + cls.federal_agency_1, _ = FederalAgency.objects.get_or_create(agency="World War I Centennial Commission") + cls.federal_agency_2, _ = FederalAgency.objects.get_or_create(agency="Armed Forces Retirement Home") - self.domain_1, _ = Domain.objects.get_or_create( + cls.domain_1, _ = Domain.objects.get_or_create( name="cdomain1.gov", state=Domain.State.READY, first_ready=get_time_aware_date(datetime(2024, 4, 2)) ) - self.domain_2, _ = Domain.objects.get_or_create(name="adomain2.gov", state=Domain.State.DNS_NEEDED) - self.domain_3, _ = Domain.objects.get_or_create(name="ddomain3.gov", state=Domain.State.ON_HOLD) - self.domain_4, _ = Domain.objects.get_or_create(name="bdomain4.gov", state=Domain.State.UNKNOWN) - self.domain_5, _ = Domain.objects.get_or_create( + cls.domain_2, _ = Domain.objects.get_or_create(name="adomain2.gov", state=Domain.State.DNS_NEEDED) + cls.domain_3, _ = Domain.objects.get_or_create(name="ddomain3.gov", state=Domain.State.ON_HOLD) + cls.domain_4, _ = Domain.objects.get_or_create(name="bdomain4.gov", state=Domain.State.UNKNOWN) + cls.domain_5, _ = Domain.objects.get_or_create( name="bdomain5.gov", state=Domain.State.DELETED, deleted=get_time_aware_date(datetime(2023, 11, 1)) ) - self.domain_6, _ = Domain.objects.get_or_create( + cls.domain_6, _ = Domain.objects.get_or_create( name="bdomain6.gov", state=Domain.State.DELETED, deleted=get_time_aware_date(datetime(1980, 10, 16)) ) - self.domain_7, _ = Domain.objects.get_or_create( + cls.domain_7, _ = Domain.objects.get_or_create( name="xdomain7.gov", state=Domain.State.DELETED, deleted=get_time_aware_date(datetime(2024, 4, 2)) ) - self.domain_8, _ = Domain.objects.get_or_create( + cls.domain_8, _ = Domain.objects.get_or_create( name="sdomain8.gov", state=Domain.State.DELETED, deleted=get_time_aware_date(datetime(2024, 4, 2)) ) # We use timezone.make_aware to sync to server time a datetime object with the current date (using date.today()) # and a specific time (using datetime.min.time()). # Deleted yesterday - self.domain_9, _ = Domain.objects.get_or_create( + cls.domain_9, _ = Domain.objects.get_or_create( name="zdomain9.gov", state=Domain.State.DELETED, deleted=get_time_aware_date(datetime(2024, 4, 1)), ) # ready tomorrow - self.domain_10, _ = Domain.objects.get_or_create( + cls.domain_10, _ = Domain.objects.get_or_create( name="adomain10.gov", state=Domain.State.READY, first_ready=get_time_aware_date(datetime(2024, 4, 3)), ) - self.domain_11, _ = Domain.objects.get_or_create( + cls.domain_11, _ = Domain.objects.get_or_create( name="cdomain11.gov", state=Domain.State.READY, first_ready=get_time_aware_date(datetime(2024, 4, 2)) ) - self.domain_12, _ = Domain.objects.get_or_create( + cls.domain_12, _ = Domain.objects.get_or_create( name="zdomain12.gov", state=Domain.State.READY, first_ready=get_time_aware_date(datetime(2024, 4, 2)) ) - self.domain_information_1, _ = DomainInformation.objects.get_or_create( - creator=self.user, - domain=self.domain_1, + cls.domain_information_1, _ = DomainInformation.objects.get_or_create( + creator=cls.user, + domain=cls.domain_1, generic_org_type="federal", - federal_agency=self.federal_agency_1, + federal_agency=cls.federal_agency_1, federal_type="executive", is_election_board=False, ) - self.domain_information_2, _ = DomainInformation.objects.get_or_create( - creator=self.user, domain=self.domain_2, generic_org_type="interstate", is_election_board=True + cls.domain_information_2, _ = DomainInformation.objects.get_or_create( + creator=cls.user, domain=cls.domain_2, generic_org_type="interstate", is_election_board=True ) - self.domain_information_3, _ = DomainInformation.objects.get_or_create( - creator=self.user, - domain=self.domain_3, + cls.domain_information_3, _ = DomainInformation.objects.get_or_create( + creator=cls.user, + domain=cls.domain_3, generic_org_type="federal", - federal_agency=self.federal_agency_2, + federal_agency=cls.federal_agency_2, is_election_board=False, ) - self.domain_information_4, _ = DomainInformation.objects.get_or_create( - creator=self.user, - domain=self.domain_4, + cls.domain_information_4, _ = DomainInformation.objects.get_or_create( + creator=cls.user, + domain=cls.domain_4, generic_org_type="federal", - federal_agency=self.federal_agency_2, + federal_agency=cls.federal_agency_2, is_election_board=False, ) - self.domain_information_5, _ = DomainInformation.objects.get_or_create( - creator=self.user, - domain=self.domain_5, + cls.domain_information_5, _ = DomainInformation.objects.get_or_create( + creator=cls.user, + domain=cls.domain_5, generic_org_type="federal", - federal_agency=self.federal_agency_2, + federal_agency=cls.federal_agency_2, is_election_board=False, ) - self.domain_information_6, _ = DomainInformation.objects.get_or_create( - creator=self.user, - domain=self.domain_6, + cls.domain_information_6, _ = DomainInformation.objects.get_or_create( + creator=cls.user, + domain=cls.domain_6, generic_org_type="federal", - federal_agency=self.federal_agency_2, + federal_agency=cls.federal_agency_2, is_election_board=False, ) - self.domain_information_7, _ = DomainInformation.objects.get_or_create( - creator=self.user, - domain=self.domain_7, + cls.domain_information_7, _ = DomainInformation.objects.get_or_create( + creator=cls.user, + domain=cls.domain_7, generic_org_type="federal", - federal_agency=self.federal_agency_2, + federal_agency=cls.federal_agency_2, is_election_board=False, ) - self.domain_information_8, _ = DomainInformation.objects.get_or_create( - creator=self.user, - domain=self.domain_8, + cls.domain_information_8, _ = DomainInformation.objects.get_or_create( + creator=cls.user, + domain=cls.domain_8, generic_org_type="federal", - federal_agency=self.federal_agency_2, + federal_agency=cls.federal_agency_2, is_election_board=False, ) - self.domain_information_9, _ = DomainInformation.objects.get_or_create( - creator=self.user, - domain=self.domain_9, + cls.domain_information_9, _ = DomainInformation.objects.get_or_create( + creator=cls.user, + domain=cls.domain_9, generic_org_type="federal", - federal_agency=self.federal_agency_2, + federal_agency=cls.federal_agency_2, is_election_board=False, ) - self.domain_information_10, _ = DomainInformation.objects.get_or_create( - creator=self.user, - domain=self.domain_10, + cls.domain_information_10, _ = DomainInformation.objects.get_or_create( + creator=cls.user, + domain=cls.domain_10, generic_org_type="federal", - federal_agency=self.federal_agency_2, + federal_agency=cls.federal_agency_2, is_election_board=False, ) - self.domain_information_11, _ = DomainInformation.objects.get_or_create( - creator=self.user, - domain=self.domain_11, + cls.domain_information_11, _ = DomainInformation.objects.get_or_create( + creator=cls.user, + domain=cls.domain_11, generic_org_type="federal", - federal_agency=self.federal_agency_1, + federal_agency=cls.federal_agency_1, federal_type="executive", is_election_board=False, ) - self.domain_information_12, _ = DomainInformation.objects.get_or_create( - creator=self.user, - domain=self.domain_12, + cls.domain_information_12, _ = DomainInformation.objects.get_or_create( + creator=cls.user, + domain=cls.domain_12, generic_org_type="interstate", is_election_board=False, ) - self.meoward_user = get_user_model().objects.create( + cls.meoward_user = get_user_model().objects.create( username="meoward_username", first_name="first_meoward", last_name="last_meoward", email="meoward@rocks.com" ) - lebowski_user = get_user_model().objects.create( + cls.lebowski_user = get_user_model().objects.create( username="big_lebowski", first_name="big", last_name="lebowski", email="big_lebowski@dude.co" ) _, created = UserDomainRole.objects.get_or_create( - user=self.meoward_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER + user=cls.meoward_user, domain=cls.domain_1, role=UserDomainRole.Roles.MANAGER ) _, created = UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER + user=cls.user, domain=cls.domain_1, role=UserDomainRole.Roles.MANAGER ) _, created = UserDomainRole.objects.get_or_create( - user=lebowski_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER + user=cls.lebowski_user, domain=cls.domain_1, role=UserDomainRole.Roles.MANAGER ) _, created = UserDomainRole.objects.get_or_create( - user=self.meoward_user, domain=self.domain_2, role=UserDomainRole.Roles.MANAGER + user=cls.meoward_user, domain=cls.domain_2, role=UserDomainRole.Roles.MANAGER ) _, created = UserDomainRole.objects.get_or_create( - user=self.meoward_user, domain=self.domain_11, role=UserDomainRole.Roles.MANAGER + user=cls.meoward_user, domain=cls.domain_11, role=UserDomainRole.Roles.MANAGER ) _, created = UserDomainRole.objects.get_or_create( - user=self.meoward_user, domain=self.domain_12, role=UserDomainRole.Roles.MANAGER + user=cls.meoward_user, domain=cls.domain_12, role=UserDomainRole.Roles.MANAGER ) _, created = DomainInvitation.objects.get_or_create( - email=self.meoward_user.email, - domain=self.domain_1, + email=cls.meoward_user.email, + domain=cls.domain_1, status=DomainInvitation.DomainInvitationStatus.RETRIEVED, ) _, created = DomainInvitation.objects.get_or_create( email="woofwardthethird@rocks.com", - domain=self.domain_1, + domain=cls.domain_1, status=DomainInvitation.DomainInvitationStatus.INVITED, ) _, created = DomainInvitation.objects.get_or_create( - email="squeaker@rocks.com", domain=self.domain_2, status=DomainInvitation.DomainInvitationStatus.INVITED + email="squeaker@rocks.com", domain=cls.domain_2, status=DomainInvitation.DomainInvitationStatus.INVITED ) _, created = DomainInvitation.objects.get_or_create( - email="squeaker@rocks.com", domain=self.domain_10, status=DomainInvitation.DomainInvitationStatus.INVITED + email="squeaker@rocks.com", domain=cls.domain_10, status=DomainInvitation.DomainInvitationStatus.INVITED ) with less_console_noise(): - self.domain_request_1 = completed_domain_request( + cls.domain_request_1 = completed_domain_request( status=DomainRequest.DomainRequestStatus.STARTED, name="city1.gov", ) - self.domain_request_2 = completed_domain_request( + cls.domain_request_2 = completed_domain_request( status=DomainRequest.DomainRequestStatus.IN_REVIEW, name="city2.gov", ) - self.domain_request_3 = completed_domain_request( + cls.domain_request_3 = completed_domain_request( status=DomainRequest.DomainRequestStatus.STARTED, name="city3.gov", ) - self.domain_request_4 = completed_domain_request( + cls.domain_request_4 = completed_domain_request( status=DomainRequest.DomainRequestStatus.STARTED, name="city4.gov", is_election_board=True, generic_org_type="city", ) - self.domain_request_5 = completed_domain_request( + cls.domain_request_5 = completed_domain_request( status=DomainRequest.DomainRequestStatus.APPROVED, name="city5.gov", ) - self.domain_request_6 = completed_domain_request( + cls.domain_request_6 = completed_domain_request( status=DomainRequest.DomainRequestStatus.STARTED, name="city6.gov", ) - self.domain_request_3.submit() - self.domain_request_4.submit() - self.domain_request_6.submit() + cls.domain_request_3.submit() + cls.domain_request_4.submit() + cls.domain_request_6.submit() other, _ = Contact.objects.get_or_create( first_name="Testy1232", @@ -769,29 +771,56 @@ class MockDb(TestCase): website_3, _ = Website.objects.get_or_create(website="https://www.example.com") website_4, _ = Website.objects.get_or_create(website="https://www.example2.com") - self.domain_request_3.other_contacts.add(other, other_2) - self.domain_request_3.alternative_domains.add(website, website_2) - self.domain_request_3.current_websites.add(website_3, website_4) - self.domain_request_3.cisa_representative_email = "test@igorville.com" - self.domain_request_3.submission_date = get_time_aware_date(datetime(2024, 4, 2)) - self.domain_request_3.save() + cls.domain_request_3.other_contacts.add(other, other_2) + cls.domain_request_3.alternative_domains.add(website, website_2) + cls.domain_request_3.current_websites.add(website_3, website_4) + cls.domain_request_3.cisa_representative_email = "test@igorville.com" + cls.domain_request_3.submission_date = get_time_aware_date(datetime(2024, 4, 2)) + cls.domain_request_3.save() - self.domain_request_4.submission_date = get_time_aware_date(datetime(2024, 4, 2)) - self.domain_request_4.save() + cls.domain_request_4.submission_date = get_time_aware_date(datetime(2024, 4, 2)) + cls.domain_request_4.save() - self.domain_request_6.submission_date = get_time_aware_date(datetime(2024, 4, 2)) - self.domain_request_6.save() + cls.domain_request_6.submission_date = get_time_aware_date(datetime(2024, 4, 2)) + cls.domain_request_6.save() - def tearDown(self): - super().tearDown() + @classmethod + def sharedTearDown(cls): PublicContact.objects.all().delete() Domain.objects.all().delete() DomainInformation.objects.all().delete() DomainRequest.objects.all().delete() - User.objects.all().delete() UserDomainRole.objects.all().delete() + User.objects.all().delete() DomainInvitation.objects.all().delete() - FederalAgency.objects.all().delete() + cls.federal_agency_1.delete() + cls.federal_agency_2.delete() + + +class MockDbForSharedTests(MockDb): + """Set up and tear down test data that is shared across all tests in a class""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.sharedSetUp() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + cls.sharedTearDown() + + +class MockDbForIndividualTests(MockDb): + """Set up and tear down test data for each test in a class""" + + def setUp(self): + super().setUp() + self.sharedSetUp() + + def tearDown(self): + super().tearDown() + self.sharedTearDown() def mock_user(): @@ -840,6 +869,19 @@ def create_user(): return user +def create_test_user(): + username = "test_user" + first_name = "First" + last_name = "Last" + email = "info@example.com" + phone = "8003111234" + title = "test title" + user = get_user_model().objects.create( + username=username, first_name=first_name, last_name=last_name, email=email, phone=phone, title=title + ) + return user + + def create_ready_domain(): domain, _ = Domain.objects.get_or_create(name="city.gov", state=Domain.State.READY) return domain diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 39559523b..c145e1f98 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1,17 +1,11 @@ -from datetime import date, datetime +from datetime import datetime from django.utils import timezone -import re -from django.test import TestCase, RequestFactory, Client, override_settings +from django.test import TestCase, RequestFactory, Client from django.contrib.admin.sites import AdminSite -from contextlib import ExitStack from api.tests.common import less_console_noise_decorator -from django_webtest import WebTest # type: ignore -from django.contrib import messages from django.urls import reverse from registrar.admin import ( DomainAdmin, - DomainRequestAdmin, - DomainRequestAdminForm, DomainInvitationAdmin, ListHeaderAdmin, MyUserAdmin, @@ -48,8 +42,7 @@ from registrar.models.senior_official import SeniorOfficial from registrar.models.user_domain_role import UserDomainRole from registrar.models.verified_by_staff import VerifiedByStaff from .common import ( - MockDb, - MockSESClient, + MockDbForSharedTests, AuditedAdminMockData, completed_domain_request, generic_domain_object, @@ -57,18 +50,13 @@ from .common import ( mock_user, create_superuser, create_user, - create_ready_domain, multiple_unalphabetical_domain_objects, - MockEppLib, GenericTestHelper, ) from django.contrib.sessions.backends.db import SessionStore from django.contrib.auth import get_user_model -from unittest.mock import ANY, call, patch, Mock -from unittest import skip +from unittest.mock import patch, Mock -from django.conf import settings -import boto3_mocking # type: ignore import logging logger = logging.getLogger(__name__) @@ -78,6 +66,7 @@ class TestFsmModelResource(TestCase): def setUp(self): self.resource = FsmModelResource() + @less_console_noise_decorator def test_init_instance(self): """Test initializing an instance of a class with a FSM field""" @@ -92,6 +81,7 @@ class TestFsmModelResource(TestCase): self.assertIsInstance(instance, Domain) self.assertEqual(instance.state, "ready") + @less_console_noise_decorator def test_import_field(self): """Test that importing a field does not import FSM field""" @@ -117,2709 +107,37 @@ class TestFsmModelResource(TestCase): fsm_field_mock.save.assert_not_called() -class TestDomainAdmin(MockEppLib, WebTest): - # csrf checks do not work with WebTest. - # We disable them here. TODO for another ticket. - csrf_checks = False - - def setUp(self): - self.site = AdminSite() - self.admin = DomainAdmin(model=Domain, admin_site=self.site) - self.client = Client(HTTP_HOST="localhost:8080") - self.superuser = create_superuser() - self.staffuser = create_user() - self.factory = RequestFactory() - self.app.set_user(self.superuser.username) - self.client.force_login(self.superuser) - - # Add domain data - self.ready_domain, _ = Domain.objects.get_or_create(name="fakeready.gov", state=Domain.State.READY) - self.unknown_domain, _ = Domain.objects.get_or_create(name="fakeunknown.gov", state=Domain.State.UNKNOWN) - self.dns_domain, _ = Domain.objects.get_or_create(name="fakedns.gov", state=Domain.State.DNS_NEEDED) - self.hold_domain, _ = Domain.objects.get_or_create(name="fakehold.gov", state=Domain.State.ON_HOLD) - self.deleted_domain, _ = Domain.objects.get_or_create(name="fakedeleted.gov", state=Domain.State.DELETED) - - # Contains some test tools - self.test_helper = GenericTestHelper( - factory=self.factory, - user=self.superuser, - admin=self.admin, - url=reverse("admin:registrar_domain_changelist"), - model=Domain, - client=self.client, - ) - super().setUp() - - @less_console_noise_decorator - def test_staff_can_see_cisa_region_federal(self): - """Tests if staff can see CISA Region: N/A""" - - # Create a fake domain request - _domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - _domain_request.approve() - - domain = _domain_request.approved_domain - p = "userpass" - self.client.login(username="staffuser", password=p) - response = self.client.get( - "/admin/registrar/domain/{}/change/".format(domain.pk), - follow=True, - ) - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - - # Test if the page has the right CISA region - expected_html = '
CISA region: N/A
' - # Remove whitespace from expected_html - expected_html = "".join(expected_html.split()) - - # Remove whitespace from response content - response_content = "".join(response.content.decode().split()) - - # Check if response contains expected_html - self.assertIn(expected_html, response_content) - - @less_console_noise_decorator - def test_staff_can_see_cisa_region_non_federal(self): - """Tests if staff can see the correct CISA region""" - - # Create a fake domain request. State will be NY (2). - _domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.IN_REVIEW, generic_org_type="interstate" - ) - - _domain_request.approve() - - domain = _domain_request.approved_domain - - p = "userpass" - self.client.login(username="staffuser", password=p) - response = self.client.get( - "/admin/registrar/domain/{}/change/".format(domain.pk), - follow=True, - ) - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - - # Test if the page has the right CISA region - expected_html = '
CISA region: 2
' - # Remove whitespace from expected_html - expected_html = "".join(expected_html.split()) - - # Remove whitespace from response content - response_content = "".join(response.content.decode().split()) - - # Check if response contains expected_html - self.assertIn(expected_html, response_content) - - @less_console_noise_decorator - def test_has_model_description(self): - """Tests if this model has a model description on the table view""" - p = "adminpass" - self.client.login(username="superuser", password=p) - response = self.client.get( - "/admin/registrar/domain/", - follow=True, - ) - - # Make sure that the page is loaded correctly - self.assertEqual(response.status_code, 200) - - # Test for a description snippet - self.assertContains(response, "This table contains all approved domains in the .gov registrar.") - self.assertContains(response, "Show more") - - @less_console_noise_decorator - def test_contact_fields_on_domain_change_form_have_detail_table(self): - """Tests if the contact fields in the inlined Domain information have the detail table - which displays title, email, and phone""" - - # Create fake creator - _creator = User.objects.create( - username="MrMeoward", - first_name="Meoward", - last_name="Jones", - email="meoward.jones@igorville.gov", - phone="(555) 123 12345", - title="Treat inspector", - ) - - # Create a fake domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator) - domain_request.approve() - _domain_info = DomainInformation.objects.filter(domain=domain_request.approved_domain).get() - domain = Domain.objects.filter(domain_info=_domain_info).get() - - p = "adminpass" - self.client.login(username="superuser", password=p) - response = self.client.get( - "/admin/registrar/domain/{}/change/".format(domain.pk), - follow=True, - ) - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - - # Check that the fields have the right values. - # == Check for the creator == # - - # Check for the right title, email, and phone number in the response. - # We only need to check for the end tag - # (Otherwise this test will fail if we change classes, etc) - self.assertContains(response, "Treat inspector") - self.assertContains(response, "meoward.jones@igorville.gov") - self.assertContains(response, "(555) 123 12345") - - # Check for the field itself - self.assertContains(response, "Meoward Jones") - - # == Check for the submitter == # - self.assertContains(response, "mayor@igorville.gov") - - self.assertContains(response, "Admin Tester") - self.assertContains(response, "(555) 555 5556") - self.assertContains(response, "Testy2 Tester2") - - # == Check for the senior_official == # - self.assertContains(response, "testy@town.com") - self.assertContains(response, "Chief Tester") - self.assertContains(response, "(555) 555 5555") - - # Includes things like readonly fields - self.assertContains(response, "Testy Tester") - - # == Test the other_employees field == # - self.assertContains(response, "testy2@town.com") - self.assertContains(response, "Another Tester") - self.assertContains(response, "(555) 555 5557") - - # Test for the copy link - self.assertContains(response, "usa-button__clipboard") - - @less_console_noise_decorator - def test_helper_text(self): - """ - Tests for the correct helper text on this page - """ - - # Create a ready domain with a preset expiration date - domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) - - p = "adminpass" - self.client.login(username="superuser", password=p) - response = self.client.get( - "/admin/registrar/domain/{}/change/".format(domain.pk), - follow=True, - ) - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - - # These should exist in the response - expected_values = [ - ("expiration_date", "Date the domain expires in the registry"), - ("first_ready_at", 'Date when this domain first moved into "ready" state; date will never change'), - ("deleted_at", 'Will appear blank unless the domain is in "deleted" state'), - ] - self.test_helper.assert_response_contains_distinct_values(response, expected_values) - - @less_console_noise_decorator - def test_helper_text_state(self): - """ - Tests for the correct state helper text on this page - """ - - # We don't need to check for all text content, just a portion of it - expected_unknown_domain_message = "The creator of the associated domain request has not logged in to" - expected_dns_message = "Before this domain can be used, name server addresses need" - expected_hold_message = "While on hold, this domain" - expected_deleted_message = "This domain was permanently removed from the registry." - expected_messages = [ - (self.ready_domain, "This domain has name servers and is ready for use."), - (self.unknown_domain, expected_unknown_domain_message), - (self.dns_domain, expected_dns_message), - (self.hold_domain, expected_hold_message), - (self.deleted_domain, expected_deleted_message), - ] - - p = "adminpass" - self.client.login(username="superuser", password=p) - for domain, message in expected_messages: - with self.subTest(domain_state=domain.state): - response = self.client.get( - "/admin/registrar/domain/{}/change/".format(domain.id), - ) - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - - # Check that the right help text exists - self.assertContains(response, message) - - @patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1)) - def test_extend_expiration_date_button(self, mock_date_today): - """ - Tests if extend_expiration_date modal gives an accurate date - """ - - # Create a ready domain with a preset expiration date - domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) - response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk])) - # load expiration date into cache and registrar with below command - domain.registry_expiration_date - # Make sure the ex date is what we expect it to be - domain_ex_date = Domain.objects.get(id=domain.id).expiration_date - self.assertEqual(domain_ex_date, date(2023, 5, 25)) - - # Make sure that the page is loading as expected - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - self.assertContains(response, "Extend expiration date") - - # Grab the form to submit - form = response.forms["domain_form"] - - with patch("django.contrib.messages.add_message") as mock_add_message: - # Submit the form - response = form.submit("_extend_expiration_date") - - # Follow the response - response = response.follow() - - # Assert that everything on the page looks correct - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - self.assertContains(response, "Extend expiration date") - - # Ensure the message we recieve is in line with what we expect - expected_message = "Successfully extended the expiration date." - expected_call = call( - # The WGSI request doesn't need to be tested - ANY, - messages.INFO, - expected_message, - extra_tags="", - fail_silently=False, - ) - - mock_add_message.assert_has_calls([expected_call], 1) - - @less_console_noise_decorator - def test_analyst_can_see_inline_domain_information_in_domain_change_form(self): - """Tests if an analyst can still see the inline domain information form""" - - # Create fake creator - _creator = User.objects.create( - username="MrMeoward", - first_name="Meoward", - last_name="Jones", - ) - - # Create a fake domain request - _domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator) - - # Creates a Domain and DomainInformation object - _domain_request.approve() - - domain_information = DomainInformation.objects.filter(domain_request=_domain_request).get() - domain_information.organization_name = "MonkeySeeMonkeyDo" - domain_information.save() - - # We use filter here rather than just domain_information.domain just to get the latest data. - domain = Domain.objects.filter(domain_info=domain_information).get() - - p = "userpass" - self.client.login(username="staffuser", password=p) - response = self.client.get( - "/admin/registrar/domain/{}/change/".format(domain.pk), - follow=True, - ) - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - - # Test for data. We only need to test one since its all interconnected. - expected_organization_name = "MonkeySeeMonkeyDo" - self.assertContains(response, expected_organization_name) - - @less_console_noise_decorator - def test_admin_can_see_inline_domain_information_in_domain_change_form(self): - """Tests if an admin can still see the inline domain information form""" - # Create fake creator - _creator = User.objects.create( - username="MrMeoward", - first_name="Meoward", - last_name="Jones", - ) - - # Create a fake domain request - _domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator) - - # Creates a Domain and DomainInformation object - _domain_request.approve() - - domain_information = DomainInformation.objects.filter(domain_request=_domain_request).get() - domain_information.organization_name = "MonkeySeeMonkeyDo" - domain_information.save() - - # We use filter here rather than just domain_information.domain just to get the latest data. - domain = Domain.objects.filter(domain_info=domain_information).get() - - p = "adminpass" - self.client.login(username="superuser", password=p) - response = self.client.get( - "/admin/registrar/domain/{}/change/".format(domain.pk), - follow=True, - ) - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - - # Test for data. We only need to test one since its all interconnected. - expected_organization_name = "MonkeySeeMonkeyDo" - self.assertContains(response, expected_organization_name) - - @patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1)) - def test_extend_expiration_date_button_epp(self, mock_date_today): - """ - Tests if extend_expiration_date button sends the right epp command - """ - - # Create a ready domain with a preset expiration date - domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) - - response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk])) - - # Make sure that the page is loading as expected - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - self.assertContains(response, "Extend expiration date") - - # Grab the form to submit - form = response.forms["domain_form"] - - with patch("django.contrib.messages.add_message") as mock_add_message: - with patch("registrar.models.Domain.renew_domain") as renew_mock: - # Submit the form - response = form.submit("_extend_expiration_date") - - # Follow the response - response = response.follow() - - # Assert that it is calling the function with the default extension length. - # We only need to test the value that EPP sends, as we can assume the other - # test cases cover the "renew" function. - renew_mock.assert_has_calls([call()], any_order=False) - - # We should not make duplicate calls - self.assertEqual(renew_mock.call_count, 1) - - # Assert that everything on the page looks correct - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - self.assertContains(response, "Extend expiration date") - - # Ensure the message we recieve is in line with what we expect - expected_message = "Successfully extended the expiration date." - expected_call = call( - # The WGSI request doesn't need to be tested - ANY, - messages.INFO, - expected_message, - extra_tags="", - fail_silently=False, - ) - mock_add_message.assert_has_calls([expected_call], 1) - - def test_custom_delete_confirmation_page(self): - """Tests if we override the delete confirmation page for custom content""" - # Create a ready domain with a preset expiration date - domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) - - domain_change_page = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk])) - - self.assertContains(domain_change_page, "fake.gov") - # click the "Manage" link - confirmation_page = domain_change_page.click("Delete", index=0) - - content_slice = "When a domain is deleted:" - self.assertContains(confirmation_page, content_slice) - - def test_custom_delete_confirmation_page_table(self): - """Tests if we override the delete confirmation page for custom content on the table""" - # Create a ready domain - domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) - - # Get the index. The post expects the index to be encoded as a string - index = f"{domain.id}" - - # Simulate selecting a single record, then clicking "Delete selected domains" - response = self.test_helper.get_table_delete_confirmation_page("0", index) - - # Check that our content exists - content_slice = "When a domain is deleted:" - self.assertContains(response, content_slice) - - def test_short_org_name_in_domains_list(self): - """ - Make sure the short name is displaying in admin on the list page - """ - with less_console_noise(): - self.client.force_login(self.superuser) - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - mock_client = MockSESClient() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - domain_request.approve() - - response = self.client.get("/admin/registrar/domain/") - # There are 4 template references to Federal (4) plus four references in the table - # for our actual domain_request - self.assertContains(response, "Federal", count=56) - # This may be a bit more robust - self.assertContains(response, 'Federal', count=1) - # Now let's make sure the long description does not exist - self.assertNotContains(response, "Federal: an agency of the U.S. government") - - @skip("Why did this test stop working, and is is a good test") - def test_place_and_remove_hold(self): - domain = create_ready_domain() - # get admin page and assert Place Hold button - p = "userpass" - self.client.login(username="staffuser", password=p) - response = self.client.get( - "/admin/registrar/domain/{}/change/".format(domain.pk), - follow=True, - ) - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - self.assertContains(response, "Place hold") - self.assertNotContains(response, "Remove hold") - - # submit place_client_hold and assert Remove Hold button - response = self.client.post( - "/admin/registrar/domain/{}/change/".format(domain.pk), - {"_place_client_hold": "Place hold", "name": domain.name}, - follow=True, - ) - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - self.assertContains(response, "Remove hold") - self.assertNotContains(response, "Place hold") - - # submit remove client hold and assert Place hold button - response = self.client.post( - "/admin/registrar/domain/{}/change/".format(domain.pk), - {"_remove_client_hold": "Remove hold", "name": domain.name}, - follow=True, - ) - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - self.assertContains(response, "Place hold") - self.assertNotContains(response, "Remove hold") - - def test_deletion_is_successful(self): - """ - Scenario: Domain deletion is unsuccessful - When the domain is deleted - Then a user-friendly success message is returned for displaying on the web - And `state` is set to `DELETED` - """ - with less_console_noise(): - domain = create_ready_domain() - # Put in client hold - domain.place_client_hold() - p = "userpass" - self.client.login(username="staffuser", password=p) - # Ensure everything is displaying correctly - response = self.client.get( - "/admin/registrar/domain/{}/change/".format(domain.pk), - follow=True, - ) - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - self.assertContains(response, "Remove from registry") - - # The contents of the modal should exist before and after the post. - # Check for the header - self.assertContains(response, "Are you sure you want to remove this domain from the registry?") - - # Check for some of its body - self.assertContains(response, "When a domain is removed from the registry:") - - # Check for some of the button content - self.assertContains(response, "Yes, remove from registry") - - # Test the info dialog - request = self.factory.post( - "/admin/registrar/domain/{}/change/".format(domain.pk), - {"_delete_domain": "Remove from registry", "name": domain.name}, - follow=True, - ) - request.user = self.client - with patch("django.contrib.messages.add_message") as mock_add_message: - self.admin.do_delete_domain(request, domain) - mock_add_message.assert_called_once_with( - request, - messages.INFO, - "Domain city.gov has been deleted. Thanks!", - extra_tags="", - fail_silently=False, - ) - - # The modal should still exist - self.assertContains(response, "Are you sure you want to remove this domain from the registry?") - self.assertContains(response, "When a domain is removed from the registry:") - self.assertContains(response, "Yes, remove from registry") - - self.assertEqual(domain.state, Domain.State.DELETED) - - def test_on_hold_is_successful_web_test(self): - """ - Scenario: Domain on_hold is successful through webtest - """ - with less_console_noise(): - domain = create_ready_domain() - - response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk])) - - # Check the contents of the modal - # Check for the header - self.assertContains(response, "Are you sure you want to place this domain on hold?") - - # Check for some of its body - self.assertContains(response, "When a domain is on hold:") - - # Check for some of the button content - self.assertContains(response, "Yes, place hold") - - # Grab the form to submit - form = response.forms["domain_form"] - - # Submit the form - response = form.submit("_place_client_hold") - - # Follow the response - response = response.follow() - - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - self.assertContains(response, "Remove hold") - - # The modal should still exist - # Check for the header - self.assertContains(response, "Are you sure you want to place this domain on hold?") - - # Check for some of its body - self.assertContains(response, "When a domain is on hold:") - - # Check for some of the button content - self.assertContains(response, "Yes, place hold") - - # Web test has issues grabbing up to date data from the db, so we can test - # the returned view instead - self.assertContains(response, '
On hold
') - - def test_deletion_ready_fsm_failure(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` - """ - with less_console_noise(): - domain = create_ready_domain() - p = "userpass" - self.client.login(username="staffuser", password=p) - # Ensure everything is displaying correctly - response = self.client.get( - "/admin/registrar/domain/{}/change/".format(domain.pk), - follow=True, - ) - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - self.assertContains(response, "Remove from registry") - # Test the error - request = self.factory.post( - "/admin/registrar/domain/{}/change/".format(domain.pk), - {"_delete_domain": "Remove from registry", "name": domain.name}, - follow=True, - ) - request.user = self.client - with patch("django.contrib.messages.add_message") as mock_add_message: - self.admin.do_delete_domain(request, domain) - mock_add_message.assert_called_once_with( - request, - messages.ERROR, - "Error deleting this Domain: " - "Can't switch from state 'ready' to 'deleted'" - ", must be either 'dns_needed' or 'on_hold'", - extra_tags="", - fail_silently=False, - ) - - self.assertEqual(domain.state, Domain.State.READY) - - def test_analyst_deletes_domain_idempotent(self): - """ - Scenario: Analyst tries to delete an already deleted domain - Given `state` is already `DELETED` - When `domain.deletedInEpp()` is called - Then `commands.DeleteDomain` is sent to the registry - And Domain returns normally without an error dialog - """ - with less_console_noise(): - domain = create_ready_domain() - # Put in client hold - domain.place_client_hold() - p = "userpass" - self.client.login(username="staffuser", password=p) - # Ensure everything is displaying correctly - response = self.client.get( - "/admin/registrar/domain/{}/change/".format(domain.pk), - follow=True, - ) - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain.name) - self.assertContains(response, "Remove from registry") - # Test the info dialog - request = self.factory.post( - "/admin/registrar/domain/{}/change/".format(domain.pk), - {"_delete_domain": "Remove from registry", "name": domain.name}, - follow=True, - ) - request.user = self.client - # Delete it once - with patch("django.contrib.messages.add_message") as mock_add_message: - self.admin.do_delete_domain(request, domain) - mock_add_message.assert_called_once_with( - request, - messages.INFO, - "Domain city.gov has been deleted. Thanks!", - extra_tags="", - fail_silently=False, - ) - - self.assertEqual(domain.state, Domain.State.DELETED) - # Try to delete it again - # Test the info dialog - request = self.factory.post( - "/admin/registrar/domain/{}/change/".format(domain.pk), - {"_delete_domain": "Remove from registry", "name": domain.name}, - follow=True, - ) - request.user = self.client - with patch("django.contrib.messages.add_message") as mock_add_message: - self.admin.do_delete_domain(request, domain) - mock_add_message.assert_called_once_with( - request, - messages.INFO, - "This domain is already deleted", - extra_tags="", - fail_silently=False, - ) - self.assertEqual(domain.state, Domain.State.DELETED) - - @skip("Waiting on epp lib to implement") - def test_place_and_remove_hold_epp(self): - raise - - @override_settings(IS_PRODUCTION=True) - def test_prod_only_shows_export(self): - """Test that production environment only displays export""" - with less_console_noise(): - response = self.client.get("/admin/registrar/domain/") - self.assertContains(response, ">Export<") - self.assertNotContains(response, ">Import<") - - def tearDown(self): - super().tearDown() - PublicContact.objects.all().delete() - Host.objects.all().delete() - Domain.objects.all().delete() - DomainInformation.objects.all().delete() - DomainRequest.objects.all().delete() - User.objects.all().delete() - - -class TestDomainRequestAdminForm(TestCase): - def setUp(self): - # Create a test domain request with an initial state of started - self.domain_request = completed_domain_request() - - def test_form_choices(self): - with less_console_noise(): - # Create a form instance with the test domain request - form = DomainRequestAdminForm(instance=self.domain_request) - - # Verify that the form choices match the available transitions for started - expected_choices = [("started", "Started"), ("submitted", "Submitted")] - self.assertEqual(form.fields["status"].widget.choices, expected_choices) - - def test_form_no_rejection_reason(self): - with less_console_noise(): - # Create a form instance with the test domain request - form = DomainRequestAdminForm(instance=self.domain_request) - - form = DomainRequestAdminForm( - instance=self.domain_request, - data={ - "status": DomainRequest.DomainRequestStatus.REJECTED, - "rejection_reason": None, - }, - ) - self.assertFalse(form.is_valid()) - self.assertIn("rejection_reason", form.errors) - - rejection_reason = form.errors.get("rejection_reason") - self.assertEqual(rejection_reason, ["A reason is required for this status."]) - - def test_form_choices_when_no_instance(self): - with less_console_noise(): - # Create a form instance without an instance - form = DomainRequestAdminForm() - - # Verify that the form choices show all choices when no instance is provided; - # this is necessary to show all choices when creating a new domain - # request in django admin; - # note that FSM ensures that no domain request exists with invalid status, - # so don't need to test for invalid status - self.assertEqual( - form.fields["status"].widget.choices, - DomainRequest._meta.get_field("status").choices, - ) - - def test_form_choices_when_ineligible(self): - with less_console_noise(): - # Create a form instance with a domain request with ineligible status - ineligible_domain_request = DomainRequest(status="ineligible") - - # Attempt to create a form with the ineligible domain request - # The form should not raise an error, but choices should be the - # full list of possible choices - form = DomainRequestAdminForm(instance=ineligible_domain_request) - - self.assertEqual( - form.fields["status"].widget.choices, - DomainRequest._meta.get_field("status").choices, - ) - - -@boto3_mocking.patching -class TestDomainRequestAdmin(MockEppLib): - def setUp(self): - super().setUp() - self.site = AdminSite() - self.factory = RequestFactory() - self.admin = DomainRequestAdmin(model=DomainRequest, admin_site=self.site) - self.superuser = create_superuser() - self.staffuser = create_user() - self.client = Client(HTTP_HOST="localhost:8080") - self.test_helper = GenericTestHelper( - factory=self.factory, - user=self.superuser, - admin=self.admin, - url="/admin/registrar/domainrequest/", - model=DomainRequest, - ) - self.mock_client = MockSESClient() - - def test_domain_request_senior_official_is_alphabetically_sorted(self): - """Tests if the senior offical dropdown is alphanetically sorted in the django admin display""" - - SeniorOfficial.objects.get_or_create(first_name="mary", last_name="joe", title="some other guy") - SeniorOfficial.objects.get_or_create(first_name="alex", last_name="smoe", title="some guy") - SeniorOfficial.objects.get_or_create(first_name="Zoup", last_name="Soup", title="title") - - contact, _ = Contact.objects.get_or_create(first_name="Henry", last_name="McFakerson") - domain_request = completed_domain_request(submitter=contact, name="city1.gov") - request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk)) - model_admin = AuditedAdmin(DomainRequest, self.site) - - # Get the queryset that would be returned for the list - senior_offical_queryset = model_admin.formfield_for_foreignkey( - DomainInformation.senior_official.field, request - ).queryset - - # Make the list we're comparing on a bit prettier display-wise. Optional step. - current_sort_order = [] - for official in senior_offical_queryset: - current_sort_order.append(f"{official.first_name} {official.last_name}") - - expected_sort_order = ["alex smoe", "mary joe", "Zoup Soup"] - - self.assertEqual(current_sort_order, expected_sort_order) - - @less_console_noise_decorator - def test_has_model_description(self): - """Tests if this model has a model description on the table view""" - p = "adminpass" - self.client.login(username="superuser", password=p) - response = self.client.get( - "/admin/registrar/domainrequest/", - follow=True, - ) - - # Make sure that the page is loaded correctly - self.assertEqual(response.status_code, 200) - - # Test for a description snippet - self.assertContains(response, "This table contains all domain requests") - self.assertContains(response, "Show more") - - @less_console_noise_decorator - def test_helper_text(self): - """ - Tests for the correct helper text on this page - """ - - # Create a fake domain request and domain - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - - p = "adminpass" - self.client.login(username="superuser", password=p) - response = self.client.get( - "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), - follow=True, - ) - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain_request.requested_domain.name) - - # These should exist in the response - expected_values = [ - ("creator", "Person who submitted the domain request; will not receive email updates"), - ( - "submitter", - 'Person listed under "your contact information" in the request form; will receive email updates', - ), - ("approved_domain", "Domain associated with this request; will be blank until request is approved"), - ("no_other_contacts_rationale", "Required if creator does not list other employees"), - ("alternative_domains", "Other domain names the creator provided for consideration"), - ("no_other_contacts_rationale", "Required if creator does not list other employees"), - ("Urbanization", "Required for Puerto Rico only"), - ] - self.test_helper.assert_response_contains_distinct_values(response, expected_values) - - @less_console_noise_decorator - def test_status_logs(self): - """ - Tests that the status changes are shown in a table on the domain request change form, - accurately and in chronological order. - """ - - def assert_status_count(normalized_content, status, count): - """Helper function to assert the count of a status in the HTML content.""" - self.assertEqual(normalized_content.count(f" {status} "), count) - - def assert_status_order(normalized_content, statuses): - """Helper function to assert the order of statuses in the HTML content.""" - start_index = 0 - for status in statuses: - index = normalized_content.find(f" {status} ", start_index) - self.assertNotEqual(index, -1, f"Status '{status}' not found in the expected order.") - start_index = index + len(status) - - # Create a fake domain request and domain - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.STARTED) - - p = "adminpass" - self.client.login(username="superuser", password=p) - response = self.client.get( - "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), - follow=True, - ) - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain_request.requested_domain.name) - - domain_request.submit() - domain_request.save() - - domain_request.in_review() - domain_request.save() - - domain_request.action_needed() - domain_request.action_needed_reason = DomainRequest.ActionNeededReasons.ALREADY_HAS_DOMAINS - domain_request.save() - - # Let's just change the action needed reason - domain_request.action_needed_reason = DomainRequest.ActionNeededReasons.ELIGIBILITY_UNCLEAR - domain_request.save() - - domain_request.reject() - domain_request.rejection_reason = DomainRequest.RejectionReasons.DOMAIN_PURPOSE - domain_request.save() - - domain_request.in_review() - domain_request.save() - - response = self.client.get( - "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), - follow=True, - ) - - # Normalize the HTML response content - normalized_content = " ".join(response.content.decode("utf-8").split()) - - # Define the expected sequence of status changes - expected_status_changes = [ - "In review", - "Rejected - Purpose requirements not met", - "Action needed - Unclear organization eligibility", - "Action needed - Already has domains", - "In review", - "Submitted", - "Started", - ] - - assert_status_order(normalized_content, expected_status_changes) - - assert_status_count(normalized_content, "Started", 1) - assert_status_count(normalized_content, "Submitted", 1) - assert_status_count(normalized_content, "In review", 2) - assert_status_count(normalized_content, "Action needed - Already has domains", 1) - assert_status_count(normalized_content, "Action needed - Unclear organization eligibility", 1) - assert_status_count(normalized_content, "Rejected - Purpose requirements not met", 1) - - def test_collaspe_toggle_button_markup(self): - """ - Tests for the correct collapse toggle button markup - """ - - # Create a fake domain request and domain - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - - p = "adminpass" - self.client.login(username="superuser", password=p) - response = self.client.get( - "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), - follow=True, - ) - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain_request.requested_domain.name) - self.test_helper.assertContains(response, "Show details") - - @less_console_noise_decorator - def test_analyst_can_see_and_edit_alternative_domain(self): - """Tests if an analyst can still see and edit the alternative domain field""" - - # Create fake creator - _creator = User.objects.create( - username="MrMeoward", - first_name="Meoward", - last_name="Jones", - ) - - # Create a fake domain request - _domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator) - - fake_website = Website.objects.create(website="thisisatest.gov") - _domain_request.alternative_domains.add(fake_website) - _domain_request.save() - - p = "userpass" - self.client.login(username="staffuser", password=p) - response = self.client.get( - "/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk), - follow=True, - ) - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, _domain_request.requested_domain.name) - - # Test if the page has the alternative domain - self.assertContains(response, "thisisatest.gov") - - # Check that the page contains the url we expect - expected_href = reverse("admin:registrar_website_change", args=[fake_website.id]) - self.assertContains(response, expected_href) - - # Navigate to the website to ensure that we can still edit it - response = self.client.get( - "/admin/registrar/website/{}/change/".format(fake_website.pk), - follow=True, - ) - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, "thisisatest.gov") - - @less_console_noise_decorator - def test_analyst_can_see_and_edit_requested_domain(self): - """Tests if an analyst can still see and edit the requested domain field""" - - # Create fake creator - _creator = User.objects.create( - username="MrMeoward", - first_name="Meoward", - last_name="Jones", - ) - - # Create a fake domain request - _domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator) - - p = "userpass" - self.client.login(username="staffuser", password=p) - response = self.client.get( - "/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk), - follow=True, - ) - - # Filter to get the latest from the DB (rather than direct assignment) - requested_domain = DraftDomain.objects.filter(name=_domain_request.requested_domain.name).get() - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, requested_domain.name) - - # Check that the page contains the url we expect - expected_href = reverse("admin:registrar_draftdomain_change", args=[requested_domain.id]) - self.assertContains(response, expected_href) - - # Navigate to the website to ensure that we can still edit it - response = self.client.get( - "/admin/registrar/draftdomain/{}/change/".format(requested_domain.pk), - follow=True, - ) - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, "city.gov") - - @less_console_noise_decorator - def test_analyst_can_see_current_websites(self): - """Tests if an analyst can still see current website field""" - - # Create fake creator - _creator = User.objects.create( - username="MrMeoward", - first_name="Meoward", - last_name="Jones", - ) - - # Create a fake domain request - _domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator) - - fake_website = Website.objects.create(website="thisisatest.gov") - _domain_request.current_websites.add(fake_website) - _domain_request.save() - - p = "userpass" - self.client.login(username="staffuser", password=p) - response = self.client.get( - "/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk), - follow=True, - ) - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, _domain_request.requested_domain.name) - - # Test if the page has the current website - self.assertContains(response, "thisisatest.gov") - - def test_domain_sortable(self): - """Tests if the DomainRequest sorts by domain correctly""" - with less_console_noise(): - p = "adminpass" - self.client.login(username="superuser", password=p) - - multiple_unalphabetical_domain_objects("domain_request") - - # Assert that our sort works correctly - self.test_helper.assert_table_sorted("1", ("requested_domain__name",)) - - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted("-1", ("-requested_domain__name",)) - - def test_submitter_sortable(self): - """Tests if the DomainRequest sorts by submitter correctly""" - with less_console_noise(): - p = "adminpass" - self.client.login(username="superuser", password=p) - - multiple_unalphabetical_domain_objects("domain_request") - - additional_domain_request = generic_domain_object("domain_request", "Xylophone") - new_user = User.objects.filter(username=additional_domain_request.investigator.username).get() - new_user.first_name = "Xylophonic" - new_user.save() - - # Assert that our sort works correctly - self.test_helper.assert_table_sorted( - "11", - ( - "submitter__first_name", - "submitter__last_name", - ), - ) - - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted( - "-11", - ( - "-submitter__first_name", - "-submitter__last_name", - ), - ) - - def test_investigator_sortable(self): - """Tests if the DomainRequest sorts by investigator correctly""" - with less_console_noise(): - p = "adminpass" - self.client.login(username="superuser", password=p) - - multiple_unalphabetical_domain_objects("domain_request") - additional_domain_request = generic_domain_object("domain_request", "Xylophone") - new_user = User.objects.filter(username=additional_domain_request.investigator.username).get() - new_user.first_name = "Xylophonic" - new_user.save() - - # Assert that our sort works correctly - self.test_helper.assert_table_sorted( - "12", - ( - "investigator__first_name", - "investigator__last_name", - ), - ) - - # Assert that sorting in reverse works correctly - self.test_helper.assert_table_sorted( - "-12", - ( - "-investigator__first_name", - "-investigator__last_name", - ), - ) - - @less_console_noise_decorator - def test_default_sorting_in_domain_requests_list(self): - """ - Make sure the default sortin in on the domain requests list page is reverse submission_date - then alphabetical requested_domain - """ - - # Create domain requests with different names - domain_requests = [ - completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, name=name) - for name in ["ccc.gov", "bbb.gov", "eee.gov", "aaa.gov", "zzz.gov", "ddd.gov"] - ] - - domain_requests[0].submission_date = timezone.make_aware(datetime(2024, 10, 16)) - domain_requests[1].submission_date = timezone.make_aware(datetime(2001, 10, 16)) - domain_requests[2].submission_date = timezone.make_aware(datetime(1980, 10, 16)) - domain_requests[3].submission_date = timezone.make_aware(datetime(1998, 10, 16)) - domain_requests[4].submission_date = timezone.make_aware(datetime(2013, 10, 16)) - domain_requests[5].submission_date = timezone.make_aware(datetime(1980, 10, 16)) - - # Save the modified domain requests to update their attributes in the database - for domain_request in domain_requests: - domain_request.save() - - # Refresh domain request objects from the database to reflect the changes - domain_requests = [DomainRequest.objects.get(pk=domain_request.pk) for domain_request in domain_requests] - - # Login as superuser and retrieve the domain request list page - self.client.force_login(self.superuser) - response = self.client.get("/admin/registrar/domainrequest/") - - # Check that the response is successful - self.assertEqual(response.status_code, 200) - - # Extract the domain names from the response content using regex - domain_names_match = re.findall(r"(\w+\.gov)", response.content.decode("utf-8")) - - logger.info(f"domain_names_match {domain_names_match}") - - # Verify that domain names are found - self.assertTrue(domain_names_match) - - # Extract the domain names - domain_names = [match for match in domain_names_match] - - # Verify that the domain names are displayed in the expected order - expected_order = [ - "ccc.gov", - "zzz.gov", - "bbb.gov", - "aaa.gov", - "ddd.gov", - "eee.gov", - ] - - # Remove duplicates - # Remove duplicates from domain_names list while preserving order - unique_domain_names = [] - for domain_name in domain_names: - if domain_name not in unique_domain_names: - unique_domain_names.append(domain_name) - - self.assertEqual(unique_domain_names, expected_order) - - def test_short_org_name_in_domain_requests_list(self): - """ - Make sure the short name is displaying in admin on the list page - """ - with less_console_noise(): - self.client.force_login(self.superuser) - completed_domain_request() - response = self.client.get("/admin/registrar/domainrequest/?generic_org_type__exact=federal") - # There are 2 template references to Federal (4) and two in the results data - # of the request - self.assertContains(response, "Federal", count=52) - # This may be a bit more robust - self.assertContains(response, 'Federal', count=1) - # Now let's make sure the long description does not exist - self.assertNotContains(response, "Federal: an agency of the U.S. government") - - def test_default_status_in_domain_requests_list(self): - """ - Make sure the default status in admin is selected on the domain requests list page - """ - with less_console_noise(): - self.client.force_login(self.superuser) - completed_domain_request() - response = self.client.get("/admin/registrar/domainrequest/") - # The results are filtered by "status in [submitted,in review,action needed]" - self.assertContains(response, "status in [submitted,in review,action needed]", count=1) - - @less_console_noise_decorator - def transition_state_and_send_email( - self, domain_request, status, rejection_reason=None, action_needed_reason=None, action_needed_reason_email=None - ): - """Helper method for the email test cases.""" - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - # Create a mock request - request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk)) - - # Create a fake session to hook to - request.session = {} - - # Modify the domain request's properties - domain_request.status = status - - if rejection_reason: - domain_request.rejection_reason = rejection_reason - - if action_needed_reason: - domain_request.action_needed_reason = action_needed_reason - - if action_needed_reason_email: - domain_request.action_needed_reason_email = action_needed_reason_email - - # Use the model admin's save_model method - self.admin.save_model(request, domain_request, form=None, change=True) - - def assert_email_is_accurate( - self, expected_string, email_index, email_address, test_that_no_bcc=False, bcc_email_address="" - ): - """Helper method for the email test cases. - email_index is the index of the email in mock_client.""" - - with less_console_noise(): - # Access the arguments passed to send_email - call_args = self.mock_client.EMAILS_SENT - kwargs = call_args[email_index]["kwargs"] - - # Retrieve the email details from the arguments - from_email = kwargs.get("FromEmailAddress") - to_email = kwargs["Destination"]["ToAddresses"][0] - email_content = kwargs["Content"] - email_body = email_content["Simple"]["Body"]["Text"]["Data"] - - # Assert or perform other checks on the email details - self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL) - self.assertEqual(to_email, email_address) - self.assertIn(expected_string, email_body) - - if test_that_no_bcc: - _ = "" - with self.assertRaises(KeyError): - with less_console_noise(): - _ = kwargs["Destination"]["BccAddresses"][0] - self.assertEqual(_, "") - - if bcc_email_address: - bcc_email = kwargs["Destination"]["BccAddresses"][0] - self.assertEqual(bcc_email, bcc_email_address) - - @override_settings(IS_PRODUCTION=True) - def test_action_needed_sends_reason_email_prod_bcc(self): - """When an action needed reason is set, an email is sent out and help@get.gov - is BCC'd in production""" - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - BCC_EMAIL = settings.DEFAULT_FROM_EMAIL - User.objects.filter(email=EMAIL).delete() - in_review = DomainRequest.DomainRequestStatus.IN_REVIEW - action_needed = DomainRequest.DomainRequestStatus.ACTION_NEEDED - - # Create a sample domain request - domain_request = completed_domain_request(status=in_review) - - # Test the email sent out for already_has_domains - already_has_domains = DomainRequest.ActionNeededReasons.ALREADY_HAS_DOMAINS - self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=already_has_domains) - - self.assert_email_is_accurate("ORGANIZATION ALREADY HAS A .GOV DOMAIN", 0, EMAIL, bcc_email_address=BCC_EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - - # Test the email sent out for bad_name - bad_name = DomainRequest.ActionNeededReasons.BAD_NAME - self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=bad_name) - self.assert_email_is_accurate( - "DOMAIN NAME DOES NOT MEET .GOV REQUIREMENTS", 1, EMAIL, bcc_email_address=BCC_EMAIL - ) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - - # Test the email sent out for eligibility_unclear - eligibility_unclear = DomainRequest.ActionNeededReasons.ELIGIBILITY_UNCLEAR - self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=eligibility_unclear) - self.assert_email_is_accurate( - "ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS", 2, EMAIL, bcc_email_address=BCC_EMAIL - ) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - - # Test that a custom email is sent out for questionable_so - questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL - self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=questionable_so) - self.assert_email_is_accurate( - "SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", 3, EMAIL, bcc_email_address=BCC_EMAIL - ) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 4) - - # Assert that no other emails are sent on OTHER - other = DomainRequest.ActionNeededReasons.OTHER - self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=other) - - # Should be unchanged from before - self.assertEqual(len(self.mock_client.EMAILS_SENT), 4) - - # Tests if an analyst can override existing email content - questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL - self.transition_state_and_send_email( - domain_request, - action_needed, - action_needed_reason=questionable_so, - action_needed_reason_email="custom email content", - ) - - domain_request.refresh_from_db() - self.assert_email_is_accurate("custom email content", 4, EMAIL, bcc_email_address=BCC_EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 5) - - # Tests if a new email gets sent when just the email is changed. - # An email should NOT be sent out if we just modify the email content. - self.transition_state_and_send_email( - domain_request, - action_needed, - action_needed_reason=questionable_so, - action_needed_reason_email="dummy email content", - ) - - self.assertEqual(len(self.mock_client.EMAILS_SENT), 5) - - # Set the request back to in review - domain_request.in_review() - - # Try sending another email when changing states AND including content - self.transition_state_and_send_email( - domain_request, - action_needed, - action_needed_reason=eligibility_unclear, - action_needed_reason_email="custom content when starting anew", - ) - self.assert_email_is_accurate("custom content when starting anew", 5, EMAIL, bcc_email_address=BCC_EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 6) - - def test_save_model_sends_submitted_email(self): - """When transitioning to submitted from started or withdrawn on a domain request, - an email is sent out. - - When transitioning to submitted from dns needed or in review on a domain request, - no email is sent out. - - Also test that the default email set in settings is NOT BCCd on non-prod whenever - an email does go out.""" - - with less_console_noise(): - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - - # Create a sample domain request - domain_request = completed_domain_request() - - # Test Submitted Status from started - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED) - self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, True) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - - # Test Withdrawn Status - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN) - self.assert_email_is_accurate( - "Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL, True - ) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - - # Test Submitted Status Again (from withdrawn) - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - - # Move it to IN_REVIEW - other = DomainRequest.ActionNeededReasons.OTHER - in_review = DomainRequest.DomainRequestStatus.IN_REVIEW - self.transition_state_and_send_email(domain_request, in_review, action_needed_reason=other) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - - # Test Submitted Status Again from in IN_REVIEW, no new email should be sent - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - - # Move it to IN_REVIEW - self.transition_state_and_send_email(domain_request, in_review, action_needed_reason=other) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - - # Move it to ACTION_NEEDED - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.ACTION_NEEDED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - - # Test Submitted Status Again from in ACTION_NEEDED, no new email should be sent - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - - @less_console_noise_decorator - def test_model_displays_action_needed_email(self): - """Tests if the action needed email is visible for Domain Requests""" - - _domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.ACTION_NEEDED, - action_needed_reason=DomainRequest.ActionNeededReasons.BAD_NAME, - ) - - p = "userpass" - self.client.login(username="staffuser", password=p) - response = self.client.get( - "/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk), - follow=True, - ) - - self.assertContains(response, "DOMAIN NAME DOES NOT MEET .GOV REQUIREMENTS") - - @override_settings(IS_PRODUCTION=True) - def test_save_model_sends_submitted_email_with_bcc_on_prod(self): - """When transitioning to submitted from started or withdrawn on a domain request, - an email is sent out. - - When transitioning to submitted from dns needed or in review on a domain request, - no email is sent out. - - Also test that the default email set in settings IS BCCd on prod whenever - an email does go out.""" - - with less_console_noise(): - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - - BCC_EMAIL = settings.DEFAULT_FROM_EMAIL - - # Create a sample domain request - domain_request = completed_domain_request() - - # Test Submitted Status from started - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED) - self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, False, BCC_EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - - # Test Withdrawn Status - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN) - self.assert_email_is_accurate( - "Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL - ) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - - # Test Submitted Status Again (from withdrawn) - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED) - self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, False, BCC_EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - - # Move it to IN_REVIEW - other = domain_request.ActionNeededReasons.OTHER - in_review = DomainRequest.DomainRequestStatus.IN_REVIEW - self.transition_state_and_send_email(domain_request, in_review, action_needed_reason=other) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - - # Test Submitted Status Again from in IN_REVIEW, no new email should be sent - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - - # Move it to IN_REVIEW - self.transition_state_and_send_email(domain_request, in_review, action_needed_reason=other) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - - # Move it to ACTION_NEEDED - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.ACTION_NEEDED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - - # Test Submitted Status Again from in ACTION_NEEDED, no new email should be sent - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - - def test_save_model_sends_approved_email(self): - """When transitioning to approved on a domain request, - an email is sent out every time.""" - - with less_console_noise(): - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - - # Create a sample domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - - # Test Submitted Status - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED) - self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 0, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - - # Test Withdrawn Status - self.transition_state_and_send_email( - domain_request, - DomainRequest.DomainRequestStatus.REJECTED, - DomainRequest.RejectionReasons.DOMAIN_PURPOSE, - ) - self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - - # Test Submitted Status Again (No new email should be sent) - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - - def test_save_model_sends_rejected_email_purpose_not_met(self): - """When transitioning to rejected on a domain request, an email is sent - explaining why when the reason is domain purpose.""" - - with less_console_noise(): - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - - # Create a sample domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - - # Reject for reason DOMAIN_PURPOSE and test email - self.transition_state_and_send_email( - domain_request, - DomainRequest.DomainRequestStatus.REJECTED, - DomainRequest.RejectionReasons.DOMAIN_PURPOSE, - ) - self.assert_email_is_accurate( - "Your domain request was rejected because the purpose you provided did not meet our \nrequirements.", - 0, - EMAIL, - ) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - - # Approve - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED) - self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - - def test_save_model_sends_rejected_email_requestor(self): - """When transitioning to rejected on a domain request, an email is sent - explaining why when the reason is requestor.""" - - with less_console_noise(): - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - - # Create a sample domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - - # Reject for reason REQUESTOR and test email including dynamic organization name - self.transition_state_and_send_email( - domain_request, DomainRequest.DomainRequestStatus.REJECTED, DomainRequest.RejectionReasons.REQUESTOR - ) - self.assert_email_is_accurate( - "Your domain request was rejected because we don’t believe you’re eligible to request a \n.gov " - "domain on behalf of Testorg", - 0, - EMAIL, - ) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - - # Approve - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED) - self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - - def test_save_model_sends_rejected_email_org_has_domain(self): - """When transitioning to rejected on a domain request, an email is sent - explaining why when the reason is second domain.""" - - with less_console_noise(): - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - - # Create a sample domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - - # Reject for reason SECOND_DOMAIN_REASONING and test email including dynamic organization name - self.transition_state_and_send_email( - domain_request, - DomainRequest.DomainRequestStatus.REJECTED, - DomainRequest.RejectionReasons.SECOND_DOMAIN_REASONING, - ) - self.assert_email_is_accurate( - "Your domain request was rejected because Testorg has a .gov domain.", 0, EMAIL - ) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - - # Approve - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED) - self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - - def test_save_model_sends_rejected_email_contacts_or_org_legitimacy(self): - """When transitioning to rejected on a domain request, an email is sent - explaining why when the reason is contacts or org legitimacy.""" - - with less_console_noise(): - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - - # Create a sample domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - - # Reject for reason CONTACTS_OR_ORGANIZATION_LEGITIMACY and test email including dynamic organization name - self.transition_state_and_send_email( - domain_request, - DomainRequest.DomainRequestStatus.REJECTED, - DomainRequest.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY, - ) - self.assert_email_is_accurate( - "Your domain request was rejected because we could not verify the organizational \n" - "contacts you provided. If you have questions or comments, reply to this email.", - 0, - EMAIL, - ) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - - # Approve - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED) - self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - - def test_save_model_sends_rejected_email_org_eligibility(self): - """When transitioning to rejected on a domain request, an email is sent - explaining why when the reason is org eligibility.""" - - with less_console_noise(): - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - - # Create a sample domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - - # Reject for reason ORGANIZATION_ELIGIBILITY and test email including dynamic organization name - self.transition_state_and_send_email( - domain_request, - DomainRequest.DomainRequestStatus.REJECTED, - DomainRequest.RejectionReasons.ORGANIZATION_ELIGIBILITY, - ) - self.assert_email_is_accurate( - "Your domain request was rejected because we determined that Testorg is not \neligible for " - "a .gov domain.", - 0, - EMAIL, - ) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - - # Approve - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED) - self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - - def test_save_model_sends_rejected_email_naming(self): - """When transitioning to rejected on a domain request, an email is sent - explaining why when the reason is naming.""" - - with less_console_noise(): - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - - # Create a sample domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - - # Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name - self.transition_state_and_send_email( - domain_request, - DomainRequest.DomainRequestStatus.REJECTED, - DomainRequest.RejectionReasons.NAMING_REQUIREMENTS, - ) - self.assert_email_is_accurate( - "Your domain request was rejected because it does not meet our naming requirements.", 0, EMAIL - ) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - - # Approve - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED) - self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - - def test_save_model_sends_rejected_email_other(self): - """When transitioning to rejected on a domain request, an email is sent - explaining why when the reason is other.""" - - with less_console_noise(): - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - - # Create a sample domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - - # Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name - self.transition_state_and_send_email( - domain_request, - DomainRequest.DomainRequestStatus.REJECTED, - DomainRequest.RejectionReasons.OTHER, - ) - self.assert_email_is_accurate("Choosing a .gov domain name", 0, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - - # Approve - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED) - self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - - def test_transition_to_rejected_without_rejection_reason_does_trigger_error(self): - """ - When transitioning to rejected without a rejection reason, admin throws a user friendly message. - - The transition fails. - """ - - with less_console_noise(): - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.APPROVED) - - # Create a request object with a superuser - request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk)) - request.user = self.superuser - - with ExitStack() as stack: - stack.enter_context(patch.object(messages, "error")) - domain_request.status = DomainRequest.DomainRequestStatus.REJECTED - - self.admin.save_model(request, domain_request, None, True) - - messages.error.assert_called_once_with( - request, - "A reason is required for this status.", - ) - - domain_request.refresh_from_db() - self.assertEqual(domain_request.status, DomainRequest.DomainRequestStatus.APPROVED) - - def test_transition_to_rejected_with_rejection_reason_does_not_trigger_error(self): - """ - When transitioning to rejected with a rejection reason, admin does not throw an error alert. - - The transition is successful. - """ - - with less_console_noise(): - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.APPROVED) - - # Create a request object with a superuser - request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk)) - request.user = self.superuser - - with ExitStack() as stack: - stack.enter_context(patch.object(messages, "error")) - domain_request.status = DomainRequest.DomainRequestStatus.REJECTED - domain_request.rejection_reason = DomainRequest.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY - - self.admin.save_model(request, domain_request, None, True) - - messages.error.assert_not_called() - - domain_request.refresh_from_db() - self.assertEqual(domain_request.status, DomainRequest.DomainRequestStatus.REJECTED) - - def test_save_model_sends_withdrawn_email(self): - """When transitioning to withdrawn on a domain request, - an email is sent out every time.""" - - with less_console_noise(): - # Ensure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - - # Create a sample domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - - # Test Submitted Status - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN) - self.assert_email_is_accurate( - "Your .gov domain request has been withdrawn and will not be reviewed by our team.", 0, EMAIL - ) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) - - # Test Withdrawn Status - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED) - self.assert_email_is_accurate("We received your .gov domain request.", 1, EMAIL) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) - - # Test Submitted Status Again (No new email should be sent) - self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN) - self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) - - def test_save_model_sets_approved_domain(self): - with less_console_noise(): - # make sure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - - # Create a sample domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - - # Create a mock request - request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk)) - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - # Modify the domain request's property - domain_request.status = DomainRequest.DomainRequestStatus.APPROVED - - # Use the model admin's save_model method - self.admin.save_model(request, domain_request, form=None, change=True) - - # Test that approved domain exists and equals requested domain - self.assertEqual(domain_request.requested_domain.name, domain_request.approved_domain.name) - - @less_console_noise_decorator - def test_sticky_submit_row(self): - """Test that the change_form template contains strings indicative of the customization - of the sticky submit bar. - - Also test that it does NOT contain a CSS class meant for analysts only when logged in as superuser.""" - - # make sure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - self.client.force_login(self.superuser) - - # Create a sample domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - - # Create a mock request - request = self.client.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk)) - - # Since we're using client to mock the request, we can only test against - # non-interpolated values - expected_content = "Requested domain:" - expected_content2 = '' - expected_content3 = '
' - not_expected_content = "submit-row-wrapper--analyst-view>" - self.assertContains(request, expected_content) - self.assertContains(request, expected_content2) - self.assertContains(request, expected_content3) - self.assertNotContains(request, not_expected_content) - - @less_console_noise_decorator - def test_sticky_submit_row_has_extra_class_for_analysts(self): - """Test that the change_form template contains strings indicative of the customization - of the sticky submit bar. - - Also test that it DOES contain a CSS class meant for analysts only when logged in as analyst.""" - - # make sure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - self.client.force_login(self.staffuser) - - # Create a sample domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - - # Create a mock request - request = self.client.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk)) - - # Since we're using client to mock the request, we can only test against - # non-interpolated values - expected_content = "Requested domain:" - expected_content2 = '' - expected_content3 = '
' - self.assertContains(request, expected_content) - self.assertContains(request, expected_content2) - self.assertContains(request, expected_content3) - - def test_other_contacts_has_readonly_link(self): - """Tests if the readonly other_contacts field has links""" - - # Create a fake domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - - # Get the other contact - other_contact = domain_request.other_contacts.all().first() - - p = "userpass" - self.client.login(username="staffuser", password=p) - response = self.client.get( - "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), - follow=True, - ) - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain_request.requested_domain.name) - - # Check that the page contains the url we expect - expected_href = reverse("admin:registrar_contact_change", args=[other_contact.id]) - self.assertContains(response, expected_href) - - # Check that the page contains the link we expect. - # Since the url is dynamic (populated by JS), we can test for its existence - # by checking for the end tag. - expected_url = "Testy Tester" - self.assertContains(response, expected_url) - - @less_console_noise_decorator - def test_other_websites_has_readonly_link(self): - """Tests if the readonly other_websites field has links""" - - # Create a fake domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - - p = "userpass" - self.client.login(username="staffuser", password=p) - response = self.client.get( - "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), - follow=True, - ) - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain_request.requested_domain.name) - - # Check that the page contains the link we expect. - expected_url = 'city.com' - self.assertContains(response, expected_url) - - @less_console_noise_decorator - def test_contact_fields_have_detail_table(self): - """Tests if the contact fields have the detail table which displays title, email, and phone""" - - # Create fake creator - _creator = User.objects.create( - username="MrMeoward", - first_name="Meoward", - last_name="Jones", - email="meoward.jones@igorville.gov", - phone="(555) 123 12345", - title="Treat inspector", - ) - - # Create a fake domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator) - - p = "userpass" - self.client.login(username="staffuser", password=p) - response = self.client.get( - "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), - follow=True, - ) - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain_request.requested_domain.name) - - # == Check for the creator == # - - # Check for the right title and phone number in the response. - # Email will appear more than once - expected_creator_fields = [ - # Field, expected value - ("title", "Treat inspector"), - ("phone", "(555) 123 12345"), - ] - self.test_helper.assert_response_contains_distinct_values(response, expected_creator_fields) - - # Check for the field itself - self.assertContains(response, "Meoward Jones") - - # == Check for the submitter == # - self.assertContains(response, "mayor@igorville.gov", count=2) - expected_submitter_fields = [ - # Field, expected value - ("title", "Admin Tester"), - ("phone", "(555) 555 5556"), - ] - self.test_helper.assert_response_contains_distinct_values(response, expected_submitter_fields) - self.assertContains(response, "Testy2 Tester2") - self.assertContains(response, "meoward.jones@igorville.gov") - - # == Check for the senior_official == # - self.assertContains(response, "testy@town.com", count=2) - expected_so_fields = [ - # Field, expected value - ("phone", "(555) 555 5555"), - ] - - self.test_helper.assert_response_contains_distinct_values(response, expected_so_fields) - self.assertContains(response, "Chief Tester") - - # == Test the other_employees field == # - self.assertContains(response, "testy2@town.com") - expected_other_employees_fields = [ - # Field, expected value - ("title", "Another Tester"), - ("phone", "(555) 555 5557"), - ] - self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields) - - # Test for the copy link - self.assertContains(response, "usa-button__clipboard", count=4) - - # Test that Creator counts display properly - self.assertNotContains(response, "Approved domains") - self.assertContains(response, "Active requests") - - def test_save_model_sets_restricted_status_on_user(self): - with less_console_noise(): - # make sure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - - # Create a sample domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - - # Create a mock request - request = self.factory.post( - "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), follow=True - ) - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - # Modify the domain request's property - domain_request.status = DomainRequest.DomainRequestStatus.INELIGIBLE - - # Use the model admin's save_model method - self.admin.save_model(request, domain_request, form=None, change=True) - - # Test that approved domain exists and equals requested domain - self.assertEqual(domain_request.creator.status, "restricted") - - def test_user_sets_restricted_status_modal(self): - """Tests the modal for when a user sets the status to restricted""" - with less_console_noise(): - # make sure there is no user with this email - EMAIL = "mayor@igorville.gov" - User.objects.filter(email=EMAIL).delete() - - # Create a sample domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - - p = "userpass" - self.client.login(username="staffuser", password=p) - response = self.client.get( - "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), - follow=True, - ) - - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain_request.requested_domain.name) - - # Check that the modal has the right content - # Check for the header - self.assertContains(response, "Are you sure you want to select ineligible status?") - - # Check for some of its body - self.assertContains(response, "When a domain request is in ineligible status") - - # Check for some of the button content - self.assertContains(response, "Yes, select ineligible status") - - # Create a mock request - request = self.factory.post( - "/admin/registrar/domainrequest{}/change/".format(domain_request.pk), follow=True - ) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - # Modify the domain request's property - domain_request.status = DomainRequest.DomainRequestStatus.INELIGIBLE - - # Use the model admin's save_model method - self.admin.save_model(request, domain_request, form=None, change=True) - - # Test that approved domain exists and equals requested domain - self.assertEqual(domain_request.creator.status, "restricted") - - # 'Get' to the domain request again - response = self.client.get( - "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), - follow=True, - ) - - self.assertEqual(response.status_code, 200) - self.assertContains(response, domain_request.requested_domain.name) - - # The modal should be unchanged - self.assertContains(response, "Are you sure you want to select ineligible status?") - self.assertContains(response, "When a domain request is in ineligible status") - self.assertContains(response, "Yes, select ineligible status") - - @less_console_noise_decorator - def test_readonly_when_restricted_creator(self): - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - domain_request.creator.status = User.RESTRICTED - domain_request.creator.save() - - request = self.factory.get("/") - request.user = self.superuser - - readonly_fields = self.admin.get_readonly_fields(request, domain_request) - - expected_fields = [ - "other_contacts", - "current_websites", - "alternative_domains", - "is_election_board", - "status_history", - "id", - "created_at", - "updated_at", - "status", - "rejection_reason", - "action_needed_reason", - "action_needed_reason_email", - "federal_agency", - "portfolio", - "sub_organization", - "creator", - "investigator", - "generic_org_type", - "is_election_board", - "organization_type", - "federally_recognized_tribe", - "state_recognized_tribe", - "tribe_name", - "federal_type", - "organization_name", - "address_line1", - "address_line2", - "city", - "state_territory", - "zipcode", - "urbanization", - "about_your_organization", - "senior_official", - "approved_domain", - "requested_domain", - "submitter", - "purpose", - "no_other_contacts_rationale", - "anything_else", - "has_anything_else_text", - "cisa_representative_email", - "cisa_representative_first_name", - "cisa_representative_last_name", - "has_cisa_representative", - "is_policy_acknowledged", - "submission_date", - "notes", - "alternative_domains", - ] - - self.assertEqual(readonly_fields, expected_fields) - - def test_readonly_fields_for_analyst(self): - with less_console_noise(): - request = self.factory.get("/") # Use the correct method and path - request.user = self.staffuser - - readonly_fields = self.admin.get_readonly_fields(request) - - expected_fields = [ - "other_contacts", - "current_websites", - "alternative_domains", - "is_election_board", - "status_history", - "federal_agency", - "creator", - "about_your_organization", - "requested_domain", - "approved_domain", - "alternative_domains", - "purpose", - "submitter", - "no_other_contacts_rationale", - "anything_else", - "is_policy_acknowledged", - "cisa_representative_first_name", - "cisa_representative_last_name", - "cisa_representative_email", - ] - - self.assertEqual(readonly_fields, expected_fields) - - def test_readonly_fields_for_superuser(self): - with less_console_noise(): - request = self.factory.get("/") # Use the correct method and path - request.user = self.superuser - - readonly_fields = self.admin.get_readonly_fields(request) - - expected_fields = [ - "other_contacts", - "current_websites", - "alternative_domains", - "is_election_board", - "status_history", - ] - - self.assertEqual(readonly_fields, expected_fields) - - def test_saving_when_restricted_creator(self): - with less_console_noise(): - # Create an instance of the model - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - domain_request.creator.status = User.RESTRICTED - domain_request.creator.save() - - # Create a request object with a superuser - request = self.factory.get("/") - request.user = self.superuser - - with patch("django.contrib.messages.error") as mock_error: - # Simulate saving the model - self.admin.save_model(request, domain_request, None, False) - - # Assert that the error message was called with the correct argument - mock_error.assert_called_once_with( - request, - "This action is not permitted for domain requests with a restricted creator.", - ) - - # Assert that the status has not changed - self.assertEqual(domain_request.status, DomainRequest.DomainRequestStatus.IN_REVIEW) - - def test_change_view_with_restricted_creator(self): - with less_console_noise(): - # Create an instance of the model - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - domain_request.creator.status = User.RESTRICTED - domain_request.creator.save() - - with patch("django.contrib.messages.warning") as mock_warning: - # Create a request object with a superuser - request = self.factory.get("/admin/your_app/domainrequest/{}/change/".format(domain_request.pk)) - request.user = self.superuser - - self.admin.display_restricted_warning(request, domain_request) - - # Assert that the error message was called with the correct argument - mock_warning.assert_called_once_with( - request, - "Cannot edit a domain request with a restricted creator.", - ) - - def trigger_saving_approved_to_another_state(self, domain_is_active, another_state, rejection_reason=None): - """Helper method that triggers domain request state changes from approved to another state, - with an associated domain that can be either active (READY) or not. - - Used to test errors when saving a change with an active domain, also used to test side effects - when saving a change goes through.""" - - with less_console_noise(): - # Create an instance of the model - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.APPROVED) - domain = Domain.objects.create(name=domain_request.requested_domain.name) - domain_information = DomainInformation.objects.create(creator=self.superuser, domain=domain) - domain_request.approved_domain = domain - domain_request.save() - - # Create a request object with a superuser - request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk)) - request.user = self.superuser - - request.session = {} - - # Define a custom implementation for is_active - def custom_is_active(self): - return domain_is_active # Override to return True - - # Use ExitStack to combine patch contexts - with ExitStack() as stack: - # Patch Domain.is_active and django.contrib.messages.error simultaneously - stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) - stack.enter_context(patch.object(messages, "error")) - - domain_request.status = another_state - - if another_state == DomainRequest.DomainRequestStatus.ACTION_NEEDED: - domain_request.action_needed_reason = domain_request.ActionNeededReasons.OTHER - - domain_request.rejection_reason = rejection_reason - - self.admin.save_model(request, domain_request, None, True) - - # Assert that the error message was called with the correct argument - if domain_is_active: - messages.error.assert_called_once_with( - request, - "This action is not permitted. The domain " + "is already active.", - ) - else: - # Assert that the error message was never called - messages.error.assert_not_called() - - self.assertEqual(domain_request.approved_domain, None) - - # Assert that Domain got Deleted - with self.assertRaises(Domain.DoesNotExist): - domain.refresh_from_db() - - # Assert that DomainInformation got Deleted - with self.assertRaises(DomainInformation.DoesNotExist): - domain_information.refresh_from_db() - - def test_error_when_saving_approved_to_in_review_and_domain_is_active(self): - self.trigger_saving_approved_to_another_state(True, DomainRequest.DomainRequestStatus.IN_REVIEW) - - def test_error_when_saving_approved_to_action_needed_and_domain_is_active(self): - self.trigger_saving_approved_to_another_state(True, DomainRequest.DomainRequestStatus.ACTION_NEEDED) - - def test_error_when_saving_approved_to_rejected_and_domain_is_active(self): - self.trigger_saving_approved_to_another_state(True, DomainRequest.DomainRequestStatus.REJECTED) - - def test_error_when_saving_approved_to_ineligible_and_domain_is_active(self): - self.trigger_saving_approved_to_another_state(True, DomainRequest.DomainRequestStatus.INELIGIBLE) - - def test_side_effects_when_saving_approved_to_in_review(self): - self.trigger_saving_approved_to_another_state(False, DomainRequest.DomainRequestStatus.IN_REVIEW) - - def test_side_effects_when_saving_approved_to_action_needed(self): - self.trigger_saving_approved_to_another_state(False, DomainRequest.DomainRequestStatus.ACTION_NEEDED) - - def test_side_effects_when_saving_approved_to_rejected(self): - self.trigger_saving_approved_to_another_state( - False, - DomainRequest.DomainRequestStatus.REJECTED, - DomainRequest.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY, - ) - - def test_side_effects_when_saving_approved_to_ineligible(self): - self.trigger_saving_approved_to_another_state(False, DomainRequest.DomainRequestStatus.INELIGIBLE) - - def test_has_correct_filters(self): - """ - This test verifies that DomainRequestAdmin has the correct filters set up. - - It retrieves the current list of filters from DomainRequestAdmin - and checks that it matches the expected list of filters. - """ - with less_console_noise(): - request = self.factory.get("/") - request.user = self.superuser - - # Grab the current list of table filters - readonly_fields = self.admin.get_list_filter(request) - expected_fields = ( - DomainRequestAdmin.StatusListFilter, - "generic_org_type", - "federal_type", - DomainRequestAdmin.ElectionOfficeFilter, - "rejection_reason", - DomainRequestAdmin.InvestigatorFilter, - ) - - self.assertEqual(readonly_fields, expected_fields) - - def test_table_sorted_alphabetically(self): - """ - This test verifies that the DomainRequestAdmin table is sorted alphabetically - by the 'requested_domain__name' field. - - It creates a list of DomainRequest instances in a non-alphabetical order, - then retrieves the queryset from the DomainRequestAdmin and checks - that it matches the expected queryset, - which is sorted alphabetically by the 'requested_domain__name' field. - """ - with less_console_noise(): - # Creates a list of DomainRequests in scrambled order - multiple_unalphabetical_domain_objects("domain_request") - - request = self.factory.get("/") - request.user = self.superuser - - # Get the expected list of alphabetically sorted DomainRequests - expected_order = DomainRequest.objects.order_by("requested_domain__name") - - # Get the returned queryset - queryset = self.admin.get_queryset(request) - - # Check the order - self.assertEqual( - list(queryset), - list(expected_order), - ) - - def test_displays_investigator_filter(self): - """ - This test verifies that the investigator filter in the admin interface for - the DomainRequest model displays correctly. - - It creates two DomainRequest instances, each with a different investigator. - It then simulates a staff user logging in and applying the investigator filter - on the DomainRequest admin page. - - We then test if the page displays the filter we expect, but we do not test - if we get back the correct response in the table. This is to isolate if - the filter displays correctly, when the filter isn't filtering correctly. - """ - - with less_console_noise(): - # Create a mock DomainRequest object, with a fake investigator - domain_request: DomainRequest = generic_domain_object("domain_request", "SomeGuy") - investigator_user = User.objects.filter(username=domain_request.investigator.username).get() - investigator_user.is_staff = True - investigator_user.save() - - p = "userpass" - self.client.login(username="staffuser", password=p) - response = self.client.get( - "/admin/registrar/domainrequest/", - { - "investigator__id__exact": investigator_user.id, - }, - follow=True, - ) - - # Then, test if the filter actually exists - self.assertIn("filters", response.context) - - # Assert the content of filters and search_query - filters = response.context["filters"] - - self.assertEqual( - filters, - [ - { - "parameter_name": "investigator", - "parameter_value": "SomeGuy first_name:investigator SomeGuy last_name:investigator", - }, - ], - ) - - def test_investigator_dropdown_displays_only_staff(self): - """ - This test verifies that the dropdown for the 'investigator' field in the DomainRequestAdmin - interface only displays users who are marked as staff. - - It creates two DomainRequest instances, one with an investigator - who is a staff user and another with an investigator who is not a staff user. - - It then retrieves the queryset for the 'investigator' dropdown from DomainRequestAdmin - and checks that it matches the expected queryset, which only includes staff users. - """ - - with less_console_noise(): - # Create a mock DomainRequest object, with a fake investigator - domain_request: DomainRequest = generic_domain_object("domain_request", "SomeGuy") - investigator_user = User.objects.filter(username=domain_request.investigator.username).get() - investigator_user.is_staff = True - investigator_user.save() - - # Create a mock DomainRequest object, with a user that is not staff - domain_request_2: DomainRequest = generic_domain_object("domain_request", "SomeOtherGuy") - investigator_user_2 = User.objects.filter(username=domain_request_2.investigator.username).get() - investigator_user_2.is_staff = False - investigator_user_2.save() - - p = "userpass" - self.client.login(username="staffuser", password=p) - - request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk)) - - # Get the actual field from the model's meta information - investigator_field = DomainRequest._meta.get_field("investigator") - - # We should only be displaying staff users, in alphabetical order - sorted_fields = ["first_name", "last_name", "email"] - expected_dropdown = list(User.objects.filter(is_staff=True).order_by(*sorted_fields)) - - # Grab the current dropdown. We do an API call to autocomplete to get this info. - domain_request_queryset = self.admin.formfield_for_foreignkey(investigator_field, request).queryset - user_request = self.factory.post( - "/admin/autocomplete/?app_label=registrar&model_name=domainrequest&field_name=investigator" - ) - user_admin = MyUserAdmin(User, self.site) - user_queryset = user_admin.get_search_results(user_request, domain_request_queryset, None)[0] - current_dropdown = list(user_queryset) - - self.assertEqual(expected_dropdown, current_dropdown) - - # Non staff users should not be in the list - self.assertNotIn(domain_request_2, current_dropdown) - - def test_investigator_list_is_alphabetically_sorted(self): - """ - This test verifies that filter list for the 'investigator' - is displayed alphabetically - """ - with less_console_noise(): - # Create a mock DomainRequest object, with a fake investigator - domain_request: DomainRequest = generic_domain_object("domain_request", "SomeGuy") - investigator_user = User.objects.filter(username=domain_request.investigator.username).get() - investigator_user.is_staff = True - investigator_user.save() - - domain_request_2: DomainRequest = generic_domain_object("domain_request", "AGuy") - investigator_user_2 = User.objects.filter(username=domain_request_2.investigator.username).get() - investigator_user_2.first_name = "AGuy" - investigator_user_2.is_staff = True - investigator_user_2.save() - - domain_request_3: DomainRequest = generic_domain_object("domain_request", "FinalGuy") - investigator_user_3 = User.objects.filter(username=domain_request_3.investigator.username).get() - investigator_user_3.first_name = "FinalGuy" - investigator_user_3.is_staff = True - investigator_user_3.save() - - p = "userpass" - self.client.login(username="staffuser", password=p) - request = RequestFactory().get("/") - - # These names have metadata embedded in them. :investigator implicitly tests if - # these are actually from the attribute "investigator". - expected_list = [ - "AGuy AGuy last_name:investigator", - "FinalGuy FinalGuy last_name:investigator", - "SomeGuy first_name:investigator SomeGuy last_name:investigator", - ] - - # Get the actual sorted list of investigators from the lookups method - actual_list = [item for _, item in self.admin.InvestigatorFilter.lookups(self, request, self.admin)] - - self.assertEqual(expected_list, actual_list) - - @less_console_noise_decorator - def test_staff_can_see_cisa_region_federal(self): - """Tests if staff can see CISA Region: N/A""" - - # Create a fake domain request - _domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) - - p = "userpass" - self.client.login(username="staffuser", password=p) - response = self.client.get( - "/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk), - follow=True, - ) - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, _domain_request.requested_domain.name) - - # Test if the page has the right CISA region - expected_html = '
CISA region: N/A
' - # Remove whitespace from expected_html - expected_html = "".join(expected_html.split()) - - # Remove whitespace from response content - response_content = "".join(response.content.decode().split()) - - # Check if response contains expected_html - self.assertIn(expected_html, response_content) - - @less_console_noise_decorator - def test_staff_can_see_cisa_region_non_federal(self): - """Tests if staff can see the correct CISA region""" - - # Create a fake domain request. State will be NY (2). - _domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.IN_REVIEW, generic_org_type="interstate" - ) - - p = "userpass" - self.client.login(username="staffuser", password=p) - response = self.client.get( - "/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk), - follow=True, - ) - - # Make sure the page loaded, and that we're on the right page - self.assertEqual(response.status_code, 200) - self.assertContains(response, _domain_request.requested_domain.name) - - # Test if the page has the right CISA region - expected_html = '
CISA region: 2
' - # Remove whitespace from expected_html - expected_html = "".join(expected_html.split()) - - # Remove whitespace from response content - response_content = "".join(response.content.decode().split()) - - # Check if response contains expected_html - self.assertIn(expected_html, response_content) - - def tearDown(self): - super().tearDown() - Domain.objects.all().delete() - DomainInformation.objects.all().delete() - DomainRequest.objects.all().delete() - User.objects.all().delete() - Contact.objects.all().delete() - Website.objects.all().delete() - SeniorOfficial.objects.all().delete() - self.mock_client.EMAILS_SENT.clear() - - class TestDomainInvitationAdmin(TestCase): - """Tests for the DomainInvitation page""" + """Tests for the DomainInvitationAdmin class as super user + + Notes: + all tests share superuser; do not change this model in tests + tests have available superuser, client, and admin + """ + + @classmethod + def setUpClass(cls): + cls.factory = RequestFactory() + cls.admin = ListHeaderAdmin(model=DomainInvitationAdmin, admin_site=AdminSite()) + cls.superuser = create_superuser() def setUp(self): """Create a client object""" self.client = Client(HTTP_HOST="localhost:8080") - self.factory = RequestFactory() - self.admin = ListHeaderAdmin(model=DomainInvitationAdmin, admin_site=AdminSite()) - self.superuser = create_superuser() def tearDown(self): """Delete all DomainInvitation objects""" DomainInvitation.objects.all().delete() - User.objects.all().delete() Contact.objects.all().delete() + @classmethod + def tearDownClass(self): + User.objects.all().delete() + @less_console_noise_decorator def test_has_model_description(self): """Tests if this model has a model description on the table view""" - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/domaininvitation/", follow=True, @@ -2837,9 +155,7 @@ class TestDomainInvitationAdmin(TestCase): def test_get_filters(self): """Ensures that our filters are displaying correctly""" with less_console_noise(): - # Have to get creative to get past linter - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/domaininvitation/", @@ -2862,32 +178,38 @@ class TestDomainInvitationAdmin(TestCase): class TestHostAdmin(TestCase): + """Tests for the HostAdmin class as super user + + Notes: + all tests share superuser; do not change this model in tests + tests have available superuser, client, and admin + """ + + @classmethod + def setUpClass(cls): + cls.site = AdminSite() + cls.factory = RequestFactory() + cls.admin = MyHostAdmin(model=Host, admin_site=cls.site) + cls.superuser = create_superuser() + def setUp(self): """Setup environment for a mock admin user""" super().setUp() - self.site = AdminSite() - self.factory = RequestFactory() - self.admin = MyHostAdmin(model=Host, admin_site=self.site) self.client = Client(HTTP_HOST="localhost:8080") - self.superuser = create_superuser() - self.test_helper = GenericTestHelper( - factory=self.factory, - user=self.superuser, - admin=self.admin, - url="/admin/registrar/Host/", - model=Host, - ) def tearDown(self): super().tearDown() Host.objects.all().delete() Domain.objects.all().delete() + @classmethod + def tearDownClass(cls): + User.objects.all().delete() + @less_console_noise_decorator def test_has_model_description(self): """Tests if this model has a model description on the table view""" - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/host/", follow=True, @@ -2909,8 +231,7 @@ class TestHostAdmin(TestCase): # Create a fake host host, _ = Host.objects.get_or_create(name="ns1.test.gov", domain=domain) - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/host/{}/change/".format(host.pk), follow=True, @@ -2919,6 +240,13 @@ class TestHostAdmin(TestCase): # Make sure the page loaded self.assertEqual(response.status_code, 200) + self.test_helper = GenericTestHelper( + factory=self.factory, + user=self.superuser, + admin=self.admin, + url="/admin/registrar/Host/", + model=Host, + ) # These should exist in the response expected_values = [ ("domain", "Domain associated with this host"), @@ -2927,48 +255,32 @@ class TestHostAdmin(TestCase): class TestDomainInformationAdmin(TestCase): - def setUp(self): - """Setup environment for a mock admin user""" - self.site = AdminSite() - self.factory = RequestFactory() - self.admin = DomainInformationAdmin(model=DomainInformation, admin_site=self.site) - self.client = Client(HTTP_HOST="localhost:8080") - self.superuser = create_superuser() - self.staffuser = create_user() - self.mock_data_generator = AuditedAdminMockData() + """Tests for the DomainInformationAdmin class as super or staff user - self.test_helper = GenericTestHelper( - factory=self.factory, - user=self.superuser, - admin=self.admin, + Notes: + all tests share superuser/staffuser; do not change these models in tests + tests have available staffuser, superuser, client, test_helper and admin + """ + + @classmethod + def setUpClass(cls): + """Setup environment for a mock admin user""" + cls.site = AdminSite() + cls.factory = RequestFactory() + cls.admin = DomainInformationAdmin(model=DomainInformation, admin_site=cls.site) + cls.superuser = create_superuser() + cls.staffuser = create_user() + cls.mock_data_generator = AuditedAdminMockData() + cls.test_helper = GenericTestHelper( + factory=cls.factory, + user=cls.superuser, + admin=cls.admin, url="/admin/registrar/DomainInformation/", model=DomainInformation, ) - # Create fake DomainInformation objects - DomainInformation.objects.create( - creator=self.mock_data_generator.dummy_user("fake", "creator"), - domain=self.mock_data_generator.dummy_domain("Apple"), - submitter=self.mock_data_generator.dummy_contact("Zebra", "submitter"), - ) - - DomainInformation.objects.create( - creator=self.mock_data_generator.dummy_user("fake", "creator"), - domain=self.mock_data_generator.dummy_domain("Zebra"), - submitter=self.mock_data_generator.dummy_contact("Apple", "submitter"), - ) - - DomainInformation.objects.create( - creator=self.mock_data_generator.dummy_user("fake", "creator"), - domain=self.mock_data_generator.dummy_domain("Circus"), - submitter=self.mock_data_generator.dummy_contact("Xylophone", "submitter"), - ) - - DomainInformation.objects.create( - creator=self.mock_data_generator.dummy_user("fake", "creator"), - domain=self.mock_data_generator.dummy_domain("Xylophone"), - submitter=self.mock_data_generator.dummy_contact("Circus", "submitter"), - ) + def setUp(self): + self.client = Client(HTTP_HOST="localhost:8080") def tearDown(self): """Delete all Users, Domains, and UserDomainRoles""" @@ -2976,9 +288,13 @@ class TestDomainInformationAdmin(TestCase): DomainRequest.objects.all().delete() Domain.objects.all().delete() Contact.objects.all().delete() + + @classmethod + def tearDownClass(cls): User.objects.all().delete() SeniorOfficial.objects.all().delete() + @less_console_noise_decorator def test_domain_information_senior_official_is_alphabetically_sorted(self): """Tests if the senior offical dropdown is alphanetically sorted in the django admin display""" @@ -3020,8 +336,7 @@ class TestDomainInformationAdmin(TestCase): domain_information = DomainInformation.objects.filter(domain_request=_domain_request).get() - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/domaininformation/{}/change/".format(domain_information.pk), follow=True, @@ -3053,8 +368,7 @@ class TestDomainInformationAdmin(TestCase): _domain_request.approve() domain_information = DomainInformation.objects.filter(domain_request=_domain_request).get() - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/domaininformation/{}/change/".format(domain_information.pk), follow=True, @@ -3078,8 +392,7 @@ class TestDomainInformationAdmin(TestCase): @less_console_noise_decorator def test_has_model_description(self): """Tests if this model has a model description on the table view""" - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/domaininformation/", follow=True, @@ -3103,8 +416,7 @@ class TestDomainInformationAdmin(TestCase): domain_request.approve() domain_info = DomainInformation.objects.filter(domain=domain_request.approved_domain).get() - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/domaininformation/{}/change/".format(domain_info.pk), follow=True, @@ -3136,8 +448,7 @@ class TestDomainInformationAdmin(TestCase): # Get the other contact other_contact = domain_info.other_contacts.all().first() - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/domaininformation/{}/change/".format(domain_info.pk), @@ -3173,8 +484,7 @@ class TestDomainInformationAdmin(TestCase): domain_request.approve() domain_info = DomainInformation.objects.filter(domain=domain_request.approved_domain).get() - p = "userpass" - self.client.login(username="staffuser", password=p) + self.client.force_login(self.staffuser) response = self.client.get( "/admin/registrar/domaininformation/{}/change/".format(domain_info.pk), follow=True, @@ -3185,8 +495,7 @@ class TestDomainInformationAdmin(TestCase): # To make sure that its not a fluke, swap to an admin user # and try to access the same page. This should succeed. - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/domaininformation/{}/change/".format(domain_info.pk), follow=True, @@ -3215,8 +524,7 @@ class TestDomainInformationAdmin(TestCase): domain_request.approve() domain_info = DomainInformation.objects.filter(domain=domain_request.approved_domain).get() - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/domaininformation/{}/change/".format(domain_info.pk), follow=True, @@ -3278,6 +586,11 @@ class TestDomainInformationAdmin(TestCase): # Test for the copy link self.assertContains(response, "usa-button__clipboard", count=4) + # cleanup this test + domain_info.delete() + domain_request.delete() + _creator.delete() + def test_readonly_fields_for_analyst(self): """Ensures that analysts have their permissions setup correctly""" with less_console_noise(): @@ -3306,8 +619,7 @@ class TestDomainInformationAdmin(TestCase): def test_domain_sortable(self): """Tests if DomainInformation sorts by domain correctly""" with less_console_noise(): - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) # Assert that our sort works correctly self.test_helper.assert_table_sorted("1", ("domain__name",)) @@ -3318,8 +630,7 @@ class TestDomainInformationAdmin(TestCase): def test_submitter_sortable(self): """Tests if DomainInformation sorts by submitter correctly""" with less_console_noise(): - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) # Assert that our sort works correctly self.test_helper.assert_table_sorted( @@ -3332,32 +643,49 @@ class TestDomainInformationAdmin(TestCase): class TestUserDomainRoleAdmin(TestCase): - def setUp(self): - """Setup environment for a mock admin user""" - self.site = AdminSite() - self.factory = RequestFactory() - self.admin = UserDomainRoleAdmin(model=UserDomainRole, admin_site=self.site) - self.client = Client(HTTP_HOST="localhost:8080") - self.superuser = create_superuser() - self.test_helper = GenericTestHelper( - factory=self.factory, - user=self.superuser, - admin=self.admin, + """Tests for the UserDomainRoleAdmin class as super user + + Notes: + all tests share superuser; do not change this model in tests + tests have available superuser, client, test_helper and admin + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.site = AdminSite() + cls.factory = RequestFactory() + cls.admin = UserDomainRoleAdmin(model=UserDomainRole, admin_site=cls.site) + cls.superuser = create_superuser() + cls.test_helper = GenericTestHelper( + factory=cls.factory, + user=cls.superuser, + admin=cls.admin, url="/admin/registrar/UserDomainRole/", model=UserDomainRole, ) + def setUp(self): + """Setup environment for a mock admin user""" + super().setUp() + self.client = Client(HTTP_HOST="localhost:8080") + def tearDown(self): """Delete all Users, Domains, and UserDomainRoles""" - User.objects.all().delete() - Domain.objects.all().delete() + super().tearDown() UserDomainRole.objects.all().delete() + Domain.objects.all().delete() + User.objects.exclude(username="superuser").delete() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + User.objects.all().delete() @less_console_noise_decorator def test_has_model_description(self): """Tests if this model has a model description on the table view""" - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/userdomainrole/", follow=True, @@ -3375,8 +703,7 @@ class TestUserDomainRoleAdmin(TestCase): def test_domain_sortable(self): """Tests if the UserDomainrole sorts by domain correctly""" with less_console_noise(): - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) fake_user = User.objects.create( username="dummyuser", first_name="Stewart", last_name="Jones", email="AntarcticPolarBears@example.com" @@ -3397,8 +724,7 @@ class TestUserDomainRoleAdmin(TestCase): def test_user_sortable(self): """Tests if the UserDomainrole sorts by user correctly""" with less_console_noise(): - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) mock_data_generator = AuditedAdminMockData() @@ -3421,8 +747,7 @@ class TestUserDomainRoleAdmin(TestCase): Should return no results for an invalid email.""" with less_console_noise(): # Have to get creative to get past linter - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) fake_user = User.objects.create( username="dummyuser", first_name="Stewart", last_name="Jones", email="AntarcticPolarBears@example.com" @@ -3454,8 +779,7 @@ class TestUserDomainRoleAdmin(TestCase): Should return results for an valid email.""" with less_console_noise(): # Have to get creative to get past linter - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) fake_user = User.objects.create( username="dummyuser", first_name="Joe", last_name="Jones", email="AntarcticPolarBears@example.com" @@ -3484,18 +808,38 @@ class TestUserDomainRoleAdmin(TestCase): class TestListHeaderAdmin(TestCase): + """Tests for the ListHeaderAdmin class as super user + + Notes: + all tests share superuser; do not change this model in tests + tests have available superuser, client and admin + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.site = AdminSite() + cls.factory = RequestFactory() + cls.admin = ListHeaderAdmin(model=DomainRequest, admin_site=None) + cls.superuser = create_superuser() + def setUp(self): - self.site = AdminSite() - self.factory = RequestFactory() - self.admin = ListHeaderAdmin(model=DomainRequest, admin_site=None) + super().setUp() self.client = Client(HTTP_HOST="localhost:8080") - self.superuser = create_superuser() + + def tearDown(self): + # delete any domain requests too + DomainInformation.objects.all().delete() + DomainRequest.objects.all().delete() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + User.objects.all().delete() def test_changelist_view(self): with less_console_noise(): - # Have to get creative to get past linter - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) # Mock a user user = mock_user() # Make the request using the Client class @@ -3549,33 +893,42 @@ class TestListHeaderAdmin(TestCase): ], ) - def tearDown(self): - # delete any domain requests too - DomainInformation.objects.all().delete() - DomainRequest.objects.all().delete() - User.objects.all().delete() +class TestMyUserAdmin(MockDbForSharedTests): + """Tests for the MyUserAdmin class as super or staff user + + Notes: + all tests share superuser/staffuser; do not change these models in tests + all tests share MockDb; do not change models defined therein in tests + tests have available staffuser, superuser, client, test_helper and admin + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + admin_site = AdminSite() + cls.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site) + cls.superuser = create_superuser() + cls.staffuser = create_user() + cls.test_helper = GenericTestHelper(admin=cls.admin) -class TestMyUserAdmin(MockDb): def setUp(self): super().setUp() - admin_site = AdminSite() - self.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site) self.client = Client(HTTP_HOST="localhost:8080") - self.superuser = create_superuser() - self.staffuser = create_user() - self.test_helper = GenericTestHelper(admin=self.admin) def tearDown(self): super().tearDown() DomainRequest.objects.all().delete() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() User.objects.all().delete() @less_console_noise_decorator def test_has_model_description(self): """Tests if this model has a model description on the table view""" - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/user/", follow=True, @@ -3595,8 +948,7 @@ class TestMyUserAdmin(MockDb): """ user = self.staffuser - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/user/{}/change/".format(user.pk), follow=True, @@ -3616,21 +968,20 @@ class TestMyUserAdmin(MockDb): @less_console_noise_decorator def test_list_display_without_username(self): - with less_console_noise(): - request = self.client.request().wsgi_request - request.user = self.staffuser + request = self.client.request().wsgi_request + request.user = self.staffuser - list_display = self.admin.get_list_display(request) - expected_list_display = [ - "email", - "first_name", - "last_name", - "group", - "status", - ] + list_display = self.admin.get_list_display(request) + expected_list_display = [ + "email", + "first_name", + "last_name", + "group", + "status", + ] - self.assertEqual(list_display, expected_list_display) - self.assertNotIn("username", list_display) + self.assertEqual(list_display, expected_list_display) + self.assertNotIn("username", list_display) def test_get_fieldsets_superuser(self): with less_console_noise(): @@ -3670,6 +1021,7 @@ class TestMyUserAdmin(MockDb): ) self.assertEqual(fieldsets, expected_fieldsets) + @less_console_noise_decorator def test_analyst_can_see_related_domains_and_requests_in_user_form(self): """Tests if an analyst can see the related domains and domain requests for a user in that user's form""" @@ -3706,12 +1058,11 @@ class TestMyUserAdmin(MockDb): domain_deleted, _ = Domain.objects.get_or_create( name="domain_deleted.gov", state=Domain.State.DELETED, deleted=timezone.make_aware(datetime(2024, 4, 2)) ) - _, created = UserDomainRole.objects.get_or_create( + role, _ = UserDomainRole.objects.get_or_create( user=self.meoward_user, domain=domain_deleted, role=UserDomainRole.Roles.MANAGER ) - p = "userpass" - self.client.login(username="staffuser", password=p) + self.client.force_login(self.staffuser) response = self.client.get( "/admin/registrar/user/{}/change/".format(self.meoward_user.id), follow=True, @@ -3761,12 +1112,32 @@ class TestMyUserAdmin(MockDb): expected_href = reverse("admin:registrar_domain_change", args=[domain_deleted.pk]) self.assertNotContains(response, expected_href) + # Must clean up within test since MockDB is shared across tests for performance reasons + domain_request_started_id = domain_request_started.id + domain_request_submitted_id = domain_request_submitted.id + domain_request_in_review_id = domain_request_in_review.id + domain_request_withdrawn_id = domain_request_withdrawn.id + domain_request_approved_id = domain_request_approved.id + domain_request_rejected_id = domain_request_rejected.id + domain_request_ineligible_id = domain_request_ineligible.id + domain_request_ids = [ + domain_request_started_id, + domain_request_submitted_id, + domain_request_in_review_id, + domain_request_withdrawn_id, + domain_request_approved_id, + domain_request_rejected_id, + domain_request_ineligible_id, + ] + DomainRequest.objects.filter(id__in=domain_request_ids).delete() + domain_deleted.delete() + role.delete() + def test_analyst_cannot_see_selects_for_portfolio_role_and_permissions_in_user_form(self): """Can only test for the presence of a base element. The multiselects and the h2->h3 conversion are all dynamically generated.""" - p = "userpass" - self.client.login(username="staffuser", password=p) + self.client.force_login(self.staffuser) response = self.client.get( "/admin/registrar/user/{}/change/".format(self.meoward_user.id), follow=True, @@ -3779,12 +1150,24 @@ class TestMyUserAdmin(MockDb): class AuditedAdminTest(TestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.site = AdminSite() + cls.factory = RequestFactory() + def setUp(self): - self.site = AdminSite() - self.factory = RequestFactory() + super().setUp() self.client = Client(HTTP_HOST="localhost:8080") self.staffuser = create_user() + def tearDown(self): + super().tearDown() + DomainInformation.objects.all().delete() + DomainRequest.objects.all().delete() + DomainInvitation.objects.all().delete() + def order_by_desired_field_helper(self, obj_to_sort: AuditedAdmin, request, field_name, *obj_names): with less_console_noise(): formatted_sort_fields = [] @@ -3797,6 +1180,7 @@ class AuditedAdminTest(TestCase): return ordered_list + @less_console_noise_decorator def test_alphabetically_sorted_domain_request_investigator(self): """Tests if the investigator field is alphabetically sorted by mimicking the call event flow""" @@ -4012,26 +1396,31 @@ class AuditedAdminTest(TestCase): else: return None - def tearDown(self): - DomainInformation.objects.all().delete() - DomainRequest.objects.all().delete() - DomainInvitation.objects.all().delete() - class DomainSessionVariableTest(TestCase): """Test cases for session variables in Django Admin""" + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.factory = RequestFactory() + cls.admin = DomainAdmin(Domain, None) + cls.superuser = create_superuser() + def setUp(self): - self.factory = RequestFactory() - self.admin = DomainAdmin(Domain, None) + super().setUp() self.client = Client(HTTP_HOST="localhost:8080") + @classmethod + def tearDownClass(cls): + super().tearDownClass() + User.objects.all().delete() + def test_session_vars_set_correctly(self): """Checks if session variables are being set correctly""" with less_console_noise(): - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) dummy_domain_information = generic_domain_object("information", "session") request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) @@ -4046,8 +1435,7 @@ class DomainSessionVariableTest(TestCase): """Checks if session variables are being set correctly""" with less_console_noise(): - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) dummy_domain_information: Domain = generic_domain_object("information", "session") dummy_domain_information.domain.pk = 1 @@ -4061,8 +1449,7 @@ class DomainSessionVariableTest(TestCase): """Checks if incorrect session variables get overridden""" with less_console_noise(): - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) dummy_domain_information = generic_domain_object("information", "session") request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk) @@ -4079,8 +1466,7 @@ class DomainSessionVariableTest(TestCase): """Checks to see if session variables retain old information""" with less_console_noise(): - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) dummy_domain_information_list = multiple_unalphabetical_domain_objects("information") for item in dummy_domain_information_list: @@ -4094,8 +1480,7 @@ class DomainSessionVariableTest(TestCase): """Simulates two requests at once""" with less_console_noise(): - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) info_first = generic_domain_object("information", "session") info_second = generic_domain_object("information", "session2") @@ -4144,19 +1529,34 @@ class DomainSessionVariableTest(TestCase): class TestContactAdmin(TestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.site = AdminSite() + cls.factory = RequestFactory() + cls.admin = ContactAdmin(model=Contact, admin_site=None) + cls.superuser = create_superuser() + cls.staffuser = create_user() + def setUp(self): - self.site = AdminSite() - self.factory = RequestFactory() + super().setUp() self.client = Client(HTTP_HOST="localhost:8080") - self.admin = ContactAdmin(model=get_user_model(), admin_site=None) - self.superuser = create_superuser() - self.staffuser = create_user() + + def tearDown(self): + super().tearDown() + DomainRequest.objects.all().delete() + Contact.objects.all().delete() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + User.objects.all().delete() @less_console_noise_decorator def test_has_model_description(self): """Tests if this model has a model description on the table view""" - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/contact/", follow=True, @@ -4229,6 +1629,10 @@ class TestContactAdmin(TestCase): "", ) + # cleanup this test + DomainRequest.objects.all().delete() + contact.delete() + def test_change_view_for_joined_contact_five_or_more(self): """Create a contact, join it to 6 domain requests. Assert that the warning on the contact form lists 5 joins and a '1 more' ellispsis.""" @@ -4268,33 +1672,39 @@ class TestContactAdmin(TestCase): "" "

And 1 more...

", ) - - def tearDown(self): - DomainRequest.objects.all().delete() - Contact.objects.all().delete() - User.objects.all().delete() + # cleanup this test + DomainRequest.objects.all().delete() + contact.delete() class TestVerifiedByStaffAdmin(TestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.site = AdminSite() + cls.superuser = create_superuser() + cls.admin = VerifiedByStaffAdmin(model=VerifiedByStaff, admin_site=cls.site) + cls.factory = RequestFactory() + cls.test_helper = GenericTestHelper(admin=cls.admin) + def setUp(self): super().setUp() - self.site = AdminSite() - self.superuser = create_superuser() - self.admin = VerifiedByStaffAdmin(model=VerifiedByStaff, admin_site=self.site) - self.factory = RequestFactory() self.client = Client(HTTP_HOST="localhost:8080") - self.test_helper = GenericTestHelper(admin=self.admin) def tearDown(self): super().tearDown() VerifiedByStaff.objects.all().delete() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() User.objects.all().delete() @less_console_noise_decorator def test_has_model_description(self): """Tests if this model has a model description on the table view""" - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/verifiedbystaff/", follow=True, @@ -4316,8 +1726,7 @@ class TestVerifiedByStaffAdmin(TestCase): """ vip_instance, _ = VerifiedByStaff.objects.get_or_create(email="test@example.com", notes="Test Notes") - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/verifiedbystaff/{}/change/".format(vip_instance.pk), follow=True, @@ -4371,8 +1780,7 @@ class TestWebsiteAdmin(TestCase): @less_console_noise_decorator def test_has_model_description(self): """Tests if this model has a model description on the table view""" - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/website/", follow=True, @@ -4387,25 +1795,33 @@ class TestWebsiteAdmin(TestCase): class TestDraftDomain(TestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.site = AdminSite() + cls.superuser = create_superuser() + cls.admin = DraftDomainAdmin(model=DraftDomain, admin_site=cls.site) + cls.factory = RequestFactory() + cls.test_helper = GenericTestHelper(admin=cls.admin) + def setUp(self): super().setUp() - self.site = AdminSite() - self.superuser = create_superuser() - self.admin = DraftDomainAdmin(model=DraftDomain, admin_site=self.site) - self.factory = RequestFactory() self.client = Client(HTTP_HOST="localhost:8080") - self.test_helper = GenericTestHelper(admin=self.admin) def tearDown(self): super().tearDown() DraftDomain.objects.all().delete() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() User.objects.all().delete() @less_console_noise_decorator def test_has_model_description(self): """Tests if this model has a model description on the table view""" - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/draftdomain/", follow=True, @@ -4422,25 +1838,28 @@ class TestDraftDomain(TestCase): class TestFederalAgency(TestCase): - def setUp(self): - super().setUp() - self.site = AdminSite() - self.superuser = create_superuser() - self.admin = FederalAgencyAdmin(model=FederalAgency, admin_site=self.site) - self.factory = RequestFactory() - self.client = Client(HTTP_HOST="localhost:8080") - self.test_helper = GenericTestHelper(admin=self.admin) - def tearDown(self): - super().tearDown() - FederalAgency.objects.all().delete() + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.site = AdminSite() + cls.superuser = create_superuser() + cls.admin = FederalAgencyAdmin(model=FederalAgency, admin_site=cls.site) + cls.factory = RequestFactory() + cls.test_helper = GenericTestHelper(admin=cls.admin) + + def setUp(self): + self.client = Client(HTTP_HOST="localhost:8080") + + @classmethod + def tearDownClass(cls): + super().tearDownClass() User.objects.all().delete() @less_console_noise_decorator def test_has_model_description(self): """Tests if this model has a model description on the table view""" - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/federalagency/", follow=True, @@ -4505,8 +1924,7 @@ class TestTransitionDomain(TestCase): @less_console_noise_decorator def test_has_model_description(self): """Tests if this model has a model description on the table view""" - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/transitiondomain/", follow=True, @@ -4537,8 +1955,7 @@ class TestUserGroup(TestCase): @less_console_noise_decorator def test_has_model_description(self): """Tests if this model has a model description on the table view""" - p = "adminpass" - self.client.login(username="superuser", password=p) + self.client.force_login(self.superuser) response = self.client.get( "/admin/registrar/usergroup/", follow=True, diff --git a/src/registrar/tests/test_admin_domain.py b/src/registrar/tests/test_admin_domain.py new file mode 100644 index 000000000..e63a9d8a9 --- /dev/null +++ b/src/registrar/tests/test_admin_domain.py @@ -0,0 +1,824 @@ +from datetime import date +from django.test import TestCase, RequestFactory, Client, override_settings +from django.contrib.admin.sites import AdminSite +from api.tests.common import less_console_noise_decorator +from django_webtest import WebTest # type: ignore +from django.contrib import messages +from django.urls import reverse +from registrar.admin import ( + DomainAdmin, +) +from registrar.models import ( + Domain, + DomainRequest, + DomainInformation, + User, + Host, +) +from .common import ( + MockSESClient, + completed_domain_request, + less_console_noise, + create_superuser, + create_user, + create_ready_domain, + MockEppLib, + GenericTestHelper, +) +from unittest.mock import ANY, call, patch + +import boto3_mocking # type: ignore +import logging + +logger = logging.getLogger(__name__) + + +class TestDomainAdminAsStaff(MockEppLib): + """Test DomainAdmin class as staff user. + + Notes: + all tests share staffuser; do not change staffuser model in tests + tests have available staffuser, client, and admin + """ + + @classmethod + def setUpClass(self): + super().setUpClass() + self.staffuser = create_user() + self.site = AdminSite() + self.admin = DomainAdmin(model=Domain, admin_site=self.site) + self.factory = RequestFactory() + + def setUp(self): + self.client = Client(HTTP_HOST="localhost:8080") + self.client.force_login(self.staffuser) + super().setUp() + + def tearDown(self): + super().tearDown() + Host.objects.all().delete() + Domain.objects.all().delete() + DomainInformation.objects.all().delete() + DomainRequest.objects.all().delete() + + @classmethod + def tearDownClass(self): + User.objects.all().delete() + super().tearDownClass() + + @less_console_noise_decorator + def test_staff_can_see_cisa_region_federal(self): + """Tests if staff can see CISA Region: N/A""" + + # Create a fake domain request + _domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + _domain_request.approve() + + domain = _domain_request.approved_domain + response = self.client.get( + "/admin/registrar/domain/{}/change/".format(domain.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + + # Test if the page has the right CISA region + expected_html = '
CISA region: N/A
' + # Remove whitespace from expected_html + expected_html = "".join(expected_html.split()) + + # Remove whitespace from response content + response_content = "".join(response.content.decode().split()) + + # Check if response contains expected_html + self.assertIn(expected_html, response_content) + + @less_console_noise_decorator + def test_staff_can_see_cisa_region_non_federal(self): + """Tests if staff can see the correct CISA region""" + + # Create a fake domain request. State will be NY (2). + _domain_request = completed_domain_request( + status=DomainRequest.DomainRequestStatus.IN_REVIEW, generic_org_type="interstate" + ) + + _domain_request.approve() + + domain = _domain_request.approved_domain + + response = self.client.get( + "/admin/registrar/domain/{}/change/".format(domain.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + + # Test if the page has the right CISA region + expected_html = '
CISA region: 2
' + # Remove whitespace from expected_html + expected_html = "".join(expected_html.split()) + + # Remove whitespace from response content + response_content = "".join(response.content.decode().split()) + + # Check if response contains expected_html + self.assertIn(expected_html, response_content) + + @less_console_noise_decorator + def test_analyst_can_see_inline_domain_information_in_domain_change_form(self): + """Tests if an analyst can still see the inline domain information form""" + + # Create fake creator + _creator = User.objects.create( + username="MrMeoward", + first_name="Meoward", + last_name="Jones", + ) + + # Create a fake domain request + _domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator) + + # Creates a Domain and DomainInformation object + _domain_request.approve() + + domain_information = DomainInformation.objects.filter(domain_request=_domain_request).get() + domain_information.organization_name = "MonkeySeeMonkeyDo" + domain_information.save() + + # We use filter here rather than just domain_information.domain just to get the latest data. + domain = Domain.objects.filter(domain_info=domain_information).get() + + response = self.client.get( + "/admin/registrar/domain/{}/change/".format(domain.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + + # Test for data. We only need to test one since its all interconnected. + expected_organization_name = "MonkeySeeMonkeyDo" + self.assertContains(response, expected_organization_name) + + # clean up this test's data + domain.delete() + domain_information.delete() + _domain_request.delete() + _creator.delete() + + @less_console_noise_decorator + def test_deletion_is_successful(self): + """ + Scenario: Domain deletion is unsuccessful + When the domain is deleted + Then a user-friendly success message is returned for displaying on the web + And `state` is set to `DELETED` + """ + domain = create_ready_domain() + # Put in client hold + domain.place_client_hold() + # Ensure everything is displaying correctly + response = self.client.get( + "/admin/registrar/domain/{}/change/".format(domain.pk), + follow=True, + ) + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + self.assertContains(response, "Remove from registry") + + # The contents of the modal should exist before and after the post. + # Check for the header + self.assertContains(response, "Are you sure you want to remove this domain from the registry?") + + # Check for some of its body + self.assertContains(response, "When a domain is removed from the registry:") + + # Check for some of the button content + self.assertContains(response, "Yes, remove from registry") + + # Test the info dialog + request = self.factory.post( + "/admin/registrar/domain/{}/change/".format(domain.pk), + {"_delete_domain": "Remove from registry", "name": domain.name}, + follow=True, + ) + request.user = self.client + with patch("django.contrib.messages.add_message") as mock_add_message: + self.admin.do_delete_domain(request, domain) + mock_add_message.assert_called_once_with( + request, + messages.INFO, + "Domain city.gov has been deleted. Thanks!", + extra_tags="", + fail_silently=False, + ) + + # The modal should still exist + self.assertContains(response, "Are you sure you want to remove this domain from the registry?") + self.assertContains(response, "When a domain is removed from the registry:") + self.assertContains(response, "Yes, remove from registry") + + self.assertEqual(domain.state, Domain.State.DELETED) + + # clean up data within this test + domain.delete() + + @less_console_noise_decorator + def test_deletion_ready_fsm_failure(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` + """ + + domain = create_ready_domain() + # Ensure everything is displaying correctly + response = self.client.get( + "/admin/registrar/domain/{}/change/".format(domain.pk), + follow=True, + ) + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + self.assertContains(response, "Remove from registry") + # Test the error + request = self.factory.post( + "/admin/registrar/domain/{}/change/".format(domain.pk), + {"_delete_domain": "Remove from registry", "name": domain.name}, + follow=True, + ) + request.user = self.client + with patch("django.contrib.messages.add_message") as mock_add_message: + self.admin.do_delete_domain(request, domain) + mock_add_message.assert_called_once_with( + request, + messages.ERROR, + "Error deleting this Domain: " + "Can't switch from state 'ready' to 'deleted'" + ", must be either 'dns_needed' or 'on_hold'", + extra_tags="", + fail_silently=False, + ) + + self.assertEqual(domain.state, Domain.State.READY) + + # delete data created in this test + domain.delete() + + @less_console_noise_decorator + def test_analyst_deletes_domain_idempotent(self): + """ + Scenario: Analyst tries to delete an already deleted domain + Given `state` is already `DELETED` + When `domain.deletedInEpp()` is called + Then `commands.DeleteDomain` is sent to the registry + And Domain returns normally without an error dialog + """ + domain = create_ready_domain() + # Put in client hold + domain.place_client_hold() + # Ensure everything is displaying correctly + response = self.client.get( + "/admin/registrar/domain/{}/change/".format(domain.pk), + follow=True, + ) + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + self.assertContains(response, "Remove from registry") + # Test the info dialog + request = self.factory.post( + "/admin/registrar/domain/{}/change/".format(domain.pk), + {"_delete_domain": "Remove from registry", "name": domain.name}, + follow=True, + ) + request.user = self.client + # Delete it once + with patch("django.contrib.messages.add_message") as mock_add_message: + self.admin.do_delete_domain(request, domain) + mock_add_message.assert_called_once_with( + request, + messages.INFO, + "Domain city.gov has been deleted. Thanks!", + extra_tags="", + fail_silently=False, + ) + + self.assertEqual(domain.state, Domain.State.DELETED) + # Try to delete it again + # Test the info dialog + request = self.factory.post( + "/admin/registrar/domain/{}/change/".format(domain.pk), + {"_delete_domain": "Remove from registry", "name": domain.name}, + follow=True, + ) + request.user = self.client + with patch("django.contrib.messages.add_message") as mock_add_message: + self.admin.do_delete_domain(request, domain) + mock_add_message.assert_called_once_with( + request, + messages.INFO, + "This domain is already deleted", + extra_tags="", + fail_silently=False, + ) + self.assertEqual(domain.state, Domain.State.DELETED) + + # delete data created in this test + domain.delete() + + +class TestDomainAdminWithClient(TestCase): + """Test DomainAdmin class as super user. + + Notes: + all tests share superuser; tests must not update superuser + tests have available superuser, client, and admin + """ + + @classmethod + def setUpClass(self): + super().setUpClass() + self.site = AdminSite() + self.admin = DomainAdmin(model=Domain, admin_site=self.site) + self.factory = RequestFactory() + self.superuser = create_superuser() + + def setUp(self): + self.client = Client(HTTP_HOST="localhost:8080") + self.client.force_login(self.superuser) + super().setUp() + + def tearDown(self): + super().tearDown() + Host.objects.all().delete() + Domain.objects.all().delete() + DomainInformation.objects.all().delete() + DomainRequest.objects.all().delete() + + @classmethod + def tearDownClass(self): + User.objects.all().delete() + super().tearDownClass() + + @less_console_noise_decorator + def test_has_model_description(self): + """Tests if this model has a model description on the table view""" + response = self.client.get( + "/admin/registrar/domain/", + follow=True, + ) + + # Make sure that the page is loaded correctly + self.assertEqual(response.status_code, 200) + + # Test for a description snippet + self.assertContains(response, "This table contains all approved domains in the .gov registrar.") + self.assertContains(response, "Show more") + + @less_console_noise_decorator + def test_contact_fields_on_domain_change_form_have_detail_table(self): + """Tests if the contact fields in the inlined Domain information have the detail table + which displays title, email, and phone""" + + # Create fake creator + _creator = User.objects.create( + username="MrMeoward", + first_name="Meoward", + last_name="Jones", + email="meoward.jones@igorville.gov", + phone="(555) 123 12345", + title="Treat inspector", + ) + + # Create a fake domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator) + domain_request.approve() + _domain_info = DomainInformation.objects.filter(domain=domain_request.approved_domain).get() + domain = Domain.objects.filter(domain_info=_domain_info).get() + + response = self.client.get( + "/admin/registrar/domain/{}/change/".format(domain.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + + # Check that the fields have the right values. + # == Check for the creator == # + + # Check for the right title, email, and phone number in the response. + # We only need to check for the end tag + # (Otherwise this test will fail if we change classes, etc) + self.assertContains(response, "Treat inspector") + self.assertContains(response, "meoward.jones@igorville.gov") + self.assertContains(response, "(555) 123 12345") + + # Check for the field itself + self.assertContains(response, "Meoward Jones") + + # == Check for the submitter == # + self.assertContains(response, "mayor@igorville.gov") + + self.assertContains(response, "Admin Tester") + self.assertContains(response, "(555) 555 5556") + self.assertContains(response, "Testy2 Tester2") + + # == Check for the senior_official == # + self.assertContains(response, "testy@town.com") + self.assertContains(response, "Chief Tester") + self.assertContains(response, "(555) 555 5555") + + # Includes things like readonly fields + self.assertContains(response, "Testy Tester") + + # == Test the other_employees field == # + self.assertContains(response, "testy2@town.com") + self.assertContains(response, "Another Tester") + self.assertContains(response, "(555) 555 5557") + + # Test for the copy link + self.assertContains(response, "usa-button__clipboard") + + # cleanup from this test + domain.delete() + _domain_info.delete() + domain_request.delete() + _creator.delete() + + @less_console_noise_decorator + def test_helper_text(self): + """ + Tests for the correct helper text on this page + """ + + # Create a ready domain with a preset expiration date + domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) + + response = self.client.get( + "/admin/registrar/domain/{}/change/".format(domain.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + + # Contains some test tools + test_helper = GenericTestHelper( + factory=self.factory, + user=self.superuser, + admin=self.admin, + url=reverse("admin:registrar_domain_changelist"), + model=Domain, + client=self.client, + ) + # These should exist in the response + expected_values = [ + ("expiration_date", "Date the domain expires in the registry"), + ("first_ready_at", 'Date when this domain first moved into "ready" state; date will never change'), + ("deleted_at", 'Will appear blank unless the domain is in "deleted" state'), + ] + test_helper.assert_response_contains_distinct_values(response, expected_values) + + @less_console_noise_decorator + def test_helper_text_state(self): + """ + Tests for the correct state helper text on this page + """ + + # Add domain data + ready_domain, _ = Domain.objects.get_or_create(name="fakeready.gov", state=Domain.State.READY) + unknown_domain, _ = Domain.objects.get_or_create(name="fakeunknown.gov", state=Domain.State.UNKNOWN) + dns_domain, _ = Domain.objects.get_or_create(name="fakedns.gov", state=Domain.State.DNS_NEEDED) + hold_domain, _ = Domain.objects.get_or_create(name="fakehold.gov", state=Domain.State.ON_HOLD) + deleted_domain, _ = Domain.objects.get_or_create(name="fakedeleted.gov", state=Domain.State.DELETED) + + # We don't need to check for all text content, just a portion of it + expected_unknown_domain_message = "The creator of the associated domain request has not logged in to" + expected_dns_message = "Before this domain can be used, name server addresses need" + expected_hold_message = "While on hold, this domain" + expected_deleted_message = "This domain was permanently removed from the registry." + expected_messages = [ + (ready_domain, "This domain has name servers and is ready for use."), + (unknown_domain, expected_unknown_domain_message), + (dns_domain, expected_dns_message), + (hold_domain, expected_hold_message), + (deleted_domain, expected_deleted_message), + ] + + for domain, message in expected_messages: + with self.subTest(domain_state=domain.state): + response = self.client.get( + "/admin/registrar/domain/{}/change/".format(domain.id), + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + + # Check that the right help text exists + self.assertContains(response, message) + + @less_console_noise_decorator + def test_admin_can_see_inline_domain_information_in_domain_change_form(self): + """Tests if an admin can still see the inline domain information form""" + # Create fake creator + _creator = User.objects.create( + username="MrMeoward", + first_name="Meoward", + last_name="Jones", + ) + + # Create a fake domain request + _domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator) + + # Creates a Domain and DomainInformation object + _domain_request.approve() + + domain_information = DomainInformation.objects.filter(domain_request=_domain_request).get() + domain_information.organization_name = "MonkeySeeMonkeyDo" + domain_information.save() + + # We use filter here rather than just domain_information.domain just to get the latest data. + domain = Domain.objects.filter(domain_info=domain_information).get() + + response = self.client.get( + "/admin/registrar/domain/{}/change/".format(domain.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + + # Test for data. We only need to test one since its all interconnected. + expected_organization_name = "MonkeySeeMonkeyDo" + self.assertContains(response, expected_organization_name) + + # cleanup from this test + domain.delete() + domain_information.delete() + _domain_request.delete() + _creator.delete() + + @less_console_noise_decorator + def test_custom_delete_confirmation_page_table(self): + """Tests if we override the delete confirmation page for custom content on the table""" + # Create a ready domain + domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) + + # Get the index. The post expects the index to be encoded as a string + index = f"{domain.id}" + + # Contains some test tools + test_helper = GenericTestHelper( + factory=self.factory, + user=self.superuser, + admin=self.admin, + url=reverse("admin:registrar_domain_changelist"), + model=Domain, + client=self.client, + ) + + # Simulate selecting a single record, then clicking "Delete selected domains" + response = test_helper.get_table_delete_confirmation_page("0", index) + + # Check that our content exists + content_slice = "When a domain is deleted:" + self.assertContains(response, content_slice) + + @less_console_noise_decorator + def test_short_org_name_in_domains_list(self): + """ + Make sure the short name is displaying in admin on the list page + """ + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + domain_request.approve() + + response = self.client.get("/admin/registrar/domain/") + # There are 4 template references to Federal (4) plus four references in the table + # for our actual domain_request + self.assertContains(response, "Federal", count=56) + # This may be a bit more robust + self.assertContains(response, 'Federal', count=1) + # Now let's make sure the long description does not exist + self.assertNotContains(response, "Federal: an agency of the U.S. government") + + @override_settings(IS_PRODUCTION=True) + @less_console_noise_decorator + def test_prod_only_shows_export(self): + """Test that production environment only displays export""" + response = self.client.get("/admin/registrar/domain/") + self.assertContains(response, ">Export<") + self.assertNotContains(response, ">Import<") + + +class TestDomainAdminWebTest(MockEppLib, WebTest): + """Test DomainAdmin class as super user, using WebTest. + WebTest allows for easier handling of forms and html responses. + + Notes: + all tests share superuser; tests must not update superuser + tests have available superuser, app, and admin + """ + + # csrf checks do not work with WebTest. + # We disable them here. TODO for another ticket. + csrf_checks = False + + @classmethod + def setUpClass(self): + super().setUpClass() + self.site = AdminSite() + self.admin = DomainAdmin(model=Domain, admin_site=self.site) + self.superuser = create_superuser() + self.factory = RequestFactory() + + def setUp(self): + super().setUp() + self.app.set_user(self.superuser.username) + + def tearDown(self): + super().tearDown() + Host.objects.all().delete() + Domain.objects.all().delete() + DomainInformation.objects.all().delete() + DomainRequest.objects.all().delete() + + @classmethod + def tearDownClass(self): + User.objects.all().delete() + super().tearDownClass() + + @less_console_noise_decorator + @patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1)) + def test_extend_expiration_date_button(self, mock_date_today): + """ + Tests if extend_expiration_date modal gives an accurate date + """ + + # Create a ready domain with a preset expiration date + domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) + response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk])) + # load expiration date into cache and registrar with below command + domain.registry_expiration_date + # Make sure the ex date is what we expect it to be + domain_ex_date = Domain.objects.get(id=domain.id).expiration_date + self.assertEqual(domain_ex_date, date(2023, 5, 25)) + + # Make sure that the page is loading as expected + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + self.assertContains(response, "Extend expiration date") + + # Grab the form to submit + form = response.forms["domain_form"] + + with patch("django.contrib.messages.add_message") as mock_add_message: + # Submit the form + response = form.submit("_extend_expiration_date") + + # Follow the response + response = response.follow() + + # Assert that everything on the page looks correct + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + self.assertContains(response, "Extend expiration date") + + # Ensure the message we recieve is in line with what we expect + expected_message = "Successfully extended the expiration date." + expected_call = call( + # The WGSI request doesn't need to be tested + ANY, + messages.INFO, + expected_message, + extra_tags="", + fail_silently=False, + ) + + mock_add_message.assert_has_calls([expected_call], 1) + + @less_console_noise_decorator + @patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1)) + def test_extend_expiration_date_button_epp(self, mock_date_today): + """ + Tests if extend_expiration_date button sends the right epp command + """ + + # Create a ready domain with a preset expiration date + domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) + + response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk])) + + # Make sure that the page is loading as expected + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + self.assertContains(response, "Extend expiration date") + + # Grab the form to submit + form = response.forms["domain_form"] + + with patch("django.contrib.messages.add_message") as mock_add_message: + with patch("registrar.models.Domain.renew_domain") as renew_mock: + # Submit the form + response = form.submit("_extend_expiration_date") + + # Follow the response + response = response.follow() + + # Assert that it is calling the function with the default extension length. + # We only need to test the value that EPP sends, as we can assume the other + # test cases cover the "renew" function. + renew_mock.assert_has_calls([call()], any_order=False) + + # We should not make duplicate calls + self.assertEqual(renew_mock.call_count, 1) + + # Assert that everything on the page looks correct + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + self.assertContains(response, "Extend expiration date") + + # Ensure the message we recieve is in line with what we expect + expected_message = "Successfully extended the expiration date." + expected_call = call( + # The WGSI request doesn't need to be tested + ANY, + messages.INFO, + expected_message, + extra_tags="", + fail_silently=False, + ) + mock_add_message.assert_has_calls([expected_call], 1) + + @less_console_noise_decorator + def test_custom_delete_confirmation_page(self): + """Tests if we override the delete confirmation page for custom content""" + # Create a ready domain with a preset expiration date + domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) + + domain_change_page = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk])) + + self.assertContains(domain_change_page, "fake.gov") + # click the "Delete" link + confirmation_page = domain_change_page.click("Delete", index=0) + + content_slice = "When a domain is deleted:" + self.assertContains(confirmation_page, content_slice) + + @less_console_noise_decorator + def test_on_hold_is_successful_web_test(self): + """ + Scenario: Domain on_hold is successful through webtest + """ + with less_console_noise(): + domain = create_ready_domain() + + response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk])) + + # Check the contents of the modal + # Check for the header + self.assertContains(response, "Are you sure you want to place this domain on hold?") + + # Check for some of its body + self.assertContains(response, "When a domain is on hold:") + + # Check for some of the button content + self.assertContains(response, "Yes, place hold") + + # Grab the form to submit + form = response.forms["domain_form"] + + # Submit the form + response = form.submit("_place_client_hold") + + # Follow the response + response = response.follow() + + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain.name) + self.assertContains(response, "Remove hold") + + # The modal should still exist + # Check for the header + self.assertContains(response, "Are you sure you want to place this domain on hold?") + + # Check for some of its body + self.assertContains(response, "When a domain is on hold:") + + # Check for some of the button content + self.assertContains(response, "Yes, place hold") + + # Web test has issues grabbing up to date data from the db, so we can test + # the returned view instead + self.assertContains(response, '
On hold
') diff --git a/src/registrar/tests/test_admin_request.py b/src/registrar/tests/test_admin_request.py new file mode 100644 index 000000000..7a7c75ffc --- /dev/null +++ b/src/registrar/tests/test_admin_request.py @@ -0,0 +1,2064 @@ +from datetime import datetime +from django.utils import timezone +import re +from django.test import RequestFactory, Client, TestCase, override_settings +from django.contrib.admin.sites import AdminSite +from contextlib import ExitStack +from api.tests.common import less_console_noise_decorator +from django.contrib import messages +from django.urls import reverse +from registrar.admin import ( + DomainRequestAdmin, + DomainRequestAdminForm, + MyUserAdmin, + AuditedAdmin, +) +from registrar.models import ( + Domain, + DomainRequest, + DomainInformation, + DraftDomain, + User, + Contact, + Website, + SeniorOfficial, +) +from .common import ( + MockSESClient, + completed_domain_request, + generic_domain_object, + less_console_noise, + create_superuser, + create_user, + multiple_unalphabetical_domain_objects, + MockEppLib, + GenericTestHelper, +) +from unittest.mock import patch + +from django.conf import settings +import boto3_mocking # type: ignore +import logging + +logger = logging.getLogger(__name__) + + +@boto3_mocking.patching +class TestDomainRequestAdmin(MockEppLib): + """Test DomainRequestAdmin class as either staff or super user. + + Notes: + all tests share superuser/staffuser; do not change these models in tests + tests have available staffuser, superuser, client, admin and test_helper + """ + + @classmethod + def setUpClass(self): + super().setUpClass() + self.site = AdminSite() + self.factory = RequestFactory() + self.admin = DomainRequestAdmin(model=DomainRequest, admin_site=self.site) + self.superuser = create_superuser() + self.staffuser = create_user() + self.client = Client(HTTP_HOST="localhost:8080") + self.test_helper = GenericTestHelper( + factory=self.factory, + user=self.superuser, + admin=self.admin, + url="/admin/registrar/domainrequest/", + model=DomainRequest, + ) + self.mock_client = MockSESClient() + + def tearDown(self): + super().tearDown() + Domain.objects.all().delete() + DomainInformation.objects.all().delete() + DomainRequest.objects.all().delete() + Contact.objects.all().delete() + Website.objects.all().delete() + SeniorOfficial.objects.all().delete() + self.mock_client.EMAILS_SENT.clear() + + @classmethod + def tearDownClass(self): + super().tearDownClass() + User.objects.all().delete() + + @less_console_noise_decorator + def test_domain_request_senior_official_is_alphabetically_sorted(self): + """Tests if the senior offical dropdown is alphanetically sorted in the django admin display""" + + SeniorOfficial.objects.get_or_create(first_name="mary", last_name="joe", title="some other guy") + SeniorOfficial.objects.get_or_create(first_name="alex", last_name="smoe", title="some guy") + SeniorOfficial.objects.get_or_create(first_name="Zoup", last_name="Soup", title="title") + + contact, _ = Contact.objects.get_or_create(first_name="Henry", last_name="McFakerson") + domain_request = completed_domain_request(submitter=contact, name="city1.gov") + request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk)) + model_admin = AuditedAdmin(DomainRequest, self.site) + + # Get the queryset that would be returned for the list + senior_offical_queryset = model_admin.formfield_for_foreignkey( + DomainInformation.senior_official.field, request + ).queryset + + # Make the list we're comparing on a bit prettier display-wise. Optional step. + current_sort_order = [] + for official in senior_offical_queryset: + current_sort_order.append(f"{official.first_name} {official.last_name}") + + expected_sort_order = ["alex smoe", "mary joe", "Zoup Soup"] + + self.assertEqual(current_sort_order, expected_sort_order) + + @less_console_noise_decorator + def test_has_model_description(self): + """Tests if this model has a model description on the table view""" + self.client.force_login(self.superuser) + response = self.client.get( + "/admin/registrar/domainrequest/", + follow=True, + ) + + # Make sure that the page is loaded correctly + self.assertEqual(response.status_code, 200) + + # Test for a description snippet + self.assertContains(response, "This table contains all domain requests") + self.assertContains(response, "Show more") + + @less_console_noise_decorator + def test_helper_text(self): + """ + Tests for the correct helper text on this page + """ + + # Create a fake domain request and domain + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + self.client.force_login(self.superuser) + response = self.client.get( + "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain_request.requested_domain.name) + + # These should exist in the response + expected_values = [ + ("creator", "Person who submitted the domain request; will not receive email updates"), + ( + "submitter", + 'Person listed under "your contact information" in the request form; will receive email updates', + ), + ("approved_domain", "Domain associated with this request; will be blank until request is approved"), + ("no_other_contacts_rationale", "Required if creator does not list other employees"), + ("alternative_domains", "Other domain names the creator provided for consideration"), + ("no_other_contacts_rationale", "Required if creator does not list other employees"), + ("Urbanization", "Required for Puerto Rico only"), + ] + self.test_helper.assert_response_contains_distinct_values(response, expected_values) + + @less_console_noise_decorator + def test_status_logs(self): + """ + Tests that the status changes are shown in a table on the domain request change form, + accurately and in chronological order. + """ + + def assert_status_count(normalized_content, status, count): + """Helper function to assert the count of a status in the HTML content.""" + self.assertEqual(normalized_content.count(f" {status} "), count) + + def assert_status_order(normalized_content, statuses): + """Helper function to assert the order of statuses in the HTML content.""" + start_index = 0 + for status in statuses: + index = normalized_content.find(f" {status} ", start_index) + self.assertNotEqual(index, -1, f"Status '{status}' not found in the expected order.") + start_index = index + len(status) + + # Create a fake domain request and domain + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.STARTED) + + self.client.force_login(self.superuser) + response = self.client.get( + "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain_request.requested_domain.name) + + domain_request.submit() + domain_request.save() + + domain_request.in_review() + domain_request.save() + + domain_request.action_needed() + domain_request.action_needed_reason = DomainRequest.ActionNeededReasons.ALREADY_HAS_DOMAINS + domain_request.save() + + # Let's just change the action needed reason + domain_request.action_needed_reason = DomainRequest.ActionNeededReasons.ELIGIBILITY_UNCLEAR + domain_request.save() + + domain_request.reject() + domain_request.rejection_reason = DomainRequest.RejectionReasons.DOMAIN_PURPOSE + domain_request.save() + + domain_request.in_review() + domain_request.save() + + response = self.client.get( + "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), + follow=True, + ) + + # Normalize the HTML response content + normalized_content = " ".join(response.content.decode("utf-8").split()) + + # Define the expected sequence of status changes + expected_status_changes = [ + "In review", + "Rejected - Purpose requirements not met", + "Action needed - Unclear organization eligibility", + "Action needed - Already has domains", + "In review", + "Submitted", + "Started", + ] + + assert_status_order(normalized_content, expected_status_changes) + + assert_status_count(normalized_content, "Started", 1) + assert_status_count(normalized_content, "Submitted", 1) + assert_status_count(normalized_content, "In review", 2) + assert_status_count(normalized_content, "Action needed - Already has domains", 1) + assert_status_count(normalized_content, "Action needed - Unclear organization eligibility", 1) + assert_status_count(normalized_content, "Rejected - Purpose requirements not met", 1) + + @less_console_noise_decorator + def test_collaspe_toggle_button_markup(self): + """ + Tests for the correct collapse toggle button markup + """ + + # Create a fake domain request and domain + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + self.client.force_login(self.superuser) + response = self.client.get( + "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain_request.requested_domain.name) + self.assertContains(response, "Show details") + + @less_console_noise_decorator + def test_analyst_can_see_and_edit_alternative_domain(self): + """Tests if an analyst can still see and edit the alternative domain field""" + + # Create fake creator + _creator = User.objects.create( + username="MrMeoward", + first_name="Meoward", + last_name="Jones", + ) + + # Create a fake domain request + _domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator) + + fake_website = Website.objects.create(website="thisisatest.gov") + _domain_request.alternative_domains.add(fake_website) + _domain_request.save() + + self.client.force_login(self.staffuser) + response = self.client.get( + "/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, _domain_request.requested_domain.name) + + # Test if the page has the alternative domain + self.assertContains(response, "thisisatest.gov") + + # Check that the page contains the url we expect + expected_href = reverse("admin:registrar_website_change", args=[fake_website.id]) + self.assertContains(response, expected_href) + + # Navigate to the website to ensure that we can still edit it + response = self.client.get( + "/admin/registrar/website/{}/change/".format(fake_website.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, "thisisatest.gov") + + # clean up objects in this test + fake_website.delete() + _domain_request.delete() + _creator.delete() + + @less_console_noise_decorator + def test_analyst_can_see_and_edit_requested_domain(self): + """Tests if an analyst can still see and edit the requested domain field""" + + # Create fake creator + _creator = User.objects.create( + username="MrMeoward", + first_name="Meoward", + last_name="Jones", + ) + + # Create a fake domain request + _domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator) + + self.client.force_login(self.staffuser) + response = self.client.get( + "/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk), + follow=True, + ) + + # Filter to get the latest from the DB (rather than direct assignment) + requested_domain = DraftDomain.objects.filter(name=_domain_request.requested_domain.name).get() + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, requested_domain.name) + + # Check that the page contains the url we expect + expected_href = reverse("admin:registrar_draftdomain_change", args=[requested_domain.id]) + self.assertContains(response, expected_href) + + # Navigate to the website to ensure that we can still edit it + response = self.client.get( + "/admin/registrar/draftdomain/{}/change/".format(requested_domain.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, "city.gov") + + # clean up objects in this test + _domain_request.delete() + requested_domain.delete() + _creator.delete() + + @less_console_noise_decorator + def test_analyst_can_see_current_websites(self): + """Tests if an analyst can still see current website field""" + + # Create fake creator + _creator = User.objects.create( + username="MrMeoward", + first_name="Meoward", + last_name="Jones", + ) + + # Create a fake domain request + _domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator) + + fake_website = Website.objects.create(website="thisisatest.gov") + _domain_request.current_websites.add(fake_website) + _domain_request.save() + + self.client.force_login(self.staffuser) + response = self.client.get( + "/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, _domain_request.requested_domain.name) + + # Test if the page has the current website + self.assertContains(response, "thisisatest.gov") + + # clean up objects in this test + fake_website.delete() + _domain_request.delete() + _creator.delete() + + @less_console_noise_decorator + def test_domain_sortable(self): + """Tests if the DomainRequest sorts by domain correctly""" + self.client.force_login(self.superuser) + + multiple_unalphabetical_domain_objects("domain_request") + + # Assert that our sort works correctly + self.test_helper.assert_table_sorted("1", ("requested_domain__name",)) + + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted("-1", ("-requested_domain__name",)) + + @less_console_noise_decorator + def test_submitter_sortable(self): + """Tests if the DomainRequest sorts by submitter correctly""" + self.client.force_login(self.superuser) + + multiple_unalphabetical_domain_objects("domain_request") + + additional_domain_request = generic_domain_object("domain_request", "Xylophone") + new_user = User.objects.filter(username=additional_domain_request.investigator.username).get() + new_user.first_name = "Xylophonic" + new_user.save() + + # Assert that our sort works correctly + self.test_helper.assert_table_sorted( + "11", + ( + "submitter__first_name", + "submitter__last_name", + ), + ) + + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted( + "-11", + ( + "-submitter__first_name", + "-submitter__last_name", + ), + ) + + # clean up objects in this test + new_user.delete() + + @less_console_noise_decorator + def test_investigator_sortable(self): + """Tests if the DomainRequest sorts by investigator correctly""" + self.client.force_login(self.superuser) + + multiple_unalphabetical_domain_objects("domain_request") + additional_domain_request = generic_domain_object("domain_request", "Xylophone") + new_user = User.objects.filter(username=additional_domain_request.investigator.username).get() + new_user.first_name = "Xylophonic" + new_user.save() + + # Assert that our sort works correctly + self.test_helper.assert_table_sorted( + "12", + ( + "investigator__first_name", + "investigator__last_name", + ), + ) + + # Assert that sorting in reverse works correctly + self.test_helper.assert_table_sorted( + "-12", + ( + "-investigator__first_name", + "-investigator__last_name", + ), + ) + + # clean up objects in this test + new_user.delete() + + @less_console_noise_decorator + def test_default_sorting_in_domain_requests_list(self): + """ + Make sure the default sortin in on the domain requests list page is reverse submission_date + then alphabetical requested_domain + """ + + # Create domain requests with different names + domain_requests = [ + completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, name=name) + for name in ["ccc.gov", "bbb.gov", "eee.gov", "aaa.gov", "zzz.gov", "ddd.gov"] + ] + + domain_requests[0].submission_date = timezone.make_aware(datetime(2024, 10, 16)) + domain_requests[1].submission_date = timezone.make_aware(datetime(2001, 10, 16)) + domain_requests[2].submission_date = timezone.make_aware(datetime(1980, 10, 16)) + domain_requests[3].submission_date = timezone.make_aware(datetime(1998, 10, 16)) + domain_requests[4].submission_date = timezone.make_aware(datetime(2013, 10, 16)) + domain_requests[5].submission_date = timezone.make_aware(datetime(1980, 10, 16)) + + # Save the modified domain requests to update their attributes in the database + for domain_request in domain_requests: + domain_request.save() + + # Refresh domain request objects from the database to reflect the changes + domain_requests = [DomainRequest.objects.get(pk=domain_request.pk) for domain_request in domain_requests] + + # Login as superuser and retrieve the domain request list page + self.client.force_login(self.superuser) + response = self.client.get("/admin/registrar/domainrequest/") + + # Check that the response is successful + self.assertEqual(response.status_code, 200) + + # Extract the domain names from the response content using regex + domain_names_match = re.findall(r"(\w+\.gov)", response.content.decode("utf-8")) + + logger.info(f"domain_names_match {domain_names_match}") + + # Verify that domain names are found + self.assertTrue(domain_names_match) + + # Extract the domain names + domain_names = [match for match in domain_names_match] + + # Verify that the domain names are displayed in the expected order + expected_order = [ + "ccc.gov", + "zzz.gov", + "bbb.gov", + "aaa.gov", + "ddd.gov", + "eee.gov", + ] + + # Remove duplicates + # Remove duplicates from domain_names list while preserving order + unique_domain_names = [] + for domain_name in domain_names: + if domain_name not in unique_domain_names: + unique_domain_names.append(domain_name) + + self.assertEqual(unique_domain_names, expected_order) + + @less_console_noise_decorator + def test_short_org_name_in_domain_requests_list(self): + """ + Make sure the short name is displaying in admin on the list page + """ + self.client.force_login(self.superuser) + completed_domain_request() + response = self.client.get("/admin/registrar/domainrequest/?generic_org_type__exact=federal") + # There are 2 template references to Federal (4) and two in the results data + # of the request + self.assertContains(response, "Federal", count=52) + # This may be a bit more robust + self.assertContains(response, 'Federal', count=1) + # Now let's make sure the long description does not exist + self.assertNotContains(response, "Federal: an agency of the U.S. government") + + @less_console_noise_decorator + def test_default_status_in_domain_requests_list(self): + """ + Make sure the default status in admin is selected on the domain requests list page + """ + self.client.force_login(self.superuser) + completed_domain_request() + response = self.client.get("/admin/registrar/domainrequest/") + # The results are filtered by "status in [submitted,in review,action needed]" + self.assertContains(response, "status in [submitted,in review,action needed]", count=1) + + @less_console_noise_decorator + def transition_state_and_send_email( + self, domain_request, status, rejection_reason=None, action_needed_reason=None, action_needed_reason_email=None + ): + """Helper method for the email test cases.""" + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + # Create a mock request + request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk)) + + # Create a fake session to hook to + request.session = {} + + # Modify the domain request's properties + domain_request.status = status + + if rejection_reason: + domain_request.rejection_reason = rejection_reason + + if action_needed_reason: + domain_request.action_needed_reason = action_needed_reason + + if action_needed_reason_email: + domain_request.action_needed_reason_email = action_needed_reason_email + + # Use the model admin's save_model method + self.admin.save_model(request, domain_request, form=None, change=True) + + def assert_email_is_accurate( + self, expected_string, email_index, email_address, test_that_no_bcc=False, bcc_email_address="" + ): + """Helper method for the email test cases. + email_index is the index of the email in mock_client.""" + + with less_console_noise(): + # Access the arguments passed to send_email + call_args = self.mock_client.EMAILS_SENT + kwargs = call_args[email_index]["kwargs"] + + # Retrieve the email details from the arguments + from_email = kwargs.get("FromEmailAddress") + to_email = kwargs["Destination"]["ToAddresses"][0] + email_content = kwargs["Content"] + email_body = email_content["Simple"]["Body"]["Text"]["Data"] + + # Assert or perform other checks on the email details + self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL) + self.assertEqual(to_email, email_address) + self.assertIn(expected_string, email_body) + + if test_that_no_bcc: + _ = "" + with self.assertRaises(KeyError): + with less_console_noise(): + _ = kwargs["Destination"]["BccAddresses"][0] + self.assertEqual(_, "") + + if bcc_email_address: + bcc_email = kwargs["Destination"]["BccAddresses"][0] + self.assertEqual(bcc_email, bcc_email_address) + + @override_settings(IS_PRODUCTION=True) + @less_console_noise_decorator + def test_action_needed_sends_reason_email_prod_bcc(self): + """When an action needed reason is set, an email is sent out and help@get.gov + is BCC'd in production""" + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + BCC_EMAIL = settings.DEFAULT_FROM_EMAIL + User.objects.filter(email=EMAIL).delete() + in_review = DomainRequest.DomainRequestStatus.IN_REVIEW + action_needed = DomainRequest.DomainRequestStatus.ACTION_NEEDED + + # Create a sample domain request + domain_request = completed_domain_request(status=in_review) + + # Test the email sent out for already_has_domains + already_has_domains = DomainRequest.ActionNeededReasons.ALREADY_HAS_DOMAINS + self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=already_has_domains) + + self.assert_email_is_accurate("ORGANIZATION ALREADY HAS A .GOV DOMAIN", 0, EMAIL, bcc_email_address=BCC_EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Test the email sent out for bad_name + bad_name = DomainRequest.ActionNeededReasons.BAD_NAME + self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=bad_name) + self.assert_email_is_accurate( + "DOMAIN NAME DOES NOT MEET .GOV REQUIREMENTS", 1, EMAIL, bcc_email_address=BCC_EMAIL + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + # Test the email sent out for eligibility_unclear + eligibility_unclear = DomainRequest.ActionNeededReasons.ELIGIBILITY_UNCLEAR + self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=eligibility_unclear) + self.assert_email_is_accurate( + "ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS", 2, EMAIL, bcc_email_address=BCC_EMAIL + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Test that a custom email is sent out for questionable_so + questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL + self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=questionable_so) + self.assert_email_is_accurate( + "SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", 3, EMAIL, bcc_email_address=BCC_EMAIL + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 4) + + # Assert that no other emails are sent on OTHER + other = DomainRequest.ActionNeededReasons.OTHER + self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=other) + + # Should be unchanged from before + self.assertEqual(len(self.mock_client.EMAILS_SENT), 4) + + # Tests if an analyst can override existing email content + questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL + self.transition_state_and_send_email( + domain_request, + action_needed, + action_needed_reason=questionable_so, + action_needed_reason_email="custom email content", + ) + + domain_request.refresh_from_db() + self.assert_email_is_accurate("custom email content", 4, EMAIL, bcc_email_address=BCC_EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 5) + + # Tests if a new email gets sent when just the email is changed. + # An email should NOT be sent out if we just modify the email content. + self.transition_state_and_send_email( + domain_request, + action_needed, + action_needed_reason=questionable_so, + action_needed_reason_email="dummy email content", + ) + + self.assertEqual(len(self.mock_client.EMAILS_SENT), 5) + + # Set the request back to in review + domain_request.in_review() + + # Try sending another email when changing states AND including content + self.transition_state_and_send_email( + domain_request, + action_needed, + action_needed_reason=eligibility_unclear, + action_needed_reason_email="custom content when starting anew", + ) + self.assert_email_is_accurate("custom content when starting anew", 5, EMAIL, bcc_email_address=BCC_EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 6) + + # def test_action_needed_sends_reason_email_prod_bcc(self): + # """When an action needed reason is set, an email is sent out and help@get.gov + # is BCC'd in production""" + # # Ensure there is no user with this email + # EMAIL = "mayor@igorville.gov" + # BCC_EMAIL = settings.DEFAULT_FROM_EMAIL + # User.objects.filter(email=EMAIL).delete() + # in_review = DomainRequest.DomainRequestStatus.IN_REVIEW + # action_needed = DomainRequest.DomainRequestStatus.ACTION_NEEDED + + # # Create a sample domain request + # domain_request = completed_domain_request(status=in_review) + + # # Test the email sent out for already_has_domains + # already_has_domains = DomainRequest.ActionNeededReasons.ALREADY_HAS_DOMAINS + # self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=already_has_domains) + # self.assert_email_is_accurate("ORGANIZATION ALREADY HAS A .GOV DOMAIN", 0, EMAIL, bcc_email_address=BCC_EMAIL) + # self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # # Test the email sent out for bad_name + # bad_name = DomainRequest.ActionNeededReasons.BAD_NAME + # self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=bad_name) + # self.assert_email_is_accurate( + # "DOMAIN NAME DOES NOT MEET .GOV REQUIREMENTS", 1, EMAIL, bcc_email_address=BCC_EMAIL + # ) + # self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + # # Test the email sent out for eligibility_unclear + # eligibility_unclear = DomainRequest.ActionNeededReasons.ELIGIBILITY_UNCLEAR + # self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=eligibility_unclear) + # self.assert_email_is_accurate( + # "ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS", 2, EMAIL, bcc_email_address=BCC_EMAIL + # ) + # self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # # Test the email sent out for questionable_so + # questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL + # self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=questionable_so) + # self.assert_email_is_accurate( + # "SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", 3, EMAIL, bcc_email_address=BCC_EMAIL + # ) + # self.assertEqual(len(self.mock_client.EMAILS_SENT), 4) + + # # Assert that no other emails are sent on OTHER + # other = DomainRequest.ActionNeededReasons.OTHER + # self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=other) + + # # Should be unchanged from before + # self.assertEqual(len(self.mock_client.EMAILS_SENT), 4) + + @less_console_noise_decorator + def test_save_model_sends_submitted_email(self): + """When transitioning to submitted from started or withdrawn on a domain request, + an email is sent out. + + When transitioning to submitted from dns needed or in review on a domain request, + no email is sent out. + + Also test that the default email set in settings is NOT BCCd on non-prod whenever + an email does go out.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample domain request + domain_request = completed_domain_request() + + # Test Submitted Status from started + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED) + self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, True) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Test Withdrawn Status + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN) + self.assert_email_is_accurate( + "Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL, True + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + # Test Submitted Status Again (from withdrawn) + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Move it to IN_REVIEW + other = DomainRequest.ActionNeededReasons.OTHER + in_review = DomainRequest.DomainRequestStatus.IN_REVIEW + self.transition_state_and_send_email(domain_request, in_review, action_needed_reason=other) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Test Submitted Status Again from in IN_REVIEW, no new email should be sent + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Move it to IN_REVIEW + self.transition_state_and_send_email(domain_request, in_review, action_needed_reason=other) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Move it to ACTION_NEEDED + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.ACTION_NEEDED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Test Submitted Status Again from in ACTION_NEEDED, no new email should be sent + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + @less_console_noise_decorator + def test_model_displays_action_needed_email(self): + """Tests if the action needed email is visible for Domain Requests""" + + _domain_request = completed_domain_request( + status=DomainRequest.DomainRequestStatus.ACTION_NEEDED, + action_needed_reason=DomainRequest.ActionNeededReasons.BAD_NAME, + ) + + self.client.force_login(self.staffuser) + response = self.client.get( + "/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk), + follow=True, + ) + + self.assertContains(response, "DOMAIN NAME DOES NOT MEET .GOV REQUIREMENTS") + + @override_settings(IS_PRODUCTION=True) + @less_console_noise_decorator + def test_save_model_sends_submitted_email_with_bcc_on_prod(self): + """When transitioning to submitted from started or withdrawn on a domain request, + an email is sent out. + + When transitioning to submitted from dns needed or in review on a domain request, + no email is sent out. + + Also test that the default email set in settings IS BCCd on prod whenever + an email does go out.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + BCC_EMAIL = settings.DEFAULT_FROM_EMAIL + + # Create a sample domain request + domain_request = completed_domain_request() + + # Test Submitted Status from started + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED) + self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, False, BCC_EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Test Withdrawn Status + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN) + self.assert_email_is_accurate( + "Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + # Test Submitted Status Again (from withdrawn) + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED) + self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, False, BCC_EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Move it to IN_REVIEW + other = domain_request.ActionNeededReasons.OTHER + in_review = DomainRequest.DomainRequestStatus.IN_REVIEW + self.transition_state_and_send_email(domain_request, in_review, action_needed_reason=other) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Test Submitted Status Again from in IN_REVIEW, no new email should be sent + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Move it to IN_REVIEW + self.transition_state_and_send_email(domain_request, in_review, action_needed_reason=other) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Move it to ACTION_NEEDED + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.ACTION_NEEDED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + # Test Submitted Status Again from in ACTION_NEEDED, no new email should be sent + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + @less_console_noise_decorator + def test_save_model_sends_approved_email(self): + """When transitioning to approved on a domain request, + an email is sent out every time.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + # Test Submitted Status + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 0, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Test Withdrawn Status + self.transition_state_and_send_email( + domain_request, + DomainRequest.DomainRequestStatus.REJECTED, + DomainRequest.RejectionReasons.DOMAIN_PURPOSE, + ) + self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + # Test Submitted Status Again (No new email should be sent) + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + @less_console_noise_decorator + def test_save_model_sends_rejected_email_purpose_not_met(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is domain purpose.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + # Reject for reason DOMAIN_PURPOSE and test email + self.transition_state_and_send_email( + domain_request, + DomainRequest.DomainRequestStatus.REJECTED, + DomainRequest.RejectionReasons.DOMAIN_PURPOSE, + ) + self.assert_email_is_accurate( + "Your domain request was rejected because the purpose you provided did not meet our \nrequirements.", + 0, + EMAIL, + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + @less_console_noise_decorator + def test_save_model_sends_rejected_email_requestor(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is requestor.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + # Reject for reason REQUESTOR and test email including dynamic organization name + self.transition_state_and_send_email( + domain_request, DomainRequest.DomainRequestStatus.REJECTED, DomainRequest.RejectionReasons.REQUESTOR + ) + self.assert_email_is_accurate( + "Your domain request was rejected because we don’t believe you’re eligible to request a \n.gov " + "domain on behalf of Testorg", + 0, + EMAIL, + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + @less_console_noise_decorator + def test_save_model_sends_rejected_email_org_has_domain(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is second domain.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + # Reject for reason SECOND_DOMAIN_REASONING and test email including dynamic organization name + self.transition_state_and_send_email( + domain_request, + DomainRequest.DomainRequestStatus.REJECTED, + DomainRequest.RejectionReasons.SECOND_DOMAIN_REASONING, + ) + self.assert_email_is_accurate("Your domain request was rejected because Testorg has a .gov domain.", 0, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + @less_console_noise_decorator + def test_save_model_sends_rejected_email_contacts_or_org_legitimacy(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is contacts or org legitimacy.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + # Reject for reason CONTACTS_OR_ORGANIZATION_LEGITIMACY and test email including dynamic organization name + self.transition_state_and_send_email( + domain_request, + DomainRequest.DomainRequestStatus.REJECTED, + DomainRequest.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY, + ) + self.assert_email_is_accurate( + "Your domain request was rejected because we could not verify the organizational \n" + "contacts you provided. If you have questions or comments, reply to this email.", + 0, + EMAIL, + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + @less_console_noise_decorator + def test_save_model_sends_rejected_email_org_eligibility(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is org eligibility.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + # Reject for reason ORGANIZATION_ELIGIBILITY and test email including dynamic organization name + self.transition_state_and_send_email( + domain_request, + DomainRequest.DomainRequestStatus.REJECTED, + DomainRequest.RejectionReasons.ORGANIZATION_ELIGIBILITY, + ) + self.assert_email_is_accurate( + "Your domain request was rejected because we determined that Testorg is not \neligible for " + "a .gov domain.", + 0, + EMAIL, + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + @less_console_noise_decorator + def test_save_model_sends_rejected_email_naming(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is naming.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + # Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name + self.transition_state_and_send_email( + domain_request, + DomainRequest.DomainRequestStatus.REJECTED, + DomainRequest.RejectionReasons.NAMING_REQUIREMENTS, + ) + self.assert_email_is_accurate( + "Your domain request was rejected because it does not meet our naming requirements.", 0, EMAIL + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + @less_console_noise_decorator + def test_save_model_sends_rejected_email_other(self): + """When transitioning to rejected on a domain request, an email is sent + explaining why when the reason is other.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + # Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name + self.transition_state_and_send_email( + domain_request, + DomainRequest.DomainRequestStatus.REJECTED, + DomainRequest.RejectionReasons.OTHER, + ) + self.assert_email_is_accurate("Choosing a .gov domain name", 0, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Approve + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED) + self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + @less_console_noise_decorator + def test_transition_to_rejected_without_rejection_reason_does_trigger_error(self): + """ + When transitioning to rejected without a rejection reason, admin throws a user friendly message. + + The transition fails. + """ + + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.APPROVED) + + # Create a request object with a superuser + request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk)) + request.user = self.superuser + + with ExitStack() as stack: + stack.enter_context(patch.object(messages, "error")) + domain_request.status = DomainRequest.DomainRequestStatus.REJECTED + + self.admin.save_model(request, domain_request, None, True) + + messages.error.assert_called_once_with( + request, + "A reason is required for this status.", + ) + + domain_request.refresh_from_db() + self.assertEqual(domain_request.status, DomainRequest.DomainRequestStatus.APPROVED) + + @less_console_noise_decorator + def test_transition_to_rejected_with_rejection_reason_does_not_trigger_error(self): + """ + When transitioning to rejected with a rejection reason, admin does not throw an error alert. + + The transition is successful. + """ + + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.APPROVED) + + # Create a request object with a superuser + request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk)) + request.user = self.superuser + + with ExitStack() as stack: + stack.enter_context(patch.object(messages, "error")) + domain_request.status = DomainRequest.DomainRequestStatus.REJECTED + domain_request.rejection_reason = DomainRequest.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY + + self.admin.save_model(request, domain_request, None, True) + + messages.error.assert_not_called() + + domain_request.refresh_from_db() + self.assertEqual(domain_request.status, DomainRequest.DomainRequestStatus.REJECTED) + + @less_console_noise_decorator + def test_save_model_sends_withdrawn_email(self): + """When transitioning to withdrawn on a domain request, + an email is sent out every time.""" + + # Ensure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + # Test Submitted Status + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN) + self.assert_email_is_accurate( + "Your .gov domain request has been withdrawn and will not be reviewed by our team.", 0, EMAIL + ) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) + + # Test Withdrawn Status + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED) + self.assert_email_is_accurate("We received your .gov domain request.", 1, EMAIL) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 2) + + # Test Submitted Status Again (No new email should be sent) + self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN) + self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) + + @less_console_noise_decorator + def test_save_model_sets_approved_domain(self): + # make sure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + # Create a mock request + request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk)) + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + # Modify the domain request's property + domain_request.status = DomainRequest.DomainRequestStatus.APPROVED + + # Use the model admin's save_model method + self.admin.save_model(request, domain_request, form=None, change=True) + + # Test that approved domain exists and equals requested domain + self.assertEqual(domain_request.requested_domain.name, domain_request.approved_domain.name) + + @less_console_noise_decorator + def test_sticky_submit_row(self): + """Test that the change_form template contains strings indicative of the customization + of the sticky submit bar. + + Also test that it does NOT contain a CSS class meant for analysts only when logged in as superuser.""" + + # make sure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + self.client.force_login(self.superuser) + + # Create a sample domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + # Create a mock request + request = self.client.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk)) + + # Since we're using client to mock the request, we can only test against + # non-interpolated values + expected_content = "Requested domain:" + expected_content2 = '' + expected_content3 = '
' + not_expected_content = "submit-row-wrapper--analyst-view>" + self.assertContains(request, expected_content) + self.assertContains(request, expected_content2) + self.assertContains(request, expected_content3) + self.assertNotContains(request, not_expected_content) + + @less_console_noise_decorator + def test_sticky_submit_row_has_extra_class_for_analysts(self): + """Test that the change_form template contains strings indicative of the customization + of the sticky submit bar. + + Also test that it DOES contain a CSS class meant for analysts only when logged in as analyst.""" + + # make sure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + self.client.force_login(self.staffuser) + + # Create a sample domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + # Create a mock request + request = self.client.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk)) + + # Since we're using client to mock the request, we can only test against + # non-interpolated values + expected_content = "Requested domain:" + expected_content2 = '' + expected_content3 = '
' + self.assertContains(request, expected_content) + self.assertContains(request, expected_content2) + self.assertContains(request, expected_content3) + + @less_console_noise_decorator + def test_other_contacts_has_readonly_link(self): + """Tests if the readonly other_contacts field has links""" + + # Create a fake domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + # Get the other contact + other_contact = domain_request.other_contacts.all().first() + + self.client.force_login(self.staffuser) + response = self.client.get( + "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain_request.requested_domain.name) + + # Check that the page contains the url we expect + expected_href = reverse("admin:registrar_contact_change", args=[other_contact.id]) + self.assertContains(response, expected_href) + + # Check that the page contains the link we expect. + # Since the url is dynamic (populated by JS), we can test for its existence + # by checking for the end tag. + expected_url = "Testy Tester" + self.assertContains(response, expected_url) + + @less_console_noise_decorator + def test_other_websites_has_readonly_link(self): + """Tests if the readonly other_websites field has links""" + + # Create a fake domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + self.client.force_login(self.staffuser) + response = self.client.get( + "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain_request.requested_domain.name) + + # Check that the page contains the link we expect. + expected_url = 'city.com' + self.assertContains(response, expected_url) + + @less_console_noise_decorator + def test_contact_fields_have_detail_table(self): + """Tests if the contact fields have the detail table which displays title, email, and phone""" + + # Create fake creator + _creator = User.objects.create( + username="MrMeoward", + first_name="Meoward", + last_name="Jones", + email="meoward.jones@igorville.gov", + phone="(555) 123 12345", + title="Treat inspector", + ) + + # Create a fake domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator) + + self.client.force_login(self.staffuser) + response = self.client.get( + "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain_request.requested_domain.name) + + # == Check for the creator == # + + # Check for the right title, email, and phone number in the response. + expected_creator_fields = [ + # Field, expected value + ("title", "Treat inspector"), + ("phone", "(555) 123 12345"), + ] + self.test_helper.assert_response_contains_distinct_values(response, expected_creator_fields) + self.assertContains(response, "meoward.jones@igorville.gov") + + # Check for the field itself + self.assertContains(response, "Meoward Jones") + + # == Check for the submitter == # + self.assertContains(response, "mayor@igorville.gov", count=2) + expected_submitter_fields = [ + # Field, expected value + ("title", "Admin Tester"), + ("phone", "(555) 555 5556"), + ] + self.test_helper.assert_response_contains_distinct_values(response, expected_submitter_fields) + self.assertContains(response, "Testy2 Tester2") + + # == Check for the senior_official == # + self.assertContains(response, "testy@town.com", count=2) + expected_so_fields = [ + # Field, expected value + ("phone", "(555) 555 5555"), + ] + + self.test_helper.assert_response_contains_distinct_values(response, expected_so_fields) + self.assertContains(response, "Chief Tester") + + # == Test the other_employees field == # + self.assertContains(response, "testy2@town.com") + expected_other_employees_fields = [ + # Field, expected value + ("title", "Another Tester"), + ("phone", "(555) 555 5557"), + ] + self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields) + + # Test for the copy link + self.assertContains(response, "usa-button__clipboard", count=4) + + # Test that Creator counts display properly + self.assertNotContains(response, "Approved domains") + self.assertContains(response, "Active requests") + + # cleanup objects from this test + domain_request.delete() + _creator.delete() + + @less_console_noise_decorator + def test_save_model_sets_restricted_status_on_user(self): + # make sure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + # Create a mock request + request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), follow=True) + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + # Modify the domain request's property + domain_request.status = DomainRequest.DomainRequestStatus.INELIGIBLE + + # Use the model admin's save_model method + self.admin.save_model(request, domain_request, form=None, change=True) + + # Test that approved domain exists and equals requested domain + self.assertEqual(domain_request.creator.status, "restricted") + + @less_console_noise_decorator + def test_user_sets_restricted_status_modal(self): + """Tests the modal for when a user sets the status to restricted""" + # make sure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + # Create a sample domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + self.client.force_login(self.staffuser) + response = self.client.get( + "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), + follow=True, + ) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain_request.requested_domain.name) + + # Check that the modal has the right content + # Check for the header + self.assertContains(response, "Are you sure you want to select ineligible status?") + + # Check for some of its body + self.assertContains(response, "When a domain request is in ineligible status") + + # Check for some of the button content + self.assertContains(response, "Yes, select ineligible status") + + # Create a mock request + request = self.factory.post("/admin/registrar/domainrequest{}/change/".format(domain_request.pk), follow=True) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + # Modify the domain request's property + domain_request.status = DomainRequest.DomainRequestStatus.INELIGIBLE + + # Use the model admin's save_model method + self.admin.save_model(request, domain_request, form=None, change=True) + + # Test that approved domain exists and equals requested domain + self.assertEqual(domain_request.creator.status, "restricted") + + # 'Get' to the domain request again + response = self.client.get( + "/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), + follow=True, + ) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, domain_request.requested_domain.name) + + # The modal should be unchanged + self.assertContains(response, "Are you sure you want to select ineligible status?") + self.assertContains(response, "When a domain request is in ineligible status") + self.assertContains(response, "Yes, select ineligible status") + + @less_console_noise_decorator + def test_readonly_when_restricted_creator(self): + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + domain_request.creator.status = User.RESTRICTED + domain_request.creator.save() + + request = self.factory.get("/") + request.user = self.superuser + + readonly_fields = self.admin.get_readonly_fields(request, domain_request) + + expected_fields = [ + "other_contacts", + "current_websites", + "alternative_domains", + "is_election_board", + "status_history", + "id", + "created_at", + "updated_at", + "status", + "rejection_reason", + "action_needed_reason", + "action_needed_reason_email", + "federal_agency", + "portfolio", + "sub_organization", + "creator", + "investigator", + "generic_org_type", + "is_election_board", + "organization_type", + "federally_recognized_tribe", + "state_recognized_tribe", + "tribe_name", + "federal_type", + "organization_name", + "address_line1", + "address_line2", + "city", + "state_territory", + "zipcode", + "urbanization", + "about_your_organization", + "senior_official", + "approved_domain", + "requested_domain", + "submitter", + "purpose", + "no_other_contacts_rationale", + "anything_else", + "has_anything_else_text", + "cisa_representative_email", + "cisa_representative_first_name", + "cisa_representative_last_name", + "has_cisa_representative", + "is_policy_acknowledged", + "submission_date", + "notes", + "alternative_domains", + ] + self.maxDiff = None + self.assertEqual(readonly_fields, expected_fields) + + def test_readonly_fields_for_analyst(self): + with less_console_noise(): + request = self.factory.get("/") # Use the correct method and path + request.user = self.staffuser + + readonly_fields = self.admin.get_readonly_fields(request) + + expected_fields = [ + "other_contacts", + "current_websites", + "alternative_domains", + "is_election_board", + "status_history", + "federal_agency", + "creator", + "about_your_organization", + "requested_domain", + "approved_domain", + "alternative_domains", + "purpose", + "submitter", + "no_other_contacts_rationale", + "anything_else", + "is_policy_acknowledged", + "cisa_representative_first_name", + "cisa_representative_last_name", + "cisa_representative_email", + ] + self.assertEqual(readonly_fields, expected_fields) + + def test_readonly_fields_for_superuser(self): + with less_console_noise(): + request = self.factory.get("/") # Use the correct method and path + request.user = self.superuser + + readonly_fields = self.admin.get_readonly_fields(request) + + expected_fields = [ + "other_contacts", + "current_websites", + "alternative_domains", + "is_election_board", + "status_history", + ] + + self.assertEqual(readonly_fields, expected_fields) + + def test_saving_when_restricted_creator(self): + with less_console_noise(): + # Create an instance of the model + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + domain_request.creator.status = User.RESTRICTED + domain_request.creator.save() + + # Create a request object with a superuser + request = self.factory.get("/") + request.user = self.superuser + + with patch("django.contrib.messages.error") as mock_error: + # Simulate saving the model + self.admin.save_model(request, domain_request, None, False) + + # Assert that the error message was called with the correct argument + mock_error.assert_called_once_with( + request, + "This action is not permitted for domain requests with a restricted creator.", + ) + + # Assert that the status has not changed + self.assertEqual(domain_request.status, DomainRequest.DomainRequestStatus.IN_REVIEW) + + def test_change_view_with_restricted_creator(self): + with less_console_noise(): + # Create an instance of the model + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + domain_request.creator.status = User.RESTRICTED + domain_request.creator.save() + + with patch("django.contrib.messages.warning") as mock_warning: + # Create a request object with a superuser + request = self.factory.get("/admin/your_app/domainrequest/{}/change/".format(domain_request.pk)) + request.user = self.superuser + + self.admin.display_restricted_warning(request, domain_request) + + # Assert that the error message was called with the correct argument + mock_warning.assert_called_once_with( + request, + "Cannot edit a domain request with a restricted creator.", + ) + + def trigger_saving_approved_to_another_state(self, domain_is_active, another_state, rejection_reason=None): + """Helper method that triggers domain request state changes from approved to another state, + with an associated domain that can be either active (READY) or not. + + Used to test errors when saving a change with an active domain, also used to test side effects + when saving a change goes through.""" + + with less_console_noise(): + # Create an instance of the model + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.APPROVED) + domain = Domain.objects.create(name=domain_request.requested_domain.name) + domain_information = DomainInformation.objects.create(creator=self.superuser, domain=domain) + domain_request.approved_domain = domain + domain_request.save() + + # Create a request object with a superuser + request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk)) + request.user = self.superuser + + request.session = {} + + # Define a custom implementation for is_active + def custom_is_active(self): + return domain_is_active # Override to return True + + # Use ExitStack to combine patch contexts + with ExitStack() as stack: + # Patch Domain.is_active and django.contrib.messages.error simultaneously + stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) + stack.enter_context(patch.object(messages, "error")) + + domain_request.status = another_state + + if another_state == DomainRequest.DomainRequestStatus.ACTION_NEEDED: + domain_request.action_needed_reason = domain_request.ActionNeededReasons.OTHER + + domain_request.rejection_reason = rejection_reason + + self.admin.save_model(request, domain_request, None, True) + + # Assert that the error message was called with the correct argument + if domain_is_active: + messages.error.assert_called_once_with( + request, + "This action is not permitted. The domain " + "is already active.", + ) + else: + # Assert that the error message was never called + messages.error.assert_not_called() + + self.assertEqual(domain_request.approved_domain, None) + + # Assert that Domain got Deleted + with self.assertRaises(Domain.DoesNotExist): + domain.refresh_from_db() + + # Assert that DomainInformation got Deleted + with self.assertRaises(DomainInformation.DoesNotExist): + domain_information.refresh_from_db() + + def test_error_when_saving_approved_to_in_review_and_domain_is_active(self): + self.trigger_saving_approved_to_another_state(True, DomainRequest.DomainRequestStatus.IN_REVIEW) + + def test_error_when_saving_approved_to_action_needed_and_domain_is_active(self): + self.trigger_saving_approved_to_another_state(True, DomainRequest.DomainRequestStatus.ACTION_NEEDED) + + def test_error_when_saving_approved_to_rejected_and_domain_is_active(self): + self.trigger_saving_approved_to_another_state(True, DomainRequest.DomainRequestStatus.REJECTED) + + def test_error_when_saving_approved_to_ineligible_and_domain_is_active(self): + self.trigger_saving_approved_to_another_state(True, DomainRequest.DomainRequestStatus.INELIGIBLE) + + def test_side_effects_when_saving_approved_to_in_review(self): + self.trigger_saving_approved_to_another_state(False, DomainRequest.DomainRequestStatus.IN_REVIEW) + + def test_side_effects_when_saving_approved_to_action_needed(self): + self.trigger_saving_approved_to_another_state(False, DomainRequest.DomainRequestStatus.ACTION_NEEDED) + + def test_side_effects_when_saving_approved_to_rejected(self): + self.trigger_saving_approved_to_another_state( + False, + DomainRequest.DomainRequestStatus.REJECTED, + DomainRequest.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY, + ) + + def test_side_effects_when_saving_approved_to_ineligible(self): + self.trigger_saving_approved_to_another_state(False, DomainRequest.DomainRequestStatus.INELIGIBLE) + + def test_has_correct_filters(self): + """ + This test verifies that DomainRequestAdmin has the correct filters set up. + + It retrieves the current list of filters from DomainRequestAdmin + and checks that it matches the expected list of filters. + """ + with less_console_noise(): + request = self.factory.get("/") + request.user = self.superuser + + # Grab the current list of table filters + readonly_fields = self.admin.get_list_filter(request) + expected_fields = ( + DomainRequestAdmin.StatusListFilter, + "generic_org_type", + "federal_type", + DomainRequestAdmin.ElectionOfficeFilter, + "rejection_reason", + DomainRequestAdmin.InvestigatorFilter, + ) + + self.assertEqual(readonly_fields, expected_fields) + + def test_table_sorted_alphabetically(self): + """ + This test verifies that the DomainRequestAdmin table is sorted alphabetically + by the 'requested_domain__name' field. + + It creates a list of DomainRequest instances in a non-alphabetical order, + then retrieves the queryset from the DomainRequestAdmin and checks + that it matches the expected queryset, + which is sorted alphabetically by the 'requested_domain__name' field. + """ + with less_console_noise(): + # Creates a list of DomainRequests in scrambled order + multiple_unalphabetical_domain_objects("domain_request") + + request = self.factory.get("/") + request.user = self.superuser + + # Get the expected list of alphabetically sorted DomainRequests + expected_order = DomainRequest.objects.order_by("requested_domain__name") + + # Get the returned queryset + queryset = self.admin.get_queryset(request) + + # Check the order + self.assertEqual( + list(queryset), + list(expected_order), + ) + + def test_displays_investigator_filter(self): + """ + This test verifies that the investigator filter in the admin interface for + the DomainRequest model displays correctly. + + It creates two DomainRequest instances, each with a different investigator. + It then simulates a staff user logging in and applying the investigator filter + on the DomainRequest admin page. + + We then test if the page displays the filter we expect, but we do not test + if we get back the correct response in the table. This is to isolate if + the filter displays correctly, when the filter isn't filtering correctly. + """ + + with less_console_noise(): + # Create a mock DomainRequest object, with a fake investigator + domain_request: DomainRequest = generic_domain_object("domain_request", "SomeGuy") + investigator_user = User.objects.filter(username=domain_request.investigator.username).get() + investigator_user.is_staff = True + investigator_user.save() + + self.client.force_login(self.staffuser) + response = self.client.get( + "/admin/registrar/domainrequest/", + { + "investigator__id__exact": investigator_user.id, + }, + follow=True, + ) + + # Then, test if the filter actually exists + self.assertIn("filters", response.context) + + # Assert the content of filters and search_query + filters = response.context["filters"] + + self.assertEqual( + filters, + [ + { + "parameter_name": "investigator", + "parameter_value": "SomeGuy first_name:investigator SomeGuy last_name:investigator", + }, + ], + ) + + def test_investigator_dropdown_displays_only_staff(self): + """ + This test verifies that the dropdown for the 'investigator' field in the DomainRequestAdmin + interface only displays users who are marked as staff. + + It creates two DomainRequest instances, one with an investigator + who is a staff user and another with an investigator who is not a staff user. + + It then retrieves the queryset for the 'investigator' dropdown from DomainRequestAdmin + and checks that it matches the expected queryset, which only includes staff users. + """ + + with less_console_noise(): + # Create a mock DomainRequest object, with a fake investigator + domain_request: DomainRequest = generic_domain_object("domain_request", "SomeGuy") + investigator_user = User.objects.filter(username=domain_request.investigator.username).get() + investigator_user.is_staff = True + investigator_user.save() + + # Create a mock DomainRequest object, with a user that is not staff + domain_request_2: DomainRequest = generic_domain_object("domain_request", "SomeOtherGuy") + investigator_user_2 = User.objects.filter(username=domain_request_2.investigator.username).get() + investigator_user_2.is_staff = False + investigator_user_2.save() + + self.client.force_login(self.staffuser) + + request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk)) + + # Get the actual field from the model's meta information + investigator_field = DomainRequest._meta.get_field("investigator") + + # We should only be displaying staff users, in alphabetical order + sorted_fields = ["first_name", "last_name", "email"] + expected_dropdown = list(User.objects.filter(is_staff=True).order_by(*sorted_fields)) + + # Grab the current dropdown. We do an API call to autocomplete to get this info. + domain_request_queryset = self.admin.formfield_for_foreignkey(investigator_field, request).queryset + user_request = self.factory.post( + "/admin/autocomplete/?app_label=registrar&model_name=domainrequest&field_name=investigator" + ) + user_admin = MyUserAdmin(User, self.site) + user_queryset = user_admin.get_search_results(user_request, domain_request_queryset, None)[0] + current_dropdown = list(user_queryset) + + self.assertEqual(expected_dropdown, current_dropdown) + + # Non staff users should not be in the list + self.assertNotIn(domain_request_2, current_dropdown) + + def test_investigator_list_is_alphabetically_sorted(self): + """ + This test verifies that filter list for the 'investigator' + is displayed alphabetically + """ + with less_console_noise(): + # Create a mock DomainRequest object, with a fake investigator + domain_request: DomainRequest = generic_domain_object("domain_request", "SomeGuy") + investigator_user = User.objects.filter(username=domain_request.investigator.username).get() + investigator_user.is_staff = True + investigator_user.save() + + domain_request_2: DomainRequest = generic_domain_object("domain_request", "AGuy") + investigator_user_2 = User.objects.filter(username=domain_request_2.investigator.username).get() + investigator_user_2.first_name = "AGuy" + investigator_user_2.is_staff = True + investigator_user_2.save() + + domain_request_3: DomainRequest = generic_domain_object("domain_request", "FinalGuy") + investigator_user_3 = User.objects.filter(username=domain_request_3.investigator.username).get() + investigator_user_3.first_name = "FinalGuy" + investigator_user_3.is_staff = True + investigator_user_3.save() + + self.client.force_login(self.staffuser) + request = RequestFactory().get("/") + + # These names have metadata embedded in them. :investigator implicitly tests if + # these are actually from the attribute "investigator". + expected_list = [ + "AGuy AGuy last_name:investigator", + "FinalGuy FinalGuy last_name:investigator", + "SomeGuy first_name:investigator SomeGuy last_name:investigator", + ] + + # Get the actual sorted list of investigators from the lookups method + actual_list = [item for _, item in self.admin.InvestigatorFilter.lookups(self, request, self.admin)] + + self.assertEqual(expected_list, actual_list) + + @less_console_noise_decorator + def test_staff_can_see_cisa_region_federal(self): + """Tests if staff can see CISA Region: N/A""" + + # Create a fake domain request + _domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) + + self.client.force_login(self.staffuser) + response = self.client.get( + "/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, _domain_request.requested_domain.name) + + # Test if the page has the right CISA region + expected_html = '
CISA region: N/A
' + # Remove whitespace from expected_html + expected_html = "".join(expected_html.split()) + + # Remove whitespace from response content + response_content = "".join(response.content.decode().split()) + + # Check if response contains expected_html + self.assertIn(expected_html, response_content) + + @less_console_noise_decorator + def test_staff_can_see_cisa_region_non_federal(self): + """Tests if staff can see the correct CISA region""" + + # Create a fake domain request. State will be NY (2). + _domain_request = completed_domain_request( + status=DomainRequest.DomainRequestStatus.IN_REVIEW, generic_org_type="interstate" + ) + + self.client.force_login(self.staffuser) + response = self.client.get( + "/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk), + follow=True, + ) + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, _domain_request.requested_domain.name) + + # Test if the page has the right CISA region + expected_html = '
CISA region: 2
' + # Remove whitespace from expected_html + expected_html = "".join(expected_html.split()) + + # Remove whitespace from response content + response_content = "".join(response.content.decode().split()) + + # Check if response contains expected_html + self.assertIn(expected_html, response_content) + + +class TestDomainRequestAdminForm(TestCase): + + def test_form_choices(self): + with less_console_noise(): + # Create a test domain request with an initial state of started + domain_request = completed_domain_request() + + # Create a form instance with the test domain request + form = DomainRequestAdminForm(instance=domain_request) + + # Verify that the form choices match the available transitions for started + expected_choices = [("started", "Started"), ("submitted", "Submitted")] + self.assertEqual(form.fields["status"].widget.choices, expected_choices) + + # cleanup + domain_request.delete() + + def test_form_no_rejection_reason(self): + with less_console_noise(): + # Create a test domain request with an initial state of started + domain_request = completed_domain_request() + + # Create a form instance with the test domain request + form = DomainRequestAdminForm(instance=domain_request) + + form = DomainRequestAdminForm( + instance=domain_request, + data={ + "status": DomainRequest.DomainRequestStatus.REJECTED, + "rejection_reason": None, + }, + ) + self.assertFalse(form.is_valid()) + self.assertIn("rejection_reason", form.errors) + + rejection_reason = form.errors.get("rejection_reason") + self.assertEqual(rejection_reason, ["A reason is required for this status."]) + + # cleanup + domain_request.delete() + + def test_form_choices_when_no_instance(self): + with less_console_noise(): + # Create a form instance without an instance + form = DomainRequestAdminForm() + + # Verify that the form choices show all choices when no instance is provided; + # this is necessary to show all choices when creating a new domain + # request in django admin; + # note that FSM ensures that no domain request exists with invalid status, + # so don't need to test for invalid status + self.assertEqual( + form.fields["status"].widget.choices, + DomainRequest._meta.get_field("status").choices, + ) + + def test_form_choices_when_ineligible(self): + with less_console_noise(): + # Create a form instance with a domain request with ineligible status + ineligible_domain_request = DomainRequest(status="ineligible") + + # Attempt to create a form with the ineligible domain request + # The form should not raise an error, but choices should be the + # full list of possible choices + form = DomainRequestAdminForm(instance=ineligible_domain_request) + + self.assertEqual( + form.fields["status"].widget.choices, + DomainRequest._meta.get_field("status").choices, + ) diff --git a/src/registrar/tests/test_admin_views.py b/src/registrar/tests/test_admin_views.py index cc4b3f1c7..e88c070df 100644 --- a/src/registrar/tests/test_admin_views.py +++ b/src/registrar/tests/test_admin_views.py @@ -1,6 +1,7 @@ from django.test import TestCase, Client from django.urls import reverse from registrar.tests.common import create_superuser +from api.tests.common import less_console_noise_decorator class TestAdminViews(TestCase): @@ -8,6 +9,7 @@ class TestAdminViews(TestCase): self.client = Client(HTTP_HOST="localhost:8080") self.superuser = create_superuser() + @less_console_noise_decorator def test_export_data_view(self): self.client.force_login(self.superuser) diff --git a/src/registrar/tests/test_emails.py b/src/registrar/tests/test_emails.py index b01272e64..8cf707004 100644 --- a/src/registrar/tests/test_emails.py +++ b/src/registrar/tests/test_emails.py @@ -6,8 +6,9 @@ from django.test import TestCase from waffle.testutils import override_flag from registrar.utility import email from registrar.utility.email import send_templated_email -from .common import completed_domain_request, less_console_noise +from .common import completed_domain_request +from api.tests.common import less_console_noise_decorator from datetime import datetime import boto3_mocking # type: ignore @@ -19,6 +20,7 @@ class TestEmails(TestCase): @boto3_mocking.patching @override_flag("disable_email_sending", active=True) + @less_console_noise_decorator def test_disable_email_flag(self): """Test if the 'disable_email_sending' stops emails from being sent""" with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): @@ -36,13 +38,13 @@ class TestEmails(TestCase): self.assertFalse(self.mock_client.send_email.called) @boto3_mocking.patching + @less_console_noise_decorator def test_submission_confirmation(self): """Submission confirmation email works.""" domain_request = completed_domain_request() with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - with less_console_noise(): - domain_request.submit() + domain_request.submit() # check that an email was sent self.assertTrue(self.mock_client.send_email.called) @@ -74,12 +76,12 @@ class TestEmails(TestCase): self.assertIn("Anything else", body) @boto3_mocking.patching + @less_console_noise_decorator def test_submission_confirmation_no_current_website_spacing(self): """Test line spacing without current_website.""" domain_request = completed_domain_request(has_current_website=False) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - with less_console_noise(): - domain_request.submit() + domain_request.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertNotIn("Current websites:", body) @@ -87,12 +89,12 @@ class TestEmails(TestCase): self.assertRegex(body, r"5555\n\n.gov domain:") @boto3_mocking.patching + @less_console_noise_decorator def test_submission_confirmation_current_website_spacing(self): """Test line spacing with current_website.""" domain_request = completed_domain_request(has_current_website=True) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - with less_console_noise(): - domain_request.submit() + domain_request.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertIn("Current websites:", body) @@ -101,12 +103,12 @@ class TestEmails(TestCase): self.assertRegex(body, r"city.com\n\n.gov domain:") @boto3_mocking.patching + @less_console_noise_decorator def test_submission_confirmation_other_contacts_spacing(self): """Test line spacing with other contacts.""" domain_request = completed_domain_request(has_other_contacts=True) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - with less_console_noise(): - domain_request.submit() + domain_request.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertIn("Other employees from your organization:", body) @@ -115,12 +117,12 @@ class TestEmails(TestCase): self.assertRegex(body, r"5557\n\nAnything else") @boto3_mocking.patching + @less_console_noise_decorator def test_submission_confirmation_no_other_contacts_spacing(self): """Test line spacing without other contacts.""" domain_request = completed_domain_request(has_other_contacts=False) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - with less_console_noise(): - domain_request.submit() + domain_request.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] # spacing should be right between adjacent elements @@ -128,12 +130,12 @@ class TestEmails(TestCase): self.assertRegex(body, r"None\n\nAnything else") @boto3_mocking.patching + @less_console_noise_decorator def test_submission_confirmation_alternative_govdomain_spacing(self): """Test line spacing with alternative .gov domain.""" domain_request = completed_domain_request(has_alternative_gov_domain=True) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - with less_console_noise(): - domain_request.submit() + domain_request.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertIn("city1.gov", body) @@ -141,12 +143,12 @@ class TestEmails(TestCase): self.assertRegex(body, r"city.gov\n\nAlternative domains:\ncity1.gov\n\nPurpose of your domain:") @boto3_mocking.patching + @less_console_noise_decorator def test_submission_confirmation_no_alternative_govdomain_spacing(self): """Test line spacing without alternative .gov domain.""" domain_request = completed_domain_request(has_alternative_gov_domain=False) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - with less_console_noise(): - domain_request.submit() + domain_request.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertNotIn("city1.gov", body) @@ -154,12 +156,12 @@ class TestEmails(TestCase): self.assertRegex(body, r"city.gov\n\nPurpose of your domain:") @boto3_mocking.patching + @less_console_noise_decorator def test_submission_confirmation_about_your_organization_spacing(self): """Test line spacing with about your organization.""" domain_request = completed_domain_request(has_about_your_organization=True) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - with less_console_noise(): - domain_request.submit() + domain_request.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertIn("About your organization:", body) @@ -167,12 +169,12 @@ class TestEmails(TestCase): self.assertRegex(body, r"10002\n\nAbout your organization:") @boto3_mocking.patching + @less_console_noise_decorator def test_submission_confirmation_no_about_your_organization_spacing(self): """Test line spacing without about your organization.""" domain_request = completed_domain_request(has_about_your_organization=False) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - with less_console_noise(): - domain_request.submit() + domain_request.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertNotIn("About your organization:", body) @@ -180,24 +182,24 @@ class TestEmails(TestCase): self.assertRegex(body, r"10002\n\nSenior official:") @boto3_mocking.patching + @less_console_noise_decorator def test_submission_confirmation_anything_else_spacing(self): """Test line spacing with anything else.""" domain_request = completed_domain_request(has_anything_else=True) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - with less_console_noise(): - domain_request.submit() + domain_request.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] # spacing should be right between adjacent elements self.assertRegex(body, r"5557\n\nAnything else?") @boto3_mocking.patching + @less_console_noise_decorator def test_submission_confirmation_no_anything_else_spacing(self): """Test line spacing without anything else.""" domain_request = completed_domain_request(has_anything_else=False) with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class): - with less_console_noise(): - domain_request.submit() + domain_request.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertNotIn("Anything else", body) @@ -205,6 +207,7 @@ class TestEmails(TestCase): self.assertRegex(body, r"5557\n\n----") @boto3_mocking.patching + @less_console_noise_decorator def test_send_email_with_attachment(self): with boto3_mocking.clients.handler_for("ses", self.mock_client_class): sender_email = "sender@example.com" diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index cfe19b091..26bd88223 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -36,6 +36,7 @@ logger = logging.getLogger(__name__) class TestPopulateVerificationType(MockEppLib): """Tests for the populate_organization_type script""" + @less_console_noise_decorator def setUp(self): """Creates a fake domain object""" super().setUp() @@ -133,6 +134,7 @@ class TestPopulateVerificationType(MockEppLib): class TestPopulateOrganizationType(MockEppLib): """Tests for the populate_organization_type script""" + @less_console_noise_decorator def setUp(self): """Creates a fake domain object""" super().setUp() @@ -205,6 +207,7 @@ class TestPopulateOrganizationType(MockEppLib): ): call_command("populate_organization_type", "registrar/tests/data/fake_election_domains.csv") + @less_console_noise_decorator def assert_expected_org_values_on_request_and_info( self, domain_request: DomainRequest, @@ -247,6 +250,7 @@ class TestPopulateOrganizationType(MockEppLib): """Does nothing for mocking purposes""" pass + @less_console_noise_decorator def test_request_and_info_city_not_in_csv(self): """ Tests what happens to a city domain that is not defined in the CSV. @@ -282,6 +286,7 @@ class TestPopulateOrganizationType(MockEppLib): # All values should be the same self.assert_expected_org_values_on_request_and_info(city_request, city_info, expected_values) + @less_console_noise_decorator def test_request_and_info_federal(self): """ Tests what happens to a federal domain after the script is run (should be unchanged). @@ -316,6 +321,7 @@ class TestPopulateOrganizationType(MockEppLib): # All values should be the same self.assert_expected_org_values_on_request_and_info(federal_request, federal_info, expected_values) + @less_console_noise_decorator def test_request_and_info_tribal_add_election_office(self): """ Tests if a tribal domain in the election csv changes organization_type to TRIBAL - ELECTION @@ -356,6 +362,7 @@ class TestPopulateOrganizationType(MockEppLib): self.assert_expected_org_values_on_request_and_info(tribal_request, tribal_info, expected_values) + @less_console_noise_decorator def test_request_and_info_tribal_doesnt_remove_election_office(self): """ Tests if a tribal domain in the election csv changes organization_type to TRIBAL_ELECTION @@ -409,6 +416,7 @@ class TestPopulateOrganizationType(MockEppLib): class TestPopulateFirstReady(TestCase): """Tests for the populate_first_ready script""" + @less_console_noise_decorator def setUp(self): """Creates a fake domain object""" super().setUp() @@ -537,6 +545,7 @@ class TestPopulateFirstReady(TestCase): class TestPatchAgencyInfo(TestCase): + @less_console_noise_decorator def setUp(self): self.user, _ = User.objects.get_or_create(username="testuser") self.domain, _ = Domain.objects.get_or_create(name="testdomain.gov") @@ -560,6 +569,7 @@ class TestPatchAgencyInfo(TestCase): class TestExtendExpirationDates(MockEppLib): + @less_console_noise_decorator def setUp(self): """Defines the file name of migration_json and the folder its contained in""" super().setUp() @@ -882,6 +892,7 @@ class TestExportTables(MockEppLib): def tearDown(self): self.logger_patcher.stop() + @less_console_noise_decorator @patch("os.makedirs") @patch("os.path.exists") @patch("os.remove") @@ -1113,6 +1124,7 @@ class TestImportTables(TestCase): class TestTransferFederalAgencyType(TestCase): """Tests for the transfer_federal_agency_type script""" + @less_console_noise_decorator def setUp(self): """Creates a fake domain object""" super().setUp() @@ -1172,7 +1184,9 @@ class TestTransferFederalAgencyType(TestCase): User.objects.all().delete() Contact.objects.all().delete() Website.objects.all().delete() - FederalAgency.objects.all().delete() + FederalAgency.objects.filter( + id__in=[self.amtrak.id, self.legislative_branch.id, self.library_of_congress.id, self.gov_admin.id] + ).delete() def run_transfer_federal_agency_type(self): """ diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 8daf15933..741ec5361 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -1,7 +1,7 @@ from django.test import TestCase from django.db.utils import IntegrityError +from django.db import transaction from unittest.mock import patch -from django.contrib.auth import get_user_model from django.test import RequestFactory @@ -24,16 +24,22 @@ from registrar.models.transition_domain import TransitionDomain from registrar.models.verified_by_staff import VerifiedByStaff # type: ignore from registrar.utility.constants import BranchChoices -from .common import MockSESClient, less_console_noise, completed_domain_request, set_domain_request_investigators +from .common import ( + MockSESClient, + less_console_noise, + completed_domain_request, + set_domain_request_investigators, + create_test_user, +) from django_fsm import TransitionNotAllowed from waffle.testutils import override_flag +from api.tests.common import less_console_noise_decorator + -# Test comment for push -- will remove -# The DomainRequest submit method has a side effect of sending an email -# with AWS SES, so mock that out in all of these test cases @boto3_mocking.patching class TestDomainRequest(TestCase): + @less_console_noise_decorator def setUp(self): self.dummy_user, _ = Contact.objects.get_or_create( @@ -91,6 +97,11 @@ class TestDomainRequest(TestCase): def tearDown(self): super().tearDown() + DomainInformation.objects.all().delete() + DomainRequest.objects.all().delete() + DraftDomain.objects.all().delete() + Domain.objects.all().delete() + User.objects.all().delete() self.mock_client.EMAILS_SENT.clear() def assertNotRaises(self, exception_type): @@ -98,6 +109,7 @@ class TestDomainRequest(TestCase): with less_console_noise(): return self.assertRaises(Exception, None, exception_type) + @less_console_noise_decorator def test_federal_agency_set_to_non_federal_on_approve(self): """Ensures that when the federal_agency field is 'none' when .approve() is called, the field is set to the 'Non-Federal Agency' record""" @@ -119,103 +131,103 @@ class TestDomainRequest(TestCase): self.assertEqual(domain_request.federal_agency, expected_federal_agency) def test_empty_create_fails(self): - """Can't create a completely empty domain request. - NOTE: something about theexception this test raises messes up with the - atomic block in a custom tearDown method for the parent test class.""" + """Can't create a completely empty domain request.""" with less_console_noise(): - with self.assertRaisesRegex(IntegrityError, "creator"): - DomainRequest.objects.create() + with transaction.atomic(): + with self.assertRaisesRegex(IntegrityError, "creator"): + DomainRequest.objects.create() + @less_console_noise_decorator def test_minimal_create(self): """Can create with just a creator.""" - with less_console_noise(): - user, _ = User.objects.get_or_create(username="testy") - domain_request = DomainRequest.objects.create(creator=user) - self.assertEqual(domain_request.status, DomainRequest.DomainRequestStatus.STARTED) + user, _ = User.objects.get_or_create(username="testy") + domain_request = DomainRequest.objects.create(creator=user) + self.assertEqual(domain_request.status, DomainRequest.DomainRequestStatus.STARTED) + @less_console_noise_decorator def test_full_create(self): """Can create with all fields.""" - with less_console_noise(): - user, _ = User.objects.get_or_create(username="testy") - contact = Contact.objects.create() - com_website, _ = Website.objects.get_or_create(website="igorville.com") - gov_website, _ = Website.objects.get_or_create(website="igorville.gov") - domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") - domain_request = DomainRequest.objects.create( - creator=user, - investigator=user, - generic_org_type=DomainRequest.OrganizationChoices.FEDERAL, - federal_type=BranchChoices.EXECUTIVE, - is_election_board=False, - organization_name="Test", - address_line1="100 Main St.", - address_line2="APT 1A", - state_territory="CA", - zipcode="12345-6789", - senior_official=contact, - requested_domain=domain, - submitter=contact, - purpose="Igorville rules!", - anything_else="All of Igorville loves the dotgov program.", - is_policy_acknowledged=True, - ) - domain_request.current_websites.add(com_website) - domain_request.alternative_domains.add(gov_website) - domain_request.other_contacts.add(contact) - domain_request.save() + user, _ = User.objects.get_or_create(username="testy") + contact = Contact.objects.create() + com_website, _ = Website.objects.get_or_create(website="igorville.com") + gov_website, _ = Website.objects.get_or_create(website="igorville.gov") + domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") + domain_request = DomainRequest.objects.create( + creator=user, + investigator=user, + generic_org_type=DomainRequest.OrganizationChoices.FEDERAL, + federal_type=BranchChoices.EXECUTIVE, + is_election_board=False, + organization_name="Test", + address_line1="100 Main St.", + address_line2="APT 1A", + state_territory="CA", + zipcode="12345-6789", + senior_official=contact, + requested_domain=domain, + submitter=contact, + purpose="Igorville rules!", + anything_else="All of Igorville loves the dotgov program.", + is_policy_acknowledged=True, + ) + domain_request.current_websites.add(com_website) + domain_request.alternative_domains.add(gov_website) + domain_request.other_contacts.add(contact) + domain_request.save() + @less_console_noise_decorator def test_domain_info(self): """Can create domain info with all fields.""" - with less_console_noise(): - user, _ = User.objects.get_or_create(username="testy") - contact = Contact.objects.create() - domain, _ = Domain.objects.get_or_create(name="igorville.gov") - information = DomainInformation.objects.create( - creator=user, - generic_org_type=DomainInformation.OrganizationChoices.FEDERAL, - federal_type=BranchChoices.EXECUTIVE, - is_election_board=False, - organization_name="Test", - address_line1="100 Main St.", - address_line2="APT 1A", - state_territory="CA", - zipcode="12345-6789", - senior_official=contact, - submitter=contact, - purpose="Igorville rules!", - anything_else="All of Igorville loves the dotgov program.", - is_policy_acknowledged=True, - domain=domain, - ) - information.other_contacts.add(contact) - information.save() - self.assertEqual(information.domain.id, domain.id) - self.assertEqual(information.id, domain.domain_info.id) + user, _ = User.objects.get_or_create(username="testy") + contact = Contact.objects.create() + domain, _ = Domain.objects.get_or_create(name="igorville.gov") + information = DomainInformation.objects.create( + creator=user, + generic_org_type=DomainInformation.OrganizationChoices.FEDERAL, + federal_type=BranchChoices.EXECUTIVE, + is_election_board=False, + organization_name="Test", + address_line1="100 Main St.", + address_line2="APT 1A", + state_territory="CA", + zipcode="12345-6789", + senior_official=contact, + submitter=contact, + purpose="Igorville rules!", + anything_else="All of Igorville loves the dotgov program.", + is_policy_acknowledged=True, + domain=domain, + ) + information.other_contacts.add(contact) + information.save() + self.assertEqual(information.domain.id, domain.id) + self.assertEqual(information.id, domain.domain_info.id) + @less_console_noise_decorator def test_status_fsm_submit_fail(self): - with less_console_noise(): - user, _ = User.objects.get_or_create(username="testy") - domain_request = DomainRequest.objects.create(creator=user) + user, _ = User.objects.get_or_create(username="testy") + domain_request = DomainRequest.objects.create(creator=user) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - with self.assertRaises(ValueError): - # can't submit a domain request with a null domain name - domain_request.submit() - - def test_status_fsm_submit_succeed(self): - with less_console_noise(): - user, _ = User.objects.get_or_create(username="testy") - site = DraftDomain.objects.create(name="igorville.gov") - domain_request = DomainRequest.objects.create(creator=user, requested_domain=site) - - # no submitter email so this emits a log warning - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + with self.assertRaises(ValueError): + # can't submit a domain request with a null domain name domain_request.submit() - self.assertEqual(domain_request.status, domain_request.DomainRequestStatus.SUBMITTED) + @less_console_noise_decorator + def test_status_fsm_submit_succeed(self): + user, _ = User.objects.get_or_create(username="testy") + site = DraftDomain.objects.create(name="igorville.gov") + domain_request = DomainRequest.objects.create(creator=user, requested_domain=site) + + # no submitter email so this emits a log warning + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + domain_request.submit() + self.assertEqual(domain_request.status, domain_request.DomainRequestStatus.SUBMITTED) + + @less_console_noise_decorator def check_email_sent( self, domain_request, msg, action, expected_count, expected_content=None, expected_email="mayor@igorville.com" ): @@ -223,10 +235,9 @@ class TestDomainRequest(TestCase): with self.subTest(msg=msg, action=action): with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Perform the specified action - action_method = getattr(domain_request, action) - action_method() + # Perform the specified action + action_method = getattr(domain_request, action) + action_method() # Check if an email was sent sent_emails = [ @@ -241,12 +252,14 @@ class TestDomainRequest(TestCase): self.assertIn(expected_content, email_content) @override_flag("profile_feature", active=False) + @less_console_noise_decorator def test_submit_from_started_sends_email(self): msg = "Create a domain request and submit it and see if email was sent." domain_request = completed_domain_request(submitter=self.dummy_user, user=self.dummy_user_2) self.check_email_sent(domain_request, msg, "submit", 1, expected_content="Hello") @override_flag("profile_feature", active=True) + @less_console_noise_decorator def test_submit_from_started_sends_email_to_creator(self): """Tests if, when the profile feature flag is on, we send an email to the creator""" msg = "Create a domain request and submit it and see if email was sent when the feature flag is on." @@ -255,6 +268,7 @@ class TestDomainRequest(TestCase): domain_request, msg, "submit", 1, expected_content="Lava", expected_email="intern@igorville.com" ) + @less_console_noise_decorator def test_submit_from_withdrawn_sends_email(self): msg = "Create a withdrawn domain request and submit it and see if email was sent." domain_request = completed_domain_request( @@ -262,16 +276,19 @@ class TestDomainRequest(TestCase): ) self.check_email_sent(domain_request, msg, "submit", 1, expected_content="Hello") + @less_console_noise_decorator def test_submit_from_action_needed_does_not_send_email(self): msg = "Create a domain request with ACTION_NEEDED status and submit it, check if email was not sent." domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.ACTION_NEEDED) self.check_email_sent(domain_request, msg, "submit", 0) + @less_console_noise_decorator def test_submit_from_in_review_does_not_send_email(self): msg = "Create a withdrawn domain request and submit it and see if email was sent." domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW) self.check_email_sent(domain_request, msg, "submit", 0) + @less_console_noise_decorator def test_approve_sends_email(self): msg = "Create a domain request and approve it and see if email was sent." domain_request = completed_domain_request( @@ -279,6 +296,7 @@ class TestDomainRequest(TestCase): ) self.check_email_sent(domain_request, msg, "approve", 1, expected_content="Hello") + @less_console_noise_decorator def test_withdraw_sends_email(self): msg = "Create a domain request and withdraw it and see if email was sent." domain_request = completed_domain_request( @@ -286,6 +304,7 @@ class TestDomainRequest(TestCase): ) self.check_email_sent(domain_request, msg, "withdraw", 1, expected_content="Hello") + @less_console_noise_decorator def test_reject_sends_email(self): msg = "Create a domain request and reject it and see if email was sent." domain_request = completed_domain_request( @@ -293,11 +312,13 @@ class TestDomainRequest(TestCase): ) self.check_email_sent(domain_request, msg, "reject", 1, expected_content="Hello") + @less_console_noise_decorator def test_reject_with_prejudice_does_not_send_email(self): msg = "Create a domain request and reject it with prejudice and see if email was sent." domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.APPROVED) self.check_email_sent(domain_request, msg, "reject_with_prejudice", 0) + @less_console_noise_decorator def assert_fsm_transition_raises_error(self, test_cases, method_to_run): """Given a list of test cases, check if each transition throws the intended error""" with boto3_mocking.clients.handler_for("sesv2", self.mock_client), less_console_noise(): @@ -309,6 +330,7 @@ class TestDomainRequest(TestCase): # Call the method method() + @less_console_noise_decorator def assert_fsm_transition_does_not_raise_error(self, test_cases, method_to_run): """Given a list of test cases, ensure that none of them throw transition errors""" with boto3_mocking.clients.handler_for("sesv2", self.mock_client), less_console_noise(): @@ -322,6 +344,7 @@ class TestDomainRequest(TestCase): except exception_type: self.fail(f"{exception_type} was raised, but it was not expected.") + @less_console_noise_decorator def test_submit_transition_allowed_with_no_investigator(self): """ Tests for attempting to transition without an investigator. @@ -340,6 +363,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_does_not_raise_error(test_cases, "submit") + @less_console_noise_decorator def test_submit_transition_allowed_with_investigator_not_staff(self): """ Tests for attempting to transition with an investigator user that is not staff. @@ -357,6 +381,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_does_not_raise_error(test_cases, "submit") + @less_console_noise_decorator def test_submit_transition_allowed(self): """ Test that calling submit from allowable statuses does raises TransitionNotAllowed. @@ -370,26 +395,27 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_does_not_raise_error(test_cases, "submit") + @less_console_noise_decorator def test_submit_transition_allowed_twice(self): """ Test that rotating between submit and in_review doesn't throw an error """ with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - try: - # Make a submission - self.in_review_domain_request.submit() + try: + # Make a submission + self.in_review_domain_request.submit() - # Rerun the old method to get back to the original state - self.in_review_domain_request.in_review() + # Rerun the old method to get back to the original state + self.in_review_domain_request.in_review() - # Make another submission - self.in_review_domain_request.submit() - except TransitionNotAllowed: - self.fail("TransitionNotAllowed was raised, but it was not expected.") + # Make another submission + self.in_review_domain_request.submit() + except TransitionNotAllowed: + self.fail("TransitionNotAllowed was raised, but it was not expected.") self.assertEqual(self.in_review_domain_request.status, DomainRequest.DomainRequestStatus.SUBMITTED) + @less_console_noise_decorator def test_submit_transition_not_allowed(self): """ Test that calling submit against transition rules raises TransitionNotAllowed. @@ -403,6 +429,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_raises_error(test_cases, "submit") + @less_console_noise_decorator def test_in_review_transition_allowed(self): """ Test that calling in_review from allowable statuses does raises TransitionNotAllowed. @@ -417,6 +444,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_does_not_raise_error(test_cases, "in_review") + @less_console_noise_decorator def test_in_review_transition_not_allowed_with_no_investigator(self): """ Tests for attempting to transition without an investigator @@ -434,6 +462,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_raises_error(test_cases, "in_review") + @less_console_noise_decorator def test_in_review_transition_not_allowed_with_investigator_not_staff(self): """ Tests for attempting to transition with an investigator that is not staff. @@ -453,6 +482,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_raises_error(test_cases, "in_review") + @less_console_noise_decorator def test_in_review_transition_not_allowed(self): """ Test that calling in_review against transition rules raises TransitionNotAllowed. @@ -465,6 +495,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_raises_error(test_cases, "in_review") + @less_console_noise_decorator def test_action_needed_transition_allowed(self): """ Test that calling action_needed from allowable statuses does raises TransitionNotAllowed. @@ -478,6 +509,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_does_not_raise_error(test_cases, "action_needed") + @less_console_noise_decorator def test_action_needed_transition_not_allowed_with_no_investigator(self): """ Tests for attempting to transition without an investigator @@ -495,6 +527,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_raises_error(test_cases, "action_needed") + @less_console_noise_decorator def test_action_needed_transition_not_allowed_with_investigator_not_staff(self): """ Tests for attempting to transition with an investigator that is not staff @@ -513,6 +546,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_raises_error(test_cases, "action_needed") + @less_console_noise_decorator def test_action_needed_transition_not_allowed(self): """ Test that calling action_needed against transition rules raises TransitionNotAllowed. @@ -526,6 +560,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_raises_error(test_cases, "action_needed") + @less_console_noise_decorator def test_approved_transition_allowed(self): """ Test that calling action_needed from allowable statuses does raises TransitionNotAllowed. @@ -539,6 +574,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_does_not_raise_error(test_cases, "approve") + @less_console_noise_decorator def test_approved_transition_not_allowed_with_no_investigator(self): """ Tests for attempting to transition without an investigator @@ -555,6 +591,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_raises_error(test_cases, "approve") + @less_console_noise_decorator def test_approved_transition_not_allowed_with_investigator_not_staff(self): """ Tests for attempting to transition with an investigator that is not staff @@ -572,6 +609,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_raises_error(test_cases, "approve") + @less_console_noise_decorator def test_approved_skips_sending_email(self): """ Test that calling .approve with send_email=False doesn't actually send @@ -579,12 +617,12 @@ class TestDomainRequest(TestCase): """ with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - self.submitted_domain_request.approve(send_email=False) + self.submitted_domain_request.approve(send_email=False) # Assert that no emails were sent self.assertEqual(len(self.mock_client.EMAILS_SENT), 0) + @less_console_noise_decorator def test_approved_transition_not_allowed(self): """ Test that calling action_needed against transition rules raises TransitionNotAllowed. @@ -597,6 +635,7 @@ class TestDomainRequest(TestCase): ] self.assert_fsm_transition_raises_error(test_cases, "approve") + @less_console_noise_decorator def test_withdraw_transition_allowed(self): """ Test that calling action_needed from allowable statuses does raises TransitionNotAllowed. @@ -609,6 +648,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_does_not_raise_error(test_cases, "withdraw") + @less_console_noise_decorator def test_withdraw_transition_allowed_with_no_investigator(self): """ Tests for attempting to transition without an investigator. @@ -626,6 +666,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_does_not_raise_error(test_cases, "withdraw") + @less_console_noise_decorator def test_withdraw_transition_allowed_with_investigator_not_staff(self): """ Tests for attempting to transition when investigator is not staff. @@ -644,6 +685,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_does_not_raise_error(test_cases, "withdraw") + @less_console_noise_decorator def test_withdraw_transition_not_allowed(self): """ Test that calling action_needed against transition rules raises TransitionNotAllowed. @@ -658,6 +700,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_raises_error(test_cases, "withdraw") + @less_console_noise_decorator def test_reject_transition_allowed(self): """ Test that calling action_needed from allowable statuses does raises TransitionNotAllowed. @@ -670,6 +713,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_does_not_raise_error(test_cases, "reject") + @less_console_noise_decorator def test_reject_transition_not_allowed_with_no_investigator(self): """ Tests for attempting to transition without an investigator @@ -686,6 +730,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_raises_error(test_cases, "reject") + @less_console_noise_decorator def test_reject_transition_not_allowed_with_investigator_not_staff(self): """ Tests for attempting to transition when investigator is not staff @@ -703,6 +748,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_raises_error(test_cases, "reject") + @less_console_noise_decorator def test_reject_transition_not_allowed(self): """ Test that calling action_needed against transition rules raises TransitionNotAllowed. @@ -717,6 +763,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_raises_error(test_cases, "reject") + @less_console_noise_decorator def test_reject_with_prejudice_transition_allowed(self): """ Test that calling action_needed from allowable statuses does raises TransitionNotAllowed. @@ -730,6 +777,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_does_not_raise_error(test_cases, "reject_with_prejudice") + @less_console_noise_decorator def test_reject_with_prejudice_transition_not_allowed_with_no_investigator(self): """ Tests for attempting to transition without an investigator @@ -747,6 +795,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_raises_error(test_cases, "reject_with_prejudice") + @less_console_noise_decorator def test_reject_with_prejudice_not_allowed_with_investigator_not_staff(self): """ Tests for attempting to transition when investigator is not staff @@ -765,6 +814,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_raises_error(test_cases, "reject_with_prejudice") + @less_console_noise_decorator def test_reject_with_prejudice_transition_not_allowed(self): """ Test that calling action_needed against transition rules raises TransitionNotAllowed. @@ -778,6 +828,7 @@ class TestDomainRequest(TestCase): self.assert_fsm_transition_raises_error(test_cases, "reject_with_prejudice") + @less_console_noise_decorator def test_transition_not_allowed_approved_in_review_when_domain_is_active(self): """Create a domain request with status approved, create a matching domain that is active, and call in_review against transition rules""" @@ -791,13 +842,13 @@ class TestDomainRequest(TestCase): return True # Override to return True with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Use patch to temporarily replace is_active with the custom implementation - with patch.object(Domain, "is_active", custom_is_active): - # Now, when you call is_active on Domain, it will return True - with self.assertRaises(TransitionNotAllowed): - self.approved_domain_request.in_review() + # Use patch to temporarily replace is_active with the custom implementation + with patch.object(Domain, "is_active", custom_is_active): + # Now, when you call is_active on Domain, it will return True + with self.assertRaises(TransitionNotAllowed): + self.approved_domain_request.in_review() + @less_console_noise_decorator def test_transition_not_allowed_approved_action_needed_when_domain_is_active(self): """Create a domain request with status approved, create a matching domain that is active, and call action_needed against transition rules""" @@ -811,13 +862,13 @@ class TestDomainRequest(TestCase): return True # Override to return True with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Use patch to temporarily replace is_active with the custom implementation - with patch.object(Domain, "is_active", custom_is_active): - # Now, when you call is_active on Domain, it will return True - with self.assertRaises(TransitionNotAllowed): - self.approved_domain_request.action_needed() + # Use patch to temporarily replace is_active with the custom implementation + with patch.object(Domain, "is_active", custom_is_active): + # Now, when you call is_active on Domain, it will return True + with self.assertRaises(TransitionNotAllowed): + self.approved_domain_request.action_needed() + @less_console_noise_decorator def test_transition_not_allowed_approved_rejected_when_domain_is_active(self): """Create a domain request with status approved, create a matching domain that is active, and call reject against transition rules""" @@ -831,13 +882,13 @@ class TestDomainRequest(TestCase): return True # Override to return True with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Use patch to temporarily replace is_active with the custom implementation - with patch.object(Domain, "is_active", custom_is_active): - # Now, when you call is_active on Domain, it will return True - with self.assertRaises(TransitionNotAllowed): - self.approved_domain_request.reject() + # Use patch to temporarily replace is_active with the custom implementation + with patch.object(Domain, "is_active", custom_is_active): + # Now, when you call is_active on Domain, it will return True + with self.assertRaises(TransitionNotAllowed): + self.approved_domain_request.reject() + @less_console_noise_decorator def test_transition_not_allowed_approved_ineligible_when_domain_is_active(self): """Create a domain request with status approved, create a matching domain that is active, and call reject_with_prejudice against transition rules""" @@ -851,88 +902,87 @@ class TestDomainRequest(TestCase): return True # Override to return True with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Use patch to temporarily replace is_active with the custom implementation - with patch.object(Domain, "is_active", custom_is_active): - # Now, when you call is_active on Domain, it will return True - with self.assertRaises(TransitionNotAllowed): - self.approved_domain_request.reject_with_prejudice() + # Use patch to temporarily replace is_active with the custom implementation + with patch.object(Domain, "is_active", custom_is_active): + # Now, when you call is_active on Domain, it will return True + with self.assertRaises(TransitionNotAllowed): + self.approved_domain_request.reject_with_prejudice() + @less_console_noise_decorator def test_approve_from_rejected_clears_rejection_reason(self): """When transitioning from rejected to approved on a domain request, the rejection_reason is cleared.""" - with less_console_noise(): - # Create a sample domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.REJECTED) - domain_request.rejection_reason = DomainRequest.RejectionReasons.DOMAIN_PURPOSE + # Create a sample domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.REJECTED) + domain_request.rejection_reason = DomainRequest.RejectionReasons.DOMAIN_PURPOSE - # Approve - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - domain_request.approve() + # Approve + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + domain_request.approve() - self.assertEqual(domain_request.status, DomainRequest.DomainRequestStatus.APPROVED) - self.assertEqual(domain_request.rejection_reason, None) + self.assertEqual(domain_request.status, DomainRequest.DomainRequestStatus.APPROVED) + self.assertEqual(domain_request.rejection_reason, None) + @less_console_noise_decorator def test_in_review_from_rejected_clears_rejection_reason(self): """When transitioning from rejected to in_review on a domain request, the rejection_reason is cleared.""" - with less_console_noise(): - # Create a sample domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.REJECTED) - domain_request.domain_is_not_active = True - domain_request.rejection_reason = DomainRequest.RejectionReasons.DOMAIN_PURPOSE + # Create a sample domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.REJECTED) + domain_request.domain_is_not_active = True + domain_request.rejection_reason = DomainRequest.RejectionReasons.DOMAIN_PURPOSE - # Approve - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - domain_request.in_review() + # Approve + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + domain_request.in_review() - self.assertEqual(domain_request.status, DomainRequest.DomainRequestStatus.IN_REVIEW) - self.assertEqual(domain_request.rejection_reason, None) + self.assertEqual(domain_request.status, DomainRequest.DomainRequestStatus.IN_REVIEW) + self.assertEqual(domain_request.rejection_reason, None) + @less_console_noise_decorator def test_action_needed_from_rejected_clears_rejection_reason(self): """When transitioning from rejected to action_needed on a domain request, the rejection_reason is cleared.""" - with less_console_noise(): - # Create a sample domain request - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.REJECTED) - domain_request.domain_is_not_active = True - domain_request.rejection_reason = DomainRequest.RejectionReasons.DOMAIN_PURPOSE + # Create a sample domain request + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.REJECTED) + domain_request.domain_is_not_active = True + domain_request.rejection_reason = DomainRequest.RejectionReasons.DOMAIN_PURPOSE - # Approve - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - domain_request.action_needed() + # Approve + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + domain_request.action_needed() - self.assertEqual(domain_request.status, DomainRequest.DomainRequestStatus.ACTION_NEEDED) - self.assertEqual(domain_request.rejection_reason, None) + self.assertEqual(domain_request.status, DomainRequest.DomainRequestStatus.ACTION_NEEDED) + self.assertEqual(domain_request.rejection_reason, None) + @less_console_noise_decorator def test_has_rationale_returns_true(self): """has_rationale() returns true when a domain request has no_other_contacts_rationale""" - with less_console_noise(): - self.started_domain_request.no_other_contacts_rationale = "You talkin' to me?" - self.started_domain_request.save() - self.assertEquals(self.started_domain_request.has_rationale(), True) + self.started_domain_request.no_other_contacts_rationale = "You talkin' to me?" + self.started_domain_request.save() + self.assertEquals(self.started_domain_request.has_rationale(), True) + @less_console_noise_decorator def test_has_rationale_returns_false(self): """has_rationale() returns false when a domain request has no no_other_contacts_rationale""" - with less_console_noise(): - self.assertEquals(self.started_domain_request.has_rationale(), False) + self.assertEquals(self.started_domain_request.has_rationale(), False) + @less_console_noise_decorator def test_has_other_contacts_returns_true(self): """has_other_contacts() returns true when a domain request has other_contacts""" - with less_console_noise(): - # completed_domain_request has other contacts by default - self.assertEquals(self.started_domain_request.has_other_contacts(), True) + # completed_domain_request has other contacts by default + self.assertEquals(self.started_domain_request.has_other_contacts(), True) + @less_console_noise_decorator def test_has_other_contacts_returns_false(self): """has_other_contacts() returns false when a domain request has no other_contacts""" - with less_console_noise(): - domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.STARTED, name="no-others.gov", has_other_contacts=False - ) - self.assertEquals(domain_request.has_other_contacts(), False) + domain_request = completed_domain_request( + status=DomainRequest.DomainRequestStatus.STARTED, name="no-others.gov", has_other_contacts=False + ) + self.assertEquals(domain_request.has_other_contacts(), False) class TestPermissions(TestCase): @@ -947,6 +997,7 @@ class TestPermissions(TestCase): self.mock_client.EMAILS_SENT.clear() @boto3_mocking.patching + @less_console_noise_decorator def test_approval_creates_role(self): draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") user, _ = User.objects.get_or_create() @@ -956,10 +1007,9 @@ class TestPermissions(TestCase): ) with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # skip using the submit method - domain_request.status = DomainRequest.DomainRequestStatus.SUBMITTED - domain_request.approve() + # skip using the submit method + domain_request.status = DomainRequest.DomainRequestStatus.SUBMITTED + domain_request.approve() # should be a role for this user domain = Domain.objects.get(name="igorville.gov") @@ -983,6 +1033,7 @@ class TestDomainInformation(TestCase): DraftDomain.objects.all().delete() @boto3_mocking.patching + @less_console_noise_decorator def test_approval_creates_info(self): draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") user, _ = User.objects.get_or_create() @@ -992,28 +1043,27 @@ class TestDomainInformation(TestCase): ) with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # skip using the submit method - domain_request.status = DomainRequest.DomainRequestStatus.SUBMITTED - domain_request.approve() + # skip using the submit method + domain_request.status = DomainRequest.DomainRequestStatus.SUBMITTED + domain_request.approve() - # should be an information present for this domain - domain = Domain.objects.get(name="igorville.gov") - domain_information = DomainInformation.objects.filter(domain=domain) - self.assertTrue(domain_information.exists()) + # should be an information present for this domain + domain = Domain.objects.get(name="igorville.gov") + domain_information = DomainInformation.objects.filter(domain=domain) + self.assertTrue(domain_information.exists()) - # Test that both objects are what we expect - current_domain_information = domain_information.get().__dict__ - expected_domain_information = DomainInformation( - creator=user, - domain=domain, - notes="test notes", - domain_request=domain_request, - federal_agency=FederalAgency.objects.get(agency="Non-Federal Agency"), - ).__dict__ + # Test that both objects are what we expect + current_domain_information = domain_information.get().__dict__ + expected_domain_information = DomainInformation( + creator=user, + domain=domain, + notes="test notes", + domain_request=domain_request, + federal_agency=FederalAgency.objects.get(agency="Non-Federal Agency"), + ).__dict__ - # Test the two records for consistency - self.assertEqual(self.clean_dict(current_domain_information), self.clean_dict(expected_domain_information)) + # Test the two records for consistency + self.assertEqual(self.clean_dict(current_domain_information), self.clean_dict(expected_domain_information)) def clean_dict(self, dict_obj): """Cleans dynamic fields in a dictionary""" @@ -1024,33 +1074,42 @@ class TestDomainInformation(TestCase): class TestInvitations(TestCase): """Test the retrieval of invitations.""" + @less_console_noise_decorator def setUp(self): self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") self.email = "mayor@igorville.gov" self.invitation, _ = DomainInvitation.objects.get_or_create(email=self.email, domain=self.domain) self.user, _ = User.objects.get_or_create(email=self.email) + def tearDown(self): + super().tearDown() # clean out the roles each time UserDomainRole.objects.all().delete() + self.domain.delete() + self.invitation.delete() + User.objects.all().delete() + @less_console_noise_decorator def test_retrieval_creates_role(self): self.invitation.retrieve() self.assertTrue(UserDomainRole.objects.get(user=self.user, domain=self.domain)) + @less_console_noise_decorator def test_retrieve_missing_user_error(self): # get rid of matching users User.objects.filter(email=self.email).delete() with self.assertRaises(RuntimeError): self.invitation.retrieve() + @less_console_noise_decorator 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.MANAGER) # this is not an error but does produce a console warning - with less_console_noise(): - self.invitation.retrieve() + self.invitation.retrieve() self.assertEqual(self.invitation.status, DomainInvitation.DomainInvitationStatus.RETRIEVED) + @less_console_noise_decorator def test_retrieve_on_each_login(self): """A user's authenticate on_each_login callback retrieves their invitations.""" self.user.on_each_login() @@ -1061,6 +1120,7 @@ class TestUser(TestCase): """Test actions that occur on user login, test class method that controls how users get validated.""" + @less_console_noise_decorator def setUp(self): self.email = "mayor@igorville.gov" self.domain_name = "igorvilleInTransition.gov" @@ -1078,6 +1138,7 @@ class TestUser(TestCase): User.objects.all().delete() UserDomainRole.objects.all().delete() + @less_console_noise_decorator def test_check_transition_domains_without_domains_on_login(self): """A user's on_each_login callback does not check transition domains. This test makes sure that in the event a domain does not exist @@ -1086,35 +1147,41 @@ class TestUser(TestCase): self.user.on_each_login() self.assertFalse(Domain.objects.filter(name=self.domain_name).exists()) + @less_console_noise_decorator def test_identity_verification_with_domain_manager(self): """A domain manager should return False when tested with class method needs_identity_verification""" UserDomainRole.objects.get_or_create(user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER) self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username)) + @less_console_noise_decorator def test_identity_verification_with_transition_user(self): """A user from the Verisign transition should return False when tested with class method needs_identity_verification""" TransitionDomain.objects.get_or_create(username=self.user.email, domain_name=self.domain_name) self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username)) + @less_console_noise_decorator def test_identity_verification_with_very_important_person(self): """A Very Important Person should return False when tested with class method needs_identity_verification""" VerifiedByStaff.objects.get_or_create(email=self.user.email) self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username)) + @less_console_noise_decorator def test_identity_verification_with_invited_user(self): """An invited user should return False when tested with class method needs_identity_verification""" DomainInvitation.objects.get_or_create(email=self.user.email, domain=self.domain) self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username)) + @less_console_noise_decorator def test_identity_verification_with_new_user(self): """A new user who's neither transitioned nor invited should return True when tested with class method needs_identity_verification""" self.assertTrue(User.needs_identity_verification(self.user.email, self.user.username)) + @less_console_noise_decorator def test_check_domain_invitations_on_login_caps_email(self): """A DomainInvitation with an email address with capital letters should match a User record whose email address is not in caps""" @@ -1123,13 +1190,13 @@ class TestUser(TestCase): caps_email = "MAYOR@igorville.gov" # mock the domain invitation save routine with patch("registrar.models.DomainInvitation.save") as save_mock: - with less_console_noise(): - DomainInvitation.objects.get_or_create(email=caps_email, domain=self.domain) - self.user.check_domain_invitations_on_login() - # if check_domain_invitations_on_login properly matches exactly one - # Domain Invitation, then save routine should be called exactly once - save_mock.assert_called_once() + DomainInvitation.objects.get_or_create(email=caps_email, domain=self.domain) + self.user.check_domain_invitations_on_login() + # if check_domain_invitations_on_login properly matches exactly one + # Domain Invitation, then save routine should be called exactly once + save_mock.assert_called_once() + @less_console_noise_decorator def test_approved_domains_count(self): """Test that the correct approved domain count is returned for a user""" # with no associated approved domains, expect this to return 0 @@ -1154,6 +1221,7 @@ class TestUser(TestCase): UserDomainRole.objects.get_or_create(user=self.user, domain=domain5, role=UserDomainRole.Roles.MANAGER) self.assertEquals(self.user.get_approved_domains_count(), 4) + @less_console_noise_decorator def test_active_requests_count(self): """Test that the correct active domain requests count is returned for a user""" # with no associated active requests, expect this to return 0 @@ -1183,6 +1251,7 @@ class TestUser(TestCase): ) self.assertEquals(self.user.get_active_requests_count(), 3) + @less_console_noise_decorator def test_rejected_requests_count(self): """Test that the correct rejected domain requests count is returned for a user""" # with no associated rejected requests, expect this to return 0 @@ -1194,6 +1263,7 @@ class TestUser(TestCase): ) self.assertEquals(self.user.get_rejected_requests_count(), 1) + @less_console_noise_decorator def test_ineligible_requests_count(self): """Test that the correct ineligible domain requests count is returned for a user""" # with no associated ineligible requests, expect this to return 0 @@ -1205,6 +1275,7 @@ class TestUser(TestCase): ) self.assertEquals(self.user.get_ineligible_requests_count(), 1) + @less_console_noise_decorator def test_has_contact_info(self): """Test that has_contact_info properly returns""" # test with a user with contact info defined @@ -1279,6 +1350,7 @@ class TestUser(TestCase): class TestContact(TestCase): + @less_console_noise_decorator def setUp(self): self.email = "mayor@igorville.gov" self.user, _ = User.objects.get_or_create( @@ -1304,6 +1376,7 @@ class TestContact(TestCase): self.assertFalse(self.contact_as_so.has_more_than_one_join("senior_official")) self.assertTrue(self.contact_as_so.has_more_than_one_join("submitted_domain_requests")) + @less_console_noise_decorator def test_has_contact_info(self): """Test that has_contact_info properly returns""" self.contact.title = "Title" @@ -1323,6 +1396,7 @@ class TestDomainRequestCustomSave(TestCase): DomainRequest.objects.all().delete() super().tearDown() + @less_console_noise_decorator def test_create_or_update_organization_type_new_instance(self): """Test create_or_update_organization_type when creating a new instance""" domain_request = completed_domain_request( @@ -1334,6 +1408,7 @@ class TestDomainRequestCustomSave(TestCase): self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION) + @less_console_noise_decorator def test_create_or_update_organization_type_new_instance_federal_does_nothing(self): """Test if create_or_update_organization_type does nothing when creating a new instance for federal""" domain_request = completed_domain_request( @@ -1345,6 +1420,7 @@ class TestDomainRequestCustomSave(TestCase): self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.FEDERAL) self.assertEqual(domain_request.is_election_board, None) + @less_console_noise_decorator def test_create_or_update_organization_type_existing_instance_updates_election_board(self): """Test create_or_update_organization_type for an existing instance.""" domain_request = completed_domain_request( @@ -1373,6 +1449,7 @@ class TestDomainRequestCustomSave(TestCase): self.assertEqual(domain_request.is_election_board, False) self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY) + @less_console_noise_decorator def test_create_or_update_organization_type_existing_instance_updates_generic_org_type(self): """Test create_or_update_organization_type when modifying generic_org_type on an existing instance.""" domain_request = completed_domain_request( @@ -1409,6 +1486,7 @@ class TestDomainRequestCustomSave(TestCase): domain_request_tribal.organization_type, DomainRequest.OrgChoicesElectionOffice.STATE_OR_TERRITORY_ELECTION ) + @less_console_noise_decorator def test_create_or_update_organization_type_no_update(self): """Test create_or_update_organization_type when there are no values to update.""" @@ -1472,6 +1550,7 @@ class TestDomainInformationCustomSave(TestCase): Domain.objects.all().delete() super().tearDown() + @less_console_noise_decorator def test_create_or_update_organization_type_new_instance(self): """Test create_or_update_organization_type when creating a new instance""" domain_request = completed_domain_request( @@ -1484,6 +1563,7 @@ class TestDomainInformationCustomSave(TestCase): domain_information = DomainInformation.create_from_da(domain_request) self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION) + @less_console_noise_decorator def test_create_or_update_organization_type_new_instance_federal_does_nothing(self): """Test if create_or_update_organization_type does nothing when creating a new instance for federal""" domain_request = completed_domain_request( @@ -1497,6 +1577,7 @@ class TestDomainInformationCustomSave(TestCase): self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.FEDERAL) self.assertEqual(domain_information.is_election_board, None) + @less_console_noise_decorator def test_create_or_update_organization_type_existing_instance_updates_election_board(self): """Test create_or_update_organization_type for an existing instance.""" domain_request = completed_domain_request( @@ -1527,6 +1608,7 @@ class TestDomainInformationCustomSave(TestCase): self.assertEqual(domain_information.is_election_board, False) self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY) + @less_console_noise_decorator def test_create_or_update_organization_type_existing_instance_updates_generic_org_type(self): """Test create_or_update_organization_type when modifying generic_org_type on an existing instance.""" domain_request = completed_domain_request( @@ -1566,6 +1648,7 @@ class TestDomainInformationCustomSave(TestCase): DomainRequest.OrgChoicesElectionOffice.STATE_OR_TERRITORY_ELECTION, ) + @less_console_noise_decorator def test_create_or_update_organization_type_no_update(self): """Test create_or_update_organization_type when there are no values to update.""" @@ -1623,16 +1706,16 @@ class TestDomainInformationCustomSave(TestCase): class TestDomainRequestIncomplete(TestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.factory = RequestFactory() + cls.user = create_test_user() + + @less_console_noise_decorator def setUp(self): super().setUp() - self.factory = RequestFactory() - username = "test_user" - first_name = "First" - last_name = "Last" - email = "info@example.com" - self.user = get_user_model().objects.create( - username=username, first_name=first_name, last_name=last_name, email=email - ) so, _ = Contact.objects.get_or_create( first_name="Meowy", last_name="Meoward", @@ -1657,6 +1740,7 @@ class TestDomainRequestIncomplete(TestCase): ) alt, _ = Website.objects.get_or_create(website="MeowardMeowardMeoward1.gov") current, _ = Website.objects.get_or_create(website="MeowardMeowardMeoward.com") + self.amtrak, _ = FederalAgency.objects.get_or_create(agency="AMTRAK") self.domain_request = DomainRequest.objects.create( generic_org_type=DomainRequest.OrganizationChoices.FEDERAL, federal_type="executive", @@ -1689,13 +1773,21 @@ class TestDomainRequestIncomplete(TestCase): super().tearDown() DomainRequest.objects.all().delete() Contact.objects.all().delete() + self.amtrak.delete() + @classmethod + def tearDownClass(cls): + super().tearDownClass() + cls.user.delete() + + @less_console_noise_decorator def test_is_federal_complete(self): self.assertTrue(self.domain_request._is_federal_complete()) self.domain_request.federal_type = None self.domain_request.save() self.assertFalse(self.domain_request._is_federal_complete()) + @less_console_noise_decorator def test_is_interstate_complete(self): self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.INTERSTATE self.domain_request.about_your_organization = "Something something about your organization" @@ -1705,6 +1797,7 @@ class TestDomainRequestIncomplete(TestCase): self.domain_request.save() self.assertFalse(self.domain_request._is_interstate_complete()) + @less_console_noise_decorator def test_is_state_or_territory_complete(self): self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.STATE_OR_TERRITORY self.domain_request.is_election_board = True @@ -1715,6 +1808,7 @@ class TestDomainRequestIncomplete(TestCase): # is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election self.assertTrue(self.domain_request._is_state_or_territory_complete()) + @less_console_noise_decorator def test_is_tribal_complete(self): self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.TRIBAL self.domain_request.tribe_name = "Tribe Name" @@ -1727,6 +1821,7 @@ class TestDomainRequestIncomplete(TestCase): # is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election self.assertFalse(self.domain_request._is_tribal_complete()) + @less_console_noise_decorator def test_is_county_complete(self): self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.COUNTY self.domain_request.is_election_board = False @@ -1737,6 +1832,7 @@ class TestDomainRequestIncomplete(TestCase): # is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election self.assertTrue(self.domain_request._is_county_complete()) + @less_console_noise_decorator def test_is_city_complete(self): self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.CITY self.domain_request.is_election_board = False @@ -1747,6 +1843,7 @@ class TestDomainRequestIncomplete(TestCase): # is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election self.assertTrue(self.domain_request._is_city_complete()) + @less_console_noise_decorator def test_is_special_district_complete(self): self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.SPECIAL_DISTRICT self.domain_request.about_your_organization = "Something something about your organization" @@ -1759,6 +1856,7 @@ class TestDomainRequestIncomplete(TestCase): # is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election self.assertFalse(self.domain_request._is_special_district_complete()) + @less_console_noise_decorator def test_is_organization_name_and_address_complete(self): self.assertTrue(self.domain_request._is_organization_name_and_address_complete()) self.domain_request.organization_name = None @@ -1766,30 +1864,35 @@ class TestDomainRequestIncomplete(TestCase): self.domain_request.save() self.assertTrue(self.domain_request._is_organization_name_and_address_complete()) + @less_console_noise_decorator def test_is_senior_official_complete(self): self.assertTrue(self.domain_request._is_senior_official_complete()) self.domain_request.senior_official = None self.domain_request.save() self.assertFalse(self.domain_request._is_senior_official_complete()) + @less_console_noise_decorator def test_is_requested_domain_complete(self): self.assertTrue(self.domain_request._is_requested_domain_complete()) self.domain_request.requested_domain = None self.domain_request.save() self.assertFalse(self.domain_request._is_requested_domain_complete()) + @less_console_noise_decorator def test_is_purpose_complete(self): self.assertTrue(self.domain_request._is_purpose_complete()) self.domain_request.purpose = None self.domain_request.save() self.assertFalse(self.domain_request._is_purpose_complete()) + @less_console_noise_decorator def test_is_submitter_complete(self): self.assertTrue(self.domain_request._is_submitter_complete()) self.domain_request.submitter = None self.domain_request.save() self.assertFalse(self.domain_request._is_submitter_complete()) + @less_console_noise_decorator def test_is_other_contacts_complete_missing_one_field(self): self.assertTrue(self.domain_request._is_other_contacts_complete()) contact = self.domain_request.other_contacts.first() @@ -1797,10 +1900,12 @@ class TestDomainRequestIncomplete(TestCase): contact.save() self.assertFalse(self.domain_request._is_other_contacts_complete()) + @less_console_noise_decorator def test_is_other_contacts_complete_all_none(self): self.domain_request.other_contacts.clear() self.assertFalse(self.domain_request._is_other_contacts_complete()) + @less_console_noise_decorator def test_is_other_contacts_False_and_has_rationale(self): # Click radio button "No" for no other contacts and give rationale self.domain_request.other_contacts.clear() @@ -1808,6 +1913,7 @@ class TestDomainRequestIncomplete(TestCase): self.domain_request.no_other_contacts_rationale = "Some rationale" self.assertTrue(self.domain_request._is_other_contacts_complete()) + @less_console_noise_decorator def test_is_other_contacts_False_and_NO_rationale(self): # Click radio button "No" for no other contacts and DONT give rationale self.domain_request.other_contacts.clear() @@ -1815,6 +1921,7 @@ class TestDomainRequestIncomplete(TestCase): self.domain_request.no_other_contacts_rationale = None self.assertFalse(self.domain_request._is_other_contacts_complete()) + @less_console_noise_decorator def test_is_additional_details_complete(self): test_cases = [ # CISA Rep - Yes @@ -2021,6 +2128,7 @@ class TestDomainRequestIncomplete(TestCase): msg=f"Failed for case: {case}", ) + @less_console_noise_decorator def test_is_policy_acknowledgement_complete(self): self.assertTrue(self.domain_request._is_policy_acknowledgement_complete()) self.domain_request.is_policy_acknowledged = False @@ -2028,6 +2136,7 @@ class TestDomainRequestIncomplete(TestCase): self.domain_request.is_policy_acknowledged = None self.assertFalse(self.domain_request._is_policy_acknowledgement_complete()) + @less_console_noise_decorator def test_form_complete(self): request = self.factory.get("/") request.user = self.user diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index ab21d60b3..74b84834e 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -1,12 +1,16 @@ import io from django.test import Client, RequestFactory from io import StringIO -from registrar.models.domain_request import DomainRequest -from registrar.models.domain import Domain +from registrar.models import ( + DomainRequest, + Domain, + UserDomainRole, +) from registrar.utility.csv_export import ( DomainDataFull, DomainDataType, DomainDataFederal, + DomainDataTypeUser, DomainGrowth, DomainManaged, DomainUnmanaged, @@ -27,14 +31,14 @@ import boto3_mocking from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # type: ignore from django.utils import timezone from api.tests.common import less_console_noise_decorator -from .common import MockDb, MockEppLib, less_console_noise, get_time_aware_date +from .common import MockDbForSharedTests, MockDbForIndividualTests, MockEppLib, less_console_noise, get_time_aware_date -class CsvReportsTest(MockDb): - """Tests to determine if we are uploading our reports correctly""" +class CsvReportsTest(MockDbForSharedTests): + """Tests to determine if we are uploading our reports correctly.""" def setUp(self): - """Create fake domain data""" + """setup fake comain data""" super().setUp() self.client = Client(HTTP_HOST="localhost:8080") self.factory = RequestFactory() @@ -198,17 +202,13 @@ class CsvReportsTest(MockDb): self.assertEqual(expected_file_content, response.content) -class ExportDataTest(MockDb, MockEppLib): - def setUp(self): - super().setUp() - - def tearDown(self): - super().tearDown() +class ExportDataTest(MockDbForIndividualTests, MockEppLib): + """Test the ExportData class from csv_export.""" @less_console_noise_decorator def test_domain_data_type(self): """Shows security contacts, domain managers, so""" - self.maxDiff = None + # Add security email information self.domain_1.name = "defaultsecurity.gov" self.domain_1.save() @@ -237,7 +237,7 @@ class ExportDataTest(MockDb, MockEppLib): "cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,World War I Centennial Commission,,,,(blank),,," "meoward@rocks.com,\n" "defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,World War I Centennial Commission,,," - ',,,(blank),"meoward@rocks.com, info@example.com, big_lebowski@dude.co",' + ',,,(blank),"big_lebowski@dude.co, info@example.com, meoward@rocks.com",' "woofwardthethird@rocks.com\n" "adomain10.gov,Ready,2024-04-03,(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,," "squeaker@rocks.com\n" @@ -260,6 +260,57 @@ class ExportDataTest(MockDb, MockEppLib): self.maxDiff = None self.assertEqual(csv_content, expected_content) + @less_console_noise_decorator + def test_domain_data_type_user(self): + """Shows security contacts, domain managers, so for the current user""" + + # Add security email information + self.domain_1.name = "defaultsecurity.gov" + self.domain_1.save() + # Invoke setter + self.domain_1.security_contact + self.domain_2.security_contact + self.domain_3.security_contact + # Add a first ready date on the first domain. Leaving the others blank. + self.domain_1.first_ready = get_default_start_date() + self.domain_1.save() + + # Create a user and associate it with some domains + UserDomainRole.objects.create(user=self.user, domain=self.domain_2) + + # Create a request object + factory = RequestFactory() + request = factory.get("/") + request.user = self.user + + # Create a CSV file in memory + csv_file = StringIO() + # Call the export functions + DomainDataTypeUser.export_data_to_csv(csv_file, request=request) + # Reset the CSV file's position to the beginning + csv_file.seek(0) + # Read the content into a variable + csv_content = csv_file.read() + + # We expect only domains associated with the user + expected_content = ( + "Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name," + "City,State,SO,SO email," + "Security contact email,Domain managers,Invited domain managers\n" + "defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,World War I Centennial Commission,,,, ,," + '(blank),"big_lebowski@dude.co, info@example.com, meoward@rocks.com",' + "woofwardthethird@rocks.com\n" + "adomain2.gov,Dns needed,(blank),(blank),Interstate,,,,, ,,(blank)," + '"info@example.com, meoward@rocks.com",squeaker@rocks.com\n' + ) + + # Normalize line endings and remove commas, + # spaces and leading/trailing whitespace + csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() + expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() + self.maxDiff = None + self.assertEqual(csv_content, expected_content) + @less_console_noise_decorator def test_domain_data_full(self): """Shows security contacts, filtered by state""" @@ -370,8 +421,8 @@ class ExportDataTest(MockDb, MockEppLib): # Call the export functions DomainGrowth.export_data_to_csv( csv_file, - self.start_date.strftime("%Y-%m-%d"), - self.end_date.strftime("%Y-%m-%d"), + start_date=self.start_date.strftime("%Y-%m-%d"), + end_date=self.end_date.strftime("%Y-%m-%d"), ) # Reset the CSV file's position to the beginning csv_file.seek(0) @@ -412,8 +463,8 @@ class ExportDataTest(MockDb, MockEppLib): # Call the export functions DomainManaged.export_data_to_csv( csv_file, - self.start_date.strftime("%Y-%m-%d"), - self.end_date.strftime("%Y-%m-%d"), + start_date=self.start_date.strftime("%Y-%m-%d"), + end_date=self.end_date.strftime("%Y-%m-%d"), ) # Reset the CSV file's position to the beginning csv_file.seek(0) @@ -433,7 +484,7 @@ class ExportDataTest(MockDb, MockEppLib): "\n" "Domain name,Domain type,Domain managers,Invited domain managers\n" "cdomain11.gov,Federal - Executive,meoward@rocks.com,\n" - 'cdomain1.gov,Federal - Executive,"meoward@rocks.com, info@example.com, big_lebowski@dude.co",' + 'cdomain1.gov,Federal - Executive,"big_lebowski@dude.co, info@example.com, meoward@rocks.com",' "woofwardthethird@rocks.com\n" "zdomain12.gov,Interstate,meoward@rocks.com,\n" ) @@ -441,6 +492,7 @@ class ExportDataTest(MockDb, MockEppLib): # spaces and leading/trailing whitespace csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() + self.maxDiff = None self.assertEqual(csv_content, expected_content) @less_console_noise_decorator @@ -449,7 +501,7 @@ class ExportDataTest(MockDb, MockEppLib): # Create a CSV file in memory csv_file = StringIO() DomainUnmanaged.export_data_to_csv( - csv_file, self.start_date.strftime("%Y-%m-%d"), self.end_date.strftime("%Y-%m-%d") + csv_file, start_date=self.start_date.strftime("%Y-%m-%d"), end_date=self.end_date.strftime("%Y-%m-%d") ) # Reset the CSV file's position to the beginning @@ -496,8 +548,8 @@ class ExportDataTest(MockDb, MockEppLib): # Call the export functions DomainRequestGrowth.export_data_to_csv( csv_file, - self.start_date.strftime("%Y-%m-%d"), - self.end_date.strftime("%Y-%m-%d"), + start_date=self.start_date.strftime("%Y-%m-%d"), + end_date=self.end_date.strftime("%Y-%m-%d"), ) # Reset the CSV file's position to the beginning csv_file.seek(0) @@ -595,7 +647,7 @@ class ExportDataTest(MockDb, MockEppLib): self.assertEqual(csv_content, expected_content) -class HelperFunctions(MockDb): +class HelperFunctions(MockDbForSharedTests): """This asserts that 1=1. Its limited usefulness lies in making sure the helper methods stay healthy.""" def test_get_default_start_date(self): diff --git a/src/registrar/tests/test_transition_domain_migrations.py b/src/registrar/tests/test_transition_domain_migrations.py index c7f916976..be7bcf9e4 100644 --- a/src/registrar/tests/test_transition_domain_migrations.py +++ b/src/registrar/tests/test_transition_domain_migrations.py @@ -43,7 +43,6 @@ class TestProcessedMigrations(TestCase): DomainInformation.objects.all().delete() DomainInvitation.objects.all().delete() TransitionDomain.objects.all().delete() - FederalAgency.objects.all().delete() # Delete users User.objects.all().delete() @@ -185,6 +184,7 @@ class TestOrganizationMigration(TestCase): """Defines the file name of migration_json and the folder its contained in""" self.test_data_file_location = "registrar/tests/data" self.migration_json_filename = "test_migrationFilepaths.json" + self.federal_agency, _ = FederalAgency.objects.get_or_create(agency="Department of Commerce") def tearDown(self): """Deletes all DB objects related to migrations""" @@ -197,6 +197,7 @@ class TestOrganizationMigration(TestCase): # Delete users User.objects.all().delete() UserDomainRole.objects.all().delete() + self.federal_agency.delete() def run_load_domains(self): """ @@ -331,7 +332,6 @@ class TestOrganizationMigration(TestCase): # Lets test the first one transition = transition_domains.first() - federal_agency, _ = FederalAgency.objects.get_or_create(agency="Department of Commerce") expected_transition_domain = TransitionDomain( username="alexandra.bobbitt5@test.com", domain_name="fakewebsite2.gov", @@ -340,7 +340,7 @@ class TestOrganizationMigration(TestCase): generic_org_type="Federal", organization_name="Fanoodle", federal_type="Executive", - federal_agency=federal_agency, + federal_agency=self.federal_agency, epp_creation_date=datetime.date(2004, 5, 7), epp_expiration_date=datetime.date(2023, 9, 30), first_name="Seline", @@ -395,7 +395,6 @@ class TestOrganizationMigration(TestCase): # == Third, test that we've loaded data as we expect == # _domain = Domain.objects.filter(name="fakewebsite2.gov").get() domain_information = DomainInformation.objects.filter(domain=_domain).get() - federal_agency, _ = FederalAgency.objects.get_or_create(agency="Department of Commerce") expected_creator = User.objects.filter(username="System").get() expected_so = Contact.objects.filter( @@ -404,7 +403,7 @@ class TestOrganizationMigration(TestCase): expected_domain_information = DomainInformation( creator=expected_creator, generic_org_type="federal", - federal_agency=federal_agency, + federal_agency=self.federal_agency, federal_type="executive", organization_name="Fanoodle", address_line1="93001 Arizona Drive", @@ -451,7 +450,6 @@ class TestOrganizationMigration(TestCase): # == Fourth, test that no data is overwritten as we expect == # _domain = Domain.objects.filter(name="fakewebsite2.gov").get() domain_information = DomainInformation.objects.filter(domain=_domain).get() - federal_agency, _ = FederalAgency.objects.get_or_create(agency="Department of Commerce") expected_creator = User.objects.filter(username="System").get() expected_so = Contact.objects.filter( @@ -460,7 +458,7 @@ class TestOrganizationMigration(TestCase): expected_domain_information = DomainInformation( creator=expected_creator, generic_org_type="federal", - federal_agency=federal_agency, + federal_agency=self.federal_agency, federal_type="executive", organization_name="Fanoodle", address_line1="93001 Galactic Way", diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index df24648c7..694b5bf29 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -8,14 +8,14 @@ from api.tests.common import less_console_noise_decorator from registrar.models.contact import Contact from registrar.models.domain import Domain from registrar.models.draft_domain import DraftDomain +from registrar.models.federal_agency import FederalAgency from registrar.models.portfolio import Portfolio from registrar.models.public_contact import PublicContact from registrar.models.user import User from registrar.models.user_domain_role import UserDomainRole from registrar.views.domain import DomainNameserversView from registrar.models import SeniorOfficial, Suborganization - -from .common import MockEppLib, less_console_noise # type: ignore +from .common import MockEppLib, create_test_user, less_console_noise # type: ignore from unittest.mock import patch from django.urls import reverse @@ -31,18 +31,23 @@ logger = logging.getLogger(__name__) class TestViews(TestCase): + def setUp(self): + super().setUp() self.client = Client() + @less_console_noise_decorator def test_health_check_endpoint(self): response = self.client.get("/health") self.assertContains(response, "OK", status_code=200) + @less_console_noise_decorator def test_home_page(self): """Home page should NOT be available without a login.""" response = self.client.get("/") self.assertEqual(response.status_code, 302) + @less_console_noise_decorator def test_domain_request_form_not_logged_in(self): """Domain request form not accessible without a logged-in user.""" response = self.client.get("/request/") @@ -51,43 +56,23 @@ class TestViews(TestCase): class TestWithUser(MockEppLib): + """Class for executing tests with a test user. + Note that tests share the test user within their test class, so the user + cannot be changed within a test.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.user = create_test_user() + def setUp(self): super().setUp() - username = "test_user" - first_name = "First" - last_name = "Last" - email = "info@example.com" - phone = "8003111234" - title = "test title" - self.user = get_user_model().objects.create( - username=username, first_name=first_name, last_name=last_name, title=title, email=email, phone=phone - ) + self.client = Client() - username_regular_incomplete = "test_regular_user_incomplete" - username_other_incomplete = "test_other_user_incomplete" - first_name_2 = "Incomplete" - email_2 = "unicorn@igorville.com" - # in the case below, REGULAR user is 'Verified by Login.gov, ie. IAL2 - self.incomplete_regular_user = get_user_model().objects.create( - username=username_regular_incomplete, - first_name=first_name_2, - email=email_2, - verification_type=User.VerificationTypeChoices.REGULAR, - ) - # in the case below, other user is representative of GRANDFATHERED, - # VERIFIED_BY_STAFF, INVITED, FIXTURE_USER, ie. IAL1 - self.incomplete_other_user = get_user_model().objects.create( - username=username_other_incomplete, - first_name=first_name_2, - email=email_2, - verification_type=User.VerificationTypeChoices.VERIFIED_BY_STAFF, - ) - - def tearDown(self): - # delete any domain requests too - super().tearDown() - DomainRequest.objects.all().delete() - DomainInformation.objects.all().delete() + @classmethod + def tearDownClass(cls): + super().tearDownClass() + User.objects.all().delete() # For some reason, if this is done on the test directly, # we get a django.db.models.deletion.ProtectedError on "User". # In either event, it doesn't hurt to have these here given their @@ -96,38 +81,43 @@ class TestWithUser(MockEppLib): Portfolio.objects.all().delete() SeniorOfficial.objects.all().delete() - User.objects.all().delete() - class TestEnvironmentVariablesEffects(TestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.user = create_test_user() + def setUp(self): self.client = Client() - username = "test_user" - first_name = "First" - last_name = "Last" - email = "info@example.com" - self.user = get_user_model().objects.create( - username=username, first_name=first_name, last_name=last_name, email=email - ) self.client.force_login(self.user) def tearDown(self): super().tearDown() + UserDomainRole.objects.all().delete() Domain.objects.all().delete() - self.user.delete() + @classmethod + def tearDownClass(cls): + super().tearDownClass() + User.objects.all().delete() + + @less_console_noise_decorator @override_settings(IS_PRODUCTION=True) def test_production_environment(self): """No banner on prod.""" home_page = self.client.get("/") self.assertNotContains(home_page, "You are on a test site.") + @less_console_noise_decorator @override_settings(IS_PRODUCTION=False) def test_non_production_environment(self): """Banner on non-prod.""" home_page = self.client.get("/") self.assertContains(home_page, "You are on a test site.") + @less_console_noise_decorator def side_effect_raise_value_error(self): """Side effect that raises a 500 error""" raise ValueError("Some error") @@ -139,9 +129,7 @@ class TestEnvironmentVariablesEffects(TestCase): fake_domain, _ = Domain.objects.get_or_create(name="igorville.gov") # Add a role - fake_role, _ = UserDomainRole.objects.get_or_create( - user=self.user, domain=fake_domain, role=UserDomainRole.Roles.MANAGER - ) + UserDomainRole.objects.get_or_create(user=self.user, domain=fake_domain, role=UserDomainRole.Roles.MANAGER) with patch.object(DomainNameserversView, "get_initial", side_effect=self.side_effect_raise_value_error): with self.assertRaises(ValueError): @@ -162,9 +150,7 @@ class TestEnvironmentVariablesEffects(TestCase): fake_domain, _ = Domain.objects.get_or_create(name="igorville.gov") # Add a role - fake_role, _ = UserDomainRole.objects.get_or_create( - user=self.user, domain=fake_domain, role=UserDomainRole.Roles.MANAGER - ) + UserDomainRole.objects.get_or_create(user=self.user, domain=fake_domain, role=UserDomainRole.Roles.MANAGER) with patch.object(DomainNameserversView, "get_initial", side_effect=self.side_effect_raise_value_error): with self.assertRaises(ValueError): @@ -185,15 +171,13 @@ class HomeTests(TestWithUser): super().setUp() self.client.force_login(self.user) - def tearDown(self): - super().tearDown() - Contact.objects.all().delete() - + @less_console_noise_decorator def test_empty_domain_table(self): response = self.client.get("/") self.assertContains(response, "You don't have any registered domains.") self.assertContains(response, "Why don't I see my domain when I sign in to the registrar?") + @less_console_noise_decorator def test_state_help_text(self): """Tests if each domain state has help text""" @@ -235,6 +219,7 @@ class HomeTests(TestWithUser): user_role.delete() test_domain.delete() + @less_console_noise_decorator def test_state_help_text_expired(self): """Tests if each domain state has help text when expired""" expired_text = "This domain has expired, but it is still online. " @@ -242,7 +227,9 @@ class HomeTests(TestWithUser): test_domain.expiration_date = date(2011, 10, 10) test_domain.save() - UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER) + test_role, _ = UserDomainRole.objects.get_or_create( + user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER + ) # Grab the json response of the domains list response = self.client.get("/get-domains-json/") @@ -253,6 +240,10 @@ class HomeTests(TestWithUser): # Check that we have the right text content. self.assertContains(response, expired_text, count=1) + test_role.delete() + test_domain.delete() + + @less_console_noise_decorator def test_state_help_text_no_expiration_date(self): """Tests if each domain state has help text when expiration date is None""" @@ -296,6 +287,10 @@ class HomeTests(TestWithUser): # Check that we have the right text content. self.assertContains(response, unknown_text, count=1) + UserDomainRole.objects.all().delete() + Domain.objects.all().delete() + + @less_console_noise_decorator def test_home_deletes_withdrawn_domain_request(self): """Tests if the user can delete a DomainRequest in the 'withdrawn' status""" @@ -312,6 +307,7 @@ class HomeTests(TestWithUser): # clean up domain_request.delete() + @less_console_noise_decorator def test_home_deletes_started_domain_request(self): """Tests if the user can delete a DomainRequest in the 'started' status""" @@ -361,6 +357,7 @@ class HomeTests(TestWithUser): # clean up domain_request.delete() + @less_console_noise_decorator def test_home_deletes_domain_request_and_orphans(self): """Tests if delete for DomainRequest deletes orphaned Contact objects""" @@ -430,6 +427,10 @@ class HomeTests(TestWithUser): self.assertEqual(edge_case, contact_2) + DomainRequest.objects.all().delete() + Contact.objects.all().delete() + + @less_console_noise_decorator def test_home_deletes_domain_request_and_shared_orphans(self): """Test the edge case for an object that will become orphaned after a delete (but is not an orphan at the time of deletion)""" @@ -490,6 +491,10 @@ class HomeTests(TestWithUser): orphan = Contact.objects.filter(id=contact_shared.id) self.assertFalse(orphan.exists()) + DomainRequest.objects.all().delete() + Contact.objects.all().delete() + + @less_console_noise_decorator def test_domain_request_form_view(self): response = self.client.get("/request/", follow=True) self.assertContains( @@ -497,16 +502,24 @@ class HomeTests(TestWithUser): "You’re about to start your .gov domain request.", ) + @less_console_noise_decorator def test_domain_request_form_with_ineligible_user(self): """Domain request form not accessible for an ineligible user. This test should be solid enough since all domain request wizard views share the same permissions class""" - self.user.status = User.RESTRICTED - self.user.save() - - with less_console_noise(): - response = self.client.get("/request/", follow=True) - self.assertEqual(response.status_code, 403) + username = "restricted_user" + first_name = "First" + last_name = "Last" + email = "restricted@example.com" + phone = "8003111234" + status = User.RESTRICTED + restricted_user = get_user_model().objects.create( + username=username, first_name=first_name, last_name=last_name, email=email, phone=phone, status=status + ) + self.client.force_login(restricted_user) + response = self.client.get("/request/", follow=True) + self.assertEqual(response.status_code, 403) + restricted_user.delete() class FinishUserProfileTests(TestWithUser, WebTest): @@ -518,6 +531,7 @@ class FinishUserProfileTests(TestWithUser, WebTest): def setUp(self): super().setUp() + self.initial_user_title = self.user.title self.user.title = None self.user.save() self.client.force_login(self.user) @@ -528,6 +542,10 @@ class FinishUserProfileTests(TestWithUser, WebTest): def tearDown(self): super().tearDown() + DomainRequest.objects.all().delete() + DomainInformation.objects.all().delete() + self.user.title = self.initial_user_title + self.user.save() PublicContact.objects.filter(domain=self.domain).delete() self.role.delete() self.domain.delete() @@ -552,48 +570,70 @@ class FinishUserProfileTests(TestWithUser, WebTest): def test_full_name_initial_value(self): """Test that full_name initial value is empty when first_name or last_name is empty. This will later be displayed as "unknown" using javascript.""" - self.app.set_user(self.incomplete_regular_user.username) + username_regular_incomplete = "test_regular_user_incomplete" + first_name_2 = "Incomplete" + email_2 = "unicorn@igorville.com" + incomplete_regular_user = get_user_model().objects.create( + username=username_regular_incomplete, + first_name=first_name_2, + email=email_2, + verification_type=User.VerificationTypeChoices.REGULAR, + ) + self.app.set_user(incomplete_regular_user.username) # Test when first_name is empty - self.incomplete_regular_user.first_name = "" - self.incomplete_regular_user.last_name = "Doe" - self.incomplete_regular_user.save() + incomplete_regular_user.first_name = "" + incomplete_regular_user.last_name = "Doe" + incomplete_regular_user.save() finish_setup_page = self.app.get(reverse("home")).follow() form = finish_setup_page.form self.assertEqual(form["full_name"].value, "") # Test when last_name is empty - self.incomplete_regular_user.first_name = "John" - self.incomplete_regular_user.last_name = "" - self.incomplete_regular_user.save() + incomplete_regular_user.first_name = "John" + incomplete_regular_user.last_name = "" + incomplete_regular_user.save() finish_setup_page = self.app.get(reverse("home")).follow() form = finish_setup_page.form self.assertEqual(form["full_name"].value, "") # Test when both first_name and last_name are empty - self.incomplete_regular_user.first_name = "" - self.incomplete_regular_user.last_name = "" - self.incomplete_regular_user.save() + incomplete_regular_user.first_name = "" + incomplete_regular_user.last_name = "" + incomplete_regular_user.save() finish_setup_page = self.app.get(reverse("home")).follow() form = finish_setup_page.form self.assertEqual(form["full_name"].value, "") # Test when both first_name and last_name are present - self.incomplete_regular_user.first_name = "John" - self.incomplete_regular_user.last_name = "Doe" - self.incomplete_regular_user.save() + incomplete_regular_user.first_name = "John" + incomplete_regular_user.last_name = "Doe" + incomplete_regular_user.save() finish_setup_page = self.app.get(reverse("home")).follow() form = finish_setup_page.form self.assertEqual(form["full_name"].value, "John Doe") + incomplete_regular_user.delete() + @less_console_noise_decorator def test_new_user_with_profile_feature_on(self): """Tests that a new user is redirected to the profile setup page when profile_feature is on""" - self.app.set_user(self.incomplete_regular_user.username) + username_regular_incomplete = "test_regular_user_incomplete" + first_name_2 = "Incomplete" + email_2 = "unicorn@igorville.com" + # in the case below, REGULAR user is 'Verified by Login.gov, ie. IAL2 + incomplete_regular_user = get_user_model().objects.create( + username=username_regular_incomplete, + first_name=first_name_2, + email=email_2, + verification_type=User.VerificationTypeChoices.REGULAR, + ) + + self.app.set_user(incomplete_regular_user.username) with override_flag("profile_feature", active=True): # This will redirect the user to the setup page. # Follow implicity checks if our redirect is working. @@ -627,14 +667,22 @@ class FinishUserProfileTests(TestWithUser, WebTest): # This is the same as clicking the back button. completed_setup_page = self.app.get(reverse("home")) self.assertContains(completed_setup_page, "Manage your domain") + incomplete_regular_user.delete() @less_console_noise_decorator def test_new_user_with_empty_name_can_add_name(self): """Tests that a new user without a name can still enter this information accordingly""" - self.incomplete_regular_user.first_name = "" - self.incomplete_regular_user.last_name = "" - self.incomplete_regular_user.save() - self.app.set_user(self.incomplete_regular_user.username) + username_regular_incomplete = "test_regular_user_incomplete" + email = "unicorn@igorville.com" + # in the case below, REGULAR user is 'Verified by Login.gov, ie. IAL2 + incomplete_regular_user = get_user_model().objects.create( + username=username_regular_incomplete, + first_name="", + last_name="", + email=email, + verification_type=User.VerificationTypeChoices.REGULAR, + ) + self.app.set_user(incomplete_regular_user.username) with override_flag("profile_feature", active=True): # This will redirect the user to the setup page. # Follow implicity checks if our redirect is working. @@ -670,12 +718,22 @@ class FinishUserProfileTests(TestWithUser, WebTest): # This is the same as clicking the back button. completed_setup_page = self.app.get(reverse("home")) self.assertContains(completed_setup_page, "Manage your domain") + incomplete_regular_user.delete() @less_console_noise_decorator def test_new_user_goes_to_domain_request_with_profile_feature_on(self): """Tests that a new user is redirected to the domain request page when profile_feature is on""" - - self.app.set_user(self.incomplete_regular_user.username) + username_regular_incomplete = "test_regular_user_incomplete" + first_name_2 = "Incomplete" + email_2 = "unicorn@igorville.com" + # in the case below, REGULAR user is 'Verified by Login.gov, ie. IAL2 + incomplete_regular_user = get_user_model().objects.create( + username=username_regular_incomplete, + first_name=first_name_2, + email=email_2, + verification_type=User.VerificationTypeChoices.REGULAR, + ) + self.app.set_user(incomplete_regular_user.username) with override_flag("profile_feature", active=True): # This will redirect the user to the setup page finish_setup_page = self.app.get(reverse("domain-request:")).follow() @@ -718,6 +776,7 @@ class FinishUserProfileTests(TestWithUser, WebTest): self.assertNotContains(completed_setup_page, "What contact information should we use to reach you?") self.assertContains(completed_setup_page, "You’re about to start your .gov domain request") + incomplete_regular_user.delete() @less_console_noise_decorator def test_new_user_with_profile_feature_off(self): @@ -748,6 +807,7 @@ class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest): def setUp(self): super().setUp() + self.initial_user_title = self.user.title self.user.title = None self.user.save() self.client.force_login(self.user) @@ -758,6 +818,8 @@ class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest): def tearDown(self): super().tearDown() + self.user.title = self.initial_user_title + self.user.save() PublicContact.objects.filter(domain=self.domain).delete() self.role.delete() Domain.objects.all().delete() @@ -777,7 +839,18 @@ class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest): def test_new_user_with_profile_feature_on(self): """Tests that a new user is redirected to the profile setup page when profile_feature is on, and testing that the confirmation modal is present""" - self.app.set_user(self.incomplete_other_user.username) + username_other_incomplete = "test_other_user_incomplete" + first_name_2 = "Incomplete" + email_2 = "unicorn@igorville.com" + # in the case below, other user is representative of GRANDFATHERED, + # VERIFIED_BY_STAFF, INVITED, FIXTURE_USER, ie. IAL1 + incomplete_other_user = get_user_model().objects.create( + username=username_other_incomplete, + first_name=first_name_2, + email=email_2, + verification_type=User.VerificationTypeChoices.VERIFIED_BY_STAFF, + ) + self.app.set_user(incomplete_other_user.username) with override_flag("profile_feature", active=True): # This will redirect the user to the user profile page. # Follow implicity checks if our redirect is working. @@ -856,9 +929,10 @@ class UserProfileTests(TestWithUser, WebTest): PublicContact.objects.filter(domain=self.domain).delete() self.role.delete() self.domain.delete() - Contact.objects.all().delete() - DraftDomain.objects.all().delete() DomainRequest.objects.all().delete() + DraftDomain.objects.all().delete() + Contact.objects.all().delete() + DomainInformation.objects.all().delete() @less_console_noise_decorator def error_500_main_nav_with_profile_feature_turned_on(self): @@ -1032,16 +1106,19 @@ class PortfoliosTests(TestWithUser, WebTest): def setUp(self): super().setUp() - self.user.save() self.client.force_login(self.user) self.domain, _ = Domain.objects.get_or_create(name="sampledomain.gov", state=Domain.State.READY) self.role, _ = UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER ) - self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="xyz inc") + self.federal_agency = FederalAgency.objects.create() + self.portfolio, _ = Portfolio.objects.get_or_create( + creator=self.user, organization_name="xyz inc", federal_agency=self.federal_agency + ) def tearDown(self): Portfolio.objects.all().delete() + self.federal_agency.delete() super().tearDown() PublicContact.objects.filter(domain=self.domain).delete() UserDomainRole.objects.all().delete() diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index 081ddaa33..d489bc683 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -5,6 +5,7 @@ from django.conf import settings from django.urls import reverse from django.contrib.auth import get_user_model from waffle.testutils import override_flag +from api.tests.common import less_console_noise_decorator from .common import MockEppLib, MockSESClient, create_user # type: ignore from django_webtest import WebTest # type: ignore import boto3_mocking # type: ignore @@ -48,6 +49,7 @@ logger = logging.getLogger(__name__) class TestWithDomainPermissions(TestWithUser): + @less_console_noise_decorator def setUp(self): super().setUp() self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") @@ -145,6 +147,7 @@ class TestWithDomainPermissions(TestWithUser): class TestDomainPermissions(TestWithDomainPermissions): + @less_console_noise_decorator def test_not_logged_in(self): """Not logged in gets a redirect to Login.""" for view_name in [ @@ -161,6 +164,7 @@ class TestDomainPermissions(TestWithDomainPermissions): response = self.client.get(reverse(view_name, kwargs={"pk": self.domain.id})) self.assertEqual(response.status_code, 302) + @less_console_noise_decorator def test_no_domain_role(self): """Logged in but no role gets 403 Forbidden.""" self.client.force_login(self.user) @@ -177,10 +181,10 @@ class TestDomainPermissions(TestWithDomainPermissions): "domain-security-email", ]: with self.subTest(view_name=view_name): - with less_console_noise(): - response = self.client.get(reverse(view_name, kwargs={"pk": self.domain.id})) + response = self.client.get(reverse(view_name, kwargs={"pk": self.domain.id})) self.assertEqual(response.status_code, 403) + @less_console_noise_decorator def test_domain_pages_blocked_for_on_hold_and_deleted(self): """Test that the domain pages are blocked for on hold and deleted domains""" @@ -202,12 +206,12 @@ class TestDomainPermissions(TestWithDomainPermissions): self.domain_deleted, ]: with self.subTest(view_name=view_name, domain=domain): - with less_console_noise(): - response = self.client.get(reverse(view_name, kwargs={"pk": domain.id})) - self.assertEqual(response.status_code, 403) + response = self.client.get(reverse(view_name, kwargs={"pk": domain.id})) + self.assertEqual(response.status_code, 403) class TestDomainOverview(TestWithDomainPermissions, WebTest): + def setUp(self): super().setUp() self.app.set_user(self.user.username) @@ -315,21 +319,25 @@ class TestDomainManagers(TestDomainOverview): """Ensure that the user has its original permissions""" super().tearDown() + @less_console_noise_decorator def test_domain_managers(self): response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) self.assertContains(response, "Domain managers") + @less_console_noise_decorator 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})) add_page = management_page.click("Add a domain manager") self.assertContains(add_page, "Add a domain manager") + @less_console_noise_decorator def test_domain_user_add(self): response = self.client.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) self.assertContains(response, "Add a domain manager") @boto3_mocking.patching + @less_console_noise_decorator def test_domain_user_add_form(self): """Adding an existing user works.""" other_user, _ = get_user_model().objects.get_or_create(email="mayor@igorville.gov") @@ -356,6 +364,7 @@ class TestDomainManagers(TestDomainOverview): self.assertContains(success_page, "mayor@igorville.gov") @boto3_mocking.patching + @less_console_noise_decorator def test_domain_invitation_created(self): """Add user on a nonexistent email creates an invitation. @@ -386,6 +395,7 @@ class TestDomainManagers(TestDomainOverview): self.assertTrue(DomainInvitation.objects.filter(email=email_address).exists()) @boto3_mocking.patching + @less_console_noise_decorator def test_domain_invitation_created_for_caps_email(self): """Add user on a nonexistent email with CAPS creates an invitation to lowercase email. @@ -406,8 +416,7 @@ class TestDomainManagers(TestDomainOverview): mock_client = MockSESClient() with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - success_result = add_page.form.submit() + success_result = add_page.form.submit() self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) success_page = success_result.follow() @@ -417,6 +426,7 @@ class TestDomainManagers(TestDomainOverview): self.assertTrue(DomainInvitation.objects.filter(email=email_address).exists()) @boto3_mocking.patching + @less_console_noise_decorator def test_domain_invitation_email_sent(self): """Inviting a non-existent user sends them an email.""" # make sure there is no user with this email @@ -428,12 +438,11 @@ class TestDomainManagers(TestDomainOverview): mock_client = MagicMock() mock_client_instance = mock_client.return_value with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit() + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit() # check the mock instance to see if `send_email` was called right mock_client_instance.send_email.assert_called_once_with( @@ -443,6 +452,7 @@ class TestDomainManagers(TestDomainOverview): ) @boto3_mocking.patching + @less_console_noise_decorator def test_domain_invitation_email_has_email_as_requestor_non_existent(self): """Inviting a non existent user sends them an email, with email as the name.""" # make sure there is no user with this email @@ -455,12 +465,11 @@ class TestDomainManagers(TestDomainOverview): mock_client_instance = mock_client.return_value with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit() + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit() # check the mock instance to see if `send_email` was called right mock_client_instance.send_email.assert_called_once_with( @@ -482,6 +491,7 @@ class TestDomainManagers(TestDomainOverview): self.assertNotIn("First Last", email_content) @boto3_mocking.patching + @less_console_noise_decorator def test_domain_invitation_email_has_email_as_requestor(self): """Inviting a user sends them an email, with email as the name.""" # Create a fake user object @@ -494,12 +504,11 @@ class TestDomainManagers(TestDomainOverview): mock_client_instance = mock_client.return_value with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit() + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit() # check the mock instance to see if `send_email` was called right mock_client_instance.send_email.assert_called_once_with( @@ -521,6 +530,7 @@ class TestDomainManagers(TestDomainOverview): self.assertNotIn("First Last", email_content) @boto3_mocking.patching + @less_console_noise_decorator def test_domain_invitation_email_has_email_as_requestor_staff(self): """Inviting a user sends them an email, with email as the name.""" # Create a fake user object @@ -537,12 +547,11 @@ class TestDomainManagers(TestDomainOverview): mock_client_instance = mock_client.return_value with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit() + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit() # check the mock instance to see if `send_email` was called right mock_client_instance.send_email.assert_called_once_with( @@ -564,6 +573,7 @@ class TestDomainManagers(TestDomainOverview): self.assertNotIn("First Last", email_content) @boto3_mocking.patching + @less_console_noise_decorator def test_domain_invitation_email_displays_error_non_existent(self): """Inviting a non existent user sends them an email, with email as the name.""" # make sure there is no user with this email @@ -580,12 +590,11 @@ class TestDomainManagers(TestDomainOverview): mock_error_message = MagicMock() with boto3_mocking.clients.handler_for("sesv2", mock_client): with patch("django.contrib.messages.error") as mock_error_message: - with less_console_noise(): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit().follow() + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit().follow() expected_message_content = "Can't send invitation email. No email is associated with your account." @@ -596,6 +605,7 @@ class TestDomainManagers(TestDomainOverview): self.assertEqual(expected_message_content, returned_error_message) @boto3_mocking.patching + @less_console_noise_decorator def test_domain_invitation_email_displays_error(self): """When the requesting user has no email, an error is displayed""" # make sure there is no user with this email @@ -614,12 +624,11 @@ class TestDomainManagers(TestDomainOverview): mock_error_message = MagicMock() with boto3_mocking.clients.handler_for("sesv2", mock_client): with patch("django.contrib.messages.error") as mock_error_message: - with less_console_noise(): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit().follow() + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit().follow() expected_message_content = "Can't send invitation email. No email is associated with your account." @@ -629,34 +638,35 @@ class TestDomainManagers(TestDomainOverview): # Check that the message content is what we expect self.assertEqual(expected_message_content, returned_error_message) + @less_console_noise_decorator def test_domain_invitation_cancel(self): """Posting to the delete view deletes an invitation.""" email_address = "mayor@igorville.gov" invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address) mock_client = MockSESClient() with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id})) + self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id})) mock_client.EMAILS_SENT.clear() with self.assertRaises(DomainInvitation.DoesNotExist): DomainInvitation.objects.get(id=invitation.id) + @less_console_noise_decorator def test_domain_invitation_cancel_retrieved_invitation(self): """Posting to the delete view when invitation retrieved returns an error message""" email_address = "mayor@igorville.gov" invitation, _ = DomainInvitation.objects.get_or_create( domain=self.domain, email=email_address, status=DomainInvitation.DomainInvitationStatus.RETRIEVED ) - with less_console_noise(): - response = self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id}), follow=True) - # Assert that an error message is displayed to the user - self.assertContains(response, f"Invitation to {email_address} has already been retrieved.") - # Assert that the Cancel link is not displayed - self.assertNotContains(response, "Cancel") + response = self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id}), follow=True) + # Assert that an error message is displayed to the user + self.assertContains(response, f"Invitation to {email_address} has already been retrieved.") + # Assert that the Cancel link is not displayed + self.assertNotContains(response, "Cancel") # Assert that the DomainInvitation is not deleted self.assertTrue(DomainInvitation.objects.filter(id=invitation.id).exists()) DomainInvitation.objects.filter(email=email_address).delete() + @less_console_noise_decorator def test_domain_invitation_cancel_no_permissions(self): """Posting to the delete view as a different user should fail.""" email_address = "mayor@igorville.gov" @@ -667,12 +677,12 @@ class TestDomainManagers(TestDomainOverview): self.client.force_login(other_user) mock_client = MagicMock() with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): # permission denied makes console errors - result = self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id})) + result = self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id})) self.assertEqual(result.status_code, 403) @boto3_mocking.patching + @less_console_noise_decorator def test_domain_invitation_flow(self): """Send an invitation to a new user, log in and load the dashboard.""" email_address = "mayor@igorville.gov" @@ -688,8 +698,7 @@ class TestDomainManagers(TestDomainOverview): mock_client = MagicMock() with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - add_page.form.submit() + add_page.form.submit() # user was invited, create them new_user = User.objects.create(username=email_address, email=email_address) @@ -704,11 +713,13 @@ class TestDomainManagers(TestDomainOverview): class TestDomainNameservers(TestDomainOverview, MockEppLib): + @less_console_noise_decorator def test_domain_nameservers(self): """Can load domain's nameservers page.""" page = self.client.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) self.assertContains(page, "DNS name servers") + @less_console_noise_decorator def test_domain_nameservers_form_submit_one_nameserver(self): """Nameserver form submitted with one nameserver throws error. @@ -720,8 +731,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) # attempt to submit the form with only one nameserver, should error # regarding required fields - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() + result = nameservers_page.form.submit() # form submission was a post with an error, response should be a 200 # error text appears twice, once at the top of the page, once around # the required field. form requires a minimum of 2 name servers @@ -732,6 +742,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): status_code=200, ) + @less_console_noise_decorator def test_domain_nameservers_form_submit_subdomain_missing_ip(self): """Nameserver form catches missing ip error on subdomain. @@ -745,8 +756,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): # only one has ips nameservers_page.form["form-1-server"] = "ns2.igorville.gov" - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() + result = nameservers_page.form.submit() # form submission was a post with an error, response should be a 200 # error text appears twice, once at the top of the page, once around # the required field. subdomain missing an ip @@ -757,6 +767,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): status_code=200, ) + @less_console_noise_decorator def test_domain_nameservers_form_submit_missing_host(self): """Nameserver form catches error when host is missing. @@ -769,8 +780,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): # attempt to submit the form without two hosts, both subdomains, # only one has ips nameservers_page.form["form-1-ip"] = "127.0.0.1" - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() + result = nameservers_page.form.submit() # form submission was a post with an error, response should be a 200 # error text appears twice, once at the top of the page, once around # the required field. nameserver has ip but missing host @@ -781,6 +791,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): status_code=200, ) + @less_console_noise_decorator def test_domain_nameservers_form_submit_duplicate_host(self): """Nameserver form catches error when host is duplicated. @@ -793,8 +804,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): # attempt to submit the form with duplicate host names of fake.host.com nameservers_page.form["form-0-ip"] = "" nameservers_page.form["form-1-server"] = "fake.host.com" - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() + result = nameservers_page.form.submit() # form submission was a post with an error, response should be a 200 # error text appears twice, once at the top of the page, once around # the required field. remove duplicate entry @@ -805,6 +815,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): status_code=200, ) + @less_console_noise_decorator def test_domain_nameservers_form_submit_whitespace(self): """Nameserver form removes whitespace from ip. @@ -823,8 +834,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): 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() + result = nameservers_page.form.submit() # form submission was a post with an ip address which has been stripped of whitespace, # response should be a 302 to success page self.assertEqual(result.status_code, 302) @@ -838,6 +848,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): # with an error message displayed, so need to follow 302 and test for success message self.assertContains(page, "The name servers for this domain have been updated") + @less_console_noise_decorator def test_domain_nameservers_form_submit_glue_record_not_allowed(self): """Nameserver form catches error when IP is present but host not subdomain. @@ -856,8 +867,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): nameservers_page.form["form-0-server"] = nameserver1 nameservers_page.form["form-1-server"] = nameserver2 nameservers_page.form["form-1-ip"] = valid_ip - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() + result = nameservers_page.form.submit() # form submission was a post with an error, response should be a 200 # error text appears twice, once at the top of the page, once around # the required field. nameserver has ip but missing host @@ -868,6 +878,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): status_code=200, ) + @less_console_noise_decorator def test_domain_nameservers_form_submit_invalid_ip(self): """Nameserver form catches invalid IP on submission. @@ -883,8 +894,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): # only one has ips nameservers_page.form["form-1-server"] = nameserver nameservers_page.form["form-1-ip"] = invalid_ip - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() + result = nameservers_page.form.submit() # form submission was a post with an error, response should be a 200 # error text appears twice, once at the top of the page, once around # the required field. nameserver has ip but missing host @@ -895,6 +905,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): status_code=200, ) + @less_console_noise_decorator def test_domain_nameservers_form_submit_invalid_host(self): """Nameserver form catches invalid host on submission. @@ -910,8 +921,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): # only one has ips nameservers_page.form["form-1-server"] = nameserver nameservers_page.form["form-1-ip"] = valid_ip - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() + result = nameservers_page.form.submit() # form submission was a post with an error, response should be a 200 # error text appears twice, once at the top of the page, once around # the required field. nameserver has invalid host @@ -922,6 +932,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): status_code=200, ) + @less_console_noise_decorator def test_domain_nameservers_form_submits_successfully(self): """Nameserver form submits successfully with valid input. @@ -938,8 +949,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): nameservers_page.form["form-0-ip"] = valid_ip nameservers_page.form["form-1-server"] = nameserver2 nameservers_page.form["form-1-ip"] = valid_ip_2 - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() + result = nameservers_page.form.submit() # form submission was a successful post, response should be a 302 self.assertEqual(result.status_code, 302) self.assertEqual( @@ -950,6 +960,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): page = result.follow() self.assertContains(page, "The name servers for this domain have been updated") + @less_console_noise_decorator def test_domain_nameservers_can_blank_out_first_or_second_one_if_enough_entries(self): """Nameserver form submits successfully with 2 valid inputs, even if the first or second entries are blanked out. @@ -972,8 +983,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): nameservers_page.form["form-1-ip"] = valid_ip_2 nameservers_page.form["form-2-server"] = nameserver3 nameservers_page.form["form-2-ip"] = valid_ip_3 - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() + result = nameservers_page.form.submit() # form submission was a successful post, response should be a 302 self.assertEqual(result.status_code, 302) @@ -999,8 +1009,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): nameservers_page.form["form-1-ip"] = valid_ip_2 nameservers_page.form["form-2-server"] = nameserver3 nameservers_page.form["form-2-ip"] = valid_ip_3 - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() + result = nameservers_page.form.submit() # form submission was a successful post, response should be a 302 self.assertEqual(result.status_code, 302) @@ -1012,6 +1021,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): nameservers_page = result.follow() self.assertContains(nameservers_page, "The name servers for this domain have been updated") + @less_console_noise_decorator def test_domain_nameservers_can_blank_out_first_and_second_one_if_enough_entries(self): """Nameserver form submits successfully with 2 valid inputs, even if the first and second entries are blanked out. @@ -1048,8 +1058,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): nameservers_page.form["form-2-ip"] = valid_ip_3 nameservers_page.form["form-3-server"] = nameserver4 nameservers_page.form["form-3-ip"] = valid_ip_4 - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() + result = nameservers_page.form.submit() # form submission was a successful post, response should be a 302 self.assertEqual(result.status_code, 302) @@ -1061,6 +1070,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): nameservers_page = result.follow() self.assertContains(nameservers_page, "The name servers for this domain have been updated") + @less_console_noise_decorator def test_domain_nameservers_form_invalid(self): """Nameserver form does not submit with invalid data. @@ -1072,8 +1082,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): # first two nameservers are required, so if we empty one out we should # get a form error nameservers_page.form["form-0-server"] = "" - with less_console_noise(): # swallow logged warning message - result = nameservers_page.form.submit() + result = nameservers_page.form.submit() # form submission was a post with an error, response should be a 200 # error text appears four times, twice at the top of the page, # once around each required field. @@ -1086,11 +1095,13 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib): class TestDomainSeniorOfficial(TestDomainOverview): + @less_console_noise_decorator def test_domain_senior_official(self): """Can load domain's senior official page.""" page = self.client.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id})) self.assertContains(page, "Senior official", count=14) + @less_console_noise_decorator def test_domain_senior_official_content(self): """Senior official information appears on the page.""" self.domain_information.senior_official = Contact(first_name="Testy") @@ -1143,6 +1154,7 @@ class TestDomainSeniorOfficial(TestDomainOverview): self.assertNotContains(page, "Testy") self.assertContains(page, "Bob") + @less_console_noise_decorator def test_domain_edit_senior_official_in_place(self): """When editing a senior official for domain information and SO is not joined to any other objects""" @@ -1167,6 +1179,7 @@ class TestDomainSeniorOfficial(TestDomainOverview): self.assertEqual("Testy2", self.domain_information.senior_official.first_name) self.assertEqual(so_pk, self.domain_information.senior_official.id) + @less_console_noise_decorator def assert_all_form_fields_have_expected_values(self, form, test_cases, test_for_disabled=False): """ Asserts that each specified form field has the expected value and, optionally, checks if the field is disabled. @@ -1193,6 +1206,7 @@ class TestDomainSeniorOfficial(TestDomainOverview): # Test for disabled on each field self.assertTrue("disabled" in form[field_name].attrs) + @less_console_noise_decorator def test_domain_edit_senior_official_federal(self): """Tests that no edit can occur when the underlying domain is federal""" @@ -1249,6 +1263,7 @@ class TestDomainSeniorOfficial(TestDomainOverview): self.assertEqual("CIO", self.domain_information.senior_official.title) self.assertEqual("nobody@igorville.gov", self.domain_information.senior_official.email) + @less_console_noise_decorator def test_domain_edit_senior_official_tribal(self): """Tests that no edit can occur when the underlying domain is tribal""" @@ -1305,6 +1320,7 @@ class TestDomainSeniorOfficial(TestDomainOverview): self.assertEqual("CIO", self.domain_information.senior_official.title) self.assertEqual("nobody@igorville.gov", self.domain_information.senior_official.email) + @less_console_noise_decorator def test_domain_edit_senior_official_creates_new(self): """When editing a senior official for domain information and SO IS joined to another object""" @@ -1342,12 +1358,14 @@ class TestDomainSeniorOfficial(TestDomainOverview): class TestDomainOrganization(TestDomainOverview): + @less_console_noise_decorator def test_domain_org_name_address(self): """Can load domain's org name and mailing address page.""" page = self.client.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id})) # once on the sidebar, once in the page title, once as H1 self.assertContains(page, "Organization name and mailing address", count=4) + @less_console_noise_decorator def test_domain_org_name_address_content(self): """Org name and address information appears on the page.""" self.domain_information.organization_name = "Town of Igorville" @@ -1355,6 +1373,7 @@ class TestDomainOrganization(TestDomainOverview): page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id})) self.assertContains(page, "Town of Igorville") + @less_console_noise_decorator def test_domain_org_name_address_form(self): """Submitting changes works on the org name address page.""" self.domain_information.organization_name = "Town of Igorville" @@ -1372,6 +1391,7 @@ class TestDomainOrganization(TestDomainOverview): self.assertContains(success_result_page, "Not igorville") self.assertContains(success_result_page, "Faketown") + @less_console_noise_decorator def test_domain_org_name_address_form_tribal(self): """ Submitting a change to organization_name is blocked for tribal domains @@ -1429,6 +1449,7 @@ class TestDomainOrganization(TestDomainOverview): # Check for the value we want to update self.assertContains(success_result_page, "Faketown") + @less_console_noise_decorator def test_domain_org_name_address_form_federal(self): """ Submitting a change to federal_agency is blocked for federal domains @@ -1484,6 +1505,7 @@ class TestDomainOrganization(TestDomainOverview): # Check for the value we want to update self.assertContains(success_result_page, "Faketown") + @less_console_noise_decorator def test_federal_agency_submit_blocked(self): """ Submitting a change to federal_agency is blocked for federal domains @@ -1517,11 +1539,13 @@ class TestDomainOrganization(TestDomainOverview): class TestDomainContactInformation(TestDomainOverview): + @less_console_noise_decorator def test_domain_your_contact_information(self): """Can load domain's your contact information page.""" page = self.client.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})) self.assertContains(page, "Your contact information") + @less_console_noise_decorator def test_domain_your_contact_information_content(self): """Logged-in user's contact information appears on the page.""" self.user.first_name = "Testy" @@ -1649,20 +1673,21 @@ class TestDomainSecurityEmail(TestDomainOverview): self.assertEqual(message.tags, message_tag) self.assertEqual(message.message.strip(), expected_message.strip()) + @less_console_noise_decorator def test_domain_overview_blocked_for_ineligible_user(self): """We could easily duplicate this test for all domain management views, but a single url test should be solid enough since all domain management pages share the same permissions class""" self.user.status = User.RESTRICTED self.user.save() - with less_console_noise(): - response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id})) - self.assertEqual(response.status_code, 403) + response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id})) + self.assertEqual(response.status_code, 403) class TestDomainDNSSEC(TestDomainOverview): """MockEPPLib is already inherited.""" + @less_console_noise_decorator def test_dnssec_page_refreshes_enable_button(self): """DNSSEC overview page loads when domain has no DNSSEC data and shows a 'Enable DNSSEC' button.""" @@ -1670,6 +1695,7 @@ class TestDomainDNSSEC(TestDomainOverview): page = self.client.get(reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id})) self.assertContains(page, "Enable DNSSEC") + @less_console_noise_decorator def test_dnssec_page_loads_with_data_in_domain(self): """DNSSEC overview page loads when domain has DNSSEC data and the template contains a button to disable DNSSEC.""" @@ -1691,6 +1717,7 @@ class TestDomainDNSSEC(TestDomainOverview): self.assertContains(updated_page, "Enable DNSSEC") + @less_console_noise_decorator def test_ds_form_loads_with_no_domain_data(self): """DNSSEC Add DS data page loads when there is no domain DNSSEC data and shows a button to Add new record""" @@ -1699,6 +1726,7 @@ class TestDomainDNSSEC(TestDomainOverview): self.assertContains(page, "You have no DS data added") self.assertContains(page, "Add new record") + @less_console_noise_decorator def test_ds_form_loads_with_ds_data(self): """DNSSEC Add DS data page loads when there is domain DNSSEC DS data and shows the data""" @@ -1706,6 +1734,7 @@ class TestDomainDNSSEC(TestDomainOverview): page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) self.assertContains(page, "DS data record 1") + @less_console_noise_decorator def test_ds_data_form_modal(self): """When user clicks on save, a modal pops up.""" add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) @@ -1724,6 +1753,7 @@ class TestDomainDNSSEC(TestDomainOverview): # Now check to see whether the JS trigger for the modal is present on the page self.assertContains(response, "Trigger Disable DNSSEC Modal") + @less_console_noise_decorator def test_ds_data_form_submits(self): """DS data form submits successfully @@ -1732,8 +1762,7 @@ class TestDomainDNSSEC(TestDomainOverview): add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - with less_console_noise(): # swallow log warning message - result = add_data_page.forms[0].submit() + result = add_data_page.forms[0].submit() # form submission was a post, response should be a redirect self.assertEqual(result.status_code, 302) self.assertEqual( @@ -1744,6 +1773,7 @@ class TestDomainDNSSEC(TestDomainOverview): page = result.follow() self.assertContains(page, "The DS data records for this domain have been updated.") + @less_console_noise_decorator def test_ds_data_form_invalid(self): """DS data form errors with invalid data (missing required fields) @@ -1757,8 +1787,7 @@ class TestDomainDNSSEC(TestDomainOverview): add_data_page.forms[0]["form-0-algorithm"] = "" add_data_page.forms[0]["form-0-digest_type"] = "" add_data_page.forms[0]["form-0-digest"] = "" - with less_console_noise(): # swallow logged warning message - result = add_data_page.forms[0].submit() + result = add_data_page.forms[0].submit() # form submission was a post with an error, response should be a 200 # error text appears twice, once at the top of the page, once around # the field. @@ -1767,6 +1796,7 @@ class TestDomainDNSSEC(TestDomainOverview): self.assertContains(result, "Digest type is required", count=2, status_code=200) self.assertContains(result, "Digest is required", count=2, status_code=200) + @less_console_noise_decorator def test_ds_data_form_invalid_keytag(self): """DS data form errors with invalid data (key tag too large) @@ -1781,8 +1811,7 @@ class TestDomainDNSSEC(TestDomainOverview): add_data_page.forms[0]["form-0-algorithm"] = "" add_data_page.forms[0]["form-0-digest_type"] = "" add_data_page.forms[0]["form-0-digest"] = "" - with less_console_noise(): # swallow logged warning message - result = add_data_page.forms[0].submit() + result = add_data_page.forms[0].submit() # form submission was a post with an error, response should be a 200 # error text appears twice, once at the top of the page, once around # the field. @@ -1790,6 +1819,7 @@ class TestDomainDNSSEC(TestDomainOverview): result, str(DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE)), count=2, status_code=200 ) + @less_console_noise_decorator def test_ds_data_form_invalid_digest_chars(self): """DS data form errors with invalid data (digest contains non hexadecimal chars) @@ -1804,8 +1834,7 @@ class TestDomainDNSSEC(TestDomainOverview): add_data_page.forms[0]["form-0-algorithm"] = "3" add_data_page.forms[0]["form-0-digest_type"] = "1" add_data_page.forms[0]["form-0-digest"] = "GG1234" - with less_console_noise(): # swallow logged warning message - result = add_data_page.forms[0].submit() + result = add_data_page.forms[0].submit() # form submission was a post with an error, response should be a 200 # error text appears twice, once at the top of the page, once around # the field. @@ -1813,6 +1842,7 @@ class TestDomainDNSSEC(TestDomainOverview): result, str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_CHARS)), count=2, status_code=200 ) + @less_console_noise_decorator def test_ds_data_form_invalid_digest_sha1(self): """DS data form errors with invalid data (digest is invalid sha-1) @@ -1827,8 +1857,7 @@ class TestDomainDNSSEC(TestDomainOverview): add_data_page.forms[0]["form-0-algorithm"] = "3" add_data_page.forms[0]["form-0-digest_type"] = "1" # SHA-1 add_data_page.forms[0]["form-0-digest"] = "A123" - with less_console_noise(): # swallow logged warning message - result = add_data_page.forms[0].submit() + result = add_data_page.forms[0].submit() # form submission was a post with an error, response should be a 200 # error text appears twice, once at the top of the page, once around # the field. @@ -1836,6 +1865,7 @@ class TestDomainDNSSEC(TestDomainOverview): result, str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_SHA1)), count=2, status_code=200 ) + @less_console_noise_decorator def test_ds_data_form_invalid_digest_sha256(self): """DS data form errors with invalid data (digest is invalid sha-256) @@ -1850,8 +1880,7 @@ class TestDomainDNSSEC(TestDomainOverview): add_data_page.forms[0]["form-0-algorithm"] = "3" add_data_page.forms[0]["form-0-digest_type"] = "2" # SHA-256 add_data_page.forms[0]["form-0-digest"] = "GG1234" - with less_console_noise(): # swallow logged warning message - result = add_data_page.forms[0].submit() + result = add_data_page.forms[0].submit() # form submission was a post with an error, response should be a 200 # error text appears twice, once at the top of the page, once around # the field. diff --git a/src/registrar/tests/test_views_domains_json.py b/src/registrar/tests/test_views_domains_json.py index 28a7308f5..18d6415a0 100644 --- a/src/registrar/tests/test_views_domains_json.py +++ b/src/registrar/tests/test_views_domains_json.py @@ -24,7 +24,6 @@ class GetDomainsJsonTest(TestWithUser, WebTest): def tearDown(self): super().tearDown() UserDomainRole.objects.all().delete() - UserDomainRole.objects.all().delete() @less_console_noise_decorator def test_get_domains_json_unauthenticated(self): diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index e0fae5332..3596bf567 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -1,5 +1,6 @@ from django.urls import reverse from api.tests.common import less_console_noise_decorator +from registrar.config import settings from registrar.models.portfolio import Portfolio from django_webtest import WebTest # type: ignore from registrar.models import ( @@ -9,7 +10,7 @@ from registrar.models import ( UserDomainRole, User, ) -from .test_views import TestWithUser +from .common import create_test_user from waffle.testutils import override_flag import logging @@ -17,15 +18,25 @@ import logging logger = logging.getLogger(__name__) -class TestPortfolioViews(TestWithUser, WebTest): +class TestPortfolio(WebTest): def setUp(self): super().setUp() + self.user = create_test_user() self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California") self.role, _ = UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER ) + def tearDown(self): + Portfolio.objects.all().delete() + UserDomainRole.objects.all().delete() + DomainRequest.objects.all().delete() + DomainInformation.objects.all().delete() + Domain.objects.all().delete() + User.objects.all().delete() + super().tearDown() + @less_console_noise_decorator def test_middleware_does_not_redirect_if_no_permission(self): """Test that user with no portfolio permission is not redirected when attempting to access home""" @@ -183,10 +194,69 @@ class TestPortfolioViews(TestWithUser, WebTest): portfolio_page, reverse("portfolio-domain-requests", kwargs={"portfolio_id": self.portfolio.pk}) ) - def tearDown(self): - Portfolio.objects.all().delete() - UserDomainRole.objects.all().delete() - DomainRequest.objects.all().delete() - DomainInformation.objects.all().delete() - Domain.objects.all().delete() - super().tearDown() + +class TestPortfolioOrganization(TestPortfolio): + + def test_portfolio_org_name(self): + """Can load portfolio's org name page.""" + with override_flag("organization_feature", active=True): + self.app.set_user(self.user.username) + self.user.portfolio = self.portfolio + self.user.portfolio_additional_permissions = [ + User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO, + User.UserPortfolioPermissionChoices.EDIT_PORTFOLIO, + ] + self.user.save() + self.user.refresh_from_db() + + page = self.app.get(reverse("portfolio-organization", kwargs={"portfolio_id": self.portfolio.pk})) + self.assertContains( + page, "The name of your federal agency will be publicly listed as the domain registrant." + ) + + def test_domain_org_name_address_content(self): + """Org name and address information appears on the page.""" + with override_flag("organization_feature", active=True): + self.app.set_user(self.user.username) + self.user.portfolio = self.portfolio + self.user.portfolio_additional_permissions = [ + User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO, + User.UserPortfolioPermissionChoices.EDIT_PORTFOLIO, + ] + self.user.save() + self.user.refresh_from_db() + + self.portfolio.organization_name = "Hotel California" + self.portfolio.save() + page = self.app.get(reverse("portfolio-organization", kwargs={"portfolio_id": self.portfolio.pk})) + # Once in the sidenav, once in the main nav, once in the form + self.assertContains(page, "Hotel California", count=3) + + def test_domain_org_name_address_form(self): + """Submitting changes works on the org name address page.""" + with override_flag("organization_feature", active=True): + self.app.set_user(self.user.username) + self.user.portfolio = self.portfolio + self.user.portfolio_additional_permissions = [ + User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO, + User.UserPortfolioPermissionChoices.EDIT_PORTFOLIO, + ] + self.user.save() + self.user.refresh_from_db() + + self.portfolio.address_line1 = "1600 Penn Ave" + self.portfolio.save() + portfolio_org_name_page = self.app.get( + reverse("portfolio-organization", kwargs={"portfolio_id": self.portfolio.pk}) + ) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + portfolio_org_name_page.form["address_line1"] = "6 Downing st" + portfolio_org_name_page.form["city"] = "London" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + success_result_page = portfolio_org_name_page.form.submit() + self.assertEqual(success_result_page.status_code, 200) + + self.assertContains(success_result_page, "6 Downing st") + self.assertContains(success_result_page, "London") diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index de924576b..0cee9d563 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -3,7 +3,7 @@ from unittest.mock import Mock from django.conf import settings from django.urls import reverse - +from api.tests.common import less_console_noise_decorator from .common import MockSESClient, completed_domain_request # type: ignore from django_webtest import WebTest # type: ignore import boto3_mocking # type: ignore @@ -37,14 +37,23 @@ class DomainRequestTests(TestWithUser, WebTest): def setUp(self): super().setUp() + self.federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration") self.app.set_user(self.user.username) self.TITLES = DomainRequestWizard.TITLES + def tearDown(self): + super().tearDown() + DomainRequest.objects.all().delete() + DomainInformation.objects.all().delete() + self.federal_agency.delete() + + @less_console_noise_decorator def test_domain_request_form_intro_acknowledgement(self): """Tests that user is presented with intro acknowledgement page""" intro_page = self.app.get(reverse("domain-request:")) self.assertContains(intro_page, "You’re about to start your .gov domain request") + @less_console_noise_decorator def test_domain_request_form_intro_is_skipped_when_edit_access(self): """Tests that user is NOT presented with intro acknowledgement page when accessed through 'edit'""" domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.STARTED, user=self.user) @@ -55,6 +64,7 @@ class DomainRequestTests(TestWithUser, WebTest): redirect_url = detail_page.url self.assertEqual(redirect_url, "/request/generic_org_type/") + @less_console_noise_decorator def test_domain_request_form_empty_submit(self): """Tests empty submit on the first page after the acknowledgement page""" intro_page = self.app.get(reverse("domain-request:")) @@ -77,31 +87,31 @@ class DomainRequestTests(TestWithUser, WebTest): result = type_page.forms[0].submit() self.assertIn("What kind of U.S.-based government organization do you represent?", result) + @less_console_noise_decorator def test_domain_request_multiple_domain_requests_exist(self): """Test that an info message appears when user has multiple domain requests already""" # create and submit a domain request domain_request = completed_domain_request(user=self.user) mock_client = MockSESClient() with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - domain_request.submit() - domain_request.save() + domain_request.submit() + domain_request.save() # now, attempt to create another one - with less_console_noise(): - intro_page = self.app.get(reverse("domain-request:")) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() + intro_page = self.app.get(reverse("domain-request:")) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.assertContains(type_page, "You cannot submit this request yet") + self.assertContains(type_page, "You cannot submit this request yet") + @less_console_noise_decorator def test_domain_request_into_acknowledgement_creates_new_request(self): """ We had to solve a bug where the wizard was creating 2 requests on first intro acknowledgement ('continue') @@ -155,6 +165,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertEqual(domain_request_count, 2) @boto3_mocking.patching + @less_console_noise_decorator def test_domain_request_form_submission(self): """ Can fill out the entire form and submit. @@ -227,9 +238,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) org_contact_page = federal_result.follow() org_contact_form = org_contact_page.forms[0] - # federal agency so we have to fill in federal_agency - federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration") - org_contact_form["organization_contact-federal_agency"] = federal_agency.id + org_contact_form["organization_contact-federal_agency"] = self.federal_agency.id org_contact_form["organization_contact-organization_name"] = "Testorg" org_contact_form["organization_contact-address_line1"] = "address 1" org_contact_form["organization_contact-address_line2"] = "address 2" @@ -524,6 +533,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertEqual(num_pages, num_pages_tested) @boto3_mocking.patching + @less_console_noise_decorator def test_domain_request_form_submission_incomplete(self): num_pages_tested = 0 # skipping elections, type_of_work, tribal_government @@ -584,9 +594,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) org_contact_page = federal_result.follow() org_contact_form = org_contact_page.forms[0] - # federal agency so we have to fill in federal_agency - federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration") - org_contact_form["organization_contact-federal_agency"] = federal_agency.id + org_contact_form["organization_contact-federal_agency"] = self.federal_agency.id org_contact_form["organization_contact-organization_name"] = "Testorg" org_contact_form["organization_contact-address_line1"] = "address 1" org_contact_form["organization_contact-address_line2"] = "address 2" @@ -879,6 +887,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertEqual(num_pages, num_pages_tested) + @less_console_noise_decorator def test_domain_request_form_conditional_federal(self): """Federal branch question is shown for federal organizations.""" intro_page = self.app.get(reverse("domain-request:")) @@ -934,6 +943,7 @@ class DomainRequestTests(TestWithUser, WebTest): contact_page = federal_result.follow() self.assertContains(contact_page, "Federal agency") + @less_console_noise_decorator def test_domain_request_form_conditional_elections(self): """Election question is shown for other organizations.""" intro_page = self.app.get(reverse("domain-request:")) @@ -988,6 +998,7 @@ class DomainRequestTests(TestWithUser, WebTest): contact_page = election_result.follow() self.assertNotContains(contact_page, "Federal agency") + @less_console_noise_decorator def test_domain_request_form_section_skipping(self): """Can skip forward and back in sections""" intro_page = self.app.get(reverse("domain-request:")) @@ -1025,6 +1036,7 @@ class DomainRequestTests(TestWithUser, WebTest): 0, ) + @less_console_noise_decorator def test_domain_request_form_nonfederal(self): """Non-federal organizations don't have to provide their federal agency.""" intro_page = self.app.get(reverse("domain-request:")) @@ -1069,6 +1081,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertEqual(contact_result.status_code, 302) self.assertEqual(contact_result["Location"], "/request/about_your_organization/") + @less_console_noise_decorator def test_domain_request_about_your_organization_special(self): """Special districts have to answer an additional question.""" intro_page = self.app.get(reverse("domain-request:")) @@ -1097,6 +1110,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION]) + @less_console_noise_decorator def test_federal_agency_dropdown_excludes_expected_values(self): """The Federal Agency dropdown on a domain request form should not include options for gov Administration and Non-Federal Agency""" @@ -1144,6 +1158,7 @@ class DomainRequestTests(TestWithUser, WebTest): # make sure correct federal agency options still show up self.assertContains(org_contact_page, "General Services Administration") + @less_console_noise_decorator def test_yes_no_contact_form_inits_blank_for_new_domain_request(self): """On the Other Contacts page, the yes/no form gets initialized with nothing selected for new domain requests""" @@ -1151,6 +1166,7 @@ class DomainRequestTests(TestWithUser, WebTest): other_contacts_form = other_contacts_page.forms[0] self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, None) + @less_console_noise_decorator def test_yes_no_additional_form_inits_blank_for_new_domain_request(self): """On the Additional Details page, the yes/no form gets initialized with nothing selected for new domain requests""" @@ -1163,6 +1179,7 @@ class DomainRequestTests(TestWithUser, WebTest): # Check the anything else yes/no field self.assertEquals(additional_form["additional_details-has_anything_else_text"].value, None) + @less_console_noise_decorator def test_yes_no_form_inits_yes_for_domain_request_with_other_contacts(self): """On the Other Contacts page, the yes/no form gets initialized with YES selected if the domain request has other contacts""" @@ -1183,6 +1200,7 @@ class DomainRequestTests(TestWithUser, WebTest): other_contacts_form = other_contacts_page.forms[0] self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") + @less_console_noise_decorator def test_yes_no_form_inits_yes_for_cisa_representative_and_anything_else(self): """On the Additional Details page, the yes/no form gets initialized with YES selected for both yes/no radios if the domain request has a values for cisa_representative_first_name and @@ -1214,6 +1232,7 @@ class DomainRequestTests(TestWithUser, WebTest): yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value self.assertEquals(yes_no_anything_else, "True") + @less_console_noise_decorator def test_yes_no_form_inits_no_for_domain_request_with_no_other_contacts_rationale(self): """On the Other Contacts page, the yes/no form gets initialized with NO selected if the domain request has no other contacts""" @@ -1236,6 +1255,7 @@ class DomainRequestTests(TestWithUser, WebTest): other_contacts_form = other_contacts_page.forms[0] self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False") + @less_console_noise_decorator def test_yes_no_form_for_domain_request_with_no_cisa_representative_and_anything_else(self): """On the Additional details page, the form preselects "no" when has_cisa_representative and anything_else is no""" @@ -1271,6 +1291,7 @@ class DomainRequestTests(TestWithUser, WebTest): yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value self.assertEquals(yes_no_anything_else, "False") + @less_console_noise_decorator def test_submitting_additional_details_deletes_cisa_representative_and_anything_else(self): """When a user submits the Additional Details form with no selected for all fields, the domain request's data gets wiped when submitted""" @@ -1332,6 +1353,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertEqual(domain_request.cisa_representative_last_name, None) self.assertEqual(domain_request.cisa_representative_email, None) + @less_console_noise_decorator def test_submitting_additional_details_populates_cisa_representative_and_anything_else(self): """When a user submits the Additional Details form, the domain request's data gets submitted""" @@ -1385,6 +1407,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertEqual(domain_request.has_cisa_representative, True) self.assertEqual(domain_request.has_anything_else_text, True) + @less_console_noise_decorator def test_if_cisa_representative_yes_no_form_is_yes_then_field_is_required(self): """Applicants with a cisa representative must provide a value""" domain_request = completed_domain_request( @@ -1417,6 +1440,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertContains(response, "Enter the first name / given name of the CISA regional representative.") self.assertContains(response, "Enter the last name / family name of the CISA regional representative.") + @less_console_noise_decorator def test_if_anything_else_yes_no_form_is_yes_then_field_is_required(self): """Applicants with a anything else must provide a value""" domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False) @@ -1447,6 +1471,7 @@ class DomainRequestTests(TestWithUser, WebTest): expected_message = "Provide additional details you’d like us to know. If you have nothing to add, select “No.”" self.assertContains(response, expected_message) + @less_console_noise_decorator def test_additional_details_form_fields_required(self): """When a user submits the Additional Details form without checking the has_cisa_representative and has_anything_else_text fields, the form should deny this action""" @@ -1480,6 +1505,7 @@ class DomainRequestTests(TestWithUser, WebTest): # due to screen reader information / html. self.assertContains(response, "This question is required.", count=4) + @less_console_noise_decorator def test_submitting_other_contacts_deletes_no_other_contacts_rationale(self): """When a user submits the Other Contacts form with other contacts selected, the domain request's no other contacts rationale gets deleted""" @@ -1528,6 +1554,7 @@ class DomainRequestTests(TestWithUser, WebTest): None, ) + @less_console_noise_decorator def test_submitting_no_other_contacts_rationale_deletes_other_contacts(self): """When a user submits the Other Contacts form with no other contacts selected, the domain request's other contacts get deleted for other contacts that exist and are not joined to other objects @@ -1570,6 +1597,7 @@ class DomainRequestTests(TestWithUser, WebTest): "Hello again!", ) + @less_console_noise_decorator def test_submitting_no_other_contacts_rationale_removes_reference_other_contacts_when_joined(self): """When a user submits the Other Contacts form with no other contacts selected, the domain request's other contacts references get removed for other contacts that exist and are joined to other objects""" @@ -1665,6 +1693,7 @@ class DomainRequestTests(TestWithUser, WebTest): "Hello again!", ) + @less_console_noise_decorator def test_if_yes_no_form_is_no_then_no_other_contacts_required(self): """Applicants with no other contacts have to give a reason.""" other_contacts_page = self.app.get(reverse("domain-request:other_contacts")) @@ -1680,6 +1709,7 @@ class DomainRequestTests(TestWithUser, WebTest): # Assert that it is not returned, ie the contacts form is not required self.assertNotContains(response, "Enter the first name / given name of this contact.") + @less_console_noise_decorator def test_if_yes_no_form_is_yes_then_other_contacts_required(self): """Applicants with other contacts do not have to give a reason.""" other_contacts_page = self.app.get(reverse("domain-request:other_contacts")) @@ -1695,6 +1725,7 @@ class DomainRequestTests(TestWithUser, WebTest): # Assert that it is returned, ie the contacts form is required self.assertContains(response, "Enter the first name / given name of this contact.") + @less_console_noise_decorator def test_delete_other_contact(self): """Other contacts can be deleted after being saved to database. @@ -1779,6 +1810,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertEqual(domain_request.other_contacts.count(), 1) self.assertEqual(domain_request.other_contacts.first().first_name, "Testy3") + @less_console_noise_decorator def test_delete_other_contact_does_not_allow_zero_contacts(self): """Delete Other Contact does not allow submission with zero contacts.""" # Populate the database with a domain request that @@ -1851,6 +1883,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertEqual(domain_request.other_contacts.count(), 1) self.assertEqual(domain_request.other_contacts.first().first_name, "Testy2") + @less_console_noise_decorator def test_delete_other_contact_sets_visible_empty_form_as_required_after_failed_submit(self): """When you: 1. add an empty contact, @@ -1928,6 +1961,7 @@ class DomainRequestTests(TestWithUser, WebTest): # Enter the first name ... self.assertContains(response, "Enter the first name / given name of this contact.") + @less_console_noise_decorator def test_edit_other_contact_in_place(self): """When you: 1. edit an existing contact which is not joined to another model, @@ -2009,6 +2043,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertEquals(other_contact_pk, other_contact.id) self.assertEquals("Testy3", other_contact.first_name) + @less_console_noise_decorator def test_edit_other_contact_creates_new(self): """When you: 1. edit an existing contact which IS joined to another model, @@ -2089,6 +2124,7 @@ class DomainRequestTests(TestWithUser, WebTest): senior_official = domain_request.senior_official self.assertEquals("Testy", senior_official.first_name) + @less_console_noise_decorator def test_edit_senior_official_in_place(self): """When you: 1. edit a senior official which is not joined to another model, @@ -2154,6 +2190,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertEquals(so_pk, updated_so.id) self.assertEquals("Testy2", updated_so.first_name) + @less_console_noise_decorator def test_edit_senior_official_creates_new(self): """When you: 1. edit an existing senior official which IS joined to another model, @@ -2226,6 +2263,7 @@ class DomainRequestTests(TestWithUser, WebTest): senior_official = domain_request.senior_official self.assertEquals("Testy2", senior_official.first_name) + @less_console_noise_decorator def test_edit_submitter_in_place(self): """When you: 1. edit a submitter (your contact) which is not joined to another model, @@ -2290,6 +2328,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertEquals(submitter_pk, updated_submitter.id) self.assertEquals("Testy2", updated_submitter.first_name) + @less_console_noise_decorator def test_edit_submitter_creates_new(self): """When you: 1. edit an existing your contact which IS joined to another model, @@ -2362,6 +2401,7 @@ class DomainRequestTests(TestWithUser, WebTest): submitter = domain_request.submitter self.assertEquals("Testy2", submitter.first_name) + @less_console_noise_decorator def test_domain_request_about_your_organiztion_interstate(self): """Special districts have to answer an additional question.""" intro_page = self.app.get(reverse("domain-request:")) @@ -2390,6 +2430,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION]) + @less_console_noise_decorator def test_domain_request_tribal_government(self): """Tribal organizations have to answer an additional question.""" intro_page = self.app.get(reverse("domain-request:")) @@ -2421,6 +2462,7 @@ class DomainRequestTests(TestWithUser, WebTest): # and the step is on the sidebar list. self.assertContains(tribal_government_page, self.TITLES[Step.TRIBAL_GOVERNMENT]) + @less_console_noise_decorator def test_domain_request_so_dynamic_text(self): intro_page = self.app.get(reverse("domain-request:")) # django-webtest does not handle cookie-based sessions well because it keeps @@ -2460,9 +2502,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) org_contact_page = federal_result.follow() org_contact_form = org_contact_page.forms[0] - # federal agency so we have to fill in federal_agency - federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration") - org_contact_form["organization_contact-federal_agency"] = federal_agency.id + org_contact_form["organization_contact-federal_agency"] = self.federal_agency.id org_contact_form["organization_contact-organization_name"] = "Testorg" org_contact_form["organization_contact-address_line1"] = "address 1" org_contact_form["organization_contact-address_line2"] = "address 2" @@ -2493,6 +2533,7 @@ class DomainRequestTests(TestWithUser, WebTest): so_page = election_page.click(str(self.TITLES["senior_official"]), index=0) self.assertContains(so_page, "Domain requests from cities") + @less_console_noise_decorator def test_domain_request_dotgov_domain_dynamic_text(self): intro_page = self.app.get(reverse("domain-request:")) # django-webtest does not handle cookie-based sessions well because it keeps @@ -2532,9 +2573,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) org_contact_page = federal_result.follow() org_contact_form = org_contact_page.forms[0] - # federal agency so we have to fill in federal_agency - federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration") - org_contact_form["organization_contact-federal_agency"] = federal_agency.id + org_contact_form["organization_contact-federal_agency"] = self.federal_agency.id org_contact_form["organization_contact-organization_name"] = "Testorg" org_contact_form["organization_contact-address_line1"] = "address 1" org_contact_form["organization_contact-address_line2"] = "address 2" @@ -2595,6 +2634,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertContains(dotgov_page, "CityofEudoraKS.gov") self.assertNotContains(dotgov_page, "medicare.gov") + @less_console_noise_decorator def test_domain_request_formsets(self): """Users are able to add more than one of some fields.""" current_sites_page = self.app.get(reverse("domain-request:current_sites")) @@ -2749,6 +2789,7 @@ class DomainRequestTests(TestWithUser, WebTest): # page = self.app.get(url) # self.assertNotContains(page, "VALUE") + @less_console_noise_decorator def test_long_org_name_in_domain_request(self): """ Make sure the long name is displaying in the domain request form, @@ -2771,6 +2812,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertContains(type_page, "Federal: an agency of the U.S. government") + @less_console_noise_decorator def test_submit_modal_no_domain_text_fallback(self): """When user clicks on submit your domain request and the requested domain is null (possible through url direct access to the review page), present @@ -2790,6 +2832,12 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest): self.app.set_user(self.user.username) self.client.force_login(self.user) + def tearDown(self): + super().tearDown() + DomainRequest.objects.all().delete() + DomainInformation.objects.all().delete() + + @less_console_noise_decorator def test_domain_request_status(self): """Checking domain request status page""" domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user) @@ -2803,6 +2851,7 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest): self.assertContains(detail_page, "Admin Tester") self.assertContains(detail_page, "Status:") + @less_console_noise_decorator def test_domain_request_status_with_ineligible_user(self): """Checking domain request status page whith a blocked user. The user should still have access to view.""" @@ -2819,6 +2868,7 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest): self.assertContains(detail_page, "Admin Tester") self.assertContains(detail_page, "Status:") + @less_console_noise_decorator def test_domain_request_withdraw(self): """Checking domain request status page""" domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user) @@ -2849,6 +2899,7 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest): response = self.client.get("/get-domain-requests-json/") self.assertContains(response, "Withdrawn") + @less_console_noise_decorator def test_domain_request_withdraw_no_permissions(self): """Can't withdraw domain requests as a restricted user.""" self.user.status = User.RESTRICTED @@ -2873,6 +2924,7 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest): page = self.client.get(reverse(url_name, kwargs={"pk": domain_request.pk})) self.assertEqual(page.status_code, 403) + @less_console_noise_decorator def test_domain_request_status_no_permissions(self): """Can't access domain requests without being the creator.""" domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user) @@ -2892,6 +2944,7 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest): page = self.client.get(reverse(url_name, kwargs={"pk": domain_request.pk})) self.assertEqual(page.status_code, 403) + @less_console_noise_decorator def test_approved_domain_request_not_in_active_requests(self): """An approved domain request is not shown in the Active Requests table on home.html.""" @@ -2916,13 +2969,17 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest): def tearDown(self): super().tearDown() + DomainRequest.objects.all().delete() + DomainInformation.objects.all().delete() + @less_console_noise_decorator def test_unlocked_steps_empty_domain_request(self): """Test when all fields in the domain request are empty.""" unlocked_steps = self.wizard.db_check_for_unlocking_steps() expected_dict = [] self.assertEqual(unlocked_steps, expected_dict) + @less_console_noise_decorator def test_unlocked_steps_full_domain_request(self): """Test when all fields in the domain request are filled.""" @@ -2959,6 +3016,7 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest): else: self.fail(f"Expected a redirect, but got a different response: {response}") + @less_console_noise_decorator def test_unlocked_steps_partial_domain_request(self): """Test when some fields in the domain request are filled.""" diff --git a/src/registrar/tests/test_views_requests_json.py b/src/registrar/tests/test_views_requests_json.py index 5b36ef759..7bdc922cf 100644 --- a/src/registrar/tests/test_views_requests_json.py +++ b/src/registrar/tests/test_views_requests_json.py @@ -12,99 +12,102 @@ class GetRequestsJsonTest(TestWithUser, WebTest): super().setUp() self.app.set_user(self.user.username) + @classmethod + def setUpClass(cls): + super().setUpClass() lamb_chops, _ = DraftDomain.objects.get_or_create(name="lamb-chops.gov") short_ribs, _ = DraftDomain.objects.get_or_create(name="short-ribs.gov") beef_chuck, _ = DraftDomain.objects.get_or_create(name="beef-chuck.gov") stew_beef, _ = DraftDomain.objects.get_or_create(name="stew-beef.gov") # Create domain requests for the user - self.domain_requests = [ + cls.domain_requests = [ DomainRequest.objects.create( - creator=self.user, + creator=cls.user, requested_domain=lamb_chops, submission_date="2024-01-01", status=DomainRequest.DomainRequestStatus.STARTED, created_at="2024-01-01", ), DomainRequest.objects.create( - creator=self.user, + creator=cls.user, requested_domain=short_ribs, submission_date="2024-02-01", status=DomainRequest.DomainRequestStatus.WITHDRAWN, created_at="2024-02-01", ), DomainRequest.objects.create( - creator=self.user, + creator=cls.user, requested_domain=beef_chuck, submission_date="2024-03-01", status=DomainRequest.DomainRequestStatus.REJECTED, created_at="2024-03-01", ), DomainRequest.objects.create( - creator=self.user, + creator=cls.user, requested_domain=stew_beef, submission_date="2024-04-01", status=DomainRequest.DomainRequestStatus.STARTED, created_at="2024-04-01", ), DomainRequest.objects.create( - creator=self.user, + creator=cls.user, requested_domain=None, submission_date="2024-05-01", status=DomainRequest.DomainRequestStatus.STARTED, created_at="2024-05-01", ), DomainRequest.objects.create( - creator=self.user, + creator=cls.user, requested_domain=None, submission_date="2024-06-01", status=DomainRequest.DomainRequestStatus.WITHDRAWN, created_at="2024-06-01", ), DomainRequest.objects.create( - creator=self.user, + creator=cls.user, requested_domain=None, submission_date="2024-07-01", status=DomainRequest.DomainRequestStatus.REJECTED, created_at="2024-07-01", ), DomainRequest.objects.create( - creator=self.user, + creator=cls.user, requested_domain=None, submission_date="2024-08-01", status=DomainRequest.DomainRequestStatus.STARTED, created_at="2024-08-01", ), DomainRequest.objects.create( - creator=self.user, + creator=cls.user, requested_domain=None, submission_date="2024-09-01", status=DomainRequest.DomainRequestStatus.STARTED, created_at="2024-09-01", ), DomainRequest.objects.create( - creator=self.user, + creator=cls.user, requested_domain=None, submission_date="2024-10-01", status=DomainRequest.DomainRequestStatus.WITHDRAWN, created_at="2024-10-01", ), DomainRequest.objects.create( - creator=self.user, + creator=cls.user, requested_domain=None, submission_date="2024-11-01", status=DomainRequest.DomainRequestStatus.REJECTED, created_at="2024-11-01", ), DomainRequest.objects.create( - creator=self.user, + creator=cls.user, requested_domain=None, submission_date="2024-11-02", status=DomainRequest.DomainRequestStatus.WITHDRAWN, created_at="2024-11-02", ), DomainRequest.objects.create( - creator=self.user, + creator=cls.user, requested_domain=None, submission_date="2024-12-01", status=DomainRequest.DomainRequestStatus.APPROVED, @@ -112,9 +115,11 @@ class GetRequestsJsonTest(TestWithUser, WebTest): ), ] - def tearDown(self): - super().tearDown() + @classmethod + def tearDownClass(cls): + super().tearDownClass() DomainRequest.objects.all().delete() + DraftDomain.objects.all().delete() def test_get_domain_requests_json_authenticated(self): """Test that domain requests are returned properly for an authenticated user.""" diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 56aa41569..5fbd255aa 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -109,7 +109,7 @@ class BaseExport(ABC): return Q() @classmethod - def get_filter_conditions(cls, start_date=None, end_date=None): + def get_filter_conditions(cls, **export_kwargs): """ Get a Q object of filter conditions to filter when building queryset. """ @@ -145,7 +145,7 @@ class BaseExport(ABC): return queryset @classmethod - def write_csv_before(cls, csv_writer, start_date=None, end_date=None): + def write_csv_before(cls, csv_writer, **export_kwargs): """ Write to csv file before the write_csv method. Override in subclasses where needed. @@ -192,7 +192,7 @@ class BaseExport(ABC): return cls.update_queryset(queryset, **kwargs) @classmethod - def export_data_to_csv(cls, csv_file, start_date=None, end_date=None): + def export_data_to_csv(cls, csv_file, **export_kwargs): """ All domain metadata: Exports domains of all statuses plus domain managers. @@ -205,7 +205,7 @@ class BaseExport(ABC): prefetch_related = cls.get_prefetch_related() exclusions = cls.get_exclusions() annotations_for_sort = cls.get_annotations_for_sort() - filter_conditions = cls.get_filter_conditions(start_date, end_date) + filter_conditions = cls.get_filter_conditions(**export_kwargs) computed_fields = cls.get_computed_fields() related_table_fields = cls.get_related_table_fields() @@ -227,10 +227,13 @@ class BaseExport(ABC): models_dict = convert_queryset_to_dict(annotated_queryset, is_model=False) # Write to csv file before the write_csv - cls.write_csv_before(writer, start_date, end_date) + cls.write_csv_before(writer, **export_kwargs) # Write the csv file - cls.write_csv(writer, columns, models_dict) + rows = cls.write_csv(writer, columns, models_dict) + + # Return rows that for easier parsing and testing + return rows @classmethod def write_csv( @@ -257,6 +260,9 @@ class BaseExport(ABC): writer.writerows(rows) + # Return rows for easier parsing and testing + return rows + @classmethod @abstractmethod def parse_row(cls, columns, model): @@ -344,7 +350,11 @@ class DomainExport(BaseExport): """ Fetch all UserDomainRole entries and return a mapping of domain to user__email. """ - user_domain_roles = UserDomainRole.objects.select_related("user").values_list("domain__name", "user__email") + user_domain_roles = ( + UserDomainRole.objects.select_related("user") + .order_by("domain__name", "user__email") + .values_list("domain__name", "user__email") + ) return list(user_domain_roles) @classmethod @@ -554,6 +564,25 @@ class DomainDataType(DomainExport): ] +class DomainDataTypeUser(DomainDataType): + """ + The DomainDataType report, but sliced on the current request user + """ + + @classmethod + def get_filter_conditions(cls, request=None): + """ + Get a Q object of filter conditions to filter when building queryset. + """ + if request is None or not hasattr(request, "user") or not request.user: + # Return nothing + return Q(id__in=[]) + + user_domain_roles = UserDomainRole.objects.filter(user=request.user) + domain_ids = user_domain_roles.values_list("domain_id", flat=True) + return Q(domain__id__in=domain_ids) + + class DomainDataFull(DomainExport): """ Shows security contacts, filtered by state @@ -611,7 +640,7 @@ class DomainDataFull(DomainExport): return ["domain"] @classmethod - def get_filter_conditions(cls, start_date=None, end_date=None): + def get_filter_conditions(cls): """ Get a Q object of filter conditions to filter when building queryset. """ @@ -706,7 +735,7 @@ class DomainDataFederal(DomainExport): return ["domain"] @classmethod - def get_filter_conditions(cls, start_date=None, end_date=None): + def get_filter_conditions(cls): """ Get a Q object of filter conditions to filter when building queryset. """ diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index f693e2cc9..abd9648ba 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -1,4 +1,8 @@ +import logging from django.shortcuts import get_object_or_404, render +from django.urls import reverse +from django.contrib import messages +from registrar.forms.portfolio import PortfolioOrgAddressForm from registrar.models.portfolio import Portfolio from registrar.views.utility.permission_views import ( PortfolioDomainRequestsPermissionView, @@ -7,6 +11,10 @@ from registrar.views.utility.permission_views import ( ) from waffle.decorators import flag_is_active from django.views.generic import View +from django.views.generic.edit import FormMixin + + +logger = logging.getLogger(__name__) class PortfolioDomainsView(PortfolioDomainsPermissionView, View): @@ -42,17 +50,61 @@ class PortfolioDomainRequestsView(PortfolioDomainRequestsPermissionView, View): return render(request, "portfolio_requests.html", context) -class PortfolioOrganizationView(PortfolioBasePermissionView, View): +class PortfolioOrganizationView(PortfolioBasePermissionView, FormMixin): + """ + View to handle displaying and updating the portfolio's organization details. + """ + model = Portfolio template_name = "portfolio_organization.html" + form_class = PortfolioOrgAddressForm + context_object_name = "portfolio" - def get(self, request, portfolio_id): - context = {} + def get_context_data(self, **kwargs): + """Add additional context data to the template.""" + context = super().get_context_data(**kwargs) + # no need to add portfolio to request context here + context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature") + context["has_organization_feature_flag"] = flag_is_active(self.request, "organization_feature") + return context - if self.request.user.is_authenticated: - context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature") - context["has_organization_feature_flag"] = flag_is_active(request, "organization_feature") - portfolio = get_object_or_404(Portfolio, id=portfolio_id) - context["portfolio"] = portfolio + def get_object(self, queryset=None): + """Get the portfolio object based on the URL parameter.""" + return get_object_or_404(Portfolio, id=self.kwargs.get("portfolio_id")) - return render(request, "portfolio_organization.html", context) + def get_form_kwargs(self): + """Include the instance in the form kwargs.""" + kwargs = super().get_form_kwargs() + kwargs["instance"] = self.get_object() + return kwargs + + def get(self, request, *args, **kwargs): + """Handle GET requests to display the form.""" + self.object = self.get_object() + form = self.get_form() + return self.render_to_response(self.get_context_data(form=form)) + + def post(self, request, *args, **kwargs): + """Handle POST requests to process form submission.""" + self.object = self.get_object() + form = self.get_form() + if form.is_valid(): + return self.form_valid(form) + else: + return self.form_invalid(form) + + def form_valid(self, form): + """Handle the case when the form is valid.""" + self.object = form.save(commit=False) + self.object.creator = self.request.user + self.object.save() + messages.success(self.request, "The organization information for this portfolio has been updated.") + return super().form_valid(form) + + def form_invalid(self, form): + """Handle the case when the form is invalid.""" + return self.render_to_response(self.get_context_data(form=form)) + + def get_success_url(self): + """Redirect to the overview page for the portfolio.""" + return reverse("portfolio-organization", kwargs={"portfolio_id": self.object.pk}) diff --git a/src/registrar/views/admin_views.py b/src/registrar/views/report_views.py similarity index 94% rename from src/registrar/views/admin_views.py rename to src/registrar/views/report_views.py index 4d015ab37..428298b52 100644 --- a/src/registrar/views/admin_views.py +++ b/src/registrar/views/report_views.py @@ -158,6 +158,17 @@ class ExportDataType(View): return response +class ExportDataTypeUser(View): + """Returns a domain report for a given user on the request""" + + def get(self, request, *args, **kwargs): + # match the CSV example with all the fields + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = 'attachment; filename="your-domains.csv"' + csv_export.DomainDataTypeUser.export_data_to_csv(response, request=request) + return response + + class ExportDataFull(View): def get(self, request, *args, **kwargs): # Smaller export based on 1 @@ -194,7 +205,7 @@ class ExportDataDomainsGrowth(View): response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = f'attachment; filename="domain-growth-report-{start_date}-to-{end_date}.csv"' - csv_export.DomainGrowth.export_data_to_csv(response, start_date, end_date) + csv_export.DomainGrowth.export_data_to_csv(response, start_date=start_date, end_date=end_date) return response @@ -206,7 +217,7 @@ class ExportDataRequestsGrowth(View): response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = f'attachment; filename="requests-{start_date}-to-{end_date}.csv"' - csv_export.DomainRequestGrowth.export_data_to_csv(response, start_date, end_date) + csv_export.DomainRequestGrowth.export_data_to_csv(response, start_date=start_date, end_date=end_date) return response @@ -217,7 +228,7 @@ class ExportDataManagedDomains(View): end_date = request.GET.get("end_date", "") response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = f'attachment; filename="managed-domains-{start_date}-to-{end_date}.csv"' - csv_export.DomainManaged.export_data_to_csv(response, start_date, end_date) + csv_export.DomainManaged.export_data_to_csv(response, start_date=start_date, end_date=end_date) return response @@ -228,6 +239,6 @@ class ExportDataUnmanagedDomains(View): end_date = request.GET.get("end_date", "") response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = f'attachment; filename="unmanaged-domains-{start_date}-to-{end_date}.csv"' - csv_export.DomainUnmanaged.export_data_to_csv(response, start_date, end_date) + csv_export.DomainUnmanaged.export_data_to_csv(response, start_date=start_date, end_date=end_date) return response