Merge branch 'main' into dk/1751-oidc-outages

This commit is contained in:
David Kennedy 2024-02-13 08:57:37 -05:00
commit 1896bf5380
No known key found for this signature in database
GPG key ID: 6528A5386E66B96B
12 changed files with 430 additions and 302 deletions

View file

@ -884,14 +884,11 @@ class DomainApplicationAdmin(ListHeaderAdmin):
if ( if (
obj obj
and original_obj.status == models.DomainApplication.ApplicationStatus.APPROVED and original_obj.status == models.DomainApplication.ApplicationStatus.APPROVED
and ( and obj.status != models.DomainApplication.ApplicationStatus.APPROVED
obj.status == models.DomainApplication.ApplicationStatus.REJECTED
or obj.status == models.DomainApplication.ApplicationStatus.INELIGIBLE
)
and not obj.domain_is_not_active() and not obj.domain_is_not_active()
): ):
# If an admin tried to set an approved application to # If an admin tried to set an approved application to
# rejected or ineligible and the related domain is already # another status and the related domain is already
# active, shortcut the action and throw a friendly # active, shortcut the action and throw a friendly
# error message. This action would still not go through # error message. This action would still not go through
# shortcut or not as the rules are duplicated on the model, # shortcut or not as the rules are duplicated on the model,

View file

@ -18,4 +18,7 @@
left: 1rem !important; left: 1rem !important;
} }
} }
.usa-alert__body.margin-left-1 {
margin-left: 0.5rem!important;
}
} }

View file

@ -116,6 +116,10 @@ in the form $setting: value,
$theme-color-success-light: $dhs-green-30, $theme-color-success-light: $dhs-green-30,
$theme-color-success-lighter: $dhs-green-15, $theme-color-success-lighter: $dhs-green-15,
/*---------------------------
## Emergency state
----------------------------*/
$theme-color-emergency: #FFC3F9,
/*--------------------------- /*---------------------------
# Input settings # Input settings

View file

@ -578,6 +578,19 @@ class DomainApplication(TimeStampedModel):
return not self.approved_domain.is_active() return not self.approved_domain.is_active()
return True return True
def delete_and_clean_up_domain(self, called_from):
try:
domain_state = self.approved_domain.state
# Only reject if it exists on EPP
if domain_state != Domain.State.UNKNOWN:
self.approved_domain.deletedInEpp()
self.approved_domain.save()
self.approved_domain.delete()
self.approved_domain = None
except Exception as err:
logger.error(err)
logger.error(f"Can't query an approved domain while attempting {called_from}")
def _send_status_update_email(self, new_status, email_template, email_template_subject, send_email=True): def _send_status_update_email(self, new_status, email_template, email_template_subject, send_email=True):
"""Send a status update email to the submitter. """Send a status update email to the submitter.
@ -641,6 +654,10 @@ class DomainApplication(TimeStampedModel):
self.submission_date = timezone.now().date() self.submission_date = timezone.now().date()
self.save() self.save()
# Limit email notifications to transitions from Started and Withdrawn
limited_statuses = [self.ApplicationStatus.STARTED, self.ApplicationStatus.WITHDRAWN]
if self.status in limited_statuses:
self._send_status_update_email( self._send_status_update_email(
"submission confirmation", "submission confirmation",
"emails/submission_confirmation.txt", "emails/submission_confirmation.txt",
@ -657,11 +674,19 @@ class DomainApplication(TimeStampedModel):
ApplicationStatus.INELIGIBLE, ApplicationStatus.INELIGIBLE,
], ],
target=ApplicationStatus.IN_REVIEW, target=ApplicationStatus.IN_REVIEW,
conditions=[domain_is_not_active],
) )
def in_review(self): def in_review(self):
"""Investigate an application that has been submitted. """Investigate an application that has been submitted.
This action is logged.""" This action is logged.
As side effects this will delete the domain and domain_information
(will cascade) when they exist."""
if self.status == self.ApplicationStatus.APPROVED:
self.delete_and_clean_up_domain("in_review")
literal = DomainApplication.ApplicationStatus.IN_REVIEW literal = DomainApplication.ApplicationStatus.IN_REVIEW
# Check if the tuple exists, then grab its value # Check if the tuple exists, then grab its value
in_review = literal if literal is not None else "In Review" in_review = literal if literal is not None else "In Review"
@ -676,11 +701,19 @@ class DomainApplication(TimeStampedModel):
ApplicationStatus.INELIGIBLE, ApplicationStatus.INELIGIBLE,
], ],
target=ApplicationStatus.ACTION_NEEDED, target=ApplicationStatus.ACTION_NEEDED,
conditions=[domain_is_not_active],
) )
def action_needed(self): def action_needed(self):
"""Send back an application that is under investigation or rejected. """Send back an application that is under investigation or rejected.
This action is logged.""" This action is logged.
As side effects this will delete the domain and domain_information
(will cascade) when they exist."""
if self.status == self.ApplicationStatus.APPROVED:
self.delete_and_clean_up_domain("reject_with_prejudice")
literal = DomainApplication.ApplicationStatus.ACTION_NEEDED literal = DomainApplication.ApplicationStatus.ACTION_NEEDED
# Check if the tuple is setup correctly, then grab its value # Check if the tuple is setup correctly, then grab its value
action_needed = literal if literal is not None else "Action Needed" action_needed = literal if literal is not None else "Action Needed"
@ -735,6 +768,7 @@ class DomainApplication(TimeStampedModel):
) )
def withdraw(self): def withdraw(self):
"""Withdraw an application that has been submitted.""" """Withdraw an application that has been submitted."""
self._send_status_update_email( self._send_status_update_email(
"withdraw", "withdraw",
"emails/domain_request_withdrawn.txt", "emails/domain_request_withdrawn.txt",
@ -752,18 +786,9 @@ class DomainApplication(TimeStampedModel):
As side effects this will delete the domain and domain_information As side effects this will delete the domain and domain_information
(will cascade), and send an email notification.""" (will cascade), and send an email notification."""
if self.status == self.ApplicationStatus.APPROVED: if self.status == self.ApplicationStatus.APPROVED:
try: self.delete_and_clean_up_domain("reject")
domain_state = self.approved_domain.state
# Only reject if it exists on EPP
if domain_state != Domain.State.UNKNOWN:
self.approved_domain.deletedInEpp()
self.approved_domain.save()
self.approved_domain.delete()
self.approved_domain = None
except Exception as err:
logger.error(err)
logger.error("Can't query an approved domain while attempting a DA reject()")
self._send_status_update_email( self._send_status_update_email(
"action needed", "action needed",
@ -792,17 +817,7 @@ class DomainApplication(TimeStampedModel):
and domain_information (will cascade) when they exist.""" and domain_information (will cascade) when they exist."""
if self.status == self.ApplicationStatus.APPROVED: if self.status == self.ApplicationStatus.APPROVED:
try: self.delete_and_clean_up_domain("reject_with_prejudice")
domain_state = self.approved_domain.state
# Only reject if it exists on EPP
if domain_state != Domain.State.UNKNOWN:
self.approved_domain.deletedInEpp()
self.approved_domain.save()
self.approved_domain.delete()
self.approved_domain = None
except Exception as err:
logger.error(err)
logger.error("Can't query an approved domain while attempting a DA reject_with_prejudice()")
self.creator.restrict_user() self.creator.restrict_user()

View file

@ -24,21 +24,39 @@
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
{% block extrastyle %}{{ block.super }} {% block extrastyle %}{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "css/styles.css" %}" /> <link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}" />
{% endblock %} {% endblock %}
{% block branding %} {% block header %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">.gov admin</a></h1> {% if not IS_PRODUCTION %}
{% if user.is_anonymous %} {% with add_body_class="margin-left-1" %}
{% include "includes/non-production-alert.html" %}
{% endwith %}
{% endif %}
{# Djando update: this div will change to header #}
<div id="header">
<div id="branding">
{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">.gov admin</a></h1>
{% if user.is_anonymous %}
{% include "admin/color_theme_toggle.html" %} {% include "admin/color_theme_toggle.html" %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% comment %} </div>
{% block usertools %}
{% if has_permission %}
<div id="user-tools">
{% block welcome-msg %}
{% translate 'Welcome,' %}
<strong>{% firstof user.get_short_name user.get_username %}</strong>.
{% endblock %}
{% comment %}
This was copied from the 'userlinks' template, with a few minor changes. This was copied from the 'userlinks' template, with a few minor changes.
You can find that here: You can find that here:
https://github.com/django/django/blob/d25f3892114466d689fd6936f79f3bd9a9acc30e/django/contrib/admin/templates/admin/base.html#L59 https://github.com/django/django/blob/d25f3892114466d689fd6936f79f3bd9a9acc30e/django/contrib/admin/templates/admin/base.html#L59
{% endcomment %} {% endcomment %}
{% block userlinks %} {% block userlinks %}
{% if site_url %} {% if site_url %}
<a href="{{ site_url }}">{% translate 'View site' %}</a> / <a href="{{ site_url }}">{% translate 'View site' %}</a> /
{% endif %} {% endif %}
@ -54,4 +72,9 @@
<a href="{% url 'admin:logout' %}" id="admin-logout-button">{% translate 'Log out' %}</a> <a href="{% url 'admin:logout' %}" id="admin-logout-button">{% translate 'Log out' %}</a>
{% include "admin/color_theme_toggle.html" %} {% include "admin/color_theme_toggle.html" %}
{% endblock %} {% endblock %}
{% block nav-global %}{% endblock %} </div>
{% endif %}
{% endblock %}
{% block nav-global %}{% endblock %}
</div>
{% endblock %}

View file

@ -70,6 +70,10 @@
<script src="{% static 'js/uswds.min.js' %}" defer></script> <script src="{% static 'js/uswds.min.js' %}" defer></script>
<a class="usa-skipnav" href="#main-content">Skip to main content</a> <a class="usa-skipnav" href="#main-content">Skip to main content</a>
{% if not IS_PRODUCTION %}
{% include "includes/non-production-alert.html" %}
{% endif %}
<section class="usa-banner" aria-label="Official website of the United States government"> <section class="usa-banner" aria-label="Official website of the United States government">
<div class="usa-accordion"> <div class="usa-accordion">
<header class="usa-banner__header"> <header class="usa-banner__header">

View file

@ -18,7 +18,7 @@
<button <button
type="submit" type="submit"
class="usa-button" class="usa-button"
>Add user</button> >Add a domain manager</button>
</form> </form>
{% endblock %} {# domain_content #} {% endblock %} {# domain_content #}

View file

@ -0,0 +1,5 @@
<div class="usa-alert usa-alert--emergency margin-y-0 {% if add_class %}{{ add_class }}{% endif %}">
<div class="usa-alert__body {% if add_body_class %}{{ add_body_class }}{% endif %}">
<b>Attention:</b> You are on a test site.
</div>
</div>

View file

@ -306,6 +306,7 @@ class TestDomainApplicationAdminForm(TestCase):
) )
@boto3_mocking.patching
class TestDomainApplicationAdmin(MockEppLib): class TestDomainApplicationAdmin(MockEppLib):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
@ -411,83 +412,166 @@ class TestDomainApplicationAdmin(MockEppLib):
# Now let's make sure the long description does not exist # Now let's make sure the long description does not exist
self.assertNotContains(response, "Federal: an agency of the U.S. government") self.assertNotContains(response, "Federal: an agency of the U.S. government")
@boto3_mocking.patching def transition_state_and_send_email(self, application, status):
def test_save_model_sends_submitted_email(self): """Helper method for the email test cases."""
# make sure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
with boto3_mocking.clients.handler_for("sesv2", self.mock_client): with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise(): with less_console_noise():
# Create a mock request
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
# Modify the application's property
application.status = status
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
def assert_email_is_accurate(self, expected_string, email_index, email_address):
"""Helper method for the email test cases.
email_index is the index of the email in mock_client."""
# Access the arguments passed to send_email
call_args = self.mock_client.EMAILS_SENT
kwargs = call_args[email_index]["kwargs"]
# Retrieve the email details from the arguments
from_email = kwargs.get("FromEmailAddress")
to_email = kwargs["Destination"]["ToAddresses"][0]
email_content = kwargs["Content"]
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
# Assert or perform other checks on the email details
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
self.assertEqual(to_email, email_address)
self.assertIn(expected_string, email_body)
def test_save_model_sends_submitted_email(self):
"""When transitioning to submitted from started or withdrawn on a domain request,
an email is sent out.
When transitioning to submitted from dns needed or in review on a domain request,
no email is sent out."""
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create a sample application # Create a sample application
application = completed_application() application = completed_application()
# Create a mock request # Test Submitted Status from started
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL)
# Modify the application's property
application.status = DomainApplication.ApplicationStatus.SUBMITTED
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = self.mock_client.EMAILS_SENT
kwargs = call_args[0]["kwargs"]
# Retrieve the email details from the arguments
from_email = kwargs.get("FromEmailAddress")
to_email = kwargs["Destination"]["ToAddresses"][0]
email_content = kwargs["Content"]
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
# Assert or perform other checks on the email details
expected_string = "We received your .gov domain request."
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
self.assertEqual(to_email, EMAIL)
self.assertIn(expected_string, email_body)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
@boto3_mocking.patching # Test Withdrawn Status
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN)
self.assert_email_is_accurate(
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
# Test Submitted Status Again (from withdrawn)
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Move it to IN_REVIEW
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.IN_REVIEW)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Test Submitted Status Again from in IN_REVIEW, no new email should be sent
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Move it to IN_REVIEW
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.IN_REVIEW)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Move it to ACTION_NEEDED
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.ACTION_NEEDED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Test Submitted Status Again from in ACTION_NEEDED, no new email should be sent
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
def test_save_model_sends_approved_email(self): def test_save_model_sends_approved_email(self):
# make sure there is no user with this email """When transitioning to approved on a domain request,
an email is sent out every time."""
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov" EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete() User.objects.filter(email=EMAIL).delete()
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
# Create a sample application # Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Create a mock request # Test Submitted Status
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 0, EMAIL)
# Modify the application's property
application.status = DomainApplication.ApplicationStatus.APPROVED
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = self.mock_client.EMAILS_SENT
kwargs = call_args[0]["kwargs"]
# Retrieve the email details from the arguments
from_email = kwargs.get("FromEmailAddress")
to_email = kwargs["Destination"]["ToAddresses"][0]
email_content = kwargs["Content"]
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
# Assert or perform other checks on the email details
expected_string = "Congratulations! Your .gov domain request has been approved."
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
self.assertEqual(to_email, EMAIL)
self.assertIn(expected_string, email_body)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
@boto3_mocking.patching # Test Withdrawn Status
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED)
self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
# Test Submitted Status Again (No new email should be sent)
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
def test_save_model_sends_rejected_email(self):
"""When transitioning to rejected on a domain request,
an email is sent out every time."""
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Test Submitted Status
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED)
self.assert_email_is_accurate("Your .gov domain request has been rejected.", 0, EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Test Withdrawn Status
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
# Test Submitted Status Again (No new email should be sent)
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
def test_save_model_sends_withdrawn_email(self):
"""When transitioning to withdrawn on a domain request,
an email is sent out every time."""
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Test Submitted Status
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN)
self.assert_email_is_accurate(
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 0, EMAIL
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Test Withdrawn Status
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
self.assert_email_is_accurate("We received your .gov domain request.", 1, EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
# Test Submitted Status Again (No new email should be sent)
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
def test_save_model_sets_approved_domain(self): def test_save_model_sets_approved_domain(self):
# make sure there is no user with this email # make sure there is no user with this email
EMAIL = "mayor@igorville.gov" EMAIL = "mayor@igorville.gov"
@ -510,45 +594,6 @@ class TestDomainApplicationAdmin(MockEppLib):
# Test that approved domain exists and equals requested domain # Test that approved domain exists and equals requested domain
self.assertEqual(application.requested_domain.name, application.approved_domain.name) self.assertEqual(application.requested_domain.name, application.approved_domain.name)
@boto3_mocking.patching
def test_save_model_sends_rejected_email(self):
# make sure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Create a mock request
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
# Modify the application's property
application.status = DomainApplication.ApplicationStatus.REJECTED
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = self.mock_client.EMAILS_SENT
kwargs = call_args[0]["kwargs"]
# Retrieve the email details from the arguments
from_email = kwargs.get("FromEmailAddress")
to_email = kwargs["Destination"]["ToAddresses"][0]
email_content = kwargs["Content"]
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
# Assert or perform other checks on the email details
expected_string = "Your .gov domain request has been rejected."
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
self.assertEqual(to_email, EMAIL)
self.assertIn(expected_string, email_body)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
@boto3_mocking.patching
def test_save_model_sets_restricted_status_on_user(self): def test_save_model_sets_restricted_status_on_user(self):
# make sure there is no user with this email # make sure there is no user with this email
EMAIL = "mayor@igorville.gov" EMAIL = "mayor@igorville.gov"
@ -699,41 +744,13 @@ class TestDomainApplicationAdmin(MockEppLib):
"Cannot edit an application with a restricted creator.", "Cannot edit an application with a restricted creator.",
) )
@boto3_mocking.patching def trigger_saving_approved_to_another_state(self, domain_is_active, another_state):
def test_error_when_saving_approved_to_rejected_and_domain_is_active(self): """Helper method that triggers domain request state changes from approved to another state,
# Create an instance of the model with an associated domain that can be either active (READY) or not.
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
application.approved_domain = domain
application.save()
# Create a request object with a superuser Used to test errors when saving a change with an active domain, also used to test side effects
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) when saving a change goes through."""
request.user = self.superuser
# Define a custom implementation for is_active
def custom_is_active(self):
return True # Override to return True
# Use ExitStack to combine patch contexts
with ExitStack() as stack:
# Patch Domain.is_active and django.contrib.messages.error simultaneously
stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
stack.enter_context(patch.object(messages, "error"))
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
# Simulate saving the model
application.status = DomainApplication.ApplicationStatus.REJECTED
self.admin.save_model(request, application, None, True)
# Assert that the error message was called with the correct argument
messages.error.assert_called_once_with(
request,
"This action is not permitted. The domain " + "is already active.",
)
def test_side_effects_when_saving_approved_to_rejected(self):
# Create an instance of the model # Create an instance of the model
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name) domain = Domain.objects.create(name=application.requested_domain.name)
@ -747,19 +764,24 @@ class TestDomainApplicationAdmin(MockEppLib):
# Define a custom implementation for is_active # Define a custom implementation for is_active
def custom_is_active(self): def custom_is_active(self):
return False # Override to return False return domain_is_active # Override to return True
# Use ExitStack to combine patch contexts # Use ExitStack to combine patch contexts
with ExitStack() as stack: with ExitStack() as stack:
# Patch Domain.is_active and django.contrib.messages.error simultaneously # Patch Domain.is_active and django.contrib.messages.error simultaneously
stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
stack.enter_context(patch.object(messages, "error")) stack.enter_context(patch.object(messages, "error"))
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise(): application.status = another_state
# Simulate saving the model
application.status = DomainApplication.ApplicationStatus.REJECTED
self.admin.save_model(request, application, None, True) self.admin.save_model(request, application, None, True)
# Assert that the error message was called with the correct argument
if domain_is_active:
messages.error.assert_called_once_with(
request,
"This action is not permitted. The domain " + "is already active.",
)
else:
# Assert that the error message was never called # Assert that the error message was never called
messages.error.assert_not_called() messages.error.assert_not_called()
@ -773,75 +795,29 @@ class TestDomainApplicationAdmin(MockEppLib):
with self.assertRaises(DomainInformation.DoesNotExist): with self.assertRaises(DomainInformation.DoesNotExist):
domain_information.refresh_from_db() domain_information.refresh_from_db()
def test_error_when_saving_approved_to_in_review_and_domain_is_active(self):
self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.IN_REVIEW)
def test_error_when_saving_approved_to_action_needed_and_domain_is_active(self):
self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.ACTION_NEEDED)
def test_error_when_saving_approved_to_rejected_and_domain_is_active(self):
self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.REJECTED)
def test_error_when_saving_approved_to_ineligible_and_domain_is_active(self): def test_error_when_saving_approved_to_ineligible_and_domain_is_active(self):
# Create an instance of the model self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.INELIGIBLE)
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
application.approved_domain = domain
application.save()
# Create a request object with a superuser def test_side_effects_when_saving_approved_to_in_review(self):
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.IN_REVIEW)
request.user = self.superuser
# Define a custom implementation for is_active def test_side_effects_when_saving_approved_to_action_needed(self):
def custom_is_active(self): self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.ACTION_NEEDED)
return True # Override to return True
# Use ExitStack to combine patch contexts def test_side_effects_when_saving_approved_to_rejected(self):
with ExitStack() as stack: self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.REJECTED)
# Patch Domain.is_active and django.contrib.messages.error simultaneously
stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
stack.enter_context(patch.object(messages, "error"))
# Simulate saving the model
application.status = DomainApplication.ApplicationStatus.INELIGIBLE
self.admin.save_model(request, application, None, True)
# Assert that the error message was called with the correct argument
messages.error.assert_called_once_with(
request,
"This action is not permitted. The domain " + "is already active.",
)
def test_side_effects_when_saving_approved_to_ineligible(self): def test_side_effects_when_saving_approved_to_ineligible(self):
# Create an instance of the model self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.INELIGIBLE)
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
domain_information = DomainInformation.objects.create(creator=self.superuser, domain=domain)
application.approved_domain = domain
application.save()
# Create a request object with a superuser
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
request.user = self.superuser
# Define a custom implementation for is_active
def custom_is_active(self):
return False # Override to return False
# Use ExitStack to combine patch contexts
with ExitStack() as stack:
# Patch Domain.is_active and django.contrib.messages.error simultaneously
stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
stack.enter_context(patch.object(messages, "error"))
# Simulate saving the model
application.status = DomainApplication.ApplicationStatus.INELIGIBLE
self.admin.save_model(request, application, None, True)
# Assert that the error message was never called
messages.error.assert_not_called()
self.assertEqual(application.approved_domain, None)
# Assert that Domain got Deleted
with self.assertRaises(Domain.DoesNotExist):
domain.refresh_from_db()
# Assert that DomainInformation got Deleted
with self.assertRaises(DomainInformation.DoesNotExist):
domain_information.refresh_from_db()
def test_has_correct_filters(self): def test_has_correct_filters(self):
""" """

View file

@ -0,0 +1,31 @@
from django.test import Client, TestCase, override_settings
from django.contrib.auth import get_user_model
class MyTestCase(TestCase):
def setUp(self):
self.client = Client()
username = "test_user"
first_name = "First"
last_name = "Last"
email = "info@example.com"
self.user = get_user_model().objects.create(
username=username, first_name=first_name, last_name=last_name, email=email
)
self.client.force_login(self.user)
def tearDown(self):
super().tearDown()
self.user.delete()
@override_settings(IS_PRODUCTION=True)
def test_production_environment(self):
"""No banner on prod."""
home_page = self.client.get("/")
self.assertNotContains(home_page, "You are on a test site.")
@override_settings(IS_PRODUCTION=False)
def test_non_production_environment(self):
"""Banner on non-prod."""
home_page = self.client.get("/")
self.assertContains(home_page, "You are on a test site.")

View file

@ -161,33 +161,63 @@ class TestDomainApplication(TestCase):
application.submit() application.submit()
self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED) self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED)
def test_submit_sends_email(self): def check_email_sent(self, application, msg, action, expected_count):
"""Create an application and submit it and see if email was sent.""" """Check if an email was sent after performing an action."""
with less_console_noise():
user, _ = User.objects.get_or_create(username="testy")
contact = Contact.objects.create(email="test@test.gov")
domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
application = DomainApplication.objects.create(
creator=user,
requested_domain=domain,
submitter=contact,
)
application.save()
with self.subTest(msg=msg, action=action):
with boto3_mocking.clients.handler_for("sesv2", self.mock_client): with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
application.submit() with less_console_noise():
# Perform the specified action
action_method = getattr(application, action)
action_method()
# check to see if an email was sent # Check if an email was sent
self.assertGreater( sent_emails = [
len(
[
email email
for email in MockSESClient.EMAILS_SENT for email in MockSESClient.EMAILS_SENT
if "test@test.gov" in email["kwargs"]["Destination"]["ToAddresses"] if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"]
] ]
), self.assertEqual(len(sent_emails), expected_count)
0,
) def test_submit_from_started_sends_email(self):
msg = "Create an application and submit it and see if email was sent."
application = completed_application()
self.check_email_sent(application, msg, "submit", 1)
def test_submit_from_withdrawn_sends_email(self):
msg = "Create a withdrawn application and submit it and see if email was sent."
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
self.check_email_sent(application, msg, "submit", 1)
def test_submit_from_action_needed_does_not_send_email(self):
msg = "Create an application with ACTION_NEEDED status and submit it, check if email was not sent."
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
self.check_email_sent(application, msg, "submit", 0)
def test_submit_from_in_review_does_not_send_email(self):
msg = "Create a withdrawn application and submit it and see if email was sent."
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
self.check_email_sent(application, msg, "submit", 0)
def test_approve_sends_email(self):
msg = "Create an application and approve it and see if email was sent."
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
self.check_email_sent(application, msg, "approve", 1)
def test_withdraw_sends_email(self):
msg = "Create an application and withdraw it and see if email was sent."
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
self.check_email_sent(application, msg, "withdraw", 1)
def test_reject_sends_email(self):
msg = "Create an application and reject it and see if email was sent."
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
self.check_email_sent(application, msg, "reject", 1)
def test_reject_with_prejudice_does_not_send_email(self):
msg = "Create an application and reject it with prejudice and see if email was sent."
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
self.check_email_sent(application, msg, "reject_with_prejudice", 0)
def test_submit_transition_allowed(self): def test_submit_transition_allowed(self):
""" """
@ -464,6 +494,46 @@ class TestDomainApplication(TestCase):
with self.assertRaises(exception_type): with self.assertRaises(exception_type):
application.reject_with_prejudice() application.reject_with_prejudice()
def test_transition_not_allowed_approved_in_review_when_domain_is_active(self):
"""Create an application with status approved, create a matching domain that
is active, and call in_review against transition rules"""
domain = Domain.objects.create(name=self.approved_application.requested_domain.name)
self.approved_application.approved_domain = domain
self.approved_application.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):
with less_console_noise():
# Use patch to temporarily replace is_active with the custom implementation
with patch.object(Domain, "is_active", custom_is_active):
# Now, when you call is_active on Domain, it will return True
with self.assertRaises(TransitionNotAllowed):
self.approved_application.in_review()
def test_transition_not_allowed_approved_action_needed_when_domain_is_active(self):
"""Create an application with status approved, create a matching domain that
is active, and call action_needed against transition rules"""
domain = Domain.objects.create(name=self.approved_application.requested_domain.name)
self.approved_application.approved_domain = domain
self.approved_application.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):
with less_console_noise():
# Use patch to temporarily replace is_active with the custom implementation
with patch.object(Domain, "is_active", custom_is_active):
# Now, when you call is_active on Domain, it will return True
with self.assertRaises(TransitionNotAllowed):
self.approved_application.action_needed()
def test_transition_not_allowed_approved_rejected_when_domain_is_active(self): def test_transition_not_allowed_approved_rejected_when_domain_is_active(self):
"""Create an application with status approved, create a matching domain that """Create an application with status approved, create a matching domain that
is active, and call reject against transition rules""" is active, and call reject against transition rules"""

View file

@ -555,7 +555,7 @@ class DomainYourContactInformationView(DomainFormBaseView):
# Post to DB using values from the form # Post to DB using values from the form
form.save() form.save()
messages.success(self.request, "Your contact information has been updated.") messages.success(self.request, "Your contact information for all your domains has been updated.")
# superclass has the redirect # superclass has the redirect
return super().form_valid(form) return super().form_valid(form)