From 61e7b13df69480509f3d6c4af5b1670a5607ffdc Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 7 Oct 2024 17:41:16 -0600 Subject: [PATCH 1/9] Fixed multiple portfolio error check --- src/registrar/models/user_portfolio_permission.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index 241afd328..968ab5de6 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -5,6 +5,11 @@ from .utility.time_stamped_model import TimeStampedModel from django.contrib.postgres.fields import ArrayField +# ---Logger +import logging +from venv import logger +from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper +logger = logging.getLogger(__name__) class UserPortfolioPermission(TimeStampedModel): """This is a linking table that connects a user with a role on a portfolio.""" @@ -98,11 +103,17 @@ def _get_portfolio_permissions(self): def clean(self): """Extends clean method to perform additional validation, which can raise errors in django admin.""" super().clean() + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, f"**** CLEANING ****") # Check if a user is set without accessing the related object. has_user = bool(self.user_id) - if self.pk is None and has_user: + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, f"User: {self.user.email}") + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, f"pk: {self.pk}") + if has_user: existing_permissions = UserPortfolioPermission.objects.filter(user=self.user) + has_flag = flag_is_active_for_user(self.user, "multiple_portfolios") + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, f"multiple portfolios enabled: {has_flag}") + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, f"existing permissions detected: {existing_permissions.exists()}") if not flag_is_active_for_user(self.user, "multiple_portfolios") and existing_permissions.exists(): raise ValidationError( "This user is already assigned to a portfolio. " From c3ab816386b46327f95cac809fb4fcc2970c69f5 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 7 Oct 2024 17:41:42 -0600 Subject: [PATCH 2/9] Cleanup --- src/registrar/models/user_portfolio_permission.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index 968ab5de6..2e4c8b704 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -5,12 +5,6 @@ from .utility.time_stamped_model import TimeStampedModel from django.contrib.postgres.fields import ArrayField -# ---Logger -import logging -from venv import logger -from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper -logger = logging.getLogger(__name__) - class UserPortfolioPermission(TimeStampedModel): """This is a linking table that connects a user with a role on a portfolio.""" @@ -107,13 +101,8 @@ def clean(self): # Check if a user is set without accessing the related object. has_user = bool(self.user_id) - TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, f"User: {self.user.email}") - TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, f"pk: {self.pk}") if has_user: existing_permissions = UserPortfolioPermission.objects.filter(user=self.user) - has_flag = flag_is_active_for_user(self.user, "multiple_portfolios") - TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, f"multiple portfolios enabled: {has_flag}") - TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, f"existing permissions detected: {existing_permissions.exists()}") if not flag_is_active_for_user(self.user, "multiple_portfolios") and existing_permissions.exists(): raise ValidationError( "This user is already assigned to a portfolio. " From ae197ab3ed3f3aee7d75cbfb48707f4c70d405e5 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 7 Oct 2024 21:40:58 -0600 Subject: [PATCH 3/9] Cleanup & slight fix for other permission checks --- .../models/user_portfolio_permission.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index 2e4c8b704..1ea54acc7 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -97,8 +97,15 @@ def _get_portfolio_permissions(self): def clean(self): """Extends clean method to perform additional validation, which can raise errors in django admin.""" super().clean() - TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, f"**** CLEANING ****") + # Check if portfolio is set without accessing the related object. + has_portfolio = bool(self.portfolio_id) + if not has_portfolio and self._get_portfolio_permissions(): + raise ValidationError("When portfolio roles or additional permissions are assigned, portfolio is required.") + + if has_portfolio and not self._get_portfolio_permissions(): + raise ValidationError("When portfolio is assigned, portfolio roles or additional permissions are required.") + # Check if a user is set without accessing the related object. has_user = bool(self.user_id) if has_user: @@ -108,11 +115,3 @@ def clean(self): "This user is already assigned to a portfolio. " "Based on current waffle flag settings, users cannot be assigned to multiple portfolios." ) - - # Check if portfolio is set without accessing the related object. - has_portfolio = bool(self.portfolio_id) - if not has_portfolio and self._get_portfolio_permissions(): - raise ValidationError("When portfolio roles or additional permissions are assigned, portfolio is required.") - - if has_portfolio and not self._get_portfolio_permissions(): - raise ValidationError("When portfolio is assigned, portfolio roles or additional permissions are required.") From 23e99c054d79f6d323d55868bf78c6e54e2524f4 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 8 Oct 2024 15:53:04 -0600 Subject: [PATCH 4/9] added unit test --- .../models/user_portfolio_permission.py | 16 ++++++-- src/registrar/tests/test_models.py | 39 ++++++++++++++++--- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index 1ea54acc7..74ef9901e 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -5,6 +5,13 @@ from .utility.time_stamped_model import TimeStampedModel from django.contrib.postgres.fields import ArrayField +# ---Logger +import logging +from venv import logger +from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper +logger = logging.getLogger(__name__) + + class UserPortfolioPermission(TimeStampedModel): """This is a linking table that connects a user with a role on a portfolio.""" @@ -105,12 +112,15 @@ def clean(self): if has_portfolio and not self._get_portfolio_permissions(): raise ValidationError("When portfolio is assigned, portfolio roles or additional permissions are required.") - + # Check if a user is set without accessing the related object. has_user = bool(self.user_id) + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, f"***CLEANING***") if has_user: - existing_permissions = UserPortfolioPermission.objects.filter(user=self.user) - if not flag_is_active_for_user(self.user, "multiple_portfolios") and existing_permissions.exists(): + existing_permission_pks = UserPortfolioPermission.objects.filter(user=self.user).values_list("pk", flat=True) + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, f"existing_permission_pks: {existing_permission_pks}") + TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, f"self pk: {self.pk}") + if not flag_is_active_for_user(self.user, "multiple_portfolios") and existing_permission_pks.exists() and not self.pk in existing_permission_pks: raise ValidationError( "This user is already assigned to a portfolio. " "Based on current waffle flag settings, users cannot be assigned to multiple portfolios." diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 015c67dab..681d04c92 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -41,7 +41,6 @@ from api.tests.common import less_console_noise_decorator - @boto3_mocking.patching class TestDomainRequest(TestCase): @less_console_noise_decorator @@ -1274,6 +1273,7 @@ class TestUserPortfolioPermission(TestCase): @less_console_noise_decorator def setUp(self): self.user, _ = User.objects.get_or_create(email="mayor@igorville.gov") + self.user2, _ = User.objects.get_or_create(email="user2@igorville.gov", username="user2") super().setUp() def tearDown(self): @@ -1306,21 +1306,20 @@ def test_clean_on_multiple_portfolios_when_flag_active(self): portfolio_permission_2.clean() except ValidationError as error: self.fail(f"Raised ValidationError unexpectedly: {error}") - + @less_console_noise_decorator @override_flag("multiple_portfolios", active=False) def test_clean_on_creates_multiple_portfolios(self): """Ensures that a user cannot create multiple portfolio permission objects when the flag is disabled""" - # Create an instance of User with a portfolio but no roles or additional permissions + # Create an instance of User with a single portfolio portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California") - portfolio_2, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Motel California") portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create( portfolio=portfolio, user=self.user, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] ) + portfolio_2, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Motel California") portfolio_permission_2 = UserPortfolioPermission( portfolio=portfolio_2, user=self.user, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] ) - # This should work as intended portfolio_permission.clean() @@ -1328,7 +1327,35 @@ def test_clean_on_creates_multiple_portfolios(self): with self.assertRaises(ValidationError) as cm: portfolio_permission_2.clean() - portfolio_permission_2, _ = UserPortfolioPermission.objects.get_or_create(portfolio=portfolio, user=self.user) + self.assertEqual( + cm.exception.message, + ( + "This user is already assigned to a portfolio. " + "Based on current waffle flag settings, users cannot be assigned to multiple portfolios." + ), + ) + + @less_console_noise_decorator + @override_flag("multiple_portfolios", active=False) + def test_multiple_portfolio_reassignment(self): + """Ensures that a user cannot be assigned to multiple portfolios based on reassignment""" + # Create an instance of two users with separate portfolios + portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California") + portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create( + portfolio=portfolio, user=self.user, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + ) + portfolio_2, _ = Portfolio.objects.get_or_create(creator=self.user2, organization_name="Motel California") + portfolio_permission_2 = UserPortfolioPermission( + portfolio=portfolio_2, user=self.user2, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + ) + + # This should work as intended + portfolio_permission.clean() + portfolio_permission_2.clean() + + with self.assertRaises(ValidationError) as cm: + portfolio_permission_2.user = self.user + portfolio_permission_2.clean() self.assertEqual( cm.exception.message, From bd018e14ab5ab64141e4b7cad1dfe97f9694b0bf Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 8 Oct 2024 15:53:31 -0600 Subject: [PATCH 5/9] cleanup --- src/registrar/models/user_portfolio_permission.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index 74ef9901e..825e82c88 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -5,12 +5,6 @@ from .utility.time_stamped_model import TimeStampedModel from django.contrib.postgres.fields import ArrayField -# ---Logger -import logging -from venv import logger -from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper -logger = logging.getLogger(__name__) - class UserPortfolioPermission(TimeStampedModel): """This is a linking table that connects a user with a role on a portfolio.""" @@ -115,11 +109,8 @@ def clean(self): # Check if a user is set without accessing the related object. has_user = bool(self.user_id) - TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, f"***CLEANING***") if has_user: existing_permission_pks = UserPortfolioPermission.objects.filter(user=self.user).values_list("pk", flat=True) - TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, f"existing_permission_pks: {existing_permission_pks}") - TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, f"self pk: {self.pk}") if not flag_is_active_for_user(self.user, "multiple_portfolios") and existing_permission_pks.exists() and not self.pk in existing_permission_pks: raise ValidationError( "This user is already assigned to a portfolio. " From d1f3f0efb14aa78e1b3de175888d6885e0c84f9c Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 9 Oct 2024 14:08:05 -0600 Subject: [PATCH 6/9] Revert "resolved merge conflict" This reverts commit 85be976ee483e1364e763add30fea4e53e2d4617, reversing changes made to bd018e14ab5ab64141e4b7cad1dfe97f9694b0bf. --- src/registrar/admin.py | 32 +- src/registrar/assets/js/get-gov-admin.js | 21 +- src/registrar/assets/sass/_theme/_admin.scss | 101 +- .../admin/change_form_object_tools.html | 3 +- .../templates/admin/input_with_clipboard.html | 32 +- .../admin/includes/contact_detail_list.html | 2 +- .../admin/includes/detail_table_fieldset.html | 37 +- src/registrar/tests/test_admin.py | 2 +- src/registrar/tests/test_admin_domain.py | 2 +- src/registrar/tests/test_admin_request.py | 54 +- src/registrar/tests/test_models.py | 7 + src/registrar/tests/test_models_requests.py | 1029 ----------------- src/registrar/tests/test_views_request.py | 1 + 13 files changed, 130 insertions(+), 1193 deletions(-) delete mode 100644 src/registrar/tests/test_models_requests.py diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 485a1b07d..ca51e8b72 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1976,30 +1976,18 @@ def _handle_status_change(self, request, obj, original_obj): # If the status is not mapped properly, saving could cause # weird issues down the line. Instead, we should block this. - # NEEDS A UNIT TEST should_proceed = False - return (obj, should_proceed) - - obj_is_not_approved = obj.status != models.DomainRequest.DomainRequestStatus.APPROVED - if obj_is_not_approved and not obj.domain_is_not_active(): - # REDUNDANT CHECK / ERROR SCREEN AVOIDANCE: - # This action (moving a request from approved to - # another status) when the domain is already active (READY), - # would still not go through even without this check as the rules are - # duplicated in the model and the error is raised from the model. - # This avoids an ugly Django error screen. + return should_proceed + + request_is_not_approved = obj.status != models.DomainRequest.DomainRequestStatus.APPROVED + if request_is_not_approved and not obj.domain_is_not_active(): + # If an admin tried to set an approved domain request to + # another status and the related domain is already + # active, shortcut the action and throw a friendly + # error message. This action would still not go through + # shortcut or not as the rules are duplicated on the model, + # but the error would be an ugly Django error screen. error_message = "This action is not permitted. The domain is already active." - elif ( - original_obj.status != models.DomainRequest.DomainRequestStatus.APPROVED - and obj.status == models.DomainRequest.DomainRequestStatus.APPROVED - and original_obj.requested_domain is not None - and Domain.objects.filter(name=original_obj.requested_domain.name).exists() - ): - # REDUNDANT CHECK: - # This action (approving a request when the domain exists) - # would still not go through even without this check as the rules are - # duplicated in the model and the error is raised from the model. - error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.APPROVE_DOMAIN_IN_USE) elif obj.status == models.DomainRequest.DomainRequestStatus.REJECTED and not obj.rejection_reason: # This condition should never be triggered. # The opposite of this condition is acceptable (rejected -> other status and rejection_reason) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index f44211c6d..73f3dded1 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -515,14 +515,10 @@ document.addEventListener('DOMContentLoaded', function() { const formLabel = document.querySelector('label[for="id_action_needed_reason_email"]'); let lastSentEmailContent = document.getElementById("last-sent-email-content"); const initialDropdownValue = dropdown ? dropdown.value : null; - let initialEmailValue; - if (textarea) - initialEmailValue = textarea.value + const initialEmailValue = textarea.value; // We will use the const to control the modal - let isEmailAlreadySentConst; - if (lastSentEmailContent) - isEmailAlreadySentConst = lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, ''); + let isEmailAlreadySentConst = lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, ''); // We will use the function to control the label and help function isEmailAlreadySent() { return lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, ''); @@ -710,6 +706,18 @@ document.addEventListener('DOMContentLoaded', function() { } return ''; } + // Extract the submitter name, title, email, and phone number + const submitterDiv = document.querySelector('.form-row.field-submitter'); + const submitterNameElement = document.getElementById('id_submitter'); + // We have to account for different superuser and analyst markups + const submitterName = submitterNameElement + ? submitterNameElement.options[submitterNameElement.selectedIndex].text + : submitterDiv.querySelector('a').text; + const submitterTitle = extractTextById('contact_info_title', submitterDiv); + const submitterEmail = extractTextById('contact_info_email', submitterDiv); + const submitterPhone = extractTextById('contact_info_phone', submitterDiv); + let submitterInfo = `${submitterName}${submitterTitle}${submitterEmail}${submitterPhone}`; + //------ Senior Official const seniorOfficialDiv = document.querySelector('.form-row.field-senior_official'); @@ -726,6 +734,7 @@ document.addEventListener('DOMContentLoaded', function() { `Current Websites: ${existingWebsites.join(', ')}
` + `Rationale:
` + `Alternative Domains: ${alternativeDomains.join(', ')}
` + + `Submitter: ${submitterInfo}
` + `Senior Official: ${seniorOfficialInfo}
` + `Other Employees: ${otherContactsSummary}
`; diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index b6bc0d296..5cea72c4c 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -385,7 +385,6 @@ a.button, font-kerning: auto; font-family: inherit; font-weight: normal; - text-decoration: none !important; } .button svg, .button span, @@ -393,9 +392,6 @@ a.button, .usa-button--dja span { vertical-align: middle; } -.usa-button--dja.usa-button--unstyled { - color: var(--link-fg); -} .usa-button--dja:not(.usa-button--unstyled, .usa-button--outline, .usa-modal__close, .usa-button--secondary) { background: var(--button-bg); } @@ -425,34 +421,11 @@ input[type=submit].button--dja-toolbar { input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-toolbar:hover { border-color: var(--body-quiet-color); } -.admin-icon-group { - position: relative; - display: inline; - align-items: center; - - input { - // Allow for padding around the copy button - padding-right: 35px !important; - } - - button { - width: max-content; - } - - @media (max-width: 1000px) { - button { - display: block; - } - } - - span { - padding-left: 0.05rem; - } - -} -.usa-button__small-text, -.usa-button__small-text span { - font-size: 13px; +// Targets the DJA buttom with a nested icon +button .usa-icon, +.button .usa-icon, +.button--clipboard .usa-icon { + vertical-align: middle; } .module--custom { @@ -700,10 +673,71 @@ address.dja-address-contact-list { } } +// Make the clipboard button "float" inside of the input box +.admin-icon-group { + position: relative; + display: inline; + align-items: center; + + input { + // Allow for padding around the copy button + padding-right: 35px !important; + // Match the height of other inputs + min-height: 2.25rem !important; + } + + button { + line-height: 14px; + width: max-content; + font-size: unset; + text-decoration: none !important; + } + + @media (max-width: 1000px) { + button { + display: block; + padding-top: 8px; + } + } + + span { + padding-left: 0.1rem; + } + +} + +.admin-icon-group.admin-icon-group__clipboard-link { + position: relative; + display: inline; + align-items: center; + + + .usa-button--icon { + position: absolute; + right: auto; + left: 4px; + height: 100%; + top: -1px; + } + button { + font-size: unset !important; + display: inline-flex; + padding-top: 4px; + line-height: 14px; + width: max-content; + font-size: unset; + text-decoration: none !important; + } +} + .no-outline-on-click:focus { outline: none !important; } +.usa-button__small-text { + font-size: small; +} + // Get rid of padding on all help texts form .aligned p.help, form .aligned div.help { padding-left: 0px !important; @@ -853,9 +887,6 @@ div.dja__model-description{ padding-top: 0 !important; } -.padding-bottom-0 { - padding-bottom: 0 !important; -} .flex-container { @media screen and (min-width: 700px) and (max-width: 1150px) { diff --git a/src/registrar/templates/admin/change_form_object_tools.html b/src/registrar/templates/admin/change_form_object_tools.html index 66011a3c4..198140c19 100644 --- a/src/registrar/templates/admin/change_form_object_tools.html +++ b/src/registrar/templates/admin/change_form_object_tools.html @@ -20,11 +20,10 @@ {% if opts.model_name == 'domainrequest' %}
  • - + - {% translate "Copy request summary" %}
  • diff --git a/src/registrar/templates/admin/input_with_clipboard.html b/src/registrar/templates/admin/input_with_clipboard.html index d6a016fd5..5ad2b27f7 100644 --- a/src/registrar/templates/admin/input_with_clipboard.html +++ b/src/registrar/templates/admin/input_with_clipboard.html @@ -8,7 +8,7 @@
    {{ field }}
    {% else %} -
    + -{% endif %} +{% endif %} \ No newline at end of file diff --git a/src/registrar/templates/django/admin/includes/contact_detail_list.html b/src/registrar/templates/django/admin/includes/contact_detail_list.html index 84fb07f33..0a28a6532 100644 --- a/src/registrar/templates/django/admin/includes/contact_detail_list.html +++ b/src/registrar/templates/django/admin/includes/contact_detail_list.html @@ -26,7 +26,7 @@ {% if user.email %} {{ user.email }} {% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %} -
    +
    {% else %} None
    {% endif %} diff --git a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html index 2369f235b..6b755724e 100644 --- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html +++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html @@ -254,7 +254,7 @@

    - + @@ -267,31 +267,18 @@

    {% endfor %} diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 9c5e3b582..cdc3c97de 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -654,7 +654,7 @@ def test_contact_fields_have_detail_table(self): self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields) # Test for the copy link - self.assertContains(response, "copy-to-clipboard", count=3) + self.assertContains(response, "button--clipboard", count=3) # cleanup this test domain_info.delete() diff --git a/src/registrar/tests/test_admin_domain.py b/src/registrar/tests/test_admin_domain.py index f02b59a91..a9b94781f 100644 --- a/src/registrar/tests/test_admin_domain.py +++ b/src/registrar/tests/test_admin_domain.py @@ -535,7 +535,7 @@ def test_contact_fields_on_domain_change_form_have_detail_table(self): self.assertContains(response, "Testy Tester") # Test for the copy link - self.assertContains(response, "copy-to-clipboard") + self.assertContains(response, "button--clipboard") # cleanup from this test domain.delete() diff --git a/src/registrar/tests/test_admin_request.py b/src/registrar/tests/test_admin_request.py index f19008ca1..a9b073472 100644 --- a/src/registrar/tests/test_admin_request.py +++ b/src/registrar/tests/test_admin_request.py @@ -1511,7 +1511,7 @@ def test_contact_fields_have_detail_table(self): self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields) # Test for the copy link - self.assertContains(response, "copy-to-clipboard", count=4) + self.assertContains(response, "button--clipboard", count=4) # Test that Creator counts display properly self.assertNotContains(response, "Approved domains") @@ -1846,58 +1846,6 @@ def test_side_effects_when_saving_approved_to_rejected(self): def test_side_effects_when_saving_approved_to_ineligible(self): self.trigger_saving_approved_to_another_state(False, DomainRequest.DomainRequestStatus.INELIGIBLE) - @less_console_noise - def test_error_when_saving_to_approved_and_domain_exists(self): - """Redundant admin check on model transition not allowed.""" - Domain.objects.create(name="wabbitseason.gov") - - new_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.SUBMITTED, name="wabbitseason.gov" - ) - - # Create a request object with a superuser - request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(new_request.pk)) - request.user = self.superuser - - request.session = {} - - # Use ExitStack to combine patch contexts - with ExitStack() as stack: - # Patch django.contrib.messages.error - stack.enter_context(patch.object(messages, "error")) - - new_request.status = DomainRequest.DomainRequestStatus.APPROVED - - self.admin.save_model(request, new_request, None, True) - - messages.error.assert_called_once_with( - request, - "Cannot approve. Requested domain is already in use.", - ) - - @less_console_noise - def test_no_error_when_saving_to_approved_and_domain_exists(self): - """The negative of the redundant admin check on model transition not allowed.""" - new_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED) - - # Create a request object with a superuser - request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(new_request.pk)) - request.user = self.superuser - - request.session = {} - - # 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(messages, "error")) - - new_request.status = DomainRequest.DomainRequestStatus.APPROVED - - self.admin.save_model(request, new_request, None, True) - - # Assert that the error message was never called - messages.error.assert_not_called() - def test_has_correct_filters(self): """ This test verifies that DomainRequestAdmin has the correct filters set up. diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index f1d25ece9..681d04c92 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -1,5 +1,7 @@ from django.forms import ValidationError from django.test import TestCase +from django.db.utils import IntegrityError +from django.db import transaction from unittest.mock import patch from django.test import RequestFactory @@ -18,18 +20,23 @@ UserPortfolioPermission, AllowedEmail, ) + import boto3_mocking from registrar.models.portfolio import Portfolio from registrar.models.portfolio_invitation import PortfolioInvitation from registrar.models.transition_domain import TransitionDomain from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices 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, create_test_user, ) +from django_fsm import TransitionNotAllowed from waffle.testutils import override_flag from api.tests.common import less_console_noise_decorator diff --git a/src/registrar/tests/test_models_requests.py b/src/registrar/tests/test_models_requests.py deleted file mode 100644 index 9e86f5f9c..000000000 --- a/src/registrar/tests/test_models_requests.py +++ /dev/null @@ -1,1029 +0,0 @@ -from django.test import TestCase -from django.db.utils import IntegrityError -from django.db import transaction -from unittest.mock import patch - - -from registrar.models import ( - Contact, - DomainRequest, - DomainInformation, - User, - Website, - Domain, - DraftDomain, - FederalAgency, - AllowedEmail, -) - -import boto3_mocking -from registrar.utility.constants import BranchChoices -from registrar.utility.errors import FSMDomainRequestError - -from .common import ( - MockSESClient, - less_console_noise, - completed_domain_request, - set_domain_request_investigators, -) -from django_fsm import TransitionNotAllowed - -from api.tests.common import less_console_noise_decorator - - -@boto3_mocking.patching -class TestDomainRequest(TestCase): - @less_console_noise_decorator - def setUp(self): - - self.dummy_user, _ = Contact.objects.get_or_create( - email="mayor@igorville.com", first_name="Hello", last_name="World" - ) - self.dummy_user_2, _ = User.objects.get_or_create( - username="intern@igorville.com", email="intern@igorville.com", first_name="Lava", last_name="World" - ) - self.started_domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.STARTED, - name="started.gov", - ) - self.submitted_domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.SUBMITTED, - name="submitted.gov", - ) - self.in_review_domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.IN_REVIEW, - name="in-review.gov", - ) - self.action_needed_domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.ACTION_NEEDED, - name="action-needed.gov", - ) - self.approved_domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.APPROVED, - name="approved.gov", - ) - self.withdrawn_domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.WITHDRAWN, - name="withdrawn.gov", - ) - self.rejected_domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.REJECTED, - name="rejected.gov", - ) - self.ineligible_domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.INELIGIBLE, - name="ineligible.gov", - ) - - # Store all domain request statuses in a variable for ease of use - self.all_domain_requests = [ - self.started_domain_request, - self.submitted_domain_request, - self.in_review_domain_request, - self.action_needed_domain_request, - self.approved_domain_request, - self.withdrawn_domain_request, - self.rejected_domain_request, - self.ineligible_domain_request, - ] - - self.mock_client = MockSESClient() - - 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): - """Helper method for testing allowed transitions.""" - with less_console_noise(): - return self.assertRaises(Exception, None, exception_type) - - @less_console_noise_decorator - def test_request_is_withdrawable(self): - """Tests the is_withdrawable function""" - domain_request_1 = completed_domain_request( - status=DomainRequest.DomainRequestStatus.SUBMITTED, - name="city2.gov", - ) - domain_request_2 = completed_domain_request( - status=DomainRequest.DomainRequestStatus.IN_REVIEW, - name="city3.gov", - ) - domain_request_3 = completed_domain_request( - status=DomainRequest.DomainRequestStatus.ACTION_NEEDED, - name="city4.gov", - ) - domain_request_4 = completed_domain_request( - status=DomainRequest.DomainRequestStatus.REJECTED, - name="city5.gov", - ) - self.assertTrue(domain_request_1.is_withdrawable()) - self.assertTrue(domain_request_2.is_withdrawable()) - self.assertTrue(domain_request_3.is_withdrawable()) - self.assertFalse(domain_request_4.is_withdrawable()) - - @less_console_noise_decorator - def test_request_is_awaiting_review(self): - """Tests the is_awaiting_review function""" - domain_request_1 = completed_domain_request( - status=DomainRequest.DomainRequestStatus.SUBMITTED, - name="city2.gov", - ) - domain_request_2 = completed_domain_request( - status=DomainRequest.DomainRequestStatus.IN_REVIEW, - name="city3.gov", - ) - domain_request_3 = completed_domain_request( - status=DomainRequest.DomainRequestStatus.ACTION_NEEDED, - name="city4.gov", - ) - domain_request_4 = completed_domain_request( - status=DomainRequest.DomainRequestStatus.REJECTED, - name="city5.gov", - ) - self.assertTrue(domain_request_1.is_awaiting_review()) - self.assertTrue(domain_request_2.is_awaiting_review()) - self.assertFalse(domain_request_3.is_awaiting_review()) - self.assertFalse(domain_request_4.is_awaiting_review()) - - @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""" - domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.IN_REVIEW, - name="city2.gov", - federal_agency=None, - ) - - # Ensure that the federal agency is None - self.assertEqual(domain_request.federal_agency, None) - - # Approve the request - domain_request.approve() - self.assertEqual(domain_request.status, DomainRequest.DomainRequestStatus.APPROVED) - - # After approval, it should be "Non-Federal agency" - expected_federal_agency = FederalAgency.objects.filter(agency="Non-Federal Agency").first() - self.assertEqual(domain_request.federal_agency, expected_federal_agency) - - def test_empty_create_fails(self): - """Can't create a completely empty domain request.""" - with less_console_noise(): - 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.""" - 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.""" - 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, - 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.""" - 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, - 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): - 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() - - @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 email sent to creator 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" - ): - """Check if an email was sent after performing an action.""" - email_allowed, _ = AllowedEmail.objects.get_or_create(email=expected_email) - with self.subTest(msg=msg, action=action): - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - # Perform the specified action - action_method = getattr(domain_request, action) - action_method() - - # Check if an email was sent - sent_emails = [ - email - for email in MockSESClient.EMAILS_SENT - if expected_email in email["kwargs"]["Destination"]["ToAddresses"] - ] - self.assertEqual(len(sent_emails), expected_count) - - if expected_content: - email_content = sent_emails[0]["kwargs"]["Content"]["Simple"]["Body"]["Text"]["Data"] - self.assertIn(expected_content, email_content) - - email_allowed.delete() - - @less_console_noise_decorator - def test_submit_from_started_sends_email_to_creator(self): - """tests that 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." - domain_request = completed_domain_request(user=self.dummy_user_2) - self.check_email_sent( - 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." - user, _ = User.objects.get_or_create(username="testy") - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.WITHDRAWN, user=user) - self.check_email_sent(domain_request, msg, "submit", 1, expected_content="Hi", expected_email=user.email) - - @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." - user, _ = User.objects.get_or_create(username="testy") - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=user) - self.check_email_sent(domain_request, msg, "approve", 1, expected_content="approved", expected_email=user.email) - - @less_console_noise_decorator - def test_withdraw_sends_email(self): - msg = "Create a domain request and withdraw it and see if email was sent." - user, _ = User.objects.get_or_create(username="testy") - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=user) - self.check_email_sent( - domain_request, msg, "withdraw", 1, expected_content="withdrawn", expected_email=user.email - ) - - @less_console_noise_decorator - def test_reject_sends_email(self): - msg = "Create a domain request and reject it and see if email was sent." - user, _ = User.objects.get_or_create(username="testy") - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.APPROVED, user=user) - self.check_email_sent(domain_request, msg, "reject", 1, expected_content="Hi", expected_email=user.email) - - @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(): - for domain_request, exception_type in test_cases: - with self.subTest(domain_request=domain_request, exception_type=exception_type): - with self.assertRaises(exception_type): - # Retrieve the method by name from the domain_request object and call it - method = getattr(domain_request, method_to_run) - # 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(): - for domain_request, exception_type in test_cases: - with self.subTest(domain_request=domain_request, exception_type=exception_type): - try: - # Retrieve the method by name from the DomainRequest object and call it - method = getattr(domain_request, method_to_run) - # Call the method - method() - 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. - For submit, this should be valid in all cases. - """ - - test_cases = [ - (self.started_domain_request, TransitionNotAllowed), - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.withdrawn_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to none - set_domain_request_investigators(self.all_domain_requests, None) - - 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. - For submit, this should be valid in all cases. - """ - - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to a user with no staff privs - user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) - set_domain_request_investigators(self.all_domain_requests, user) - - 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. - """ - test_cases = [ - (self.started_domain_request, TransitionNotAllowed), - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.withdrawn_domain_request, TransitionNotAllowed), - ] - - 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): - 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() - - # 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. - """ - test_cases = [ - (self.submitted_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - 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. - """ - test_cases = [ - (self.submitted_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - 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 - """ - - test_cases = [ - (self.action_needed_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to none - set_domain_request_investigators(self.all_domain_requests, None) - - 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. - This should throw an exception. - """ - - test_cases = [ - (self.action_needed_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to a user with no staff privs - user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) - set_domain_request_investigators(self.all_domain_requests, user) - - 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. - """ - test_cases = [ - (self.started_domain_request, TransitionNotAllowed), - (self.in_review_domain_request, TransitionNotAllowed), - (self.withdrawn_domain_request, TransitionNotAllowed), - ] - - 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. - """ - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - 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 - """ - - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to none - set_domain_request_investigators(self.all_domain_requests, None) - - 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 - """ - - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to a user with no staff privs - user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) - set_domain_request_investigators(self.all_domain_requests, user) - - 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. - """ - test_cases = [ - (self.started_domain_request, TransitionNotAllowed), - (self.submitted_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.withdrawn_domain_request, TransitionNotAllowed), - ] - - 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. - """ - test_cases = [ - (self.submitted_domain_request, TransitionNotAllowed), - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - ] - - 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 - """ - - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to none - set_domain_request_investigators(self.all_domain_requests, None) - - 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 - """ - - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to a user with no staff privs - user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) - set_domain_request_investigators(self.all_domain_requests, user) - - 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 - an email - """ - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - 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 approve against transition rules raises TransitionNotAllowed. - """ - test_cases = [ - (self.started_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.withdrawn_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - self.assert_fsm_transition_raises_error(test_cases, "approve") - - @less_console_noise_decorator - def test_approved_transition_not_allowed_when_domain_already_approved(self): - """ - Test that calling approve whith an already approved requested domain raises - TransitionNotAllowed. - """ - Domain.objects.all().create(name=self.submitted_domain_request.requested_domain.name) - test_cases = [ - (self.submitted_domain_request, FSMDomainRequestError), - ] - 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. - """ - test_cases = [ - (self.submitted_domain_request, TransitionNotAllowed), - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - ] - - 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. - For withdraw, this should be valid in all cases. - """ - - test_cases = [ - (self.submitted_domain_request, TransitionNotAllowed), - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to none - set_domain_request_investigators(self.all_domain_requests, None) - - 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. - For withdraw, this should be valid in all cases. - """ - - test_cases = [ - (self.submitted_domain_request, TransitionNotAllowed), - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to a user with no staff privs - user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) - set_domain_request_investigators(self.all_domain_requests, user) - - 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. - """ - test_cases = [ - (self.started_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.withdrawn_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - 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. - """ - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - ] - - 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 - """ - - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to none - set_domain_request_investigators(self.all_domain_requests, None) - - 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 - """ - - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to a user with no staff privs - user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) - set_domain_request_investigators(self.all_domain_requests, user) - - 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. - """ - test_cases = [ - (self.started_domain_request, TransitionNotAllowed), - (self.submitted_domain_request, TransitionNotAllowed), - (self.withdrawn_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - 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. - """ - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - ] - - 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 - """ - - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to none - set_domain_request_investigators(self.all_domain_requests, None) - - 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 - """ - - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to a user with no staff privs - user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) - set_domain_request_investigators(self.all_domain_requests, user) - - 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. - """ - test_cases = [ - (self.started_domain_request, TransitionNotAllowed), - (self.submitted_domain_request, TransitionNotAllowed), - (self.withdrawn_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - 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""" - - domain = Domain.objects.create(name=self.approved_domain_request.requested_domain.name) - self.approved_domain_request.approved_domain = domain - self.approved_domain_request.save() - - # Define a custom implementation for is_active - def custom_is_active(self): - return True # Override to return True - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - # 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""" - - domain = Domain.objects.create(name=self.approved_domain_request.requested_domain.name) - self.approved_domain_request.approved_domain = domain - self.approved_domain_request.save() - - # Define a custom implementation for is_active - def custom_is_active(self): - return True # Override to return True - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - # 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""" - - domain = Domain.objects.create(name=self.approved_domain_request.requested_domain.name) - self.approved_domain_request.approved_domain = domain - self.approved_domain_request.save() - - # Define a custom implementation for is_active - def custom_is_active(self): - return True # Override to return True - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - # 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""" - - domain = Domain.objects.create(name=self.approved_domain_request.requested_domain.name) - self.approved_domain_request.approved_domain = domain - self.approved_domain_request.save() - - # Define a custom implementation for is_active - def custom_is_active(self): - return True # Override to return True - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - # 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.""" - - # 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() - - 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.""" - - # 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() - - 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.""" - - # 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() - - 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""" - 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""" - 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""" - # 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""" - 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) diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index ff2e61939..8530859e2 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -82,6 +82,7 @@ def test_get_first_status_started_date(self, mock_get_first_status_set_date): response = self.app.get(f"/domain-request/{domain_request.id}") # Ensure that the date is still set to None self.assertIsNone(domain_request.last_status_update) + print(response) # We should still grab a date for this field in this event - but it should come from the audit log instead self.assertContains(response, "Started on:") self.assertContains(response, fixed_date.strftime("%B %-d, %Y")) From 75c610476c6813205108e6cf75cabec11b114567 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 9 Oct 2024 14:09:39 -0600 Subject: [PATCH 7/9] Reapply "resolved merge conflict" This reverts commit d1f3f0efb14aa78e1b3de175888d6885e0c84f9c. --- src/registrar/admin.py | 32 +- src/registrar/assets/js/get-gov-admin.js | 21 +- src/registrar/assets/sass/_theme/_admin.scss | 101 +- .../admin/change_form_object_tools.html | 3 +- .../templates/admin/input_with_clipboard.html | 32 +- .../admin/includes/contact_detail_list.html | 2 +- .../admin/includes/detail_table_fieldset.html | 37 +- src/registrar/tests/test_admin.py | 2 +- src/registrar/tests/test_admin_domain.py | 2 +- src/registrar/tests/test_admin_request.py | 54 +- src/registrar/tests/test_models.py | 7 - src/registrar/tests/test_models_requests.py | 1029 +++++++++++++++++ src/registrar/tests/test_views_request.py | 1 - 13 files changed, 1193 insertions(+), 130 deletions(-) create mode 100644 src/registrar/tests/test_models_requests.py diff --git a/src/registrar/admin.py b/src/registrar/admin.py index ca51e8b72..485a1b07d 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1976,18 +1976,30 @@ def _handle_status_change(self, request, obj, original_obj): # If the status is not mapped properly, saving could cause # weird issues down the line. Instead, we should block this. + # NEEDS A UNIT TEST should_proceed = False - return should_proceed - - request_is_not_approved = obj.status != models.DomainRequest.DomainRequestStatus.APPROVED - if request_is_not_approved and not obj.domain_is_not_active(): - # If an admin tried to set an approved domain request to - # another status and the related domain is already - # active, shortcut the action and throw a friendly - # error message. This action would still not go through - # shortcut or not as the rules are duplicated on the model, - # but the error would be an ugly Django error screen. + return (obj, should_proceed) + + obj_is_not_approved = obj.status != models.DomainRequest.DomainRequestStatus.APPROVED + if obj_is_not_approved and not obj.domain_is_not_active(): + # REDUNDANT CHECK / ERROR SCREEN AVOIDANCE: + # This action (moving a request from approved to + # another status) when the domain is already active (READY), + # would still not go through even without this check as the rules are + # duplicated in the model and the error is raised from the model. + # This avoids an ugly Django error screen. error_message = "This action is not permitted. The domain is already active." + elif ( + original_obj.status != models.DomainRequest.DomainRequestStatus.APPROVED + and obj.status == models.DomainRequest.DomainRequestStatus.APPROVED + and original_obj.requested_domain is not None + and Domain.objects.filter(name=original_obj.requested_domain.name).exists() + ): + # REDUNDANT CHECK: + # This action (approving a request when the domain exists) + # would still not go through even without this check as the rules are + # duplicated in the model and the error is raised from the model. + error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.APPROVE_DOMAIN_IN_USE) elif obj.status == models.DomainRequest.DomainRequestStatus.REJECTED and not obj.rejection_reason: # This condition should never be triggered. # The opposite of this condition is acceptable (rejected -> other status and rejection_reason) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 73f3dded1..f44211c6d 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -515,10 +515,14 @@ document.addEventListener('DOMContentLoaded', function() { const formLabel = document.querySelector('label[for="id_action_needed_reason_email"]'); let lastSentEmailContent = document.getElementById("last-sent-email-content"); const initialDropdownValue = dropdown ? dropdown.value : null; - const initialEmailValue = textarea.value; + let initialEmailValue; + if (textarea) + initialEmailValue = textarea.value // We will use the const to control the modal - let isEmailAlreadySentConst = lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, ''); + let isEmailAlreadySentConst; + if (lastSentEmailContent) + isEmailAlreadySentConst = lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, ''); // We will use the function to control the label and help function isEmailAlreadySent() { return lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, ''); @@ -706,18 +710,6 @@ document.addEventListener('DOMContentLoaded', function() { } return ''; } - // Extract the submitter name, title, email, and phone number - const submitterDiv = document.querySelector('.form-row.field-submitter'); - const submitterNameElement = document.getElementById('id_submitter'); - // We have to account for different superuser and analyst markups - const submitterName = submitterNameElement - ? submitterNameElement.options[submitterNameElement.selectedIndex].text - : submitterDiv.querySelector('a').text; - const submitterTitle = extractTextById('contact_info_title', submitterDiv); - const submitterEmail = extractTextById('contact_info_email', submitterDiv); - const submitterPhone = extractTextById('contact_info_phone', submitterDiv); - let submitterInfo = `${submitterName}${submitterTitle}${submitterEmail}${submitterPhone}`; - //------ Senior Official const seniorOfficialDiv = document.querySelector('.form-row.field-senior_official'); @@ -734,7 +726,6 @@ document.addEventListener('DOMContentLoaded', function() { `Current Websites: ${existingWebsites.join(', ')}
    ` + `Rationale:
    ` + `Alternative Domains: ${alternativeDomains.join(', ')}
    ` + - `Submitter: ${submitterInfo}
    ` + `Senior Official: ${seniorOfficialInfo}
    ` + `Other Employees: ${otherContactsSummary}
    `; diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 5cea72c4c..b6bc0d296 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -385,6 +385,7 @@ a.button, font-kerning: auto; font-family: inherit; font-weight: normal; + text-decoration: none !important; } .button svg, .button span, @@ -392,6 +393,9 @@ a.button, .usa-button--dja span { vertical-align: middle; } +.usa-button--dja.usa-button--unstyled { + color: var(--link-fg); +} .usa-button--dja:not(.usa-button--unstyled, .usa-button--outline, .usa-modal__close, .usa-button--secondary) { background: var(--button-bg); } @@ -421,11 +425,34 @@ input[type=submit].button--dja-toolbar { input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-toolbar:hover { border-color: var(--body-quiet-color); } -// Targets the DJA buttom with a nested icon -button .usa-icon, -.button .usa-icon, -.button--clipboard .usa-icon { - vertical-align: middle; +.admin-icon-group { + position: relative; + display: inline; + align-items: center; + + input { + // Allow for padding around the copy button + padding-right: 35px !important; + } + + button { + width: max-content; + } + + @media (max-width: 1000px) { + button { + display: block; + } + } + + span { + padding-left: 0.05rem; + } + +} +.usa-button__small-text, +.usa-button__small-text span { + font-size: 13px; } .module--custom { @@ -673,71 +700,10 @@ address.dja-address-contact-list { } } -// Make the clipboard button "float" inside of the input box -.admin-icon-group { - position: relative; - display: inline; - align-items: center; - - input { - // Allow for padding around the copy button - padding-right: 35px !important; - // Match the height of other inputs - min-height: 2.25rem !important; - } - - button { - line-height: 14px; - width: max-content; - font-size: unset; - text-decoration: none !important; - } - - @media (max-width: 1000px) { - button { - display: block; - padding-top: 8px; - } - } - - span { - padding-left: 0.1rem; - } - -} - -.admin-icon-group.admin-icon-group__clipboard-link { - position: relative; - display: inline; - align-items: center; - - - .usa-button--icon { - position: absolute; - right: auto; - left: 4px; - height: 100%; - top: -1px; - } - button { - font-size: unset !important; - display: inline-flex; - padding-top: 4px; - line-height: 14px; - width: max-content; - font-size: unset; - text-decoration: none !important; - } -} - .no-outline-on-click:focus { outline: none !important; } -.usa-button__small-text { - font-size: small; -} - // Get rid of padding on all help texts form .aligned p.help, form .aligned div.help { padding-left: 0px !important; @@ -887,6 +853,9 @@ div.dja__model-description{ padding-top: 0 !important; } +.padding-bottom-0 { + padding-bottom: 0 !important; +} .flex-container { @media screen and (min-width: 700px) and (max-width: 1150px) { diff --git a/src/registrar/templates/admin/change_form_object_tools.html b/src/registrar/templates/admin/change_form_object_tools.html index 198140c19..66011a3c4 100644 --- a/src/registrar/templates/admin/change_form_object_tools.html +++ b/src/registrar/templates/admin/change_form_object_tools.html @@ -20,10 +20,11 @@ {% if opts.model_name == 'domainrequest' %}
  • - + + {% translate "Copy request summary" %}
  • diff --git a/src/registrar/templates/admin/input_with_clipboard.html b/src/registrar/templates/admin/input_with_clipboard.html index 5ad2b27f7..d6a016fd5 100644 --- a/src/registrar/templates/admin/input_with_clipboard.html +++ b/src/registrar/templates/admin/input_with_clipboard.html @@ -8,7 +8,7 @@
    {{ field }}
    {% else %} -
    Other contact informationOther contact information
    {{ contact.phone }} - {% if contact.email %} - - - {% endif %} + + + Copy email +
    - + @@ -267,18 +267,31 @@

    {% endfor %} diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index cdc3c97de..9c5e3b582 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -654,7 +654,7 @@ def test_contact_fields_have_detail_table(self): self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields) # Test for the copy link - self.assertContains(response, "button--clipboard", count=3) + self.assertContains(response, "copy-to-clipboard", count=3) # cleanup this test domain_info.delete() diff --git a/src/registrar/tests/test_admin_domain.py b/src/registrar/tests/test_admin_domain.py index a9b94781f..f02b59a91 100644 --- a/src/registrar/tests/test_admin_domain.py +++ b/src/registrar/tests/test_admin_domain.py @@ -535,7 +535,7 @@ def test_contact_fields_on_domain_change_form_have_detail_table(self): self.assertContains(response, "Testy Tester") # Test for the copy link - self.assertContains(response, "button--clipboard") + self.assertContains(response, "copy-to-clipboard") # cleanup from this test domain.delete() diff --git a/src/registrar/tests/test_admin_request.py b/src/registrar/tests/test_admin_request.py index a9b073472..f19008ca1 100644 --- a/src/registrar/tests/test_admin_request.py +++ b/src/registrar/tests/test_admin_request.py @@ -1511,7 +1511,7 @@ def test_contact_fields_have_detail_table(self): self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields) # Test for the copy link - self.assertContains(response, "button--clipboard", count=4) + self.assertContains(response, "copy-to-clipboard", count=4) # Test that Creator counts display properly self.assertNotContains(response, "Approved domains") @@ -1846,6 +1846,58 @@ def test_side_effects_when_saving_approved_to_rejected(self): def test_side_effects_when_saving_approved_to_ineligible(self): self.trigger_saving_approved_to_another_state(False, DomainRequest.DomainRequestStatus.INELIGIBLE) + @less_console_noise + def test_error_when_saving_to_approved_and_domain_exists(self): + """Redundant admin check on model transition not allowed.""" + Domain.objects.create(name="wabbitseason.gov") + + new_request = completed_domain_request( + status=DomainRequest.DomainRequestStatus.SUBMITTED, name="wabbitseason.gov" + ) + + # Create a request object with a superuser + request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(new_request.pk)) + request.user = self.superuser + + request.session = {} + + # Use ExitStack to combine patch contexts + with ExitStack() as stack: + # Patch django.contrib.messages.error + stack.enter_context(patch.object(messages, "error")) + + new_request.status = DomainRequest.DomainRequestStatus.APPROVED + + self.admin.save_model(request, new_request, None, True) + + messages.error.assert_called_once_with( + request, + "Cannot approve. Requested domain is already in use.", + ) + + @less_console_noise + def test_no_error_when_saving_to_approved_and_domain_exists(self): + """The negative of the redundant admin check on model transition not allowed.""" + new_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED) + + # Create a request object with a superuser + request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(new_request.pk)) + request.user = self.superuser + + request.session = {} + + # 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(messages, "error")) + + new_request.status = DomainRequest.DomainRequestStatus.APPROVED + + self.admin.save_model(request, new_request, None, True) + + # Assert that the error message was never called + messages.error.assert_not_called() + def test_has_correct_filters(self): """ This test verifies that DomainRequestAdmin has the correct filters set up. diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 681d04c92..f1d25ece9 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -1,7 +1,5 @@ from django.forms import ValidationError from django.test import TestCase -from django.db.utils import IntegrityError -from django.db import transaction from unittest.mock import patch from django.test import RequestFactory @@ -20,23 +18,18 @@ UserPortfolioPermission, AllowedEmail, ) - import boto3_mocking from registrar.models.portfolio import Portfolio from registrar.models.portfolio_invitation import PortfolioInvitation from registrar.models.transition_domain import TransitionDomain from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices 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, create_test_user, ) -from django_fsm import TransitionNotAllowed from waffle.testutils import override_flag from api.tests.common import less_console_noise_decorator diff --git a/src/registrar/tests/test_models_requests.py b/src/registrar/tests/test_models_requests.py new file mode 100644 index 000000000..9e86f5f9c --- /dev/null +++ b/src/registrar/tests/test_models_requests.py @@ -0,0 +1,1029 @@ +from django.test import TestCase +from django.db.utils import IntegrityError +from django.db import transaction +from unittest.mock import patch + + +from registrar.models import ( + Contact, + DomainRequest, + DomainInformation, + User, + Website, + Domain, + DraftDomain, + FederalAgency, + AllowedEmail, +) + +import boto3_mocking +from registrar.utility.constants import BranchChoices +from registrar.utility.errors import FSMDomainRequestError + +from .common import ( + MockSESClient, + less_console_noise, + completed_domain_request, + set_domain_request_investigators, +) +from django_fsm import TransitionNotAllowed + +from api.tests.common import less_console_noise_decorator + + +@boto3_mocking.patching +class TestDomainRequest(TestCase): + @less_console_noise_decorator + def setUp(self): + + self.dummy_user, _ = Contact.objects.get_or_create( + email="mayor@igorville.com", first_name="Hello", last_name="World" + ) + self.dummy_user_2, _ = User.objects.get_or_create( + username="intern@igorville.com", email="intern@igorville.com", first_name="Lava", last_name="World" + ) + self.started_domain_request = completed_domain_request( + status=DomainRequest.DomainRequestStatus.STARTED, + name="started.gov", + ) + self.submitted_domain_request = completed_domain_request( + status=DomainRequest.DomainRequestStatus.SUBMITTED, + name="submitted.gov", + ) + self.in_review_domain_request = completed_domain_request( + status=DomainRequest.DomainRequestStatus.IN_REVIEW, + name="in-review.gov", + ) + self.action_needed_domain_request = completed_domain_request( + status=DomainRequest.DomainRequestStatus.ACTION_NEEDED, + name="action-needed.gov", + ) + self.approved_domain_request = completed_domain_request( + status=DomainRequest.DomainRequestStatus.APPROVED, + name="approved.gov", + ) + self.withdrawn_domain_request = completed_domain_request( + status=DomainRequest.DomainRequestStatus.WITHDRAWN, + name="withdrawn.gov", + ) + self.rejected_domain_request = completed_domain_request( + status=DomainRequest.DomainRequestStatus.REJECTED, + name="rejected.gov", + ) + self.ineligible_domain_request = completed_domain_request( + status=DomainRequest.DomainRequestStatus.INELIGIBLE, + name="ineligible.gov", + ) + + # Store all domain request statuses in a variable for ease of use + self.all_domain_requests = [ + self.started_domain_request, + self.submitted_domain_request, + self.in_review_domain_request, + self.action_needed_domain_request, + self.approved_domain_request, + self.withdrawn_domain_request, + self.rejected_domain_request, + self.ineligible_domain_request, + ] + + self.mock_client = MockSESClient() + + 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): + """Helper method for testing allowed transitions.""" + with less_console_noise(): + return self.assertRaises(Exception, None, exception_type) + + @less_console_noise_decorator + def test_request_is_withdrawable(self): + """Tests the is_withdrawable function""" + domain_request_1 = completed_domain_request( + status=DomainRequest.DomainRequestStatus.SUBMITTED, + name="city2.gov", + ) + domain_request_2 = completed_domain_request( + status=DomainRequest.DomainRequestStatus.IN_REVIEW, + name="city3.gov", + ) + domain_request_3 = completed_domain_request( + status=DomainRequest.DomainRequestStatus.ACTION_NEEDED, + name="city4.gov", + ) + domain_request_4 = completed_domain_request( + status=DomainRequest.DomainRequestStatus.REJECTED, + name="city5.gov", + ) + self.assertTrue(domain_request_1.is_withdrawable()) + self.assertTrue(domain_request_2.is_withdrawable()) + self.assertTrue(domain_request_3.is_withdrawable()) + self.assertFalse(domain_request_4.is_withdrawable()) + + @less_console_noise_decorator + def test_request_is_awaiting_review(self): + """Tests the is_awaiting_review function""" + domain_request_1 = completed_domain_request( + status=DomainRequest.DomainRequestStatus.SUBMITTED, + name="city2.gov", + ) + domain_request_2 = completed_domain_request( + status=DomainRequest.DomainRequestStatus.IN_REVIEW, + name="city3.gov", + ) + domain_request_3 = completed_domain_request( + status=DomainRequest.DomainRequestStatus.ACTION_NEEDED, + name="city4.gov", + ) + domain_request_4 = completed_domain_request( + status=DomainRequest.DomainRequestStatus.REJECTED, + name="city5.gov", + ) + self.assertTrue(domain_request_1.is_awaiting_review()) + self.assertTrue(domain_request_2.is_awaiting_review()) + self.assertFalse(domain_request_3.is_awaiting_review()) + self.assertFalse(domain_request_4.is_awaiting_review()) + + @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""" + domain_request = completed_domain_request( + status=DomainRequest.DomainRequestStatus.IN_REVIEW, + name="city2.gov", + federal_agency=None, + ) + + # Ensure that the federal agency is None + self.assertEqual(domain_request.federal_agency, None) + + # Approve the request + domain_request.approve() + self.assertEqual(domain_request.status, DomainRequest.DomainRequestStatus.APPROVED) + + # After approval, it should be "Non-Federal agency" + expected_federal_agency = FederalAgency.objects.filter(agency="Non-Federal Agency").first() + self.assertEqual(domain_request.federal_agency, expected_federal_agency) + + def test_empty_create_fails(self): + """Can't create a completely empty domain request.""" + with less_console_noise(): + 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.""" + 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.""" + 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, + 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.""" + 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, + 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): + 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() + + @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 email sent to creator 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" + ): + """Check if an email was sent after performing an action.""" + email_allowed, _ = AllowedEmail.objects.get_or_create(email=expected_email) + with self.subTest(msg=msg, action=action): + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + # Perform the specified action + action_method = getattr(domain_request, action) + action_method() + + # Check if an email was sent + sent_emails = [ + email + for email in MockSESClient.EMAILS_SENT + if expected_email in email["kwargs"]["Destination"]["ToAddresses"] + ] + self.assertEqual(len(sent_emails), expected_count) + + if expected_content: + email_content = sent_emails[0]["kwargs"]["Content"]["Simple"]["Body"]["Text"]["Data"] + self.assertIn(expected_content, email_content) + + email_allowed.delete() + + @less_console_noise_decorator + def test_submit_from_started_sends_email_to_creator(self): + """tests that 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." + domain_request = completed_domain_request(user=self.dummy_user_2) + self.check_email_sent( + 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." + user, _ = User.objects.get_or_create(username="testy") + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.WITHDRAWN, user=user) + self.check_email_sent(domain_request, msg, "submit", 1, expected_content="Hi", expected_email=user.email) + + @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." + user, _ = User.objects.get_or_create(username="testy") + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=user) + self.check_email_sent(domain_request, msg, "approve", 1, expected_content="approved", expected_email=user.email) + + @less_console_noise_decorator + def test_withdraw_sends_email(self): + msg = "Create a domain request and withdraw it and see if email was sent." + user, _ = User.objects.get_or_create(username="testy") + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=user) + self.check_email_sent( + domain_request, msg, "withdraw", 1, expected_content="withdrawn", expected_email=user.email + ) + + @less_console_noise_decorator + def test_reject_sends_email(self): + msg = "Create a domain request and reject it and see if email was sent." + user, _ = User.objects.get_or_create(username="testy") + domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.APPROVED, user=user) + self.check_email_sent(domain_request, msg, "reject", 1, expected_content="Hi", expected_email=user.email) + + @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(): + for domain_request, exception_type in test_cases: + with self.subTest(domain_request=domain_request, exception_type=exception_type): + with self.assertRaises(exception_type): + # Retrieve the method by name from the domain_request object and call it + method = getattr(domain_request, method_to_run) + # 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(): + for domain_request, exception_type in test_cases: + with self.subTest(domain_request=domain_request, exception_type=exception_type): + try: + # Retrieve the method by name from the DomainRequest object and call it + method = getattr(domain_request, method_to_run) + # Call the method + method() + 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. + For submit, this should be valid in all cases. + """ + + test_cases = [ + (self.started_domain_request, TransitionNotAllowed), + (self.in_review_domain_request, TransitionNotAllowed), + (self.action_needed_domain_request, TransitionNotAllowed), + (self.withdrawn_domain_request, TransitionNotAllowed), + ] + + # Set all investigators to none + set_domain_request_investigators(self.all_domain_requests, None) + + 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. + For submit, this should be valid in all cases. + """ + + test_cases = [ + (self.in_review_domain_request, TransitionNotAllowed), + (self.action_needed_domain_request, TransitionNotAllowed), + ] + + # Set all investigators to a user with no staff privs + user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) + set_domain_request_investigators(self.all_domain_requests, user) + + 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. + """ + test_cases = [ + (self.started_domain_request, TransitionNotAllowed), + (self.in_review_domain_request, TransitionNotAllowed), + (self.action_needed_domain_request, TransitionNotAllowed), + (self.withdrawn_domain_request, TransitionNotAllowed), + ] + + 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): + 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() + + # 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. + """ + test_cases = [ + (self.submitted_domain_request, TransitionNotAllowed), + (self.approved_domain_request, TransitionNotAllowed), + (self.rejected_domain_request, TransitionNotAllowed), + (self.ineligible_domain_request, TransitionNotAllowed), + ] + + 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. + """ + test_cases = [ + (self.submitted_domain_request, TransitionNotAllowed), + (self.action_needed_domain_request, TransitionNotAllowed), + (self.approved_domain_request, TransitionNotAllowed), + (self.rejected_domain_request, TransitionNotAllowed), + (self.ineligible_domain_request, TransitionNotAllowed), + ] + + 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 + """ + + test_cases = [ + (self.action_needed_domain_request, TransitionNotAllowed), + (self.approved_domain_request, TransitionNotAllowed), + (self.rejected_domain_request, TransitionNotAllowed), + (self.ineligible_domain_request, TransitionNotAllowed), + ] + + # Set all investigators to none + set_domain_request_investigators(self.all_domain_requests, None) + + 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. + This should throw an exception. + """ + + test_cases = [ + (self.action_needed_domain_request, TransitionNotAllowed), + (self.approved_domain_request, TransitionNotAllowed), + (self.rejected_domain_request, TransitionNotAllowed), + (self.ineligible_domain_request, TransitionNotAllowed), + ] + + # Set all investigators to a user with no staff privs + user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) + set_domain_request_investigators(self.all_domain_requests, user) + + 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. + """ + test_cases = [ + (self.started_domain_request, TransitionNotAllowed), + (self.in_review_domain_request, TransitionNotAllowed), + (self.withdrawn_domain_request, TransitionNotAllowed), + ] + + 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. + """ + test_cases = [ + (self.in_review_domain_request, TransitionNotAllowed), + (self.approved_domain_request, TransitionNotAllowed), + (self.rejected_domain_request, TransitionNotAllowed), + (self.ineligible_domain_request, TransitionNotAllowed), + ] + + 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 + """ + + test_cases = [ + (self.in_review_domain_request, TransitionNotAllowed), + (self.approved_domain_request, TransitionNotAllowed), + (self.rejected_domain_request, TransitionNotAllowed), + (self.ineligible_domain_request, TransitionNotAllowed), + ] + + # Set all investigators to none + set_domain_request_investigators(self.all_domain_requests, None) + + 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 + """ + + test_cases = [ + (self.in_review_domain_request, TransitionNotAllowed), + (self.approved_domain_request, TransitionNotAllowed), + (self.rejected_domain_request, TransitionNotAllowed), + (self.ineligible_domain_request, TransitionNotAllowed), + ] + + # Set all investigators to a user with no staff privs + user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) + set_domain_request_investigators(self.all_domain_requests, user) + + 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. + """ + test_cases = [ + (self.started_domain_request, TransitionNotAllowed), + (self.submitted_domain_request, TransitionNotAllowed), + (self.action_needed_domain_request, TransitionNotAllowed), + (self.withdrawn_domain_request, TransitionNotAllowed), + ] + + 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. + """ + test_cases = [ + (self.submitted_domain_request, TransitionNotAllowed), + (self.in_review_domain_request, TransitionNotAllowed), + (self.action_needed_domain_request, TransitionNotAllowed), + (self.rejected_domain_request, TransitionNotAllowed), + ] + + 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 + """ + + test_cases = [ + (self.in_review_domain_request, TransitionNotAllowed), + (self.action_needed_domain_request, TransitionNotAllowed), + (self.rejected_domain_request, TransitionNotAllowed), + ] + + # Set all investigators to none + set_domain_request_investigators(self.all_domain_requests, None) + + 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 + """ + + test_cases = [ + (self.in_review_domain_request, TransitionNotAllowed), + (self.action_needed_domain_request, TransitionNotAllowed), + (self.rejected_domain_request, TransitionNotAllowed), + ] + + # Set all investigators to a user with no staff privs + user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) + set_domain_request_investigators(self.all_domain_requests, user) + + 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 + an email + """ + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + 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 approve against transition rules raises TransitionNotAllowed. + """ + test_cases = [ + (self.started_domain_request, TransitionNotAllowed), + (self.approved_domain_request, TransitionNotAllowed), + (self.withdrawn_domain_request, TransitionNotAllowed), + (self.ineligible_domain_request, TransitionNotAllowed), + ] + self.assert_fsm_transition_raises_error(test_cases, "approve") + + @less_console_noise_decorator + def test_approved_transition_not_allowed_when_domain_already_approved(self): + """ + Test that calling approve whith an already approved requested domain raises + TransitionNotAllowed. + """ + Domain.objects.all().create(name=self.submitted_domain_request.requested_domain.name) + test_cases = [ + (self.submitted_domain_request, FSMDomainRequestError), + ] + 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. + """ + test_cases = [ + (self.submitted_domain_request, TransitionNotAllowed), + (self.in_review_domain_request, TransitionNotAllowed), + (self.action_needed_domain_request, TransitionNotAllowed), + ] + + 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. + For withdraw, this should be valid in all cases. + """ + + test_cases = [ + (self.submitted_domain_request, TransitionNotAllowed), + (self.in_review_domain_request, TransitionNotAllowed), + (self.action_needed_domain_request, TransitionNotAllowed), + ] + + # Set all investigators to none + set_domain_request_investigators(self.all_domain_requests, None) + + 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. + For withdraw, this should be valid in all cases. + """ + + test_cases = [ + (self.submitted_domain_request, TransitionNotAllowed), + (self.in_review_domain_request, TransitionNotAllowed), + (self.action_needed_domain_request, TransitionNotAllowed), + ] + + # Set all investigators to a user with no staff privs + user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) + set_domain_request_investigators(self.all_domain_requests, user) + + 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. + """ + test_cases = [ + (self.started_domain_request, TransitionNotAllowed), + (self.approved_domain_request, TransitionNotAllowed), + (self.withdrawn_domain_request, TransitionNotAllowed), + (self.rejected_domain_request, TransitionNotAllowed), + (self.ineligible_domain_request, TransitionNotAllowed), + ] + + 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. + """ + test_cases = [ + (self.in_review_domain_request, TransitionNotAllowed), + (self.action_needed_domain_request, TransitionNotAllowed), + (self.approved_domain_request, TransitionNotAllowed), + ] + + 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 + """ + + test_cases = [ + (self.in_review_domain_request, TransitionNotAllowed), + (self.action_needed_domain_request, TransitionNotAllowed), + (self.approved_domain_request, TransitionNotAllowed), + ] + + # Set all investigators to none + set_domain_request_investigators(self.all_domain_requests, None) + + 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 + """ + + test_cases = [ + (self.in_review_domain_request, TransitionNotAllowed), + (self.action_needed_domain_request, TransitionNotAllowed), + (self.approved_domain_request, TransitionNotAllowed), + ] + + # Set all investigators to a user with no staff privs + user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) + set_domain_request_investigators(self.all_domain_requests, user) + + 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. + """ + test_cases = [ + (self.started_domain_request, TransitionNotAllowed), + (self.submitted_domain_request, TransitionNotAllowed), + (self.withdrawn_domain_request, TransitionNotAllowed), + (self.rejected_domain_request, TransitionNotAllowed), + (self.ineligible_domain_request, TransitionNotAllowed), + ] + + 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. + """ + test_cases = [ + (self.in_review_domain_request, TransitionNotAllowed), + (self.action_needed_domain_request, TransitionNotAllowed), + (self.approved_domain_request, TransitionNotAllowed), + (self.rejected_domain_request, TransitionNotAllowed), + ] + + 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 + """ + + test_cases = [ + (self.in_review_domain_request, TransitionNotAllowed), + (self.action_needed_domain_request, TransitionNotAllowed), + (self.approved_domain_request, TransitionNotAllowed), + (self.rejected_domain_request, TransitionNotAllowed), + ] + + # Set all investigators to none + set_domain_request_investigators(self.all_domain_requests, None) + + 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 + """ + + test_cases = [ + (self.in_review_domain_request, TransitionNotAllowed), + (self.action_needed_domain_request, TransitionNotAllowed), + (self.approved_domain_request, TransitionNotAllowed), + (self.rejected_domain_request, TransitionNotAllowed), + ] + + # Set all investigators to a user with no staff privs + user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) + set_domain_request_investigators(self.all_domain_requests, user) + + 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. + """ + test_cases = [ + (self.started_domain_request, TransitionNotAllowed), + (self.submitted_domain_request, TransitionNotAllowed), + (self.withdrawn_domain_request, TransitionNotAllowed), + (self.ineligible_domain_request, TransitionNotAllowed), + ] + + 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""" + + domain = Domain.objects.create(name=self.approved_domain_request.requested_domain.name) + self.approved_domain_request.approved_domain = domain + self.approved_domain_request.save() + + # Define a custom implementation for is_active + def custom_is_active(self): + return True # Override to return True + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + # 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""" + + domain = Domain.objects.create(name=self.approved_domain_request.requested_domain.name) + self.approved_domain_request.approved_domain = domain + self.approved_domain_request.save() + + # Define a custom implementation for is_active + def custom_is_active(self): + return True # Override to return True + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + # 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""" + + domain = Domain.objects.create(name=self.approved_domain_request.requested_domain.name) + self.approved_domain_request.approved_domain = domain + self.approved_domain_request.save() + + # Define a custom implementation for is_active + def custom_is_active(self): + return True # Override to return True + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + # 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""" + + domain = Domain.objects.create(name=self.approved_domain_request.requested_domain.name) + self.approved_domain_request.approved_domain = domain + self.approved_domain_request.save() + + # Define a custom implementation for is_active + def custom_is_active(self): + return True # Override to return True + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + # 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.""" + + # 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() + + 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.""" + + # 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() + + 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.""" + + # 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() + + 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""" + 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""" + 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""" + # 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""" + 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) diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index 8530859e2..ff2e61939 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -82,7 +82,6 @@ def test_get_first_status_started_date(self, mock_get_first_status_set_date): response = self.app.get(f"/domain-request/{domain_request.id}") # Ensure that the date is still set to None self.assertIsNone(domain_request.last_status_update) - print(response) # We should still grab a date for this field in this event - but it should come from the audit log instead self.assertContains(response, "Started on:") self.assertContains(response, fixed_date.strftime("%B %-d, %Y")) From f0b0e9d246f0331b5bd84de9f46e1a1053177963 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 9 Oct 2024 14:12:38 -0600 Subject: [PATCH 8/9] Manually correct faulty unit test merge --- src/registrar/tests/test_models.py | 1017 ---------------------------- 1 file changed, 1017 deletions(-) diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index f1d25ece9..4101cecd3 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -34,1023 +34,6 @@ from api.tests.common import less_console_noise_decorator -@boto3_mocking.patching -class TestDomainRequest(TestCase): - @less_console_noise_decorator - def setUp(self): - - self.dummy_user, _ = Contact.objects.get_or_create( - email="mayor@igorville.com", first_name="Hello", last_name="World" - ) - self.dummy_user_2, _ = User.objects.get_or_create( - username="intern@igorville.com", email="intern@igorville.com", first_name="Lava", last_name="World" - ) - self.started_domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.STARTED, - name="started.gov", - ) - self.submitted_domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.SUBMITTED, - name="submitted.gov", - ) - self.in_review_domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.IN_REVIEW, - name="in-review.gov", - ) - self.action_needed_domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.ACTION_NEEDED, - name="action-needed.gov", - ) - self.approved_domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.APPROVED, - name="approved.gov", - ) - self.withdrawn_domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.WITHDRAWN, - name="withdrawn.gov", - ) - self.rejected_domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.REJECTED, - name="rejected.gov", - ) - self.ineligible_domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.INELIGIBLE, - name="ineligible.gov", - ) - - # Store all domain request statuses in a variable for ease of use - self.all_domain_requests = [ - self.started_domain_request, - self.submitted_domain_request, - self.in_review_domain_request, - self.action_needed_domain_request, - self.approved_domain_request, - self.withdrawn_domain_request, - self.rejected_domain_request, - self.ineligible_domain_request, - ] - - self.mock_client = MockSESClient() - - 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): - """Helper method for testing allowed transitions.""" - with less_console_noise(): - return self.assertRaises(Exception, None, exception_type) - - @less_console_noise_decorator - def test_request_is_withdrawable(self): - """Tests the is_withdrawable function""" - domain_request_1 = completed_domain_request( - status=DomainRequest.DomainRequestStatus.SUBMITTED, - name="city2.gov", - ) - domain_request_2 = completed_domain_request( - status=DomainRequest.DomainRequestStatus.IN_REVIEW, - name="city3.gov", - ) - domain_request_3 = completed_domain_request( - status=DomainRequest.DomainRequestStatus.ACTION_NEEDED, - name="city4.gov", - ) - domain_request_4 = completed_domain_request( - status=DomainRequest.DomainRequestStatus.REJECTED, - name="city5.gov", - ) - self.assertTrue(domain_request_1.is_withdrawable()) - self.assertTrue(domain_request_2.is_withdrawable()) - self.assertTrue(domain_request_3.is_withdrawable()) - self.assertFalse(domain_request_4.is_withdrawable()) - - @less_console_noise_decorator - def test_request_is_awaiting_review(self): - """Tests the is_awaiting_review function""" - domain_request_1 = completed_domain_request( - status=DomainRequest.DomainRequestStatus.SUBMITTED, - name="city2.gov", - ) - domain_request_2 = completed_domain_request( - status=DomainRequest.DomainRequestStatus.IN_REVIEW, - name="city3.gov", - ) - domain_request_3 = completed_domain_request( - status=DomainRequest.DomainRequestStatus.ACTION_NEEDED, - name="city4.gov", - ) - domain_request_4 = completed_domain_request( - status=DomainRequest.DomainRequestStatus.REJECTED, - name="city5.gov", - ) - self.assertTrue(domain_request_1.is_awaiting_review()) - self.assertTrue(domain_request_2.is_awaiting_review()) - self.assertFalse(domain_request_3.is_awaiting_review()) - self.assertFalse(domain_request_4.is_awaiting_review()) - - @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""" - domain_request = completed_domain_request( - status=DomainRequest.DomainRequestStatus.IN_REVIEW, - name="city2.gov", - federal_agency=None, - ) - - # Ensure that the federal agency is None - self.assertEqual(domain_request.federal_agency, None) - - # Approve the request - domain_request.approve() - self.assertEqual(domain_request.status, DomainRequest.DomainRequestStatus.APPROVED) - - # After approval, it should be "Non-Federal agency" - expected_federal_agency = FederalAgency.objects.filter(agency="Non-Federal Agency").first() - self.assertEqual(domain_request.federal_agency, expected_federal_agency) - - def test_empty_create_fails(self): - """Can't create a completely empty domain request.""" - with less_console_noise(): - 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.""" - 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.""" - 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, - 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.""" - 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, - 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): - 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() - - @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 email sent to creator 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" - ): - """Check if an email was sent after performing an action.""" - email_allowed, _ = AllowedEmail.objects.get_or_create(email=expected_email) - with self.subTest(msg=msg, action=action): - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - # Perform the specified action - action_method = getattr(domain_request, action) - action_method() - - # Check if an email was sent - sent_emails = [ - email - for email in MockSESClient.EMAILS_SENT - if expected_email in email["kwargs"]["Destination"]["ToAddresses"] - ] - self.assertEqual(len(sent_emails), expected_count) - - if expected_content: - email_content = sent_emails[0]["kwargs"]["Content"]["Simple"]["Body"]["Text"]["Data"] - self.assertIn(expected_content, email_content) - - email_allowed.delete() - - @less_console_noise_decorator - def test_submit_from_started_sends_email_to_creator(self): - """tests that 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." - domain_request = completed_domain_request(user=self.dummy_user_2) - self.check_email_sent( - 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." - user, _ = User.objects.get_or_create(username="testy") - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.WITHDRAWN, user=user) - self.check_email_sent(domain_request, msg, "submit", 1, expected_content="Hi", expected_email=user.email) - - @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." - user, _ = User.objects.get_or_create(username="testy") - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=user) - self.check_email_sent(domain_request, msg, "approve", 1, expected_content="approved", expected_email=user.email) - - @less_console_noise_decorator - def test_withdraw_sends_email(self): - msg = "Create a domain request and withdraw it and see if email was sent." - user, _ = User.objects.get_or_create(username="testy") - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=user) - self.check_email_sent( - domain_request, msg, "withdraw", 1, expected_content="withdrawn", expected_email=user.email - ) - - @less_console_noise_decorator - def test_reject_sends_email(self): - msg = "Create a domain request and reject it and see if email was sent." - user, _ = User.objects.get_or_create(username="testy") - domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.APPROVED, user=user) - self.check_email_sent(domain_request, msg, "reject", 1, expected_content="Hi", expected_email=user.email) - - @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(): - for domain_request, exception_type in test_cases: - with self.subTest(domain_request=domain_request, exception_type=exception_type): - with self.assertRaises(exception_type): - # Retrieve the method by name from the domain_request object and call it - method = getattr(domain_request, method_to_run) - # 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(): - for domain_request, exception_type in test_cases: - with self.subTest(domain_request=domain_request, exception_type=exception_type): - try: - # Retrieve the method by name from the DomainRequest object and call it - method = getattr(domain_request, method_to_run) - # Call the method - method() - 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. - For submit, this should be valid in all cases. - """ - - test_cases = [ - (self.started_domain_request, TransitionNotAllowed), - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.withdrawn_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to none - set_domain_request_investigators(self.all_domain_requests, None) - - 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. - For submit, this should be valid in all cases. - """ - - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to a user with no staff privs - user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) - set_domain_request_investigators(self.all_domain_requests, user) - - 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. - """ - test_cases = [ - (self.started_domain_request, TransitionNotAllowed), - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.withdrawn_domain_request, TransitionNotAllowed), - ] - - 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): - 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() - - # 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. - """ - test_cases = [ - (self.submitted_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - 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. - """ - test_cases = [ - (self.submitted_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - 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 - """ - - test_cases = [ - (self.action_needed_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to none - set_domain_request_investigators(self.all_domain_requests, None) - - 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. - This should throw an exception. - """ - - test_cases = [ - (self.action_needed_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to a user with no staff privs - user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) - set_domain_request_investigators(self.all_domain_requests, user) - - 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. - """ - test_cases = [ - (self.started_domain_request, TransitionNotAllowed), - (self.in_review_domain_request, TransitionNotAllowed), - (self.withdrawn_domain_request, TransitionNotAllowed), - ] - - 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. - """ - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - 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 - """ - - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to none - set_domain_request_investigators(self.all_domain_requests, None) - - 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 - """ - - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to a user with no staff privs - user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) - set_domain_request_investigators(self.all_domain_requests, user) - - 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. - """ - test_cases = [ - (self.started_domain_request, TransitionNotAllowed), - (self.submitted_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.withdrawn_domain_request, TransitionNotAllowed), - ] - - 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. - """ - test_cases = [ - (self.submitted_domain_request, TransitionNotAllowed), - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - ] - - 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 - """ - - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to none - set_domain_request_investigators(self.all_domain_requests, None) - - 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 - """ - - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to a user with no staff privs - user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) - set_domain_request_investigators(self.all_domain_requests, user) - - 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 - an email - """ - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - 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. - """ - test_cases = [ - (self.started_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.withdrawn_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - 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. - """ - test_cases = [ - (self.submitted_domain_request, TransitionNotAllowed), - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - ] - - 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. - For withdraw, this should be valid in all cases. - """ - - test_cases = [ - (self.submitted_domain_request, TransitionNotAllowed), - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to none - set_domain_request_investigators(self.all_domain_requests, None) - - 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. - For withdraw, this should be valid in all cases. - """ - - test_cases = [ - (self.submitted_domain_request, TransitionNotAllowed), - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to a user with no staff privs - user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) - set_domain_request_investigators(self.all_domain_requests, user) - - 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. - """ - test_cases = [ - (self.started_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.withdrawn_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - 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. - """ - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - ] - - 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 - """ - - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to none - set_domain_request_investigators(self.all_domain_requests, None) - - 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 - """ - - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to a user with no staff privs - user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) - set_domain_request_investigators(self.all_domain_requests, user) - - 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. - """ - test_cases = [ - (self.started_domain_request, TransitionNotAllowed), - (self.submitted_domain_request, TransitionNotAllowed), - (self.withdrawn_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - 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. - """ - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - ] - - 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 - """ - - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to none - set_domain_request_investigators(self.all_domain_requests, None) - - 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 - """ - - test_cases = [ - (self.in_review_domain_request, TransitionNotAllowed), - (self.action_needed_domain_request, TransitionNotAllowed), - (self.approved_domain_request, TransitionNotAllowed), - (self.rejected_domain_request, TransitionNotAllowed), - ] - - # Set all investigators to a user with no staff privs - user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False) - set_domain_request_investigators(self.all_domain_requests, user) - - 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. - """ - test_cases = [ - (self.started_domain_request, TransitionNotAllowed), - (self.submitted_domain_request, TransitionNotAllowed), - (self.withdrawn_domain_request, TransitionNotAllowed), - (self.ineligible_domain_request, TransitionNotAllowed), - ] - - 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""" - - domain = Domain.objects.create(name=self.approved_domain_request.requested_domain.name) - self.approved_domain_request.approved_domain = domain - self.approved_domain_request.save() - - # Define a custom implementation for is_active - def custom_is_active(self): - return True # Override to return True - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - # 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""" - - domain = Domain.objects.create(name=self.approved_domain_request.requested_domain.name) - self.approved_domain_request.approved_domain = domain - self.approved_domain_request.save() - - # Define a custom implementation for is_active - def custom_is_active(self): - return True # Override to return True - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - # 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""" - - domain = Domain.objects.create(name=self.approved_domain_request.requested_domain.name) - self.approved_domain_request.approved_domain = domain - self.approved_domain_request.save() - - # Define a custom implementation for is_active - def custom_is_active(self): - return True # Override to return True - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - # 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""" - - domain = Domain.objects.create(name=self.approved_domain_request.requested_domain.name) - self.approved_domain_request.approved_domain = domain - self.approved_domain_request.save() - - # Define a custom implementation for is_active - def custom_is_active(self): - return True # Override to return True - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - # 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.""" - - # 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() - - 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.""" - - # 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() - - 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.""" - - # 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() - - 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""" - 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""" - 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""" - # 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""" - 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): - """Test the User-Domain-Role connection.""" - - def setUp(self): - super().setUp() - self.mock_client = MockSESClient() - - def tearDown(self): - super().tearDown() - 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() - investigator, _ = User.objects.get_or_create(username="frenchtoast", is_staff=True) - domain_request = DomainRequest.objects.create( - creator=user, requested_domain=draft_domain, investigator=investigator - ) - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - # 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") - self.assertTrue(UserDomainRole.objects.get(user=user, domain=domain)) - - class TestDomainInformation(TestCase): """Test the DomainInformation model, when approved or otherwise""" From f1be3fafebc6dd7b1e7d97060a7aa9ae061855e8 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 9 Oct 2024 14:23:03 -0600 Subject: [PATCH 9/9] linted + comments --- src/registrar/models/user_portfolio_permission.py | 10 ++++++++-- src/registrar/tests/test_models.py | 9 ++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/registrar/models/user_portfolio_permission.py b/src/registrar/models/user_portfolio_permission.py index 825e82c88..ed233dfea 100644 --- a/src/registrar/models/user_portfolio_permission.py +++ b/src/registrar/models/user_portfolio_permission.py @@ -110,8 +110,14 @@ def clean(self): # Check if a user is set without accessing the related object. has_user = bool(self.user_id) if has_user: - existing_permission_pks = UserPortfolioPermission.objects.filter(user=self.user).values_list("pk", flat=True) - if not flag_is_active_for_user(self.user, "multiple_portfolios") and existing_permission_pks.exists() and not self.pk in existing_permission_pks: + existing_permission_pks = UserPortfolioPermission.objects.filter(user=self.user).values_list( + "pk", flat=True + ) + if ( + not flag_is_active_for_user(self.user, "multiple_portfolios") + and existing_permission_pks.exists() + and self.pk not in existing_permission_pks + ): raise ValidationError( "This user is already assigned to a portfolio. " "Based on current waffle flag settings, users cannot be assigned to multiple portfolios." diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 4101cecd3..d269dc912 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -34,6 +34,7 @@ from api.tests.common import less_console_noise_decorator + class TestDomainInformation(TestCase): """Test the DomainInformation model, when approved or otherwise""" @@ -282,7 +283,7 @@ def test_clean_on_multiple_portfolios_when_flag_active(self): portfolio_permission_2.clean() except ValidationError as error: self.fail(f"Raised ValidationError unexpectedly: {error}") - + @less_console_noise_decorator @override_flag("multiple_portfolios", active=False) def test_clean_on_creates_multiple_portfolios(self): @@ -310,7 +311,7 @@ def test_clean_on_creates_multiple_portfolios(self): "Based on current waffle flag settings, users cannot be assigned to multiple portfolios." ), ) - + @less_console_noise_decorator @override_flag("multiple_portfolios", active=False) def test_multiple_portfolio_reassignment(self): @@ -328,7 +329,9 @@ def test_multiple_portfolio_reassignment(self): # This should work as intended portfolio_permission.clean() portfolio_permission_2.clean() - + + # Reassign the portfolio of "user2" to "user" (this should throw an error + # preventing "user" from having multiple portfolios) with self.assertRaises(ValidationError) as cm: portfolio_permission_2.user = self.user portfolio_permission_2.clean()
    Other contact informationOther contact information
    {{ contact.phone }} - - + + + + + Copy email + + {% endif %}