Merge branch 'main' into za/1448-filter-by-investigator

This commit is contained in:
zandercymatics 2023-12-19 09:01:32 -07:00
commit 63a58ae182
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
34 changed files with 1408 additions and 141 deletions

View file

@ -493,3 +493,34 @@ The `load_organization_data` script has five optional parameters. These are as f
| 3 | **directory** | Specifies the directory containing the files that will be parsed. Defaults to "migrationdata" |
| 4 | **domain_additional_filename** | Specifies the filename of domain_additional. Used as an override for the JSON. Has no default. |
| 5 | **organization_adhoc_filename** | Specifies the filename of organization_adhoc. Used as an override for the JSON. Has no default. |
## Extend Domain Extension Dates
This section outlines how to extend the expiration date of all ready domains (or a select subset) by a defined period of time.
### Running on sandboxes
#### Step 1: Login to CloudFoundry
```cf login -a api.fr.cloud.gov --sso```
#### Step 2: SSH into your environment
```cf ssh getgov-{space}```
Example: `cf ssh getgov-za`
#### Step 3: Create a shell instance
```/tmp/lifecycle/shell```
#### Step 4: Extend domains
```./manage.py extend_expiration_dates```
### Running locally
```docker-compose exec app ./manage.py extend_expiration_dates```
##### Optional parameters
| | Parameter | Description |
|:-:|:-------------------------- |:----------------------------------------------------------------------------|
| 1 | **extensionAmount** | Determines the period of time to extend by, in years. Defaults to 1 year. |
| 2 | **debug** | Increases logging detail. Defaults to False. |
| 3 | **limitParse** | Determines how many domains to parse. Defaults to all. |
| 4 | **disableIdempotentCheck** | Boolean that determines if we should check for idempotence or not. Compares the proposed extension date to the value in TransitionDomains. Defaults to False. |

View file

@ -19,7 +19,6 @@
"http://localhost:8080/register/other_contacts/",
"http://localhost:8080/register/anything_else/",
"http://localhost:8080/register/requirements/",
"http://localhost:8080/register/review/",
"http://localhost:8080/register/finished/"
]
}

View file

@ -91,15 +91,17 @@ def available(request, domain=""):
# validate that the given domain could be a domain name and fail early if
# not.
if not (DraftDomain.string_could_be_domain(domain) or DraftDomain.string_could_be_domain(domain + ".gov")):
return JsonResponse({"available": False, "message": DOMAIN_API_MESSAGES["invalid"]})
return JsonResponse({"available": False, "code": "invalid", "message": DOMAIN_API_MESSAGES["invalid"]})
# a domain is available if it is NOT in the list of current domains
try:
if check_domain_available(domain):
return JsonResponse({"available": True, "message": DOMAIN_API_MESSAGES["success"]})
return JsonResponse({"available": True, "code": "success", "message": DOMAIN_API_MESSAGES["success"]})
else:
return JsonResponse({"available": False, "message": DOMAIN_API_MESSAGES["unavailable"]})
return JsonResponse(
{"available": False, "code": "unavailable", "message": DOMAIN_API_MESSAGES["unavailable"]}
)
except Exception:
return JsonResponse({"available": False, "message": DOMAIN_API_MESSAGES["error"]})
return JsonResponse({"available": False, "code": "error", "message": DOMAIN_API_MESSAGES["error"]})
@require_http_methods(["GET"])

View file

@ -46,8 +46,14 @@ class OpenIdConnectBackend(ModelBackend):
# defaults _will_ be updated, these are not fallbacks
"defaults": openid_data,
}
user, created = UserModel.objects.update_or_create(**args)
if created:
user, created = UserModel.objects.get_or_create(**args)
if not created:
# If user exists, update existing user
self.update_existing_user(user, args["defaults"])
else:
# If user is created, configure the user
user = self.configure_user(user, **kwargs)
else:
try:
@ -58,6 +64,16 @@ class OpenIdConnectBackend(ModelBackend):
user.on_each_login()
return user
def update_existing_user(self, user, kwargs):
"""Update other fields without overwriting first_name and last_name.
Overwrite first_name and last_name if not empty string"""
for key, value in kwargs.items():
# Check if the key is not first_name or last_name or value is not empty string
if key not in ["first_name", "last_name"] or value:
setattr(user, key, value)
user.save()
def clean_username(self, username):
"""
Performs any cleaning on the "username" prior to using it to get or

View file

@ -0,0 +1,99 @@
from django.test import TestCase
from registrar.models import User
from ..backends import OpenIdConnectBackend # Adjust the import path based on your project structure
class OpenIdConnectBackendTestCase(TestCase):
def setUp(self):
self.backend = OpenIdConnectBackend()
self.kwargs = {
"sub": "test_user",
"given_name": "John",
"family_name": "Doe",
"email": "john.doe@example.com",
"phone": "123456789",
}
def tearDown(self) -> None:
User.objects.all().delete()
def test_authenticate_with_create_user(self):
"""Test that authenticate creates a new user if it does not find
existing user"""
# Ensure that the authenticate method creates a new user
user = self.backend.authenticate(request=None, **self.kwargs)
self.assertIsNotNone(user)
self.assertIsInstance(user, User)
self.assertEqual(user.username, "test_user")
# Verify that user fields are correctly set
self.assertEqual(user.first_name, "John")
self.assertEqual(user.last_name, "Doe")
self.assertEqual(user.email, "john.doe@example.com")
self.assertEqual(user.phone, "123456789")
def test_authenticate_with_existing_user(self):
"""Test that authenticate updates an existing user if it finds one.
For this test, given_name and family_name are supplied"""
# Create an existing user with the same username
existing_user = User.objects.create_user(username="test_user")
# Ensure that the authenticate method updates the existing user
user = self.backend.authenticate(request=None, **self.kwargs)
self.assertIsNotNone(user)
self.assertIsInstance(user, User)
self.assertEqual(user, existing_user) # The same user instance should be returned
# Verify that user fields are correctly updated
self.assertEqual(user.first_name, "John")
self.assertEqual(user.last_name, "Doe")
self.assertEqual(user.email, "john.doe@example.com")
self.assertEqual(user.phone, "123456789")
def test_authenticate_with_existing_user_no_name(self):
"""Test that authenticate updates an existing user if it finds one.
For this test, given_name and family_name are not supplied"""
# Create an existing user with the same username and with first and last names
existing_user = User.objects.create_user(username="test_user", first_name="John", last_name="Doe")
# Remove given_name and family_name from the input, self.kwargs
self.kwargs.pop("given_name", None)
self.kwargs.pop("family_name", None)
# Ensure that the authenticate method updates the existing user
# and preserves existing first and last names
user = self.backend.authenticate(request=None, **self.kwargs)
self.assertIsNotNone(user)
self.assertIsInstance(user, User)
self.assertEqual(user, existing_user) # The same user instance should be returned
# Verify that user fields are correctly updated
self.assertEqual(user.first_name, "John")
self.assertEqual(user.last_name, "Doe")
self.assertEqual(user.email, "john.doe@example.com")
self.assertEqual(user.phone, "123456789")
def test_authenticate_with_existing_user_different_name(self):
"""Test that authenticate updates an existing user if it finds one.
For this test, given_name and family_name are supplied and overwrite"""
# Create an existing user with the same username and with first and last names
existing_user = User.objects.create_user(username="test_user", first_name="WillBe", last_name="Replaced")
# Ensure that the authenticate method updates the existing user
# and preserves existing first and last names
user = self.backend.authenticate(request=None, **self.kwargs)
self.assertIsNotNone(user)
self.assertIsInstance(user, User)
self.assertEqual(user, existing_user) # The same user instance should be returned
# Verify that user fields are correctly updated
self.assertEqual(user.first_name, "John")
self.assertEqual(user.last_name, "Doe")
self.assertEqual(user.email, "john.doe@example.com")
self.assertEqual(user.phone, "123456789")
def test_authenticate_with_unknown_user(self):
"""Test that authenticate returns None when no kwargs are supplied"""
# Ensure that the authenticate method handles the case when the user is not found
user = self.backend.authenticate(request=None, **{})
self.assertIsNone(user)

View file

@ -786,6 +786,7 @@ class TransitionDomainAdmin(ListHeaderAdmin):
"domain_name",
"status",
"email_sent",
"processed",
]
search_fields = ["username", "domain_name"]

View file

@ -28,6 +28,17 @@ class DomainAddUserForm(forms.Form):
email = forms.EmailField(label="Email")
def clean(self):
"""clean form data by lowercasing email"""
cleaned_data = super().clean()
# Lowercase the value of the 'email' field
email_value = cleaned_data.get("email")
if email_value:
cleaned_data["email"] = email_value.lower()
return cleaned_data
class DomainNameserverForm(forms.Form):
"""Form for changing nameservers."""

View file

@ -0,0 +1,235 @@
import logging
import argparse
import sys
from django.core.management import BaseCommand
from registrar.management.commands.utility.terminal_helper import (
TerminalColors,
TerminalHelper,
)
from registrar.models.contact import Contact
from registrar.models.user import User
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = """Copy first and last names from a contact to
a related user if it exists and if its first and last name
properties are null or blank strings."""
# ======================================================
# ===================== ARGUMENTS =====================
# ======================================================
def add_arguments(self, parser):
parser.add_argument("--debug", action=argparse.BooleanOptionalAction)
# ======================================================
# ===================== PRINTING ======================
# ======================================================
def print_debug_mode_statements(self, debug_on: bool):
"""Prints additional terminal statements to indicate if --debug
or --limitParse are in use"""
TerminalHelper.print_conditional(
debug_on,
f"""{TerminalColors.OKCYAN}
----------DEBUG MODE ON----------
Detailed print statements activated.
{TerminalColors.ENDC}
""",
)
def print_summary_of_findings(
self,
skipped_contacts,
eligible_users,
processed_users,
debug_on,
):
"""Prints to terminal a summary of findings from
copying first and last names from contacts to users"""
total_eligible_users = len(eligible_users)
total_skipped_contacts = len(skipped_contacts)
total_processed_users = len(processed_users)
logger.info(
f"""{TerminalColors.OKGREEN}
============= FINISHED ===============
Skipped {total_skipped_contacts} contacts
Found {total_eligible_users} users linked to contacts
Processed {total_processed_users} users
{TerminalColors.ENDC}
""" # noqa
)
# DEBUG:
TerminalHelper.print_conditional(
debug_on,
f"""{TerminalColors.YELLOW}
======= DEBUG OUTPUT =======
Users who have a linked contact:
{eligible_users}
Processed users (users who have a linked contact and a missing first or last name):
{processed_users}
===== SKIPPED CONTACTS =====
{skipped_contacts}
{TerminalColors.ENDC}
""",
)
# ======================================================
# =================== USER =====================
# ======================================================
def update_user(self, contact: Contact, debug_on: bool):
"""Given a contact with a first_name and last_name, find & update an existing
corresponding user if her first_name and last_name are null.
Returns tuple of eligible (is linked to the contact) and processed
(first and last are blank) users.
"""
user_exists = User.objects.filter(contact=contact).exists()
if user_exists:
try:
# ----------------------- UPDATE USER -----------------------
# ---- GET THE USER
eligible_user = User.objects.get(contact=contact)
processed_user = None
# DEBUG:
TerminalHelper.print_conditional(
debug_on,
f"""{TerminalColors.YELLOW}
> Found linked user for contact:
{contact} {contact.email} {contact.first_name} {contact.last_name}
> The linked user is {eligible_user} {eligible_user.username}
{TerminalColors.ENDC}""", # noqa
)
# ---- UPDATE THE USER IF IT DOES NOT HAVE A FIRST AND LAST NAMES
# ---- LET'S KEEP A LIGHT TOUCH
if not eligible_user.first_name and not eligible_user.last_name:
# (expression has type "str | None", variable has type "str | int | Combinable")
# so we'll ignore type
eligible_user.first_name = contact.first_name # type: ignore
eligible_user.last_name = contact.last_name # type: ignore
eligible_user.save()
processed_user = eligible_user
return (
eligible_user,
processed_user,
)
except Exception as error:
logger.warning(
f"""
{TerminalColors.FAIL}
!!! ERROR: An exception occured in the
User table for the following user:
{contact.email} {contact.first_name} {contact.last_name}
Exception is: {error}
----------TERMINATING----------"""
)
sys.exit()
else:
return None, None
# ======================================================
# ================= PROCESS CONTACTS ==================
# ======================================================
def process_contacts(
self,
debug_on,
skipped_contacts=[],
eligible_users=[],
processed_users=[],
):
for contact in Contact.objects.all():
TerminalHelper.print_conditional(
debug_on,
f"{TerminalColors.OKCYAN}"
"Processing Contact: "
f"{contact.email},"
f" {contact.first_name},"
f" {contact.last_name}"
f"{TerminalColors.ENDC}",
)
# ======================================================
# ====================== USER =======================
(eligible_user, processed_user) = self.update_user(contact, debug_on)
debug_string = ""
if eligible_user:
# ---------------- UPDATED ----------------
eligible_users.append(contact.email)
debug_string = f"eligible user: {eligible_user}"
if processed_user:
processed_users.append(contact.email)
debug_string = f"processed user: {processed_user}"
else:
skipped_contacts.append(contact.email)
debug_string = f"skipped user: {contact.email}"
# DEBUG:
TerminalHelper.print_conditional(
debug_on,
(f"{TerminalColors.OKCYAN} {debug_string} {TerminalColors.ENDC}"),
)
return (
skipped_contacts,
eligible_users,
processed_users,
)
# ======================================================
# ===================== HANDLE ========================
# ======================================================
def handle(
self,
**options,
):
"""Parse entries in Contact table
and update valid corresponding entries in the
User table."""
# grab command line arguments and store locally...
debug_on = options.get("debug")
self.print_debug_mode_statements(debug_on)
logger.info(
f"""{TerminalColors.OKCYAN}
==========================
Beginning Data Transfer
==========================
{TerminalColors.ENDC}"""
)
logger.info(
f"""{TerminalColors.OKCYAN}
========= Adding Domains and Domain Invitations =========
{TerminalColors.ENDC}"""
)
(
skipped_contacts,
eligible_users,
processed_users,
) = self.process_contacts(
debug_on,
)
self.print_summary_of_findings(
skipped_contacts,
eligible_users,
processed_users,
debug_on,
)

View file

@ -0,0 +1,212 @@
"""Data migration: Extends expiration dates for valid domains"""
import argparse
from datetime import date
import logging
from django.core.management import BaseCommand
from epplibwrapper.errors import RegistryError
from registrar.models import Domain
from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper
from registrar.models.transition_domain import TransitionDomain
try:
from epplib.exceptions import TransportError
except ImportError:
pass
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Extends expiration dates for valid domains"
def __init__(self):
"""Sets global variables for code tidyness"""
super().__init__()
self.update_success = []
self.update_skipped = []
self.update_failed = []
self.expiration_minimum_cutoff = date(2023, 11, 15)
self.expiration_maximum_cutoff = date(2023, 12, 30)
def add_arguments(self, parser):
"""Add command line arguments."""
parser.add_argument(
"--extensionAmount",
type=int,
default=1,
help="Determines the period (in years) to extend expiration dates by",
)
parser.add_argument(
"--limitParse",
type=int,
default=0,
help="Sets a cap on the number of records to parse",
)
parser.add_argument(
"--disableIdempotentCheck", action=argparse.BooleanOptionalAction, help="Disable script idempotence"
)
parser.add_argument("--debug", action=argparse.BooleanOptionalAction, help="Increases log chattiness")
def handle(self, **options):
"""
Extends the expiration dates for valid domains.
If a parse limit is set and it's less than the total number of valid domains,
the number of domains to change is set to the parse limit.
Includes an idempotence check.
"""
# Retrieve command line options
extension_amount = options.get("extensionAmount")
limit_parse = options.get("limitParse")
disable_idempotence = options.get("disableIdempotentCheck")
debug = options.get("debug")
# Does a check to see if parse_limit is a positive int.
# Raise an error if not.
self.check_if_positive_int(limit_parse, "limitParse")
valid_domains = Domain.objects.filter(
expiration_date__gte=self.expiration_minimum_cutoff,
expiration_date__lte=self.expiration_maximum_cutoff,
state=Domain.State.READY,
).order_by("name")
domains_to_change_count = valid_domains.count()
if limit_parse != 0:
domains_to_change_count = limit_parse
valid_domains = valid_domains[:limit_parse]
# Determines if we should continue code execution or not.
# If the user prompts 'N', a sys.exit() will be called.
self.prompt_user_to_proceed(extension_amount, domains_to_change_count)
for domain in valid_domains:
try:
is_idempotent = self.idempotence_check(domain, extension_amount)
if not disable_idempotence and not is_idempotent:
self.update_skipped.append(domain.name)
logger.info(f"{TerminalColors.YELLOW}" f"Skipping update for {domain}" f"{TerminalColors.ENDC}")
else:
domain.renew_domain(extension_amount)
self.update_success.append(domain.name)
logger.info(
f"{TerminalColors.OKCYAN}"
f"Successfully updated expiration date for {domain}"
f"{TerminalColors.ENDC}"
)
# Catches registry errors. Failures indicate bad data, or a faulty connection.
except (RegistryError, KeyError, TransportError) as err:
self.update_failed.append(domain.name)
logger.error(
f"{TerminalColors.FAIL}" f"Failed to update expiration date for {domain}" f"{TerminalColors.ENDC}"
)
logger.error(err)
except Exception as err:
self.log_script_run_summary(debug)
raise err
self.log_script_run_summary(debug)
# == Helper functions == #
def idempotence_check(self, domain: Domain, extension_amount):
"""Determines if the proposed operation violates idempotency"""
# Because our migration data had a hard stop date, we can determine if our change
# is valid simply checking the date is within a valid range and it was updated
# in epp or not.
# CAVEAT: This is a workaround. A more robust solution would be a db flag
current_expiration_date = domain.registry_expiration_date
transition_domains = TransitionDomain.objects.filter(
domain_name=domain.name, epp_expiration_date=current_expiration_date
)
return transition_domains.count() > 0
def prompt_user_to_proceed(self, extension_amount, domains_to_change_count):
"""Asks if the user wants to proceed with this action"""
TerminalHelper.prompt_for_execution(
system_exit_on_terminate=True,
info_to_inspect=f"""
==Extension Amount==
Period: {extension_amount} year(s)
==Proposed Changes==
Domains to change: {domains_to_change_count}
""",
prompt_title="Do you wish to proceed with these changes?",
)
logger.info(f"{TerminalColors.MAGENTA}" "Preparing to extend expiration dates..." f"{TerminalColors.ENDC}")
def check_if_positive_int(self, value: int, var_name: str):
"""
Determines if the given integer value is positive or not.
If not, it raises an ArgumentTypeError
"""
if value < 0:
raise argparse.ArgumentTypeError(
f"{value} is an invalid integer value for {var_name}. " "Must be positive."
)
return value
def log_script_run_summary(self, debug):
"""Prints success, failed, and skipped counts, as well as
all affected domains."""
update_success_count = len(self.update_success)
update_failed_count = len(self.update_failed)
update_skipped_count = len(self.update_skipped)
# Prepare debug messages
debug_messages = {
"success": (f"{TerminalColors.OKCYAN}Updated these Domains: {self.update_success}{TerminalColors.ENDC}\n"),
"skipped": (f"{TerminalColors.YELLOW}Skipped these Domains: {self.update_skipped}{TerminalColors.ENDC}\n"),
"failed": (
f"{TerminalColors.FAIL}Failed to update these Domains: {self.update_failed}{TerminalColors.ENDC}\n"
),
}
# Print out a list of everything that was changed, if we have any changes to log.
# Otherwise, don't print anything.
TerminalHelper.print_conditional(
debug,
f"{debug_messages.get('success') if update_success_count > 0 else ''}"
f"{debug_messages.get('skipped') if update_skipped_count > 0 else ''}"
f"{debug_messages.get('failed') if update_failed_count > 0 else ''}",
)
if update_failed_count == 0 and update_skipped_count == 0:
logger.info(
f"""{TerminalColors.OKGREEN}
============= FINISHED ===============
Updated {update_success_count} Domain entries
{TerminalColors.ENDC}
"""
)
elif update_failed_count == 0:
logger.info(
f"""{TerminalColors.YELLOW}
============= FINISHED ===============
Updated {update_success_count} Domain entries
----- IDEMPOTENCY CHECK FAILED -----
Skipped updating {update_skipped_count} Domain entries
{TerminalColors.ENDC}
"""
)
else:
logger.info(
f"""{TerminalColors.FAIL}
============= FINISHED ===============
Updated {update_success_count} Domain entries
----- UPDATE FAILED -----
Failed to update {update_failed_count} Domain entries,
Skipped updating {update_skipped_count} Domain entries
{TerminalColors.ENDC}
"""
)

View file

@ -536,8 +536,8 @@ class Command(BaseCommand):
domain_name=new_entry_domain_name,
)
if not existing_entry.processed:
if existing_entry.status != new_entry_status:
# DEBUG:
TerminalHelper.print_conditional(
debug_on,
f"{TerminalColors.OKCYAN}"
@ -549,6 +549,14 @@ class Command(BaseCommand):
existing_entry.status = new_entry_status
existing_entry.email_sent = new_entry_emailSent
existing_entry.save()
else:
TerminalHelper.print_conditional(
debug_on,
f"{TerminalColors.YELLOW}"
f"Skipping update on processed domain: {existing_entry}"
f"{TerminalColors.ENDC}",
)
except TransitionDomain.MultipleObjectsReturned:
logger.info(
f"{TerminalColors.FAIL}"
@ -558,6 +566,7 @@ class Command(BaseCommand):
f"----------TERMINATING----------"
)
sys.exit()
else:
# no matching entry, make one
new_entry = TransitionDomain(
@ -565,6 +574,7 @@ class Command(BaseCommand):
domain_name=new_entry_domain_name,
status=new_entry_status,
email_sent=new_entry_emailSent,
processed=False,
)
to_create.append(new_entry)
total_new_entries += 1

View file

@ -559,7 +559,8 @@ class Command(BaseCommand):
debug_max_entries_to_parse,
total_rows_parsed,
):
for transition_domain in TransitionDomain.objects.all():
changed_transition_domains = TransitionDomain.objects.filter(processed=False)
for transition_domain in changed_transition_domains:
(
target_domain_information,
associated_domain,
@ -644,7 +645,8 @@ class Command(BaseCommand):
debug_max_entries_to_parse,
total_rows_parsed,
):
for transition_domain in TransitionDomain.objects.all():
changed_transition_domains = TransitionDomain.objects.filter(processed=False)
for transition_domain in changed_transition_domains:
# Create some local variables to make data tracing easier
transition_domain_name = transition_domain.domain_name
transition_domain_status = transition_domain.status
@ -796,6 +798,7 @@ class Command(BaseCommand):
# First, save all Domain objects to the database
Domain.objects.bulk_create(domains_to_create)
# DomainInvitation.objects.bulk_create(domain_invitations_to_create)
# TODO: this is to resolve an error where bulk_create
@ -847,6 +850,15 @@ class Command(BaseCommand):
)
DomainInformation.objects.bulk_create(domain_information_to_create)
# Loop through the list of everything created, and mark it as processed
for domain in domains_to_create:
name = domain.name
TransitionDomain.objects.filter(domain_name=name).update(processed=True)
# Loop through the list of everything updated, and mark it as processed
for name in updated_domain_entries:
TransitionDomain.objects.filter(domain_name=name).update(processed=True)
self.print_summary_of_findings(
domains_to_create,
updated_domain_entries,

View file

@ -155,13 +155,13 @@ class LoadExtraTransitionDomain:
def update_transition_domain_models(self):
"""Updates TransitionDomain objects based off the file content
given in self.parsed_data_container"""
all_transition_domains = TransitionDomain.objects.all()
if not all_transition_domains.exists():
raise ValueError("No TransitionDomain objects exist.")
valid_transition_domains = TransitionDomain.objects.filter(processed=False)
if not valid_transition_domains.exists():
raise ValueError("No updatable TransitionDomain objects exist.")
updated_transition_domains = []
failed_transition_domains = []
for transition_domain in all_transition_domains:
for transition_domain in valid_transition_domains:
domain_name = transition_domain.domain_name
updated_transition_domain = transition_domain
try:
@ -228,7 +228,7 @@ class LoadExtraTransitionDomain:
# DATA INTEGRITY CHECK
# Make sure every Transition Domain got updated
total_transition_domains = len(updated_transition_domains)
total_updates_made = TransitionDomain.objects.all().count()
total_updates_made = TransitionDomain.objects.filter(processed=False).count()
if total_transition_domains != total_updates_made:
# noqa here for line length
logger.error(
@ -787,7 +787,7 @@ class OrganizationDataLoader:
self.tds_to_update: List[TransitionDomain] = []
def update_organization_data_for_all(self):
"""Updates org address data for all TransitionDomains"""
"""Updates org address data for valid TransitionDomains"""
all_transition_domains = TransitionDomain.objects.all()
if len(all_transition_domains) == 0:
raise LoadOrganizationError(code=LoadOrganizationErrorCodes.EMPTY_TRANSITION_DOMAIN_TABLE)

View file

@ -0,0 +1,21 @@
# Generated by Django 4.2.7 on 2023-12-12 21:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0054_alter_domainapplication_federal_agency_and_more"),
]
operations = [
migrations.AddField(
model_name="transitiondomain",
name="processed",
field=models.BooleanField(
default=True,
help_text="Indicates whether this TransitionDomain was already processed",
verbose_name="Processed",
),
),
]

View file

@ -6,7 +6,7 @@ import django_fsm
class Migration(migrations.Migration):
dependencies = [
("registrar", "0054_alter_domainapplication_federal_agency_and_more"),
("registrar", "0055_transitiondomain_processed"),
]
operations = [

View file

@ -59,6 +59,16 @@ class Contact(TimeStampedModel):
names = [n for n in [self.first_name, self.middle_name, self.last_name] if n]
return " ".join(names) if names else "Unknown"
def save(self, *args, **kwargs):
# Call the parent class's save method to perform the actual save
super().save(*args, **kwargs)
# Update the related User object's first_name and last_name
if self.user:
self.user.first_name = self.first_name
self.user.last_name = self.last_name
self.user.save()
def __str__(self):
if self.first_name or self.last_name:
return self.get_formatted_name()

View file

@ -1195,7 +1195,7 @@ class Domain(TimeStampedModel, DomainHelper):
logger.error(e)
logger.error(e.code)
raise e
if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST and self.state != Domain.State.DELETED:
if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST and self.state == Domain.State.UNKNOWN:
# avoid infinite loop
already_tried_to_create = True
self.dns_needed_from_unknown()

View file

@ -122,7 +122,7 @@ class DomainApplication(TimeStampedModel):
FEDERAL = (
"federal",
"Federal: an agency of the U.S. government's executive, legislative, or judicial branches",
"Federal: an agency of the U.S. governments legislative, executive, or judicial branches",
)
INTERSTATE = "interstate", "Interstate: an organization of two or more states"
STATE_OR_TERRITORY = (
@ -139,7 +139,7 @@ class DomainApplication(TimeStampedModel):
CITY = "city", "City: a city, town, township, village, etc."
SPECIAL_DISTRICT = (
"special_district",
"Special district: an independent organization within a single state",
"Special district: an independent government that delivers specialized, essential services",
)
SCHOOL_DISTRICT = (
"school_district",

View file

@ -43,6 +43,12 @@ class TransitionDomain(TimeStampedModel):
verbose_name="email sent",
help_text="indicates whether email was sent",
)
processed = models.BooleanField(
null=False,
default=True,
verbose_name="Processed",
help_text="Indicates whether this TransitionDomain was already processed",
)
organization_type = models.TextField(
max_length=255,
null=True,

View file

@ -101,7 +101,7 @@ class User(AbstractUser):
"""When a user first arrives on the site, we need to retrieve any domain
invitations that match their email address."""
for invitation in DomainInvitation.objects.filter(
email=self.email, status=DomainInvitation.DomainInvitationStatus.INVITED
email__iexact=self.email, status=DomainInvitation.DomainInvitationStatus.INVITED
):
try:
invitation.retrieve()

View file

@ -85,16 +85,29 @@
class="usa-button usa-button--outline"
>Save and return to manage your domains</button>
{% else %}
<button
type="submit"
<a
href="#toggle-submit-domain-request"
class="usa-button usa-button--big dotgov-button--green"
>Submit your domain request</button>
aria-controls="toggle-submit-domain-request"
data-open-modal
>Submit your domain request</a
>
{% endif %}
</div>
{% endblock %}
</form>
<div
class="usa-modal"
id="toggle-submit-domain-request"
aria-labelledby="Are you sure you want to submit a domain request?"
aria-describedby="Are you sure you want to submit a domain request?"
data-force-action
>
{% include 'includes/modal.html' with modal_heading=modal_heading|safe modal_description="Once you submit this request, you wont be able to make further edits until its reviewed by our staff. Youll only be able to withdraw your request." modal_button=modal_button|safe %}
</div>
{% block after_form_content %}{% endblock %}
</main>

View file

@ -12,9 +12,11 @@
<p>You can enter your name servers, as well as other DNS-related information, in the following sections:</p>
{% url 'domain-dns-nameservers' pk=domain.id as url %}
<p><a href="{{ url }}">DNS name servers</a></p>
<ul class="usa-list">
<li><a href="{{ url }}">Name servers</a></li>
{% url 'domain-dns-dnssec' pk=domain.id as url %}
<p><a href="{{ url }}">DNSSEC</a></p>
<li><a href="{{ url }}">DNSSEC</a></li>
</ul>
{% endblock %} {# domain_content #}

View file

@ -14,7 +14,7 @@ Now that your .gov domain has been approved, there are a few more things to do b
YOU MUST ADD DOMAIN NAME SERVER INFORMATION
Before your .gov domain can be used, you have to connect it to your Domain Name System (DNS) hosting service. At this time, we dont provide DNS hosting services.
Go to the domain management page to add your domain name server information <https://registrar.get.gov/domain/{{ application.id }}/nameservers>.
Go to the domain management page to add your domain name server information <https://manage.get.gov/domain/{{ application.approved_domain.id }}/nameservers>.
Get help with adding your domain name server information <https://get.gov/help/domain-management/#manage-dns-information-for-your-domain>.
@ -23,7 +23,7 @@ ADD DOMAIN MANAGERS, SECURITY EMAIL
We strongly recommend that you add other points of contact who will help manage your domain. We also recommend that you provide a security email. This email will allow the public to report security issues on your domain. Security emails are made public.
Go to the domain management page to add domain contacts <https://registrar.get.gov/domain/{{ application.id }}/your-contact-information> and a security email <https://registrar.get.gov/domain/{{ application.id }}/security-email>.
Go to the domain management page to add domain contacts <https://manage.get.gov/domain/{{ application.approved_domain.id }}/your-contact-information> and a security email <https://manage.get.gov/domain/{{ application.approved_domain.id }}/security-email>.
Get help with managing your .gov domain <https://get.gov/help/domain-management/>.

View file

@ -22,7 +22,7 @@ NEXT STEPS
- Were reviewing your request. This usually takes 20 business days.
- You can check the status of your request at any time.
<https://registrar.get.gov/application/{{ application.id }}>
<https://manage.get.gov/application/{{ application.id }}>
- Well email you with questions or when we complete our review.

View file

@ -21,7 +21,7 @@ NEXT STEPS
- Well review your request. This usually takes 20 business days.
- You can check the status of your request at any time.
<https://registrar.get.gov/application/{{ application.id }}>
<https://manage.get.gov/application/{{ application.id }}>
- Well email you with questions or when we complete our review.

View file

@ -87,7 +87,7 @@
aria-live="polite"
></div>
{% else %}
<p>You don't have any registered domains yet</p>
<p>You don't have any registered domains.</p>
{% endif %}
</section>
@ -95,7 +95,7 @@
<h2>Domain requests</h2>
{% if domain_applications %}
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
<caption class="sr-only">Your domain applications</caption>
<caption class="sr-only">Your domain requests</caption>
<thead>
<tr>
<th data-sortable scope="col" role="columnheader">Domain name</th>
@ -138,7 +138,7 @@
aria-live="polite"
></div>
{% else %}
<p>You don't have any active domain requests right now</p>
<p>You haven't requested any domains.</p>
<!-- <p><a href="{% url 'application:' %}" class="usa-button">Start a new domain request</a></p> -->
{% endif %}
</section>

View file

@ -1,50 +1,62 @@
{% if is_federal %}
{% if federal_type == 'executive' %}
<p>Domain requests from executive branch agencies must be authorized by <strong>Chief Information Officers</strong> or <strong>agency heads</strong>.</p>
<p>Domain requests from executive branch agencies are subject to guidance issued by the U.S. Office of Management and Budget. </p>
<h3>Executive branch federal agencies</h3>
<p>Domain requests from executive branch federal agencies must be authorized by the agency's CIO or the head of the agency.</p>
<p>See <a class="usa-link usa-link--external" rel="noopener noreferrer" target="_blank" href="https://www.whitehouse.gov/wp-content/uploads/2023/02/M-23-10-DOTGOV-Act-Guidance.pdf">OMB Memorandum M-23-10</a> for more information.</p>
{% elif federal_type == 'judicial' %}
<p>Domain requests from the U.S. Supreme Court must be authorized by the <strong>director of information technology for the U.S. Supreme Court.</strong></p>
<p>Domain requests from other judicial branch agencies must be authorized by the <strong>director</strong> or <strong>Chief Information Officer of the Administrative Office (AO)</strong> of the United States Courts.
</p>
<h3>Judicial branch federal agencies</h3>
<p>Domain requests for judicial branch federal agencies, except the U.S. Supreme Court, must be authorized by the director or CIO of the Administrative Office (AO) of the United States Courts.</p>
<p>Domain requests from the U.S. Supreme Court must be authorized by the director of information technology for the U.S. Supreme Court.</p>
{% elif federal_type == 'legislative' %}
<h3> U.S. Senate </h3>
<p>Domain requests from the U.S. Senate must come from the <strong>Senate Sergeant at Arms.</strong></p>
<h3>Legislative branch federal agencies</h3>
<h3> U.S. House of Representatives </h3>
<p class="">Domain requests from the U.S. House of Representatives must come from the <strong>House Chief Administrative Officer.</strong>
<h4>U.S. Senate</h4>
<p>Domain requests from the U.S. Senate must come from the Senate Sergeant at Arms.</p>
<h3> Other legislative branch agencies </h3>
<p class="margin-top-1">Domain requests from legislative branch agencies must come from the <strong>agencys head</strong> or <strong>Chief Information Officer.</strong></p>
<p class="margin-top-1">Domain requests from legislative commissions must come from the <strong>head of the commission</strong>, or the <strong>head or Chief Information Officer of the parent agency,</strong> if there is one.
</p>
<h4>U.S. House of Representatives</h4>
<p>Domain requests from the U.S. House of Representatives must come from the House Chief Administrative Officer.</p>
<h4>Other legislative branch agencies</h4>
<p>Domain requests from legislative branch agencies must come from the agencys head or CIO.</p>
<p>Domain requests from legislative commissions must come from the head of the commission, or the head or CIO of the parent agency, if there is one.</p>
{% endif %}
{% elif organization_type == 'city' %}
<p>Domain requests from cities must be authorized by <strong>the mayor</strong> or the equivalent <strong>highest-elected official.</strong></p>
<h3>Cities</h3>
<p>Domain requests from cities must be authorized by someone in a role of significant, executive responsibility within the city (mayor, council president, city manager, township/village supervisor, select board chairperson, chief, senior technology officer, or equivalent).</p>
{% elif organization_type == 'county' %}
<p>Domain requests from counties must be authorized by the <strong>chair of the county commission </strong>or <strong>the equivalent highest-elected official.</strong></p>
<h3>Counties</h3>
<p>Domain requests from counties must be authorized by the commission chair or someone in a role of significant, executive responsibility within the county (county judge, county mayor, parish/borough president, senior technology officer, or equivalent). Other county-level offices (county clerk, sheriff, county auditor, comptroller) may qualify, as well, in some instances.</p>
{% elif organization_type == 'interstate' %}
<p>Domain requests from interstate organizations must be authorized by the <strong>highest-ranking executive</strong> (president, director, chair, or equivalent) or <strong>one of the states governors or Chief Information Officers.</strong></p>
<h3>Interstate organizations</h3>
<p>Domain requests from interstate organizations must be authorized by someone in a role of significant, executive responsibility within the organization (president, director, chair, senior technology officer, or equivalent) or one of the states governors or CIOs.</p>
{% elif organization_type == 'school_district' %}
<p>Domain requests from school district governments must be authorized by the <strong>highest-ranking executive (the chair of a school districts board or a superintendent)</strong>.</p>
<h3>School districts</h3>
<p>Domain requests from school district governments must be authorized by someone in a role of significant, executive responsibility within the district (board chair, superintendent, senior technology officer, or equivalent).</p>
{% elif organization_type == 'special_district' %}
<p>Domain requests from special districts must be authorized by the <strong>highest-ranking executive (president, director, chair, or equivalent)</strong> or <strong>state Chief Information Officers for state-based organizations</strong>.</p>
<h3>Special districts</h3>
<p>Domain requests from special districts must be authorized by someone in a role of significant, executive responsibility within the district (CEO, chair, executive director, senior technology officer, or equivalent).
</p>
{% elif organization_type == 'state_or_territory' %}
<h3>States and territories: executive branch</h3>
<p>Domain requests from states and territories must be authorized by the <strong>governor</strong> or the <strong>state Chief Information Officer.</strong></p>
<h3>States and territories: judicial and legislative branches</h3>
<p>Domain requests from state legislatures and courts must be authorized by an agencys <strong>Chief Information Officer</strong> or <strong>highest-ranking executive</strong>.</p>
<h3>U.S. states and territories</h3>
<h4>States and territories: executive branch</h4>
<p>Domain requests from states and territories must be authorized by the governor or someone in a role of significant, executive responsibility within the agency (department secretary, senior technology officer, or equivalent).</p>
<h4>States and territories: judicial and legislative branches</h4>
<p>Domain requests from state legislatures and courts must be authorized by an agencys CIO or someone in a role of significant, executive responsibility within the agency.</p>
{% elif organization_type == 'tribal' %}
<p><strong>Domain requests from federally-recognized tribal governments must be authorized by the leader of the tribe</strong>, as recognized by the <a class="usa-link usa-link--external" rel="noopener noreferrer" target="_blank" href="https://www.bia.gov/service/tribal-leaders-directory">Bureau of Indian Affairs</a>.</p>
<p><strong>Domain requests from state-recognized tribal governments must be authorized by the leader of the tribe</strong>, as determined by the states tribal recognition initiative.</p>
<h3>Tribal governments</h3>
<p>Domain requests from federally-recognized tribal governments must be authorized by the tribal leader the <a class="usa-link usa-link--external" rel="noopener noreferrer" target="_blank" href="https://www.bia.gov/service/tribal-leaders-directory">Bureau of Indian Affairs</a> recognizes.</p>
<p>Domain requests from state-recognized tribal governments must be authorized by the tribal leader the individual state recognizes.</p>
{% endif %}

View file

@ -5,6 +5,7 @@ import logging
from contextlib import contextmanager
import random
from string import ascii_uppercase
import uuid
from django.test import TestCase
from unittest.mock import MagicMock, Mock, patch
from typing import List, Dict
@ -161,7 +162,7 @@ class AuditedAdminMockData:
user = User.objects.get_or_create(
first_name="{} first_name:{}".format(item_name, short_hand),
last_name="{} last_name:{}".format(item_name, short_hand),
username="{} username:{}".format(item_name, short_hand),
username="{} username:{}".format(item_name + str(uuid.uuid4())[:8], short_hand),
)[0]
return user
@ -405,8 +406,8 @@ def mock_user():
"""A simple user."""
user_kwargs = dict(
id=4,
first_name="Rachid",
last_name="Mrad",
first_name="Jeff",
last_name="Lebowski",
)
mock_user, _ = User.objects.get_or_create(**user_kwargs)
return mock_user
@ -619,6 +620,17 @@ class MockEppLib(TestCase):
],
ex_date=datetime.date(2023, 5, 25),
)
mockDataExtensionDomain = fakedEppObject(
"fakePw",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
hosts=["fake.host.com"],
statuses=[
common.Status(state="serverTransferProhibited", description="", lang="en"),
common.Status(state="inactive", description="", lang="en"),
],
ex_date=datetime.date(2023, 11, 15),
)
mockDataInfoContact = mockDataInfoDomain.dummyInfoContactResultData(
"123", "123@mail.gov", datetime.datetime(2023, 5, 25, 19, 45, 35), "lastPw"
)
@ -819,6 +831,21 @@ class MockEppLib(TestCase):
ex_date=datetime.date(2023, 5, 25),
)
mockDnsNeededRenewedDomainExpDate = fakedEppObject(
"fakeneeded.gov",
ex_date=datetime.date(2023, 2, 15),
)
mockMaximumRenewedDomainExpDate = fakedEppObject(
"fakemaximum.gov",
ex_date=datetime.date(2024, 12, 31),
)
mockRecentRenewedDomainExpDate = fakedEppObject(
"waterbutpurple.gov",
ex_date=datetime.date(2024, 11, 15),
)
def _mockDomainName(self, _name, _avail=False):
return MagicMock(
res_data=[
@ -917,6 +944,21 @@ class MockEppLib(TestCase):
def mockRenewDomainCommand(self, _request, cleaned):
if getattr(_request, "name", None) == "fake-error.gov":
raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR)
elif getattr(_request, "name", None) == "waterbutpurple.gov":
return MagicMock(
res_data=[self.mockRecentRenewedDomainExpDate],
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
)
elif getattr(_request, "name", None) == "fakeneeded.gov":
return MagicMock(
res_data=[self.mockDnsNeededRenewedDomainExpDate],
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
)
elif getattr(_request, "name", None) == "fakemaximum.gov":
return MagicMock(
res_data=[self.mockMaximumRenewedDomainExpDate],
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
)
else:
return MagicMock(
res_data=[self.mockRenewedDomainExpDate],
@ -942,6 +984,7 @@ class MockEppLib(TestCase):
self.infoDomainTwoHosts if self.mockedSendFunction.call_count == 5 else self.infoDomainNoHost,
None,
),
"waterbutpurple.gov": (self.mockDataExtensionDomain, None),
"nameserverwithip.gov": (self.infoDomainHasIP, None),
"namerserversubdomain.gov": (self.infoDomainCheckHostIPCombo, None),
"freeman.gov": (self.InfoDomainWithContacts, None),

View file

@ -1131,7 +1131,7 @@ class ListHeaderAdminTest(TestCase):
# Set the GET parameters for testing
request.GET = {
"status": "started",
"investigator": "Rachid Mrad",
"investigator": "Jeff Lebowski",
"q": "search_value",
}
# Call the get_filters method
@ -1142,7 +1142,7 @@ class ListHeaderAdminTest(TestCase):
filters,
[
{"parameter_name": "status", "parameter_value": "started"},
{"parameter_name": "investigator", "parameter_value": "Rachid Mrad"},
{"parameter_name": "investigator", "parameter_value": "Jeff Lebowski"},
],
)
@ -1218,8 +1218,8 @@ class AuditedAdminTest(TestCase):
tested_fields = [
DomainApplication.authorizing_official.field,
DomainApplication.submitter.field,
DomainApplication.investigator.field,
DomainApplication.creator.field,
# DomainApplication.investigator.field,
# DomainApplication.creator.field,
DomainApplication.requested_domain.field,
]
@ -1274,7 +1274,7 @@ class AuditedAdminTest(TestCase):
tested_fields = [
DomainInformation.authorizing_official.field,
DomainInformation.submitter.field,
DomainInformation.creator.field,
# DomainInformation.creator.field,
(DomainInformation.domain.field, ["name"]),
(DomainInformation.domain_application.field, ["requested_domain__name"]),
]

View file

@ -0,0 +1,98 @@
from django.test import TestCase
from registrar.models import (
User,
Contact,
)
from registrar.management.commands.copy_names_from_contacts_to_users import Command
class TestDataUpdates(TestCase):
def setUp(self):
"""We cannot setup the user details because contacts will override the first and last names in its save method
so we will initiate the users, setup the contacts and link them, and leave the rest of the setup to the test(s).
"""
self.user1 = User.objects.create(username="user1")
self.user2 = User.objects.create(username="user2")
self.user3 = User.objects.create(username="user3")
self.user4 = User.objects.create(username="user4")
# The last user created triggers the creation of a contact and attaches itself to it. See signals.
# This bs_user defuses that situation.
self.bs_user = User.objects.create()
self.contact1 = Contact.objects.create(
user=self.user1, email="email1@igorville.gov", first_name="first1", last_name="last1"
)
self.contact2 = Contact.objects.create(
user=self.user2, email="email2@igorville.gov", first_name="first2", last_name="last2"
)
self.contact3 = Contact.objects.create(
user=self.user3, email="email3@igorville.gov", first_name="first3", last_name="last3"
)
self.contact4 = Contact.objects.create(email="email4@igorville.gov", first_name="first4", last_name="last4")
self.command = Command()
def tearDown(self):
"""Clean up"""
# Delete users and contacts
User.objects.all().delete()
Contact.objects.all().delete()
def test_script_updates_linked_users(self):
"""Test the script that copies contacts' first and last names into associated users that
are eligible (first or last are blank or undefined)"""
# Set up the users' first and last names here so
# they that they don't get overwritten by Contact's save()
# User with no first or last names
self.user1.first_name = ""
self.user1.last_name = ""
self.user1.save()
# User with a first name but no last name
self.user2.first_name = "First name but no last name"
self.user2.last_name = ""
self.user2.save()
# User with a first and last name
self.user3.first_name = "An existing first name"
self.user3.last_name = "An existing last name"
self.user3.save()
# Call the parent method the same way we do it in the script
skipped_contacts = []
eligible_users = []
processed_users = []
(
skipped_contacts,
eligible_users,
processed_users,
) = self.command.process_contacts(
# Set debugging to False
False,
skipped_contacts,
eligible_users,
processed_users,
)
# Trigger DB refresh
self.user1.refresh_from_db()
self.user2.refresh_from_db()
self.user3.refresh_from_db()
# Asserts
# The user that has no first and last names will get them from the contact
self.assertEqual(self.user1.first_name, "first1")
self.assertEqual(self.user1.last_name, "last1")
# The user that has a first but no last will be left alone
self.assertEqual(self.user2.first_name, "First name but no last name")
self.assertEqual(self.user2.last_name, "")
# The user that has a first and a last will be left alone
self.assertEqual(self.user3.first_name, "An existing first name")
self.assertEqual(self.user3.last_name, "An existing last name")
# The unlinked user will be left alone
self.assertEqual(self.user4.first_name, "")
self.assertEqual(self.user4.last_name, "")

View file

@ -654,3 +654,62 @@ class TestUser(TestCase):
"""A new user who's neither transitioned nor invited should
return True when tested with class method needs_identity_verification"""
self.assertTrue(User.needs_identity_verification(self.user.email, self.user.username))
def test_check_domain_invitations_on_login_caps_email(self):
"""A DomainInvitation with an email address with capital letters should match
a User record whose email address is not in caps"""
# create DomainInvitation with CAPS email that matches User email
# on a case-insensitive match
caps_email = "MAYOR@igorville.gov"
# mock the domain invitation save routine
with patch("registrar.models.DomainInvitation.save") as save_mock:
DomainInvitation.objects.get_or_create(email=caps_email, domain=self.domain)
self.user.check_domain_invitations_on_login()
# if check_domain_invitations_on_login properly matches exactly one
# Domain Invitation, then save routine should be called exactly once
save_mock.assert_called_once()
class TestContact(TestCase):
def setUp(self):
self.email = "mayor@igorville.gov"
self.user, _ = User.objects.get_or_create(email=self.email, first_name="Jeff", last_name="Lebowski")
self.contact, _ = Contact.objects.get_or_create(user=self.user)
def tearDown(self):
super().tearDown()
Contact.objects.all().delete()
User.objects.all().delete()
def test_saving_contact_updates_user_first_last_names(self):
"""When a contact is updated, we propagate the changes to the linked user if it exists."""
# User and Contact are created and linked as expected
self.assertEqual(self.contact.first_name, "Jeff")
self.assertEqual(self.contact.last_name, "Lebowski")
self.assertEqual(self.user.first_name, "Jeff")
self.assertEqual(self.user.last_name, "Lebowski")
self.contact.first_name = "Joey"
self.contact.last_name = "Baloney"
self.contact.save()
# Refresh the user object to reflect the changes made in the database
self.user.refresh_from_db()
# Updating the contact's first and last names propagate to the user
self.assertEqual(self.contact.first_name, "Joey")
self.assertEqual(self.contact.last_name, "Baloney")
self.assertEqual(self.user.first_name, "Joey")
self.assertEqual(self.user.last_name, "Baloney")
def test_saving_contact_does_not_update_user_email(self):
"""When a contact's email is updated, the change is not propagated to the lined user."""
self.contact.email = "joey.baloney@diaperville.com"
self.contact.save()
# Refresh the user object to reflect the changes made in the database
self.user.refresh_from_db()
# Updating the contact's email does not propagate
self.assertEqual(self.contact.email, "joey.baloney@diaperville.com")
self.assertEqual(self.user.email, "mayor@igorville.gov")

View file

@ -18,7 +18,324 @@ from unittest.mock import patch
from registrar.models.contact import Contact
from .common import less_console_noise
from .common import MockEppLib, less_console_noise
class TestExtendExpirationDates(MockEppLib):
def setUp(self):
"""Defines the file name of migration_json and the folder its contained in"""
super().setUp()
# Create a valid domain that is updatable
Domain.objects.get_or_create(
name="waterbutpurple.gov", state=Domain.State.READY, expiration_date=datetime.date(2023, 11, 15)
)
TransitionDomain.objects.get_or_create(
username="testytester@mail.com",
domain_name="waterbutpurple.gov",
epp_expiration_date=datetime.date(2023, 11, 15),
)
# Create a domain with an invalid expiration date
Domain.objects.get_or_create(
name="fake.gov", state=Domain.State.READY, expiration_date=datetime.date(2022, 5, 25)
)
TransitionDomain.objects.get_or_create(
username="themoonisactuallycheese@mail.com",
domain_name="fake.gov",
epp_expiration_date=datetime.date(2022, 5, 25),
)
# Create a domain with an invalid state
Domain.objects.get_or_create(
name="fakeneeded.gov", state=Domain.State.DNS_NEEDED, expiration_date=datetime.date(2023, 11, 15)
)
TransitionDomain.objects.get_or_create(
username="fakeneeded@mail.com",
domain_name="fakeneeded.gov",
epp_expiration_date=datetime.date(2023, 11, 15),
)
# Create a domain with a date greater than the maximum
Domain.objects.get_or_create(
name="fakemaximum.gov", state=Domain.State.READY, expiration_date=datetime.date(2024, 12, 31)
)
TransitionDomain.objects.get_or_create(
username="fakemaximum@mail.com",
domain_name="fakemaximum.gov",
epp_expiration_date=datetime.date(2024, 12, 31),
)
def tearDown(self):
"""Deletes all DB objects related to migrations"""
super().tearDown()
# Delete domain information
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
DomainInvitation.objects.all().delete()
TransitionDomain.objects.all().delete()
# Delete users
User.objects.all().delete()
UserDomainRole.objects.all().delete()
def run_extend_expiration_dates(self):
"""
This method executes the transfer_transition_domains_to_domains command.
The 'call_command' function from Django's management framework is then used to
execute the load_transition_domain command with the specified arguments.
"""
with patch(
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
return_value=True,
):
call_command("extend_expiration_dates")
def test_extends_expiration_date_correctly(self):
"""
Tests that the extend_expiration_dates method extends dates as expected
"""
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
desired_domain.expiration_date = datetime.date(2024, 11, 15)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date
self.assertEqual(current_domain.expiration_date, datetime.date(2024, 11, 15))
def test_extends_expiration_date_skips_non_current(self):
"""
Tests that the extend_expiration_dates method correctly skips domains
with an expiration date less than a certain threshold.
"""
desired_domain = Domain.objects.filter(name="fake.gov").get()
desired_domain.expiration_date = datetime.date(2022, 5, 25)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="fake.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date. The extend_expiration_dates script
# will skip all dates less than date(2023, 11, 15), meaning that this domain
# should not be affected by the change.
self.assertEqual(current_domain.expiration_date, datetime.date(2022, 5, 25))
def test_extends_expiration_date_skips_maximum_date(self):
"""
Tests that the extend_expiration_dates method correctly skips domains
with an expiration date more than a certain threshold.
"""
desired_domain = Domain.objects.filter(name="fakemaximum.gov").get()
desired_domain.expiration_date = datetime.date(2024, 12, 31)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="fakemaximum.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date. The extend_expiration_dates script
# will skip all dates less than date(2023, 11, 15), meaning that this domain
# should not be affected by the change.
self.assertEqual(current_domain.expiration_date, datetime.date(2024, 12, 31))
def test_extends_expiration_date_skips_non_ready(self):
"""
Tests that the extend_expiration_dates method correctly skips domains not in the state "ready"
"""
desired_domain = Domain.objects.filter(name="fakeneeded.gov").get()
desired_domain.expiration_date = datetime.date(2023, 11, 15)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="fakeneeded.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date. The extend_expiration_dates script
# will skip all dates less than date(2023, 11, 15), meaning that this domain
# should not be affected by the change.
self.assertEqual(current_domain.expiration_date, datetime.date(2023, 11, 15))
def test_extends_expiration_date_idempotent(self):
"""
Tests the idempotency of the extend_expiration_dates command.
Verifies that running the method multiple times does not change the expiration date
of a domain beyond the initial extension.
"""
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
desired_domain.expiration_date = datetime.date(2024, 11, 15)
# Run the expiration date script
self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date
self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15))
# Run the expiration date script again
self.run_extend_expiration_dates()
# The old domain shouldn't have changed
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date - should be the same
self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15))
class TestProcessedMigrations(TestCase):
"""This test case class is designed to verify the idempotency of migrations
related to domain transitions in the application."""
def setUp(self):
"""Defines the file name of migration_json and the folder its contained in"""
self.test_data_file_location = "registrar/tests/data"
self.migration_json_filename = "test_migrationFilepaths.json"
self.user, _ = User.objects.get_or_create(username="igorvillian")
def tearDown(self):
"""Deletes all DB objects related to migrations"""
# Delete domain information
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
DomainInvitation.objects.all().delete()
TransitionDomain.objects.all().delete()
# Delete users
User.objects.all().delete()
UserDomainRole.objects.all().delete()
def run_load_domains(self):
"""
This method executes the load_transition_domain command.
It uses 'unittest.mock.patch' to mock the TerminalHelper.query_yes_no_exit method,
which is a user prompt in the terminal. The mock function always returns True,
allowing the test to proceed without manual user input.
The 'call_command' function from Django's management framework is then used to
execute the load_transition_domain command with the specified arguments.
"""
# noqa here because splitting this up makes it confusing.
# ES501
with patch(
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
return_value=True,
):
call_command(
"load_transition_domain",
self.migration_json_filename,
directory=self.test_data_file_location,
)
def run_transfer_domains(self):
"""
This method executes the transfer_transition_domains_to_domains command.
The 'call_command' function from Django's management framework is then used to
execute the load_transition_domain command with the specified arguments.
"""
call_command("transfer_transition_domains_to_domains")
def test_domain_idempotent(self):
"""
This test ensures that the domain transfer process
is idempotent on Domain and DomainInformation.
"""
unchanged_domain, _ = Domain.objects.get_or_create(
name="testdomain.gov",
state=Domain.State.READY,
expiration_date=datetime.date(2000, 1, 1),
)
unchanged_domain_information, _ = DomainInformation.objects.get_or_create(
domain=unchanged_domain, organization_name="test org name", creator=self.user
)
self.run_load_domains()
# Test that a given TransitionDomain isn't set to "processed"
transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov")
self.assertFalse(transition_domain_object.processed)
self.run_transfer_domains()
# Test that old data isn't corrupted
actual_unchanged = Domain.objects.filter(name="testdomain.gov").get()
actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get()
self.assertEqual(unchanged_domain, actual_unchanged)
self.assertEqual(unchanged_domain_information, actual_unchanged_information)
# Test that a given TransitionDomain is set to "processed" after we transfer domains
transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov")
self.assertTrue(transition_domain_object.processed)
# Manually change Domain/DomainInformation objects
changed_domain = Domain.objects.filter(name="fakewebsite3.gov").get()
changed_domain.expiration_date = datetime.date(1999, 1, 1)
changed_domain.save()
changed_domain_information = DomainInformation.objects.filter(domain=changed_domain).get()
changed_domain_information.organization_name = "changed"
changed_domain_information.save()
# Rerun transfer domains
self.run_transfer_domains()
# Test that old data isn't corrupted after running this twice
actual_unchanged = Domain.objects.filter(name="testdomain.gov").get()
actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get()
self.assertEqual(unchanged_domain, actual_unchanged)
self.assertEqual(unchanged_domain_information, actual_unchanged_information)
# Ensure that domain hasn't changed
actual_domain = Domain.objects.filter(name="fakewebsite3.gov").get()
self.assertEqual(changed_domain, actual_domain)
# Ensure that DomainInformation hasn't changed
actual_domain_information = DomainInformation.objects.filter(domain=changed_domain).get()
self.assertEqual(changed_domain_information, actual_domain_information)
def test_transition_domain_is_processed(self):
"""
This test checks if a domain is correctly marked as processed in the transition.
"""
old_transition_domain, _ = TransitionDomain.objects.get_or_create(domain_name="testdomain.gov")
# Asser that old records default to 'True'
self.assertTrue(old_transition_domain.processed)
unchanged_domain, _ = Domain.objects.get_or_create(
name="testdomain.gov",
state=Domain.State.READY,
expiration_date=datetime.date(2000, 1, 1),
)
unchanged_domain_information, _ = DomainInformation.objects.get_or_create(
domain=unchanged_domain, organization_name="test org name", creator=self.user
)
self.run_load_domains()
# Test that a given TransitionDomain isn't set to "processed"
transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov")
self.assertFalse(transition_domain_object.processed)
self.run_transfer_domains()
# Test that old data isn't corrupted
actual_unchanged = Domain.objects.filter(name="testdomain.gov").get()
actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get()
self.assertEqual(unchanged_domain, actual_unchanged)
self.assertTrue(old_transition_domain.processed)
self.assertEqual(unchanged_domain_information, actual_unchanged_information)
# Test that a given TransitionDomain is set to "processed" after we transfer domains
transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov")
self.assertTrue(transition_domain_object.processed)
class TestOrganizationMigration(TestCase):

View file

@ -141,7 +141,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# 302 redirect to the first form
page = self.app.get(reverse("application:")).follow()
# submitting should get back the same page if the required field is empty
result = page.form.submit()
result = page.forms[0].submit()
self.assertIn("What kind of U.S.-based government organization do you represent?", result)
def test_application_multiple_applications_exist(self):
@ -164,6 +164,9 @@ class DomainApplicationTests(TestWithUser, WebTest):
this test work.
This test also looks for the long organization name on the summary page.
This also tests for the presence of a modal trigger and the dynamic test
in the modal header on the submit page.
"""
num_pages_tested = 0
# elections, type_of_work, tribal_government, no_other_contacts
@ -178,11 +181,11 @@ class DomainApplicationTests(TestWithUser, WebTest):
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
# ---- TYPE PAGE ----
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = "federal"
# test next button and validate data
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_page.form.submit()
type_result = type_form.submit()
# should see results in db
application = DomainApplication.objects.get() # there's only one
self.assertEqual(application.organization_type, "federal")
@ -197,7 +200,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
federal_page = type_result.follow()
federal_form = federal_page.form
federal_form = federal_page.forms[0]
federal_form["organization_federal-federal_type"] = "executive"
# test next button
@ -216,7 +219,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
org_contact_page = federal_result.follow()
org_contact_form = org_contact_page.form
org_contact_form = org_contact_page.forms[0]
# federal agency so we have to fill in federal_agency
org_contact_form["organization_contact-federal_agency"] = "General Services Administration"
org_contact_form["organization_contact-organization_name"] = "Testorg"
@ -249,7 +252,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_page = org_contact_result.follow()
ao_form = ao_page.form
ao_form = ao_page.forms[0]
ao_form["authorizing_official-first_name"] = "Testy ATO"
ao_form["authorizing_official-last_name"] = "Tester ATO"
ao_form["authorizing_official-title"] = "Chief Tester"
@ -276,7 +279,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
current_sites_page = ao_result.follow()
current_sites_form = current_sites_page.form
current_sites_form = current_sites_page.forms[0]
current_sites_form["current_sites-0-website"] = "www.city.com"
# test next button
@ -298,7 +301,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
dotgov_page = current_sites_result.follow()
dotgov_form = dotgov_page.form
dotgov_form = dotgov_page.forms[0]
dotgov_form["dotgov_domain-requested_domain"] = "city"
dotgov_form["dotgov_domain-0-alternative_domain"] = "city1"
@ -318,7 +321,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
purpose_page = dotgov_result.follow()
purpose_form = purpose_page.form
purpose_form = purpose_page.forms[0]
purpose_form["purpose-purpose"] = "For all kinds of things."
# test next button
@ -337,7 +340,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
your_contact_page = purpose_result.follow()
your_contact_form = your_contact_page.form
your_contact_form = your_contact_page.forms[0]
your_contact_form["your_contact-first_name"] = "Testy you"
your_contact_form["your_contact-last_name"] = "Tester you"
@ -365,7 +368,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
other_contacts_page = your_contact_result.follow()
other_contacts_form = other_contacts_page.form
other_contacts_form = other_contacts_page.forms[0]
other_contacts_form["other_contacts-0-first_name"] = "Testy2"
other_contacts_form["other_contacts-0-last_name"] = "Tester2"
@ -398,7 +401,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
anything_else_page = other_contacts_result.follow()
anything_else_form = anything_else_page.form
anything_else_form = anything_else_page.forms[0]
anything_else_form["anything_else-anything_else"] = "Nothing else."
@ -418,7 +421,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
requirements_page = anything_else_result.follow()
requirements_form = requirements_page.form
requirements_form = requirements_page.forms[0]
requirements_form["requirements-is_policy_acknowledged"] = True
@ -438,7 +441,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
review_page = requirements_result.follow()
review_form = review_page.form
review_form = review_page.forms[0]
# Review page contains all the previously entered data
# Let's make sure the long org name is displayed
@ -472,6 +475,14 @@ class DomainApplicationTests(TestWithUser, WebTest):
self.assertContains(review_page, "(201) 555-5557")
self.assertContains(review_page, "Nothing else.")
# We can't test the modal itself as it relies on JS for init and triggering,
# but we can test for the existence of its trigger:
self.assertContains(review_page, "toggle-submit-domain-request")
# And the existence of the modal's data parked and ready for the js init.
# The next assert also tests for the passed requested domain context from
# the view > application_form > modal
self.assertContains(review_page, "You are about to submit a domain request for city.gov")
# final submission results in a redirect to the "finished" URL
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
with less_console_noise():
@ -540,7 +551,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# the conditional step titles shouldn't appear initially
self.assertNotContains(type_page, self.TITLES["organization_federal"])
self.assertNotContains(type_page, self.TITLES["organization_election"])
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = "federal"
# set the session ID before .submit()
@ -561,9 +572,9 @@ class DomainApplicationTests(TestWithUser, WebTest):
# continuing on in the flow we need to see top-level agency on the
# contact page
federal_page.form["organization_federal-federal_type"] = "executive"
federal_page.forms[0]["organization_federal-federal_type"] = "executive"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
federal_result = federal_page.form.submit()
federal_result = federal_page.forms[0].submit()
# the post request should return a redirect to the contact
# question
self.assertEqual(federal_result.status_code, 302)
@ -586,7 +597,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# the conditional step titles shouldn't appear initially
self.assertNotContains(type_page, self.TITLES["organization_federal"])
self.assertNotContains(type_page, self.TITLES["organization_election"])
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = "county"
# set the session ID before .submit()
@ -606,9 +617,9 @@ class DomainApplicationTests(TestWithUser, WebTest):
# continuing on in the flow we need to NOT see top-level agency on the
# contact page
election_page.form["organization_election-is_election_board"] = "True"
election_page.forms[0]["organization_election-is_election_board"] = "True"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
election_result = election_page.form.submit()
election_result = election_page.forms[0].submit()
# the post request should return a redirect to the contact
# question
self.assertEqual(election_result.status_code, 302)
@ -626,10 +637,10 @@ class DomainApplicationTests(TestWithUser, WebTest):
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = "federal"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_page.form.submit()
type_result = type_form.submit()
# follow first redirect
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
@ -654,15 +665,15 @@ class DomainApplicationTests(TestWithUser, WebTest):
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.INTERSTATE
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_page.form.submit()
type_result = type_form.submit()
# follow first redirect
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
contact_page = type_result.follow()
org_contact_form = contact_page.form
org_contact_form = contact_page.forms[0]
self.assertNotIn("federal_agency", org_contact_form.fields)
@ -690,10 +701,10 @@ class DomainApplicationTests(TestWithUser, WebTest):
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.SPECIAL_DISTRICT
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_page.form.submit()
type_result = type_page.forms[0].submit()
# follow first redirect
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
contact_page = type_result.follow()
@ -710,7 +721,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
result = contacts_page.form.submit()
result = contacts_page.forms[0].submit()
# follow first redirect
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
no_contacts_page = result.follow()
@ -727,10 +738,10 @@ class DomainApplicationTests(TestWithUser, WebTest):
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.INTERSTATE
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_page.form.submit()
type_result = type_form.submit()
# follow first redirect
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
contact_page = type_result.follow()
@ -745,10 +756,10 @@ class DomainApplicationTests(TestWithUser, WebTest):
# of a "session". We are going to do it manually, saving the session ID here
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.TRIBAL
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_page.form.submit()
type_result = type_form.submit()
# the tribal government page comes immediately afterwards
self.assertIn("/tribal_government", type_result.headers["Location"])
# follow first redirect
@ -767,18 +778,18 @@ class DomainApplicationTests(TestWithUser, WebTest):
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
# ---- TYPE PAGE ----
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = "federal"
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_page.form.submit()
type_result = type_form.submit()
# ---- FEDERAL BRANCH PAGE ----
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
federal_page = type_result.follow()
federal_form = federal_page.form
federal_form = federal_page.forms[0]
federal_form["organization_federal-federal_type"] = "executive"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
federal_result = federal_form.submit()
@ -787,7 +798,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
org_contact_page = federal_result.follow()
org_contact_form = org_contact_page.form
org_contact_form = org_contact_page.forms[0]
# federal agency so we have to fill in federal_agency
org_contact_form["organization_contact-federal_agency"] = "General Services Administration"
org_contact_form["organization_contact-organization_name"] = "Testorg"
@ -804,7 +815,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# ---- AO CONTACT PAGE ----
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_page = org_contact_result.follow()
self.assertContains(ao_page, "Domain requests from executive branch agencies")
self.assertContains(ao_page, "Executive branch federal agencies")
# Go back to organization type page and change type
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
@ -828,18 +839,18 @@ class DomainApplicationTests(TestWithUser, WebTest):
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
# ---- TYPE PAGE ----
type_form = type_page.form
type_form = type_page.forms[0]
type_form["organization_type-organization_type"] = "federal"
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_page.form.submit()
type_result = type_form.submit()
# ---- FEDERAL BRANCH PAGE ----
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
federal_page = type_result.follow()
federal_form = federal_page.form
federal_form = federal_page.forms[0]
federal_form["organization_federal-federal_type"] = "executive"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
federal_result = federal_form.submit()
@ -848,7 +859,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
org_contact_page = federal_result.follow()
org_contact_form = org_contact_page.form
org_contact_form = org_contact_page.forms[0]
# federal agency so we have to fill in federal_agency
org_contact_form["organization_contact-federal_agency"] = "General Services Administration"
org_contact_form["organization_contact-organization_name"] = "Testorg"
@ -870,7 +881,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_page = org_contact_result.follow()
ao_form = ao_page.form
ao_form = ao_page.forms[0]
ao_form["authorizing_official-first_name"] = "Testy ATO"
ao_form["authorizing_official-last_name"] = "Tester ATO"
ao_form["authorizing_official-title"] = "Chief Tester"
@ -884,7 +895,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
current_sites_page = ao_result.follow()
current_sites_form = current_sites_page.form
current_sites_form = current_sites_page.forms[0]
current_sites_form["current_sites-0-website"] = "www.city.com"
# test saving the page
@ -917,7 +928,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
current_sites_page = self.app.get(reverse("application:current_sites"))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
# fill in the form field
current_sites_form = current_sites_page.form
current_sites_form = current_sites_page.forms[0]
self.assertIn("current_sites-0-website", current_sites_form.fields)
self.assertNotIn("current_sites-1-website", current_sites_form.fields)
current_sites_form["current_sites-0-website"] = "https://example.com"
@ -926,7 +937,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
current_sites_result = current_sites_form.submit("submit_button", value="save")
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
current_sites_form = current_sites_result.follow().form
current_sites_form = current_sites_result.follow().forms[0]
# verify that there are two form fields
value = current_sites_form["current_sites-0-website"].value
@ -1086,6 +1097,18 @@ class DomainApplicationTests(TestWithUser, WebTest):
detail_page = home_page.click("Manage", index=0)
self.assertContains(detail_page, "Federal: an agency of the U.S. government")
def test_submit_modal_no_domain_text_fallback(self):
"""When user clicks on submit your domain request and the requested domain
is null (possible through url direct access to the review page), present
fallback copy in the modal's header.
NOTE: This may be a moot point if we implement a more solid pattern in the
future, like not a submit action at all on the review page."""
review_page = self.app.get(reverse("application:review"))
self.assertContains(review_page, "toggle-submit-domain-request")
self.assertContains(review_page, "You are about to submit an incomplete request")
class TestWithDomainPermissions(TestWithUser):
def setUp(self):
@ -1355,29 +1378,55 @@ class TestDomainManagers(TestDomainOverview):
out the boto3 SES email sending here.
"""
# make sure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
email_address = "mayor@igorville.gov"
User.objects.filter(email=email_address).delete()
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = EMAIL
add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_result = add_page.form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_page = success_result.follow()
self.assertContains(success_page, EMAIL)
self.assertContains(success_page, email_address)
self.assertContains(success_page, "Cancel") # link to cancel invitation
self.assertTrue(DomainInvitation.objects.filter(email=EMAIL).exists())
self.assertTrue(DomainInvitation.objects.filter(email=email_address).exists())
@boto3_mocking.patching
def test_domain_invitation_created_for_caps_email(self):
"""Add user on a nonexistent email with CAPS creates an invitation to lowercase email.
Adding a non-existent user sends an email as a side-effect, so mock
out the boto3 SES email sending here.
"""
# make sure there is no user with this email
email_address = "mayor@igorville.gov"
caps_email_address = "MAYOR@igorville.gov"
User.objects.filter(email=email_address).delete()
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = caps_email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_result = add_page.form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_page = success_result.follow()
self.assertContains(success_page, email_address)
self.assertContains(success_page, "Cancel") # link to cancel invitation
self.assertTrue(DomainInvitation.objects.filter(email=email_address).exists())
@boto3_mocking.patching
def test_domain_invitation_email_sent(self):
"""Inviting a non-existent user sends them an email."""
# make sure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
email_address = "mayor@igorville.gov"
User.objects.filter(email=email_address).delete()
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
@ -1386,28 +1435,28 @@ class TestDomainManagers(TestDomainOverview):
with boto3_mocking.clients.handler_for("sesv2", mock_client):
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = EMAIL
add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
add_page.form.submit()
# check the mock instance to see if `send_email` was called right
mock_client_instance.send_email.assert_called_once_with(
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
Destination={"ToAddresses": [EMAIL]},
Destination={"ToAddresses": [email_address]},
Content=ANY,
)
def test_domain_invitation_cancel(self):
"""Posting to the delete view deletes an invitation."""
EMAIL = "mayor@igorville.gov"
invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=EMAIL)
email_address = "mayor@igorville.gov"
invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address)
self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id}))
with self.assertRaises(DomainInvitation.DoesNotExist):
DomainInvitation.objects.get(id=invitation.id)
def test_domain_invitation_cancel_no_permissions(self):
"""Posting to the delete view as a different user should fail."""
EMAIL = "mayor@igorville.gov"
invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=EMAIL)
email_address = "mayor@igorville.gov"
invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address)
other_user = User()
other_user.save()
@ -1419,20 +1468,20 @@ class TestDomainManagers(TestDomainOverview):
@boto3_mocking.patching
def test_domain_invitation_flow(self):
"""Send an invitation to a new user, log in and load the dashboard."""
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
email_address = "mayor@igorville.gov"
User.objects.filter(email=email_address).delete()
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = EMAIL
add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
add_page.form.submit()
# user was invited, create them
new_user = User.objects.create(username=EMAIL, email=EMAIL)
new_user = User.objects.create(username=email_address, email=email_address)
# log them in to `self.app`
self.app.set_user(new_user.username)
# and manually call the on each login callback

View file

@ -43,11 +43,11 @@ class GenericError(Exception):
"""
_error_mapping = {
GenericErrorCodes.CANNOT_CONTACT_REGISTRY: """
Were experiencing a system connection error. Please wait a few minutes
and try again. If you continue to receive this error after a few tries,
contact help@get.gov.
""",
GenericErrorCodes.CANNOT_CONTACT_REGISTRY: (
"Were experiencing a system connection error. Please wait a few minutes "
"and try again. If you continue to receive this error after a few tries, "
"contact help@get.gov."
),
GenericErrorCodes.GENERIC_ERROR: ("Value entered was wrong."),
}

View file

@ -321,12 +321,21 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
def get_context_data(self):
"""Define context for access on all wizard pages."""
# Build the submit button that we'll pass to the modal.
modal_button = '<button type="submit" ' 'class="usa-button" ' ">Submit request</button>"
# Concatenate the modal header that we'll pass to the modal.
if self.application.requested_domain:
modal_heading = "You are about to submit a domain request for " + str(self.application.requested_domain)
else:
modal_heading = "You are about to submit an incomplete request"
return {
"form_titles": self.TITLES,
"steps": self.steps,
# Add information about which steps should be unlocked
"visited": self.storage.get("step_history", []),
"is_federal": self.application.is_federal(),
"modal_button": modal_button,
"modal_heading": modal_heading,
}
def get_step_list(self) -> list: