mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-01 08:43:30 +02:00
Merge and email template revision
This commit is contained in:
commit
dd247977c7
15 changed files with 428 additions and 85 deletions
|
@ -758,6 +758,14 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
# to activate the edit/delete/view buttons
|
||||
filter_horizontal = ("other_contacts",)
|
||||
|
||||
autocomplete_fields = [
|
||||
"creator",
|
||||
"domain_application",
|
||||
"authorizing_official",
|
||||
"domain",
|
||||
"submitter",
|
||||
]
|
||||
|
||||
# Table ordering
|
||||
ordering = ["domain__name"]
|
||||
|
||||
|
@ -1097,6 +1105,14 @@ class DomainInformationInline(admin.StackedInline):
|
|||
# to activate the edit/delete/view buttons
|
||||
filter_horizontal = ("other_contacts",)
|
||||
|
||||
autocomplete_fields = [
|
||||
"creator",
|
||||
"domain_application",
|
||||
"authorizing_official",
|
||||
"domain",
|
||||
"submitter",
|
||||
]
|
||||
|
||||
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
||||
"""customize the behavior of formfields with manytomany relationships. the customized
|
||||
behavior includes sorting of objects in lists as well as customizing helper text"""
|
||||
|
@ -1176,7 +1192,14 @@ class DomainAdmin(ListHeaderAdmin):
|
|||
if object_id is not None:
|
||||
domain = Domain.objects.get(pk=object_id)
|
||||
years_to_extend_by = self._get_calculated_years_for_exp_date(domain)
|
||||
curr_exp_date = domain.registry_expiration_date
|
||||
|
||||
try:
|
||||
curr_exp_date = domain.registry_expiration_date
|
||||
except KeyError:
|
||||
# No expiration date was found. Return none.
|
||||
extra_context["extended_expiration_date"] = None
|
||||
return super().changeform_view(request, object_id, form_url, extra_context)
|
||||
|
||||
if curr_exp_date < date.today():
|
||||
extra_context["extended_expiration_date"] = date.today() + relativedelta(years=years_to_extend_by)
|
||||
else:
|
||||
|
|
|
@ -286,6 +286,7 @@ AWS_MAX_ATTEMPTS = 3
|
|||
BOTO_CONFIG = Config(retries={"mode": AWS_RETRY_MODE, "max_attempts": AWS_MAX_ATTEMPTS})
|
||||
|
||||
# email address to use for various automated correspondence
|
||||
# also used as a default to and bcc email
|
||||
DEFAULT_FROM_EMAIL = "help@get.gov <help@get.gov>"
|
||||
|
||||
# connect to an (external) SMTP server for sending email
|
||||
|
|
|
@ -284,6 +284,7 @@ class OrganizationContactForm(RegistrarForm):
|
|||
message="Enter a zip code in the form of 12345 or 12345-6789.",
|
||||
)
|
||||
],
|
||||
error_messages={"required": ("Enter a zip code in the form of 12345 or 12345-6789.")},
|
||||
)
|
||||
urbanization = forms.CharField(
|
||||
required=False,
|
||||
|
|
|
@ -13,6 +13,7 @@ from typing import Any
|
|||
from registrar.models.host import Host
|
||||
from registrar.models.host_ip import HostIP
|
||||
from registrar.utility.enums import DefaultEmail
|
||||
from registrar.utility import errors
|
||||
|
||||
from registrar.utility.errors import (
|
||||
ActionNotAllowed,
|
||||
|
@ -192,9 +193,17 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
|
||||
@classmethod
|
||||
def available(cls, domain: str) -> bool:
|
||||
"""Check if a domain is available."""
|
||||
"""Check if a domain is available.
|
||||
This is called by the availablility api and
|
||||
is called in the validate function on the request/domain page
|
||||
|
||||
throws- RegistryError or InvalidDomainError"""
|
||||
if not cls.string_could_be_domain(domain):
|
||||
raise ValueError("Not a valid domain: %s" % str(domain))
|
||||
logger.warning("Not a valid domain: %s" % str(domain))
|
||||
# throw invalid domain error so that it can be caught in
|
||||
# validate_and_handle_errors in domain_helper
|
||||
raise errors.InvalidDomainError()
|
||||
|
||||
domain_name = domain.lower()
|
||||
req = commands.CheckDomain([domain_name])
|
||||
return registry.send(req, cleaned=True).res_data[0].avail
|
||||
|
@ -429,7 +438,6 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
raise NameserverError(code=nsErrorCodes.INVALID_HOST, nameserver=nameserver)
|
||||
elif cls.isSubdomain(name, nameserver) and (ip is None or ip == []):
|
||||
raise NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=nameserver)
|
||||
|
||||
elif not cls.isSubdomain(name, nameserver) and (ip is not None and ip != []):
|
||||
raise NameserverError(code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED, nameserver=nameserver, ip=ip)
|
||||
elif ip is not None and ip != []:
|
||||
|
@ -1780,6 +1788,10 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
for cleaned_host in cleaned_hosts:
|
||||
# Check if the cleaned_host already exists
|
||||
host_in_db, host_created = Host.objects.get_or_create(domain=self, name=cleaned_host["name"])
|
||||
# Check if the nameserver is a subdomain of the current domain
|
||||
# If it is NOT a subdomain, we remove the IP address
|
||||
if not Domain.isSubdomain(self.name, cleaned_host["name"]):
|
||||
cleaned_host["addrs"] = []
|
||||
# Get cleaned list of ips for update
|
||||
cleaned_ips = cleaned_host["addrs"]
|
||||
if not host_created:
|
||||
|
|
|
@ -4,6 +4,7 @@ from typing import Union
|
|||
import logging
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django_fsm import FSMField, transition # type: ignore
|
||||
from django.utils import timezone
|
||||
|
@ -611,7 +612,9 @@ class DomainApplication(TimeStampedModel):
|
|||
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, bcc_address=""
|
||||
):
|
||||
"""Send a status update email to the submitter.
|
||||
|
||||
The email goes to the email address that the submitter gave as their
|
||||
|
@ -636,6 +639,7 @@ class DomainApplication(TimeStampedModel):
|
|||
email_template_subject,
|
||||
self.submitter.email,
|
||||
context={"application": self},
|
||||
bcc_address=bcc_address,
|
||||
)
|
||||
logger.info(f"The {new_status} email sent to: {self.submitter.email}")
|
||||
except EmailSendingError:
|
||||
|
@ -677,11 +681,17 @@ class DomainApplication(TimeStampedModel):
|
|||
# Limit email notifications to transitions from Started and Withdrawn
|
||||
limited_statuses = [self.ApplicationStatus.STARTED, self.ApplicationStatus.WITHDRAWN]
|
||||
|
||||
bcc_address = ""
|
||||
if settings.IS_PRODUCTION:
|
||||
bcc_address = settings.DEFAULT_FROM_EMAIL
|
||||
|
||||
if self.status in limited_statuses:
|
||||
self._send_status_update_email(
|
||||
"submission confirmation",
|
||||
"emails/submission_confirmation.txt",
|
||||
"emails/submission_confirmation_subject.txt",
|
||||
True,
|
||||
bcc_address,
|
||||
)
|
||||
|
||||
@transition(
|
||||
|
|
|
@ -33,11 +33,12 @@ class DomainHelper:
|
|||
# Split into pieces for the linter
|
||||
domain = cls._validate_domain_string(domain, blank_ok)
|
||||
|
||||
try:
|
||||
if not check_domain_available(domain):
|
||||
raise errors.DomainUnavailableError()
|
||||
except RegistryError as err:
|
||||
raise errors.RegistrySystemError() from err
|
||||
if domain != "":
|
||||
try:
|
||||
if not check_domain_available(domain):
|
||||
raise errors.DomainUnavailableError()
|
||||
except RegistryError as err:
|
||||
raise errors.RegistrySystemError() from err
|
||||
return domain
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -9,8 +9,7 @@ STATUS: Rejected
|
|||
|
||||
----------------------------------------------------------------
|
||||
{% if application.rejection_reason != 'other' %}
|
||||
REJECTION REASON{% endif %}
|
||||
{% if application.rejection_reason == 'domain_purpose' %}
|
||||
REJECTION REASON{% endif %}{% if application.rejection_reason == 'domain_purpose' %}
|
||||
Your domain request was rejected because the purpose you provided did not meet our
|
||||
requirements. You didn’t provide enough information about how you intend to use the
|
||||
domain.
|
||||
|
@ -19,8 +18,7 @@ Learn more about:
|
|||
- Eligibility for a .gov domain <https://get.gov/domains/eligibility>
|
||||
- What you can and can’t do with .gov domains <https://get.gov/domains/requirements/>
|
||||
|
||||
If you have questions or comments, reply to this email.
|
||||
{% elif application.rejection_reason == 'requestor' %}
|
||||
If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'requestor' %}
|
||||
Your domain request was rejected because we don’t believe you’re eligible to request a
|
||||
.gov domain on behalf of {{ application.organization_name }}. You must be a government employee, or be
|
||||
working on behalf of a government organization, to request a .gov domain.
|
||||
|
@ -28,8 +26,7 @@ working on behalf of a government organization, to request a .gov domain.
|
|||
|
||||
DEMONSTRATE ELIGIBILITY
|
||||
If you can provide more information that demonstrates your eligibility, or you want to
|
||||
discuss further, reply to this email.
|
||||
{% elif application.rejection_reason == 'second_domain_reasoning' %}
|
||||
discuss further, reply to this email.{% elif application.rejection_reason == 'second_domain_reasoning' %}
|
||||
Your domain request was rejected because {{ application.organization_name }} has a .gov domain. Our
|
||||
practice is to approve one domain per online service per government organization. We
|
||||
evaluate additional requests on a case-by-case basis. You did not provide sufficient
|
||||
|
@ -38,11 +35,9 @@ justification for an additional domain.
|
|||
Read more about our practice of approving one domain per online service
|
||||
<https://get.gov/domains/before/#one-domain-per-service>.
|
||||
|
||||
If you have questions or comments, reply to this email.
|
||||
{% elif application.rejection_reason == 'contacts_or_organization_legitimacy' %}
|
||||
If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'contacts_or_organization_legitimacy' %}
|
||||
Your domain request was rejected because we could not verify the organizational
|
||||
contacts you provided. If you have questions or comments, reply to this email.
|
||||
{% elif application.rejection_reason == 'organization_eligibility' %}
|
||||
contacts you provided. If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'organization_eligibility' %}
|
||||
Your domain request was rejected because we determined that {{ application.organization_name }} is not
|
||||
eligible for a .gov domain. .Gov domains are only available to official U.S.-based
|
||||
government organizations.
|
||||
|
@ -53,8 +48,7 @@ If you can provide documentation that demonstrates your eligibility, reply to th
|
|||
This can include links to (or copies of) your authorizing legislation, your founding
|
||||
charter or bylaws, or other similar documentation. Without this, we can’t approve a
|
||||
.gov domain for your organization. Learn more about eligibility for .gov domains
|
||||
<https://get.gov/domains/eligibility/>.
|
||||
{% elif application.rejection_reason == 'naming_requirements' %}
|
||||
<https://get.gov/domains/eligibility/>.{% elif application.rejection_reason == 'naming_requirements' %}
|
||||
Your domain request was rejected because it does not meet our naming requirements.
|
||||
Domains should uniquely identify a government organization and be clear to the
|
||||
general public. Learn more about naming requirements for your type of organization
|
||||
|
@ -63,8 +57,7 @@ general public. Learn more about naming requirements for your type of organizati
|
|||
|
||||
YOU CAN SUBMIT A NEW REQUEST
|
||||
We encourage you to request a domain that meets our requirements. If you have
|
||||
questions or want to discuss potential domain names, reply to this email.
|
||||
{% elif application.rejection_reason == 'other' %}
|
||||
questions or want to discuss potential domain names, reply to this email.{% elif application.rejection_reason == 'other' %}
|
||||
YOU CAN SUBMIT A NEW REQUEST
|
||||
If your organization is eligible for a .gov domain and you meet our other requirements, you can submit a new request.
|
||||
|
||||
|
|
|
@ -692,6 +692,56 @@ class MockEppLib(TestCase):
|
|||
],
|
||||
ex_date=datetime.date(2023, 5, 25),
|
||||
)
|
||||
|
||||
mockDataInfoDomainSubdomain = fakedEppObject(
|
||||
"fakePw",
|
||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||
hosts=["fake.meoward.gov"],
|
||||
statuses=[
|
||||
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||
common.Status(state="inactive", description="", lang="en"),
|
||||
],
|
||||
ex_date=datetime.date(2023, 5, 25),
|
||||
)
|
||||
|
||||
mockDataInfoDomainSubdomainAndIPAddress = fakedEppObject(
|
||||
"fakePw",
|
||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||
hosts=["fake.meow.gov"],
|
||||
statuses=[
|
||||
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||
common.Status(state="inactive", description="", lang="en"),
|
||||
],
|
||||
ex_date=datetime.date(2023, 5, 25),
|
||||
addrs=[common.Ip(addr="2.0.0.8")],
|
||||
)
|
||||
|
||||
mockDataInfoDomainNotSubdomainNoIP = fakedEppObject(
|
||||
"fakePw",
|
||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||
hosts=["fake.meow.com"],
|
||||
statuses=[
|
||||
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||
common.Status(state="inactive", description="", lang="en"),
|
||||
],
|
||||
ex_date=datetime.date(2023, 5, 25),
|
||||
)
|
||||
|
||||
mockDataInfoDomainSubdomainNoIP = fakedEppObject(
|
||||
"fakePw",
|
||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||
hosts=["fake.subdomainwoip.gov"],
|
||||
statuses=[
|
||||
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||
common.Status(state="inactive", description="", lang="en"),
|
||||
],
|
||||
ex_date=datetime.date(2023, 5, 25),
|
||||
)
|
||||
|
||||
mockDataExtensionDomain = fakedEppObject(
|
||||
"fakePw",
|
||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||
|
@ -829,6 +879,24 @@ class MockEppLib(TestCase):
|
|||
addrs=[common.Ip(addr="1.2.3.4"), common.Ip(addr="2.3.4.5")],
|
||||
)
|
||||
|
||||
mockDataInfoHosts1IP = fakedEppObject(
|
||||
"lastPw",
|
||||
cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)),
|
||||
addrs=[common.Ip(addr="2.0.0.8")],
|
||||
)
|
||||
|
||||
mockDataInfoHostsNotSubdomainNoIP = fakedEppObject(
|
||||
"lastPw",
|
||||
cr_date=make_aware(datetime.datetime(2023, 8, 26, 19, 45, 35)),
|
||||
addrs=[],
|
||||
)
|
||||
|
||||
mockDataInfoHostsSubdomainNoIP = fakedEppObject(
|
||||
"lastPw",
|
||||
cr_date=make_aware(datetime.datetime(2023, 8, 27, 19, 45, 35)),
|
||||
addrs=[],
|
||||
)
|
||||
|
||||
mockDataHostChange = fakedEppObject("lastPw", cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)))
|
||||
addDsData1 = {
|
||||
"keyTag": 1234,
|
||||
|
@ -995,6 +1063,8 @@ class MockEppLib(TestCase):
|
|||
return self.mockDeleteDomainCommands(_request, cleaned)
|
||||
case commands.RenewDomain:
|
||||
return self.mockRenewDomainCommand(_request, cleaned)
|
||||
case commands.InfoHost:
|
||||
return self.mockInfoHostCommmands(_request, cleaned)
|
||||
case _:
|
||||
return MagicMock(res_data=[self.mockDataInfoHosts])
|
||||
|
||||
|
@ -1009,6 +1079,25 @@ class MockEppLib(TestCase):
|
|||
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
||||
)
|
||||
|
||||
def mockInfoHostCommmands(self, _request, cleaned):
|
||||
request_name = getattr(_request, "name", None)
|
||||
|
||||
# Define a dictionary to map request names to data and extension values
|
||||
request_mappings = {
|
||||
"fake.meow.gov": (self.mockDataInfoHosts1IP, None), # is subdomain and has ip
|
||||
"fake.meow.com": (self.mockDataInfoHostsNotSubdomainNoIP, None), # not subdomain w no ip
|
||||
"fake.subdomainwoip.gov": (self.mockDataInfoHostsSubdomainNoIP, None), # subdomain w no ip
|
||||
}
|
||||
|
||||
# Retrieve the corresponding values from the dictionary
|
||||
default_mapping = (self.mockDataInfoHosts, None)
|
||||
res_data, extensions = request_mappings.get(request_name, default_mapping)
|
||||
|
||||
return MagicMock(
|
||||
res_data=[res_data],
|
||||
extensions=[extensions] if extensions is not None else [],
|
||||
)
|
||||
|
||||
def mockUpdateHostCommands(self, _request, cleaned):
|
||||
test_ws_ip = common.Ip(addr="1.1. 1.1")
|
||||
addrs_submitted = getattr(_request, "addrs", [])
|
||||
|
@ -1097,6 +1186,10 @@ class MockEppLib(TestCase):
|
|||
"adomain2.gov": (self.InfoDomainWithVerisignSecurityContact, None),
|
||||
"defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None),
|
||||
"justnameserver.com": (self.justNameserver, None),
|
||||
"meoward.gov": (self.mockDataInfoDomainSubdomain, None),
|
||||
"meow.gov": (self.mockDataInfoDomainSubdomainAndIPAddress, None),
|
||||
"fakemeow.gov": (self.mockDataInfoDomainNotSubdomainNoIP, None),
|
||||
"subdomainwoip.gov": (self.mockDataInfoDomainSubdomainNoIP, None),
|
||||
}
|
||||
|
||||
# Retrieve the corresponding values from the dictionary
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from datetime import date
|
||||
from django.test import TestCase, RequestFactory, Client
|
||||
from django.test import TestCase, RequestFactory, Client, override_settings
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from contextlib import ExitStack
|
||||
from django_webtest import WebTest # type: ignore
|
||||
|
@ -609,7 +609,9 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
# 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):
|
||||
def assert_email_is_accurate(
|
||||
self, expected_string, email_index, email_address, test_that_no_bcc=False, bcc_email_address=""
|
||||
):
|
||||
"""Helper method for the email test cases.
|
||||
email_index is the index of the email in mock_client."""
|
||||
|
||||
|
@ -629,12 +631,26 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
self.assertEqual(to_email, email_address)
|
||||
self.assertIn(expected_string, email_body)
|
||||
|
||||
if test_that_no_bcc:
|
||||
_ = ""
|
||||
with self.assertRaises(KeyError):
|
||||
with less_console_noise():
|
||||
_ = kwargs["Destination"]["BccAddresses"][0]
|
||||
self.assertEqual(_, "")
|
||||
|
||||
if bcc_email_address:
|
||||
bcc_email = kwargs["Destination"]["BccAddresses"][0]
|
||||
self.assertEqual(bcc_email, bcc_email_address)
|
||||
|
||||
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."""
|
||||
no email is sent out.
|
||||
|
||||
Also test that the default email set in settings is NOT BCCd on non-prod whenever
|
||||
an email does go out."""
|
||||
|
||||
with less_console_noise():
|
||||
# Ensure there is no user with this email
|
||||
|
@ -646,7 +662,64 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
|
||||
# Test Submitted Status from started
|
||||
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
|
||||
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL)
|
||||
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, True)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||
|
||||
# Test Withdrawn Status
|
||||
self.transition_state_and_send_email(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, True
|
||||
)
|
||||
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)
|
||||
|
||||
@override_settings(IS_PRODUCTION=True)
|
||||
def test_save_model_sends_submitted_email_with_bcc_on_prod(self):
|
||||
"""When transitioning to submitted from started or withdrawn on a domain request,
|
||||
an email is sent out.
|
||||
|
||||
When transitioning to submitted from dns needed or in review on a domain request,
|
||||
no email is sent out.
|
||||
|
||||
Also test that the default email set in settings IS BCCd on prod whenever
|
||||
an email does go out."""
|
||||
|
||||
with less_console_noise():
|
||||
# Ensure there is no user with this email
|
||||
EMAIL = "mayor@igorville.gov"
|
||||
User.objects.filter(email=EMAIL).delete()
|
||||
|
||||
BCC_EMAIL = settings.DEFAULT_FROM_EMAIL
|
||||
|
||||
# Create a sample application
|
||||
application = completed_application()
|
||||
|
||||
# Test Submitted Status from started
|
||||
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
|
||||
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, False, BCC_EMAIL)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||
|
||||
# Test Withdrawn Status
|
||||
|
@ -658,6 +731,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
|
||||
# Test Submitted Status Again (from withdrawn)
|
||||
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
|
||||
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, False, BCC_EMAIL)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||
|
||||
# Move it to IN_REVIEW
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
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.")
|
|
@ -20,7 +20,7 @@ from registrar.models.user import User
|
|||
from registrar.utility.errors import ActionNotAllowed, NameserverError
|
||||
|
||||
from registrar.models.utility.contact_error import ContactError, ContactErrorCodes
|
||||
|
||||
from registrar.utility import errors
|
||||
|
||||
from django_fsm import TransitionNotAllowed # type: ignore
|
||||
from epplibwrapper import (
|
||||
|
@ -96,7 +96,7 @@ class TestDomainCache(MockEppLib):
|
|||
|
||||
self.mockedSendFunction.assert_has_calls(expectedCalls)
|
||||
|
||||
def test_cache_nested_elements(self):
|
||||
def test_cache_nested_elements_not_subdomain(self):
|
||||
"""Cache works correctly with the nested objects cache and hosts"""
|
||||
with less_console_noise():
|
||||
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||
|
@ -113,7 +113,7 @@ class TestDomainCache(MockEppLib):
|
|||
}
|
||||
expectedHostsDict = {
|
||||
"name": self.mockDataInfoDomain.hosts[0],
|
||||
"addrs": [item.addr for item in self.mockDataInfoHosts.addrs],
|
||||
"addrs": [], # should return empty bc fake.host.com is not a subdomain of igorville.gov
|
||||
"cr_date": self.mockDataInfoHosts.cr_date,
|
||||
}
|
||||
|
||||
|
@ -138,6 +138,59 @@ class TestDomainCache(MockEppLib):
|
|||
# invalidate cache
|
||||
domain._cache = {}
|
||||
|
||||
# get host
|
||||
domain._get_property("hosts")
|
||||
# Should return empty bc fake.host.com is not a subdomain of igorville.gov
|
||||
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
|
||||
|
||||
# get contacts
|
||||
domain._get_property("contacts")
|
||||
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
|
||||
self.assertEqual(domain._cache["contacts"], expectedContactsDict)
|
||||
|
||||
def test_cache_nested_elements_is_subdomain(self):
|
||||
"""Cache works correctly with the nested objects cache and hosts"""
|
||||
with less_console_noise():
|
||||
domain, _ = Domain.objects.get_or_create(name="meoward.gov")
|
||||
|
||||
# The contact list will initially contain objects of type 'DomainContact'
|
||||
# this is then transformed into PublicContact, and cache should NOT
|
||||
# hold onto the DomainContact object
|
||||
expectedUnfurledContactsList = [
|
||||
common.DomainContact(contact="123", type="security"),
|
||||
]
|
||||
expectedContactsDict = {
|
||||
PublicContact.ContactTypeChoices.ADMINISTRATIVE: None,
|
||||
PublicContact.ContactTypeChoices.SECURITY: "123",
|
||||
PublicContact.ContactTypeChoices.TECHNICAL: None,
|
||||
}
|
||||
expectedHostsDict = {
|
||||
"name": self.mockDataInfoDomainSubdomain.hosts[0],
|
||||
"addrs": [item.addr for item in self.mockDataInfoHosts.addrs],
|
||||
"cr_date": self.mockDataInfoHosts.cr_date,
|
||||
}
|
||||
|
||||
# this can be changed when the getter for contacts is implemented
|
||||
domain._get_property("contacts")
|
||||
|
||||
# check domain info is still correct and not overridden
|
||||
self.assertEqual(domain._cache["auth_info"], self.mockDataInfoDomainSubdomain.auth_info)
|
||||
self.assertEqual(domain._cache["cr_date"], self.mockDataInfoDomainSubdomain.cr_date)
|
||||
|
||||
# check contacts
|
||||
self.assertEqual(domain._cache["_contacts"], self.mockDataInfoDomainSubdomain.contacts)
|
||||
# The contact list should not contain what is sent by the registry by default,
|
||||
# as _fetch_cache will transform the type to PublicContact
|
||||
self.assertNotEqual(domain._cache["contacts"], expectedUnfurledContactsList)
|
||||
self.assertEqual(domain._cache["contacts"], expectedContactsDict)
|
||||
|
||||
# get and check hosts is set correctly
|
||||
domain._get_property("hosts")
|
||||
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
|
||||
self.assertEqual(domain._cache["contacts"], expectedContactsDict)
|
||||
# invalidate cache
|
||||
domain._cache = {}
|
||||
|
||||
# get host
|
||||
domain._get_property("hosts")
|
||||
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
|
||||
|
@ -502,16 +555,26 @@ class TestDomainAvailable(MockEppLib):
|
|||
self.assertFalse(available)
|
||||
patcher.stop()
|
||||
|
||||
def test_domain_available_with_value_error(self):
|
||||
def test_domain_available_with_invalid_error(self):
|
||||
"""
|
||||
Scenario: Testing whether an invalid domain is available
|
||||
Should throw ValueError
|
||||
Should throw InvalidDomainError
|
||||
|
||||
Validate ValueError is raised
|
||||
Validate InvalidDomainError is raised
|
||||
"""
|
||||
with self.assertRaises(ValueError):
|
||||
with self.assertRaises(errors.InvalidDomainError):
|
||||
Domain.available("invalid-string")
|
||||
|
||||
def test_domain_available_with_empty_string(self):
|
||||
"""
|
||||
Scenario: Testing whether an empty string domain name is available
|
||||
Should throw InvalidDomainError
|
||||
|
||||
Validate InvalidDomainError is raised
|
||||
"""
|
||||
with self.assertRaises(errors.InvalidDomainError):
|
||||
Domain.available("")
|
||||
|
||||
def test_domain_available_unsuccessful(self):
|
||||
"""
|
||||
Scenario: Testing behavior when registry raises a RegistryError
|
||||
|
@ -1572,31 +1635,100 @@ class TestRegistrantNameservers(MockEppLib):
|
|||
self.assertEqual(nameservers[0][1], ["1.1.1.1"])
|
||||
patcher.stop()
|
||||
|
||||
def test_nameservers_stored_on_fetch_cache(self):
|
||||
def test_nameservers_stored_on_fetch_cache_a_subdomain_with_ip(self):
|
||||
"""
|
||||
#1: Nameserver is a subdomain, and has an IP address
|
||||
referenced by mockDataInfoDomainSubdomainAndIPAddress
|
||||
"""
|
||||
with less_console_noise():
|
||||
# make the domain
|
||||
domain, _ = Domain.objects.get_or_create(name="meow.gov", state=Domain.State.READY)
|
||||
|
||||
# mock the get_or_create methods for Host and HostIP
|
||||
with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object(
|
||||
HostIP.objects, "get_or_create"
|
||||
) as mock_host_ip_get_or_create:
|
||||
mock_host_get_or_create.return_value = (Host(domain=domain), True)
|
||||
mock_host_ip_get_or_create.return_value = (HostIP(), True)
|
||||
|
||||
# force fetch_cache to be called, which will return above documented mocked hosts
|
||||
domain.nameservers
|
||||
|
||||
mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.meow.gov")
|
||||
# Retrieve the mocked_host from the return value of the mock
|
||||
actual_mocked_host, _ = mock_host_get_or_create.return_value
|
||||
mock_host_ip_get_or_create.assert_called_with(address="2.0.0.8", host=actual_mocked_host)
|
||||
self.assertEqual(mock_host_ip_get_or_create.call_count, 1)
|
||||
|
||||
def test_nameservers_stored_on_fetch_cache_a_subdomain_without_ip(self):
|
||||
"""
|
||||
#2: Nameserver is a subdomain, but doesn't have an IP address associated
|
||||
referenced by mockDataInfoDomainSubdomainNoIP
|
||||
"""
|
||||
with less_console_noise():
|
||||
# make the domain
|
||||
domain, _ = Domain.objects.get_or_create(name="subdomainwoip.gov", state=Domain.State.READY)
|
||||
|
||||
# mock the get_or_create methods for Host and HostIP
|
||||
with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object(
|
||||
HostIP.objects, "get_or_create"
|
||||
) as mock_host_ip_get_or_create:
|
||||
mock_host_get_or_create.return_value = (Host(domain=domain), True)
|
||||
mock_host_ip_get_or_create.return_value = (HostIP(), True)
|
||||
|
||||
# force fetch_cache to be called, which will return above documented mocked hosts
|
||||
domain.nameservers
|
||||
|
||||
mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.subdomainwoip.gov")
|
||||
mock_host_ip_get_or_create.assert_not_called()
|
||||
self.assertEqual(mock_host_ip_get_or_create.call_count, 0)
|
||||
|
||||
def test_nameservers_stored_on_fetch_cache_not_subdomain_with_ip(self):
|
||||
"""
|
||||
Scenario: Nameservers are stored in db when they are retrieved from fetch_cache.
|
||||
Verify the success of this by asserting get_or_create calls to db.
|
||||
The mocked data for the EPP calls returns a host name
|
||||
of 'fake.host.com' from InfoDomain and an array of 2 IPs: 1.2.3.4 and 2.3.4.5
|
||||
from InfoHost
|
||||
|
||||
#3: Nameserver is not a subdomain, but it does have an IP address returned
|
||||
due to how we set up our defaults
|
||||
"""
|
||||
with less_console_noise():
|
||||
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
|
||||
# mock the get_or_create methods for Host and HostIP
|
||||
|
||||
with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object(
|
||||
HostIP.objects, "get_or_create"
|
||||
) as mock_host_ip_get_or_create:
|
||||
# Set the return value for the mocks
|
||||
mock_host_get_or_create.return_value = (Host(), True)
|
||||
mock_host_get_or_create.return_value = (Host(domain=domain), True)
|
||||
mock_host_ip_get_or_create.return_value = (HostIP(), True)
|
||||
|
||||
# force fetch_cache to be called, which will return above documented mocked hosts
|
||||
domain.nameservers
|
||||
# assert that the mocks are called
|
||||
|
||||
mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.host.com")
|
||||
# Retrieve the mocked_host from the return value of the mock
|
||||
actual_mocked_host, _ = mock_host_get_or_create.return_value
|
||||
mock_host_ip_get_or_create.assert_called_with(address="2.3.4.5", host=actual_mocked_host)
|
||||
self.assertEqual(mock_host_ip_get_or_create.call_count, 2)
|
||||
mock_host_ip_get_or_create.assert_not_called()
|
||||
self.assertEqual(mock_host_ip_get_or_create.call_count, 0)
|
||||
|
||||
def test_nameservers_stored_on_fetch_cache_not_subdomain_without_ip(self):
|
||||
"""
|
||||
#4: Nameserver is not a subdomain and doesn't have an associated IP address
|
||||
referenced by self.mockDataInfoDomainNotSubdomainNoIP
|
||||
"""
|
||||
with less_console_noise():
|
||||
domain, _ = Domain.objects.get_or_create(name="fakemeow.gov", state=Domain.State.READY)
|
||||
|
||||
with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object(
|
||||
HostIP.objects, "get_or_create"
|
||||
) as mock_host_ip_get_or_create:
|
||||
mock_host_get_or_create.return_value = (Host(domain=domain), True)
|
||||
mock_host_ip_get_or_create.return_value = (HostIP(), True)
|
||||
|
||||
# force fetch_cache to be called, which will return above documented mocked hosts
|
||||
domain.nameservers
|
||||
mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.meow.com")
|
||||
mock_host_ip_get_or_create.assert_not_called()
|
||||
self.assertEqual(mock_host_ip_get_or_create.call_count, 0)
|
||||
|
||||
@skip("not implemented yet")
|
||||
def test_update_is_unsuccessful(self):
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from django.test import Client, TestCase
|
||||
from django.test import Client, TestCase, override_settings
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from .common import MockEppLib # type: ignore
|
||||
|
@ -50,3 +50,32 @@ class TestWithUser(MockEppLib):
|
|||
DomainApplication.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
self.user.delete()
|
||||
|
||||
|
||||
class TestEnvironmentVariablesEffects(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.")
|
||||
|
|
|
@ -821,14 +821,15 @@ class TestDomainNameservers(TestDomainOverview):
|
|||
nameserver1 = "ns1.igorville.gov"
|
||||
nameserver2 = "ns2.igorville.gov"
|
||||
valid_ip = "1.1. 1.1"
|
||||
# initial nameservers page has one server with two ips
|
||||
valid_ip_2 = "2.2. 2.2"
|
||||
# have to throw an error in order to test that the whitespace has been stripped from ip
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# attempt to submit the form without one host and an ip with whitespace
|
||||
nameservers_page.form["form-0-server"] = nameserver1
|
||||
nameservers_page.form["form-1-ip"] = valid_ip
|
||||
nameservers_page.form["form-0-ip"] = valid_ip
|
||||
nameservers_page.form["form-1-ip"] = valid_ip_2
|
||||
nameservers_page.form["form-1-server"] = nameserver2
|
||||
with less_console_noise(): # swallow log warning message
|
||||
result = nameservers_page.form.submit()
|
||||
|
@ -937,15 +938,14 @@ class TestDomainNameservers(TestDomainOverview):
|
|||
nameserver1 = "ns1.igorville.gov"
|
||||
nameserver2 = "ns2.igorville.gov"
|
||||
valid_ip = "127.0.0.1"
|
||||
# initial nameservers page has one server with two ips
|
||||
valid_ip_2 = "128.0.0.2"
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# attempt to submit the form without two hosts, both subdomains,
|
||||
# only one has ips
|
||||
nameservers_page.form["form-0-server"] = nameserver1
|
||||
nameservers_page.form["form-0-ip"] = valid_ip
|
||||
nameservers_page.form["form-1-server"] = nameserver2
|
||||
nameservers_page.form["form-1-ip"] = valid_ip
|
||||
nameservers_page.form["form-1-ip"] = valid_ip_2
|
||||
with less_console_noise(): # swallow log warning message
|
||||
result = nameservers_page.form.submit()
|
||||
# form submission was a successful post, response should be a 302
|
||||
|
|
|
@ -15,7 +15,7 @@ class EmailSendingError(RuntimeError):
|
|||
pass
|
||||
|
||||
|
||||
def send_templated_email(template_name: str, subject_template_name: str, to_address: str, context={}):
|
||||
def send_templated_email(template_name: str, subject_template_name: str, to_address: str, bcc_address="", context={}):
|
||||
"""Send an email built from a template to one email address.
|
||||
|
||||
template_name and subject_template_name are relative to the same template
|
||||
|
@ -40,10 +40,14 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr
|
|||
except Exception as exc:
|
||||
raise EmailSendingError("Could not access the SES client.") from exc
|
||||
|
||||
destination = {"ToAddresses": [to_address]}
|
||||
if bcc_address:
|
||||
destination["BccAddresses"] = [bcc_address]
|
||||
|
||||
try:
|
||||
ses_client.send_email(
|
||||
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
|
||||
Destination={"ToAddresses": [to_address]},
|
||||
Destination=destination,
|
||||
Content={
|
||||
"Simple": {
|
||||
"Subject": {"Data": subject},
|
||||
|
|
|
@ -14,6 +14,7 @@ from django.http import HttpResponseRedirect
|
|||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.views.generic.edit import FormMixin
|
||||
from django.conf import settings
|
||||
|
||||
from registrar.models import (
|
||||
Domain,
|
||||
|
@ -707,7 +708,7 @@ class DomainAddUserView(DomainFormBaseView):
|
|||
adding a success message to the view if the email sending succeeds"""
|
||||
|
||||
# Set a default email address to send to for staff
|
||||
requestor_email = "help@get.gov"
|
||||
requestor_email = settings.DEFAULT_FROM_EMAIL
|
||||
|
||||
# Check if the email requestor has a valid email address
|
||||
if not requestor.is_staff and requestor.email is not None and requestor.email.strip() != "":
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue