From 88fb945573b2324b934c4fdaceb1e077052b0b7b Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 1 Dec 2023 14:17:30 -0700
Subject: [PATCH 01/54] Change to sentence case
---
src/registrar/models/transition_domain.py | 2 +-
src/registrar/templates/application_status.html | 2 +-
src/registrar/templates/domain_detail.html | 2 +-
src/registrar/templates/home.html | 2 +-
src/registrar/tests/test_models_domain.py | 2 +-
src/registrar/tests/test_views.py | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/registrar/models/transition_domain.py b/src/registrar/models/transition_domain.py
index 9e6d40cf1..4be1836e8 100644
--- a/src/registrar/models/transition_domain.py
+++ b/src/registrar/models/transition_domain.py
@@ -4,7 +4,7 @@ from .utility.time_stamped_model import TimeStampedModel
class StatusChoices(models.TextChoices):
READY = "ready", "Ready"
- ON_HOLD = "on hold", "On Hold"
+ ON_HOLD = "on hold", "On hold"
UNKNOWN = "unknown", "Unknown"
diff --git a/src/registrar/templates/application_status.html b/src/registrar/templates/application_status.html
index 79d0f7ff9..e766d406f 100644
--- a/src/registrar/templates/application_status.html
+++ b/src/registrar/templates/application_status.html
@@ -31,7 +31,7 @@
Status:
{% if domainapplication.status == 'approved' %} Approved
- {% elif domainapplication.status == 'in review' %} In Review
+ {% elif domainapplication.status == 'in review' %} In review
{% elif domainapplication.status == 'rejected' %} Rejected
{% elif domainapplication.status == 'submitted' %} Submitted
{% elif domainapplication.status == 'ineligible' %} Ineligible
diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html
index 81a350f82..470df7537 100644
--- a/src/registrar/templates/domain_detail.html
+++ b/src/registrar/templates/domain_detail.html
@@ -18,7 +18,7 @@
Status:
{% if domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED%}
- DNS Needed
+ DNS needed
{% else %}
{{ domain.state|title }}
{% endif %}
diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html
index 0bc792cef..8480c671b 100644
--- a/src/registrar/templates/home.html
+++ b/src/registrar/templates/home.html
@@ -53,7 +53,7 @@
{{ domain.created_time|date }} |
{% if domain.state == "unknown" or domain.state == "dns needed"%}
- DNS Needed
+ DNS needed
{% else %}
{{ domain.state|title }}
{% endif %}
diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py
index 4a2023243..fac1c0fcd 100644
--- a/src/registrar/tests/test_models_domain.py
+++ b/src/registrar/tests/test_models_domain.py
@@ -1506,7 +1506,7 @@ class TestRegistrantNameservers(MockEppLib):
]
def test_setting_not_allowed(self):
- """Scenario: A domain state is not Ready or DNS Needed
+ """Scenario: A domain state is not Ready or DNS needed
then setting nameservers is not allowed"""
domain, _ = Domain.objects.get_or_create(name="onholdDomain.gov", state=Domain.State.ON_HOLD)
with self.assertRaises(ActionNotAllowed):
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py
index 6599a2436..8ddfa8999 100644
--- a/src/registrar/tests/test_views.py
+++ b/src/registrar/tests/test_views.py
@@ -100,7 +100,7 @@ class LoggedInTests(TestWithUser):
response = self.client.get("/")
# count = 2 because it is also in screenreader content
self.assertContains(response, "igorville.gov", count=2)
- self.assertContains(response, "DNS Needed")
+ self.assertContains(response, "DNS needed")
# clean up
role.delete()
From 2a22be413157c7218c70cce2af8866cb9ab75231 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 1 Dec 2023 14:47:12 -0700
Subject: [PATCH 02/54] Change fixtures
---
src/registrar/fixtures_applications.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/registrar/fixtures_applications.py b/src/registrar/fixtures_applications.py
index aea153aef..679e5f7fe 100644
--- a/src/registrar/fixtures_applications.py
+++ b/src/registrar/fixtures_applications.py
@@ -50,15 +50,15 @@ class DomainApplicationFixture:
DA = [
{
"status": "started",
- "organization_name": "Example - Finished but not Submitted",
+ "organization_name": "Example - Finished but not submitted",
},
{
"status": "submitted",
- "organization_name": "Example - Submitted but pending Investigation",
+ "organization_name": "Example - Submitted but pending investigation",
},
{
"status": "in review",
- "organization_name": "Example - In Investigation",
+ "organization_name": "Example - In investigation",
},
{
"status": "in review",
@@ -70,7 +70,7 @@ class DomainApplicationFixture:
},
{
"status": "action needed",
- "organization_name": "Example - Action Needed",
+ "organization_name": "Example - Action needed",
},
{
"status": "rejected",
From cb2ea02d425242bed5bce0cf0fc964e3e86803ca Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Fri, 1 Dec 2023 14:57:36 -0700
Subject: [PATCH 03/54] Create 0049_alter_transitiondomain_status.py
---
.../0049_alter_transitiondomain_status.py | 24 +++++++++++++++++++
1 file changed, 24 insertions(+)
create mode 100644 src/registrar/migrations/0049_alter_transitiondomain_status.py
diff --git a/src/registrar/migrations/0049_alter_transitiondomain_status.py b/src/registrar/migrations/0049_alter_transitiondomain_status.py
new file mode 100644
index 000000000..0c358e78e
--- /dev/null
+++ b/src/registrar/migrations/0049_alter_transitiondomain_status.py
@@ -0,0 +1,24 @@
+# Generated by Django 4.2.7 on 2023-12-01 21:57
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("registrar", "0048_alter_transitiondomain_status"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="transitiondomain",
+ name="status",
+ field=models.CharField(
+ blank=True,
+ choices=[("ready", "Ready"), ("on hold", "On hold"), ("unknown", "Unknown")],
+ default="ready",
+ help_text="domain status during the transfer",
+ max_length=255,
+ verbose_name="Status",
+ ),
+ ),
+ ]
From 4a25642d8d60353c5dc99278139a6342913f8aae Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Tue, 5 Dec 2023 11:35:08 -0800
Subject: [PATCH 04/54] First pass EPP retry
---
src/api/views.py | 5 +++++
src/epplibwrapper/client.py | 10 +++++++---
src/epplibwrapper/errors.py | 7 ++++++-
src/epplibwrapper/tests/test_pool.py | 4 ++++
4 files changed, 22 insertions(+), 4 deletions(-)
diff --git a/src/api/views.py b/src/api/views.py
index 85ae021c9..b3c371494 100644
--- a/src/api/views.py
+++ b/src/api/views.py
@@ -6,6 +6,8 @@ from django.utils.safestring import mark_safe
from registrar.templatetags.url_helpers import public_site_url
from registrar.utility.errors import GenericError, GenericErrorCodes
+# comment out after testing
+from epplibwrapper.errors import RegistryError
import requests
@@ -67,6 +69,9 @@ def check_domain_available(domain):
a match. If check fails, throws a RegistryError.
"""
Domain = apps.get_model("registrar.Domain")
+ # TODO: remove this block it is used for testing on dev sandbox to verify error retry
+ if "bad" in domain:
+ raise RegistryError("Forced Registry Error from bad domain")
if domain.endswith(".gov"):
return Domain.available(domain)
else:
diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py
index cfb41f4ea..5693b6993 100644
--- a/src/epplibwrapper/client.py
+++ b/src/epplibwrapper/client.py
@@ -17,7 +17,7 @@ except ImportError:
from django.conf import settings
from .cert import Cert, Key
-from .errors import LoginError, RegistryError
+from .errors import ErrorCode, LoginError, RegistryError
from .socket import Socket
from .utility.pool import EPPConnectionPool
@@ -115,7 +115,7 @@ class EPPLibWrapper:
except TransportError as err:
message = f"{cmd_type} failed to execute due to a connection error."
logger.error(f"{message} Error: {err}", exc_info=True)
- raise RegistryError(message) from err
+ raise RegistryError(message, code=ErrorCode.TRANSPORT_ERROR) from err
except LoginError as err:
# For linter due to it not liking this line length
text = "failed to execute due to a registry login error."
@@ -159,11 +159,15 @@ class EPPLibWrapper:
self.start_connection_pool()
counter = 0 # we'll try 3 times
+ logger.info("Counter set to: ", counter)
while True:
try:
+ logger.info("Trying self._send(command)")
return self._send(command)
except RegistryError as err:
- if err.should_retry() and counter < 3:
+ logger.info("Exception found")
+ if counter < 3 and (err.should_retry() or err.is_transport_error()):
+ logger.info("Retrying transport error. Attempt #", counter)
counter += 1
sleep((counter * 50) / 1000) # sleep 50 ms to 150 ms
else: # don't try again
diff --git a/src/epplibwrapper/errors.py b/src/epplibwrapper/errors.py
index d34ed5e91..2b7bdd255 100644
--- a/src/epplibwrapper/errors.py
+++ b/src/epplibwrapper/errors.py
@@ -4,13 +4,15 @@ from enum import IntEnum
class ErrorCode(IntEnum):
"""
Overview of registry response codes from RFC 5730. See RFC 5730 for full text.
-
+ - 0 System connection error
- 1000 - 1500 Success
- 2000 - 2308 Registrar did something silly
- 2400 - 2500 Registry did something silly
- 2501 - 2502 Something malicious or abusive may have occurred
"""
+ TRANSPORT_ERROR = 0
+
COMMAND_COMPLETED_SUCCESSFULLY = 1000
COMMAND_COMPLETED_SUCCESSFULLY_ACTION_PENDING = 1001
COMMAND_COMPLETED_SUCCESSFULLY_NO_MESSAGES = 1300
@@ -67,6 +69,9 @@ class RegistryError(Exception):
def should_retry(self):
return self.code == ErrorCode.COMMAND_FAILED
+ def is_transport_error(self):
+ return self.code == ErrorCode.TRANSPORT_ERROR
+
# connection errors have error code of None and [Errno 99] in the err message
def is_connection_error(self):
return self.code is None
diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py
index 1c36d26da..ab4323ff3 100644
--- a/src/epplibwrapper/tests/test_pool.py
+++ b/src/epplibwrapper/tests/test_pool.py
@@ -246,3 +246,7 @@ class TestConnectionPool(TestCase):
expected = "InfoDomain failed to execute due to a connection error."
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
self.assertEqual(result, expected)
+
+ @patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success)
+ def test_retries_on_transport_error(self):
+ """A .send is invoked on the pool, but [description for transport error]."""
From 6fe9af18d971a0c16d348d56a49e15541ded2a15 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 5 Dec 2023 13:10:36 -0700
Subject: [PATCH 05/54] Update settings.py
---
src/registrar/config/settings.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py
index 7f20c8129..b9172f72b 100644
--- a/src/registrar/config/settings.py
+++ b/src/registrar/config/settings.py
@@ -64,7 +64,7 @@ secret_registry_cert = b64decode(secret("REGISTRY_CERT", ""))
secret_registry_key = b64decode(secret("REGISTRY_KEY", ""))
secret_registry_key_passphrase = secret("REGISTRY_KEY_PASSPHRASE", "")
secret_registry_hostname = secret("REGISTRY_HOSTNAME")
-
+# WILL REMOVE (TO PUSH TO SANDBOX)
# region: Basic Django Config-----------------------------------------------###
# Build paths inside the project like this: BASE_DIR / "subdir".
From cdc4e366a072fb480d3d47c605fba45a5eb3dedf Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 5 Dec 2023 13:13:58 -0700
Subject: [PATCH 06/54] Fix migrations
---
.../0049_alter_domainapplication_current_websites_and_more.py | 2 +-
...ondomain_status.py => 0050_alter_transitiondomain_status.py} | 0
2 files changed, 1 insertion(+), 1 deletion(-)
rename src/registrar/migrations/{0049_alter_transitiondomain_status.py => 0050_alter_transitiondomain_status.py} (100%)
diff --git a/src/registrar/migrations/0049_alter_domainapplication_current_websites_and_more.py b/src/registrar/migrations/0049_alter_domainapplication_current_websites_and_more.py
index 4341bdad6..42dc76d7e 100644
--- a/src/registrar/migrations/0049_alter_domainapplication_current_websites_and_more.py
+++ b/src/registrar/migrations/0049_alter_domainapplication_current_websites_and_more.py
@@ -5,7 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ("registrar", "0048_alter_transitiondomain_status"),
+ ("registrar", "0049_alter_domainapplication_current_websites_and_more"),
]
operations = [
diff --git a/src/registrar/migrations/0049_alter_transitiondomain_status.py b/src/registrar/migrations/0050_alter_transitiondomain_status.py
similarity index 100%
rename from src/registrar/migrations/0049_alter_transitiondomain_status.py
rename to src/registrar/migrations/0050_alter_transitiondomain_status.py
From b58dc77788fe47e24c4bf06d1c7202e2f65e1390 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 5 Dec 2023 13:17:20 -0700
Subject: [PATCH 07/54] Fix migrations (typo)
---
.../0049_alter_domainapplication_current_websites_and_more.py | 2 +-
src/registrar/migrations/0050_alter_transitiondomain_status.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/registrar/migrations/0049_alter_domainapplication_current_websites_and_more.py b/src/registrar/migrations/0049_alter_domainapplication_current_websites_and_more.py
index 42dc76d7e..4341bdad6 100644
--- a/src/registrar/migrations/0049_alter_domainapplication_current_websites_and_more.py
+++ b/src/registrar/migrations/0049_alter_domainapplication_current_websites_and_more.py
@@ -5,7 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ("registrar", "0049_alter_domainapplication_current_websites_and_more"),
+ ("registrar", "0048_alter_transitiondomain_status"),
]
operations = [
diff --git a/src/registrar/migrations/0050_alter_transitiondomain_status.py b/src/registrar/migrations/0050_alter_transitiondomain_status.py
index 0c358e78e..72bebb7b5 100644
--- a/src/registrar/migrations/0050_alter_transitiondomain_status.py
+++ b/src/registrar/migrations/0050_alter_transitiondomain_status.py
@@ -5,7 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ("registrar", "0048_alter_transitiondomain_status"),
+ ("registrar", "0049_alter_domainapplication_current_websites_and_more"),
]
operations = [
From 8c9068d3cc01fec297d832db239975e0c3dbb931 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 5 Dec 2023 14:10:01 -0700
Subject: [PATCH 08/54] Fix title issue
---
src/registrar/templates/home.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html
index 8480c671b..f3089dfd0 100644
--- a/src/registrar/templates/home.html
+++ b/src/registrar/templates/home.html
@@ -111,7 +111,7 @@
{{ application.requested_domain.name|default:"New domain request" }}
| {{ application.created_at|date }} |
- {{ application.status|title }} |
+ {{ application.status|capfirst }} |
{% if application.status == "started" or application.status == "action needed" or application.status == "withdrawn" %}
From 2fbbb34fb2c990ab2c914e9e5eb72d436f9abdb8 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 5 Dec 2023 14:33:09 -0700
Subject: [PATCH 09/54] Fix casing issue
---
.../migrations/0051_alter_domain_state.py | 30 ++++++++++++++++++
.../0052_alter_domainapplication_status.py | 31 +++++++++++++++++++
src/registrar/models/domain.py | 4 +--
src/registrar/models/domain_application.py | 16 +++++-----
4 files changed, 71 insertions(+), 10 deletions(-)
create mode 100644 src/registrar/migrations/0051_alter_domain_state.py
create mode 100644 src/registrar/migrations/0052_alter_domainapplication_status.py
diff --git a/src/registrar/migrations/0051_alter_domain_state.py b/src/registrar/migrations/0051_alter_domain_state.py
new file mode 100644
index 000000000..df1240d93
--- /dev/null
+++ b/src/registrar/migrations/0051_alter_domain_state.py
@@ -0,0 +1,30 @@
+# Generated by Django 4.2.7 on 2023-12-05 21:21
+
+from django.db import migrations
+import django_fsm
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("registrar", "0050_alter_transitiondomain_status"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="domain",
+ name="state",
+ field=django_fsm.FSMField(
+ choices=[
+ ("unknown", "Unknown"),
+ ("dns needed", "Dns needed"),
+ ("ready", "Ready"),
+ ("on hold", "On hold"),
+ ("deleted", "Deleted"),
+ ],
+ default="unknown",
+ help_text="Very basic info about the lifecycle of this domain object",
+ max_length=21,
+ protected=True,
+ ),
+ ),
+ ]
diff --git a/src/registrar/migrations/0052_alter_domainapplication_status.py b/src/registrar/migrations/0052_alter_domainapplication_status.py
new file mode 100644
index 000000000..5a274d731
--- /dev/null
+++ b/src/registrar/migrations/0052_alter_domainapplication_status.py
@@ -0,0 +1,31 @@
+# Generated by Django 4.2.7 on 2023-12-05 21:25
+
+from django.db import migrations
+import django_fsm
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("registrar", "0051_alter_domain_state"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="domainapplication",
+ name="status",
+ field=django_fsm.FSMField(
+ choices=[
+ ("started", "Started"),
+ ("submitted", "Submitted"),
+ ("in review", "In review"),
+ ("action needed", "Action needed"),
+ ("approved", "Approved"),
+ ("withdrawn", "Withdrawn"),
+ ("rejected", "Rejected"),
+ ("ineligible", "Ineligible"),
+ ],
+ default="started",
+ max_length=50,
+ ),
+ ),
+ ]
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 94430fb36..16c918f84 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -126,13 +126,13 @@ class Domain(TimeStampedModel, DomainHelper):
# The domain object exists in the registry
# but nameservers don't exist for it yet
- DNS_NEEDED = "dns needed"
+ DNS_NEEDED = "dns needed", "Dns needed"
# Domain has had nameservers set, may or may not be active
READY = "ready"
# Registrar manually changed state to client hold
- ON_HOLD = "on hold"
+ ON_HOLD = "on hold", "On hold"
# previously existed but has been deleted from the registry
DELETED = "deleted"
diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py
index 9ab3908d4..b35ad28c8 100644
--- a/src/registrar/models/domain_application.py
+++ b/src/registrar/models/domain_application.py
@@ -29,14 +29,14 @@ class DomainApplication(TimeStampedModel):
REJECTED = "rejected"
INELIGIBLE = "ineligible"
STATUS_CHOICES = [
- (STARTED, STARTED),
- (SUBMITTED, SUBMITTED),
- (IN_REVIEW, IN_REVIEW),
- (ACTION_NEEDED, ACTION_NEEDED),
- (APPROVED, APPROVED),
- (WITHDRAWN, WITHDRAWN),
- (REJECTED, REJECTED),
- (INELIGIBLE, INELIGIBLE),
+ (STARTED, STARTED.capitalize()),
+ (SUBMITTED, SUBMITTED.capitalize()),
+ (IN_REVIEW, IN_REVIEW.capitalize()),
+ (ACTION_NEEDED, ACTION_NEEDED.capitalize()),
+ (APPROVED, APPROVED.capitalize()),
+ (WITHDRAWN, WITHDRAWN.capitalize()),
+ (REJECTED, REJECTED.capitalize()),
+ (INELIGIBLE, INELIGIBLE.capitalize()),
]
class StateTerritoryChoices(models.TextChoices):
From f32952f6be3743cd83b9c8414376032aca023dc0 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 5 Dec 2023 14:59:25 -0700
Subject: [PATCH 10/54] Fix status dropdowns
---
src/registrar/assets/js/get-gov-admin.js | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js
index 53eeb22a3..bdbd89c23 100644
--- a/src/registrar/assets/js/get-gov-admin.js
+++ b/src/registrar/assets/js/get-gov-admin.js
@@ -63,6 +63,25 @@ function openInNewTab(el, removeAttribute = false){
checkToListThenInitWidget('id_alternative_domains_to', 0);
})();
+/** An IIFE to capitalize statuses in the Domain Application status dropdown
+*/
+(function (){
+ function capitalizeFirstLetterInDropdownOptions(dropdown_id) {
+ // Grabs the status dropdown
+ var selectElement = document.getElementById(dropdown_id);
+ if (selectElement) {
+ var options = selectElement.options;
+ // Loop through each option, and convert to sentence case
+ for (var i = 0; i < options.length; i++) {
+ var option = options[i];
+ option.text = option.text.charAt(0).toUpperCase() + option.text.slice(1).toLowerCase();
+ }
+ }
+ }
+
+ capitalizeFirstLetterInDropdownOptions('id_status');
+})();
+
// Function to check for the existence of the "to" select list element in the DOM, and if and when found,
// initialize the associated widget
function checkToListThenInitWidget(toListId, attempts) {
From 58bc602742f94915776d7ec113e566abccde3756 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Tue, 5 Dec 2023 15:07:46 -0700
Subject: [PATCH 11/54] Fix migration merge conflict
---
.../0050_alter_transitiondomain_status.py | 24 --------
.../migrations/0051_alter_domain_state.py | 30 ----------
.../0052_alter_domainapplication_status.py | 31 ----------
...alter_domainapplication_status_and_more.py | 60 +++++++++++++++++++
4 files changed, 60 insertions(+), 85 deletions(-)
delete mode 100644 src/registrar/migrations/0050_alter_transitiondomain_status.py
delete mode 100644 src/registrar/migrations/0051_alter_domain_state.py
delete mode 100644 src/registrar/migrations/0052_alter_domainapplication_status.py
create mode 100644 src/registrar/migrations/0053_alter_domain_state_alter_domainapplication_status_and_more.py
diff --git a/src/registrar/migrations/0050_alter_transitiondomain_status.py b/src/registrar/migrations/0050_alter_transitiondomain_status.py
deleted file mode 100644
index 72bebb7b5..000000000
--- a/src/registrar/migrations/0050_alter_transitiondomain_status.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Generated by Django 4.2.7 on 2023-12-01 21:57
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
- dependencies = [
- ("registrar", "0049_alter_domainapplication_current_websites_and_more"),
- ]
-
- operations = [
- migrations.AlterField(
- model_name="transitiondomain",
- name="status",
- field=models.CharField(
- blank=True,
- choices=[("ready", "Ready"), ("on hold", "On hold"), ("unknown", "Unknown")],
- default="ready",
- help_text="domain status during the transfer",
- max_length=255,
- verbose_name="Status",
- ),
- ),
- ]
diff --git a/src/registrar/migrations/0051_alter_domain_state.py b/src/registrar/migrations/0051_alter_domain_state.py
deleted file mode 100644
index df1240d93..000000000
--- a/src/registrar/migrations/0051_alter_domain_state.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# Generated by Django 4.2.7 on 2023-12-05 21:21
-
-from django.db import migrations
-import django_fsm
-
-
-class Migration(migrations.Migration):
- dependencies = [
- ("registrar", "0050_alter_transitiondomain_status"),
- ]
-
- operations = [
- migrations.AlterField(
- model_name="domain",
- name="state",
- field=django_fsm.FSMField(
- choices=[
- ("unknown", "Unknown"),
- ("dns needed", "Dns needed"),
- ("ready", "Ready"),
- ("on hold", "On hold"),
- ("deleted", "Deleted"),
- ],
- default="unknown",
- help_text="Very basic info about the lifecycle of this domain object",
- max_length=21,
- protected=True,
- ),
- ),
- ]
diff --git a/src/registrar/migrations/0052_alter_domainapplication_status.py b/src/registrar/migrations/0052_alter_domainapplication_status.py
deleted file mode 100644
index 5a274d731..000000000
--- a/src/registrar/migrations/0052_alter_domainapplication_status.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# Generated by Django 4.2.7 on 2023-12-05 21:25
-
-from django.db import migrations
-import django_fsm
-
-
-class Migration(migrations.Migration):
- dependencies = [
- ("registrar", "0051_alter_domain_state"),
- ]
-
- operations = [
- migrations.AlterField(
- model_name="domainapplication",
- name="status",
- field=django_fsm.FSMField(
- choices=[
- ("started", "Started"),
- ("submitted", "Submitted"),
- ("in review", "In review"),
- ("action needed", "Action needed"),
- ("approved", "Approved"),
- ("withdrawn", "Withdrawn"),
- ("rejected", "Rejected"),
- ("ineligible", "Ineligible"),
- ],
- default="started",
- max_length=50,
- ),
- ),
- ]
diff --git a/src/registrar/migrations/0053_alter_domain_state_alter_domainapplication_status_and_more.py b/src/registrar/migrations/0053_alter_domain_state_alter_domainapplication_status_and_more.py
new file mode 100644
index 000000000..856ce7f0f
--- /dev/null
+++ b/src/registrar/migrations/0053_alter_domain_state_alter_domainapplication_status_and_more.py
@@ -0,0 +1,60 @@
+# Generated by Django 4.2.7 on 2023-12-05 22:07
+
+from django.db import migrations, models
+import django_fsm
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("registrar", "0052_alter_domainapplication_anything_else_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="domain",
+ name="state",
+ field=django_fsm.FSMField(
+ choices=[
+ ("unknown", "Unknown"),
+ ("dns needed", "Dns needed"),
+ ("ready", "Ready"),
+ ("on hold", "On hold"),
+ ("deleted", "Deleted"),
+ ],
+ default="unknown",
+ help_text="Very basic info about the lifecycle of this domain object",
+ max_length=21,
+ protected=True,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domainapplication",
+ name="status",
+ field=django_fsm.FSMField(
+ choices=[
+ ("started", "Started"),
+ ("submitted", "Submitted"),
+ ("in review", "In review"),
+ ("action needed", "Action needed"),
+ ("approved", "Approved"),
+ ("withdrawn", "Withdrawn"),
+ ("rejected", "Rejected"),
+ ("ineligible", "Ineligible"),
+ ],
+ default="started",
+ max_length=50,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="transitiondomain",
+ name="status",
+ field=models.CharField(
+ blank=True,
+ choices=[("ready", "Ready"), ("on hold", "On hold"), ("unknown", "Unknown")],
+ default="ready",
+ help_text="domain status during the transfer",
+ max_length=255,
+ verbose_name="Status",
+ ),
+ ),
+ ]
From d3b732f7350e68e63891509f72e503cb9ca9910b Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Tue, 5 Dec 2023 19:18:11 -0500
Subject: [PATCH 12/54] Revise Domain Application model to change the status
constants to use tuples
---
src/registrar/admin.py | 26 +++---
src/registrar/assets/js/get-gov-admin.js | 30 +++---
src/registrar/fixtures_applications.py | 2 +-
.../0054_alter_domainapplication_status.py | 31 +++++++
src/registrar/models/domain_application.py | 59 +++++-------
src/registrar/tests/common.py | 14 +--
src/registrar/tests/test_admin.py | 54 +++++------
src/registrar/tests/test_models.py | 92 +++++++++----------
src/registrar/tests/test_models_domain.py | 2 +-
src/registrar/tests/test_views.py | 12 +--
src/registrar/views/application.py | 12 +--
src/registrar/views/utility/mixins.py | 8 +-
12 files changed, 182 insertions(+), 160 deletions(-)
create mode 100644 src/registrar/migrations/0054_alter_domainapplication_status.py
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index aca246a08..3db706c94 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -515,14 +515,14 @@ class DomainApplicationAdminForm(forms.ModelForm):
current_state = application.status
# first option in status transitions is current state
- available_transitions = [(current_state, current_state)]
+ available_transitions = [(current_state, application.get_status_display())]
transitions = get_available_FIELD_transitions(
application, models.DomainApplication._meta.get_field("status")
)
for transition in transitions:
- available_transitions.append((transition.target, transition.target))
+ available_transitions.append((transition.target, transition.target.label))
# only set the available transitions if the user is not restricted
# from editing the domain application; otherwise, the form will be
@@ -638,10 +638,10 @@ class DomainApplicationAdmin(ListHeaderAdmin):
if (
obj
- and original_obj.status == models.DomainApplication.APPROVED
+ and original_obj.status == models.DomainApplication.ApplicationStatus.APPROVED
and (
- obj.status == models.DomainApplication.REJECTED
- or obj.status == models.DomainApplication.INELIGIBLE
+ obj.status == models.DomainApplication.ApplicationStatus.REJECTED
+ or obj.status == models.DomainApplication.ApplicationStatus.INELIGIBLE
)
and not obj.domain_is_not_active()
):
@@ -663,14 +663,14 @@ class DomainApplicationAdmin(ListHeaderAdmin):
else:
if obj.status != original_obj.status:
status_method_mapping = {
- models.DomainApplication.STARTED: None,
- models.DomainApplication.SUBMITTED: obj.submit,
- models.DomainApplication.IN_REVIEW: obj.in_review,
- models.DomainApplication.ACTION_NEEDED: obj.action_needed,
- models.DomainApplication.APPROVED: obj.approve,
- models.DomainApplication.WITHDRAWN: obj.withdraw,
- models.DomainApplication.REJECTED: obj.reject,
- models.DomainApplication.INELIGIBLE: (obj.reject_with_prejudice),
+ models.DomainApplication.ApplicationStatus.STARTED: None,
+ models.DomainApplication.ApplicationStatus.SUBMITTED: obj.submit,
+ models.DomainApplication.ApplicationStatus.IN_REVIEW: obj.in_review,
+ models.DomainApplication.ApplicationStatus.ACTION_NEEDED: obj.action_needed,
+ models.DomainApplication.ApplicationStatus.APPROVED: obj.approve,
+ models.DomainApplication.ApplicationStatus.WITHDRAWN: obj.withdraw,
+ models.DomainApplication.ApplicationStatus.REJECTED: obj.reject,
+ models.DomainApplication.ApplicationStatus.INELIGIBLE: (obj.reject_with_prejudice),
}
selected_method = status_method_mapping.get(obj.status)
if selected_method is None:
diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js
index bdbd89c23..7b92f1192 100644
--- a/src/registrar/assets/js/get-gov-admin.js
+++ b/src/registrar/assets/js/get-gov-admin.js
@@ -65,22 +65,22 @@ function openInNewTab(el, removeAttribute = false){
/** An IIFE to capitalize statuses in the Domain Application status dropdown
*/
-(function (){
- function capitalizeFirstLetterInDropdownOptions(dropdown_id) {
- // Grabs the status dropdown
- var selectElement = document.getElementById(dropdown_id);
- if (selectElement) {
- var options = selectElement.options;
- // Loop through each option, and convert to sentence case
- for (var i = 0; i < options.length; i++) {
- var option = options[i];
- option.text = option.text.charAt(0).toUpperCase() + option.text.slice(1).toLowerCase();
- }
- }
- }
+// (function (){
+// function capitalizeFirstLetterInDropdownOptions(dropdown_id) {
+// // Grabs the status dropdown
+// var selectElement = document.getElementById(dropdown_id);
+// if (selectElement) {
+// var options = selectElement.options;
+// // Loop through each option, and convert to sentence case
+// for (var i = 0; i < options.length; i++) {
+// var option = options[i];
+// option.text = option.text.charAt(0).toUpperCase() + option.text.slice(1).toLowerCase();
+// }
+// }
+// }
- capitalizeFirstLetterInDropdownOptions('id_status');
-})();
+// capitalizeFirstLetterInDropdownOptions('id_status');
+// })();
// Function to check for the existence of the "to" select list element in the DOM, and if and when found,
// initialize the associated widget
diff --git a/src/registrar/fixtures_applications.py b/src/registrar/fixtures_applications.py
index 679e5f7fe..7f6a63719 100644
--- a/src/registrar/fixtures_applications.py
+++ b/src/registrar/fixtures_applications.py
@@ -214,7 +214,7 @@ class DomainFixture(DomainApplicationFixture):
for user in users:
# approve one of each users in review status domains
- application = DomainApplication.objects.filter(creator=user, status=DomainApplication.IN_REVIEW).last()
+ application = DomainApplication.objects.filter(creator=user, status=DomainApplication.ApplicationStatus.IN_REVIEW).last()
logger.debug(f"Approving {application} for {user}")
application.approve()
application.save()
diff --git a/src/registrar/migrations/0054_alter_domainapplication_status.py b/src/registrar/migrations/0054_alter_domainapplication_status.py
new file mode 100644
index 000000000..c0c427179
--- /dev/null
+++ b/src/registrar/migrations/0054_alter_domainapplication_status.py
@@ -0,0 +1,31 @@
+# Generated by Django 4.2.7 on 2023-12-05 23:14
+
+from django.db import migrations
+import django_fsm
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("registrar", "0053_alter_domain_state_alter_domainapplication_status_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="domainapplication",
+ name="status",
+ field=django_fsm.FSMField(
+ choices=[
+ ("started", "Started"),
+ ("submitted", "Submitted"),
+ ("in_review", "In review"),
+ ("action_needed", "Action needed"),
+ ("approved", "Approved"),
+ ("withdrawn", "Withdrawn"),
+ ("rejected", "Rejected"),
+ ("ineligible", "Ineligible"),
+ ],
+ default="started",
+ max_length=50,
+ ),
+ ),
+ ]
diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py
index 8d9083573..32cd239dc 100644
--- a/src/registrar/models/domain_application.py
+++ b/src/registrar/models/domain_application.py
@@ -18,26 +18,17 @@ logger = logging.getLogger(__name__)
class DomainApplication(TimeStampedModel):
"""A registrant's application for a new domain."""
-
- # #### Constants for choice fields ####
- STARTED = "started"
- SUBMITTED = "submitted"
- IN_REVIEW = "in review"
- ACTION_NEEDED = "action needed"
- APPROVED = "approved"
- WITHDRAWN = "withdrawn"
- REJECTED = "rejected"
- INELIGIBLE = "ineligible"
- STATUS_CHOICES = [
- (STARTED, STARTED.capitalize()),
- (SUBMITTED, SUBMITTED.capitalize()),
- (IN_REVIEW, IN_REVIEW.capitalize()),
- (ACTION_NEEDED, ACTION_NEEDED.capitalize()),
- (APPROVED, APPROVED.capitalize()),
- (WITHDRAWN, WITHDRAWN.capitalize()),
- (REJECTED, REJECTED.capitalize()),
- (INELIGIBLE, INELIGIBLE.capitalize()),
- ]
+
+ # Constants for choice fields
+ class ApplicationStatus(models.TextChoices):
+ STARTED = "started", "Started"
+ SUBMITTED = "submitted", "Submitted"
+ IN_REVIEW = "in_review", "In review"
+ ACTION_NEEDED = "action_needed", "Action needed"
+ APPROVED = "approved", "Approved"
+ WITHDRAWN = "withdrawn", "Withdrawn"
+ REJECTED = "rejected", "Rejected"
+ INELIGIBLE = "ineligible", "Ineligible"
class StateTerritoryChoices(models.TextChoices):
ALABAMA = "AL", "Alabama (AL)"
@@ -363,8 +354,8 @@ class DomainApplication(TimeStampedModel):
# #### Internal fields about the application #####
status = FSMField(
- choices=STATUS_CHOICES, # possible states as an array of constants
- default=STARTED, # sensible default
+ choices=ApplicationStatus.choices, # possible states as an array of constants
+ default=ApplicationStatus.STARTED, # sensible default
protected=False, # can change state directly, particularly in Django admin
)
# This is the application user who created this application. The contact
@@ -590,7 +581,7 @@ class DomainApplication(TimeStampedModel):
except EmailSendingError:
logger.warning("Failed to send confirmation email", exc_info=True)
- @transition(field="status", source=[STARTED, ACTION_NEEDED, WITHDRAWN], target=SUBMITTED)
+ @transition(field="status", source=[ApplicationStatus.STARTED, ApplicationStatus.ACTION_NEEDED, ApplicationStatus.WITHDRAWN], target=ApplicationStatus.SUBMITTED)
def submit(self):
"""Submit an application that is started.
@@ -616,7 +607,7 @@ class DomainApplication(TimeStampedModel):
"emails/submission_confirmation_subject.txt",
)
- @transition(field="status", source=SUBMITTED, target=IN_REVIEW)
+ @transition(field="status", source=ApplicationStatus.SUBMITTED, target=ApplicationStatus.IN_REVIEW)
def in_review(self):
"""Investigate an application that has been submitted.
@@ -628,7 +619,7 @@ class DomainApplication(TimeStampedModel):
"emails/status_change_in_review_subject.txt",
)
- @transition(field="status", source=[IN_REVIEW, REJECTED], target=ACTION_NEEDED)
+ @transition(field="status", source=[ApplicationStatus.IN_REVIEW, ApplicationStatus.REJECTED], target=ApplicationStatus.ACTION_NEEDED)
def action_needed(self):
"""Send back an application that is under investigation or rejected.
@@ -642,8 +633,8 @@ class DomainApplication(TimeStampedModel):
@transition(
field="status",
- source=[SUBMITTED, IN_REVIEW, REJECTED, INELIGIBLE],
- target=APPROVED,
+ source=[ApplicationStatus.SUBMITTED, ApplicationStatus.IN_REVIEW, ApplicationStatus.REJECTED, ApplicationStatus.INELIGIBLE],
+ target=ApplicationStatus.APPROVED,
)
def approve(self):
"""Approve an application that has been submitted.
@@ -676,7 +667,7 @@ class DomainApplication(TimeStampedModel):
"emails/status_change_approved_subject.txt",
)
- @transition(field="status", source=[SUBMITTED, IN_REVIEW], target=WITHDRAWN)
+ @transition(field="status", source=[ApplicationStatus.SUBMITTED, ApplicationStatus.IN_REVIEW], target=ApplicationStatus.WITHDRAWN)
def withdraw(self):
"""Withdraw an application that has been submitted."""
self._send_status_update_email(
@@ -687,8 +678,8 @@ class DomainApplication(TimeStampedModel):
@transition(
field="status",
- source=[IN_REVIEW, APPROVED],
- target=REJECTED,
+ source=[ApplicationStatus.IN_REVIEW, ApplicationStatus.APPROVED],
+ target=ApplicationStatus.REJECTED,
conditions=[domain_is_not_active],
)
def reject(self):
@@ -696,7 +687,7 @@ class DomainApplication(TimeStampedModel):
As side effects this will delete the domain and domain_information
(will cascade), and send an email notification."""
- if self.status == self.APPROVED:
+ if self.status == self.ApplicationStatus.APPROVED:
domain_state = self.approved_domain.state
# Only reject if it exists on EPP
if domain_state != Domain.State.UNKNOWN:
@@ -712,8 +703,8 @@ class DomainApplication(TimeStampedModel):
@transition(
field="status",
- source=[IN_REVIEW, APPROVED],
- target=INELIGIBLE,
+ source=[ApplicationStatus.IN_REVIEW, ApplicationStatus.APPROVED],
+ target=ApplicationStatus.INELIGIBLE,
conditions=[domain_is_not_active],
)
def reject_with_prejudice(self):
@@ -725,7 +716,7 @@ class DomainApplication(TimeStampedModel):
permissions classes test against. This will also delete the domain
and domain_information (will cascade) when they exist."""
- if self.status == self.APPROVED:
+ if self.status == self.ApplicationStatus.APPROVED:
domain_state = self.approved_domain.state
# Only reject if it exists on EPP
if domain_state != Domain.State.UNKNOWN:
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index f17e0f9fa..89967ee90 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -294,7 +294,7 @@ class AuditedAdminMockData:
self,
domain_type,
item_name,
- status=DomainApplication.STARTED,
+ status=DomainApplication.ApplicationStatus.STARTED,
org_type="federal",
federal_type="executive",
purpose="Purpose of the site",
@@ -311,7 +311,7 @@ class AuditedAdminMockData:
title, email, and username.
status (str - optional): Defines the status for DomainApplication,
- e.g. DomainApplication.STARTED
+ e.g. DomainApplication.ApplicationStatus.STARTED
org_type (str - optional): Sets a domains org_type
@@ -348,19 +348,19 @@ class AuditedAdminMockData:
)
return full_arg_dict
- def create_full_dummy_domain_application(self, item_name, status=DomainApplication.STARTED):
+ def create_full_dummy_domain_application(self, item_name, status=DomainApplication.ApplicationStatus.STARTED):
"""Creates a dummy domain application object"""
domain_application_kwargs = self.dummy_kwarg_boilerplate(self.APPLICATION, item_name, status)
application = DomainApplication.objects.get_or_create(**domain_application_kwargs)[0]
return application
- def create_full_dummy_domain_information(self, item_name, status=DomainApplication.STARTED):
+ def create_full_dummy_domain_information(self, item_name, status=DomainApplication.ApplicationStatus.STARTED):
"""Creates a dummy domain information object"""
domain_application_kwargs = self.dummy_kwarg_boilerplate(self.INFORMATION, item_name, status)
application = DomainInformation.objects.get_or_create(**domain_application_kwargs)[0]
return application
- def create_full_dummy_domain_invitation(self, item_name, status=DomainApplication.STARTED):
+ def create_full_dummy_domain_invitation(self, item_name, status=DomainApplication.ApplicationStatus.STARTED):
"""Creates a dummy domain invitation object"""
domain_application_kwargs = self.dummy_kwarg_boilerplate(self.INVITATION, item_name, status)
application = DomainInvitation.objects.get_or_create(**domain_application_kwargs)[0]
@@ -374,7 +374,7 @@ class AuditedAdminMockData:
has_other_contacts=True,
has_current_website=True,
has_alternative_gov_domain=True,
- status=DomainApplication.STARTED,
+ status=DomainApplication.ApplicationStatus.STARTED,
):
"""A helper to create a dummy domain application object"""
application = None
@@ -455,7 +455,7 @@ def completed_application(
has_alternative_gov_domain=True,
has_about_your_organization=True,
has_anything_else=True,
- status=DomainApplication.STARTED,
+ status=DomainApplication.ApplicationStatus.STARTED,
user=False,
name="city.gov",
):
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 526a9ea2a..5db01492a 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -61,7 +61,7 @@ class TestDomainAdmin(MockEppLib):
Make sure the short name is displaying in admin on the list page
"""
self.client.force_login(self.superuser)
- application = completed_application(status=DomainApplication.IN_REVIEW)
+ application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
application.approve()
response = self.client.get("/admin/registrar/domain/")
@@ -282,7 +282,7 @@ class TestDomainApplicationAdminForm(TestCase):
form = DomainApplicationAdminForm(instance=self.application)
# Verify that the form choices match the available transitions for started
- expected_choices = [("started", "started"), ("submitted", "submitted")]
+ expected_choices = [("started", "Started"), ("submitted", "Submitted")]
self.assertEqual(form.fields["status"].widget.choices, expected_choices)
def test_form_choices_when_no_instance(self):
@@ -355,7 +355,7 @@ class TestDomainApplicationAdmin(MockEppLib):
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
# Modify the application's property
- application.status = DomainApplication.SUBMITTED
+ application.status = DomainApplication.ApplicationStatus.SUBMITTED
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
@@ -390,13 +390,13 @@ class TestDomainApplicationAdmin(MockEppLib):
with boto3_mocking.clients.handler_for("sesv2", mock_client):
# Create a sample application
- application = completed_application(status=DomainApplication.SUBMITTED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
# Create a mock request
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
# Modify the application's property
- application.status = DomainApplication.IN_REVIEW
+ application.status = DomainApplication.ApplicationStatus.IN_REVIEW
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
@@ -431,13 +431,13 @@ class TestDomainApplicationAdmin(MockEppLib):
with boto3_mocking.clients.handler_for("sesv2", mock_client):
# Create a sample application
- application = completed_application(status=DomainApplication.IN_REVIEW)
+ application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Create a mock request
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
# Modify the application's property
- application.status = DomainApplication.APPROVED
+ application.status = DomainApplication.ApplicationStatus.APPROVED
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
@@ -467,13 +467,13 @@ class TestDomainApplicationAdmin(MockEppLib):
User.objects.filter(email=EMAIL).delete()
# Create a sample application
- application = completed_application(status=DomainApplication.IN_REVIEW)
+ application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Create a mock request
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
# Modify the application's property
- application.status = DomainApplication.APPROVED
+ application.status = DomainApplication.ApplicationStatus.APPROVED
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
@@ -492,13 +492,13 @@ class TestDomainApplicationAdmin(MockEppLib):
with boto3_mocking.clients.handler_for("sesv2", mock_client):
# Create a sample application
- application = completed_application(status=DomainApplication.IN_REVIEW)
+ application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Create a mock request
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
# Modify the application's property
- application.status = DomainApplication.ACTION_NEEDED
+ application.status = DomainApplication.ApplicationStatus.ACTION_NEEDED
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
@@ -533,13 +533,13 @@ class TestDomainApplicationAdmin(MockEppLib):
with boto3_mocking.clients.handler_for("sesv2", mock_client):
# Create a sample application
- application = completed_application(status=DomainApplication.IN_REVIEW)
+ application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Create a mock request
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
# Modify the application's property
- application.status = DomainApplication.REJECTED
+ application.status = DomainApplication.ApplicationStatus.REJECTED
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
@@ -569,13 +569,13 @@ class TestDomainApplicationAdmin(MockEppLib):
User.objects.filter(email=EMAIL).delete()
# Create a sample application
- application = completed_application(status=DomainApplication.IN_REVIEW)
+ application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Create a mock request
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
# Modify the application's property
- application.status = DomainApplication.INELIGIBLE
+ application.status = DomainApplication.ApplicationStatus.INELIGIBLE
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
@@ -584,7 +584,7 @@ class TestDomainApplicationAdmin(MockEppLib):
self.assertEqual(application.creator.status, "restricted")
def test_readonly_when_restricted_creator(self):
- application = completed_application(status=DomainApplication.IN_REVIEW)
+ application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
application.creator.status = User.RESTRICTED
application.creator.save()
@@ -662,7 +662,7 @@ class TestDomainApplicationAdmin(MockEppLib):
def test_saving_when_restricted_creator(self):
# Create an instance of the model
- application = completed_application(status=DomainApplication.IN_REVIEW)
+ application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
application.creator.status = User.RESTRICTED
application.creator.save()
@@ -681,11 +681,11 @@ class TestDomainApplicationAdmin(MockEppLib):
)
# Assert that the status has not changed
- self.assertEqual(application.status, DomainApplication.IN_REVIEW)
+ self.assertEqual(application.status, DomainApplication.ApplicationStatus.IN_REVIEW)
def test_change_view_with_restricted_creator(self):
# Create an instance of the model
- application = completed_application(status=DomainApplication.IN_REVIEW)
+ application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
application.creator.status = User.RESTRICTED
application.creator.save()
@@ -704,7 +704,7 @@ class TestDomainApplicationAdmin(MockEppLib):
def test_error_when_saving_approved_to_rejected_and_domain_is_active(self):
# Create an instance of the model
- application = completed_application(status=DomainApplication.APPROVED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
application.approved_domain = domain
application.save()
@@ -724,7 +724,7 @@ class TestDomainApplicationAdmin(MockEppLib):
stack.enter_context(patch.object(messages, "error"))
# Simulate saving the model
- application.status = DomainApplication.REJECTED
+ application.status = DomainApplication.ApplicationStatus.REJECTED
self.admin.save_model(request, application, None, True)
# Assert that the error message was called with the correct argument
@@ -735,7 +735,7 @@ class TestDomainApplicationAdmin(MockEppLib):
def test_side_effects_when_saving_approved_to_rejected(self):
# Create an instance of the model
- application = completed_application(status=DomainApplication.APPROVED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
domain_information = DomainInformation.objects.create(creator=self.superuser, domain=domain)
application.approved_domain = domain
@@ -756,7 +756,7 @@ class TestDomainApplicationAdmin(MockEppLib):
stack.enter_context(patch.object(messages, "error"))
# Simulate saving the model
- application.status = DomainApplication.REJECTED
+ application.status = DomainApplication.ApplicationStatus.REJECTED
self.admin.save_model(request, application, None, True)
# Assert that the error message was never called
@@ -774,7 +774,7 @@ class TestDomainApplicationAdmin(MockEppLib):
def test_error_when_saving_approved_to_ineligible_and_domain_is_active(self):
# Create an instance of the model
- application = completed_application(status=DomainApplication.APPROVED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
application.approved_domain = domain
application.save()
@@ -794,7 +794,7 @@ class TestDomainApplicationAdmin(MockEppLib):
stack.enter_context(patch.object(messages, "error"))
# Simulate saving the model
- application.status = DomainApplication.INELIGIBLE
+ application.status = DomainApplication.ApplicationStatus.INELIGIBLE
self.admin.save_model(request, application, None, True)
# Assert that the error message was called with the correct argument
@@ -805,7 +805,7 @@ class TestDomainApplicationAdmin(MockEppLib):
def test_side_effects_when_saving_approved_to_ineligible(self):
# Create an instance of the model
- application = completed_application(status=DomainApplication.APPROVED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
domain_information = DomainInformation.objects.create(creator=self.superuser, domain=domain)
application.approved_domain = domain
@@ -826,7 +826,7 @@ class TestDomainApplicationAdmin(MockEppLib):
stack.enter_context(patch.object(messages, "error"))
# Simulate saving the model
- application.status = DomainApplication.INELIGIBLE
+ application.status = DomainApplication.ApplicationStatus.INELIGIBLE
self.admin.save_model(request, application, None, True)
# Assert that the error message was never called
diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py
index d397cb129..c2820696d 100644
--- a/src/registrar/tests/test_models.py
+++ b/src/registrar/tests/test_models.py
@@ -35,7 +35,7 @@ class TestDomainApplication(TestCase):
"""Can create with just a creator."""
user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(creator=user)
- self.assertEqual(application.status, DomainApplication.STARTED)
+ self.assertEqual(application.status, DomainApplication.ApplicationStatus.STARTED)
def test_full_create(self):
"""Can create with all fields."""
@@ -108,7 +108,7 @@ class TestDomainApplication(TestCase):
# no submitter email so this emits a log warning
with less_console_noise():
application.submit()
- self.assertEqual(application.status, application.SUBMITTED)
+ self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED)
def test_submit_sends_email(self):
"""Create an application and submit it and see if email was sent."""
@@ -139,7 +139,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status submitted and call submit
against transition rules"""
- application = completed_application(status=DomainApplication.SUBMITTED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
with self.assertRaises(TransitionNotAllowed):
application.submit()
@@ -148,7 +148,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status in review and call submit
against transition rules"""
- application = completed_application(status=DomainApplication.IN_REVIEW)
+ application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
with self.assertRaises(TransitionNotAllowed):
application.submit()
@@ -157,7 +157,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status approved and call submit
against transition rules"""
- application = completed_application(status=DomainApplication.APPROVED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
with self.assertRaises(TransitionNotAllowed):
application.submit()
@@ -166,7 +166,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status rejected and call submit
against transition rules"""
- application = completed_application(status=DomainApplication.REJECTED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
with self.assertRaises(TransitionNotAllowed):
application.submit()
@@ -175,7 +175,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status ineligible and call submit
against transition rules"""
- application = completed_application(status=DomainApplication.INELIGIBLE)
+ application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.submit()
@@ -184,7 +184,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status started and call in_review
against transition rules"""
- application = completed_application(status=DomainApplication.STARTED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
with self.assertRaises(TransitionNotAllowed):
application.in_review()
@@ -193,7 +193,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status in review and call in_review
against transition rules"""
- application = completed_application(status=DomainApplication.IN_REVIEW)
+ application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
with self.assertRaises(TransitionNotAllowed):
application.in_review()
@@ -202,7 +202,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status approved and call in_review
against transition rules"""
- application = completed_application(status=DomainApplication.APPROVED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
with self.assertRaises(TransitionNotAllowed):
application.in_review()
@@ -211,7 +211,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status action needed and call in_review
against transition rules"""
- application = completed_application(status=DomainApplication.ACTION_NEEDED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
with self.assertRaises(TransitionNotAllowed):
application.in_review()
@@ -220,7 +220,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status rejected and call in_review
against transition rules"""
- application = completed_application(status=DomainApplication.REJECTED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
with self.assertRaises(TransitionNotAllowed):
application.in_review()
@@ -229,7 +229,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status withdrawn and call in_review
against transition rules"""
- application = completed_application(status=DomainApplication.WITHDRAWN)
+ application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
with self.assertRaises(TransitionNotAllowed):
application.in_review()
@@ -238,7 +238,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status ineligible and call in_review
against transition rules"""
- application = completed_application(status=DomainApplication.INELIGIBLE)
+ application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.in_review()
@@ -247,7 +247,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status started and call action_needed
against transition rules"""
- application = completed_application(status=DomainApplication.STARTED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
with self.assertRaises(TransitionNotAllowed):
application.action_needed()
@@ -256,7 +256,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status submitted and call action_needed
against transition rules"""
- application = completed_application(status=DomainApplication.SUBMITTED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
with self.assertRaises(TransitionNotAllowed):
application.action_needed()
@@ -265,7 +265,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status action needed and call action_needed
against transition rules"""
- application = completed_application(status=DomainApplication.ACTION_NEEDED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
with self.assertRaises(TransitionNotAllowed):
application.action_needed()
@@ -274,7 +274,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status approved and call action_needed
against transition rules"""
- application = completed_application(status=DomainApplication.APPROVED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
with self.assertRaises(TransitionNotAllowed):
application.action_needed()
@@ -283,7 +283,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status withdrawn and call action_needed
against transition rules"""
- application = completed_application(status=DomainApplication.WITHDRAWN)
+ application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
with self.assertRaises(TransitionNotAllowed):
application.action_needed()
@@ -292,7 +292,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status ineligible and call action_needed
against transition rules"""
- application = completed_application(status=DomainApplication.INELIGIBLE)
+ application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.action_needed()
@@ -301,7 +301,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status started and call approve
against transition rules"""
- application = completed_application(status=DomainApplication.STARTED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
with self.assertRaises(TransitionNotAllowed):
application.approve()
@@ -310,7 +310,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status approved and call approve
against transition rules"""
- application = completed_application(status=DomainApplication.APPROVED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
with self.assertRaises(TransitionNotAllowed):
application.approve()
@@ -319,7 +319,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status action needed and call approve
against transition rules"""
- application = completed_application(status=DomainApplication.ACTION_NEEDED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
with self.assertRaises(TransitionNotAllowed):
application.approve()
@@ -328,7 +328,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status withdrawn and call approve
against transition rules"""
- application = completed_application(status=DomainApplication.WITHDRAWN)
+ application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
with self.assertRaises(TransitionNotAllowed):
application.approve()
@@ -337,7 +337,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status started and call withdraw
against transition rules"""
- application = completed_application(status=DomainApplication.STARTED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
with self.assertRaises(TransitionNotAllowed):
application.withdraw()
@@ -346,7 +346,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status approved and call withdraw
against transition rules"""
- application = completed_application(status=DomainApplication.APPROVED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
with self.assertRaises(TransitionNotAllowed):
application.withdraw()
@@ -355,7 +355,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status action needed and call withdraw
against transition rules"""
- application = completed_application(status=DomainApplication.ACTION_NEEDED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
with self.assertRaises(TransitionNotAllowed):
application.withdraw()
@@ -364,7 +364,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status rejected and call withdraw
against transition rules"""
- application = completed_application(status=DomainApplication.REJECTED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
with self.assertRaises(TransitionNotAllowed):
application.withdraw()
@@ -373,7 +373,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status withdrawn and call withdraw
against transition rules"""
- application = completed_application(status=DomainApplication.WITHDRAWN)
+ application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
with self.assertRaises(TransitionNotAllowed):
application.withdraw()
@@ -382,7 +382,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status ineligible and call withdraw
against transition rules"""
- application = completed_application(status=DomainApplication.INELIGIBLE)
+ application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.withdraw()
@@ -391,7 +391,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status started and call reject
against transition rules"""
- application = completed_application(status=DomainApplication.STARTED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
with self.assertRaises(TransitionNotAllowed):
application.reject()
@@ -400,7 +400,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status submitted and call reject
against transition rules"""
- application = completed_application(status=DomainApplication.SUBMITTED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
with self.assertRaises(TransitionNotAllowed):
application.reject()
@@ -409,7 +409,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status action needed and call reject
against transition rules"""
- application = completed_application(status=DomainApplication.ACTION_NEEDED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
with self.assertRaises(TransitionNotAllowed):
application.reject()
@@ -418,7 +418,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status withdrawn and call reject
against transition rules"""
- application = completed_application(status=DomainApplication.WITHDRAWN)
+ application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
with self.assertRaises(TransitionNotAllowed):
application.reject()
@@ -427,7 +427,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status rejected and call reject
against transition rules"""
- application = completed_application(status=DomainApplication.REJECTED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
with self.assertRaises(TransitionNotAllowed):
application.reject()
@@ -436,7 +436,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status ineligible and call reject
against transition rules"""
- application = completed_application(status=DomainApplication.INELIGIBLE)
+ application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.reject()
@@ -445,7 +445,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status approved, create a matching domain that
is active, and call reject against transition rules"""
- application = completed_application(status=DomainApplication.APPROVED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
application.approved_domain = domain
application.save()
@@ -464,7 +464,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status started and call reject
against transition rules"""
- application = completed_application(status=DomainApplication.STARTED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.STARTED)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
@@ -473,7 +473,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status submitted and call reject
against transition rules"""
- application = completed_application(status=DomainApplication.SUBMITTED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
@@ -482,7 +482,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status action needed and call reject
against transition rules"""
- application = completed_application(status=DomainApplication.ACTION_NEEDED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
@@ -491,7 +491,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status withdrawn and call reject
against transition rules"""
- application = completed_application(status=DomainApplication.WITHDRAWN)
+ application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
@@ -500,7 +500,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status rejected and call reject
against transition rules"""
- application = completed_application(status=DomainApplication.REJECTED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
@@ -509,7 +509,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status ineligible and call reject
against transition rules"""
- application = completed_application(status=DomainApplication.INELIGIBLE)
+ application = completed_application(status=DomainApplication.ApplicationStatus.INELIGIBLE)
with self.assertRaises(TransitionNotAllowed):
application.reject_with_prejudice()
@@ -518,7 +518,7 @@ class TestDomainApplication(TestCase):
"""Create an application with status approved, create a matching domain that
is active, and call reject_with_prejudice against transition rules"""
- application = completed_application(status=DomainApplication.APPROVED)
+ application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
application.approved_domain = domain
application.save()
@@ -543,7 +543,7 @@ class TestPermissions(TestCase):
user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain)
# skip using the submit method
- application.status = DomainApplication.SUBMITTED
+ application.status = DomainApplication.ApplicationStatus.SUBMITTED
application.approve()
# should be a role for this user
@@ -560,7 +560,7 @@ class TestDomainInfo(TestCase):
user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain)
# skip using the submit method
- application.status = DomainApplication.SUBMITTED
+ application.status = DomainApplication.ApplicationStatus.SUBMITTED
application.approve()
# should be an information present for this domain
diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py
index fac1c0fcd..39f63c942 100644
--- a/src/registrar/tests/test_models_domain.py
+++ b/src/registrar/tests/test_models_domain.py
@@ -261,7 +261,7 @@ class TestDomainCreation(MockEppLib):
user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain)
# skip using the submit method
- application.status = DomainApplication.SUBMITTED
+ application.status = DomainApplication.ApplicationStatus.SUBMITTED
# transition to approve state
application.approve()
# should have information present for this domain
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py
index e0e2c8c97..b5b2f1f76 100644
--- a/src/registrar/tests/test_views.py
+++ b/src/registrar/tests/test_views.py
@@ -1079,7 +1079,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
Make sure the long name is displaying in the application summary
page (manage your application)
"""
- completed_application(status=DomainApplication.SUBMITTED, user=self.user)
+ completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user)
home_page = self.app.get("/")
self.assertContains(home_page, "city.gov")
# click the "Edit" link
@@ -2117,7 +2117,7 @@ class TestApplicationStatus(TestWithUser, WebTest):
def test_application_status(self):
"""Checking application status page"""
- application = completed_application(status=DomainApplication.SUBMITTED, user=self.user)
+ application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user)
application.save()
home_page = self.app.get("/")
@@ -2137,7 +2137,7 @@ class TestApplicationStatus(TestWithUser, WebTest):
self.user.status = "ineligible"
self.user.save()
- application = completed_application(status=DomainApplication.SUBMITTED, user=self.user)
+ application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user)
application.save()
home_page = self.app.get("/")
@@ -2152,7 +2152,7 @@ class TestApplicationStatus(TestWithUser, WebTest):
def test_application_withdraw(self):
"""Checking application status page"""
- application = completed_application(status=DomainApplication.SUBMITTED, user=self.user)
+ application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user)
application.save()
home_page = self.app.get("/")
@@ -2182,7 +2182,7 @@ class TestApplicationStatus(TestWithUser, WebTest):
def test_application_status_no_permissions(self):
"""Can't access applications without being the creator."""
- application = completed_application(status=DomainApplication.SUBMITTED, user=self.user)
+ application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user)
other_user = User()
other_user.save()
application.creator = other_user
@@ -2202,7 +2202,7 @@ class TestApplicationStatus(TestWithUser, WebTest):
def test_approved_application_not_in_active_requests(self):
"""An approved application is not shown in the Active
Requests table on home.html."""
- application = completed_application(status=DomainApplication.APPROVED, user=self.user)
+ application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED, user=self.user)
application.save()
home_page = self.app.get("/")
diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py
index 32633a89b..f6489bedf 100644
--- a/src/registrar/views/application.py
+++ b/src/registrar/views/application.py
@@ -293,9 +293,9 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
return self.pending_applications()
def approved_applications_exist(self):
- """Checks if user is creator of applications with APPROVED status"""
+ """Checks if user is creator of applications with ApplicationStatus.APPROVED status"""
approved_application_count = DomainApplication.objects.filter(
- creator=self.request.user, status=DomainApplication.APPROVED
+ creator=self.request.user, status=DomainApplication.ApplicationStatus.APPROVED
).count()
return approved_application_count > 0
@@ -308,11 +308,11 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
def pending_applications(self):
"""Returns a List of user's applications with one of the following states:
- SUBMITTED, IN_REVIEW, ACTION_NEEDED"""
- # if the current application has ACTION_NEEDED status, this check should not be performed
- if self.application.status == DomainApplication.ACTION_NEEDED:
+ ApplicationStatus.SUBMITTED, ApplicationStatus.IN_REVIEW, ApplicationStatus.ACTION_NEEDED"""
+ # if the current application has ApplicationStatus.ACTION_NEEDED status, this check should not be performed
+ if self.application.status == DomainApplication.ApplicationStatus.ACTION_NEEDED:
return []
- check_statuses = [DomainApplication.SUBMITTED, DomainApplication.IN_REVIEW, DomainApplication.ACTION_NEEDED]
+ check_statuses = [DomainApplication.ApplicationStatus.SUBMITTED, DomainApplication.ApplicationStatus.IN_REVIEW, DomainApplication.ApplicationStatus.ACTION_NEEDED]
return DomainApplication.objects.filter(creator=self.request.user, status__in=check_statuses)
def get_context_data(self):
diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py
index aaa2e849c..37c0f6e98 100644
--- a/src/registrar/views/utility/mixins.py
+++ b/src/registrar/views/utility/mixins.py
@@ -101,10 +101,10 @@ class DomainPermission(PermissionsLoginMixin):
# Analysts may manage domains, when they are in these statuses:
valid_domain_statuses = [
- DomainApplication.APPROVED,
- DomainApplication.IN_REVIEW,
- DomainApplication.REJECTED,
- DomainApplication.ACTION_NEEDED,
+ DomainApplication.ApplicationStatus.APPROVED,
+ DomainApplication.ApplicationStatus.IN_REVIEW,
+ DomainApplication.ApplicationStatus.REJECTED,
+ DomainApplication.ApplicationStatus.ACTION_NEEDED,
# Edge case - some domains do not have
# a status or DomainInformation... aka a status of 'None'.
# It is necessary to access those to correct errors.
From af1b598699d0c2463ae95b12c12b86ce44ffe2c7 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Tue, 5 Dec 2023 19:21:04 -0500
Subject: [PATCH 13/54] Complete Domain State options revisions to tuples
---
src/registrar/models/domain.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 16c918f84..632a6200d 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -122,20 +122,20 @@ class Domain(TimeStampedModel, DomainHelper):
"""These capture (some of) the states a domain object can be in."""
# the state is indeterminate
- UNKNOWN = "unknown"
+ UNKNOWN = "unknown", "Unknown"
# The domain object exists in the registry
# but nameservers don't exist for it yet
DNS_NEEDED = "dns needed", "Dns needed"
# Domain has had nameservers set, may or may not be active
- READY = "ready"
+ READY = "ready", "Ready"
# Registrar manually changed state to client hold
ON_HOLD = "on hold", "On hold"
# previously existed but has been deleted from the registry
- DELETED = "deleted"
+ DELETED = "deleted", "Deleted"
class Cache(property):
"""
From 00c04783b595b8d9283534e41c889c6f2862d046 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Tue, 5 Dec 2023 19:28:20 -0500
Subject: [PATCH 14/54] Change choices for Domain INvitation status to tuple
constants
---
.../commands/load_domain_invitations.py | 2 +-
src/registrar/models/domain_invitation.py | 16 ++++++++--------
src/registrar/models/user.py | 2 +-
src/registrar/tests/common.py | 2 +-
src/registrar/tests/test_models.py | 2 +-
5 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/src/registrar/management/commands/load_domain_invitations.py b/src/registrar/management/commands/load_domain_invitations.py
index 28eb09def..32a63d860 100644
--- a/src/registrar/management/commands/load_domain_invitations.py
+++ b/src/registrar/management/commands/load_domain_invitations.py
@@ -62,7 +62,7 @@ class Command(BaseCommand):
DomainInvitation(
email=email_address.lower(),
domain=domain,
- status=DomainInvitation.INVITED,
+ status=DomainInvitation.DomainInvitationStatus.INVITED,
)
)
logger.info("Creating %d invitations", len(to_create))
diff --git a/src/registrar/models/domain_invitation.py b/src/registrar/models/domain_invitation.py
index 1e0b7fec8..395244df5 100644
--- a/src/registrar/models/domain_invitation.py
+++ b/src/registrar/models/domain_invitation.py
@@ -15,8 +15,11 @@ logger = logging.getLogger(__name__)
class DomainInvitation(TimeStampedModel):
- INVITED = "invited"
- RETRIEVED = "retrieved"
+
+ # Constants for status field
+ class DomainInvitationStatus(models.TextChoices):
+ INVITED = "invited", "Invited"
+ RETRIEVED = "retrieved", "Retrieved"
email = models.EmailField(
null=False,
@@ -31,18 +34,15 @@ class DomainInvitation(TimeStampedModel):
)
status = FSMField(
- choices=[
- (INVITED, INVITED),
- (RETRIEVED, RETRIEVED),
- ],
- default=INVITED,
+ choices=DomainInvitationStatus.choices,
+ default=DomainInvitationStatus.INVITED,
protected=True, # can't alter state except through transition methods!
)
def __str__(self):
return f"Invitation for {self.email} on {self.domain} is {self.status}"
- @transition(field="status", source=INVITED, target=RETRIEVED)
+ @transition(field="status", source=DomainInvitationStatus.INVITED, target=DomainInvitationStatus.RETRIEVED)
def retrieve(self):
"""When an invitation is retrieved, create the corresponding permission.
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index 2daa3c253..346a97aa6 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -67,7 +67,7 @@ class User(AbstractUser):
def check_domain_invitations_on_login(self):
"""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.INVITED):
+ for invitation in DomainInvitation.objects.filter(email=self.email, status=DomainInvitation.DomainInvitationStatus.INVITED):
try:
invitation.retrieve()
invitation.save()
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index 89967ee90..f459cab60 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -344,7 +344,7 @@ class AuditedAdminMockData:
full_arg_dict = dict(
email="test_mail@mail.com",
domain=self.dummy_domain(item_name, True),
- status=DomainInvitation.INVITED,
+ status=DomainInvitation.DomainInvitationStatus.INVITED,
)
return full_arg_dict
diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py
index c2820696d..0a02ccb90 100644
--- a/src/registrar/tests/test_models.py
+++ b/src/registrar/tests/test_models.py
@@ -597,7 +597,7 @@ class TestInvitations(TestCase):
# this is not an error but does produce a console warning
with less_console_noise():
self.invitation.retrieve()
- self.assertEqual(self.invitation.status, DomainInvitation.RETRIEVED)
+ self.assertEqual(self.invitation.status, DomainInvitation.DomainInvitationStatus.RETRIEVED)
def test_retrieve_on_each_login(self):
"""A user's authenticate on_each_login callback retrieves their invitations."""
From a27eb7c3ae3ee54bd4583b61ad806407fd3bbaf6 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Tue, 5 Dec 2023 19:38:19 -0500
Subject: [PATCH 15/54] Fix tests for DomainInvitationAdmin
---
src/registrar/tests/test_admin.py | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 5db01492a..8a67fc191 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -877,12 +877,14 @@ class DomainInvitationAdminTest(TestCase):
)
# Assert that the filters are added
- self.assertContains(response, "invited", count=4)
- self.assertContains(response, "retrieved", count=4)
+ self.assertContains(response, "invited", count=2)
+ self.assertContains(response, "Invited", count=2)
+ self.assertContains(response, "retrieved", count=2)
+ self.assertContains(response, "Retrieved", count=2)
# Check for the HTML context specificially
- invited_html = 'invited'
- retrieved_html = 'retrieved'
+ invited_html = 'Invited'
+ retrieved_html = 'Retrieved'
self.assertContains(response, invited_html, count=1)
self.assertContains(response, retrieved_html, count=1)
From ebb257e6b0dc7e81f9e30cb4a0017df513ea64f9 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Wed, 6 Dec 2023 09:37:13 -0700
Subject: [PATCH 16/54] Fix fixtures
---
src/registrar/fixtures_applications.py | 12 +++----
...alter_domainapplication_status_and_more.py | 12 ++++++-
.../0054_alter_domainapplication_status.py | 31 -------------------
src/registrar/models/domain_application.py | 4 +--
4 files changed, 19 insertions(+), 40 deletions(-)
delete mode 100644 src/registrar/migrations/0054_alter_domainapplication_status.py
diff --git a/src/registrar/fixtures_applications.py b/src/registrar/fixtures_applications.py
index 7f6a63719..69dfd0e50 100644
--- a/src/registrar/fixtures_applications.py
+++ b/src/registrar/fixtures_applications.py
@@ -49,27 +49,27 @@ class DomainApplicationFixture:
# },
DA = [
{
- "status": "started",
+ "status": DomainApplication.ApplicationStatus.STARTED,
"organization_name": "Example - Finished but not submitted",
},
{
- "status": "submitted",
+ "status": DomainApplication.ApplicationStatus.SUBMITTED,
"organization_name": "Example - Submitted but pending investigation",
},
{
- "status": "in review",
+ "status": DomainApplication.ApplicationStatus.IN_REVIEW,
"organization_name": "Example - In investigation",
},
{
- "status": "in review",
+ "status": DomainApplication.ApplicationStatus.IN_REVIEW,
"organization_name": "Example - Approved",
},
{
- "status": "withdrawn",
+ "status": DomainApplication.ApplicationStatus.WITHDRAWN,
"organization_name": "Example - Withdrawn",
},
{
- "status": "action needed",
+ "status": DomainApplication.ApplicationStatus.ACTION_NEEDED,
"organization_name": "Example - Action needed",
},
{
diff --git a/src/registrar/migrations/0053_alter_domain_state_alter_domainapplication_status_and_more.py b/src/registrar/migrations/0053_alter_domain_state_alter_domainapplication_status_and_more.py
index 856ce7f0f..2abf31364 100644
--- a/src/registrar/migrations/0053_alter_domain_state_alter_domainapplication_status_and_more.py
+++ b/src/registrar/migrations/0053_alter_domain_state_alter_domainapplication_status_and_more.py
@@ -1,4 +1,4 @@
-# Generated by Django 4.2.7 on 2023-12-05 22:07
+# Generated by Django 4.2.7 on 2023-12-06 16:16
from django.db import migrations, models
import django_fsm
@@ -45,6 +45,16 @@ class Migration(migrations.Migration):
max_length=50,
),
),
+ migrations.AlterField(
+ model_name="domaininvitation",
+ name="status",
+ field=django_fsm.FSMField(
+ choices=[("invited", "Invited"), ("retrieved", "Retrieved")],
+ default="invited",
+ max_length=50,
+ protected=True,
+ ),
+ ),
migrations.AlterField(
model_name="transitiondomain",
name="status",
diff --git a/src/registrar/migrations/0054_alter_domainapplication_status.py b/src/registrar/migrations/0054_alter_domainapplication_status.py
deleted file mode 100644
index c0c427179..000000000
--- a/src/registrar/migrations/0054_alter_domainapplication_status.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# Generated by Django 4.2.7 on 2023-12-05 23:14
-
-from django.db import migrations
-import django_fsm
-
-
-class Migration(migrations.Migration):
- dependencies = [
- ("registrar", "0053_alter_domain_state_alter_domainapplication_status_and_more"),
- ]
-
- operations = [
- migrations.AlterField(
- model_name="domainapplication",
- name="status",
- field=django_fsm.FSMField(
- choices=[
- ("started", "Started"),
- ("submitted", "Submitted"),
- ("in_review", "In review"),
- ("action_needed", "Action needed"),
- ("approved", "Approved"),
- ("withdrawn", "Withdrawn"),
- ("rejected", "Rejected"),
- ("ineligible", "Ineligible"),
- ],
- default="started",
- max_length=50,
- ),
- ),
- ]
diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py
index 32cd239dc..8a56fb883 100644
--- a/src/registrar/models/domain_application.py
+++ b/src/registrar/models/domain_application.py
@@ -23,8 +23,8 @@ class DomainApplication(TimeStampedModel):
class ApplicationStatus(models.TextChoices):
STARTED = "started", "Started"
SUBMITTED = "submitted", "Submitted"
- IN_REVIEW = "in_review", "In review"
- ACTION_NEEDED = "action_needed", "Action needed"
+ IN_REVIEW = "in review", "In review"
+ ACTION_NEEDED = "action needed", "Action needed"
APPROVED = "approved", "Approved"
WITHDRAWN = "withdrawn", "Withdrawn"
REJECTED = "rejected", "Rejected"
From 2e1ff849cf17a33860a2b41dfe6dc849e7816061 Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Wed, 6 Dec 2023 13:34:13 -0500
Subject: [PATCH 17/54] wip
---
src/djangooidc/oidc.py | 4 ++--
src/djangooidc/views.py | 11 ++++++++++-
2 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/src/djangooidc/oidc.py b/src/djangooidc/oidc.py
index b2b1acd8e..074dbdd1c 100644
--- a/src/djangooidc/oidc.py
+++ b/src/djangooidc/oidc.py
@@ -162,7 +162,7 @@ class Client(oic.Client):
logger.error(err)
logger.error("Unable to parse response for %s" % state)
raise o_e.AuthenticationFailed(locator=state)
-
+ logger.info(authn_response)
# ErrorResponse is not raised, it is passed back...
if isinstance(authn_response, ErrorResponse):
error = authn_response.get("error", "")
@@ -207,7 +207,7 @@ class Client(oic.Client):
logger.error(err)
logger.error("Unable to request user info for %s" % state)
raise o_e.AuthenticationFailed(locator=state)
-
+ logger.info(info_response)
# ErrorResponse is not raised, it is passed back...
if isinstance(info_response, ErrorResponse):
logger.error("Unable to get user info (%s) for %s" % (info_response.get("error", ""), state))
diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py
index ea893daf2..89ff6e0f8 100644
--- a/src/djangooidc/views.py
+++ b/src/djangooidc/views.py
@@ -56,6 +56,7 @@ def error_page(request, error):
def openid(request):
"""Redirect the user to an authentication provider (OP)."""
request.session["next"] = request.GET.get("next", "/")
+ request.session["acr_value"] = request.GET.get("acr_value",)
try:
return CLIENT.create_authn_request(request.session)
@@ -70,6 +71,13 @@ def login_callback(request):
userinfo = CLIENT.callback(query, request.session)
user = authenticate(request=request, **userinfo)
if user:
+ # test for need for identity verification and if it is satisfied
+ # if not satisfied, redirect user to login with stepped up acr_value
+ if requires_step_up_auth(userinfo):
+ return
+ #
+ # if User.needs_identity_verification and step_up_acr_value not in
+ # ial returned from callback, redirect to
login(request, user)
logger.info("Successfully logged in user %s" % user)
return redirect(request.session.get("next", "/"))
@@ -78,7 +86,8 @@ def login_callback(request):
except Exception as err:
return error_page(request, err)
-
+def requires_step_up_auth(userinfo):
+ step_up_acr_value =
def logout(request, next_page=None):
"""Redirect the user to the authentication provider (OP) logout page."""
try:
From 695b4199f37d1c1e16ebfbf5a4c989332caca8d8 Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Wed, 6 Dec 2023 13:58:47 -0500
Subject: [PATCH 18/54] wip
---
src/djangooidc/oidc.py | 3 ++-
src/djangooidc/views.py | 19 +++++++++++++------
src/registrar/config/settings.py | 6 ++++--
src/registrar/models/user.py | 4 ++++
4 files changed, 23 insertions(+), 9 deletions(-)
diff --git a/src/djangooidc/oidc.py b/src/djangooidc/oidc.py
index 074dbdd1c..2c48fb122 100644
--- a/src/djangooidc/oidc.py
+++ b/src/djangooidc/oidc.py
@@ -89,6 +89,7 @@ class Client(oic.Client):
"""Step 2: Construct a login URL at OP's domain and send the user to it."""
logger.debug("Creating the OpenID Connect authn request...")
state = rndstr(size=32)
+ logger.info(session["acr_value"])
try:
session["state"] = state
session["nonce"] = rndstr(size=32)
@@ -100,7 +101,7 @@ class Client(oic.Client):
"state": session["state"],
"nonce": session["nonce"],
"redirect_uri": self.registration_response["redirect_uris"][0],
- "acr_values": self.behaviour.get("acr_value"),
+ "acr_values": session["acr_value"] if session["acr_value"] else self.behaviour.get("acr_value"),
}
if extra_args is not None:
diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py
index 89ff6e0f8..08438a521 100644
--- a/src/djangooidc/views.py
+++ b/src/djangooidc/views.py
@@ -11,6 +11,7 @@ from urllib.parse import parse_qs, urlencode
from djangooidc.oidc import Client
from djangooidc import exceptions as o_e
+from registrar.models import User
logger = logging.getLogger(__name__)
@@ -56,7 +57,6 @@ def error_page(request, error):
def openid(request):
"""Redirect the user to an authentication provider (OP)."""
request.session["next"] = request.GET.get("next", "/")
- request.session["acr_value"] = request.GET.get("acr_value",)
try:
return CLIENT.create_authn_request(request.session)
@@ -74,10 +74,10 @@ def login_callback(request):
# test for need for identity verification and if it is satisfied
# if not satisfied, redirect user to login with stepped up acr_value
if requires_step_up_auth(userinfo):
- return
- #
- # if User.needs_identity_verification and step_up_acr_value not in
- # ial returned from callback, redirect to
+ # add acr_value to request.session
+ request.session["acr_value"] = CLIENT.behaviour.get("step_up_acr_value")
+ return CLIENT.create_authn_request(request.session)
+
login(request, user)
logger.info("Successfully logged in user %s" % user)
return redirect(request.session.get("next", "/"))
@@ -87,7 +87,14 @@ def login_callback(request):
return error_page(request, err)
def requires_step_up_auth(userinfo):
- step_up_acr_value =
+ # if User.needs_identity_verification and step_up_acr_value not in
+ # ial returned from callback, redirect to
+ step_up_acr_value = CLIENT.behavior.get("step_up_acr_value", "UNKNOWN")
+ acr_value = userinfo.get("ial", "")
+ uuid = userinfo.get("sub", "")
+ email = userinfo.get("email", "")
+ return User.needs_identity_verification(email, uuid) and acr_value == step_up_acr_value
+
def logout(request, next_page=None):
"""Redirect the user to the authentication provider (OP) logout page."""
try:
diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py
index cc779911a..c99daf72b 100644
--- a/src/registrar/config/settings.py
+++ b/src/registrar/config/settings.py
@@ -540,7 +540,8 @@ OIDC_PROVIDERS = {
"response_type": "code",
"scope": ["email", "profile:name", "phone"],
"user_info_request": ["email", "first_name", "last_name", "phone"],
- "acr_value": "http://idmanagement.gov/ns/assurance/ial/2",
+ "acr_value": "http://idmanagement.gov/ns/assurance/ial/1",
+ "step_up_acr_value": "http://idmanagement.gov/ns/assurance/ial/2",
},
"client_registration": {
"client_id": "cisa_dotgov_registrar",
@@ -557,7 +558,8 @@ OIDC_PROVIDERS = {
"response_type": "code",
"scope": ["email", "profile:name", "phone"],
"user_info_request": ["email", "first_name", "last_name", "phone"],
- "acr_value": "http://idmanagement.gov/ns/assurance/ial/2",
+ "acr_value": "http://idmanagement.gov/ns/assurance/ial/1",
+ "step_up_acr_value": "http://idmanagement.gov/ns/assurance/ial/2",
},
"client_registration": {
"client_id": ("urn:gov:cisa:openidconnect.profiles:sp:sso:cisa:dotgov_registrar"),
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index 2daa3c253..2911612de 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -64,6 +64,10 @@ class User(AbstractUser):
def is_restricted(self):
return self.status == self.RESTRICTED
+ @classmethod
+ def needs_identity_verification(email, uuid):
+ return True
+
def check_domain_invitations_on_login(self):
"""When a user first arrives on the site, we need to retrieve any domain
invitations that match their email address."""
From bba5e8affb7700f9b53f8cf64a29d375fd995380 Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Wed, 6 Dec 2023 12:24:36 -0800
Subject: [PATCH 19/54] Add EPP retry on transport errors
---
src/epplibwrapper/client.py | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py
index 5693b6993..f2a5d604a 100644
--- a/src/epplibwrapper/client.py
+++ b/src/epplibwrapper/client.py
@@ -158,16 +158,13 @@ class EPPLibWrapper:
# The end user will need to recall .send.
self.start_connection_pool()
- counter = 0 # we'll try 3 times
- logger.info("Counter set to: ", counter)
+ counter = 0
while True:
try:
- logger.info("Trying self._send(command)")
return self._send(command)
except RegistryError as err:
- logger.info("Exception found")
if counter < 3 and (err.should_retry() or err.is_transport_error()):
- logger.info("Retrying transport error. Attempt #", counter)
+ logger.info(f"Retrying transport error. Attempt #{counter+1} of 3.")
counter += 1
sleep((counter * 50) / 1000) # sleep 50 ms to 150 ms
else: # don't try again
From 6b0be2e7e8e52a0fcd8cb65138339a7c4ff3a9cc Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Wed, 6 Dec 2023 12:25:07 -0800
Subject: [PATCH 20/54] Temporarily increase instances to 4 on manifest-es
---
ops/manifests/manifest-es.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ops/manifests/manifest-es.yaml b/ops/manifests/manifest-es.yaml
index 47c78ce1b..131e92f3c 100644
--- a/ops/manifests/manifest-es.yaml
+++ b/ops/manifests/manifest-es.yaml
@@ -4,7 +4,7 @@ applications:
buildpacks:
- python_buildpack
path: ../../src
- instances: 1
+ instances: 4
memory: 512M
stack: cflinuxfs4
timeout: 180
From f6a288f51180ad962322cda69506333fd5b337e8 Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Wed, 6 Dec 2023 15:49:21 -0500
Subject: [PATCH 21/54] set up step_up_auth using stubbed out
User.needs_identity_verification
---
src/djangooidc/oidc.py | 12 +++++++++---
src/djangooidc/views.py | 11 +++++------
src/registrar/models/user.py | 2 +-
3 files changed, 15 insertions(+), 10 deletions(-)
diff --git a/src/djangooidc/oidc.py b/src/djangooidc/oidc.py
index 2c48fb122..a7d8af167 100644
--- a/src/djangooidc/oidc.py
+++ b/src/djangooidc/oidc.py
@@ -89,7 +89,6 @@ class Client(oic.Client):
"""Step 2: Construct a login URL at OP's domain and send the user to it."""
logger.debug("Creating the OpenID Connect authn request...")
state = rndstr(size=32)
- logger.info(session["acr_value"])
try:
session["state"] = state
session["nonce"] = rndstr(size=32)
@@ -101,7 +100,9 @@ class Client(oic.Client):
"state": session["state"],
"nonce": session["nonce"],
"redirect_uri": self.registration_response["redirect_uris"][0],
- "acr_values": session["acr_value"] if session["acr_value"] else self.behaviour.get("acr_value"),
+ # acr_value may be passed in session if overriding, as in the case
+ # of step up auth, otherwise get from settings.py
+ "acr_values": session.get("acr_value") or self.behaviour.get("acr_value"),
}
if extra_args is not None:
@@ -146,7 +147,7 @@ class Client(oic.Client):
raise o_e.InternalError(locator=state)
return response
-
+
def callback(self, unparsed_response, session):
"""Step 3: Receive OP's response, request an access token, and user info."""
logger.debug("Processing the OpenID Connect callback response...")
@@ -273,6 +274,11 @@ class Client(oic.Client):
super(Client, self).store_response(resp, info)
+ def get_step_up_acr_value(self):
+ """returns the step_up_acr_value from settings
+ this helper function is called from djangooidc views"""
+ return self.behaviour.get("step_up_acr_value")
+
def __repr__(self):
return "Client {} {} {}".format(
self.client_id,
diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py
index 08438a521..d203f18c0 100644
--- a/src/djangooidc/views.py
+++ b/src/djangooidc/views.py
@@ -75,7 +75,7 @@ def login_callback(request):
# if not satisfied, redirect user to login with stepped up acr_value
if requires_step_up_auth(userinfo):
# add acr_value to request.session
- request.session["acr_value"] = CLIENT.behaviour.get("step_up_acr_value")
+ request.session["acr_value"] = CLIENT.get_step_up_acr_value()
return CLIENT.create_authn_request(request.session)
login(request, user)
@@ -87,13 +87,13 @@ def login_callback(request):
return error_page(request, err)
def requires_step_up_auth(userinfo):
- # if User.needs_identity_verification and step_up_acr_value not in
- # ial returned from callback, redirect to
- step_up_acr_value = CLIENT.behavior.get("step_up_acr_value", "UNKNOWN")
+ """ if User.needs_identity_verification and step_up_acr_value not in
+ ial returned from callback, return True """
+ step_up_acr_value = CLIENT.get_step_up_acr_value()
acr_value = userinfo.get("ial", "")
uuid = userinfo.get("sub", "")
email = userinfo.get("email", "")
- return User.needs_identity_verification(email, uuid) and acr_value == step_up_acr_value
+ return User.needs_identity_verification(email, uuid) and acr_value != step_up_acr_value
def logout(request, next_page=None):
"""Redirect the user to the authentication provider (OP) logout page."""
@@ -125,7 +125,6 @@ def logout(request, next_page=None):
if next_page:
request.session["next"] = next_page
-
def logout_callback(request):
"""Simple redirection view: after logout, redirect to `next`."""
next = request.session.get("next", "/")
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index 2911612de..e1880b9dd 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -65,7 +65,7 @@ class User(AbstractUser):
return self.status == self.RESTRICTED
@classmethod
- def needs_identity_verification(email, uuid):
+ def needs_identity_verification(cls, email, uuid):
return True
def check_domain_invitations_on_login(self):
From 9a843e7a7df1a589c398ad8b62ec528a0905b940 Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Wed, 6 Dec 2023 14:02:52 -0800
Subject: [PATCH 22/54] Remove code block used to test manually
---
src/api/views.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/src/api/views.py b/src/api/views.py
index b3c371494..bf22b0c5a 100644
--- a/src/api/views.py
+++ b/src/api/views.py
@@ -69,9 +69,6 @@ def check_domain_available(domain):
a match. If check fails, throws a RegistryError.
"""
Domain = apps.get_model("registrar.Domain")
- # TODO: remove this block it is used for testing on dev sandbox to verify error retry
- if "bad" in domain:
- raise RegistryError("Forced Registry Error from bad domain")
if domain.endswith(".gov"):
return Domain.available(domain)
else:
From 24f1b6ce6af71780e2bbda15b242583552e9b023 Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Wed, 6 Dec 2023 14:03:31 -0800
Subject: [PATCH 23/54] Fix lint errors
---
src/api/views.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/api/views.py b/src/api/views.py
index bf22b0c5a..69f440fcd 100644
--- a/src/api/views.py
+++ b/src/api/views.py
@@ -6,6 +6,7 @@ from django.utils.safestring import mark_safe
from registrar.templatetags.url_helpers import public_site_url
from registrar.utility.errors import GenericError, GenericErrorCodes
+
# comment out after testing
from epplibwrapper.errors import RegistryError
From 68abc1cda472dfdf75c0ff24e6066172b1b38e27 Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Wed, 6 Dec 2023 14:04:12 -0800
Subject: [PATCH 24/54] Remove unused imports
---
src/api/views.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/src/api/views.py b/src/api/views.py
index 69f440fcd..85ae021c9 100644
--- a/src/api/views.py
+++ b/src/api/views.py
@@ -7,9 +7,6 @@ from django.utils.safestring import mark_safe
from registrar.templatetags.url_helpers import public_site_url
from registrar.utility.errors import GenericError, GenericErrorCodes
-# comment out after testing
-from epplibwrapper.errors import RegistryError
-
import requests
from login_required import login_not_required
From 0b0f72dc4fb1522ce21416172f4bd35eaeb0d604 Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Wed, 6 Dec 2023 14:13:37 -0800
Subject: [PATCH 25/54] Reduce manifest instances to 2
---
ops/manifests/manifest-es.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ops/manifests/manifest-es.yaml b/ops/manifests/manifest-es.yaml
index 131e92f3c..f4d9b0df4 100644
--- a/ops/manifests/manifest-es.yaml
+++ b/ops/manifests/manifest-es.yaml
@@ -4,7 +4,7 @@ applications:
buildpacks:
- python_buildpack
path: ../../src
- instances: 4
+ instances: 2
memory: 512M
stack: cflinuxfs4
timeout: 180
From d55277cc657303a962afbedef3604a545f1f16e9 Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Wed, 6 Dec 2023 14:34:11 -0800
Subject: [PATCH 26/54] Remove test_pool test for retry for now
---
src/epplibwrapper/tests/test_pool.py | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py
index ab4323ff3..ef033ff6a 100644
--- a/src/epplibwrapper/tests/test_pool.py
+++ b/src/epplibwrapper/tests/test_pool.py
@@ -245,8 +245,4 @@ class TestConnectionPool(TestCase):
with self.assertRaises(RegistryError):
expected = "InfoDomain failed to execute due to a connection error."
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
- self.assertEqual(result, expected)
-
- @patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success)
- def test_retries_on_transport_error(self):
- """A .send is invoked on the pool, but [description for transport error]."""
+ self.assertEqual(result, expected)
\ No newline at end of file
From 2db344f7182f8fba21161529af01eda61d2ca424 Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Wed, 6 Dec 2023 14:37:21 -0800
Subject: [PATCH 27/54] Fix lint errors
---
src/epplibwrapper/tests/test_pool.py | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py
index ef033ff6a..916015980 100644
--- a/src/epplibwrapper/tests/test_pool.py
+++ b/src/epplibwrapper/tests/test_pool.py
@@ -245,4 +245,17 @@ class TestConnectionPool(TestCase):
with self.assertRaises(RegistryError):
expected = "InfoDomain failed to execute due to a connection error."
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
- self.assertEqual(result, expected)
\ No newline at end of file
+ self.assertEqual(result, expected)
+
+ @patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success)
+ def test_retries_on_transport_error(self):
+ """A .send is invoked on the pool, but transport error occurs and EPP
+ retries connection."""
+
+ with ExitStack() as stack:
+ stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket))
+ stack.enter_context(patch.object(Socket, "connect", self.fake_client))
+
+ # Pool should be running
+ self.assertEqual(registry.pool_status.connection_success, True)
+ self.assertEqual(registry.pool_status.pool_running, True)
From 4cfa68e64f71765e793821fa25f8e9cc833ab168 Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Wed, 6 Dec 2023 14:39:12 -0800
Subject: [PATCH 28/54] Re-remove unused test
---
src/epplibwrapper/tests/test_pool.py | 13 -------------
1 file changed, 13 deletions(-)
diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py
index 916015980..1c36d26da 100644
--- a/src/epplibwrapper/tests/test_pool.py
+++ b/src/epplibwrapper/tests/test_pool.py
@@ -246,16 +246,3 @@ class TestConnectionPool(TestCase):
expected = "InfoDomain failed to execute due to a connection error."
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
self.assertEqual(result, expected)
-
- @patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success)
- def test_retries_on_transport_error(self):
- """A .send is invoked on the pool, but transport error occurs and EPP
- retries connection."""
-
- with ExitStack() as stack:
- stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket))
- stack.enter_context(patch.object(Socket, "connect", self.fake_client))
-
- # Pool should be running
- self.assertEqual(registry.pool_status.connection_success, True)
- self.assertEqual(registry.pool_status.pool_running, True)
From 5d5f0ad041945fe8ead8e6541f70f67720a985e4 Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Wed, 6 Dec 2023 14:52:54 -0800
Subject: [PATCH 29/54] Revert instances to 1
---
ops/manifests/manifest-es.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ops/manifests/manifest-es.yaml b/ops/manifests/manifest-es.yaml
index f4d9b0df4..47c78ce1b 100644
--- a/ops/manifests/manifest-es.yaml
+++ b/ops/manifests/manifest-es.yaml
@@ -4,7 +4,7 @@ applications:
buildpacks:
- python_buildpack
path: ../../src
- instances: 2
+ instances: 1
memory: 512M
stack: cflinuxfs4
timeout: 180
From e392d68453ae683e7bced05823deb66929366298 Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Wed, 6 Dec 2023 14:56:45 -0800
Subject: [PATCH 30/54] Readd accidentally deleted comment
---
src/epplibwrapper/client.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py
index f2a5d604a..9ed437aef 100644
--- a/src/epplibwrapper/client.py
+++ b/src/epplibwrapper/client.py
@@ -158,7 +158,7 @@ class EPPLibWrapper:
# The end user will need to recall .send.
self.start_connection_pool()
- counter = 0
+ counter = 0 # we'll try 3 times
while True:
try:
return self._send(command)
From 67d20a62964cd0c0653405d0db9aaa3f3a89a522 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Wed, 6 Dec 2023 18:16:50 -0500
Subject: [PATCH 31/54] Move the test for requires_step_up_auth up above the
user authentication in login_callback, imlement needs_identity_verification
in user model
---
src/djangooidc/views.py | 15 ++++++++-------
src/registrar/models/user.py | 36 ++++++++++++++++++++++++++++++++++++
2 files changed, 44 insertions(+), 7 deletions(-)
diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py
index d203f18c0..5b31b83c6 100644
--- a/src/djangooidc/views.py
+++ b/src/djangooidc/views.py
@@ -69,15 +69,16 @@ def login_callback(request):
try:
query = parse_qs(request.GET.urlencode())
userinfo = CLIENT.callback(query, request.session)
+
+ # test for need for identity verification and if it is satisfied
+ # if not satisfied, redirect user to login with stepped up acr_value
+ if requires_step_up_auth(userinfo):
+ # add acr_value to request.session
+ request.session["acr_value"] = CLIENT.get_step_up_acr_value()
+ return CLIENT.create_authn_request(request.session)
+
user = authenticate(request=request, **userinfo)
if user:
- # test for need for identity verification and if it is satisfied
- # if not satisfied, redirect user to login with stepped up acr_value
- if requires_step_up_auth(userinfo):
- # add acr_value to request.session
- request.session["acr_value"] = CLIENT.get_step_up_acr_value()
- return CLIENT.create_authn_request(request.session)
-
login(request, user)
logger.info("Successfully logged in user %s" % user)
return redirect(request.session.get("next", "/"))
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index e1880b9dd..5c1170f78 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -3,6 +3,8 @@ import logging
from django.contrib.auth.models import AbstractUser
from django.db import models
+from registrar.models.user_domain_role import UserDomainRole
+
from .domain_invitation import DomainInvitation
from .transition_domain import TransitionDomain
from .domain import Domain
@@ -66,6 +68,40 @@ class User(AbstractUser):
@classmethod
def needs_identity_verification(cls, email, uuid):
+
+ logger.info('needs_identity_verification')
+
+ try:
+
+ existing_user = cls.objects.get(username=uuid)
+
+ # An existing user who is a domain manager of a domain (that is, they have an entry in UserDomainRole for their User)
+ if existing_user and UserDomainRole.objects.filter(user=existing_user).exists():
+
+ logger.info(f'Existing user email {existing_user.email}')
+ logger.info(f'User doman role email {UserDomainRole.objects.filter(user=existing_user).first().user.email}')
+ return False
+
+ except:
+ pass
+
+ # logger.info(f'UserDomainRole.objects.filter(user=existing_user).exists() {UserDomainRole.objects.filter(user=existing_user).exists()}')
+ logger.info('got past the existing_user get')
+
+
+
+ # A new incoming user who is a domain manager for one of the domains that we inputted from Verisign (that is, their email address appears in the username field of a TransitionDomain)
+ if TransitionDomain.objects.filter(username=email).exists():
+ logger.info('Transition user')
+ return False
+
+ # A new incoming user who is being invited to be a domain manager (that is, their email address is in DomainInvitation for an invitation that is not yet "retrieved").
+ if DomainInvitation.objects.filter(email=email, status=DomainInvitation.INVITED):
+ logger.info('Invited user')
+ return False
+
+ logger.info('needs_identity_verification is TRUE')
+
return True
def check_domain_invitations_on_login(self):
From ef9a542ddade83e7310d40d8a5c28667ed603363 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Wed, 6 Dec 2023 19:53:15 -0500
Subject: [PATCH 32/54] Clean up loggers
---
src/djangooidc/oidc.py | 2 --
src/djangooidc/views.py | 8 ++------
src/registrar/models/user.py | 27 +++++----------------------
3 files changed, 7 insertions(+), 30 deletions(-)
diff --git a/src/djangooidc/oidc.py b/src/djangooidc/oidc.py
index a7d8af167..798771da2 100644
--- a/src/djangooidc/oidc.py
+++ b/src/djangooidc/oidc.py
@@ -164,7 +164,6 @@ class Client(oic.Client):
logger.error(err)
logger.error("Unable to parse response for %s" % state)
raise o_e.AuthenticationFailed(locator=state)
- logger.info(authn_response)
# ErrorResponse is not raised, it is passed back...
if isinstance(authn_response, ErrorResponse):
error = authn_response.get("error", "")
@@ -209,7 +208,6 @@ class Client(oic.Client):
logger.error(err)
logger.error("Unable to request user info for %s" % state)
raise o_e.AuthenticationFailed(locator=state)
- logger.info(info_response)
# ErrorResponse is not raised, it is passed back...
if isinstance(info_response, ErrorResponse):
logger.error("Unable to get user info (%s) for %s" % (info_response.get("error", ""), state))
diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py
index 5b31b83c6..31f84d41f 100644
--- a/src/djangooidc/views.py
+++ b/src/djangooidc/views.py
@@ -13,7 +13,6 @@ from djangooidc.oidc import Client
from djangooidc import exceptions as o_e
from registrar.models import User
-
logger = logging.getLogger(__name__)
try:
@@ -69,14 +68,12 @@ def login_callback(request):
try:
query = parse_qs(request.GET.urlencode())
userinfo = CLIENT.callback(query, request.session)
-
# test for need for identity verification and if it is satisfied
# if not satisfied, redirect user to login with stepped up acr_value
if requires_step_up_auth(userinfo):
# add acr_value to request.session
request.session["acr_value"] = CLIENT.get_step_up_acr_value()
return CLIENT.create_authn_request(request.session)
-
user = authenticate(request=request, **userinfo)
if user:
login(request, user)
@@ -99,7 +96,7 @@ def requires_step_up_auth(userinfo):
def logout(request, next_page=None):
"""Redirect the user to the authentication provider (OP) logout page."""
try:
- username = request.user.username
+ user = request.user
request_args = {
"client_id": CLIENT.client_id,
"state": request.session["state"],
@@ -111,7 +108,6 @@ def logout(request, next_page=None):
request_args.update(
{"post_logout_redirect_uri": CLIENT.registration_response["post_logout_redirect_uris"][0]}
)
-
url = CLIENT.provider_info["end_session_endpoint"]
url += "?" + urlencode(request_args)
return HttpResponseRedirect(url)
@@ -121,7 +117,7 @@ def logout(request, next_page=None):
# Always remove Django session stuff - even if not logged out from OP.
# Don't wait for the callback as it may never come.
auth_logout(request)
- logger.info("Successfully logged out user %s" % username)
+ logger.info("Successfully logged out user %s" % user)
next_page = getattr(settings, "LOGOUT_REDIRECT_URL", None)
if next_page:
request.session["next"] = next_page
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index 5c1170f78..af9f3eb3b 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -68,40 +68,23 @@ class User(AbstractUser):
@classmethod
def needs_identity_verification(cls, email, uuid):
-
- logger.info('needs_identity_verification')
-
- try:
-
- existing_user = cls.objects.get(username=uuid)
-
- # An existing user who is a domain manager of a domain (that is, they have an entry in UserDomainRole for their User)
- if existing_user and UserDomainRole.objects.filter(user=existing_user).exists():
- logger.info(f'Existing user email {existing_user.email}')
- logger.info(f'User doman role email {UserDomainRole.objects.filter(user=existing_user).first().user.email}')
+ # An existing user who is a domain manager of a domain (that is, they have an entry in UserDomainRole for their User)
+ try:
+ existing_user = cls.objects.get(username=uuid)
+ if existing_user and UserDomainRole.objects.filter(user=existing_user).exists():
return False
-
except:
pass
- # logger.info(f'UserDomainRole.objects.filter(user=existing_user).exists() {UserDomainRole.objects.filter(user=existing_user).exists()}')
- logger.info('got past the existing_user get')
-
-
-
# A new incoming user who is a domain manager for one of the domains that we inputted from Verisign (that is, their email address appears in the username field of a TransitionDomain)
if TransitionDomain.objects.filter(username=email).exists():
- logger.info('Transition user')
return False
# A new incoming user who is being invited to be a domain manager (that is, their email address is in DomainInvitation for an invitation that is not yet "retrieved").
if DomainInvitation.objects.filter(email=email, status=DomainInvitation.INVITED):
- logger.info('Invited user')
return False
-
- logger.info('needs_identity_verification is TRUE')
-
+
return True
def check_domain_invitations_on_login(self):
From bb19da9008cebe0a73d9b2ce9e1c61f01d28e6b3 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Wed, 6 Dec 2023 22:59:56 -0500
Subject: [PATCH 33/54] Unit tests for needs_identity_verification
---
src/registrar/models/user.py | 12 +++++++---
src/registrar/tests/test_models.py | 37 +++++++++++++++++++++++-------
2 files changed, 38 insertions(+), 11 deletions(-)
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index af9f3eb3b..b4f2c08ab 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -68,8 +68,11 @@ class User(AbstractUser):
@classmethod
def needs_identity_verification(cls, email, uuid):
+ """A method used by our oidc classes to test whether a user needs email/uuid verification
+ or the full identity PII verification"""
- # An existing user who is a domain manager of a domain (that is, they have an entry in UserDomainRole for their User)
+ # An existing user who is a domain manager of a domain (that is,
+ # they have an entry in UserDomainRole for their User)
try:
existing_user = cls.objects.get(username=uuid)
if existing_user and UserDomainRole.objects.filter(user=existing_user).exists():
@@ -77,11 +80,14 @@ class User(AbstractUser):
except:
pass
- # A new incoming user who is a domain manager for one of the domains that we inputted from Verisign (that is, their email address appears in the username field of a TransitionDomain)
+ # A new incoming user who is a domain manager for one of the domains
+ # that we inputted from Verisign (that is, their email address appears
+ # in the username field of a TransitionDomain)
if TransitionDomain.objects.filter(username=email).exists():
return False
- # A new incoming user who is being invited to be a domain manager (that is, their email address is in DomainInvitation for an invitation that is not yet "retrieved").
+ # A new incoming user who is being invited to be a domain manager (that is,
+ # their email address is in DomainInvitation for an invitation that is not yet "retrieved").
if DomainInvitation.objects.filter(email=email, status=DomainInvitation.INVITED):
return False
diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py
index d397cb129..0add94ce6 100644
--- a/src/registrar/tests/test_models.py
+++ b/src/registrar/tests/test_models.py
@@ -606,18 +606,14 @@ class TestInvitations(TestCase):
class TestUser(TestCase):
- """For now, just test actions that
- occur on user login."""
+ """Test actions that occur on user login,
+ test class method that controls how users get validated."""
def setUp(self):
self.email = "mayor@igorville.gov"
self.domain_name = "igorvilleInTransition.gov"
- self.user, _ = User.objects.get_or_create(email=self.email)
-
- # clean out the roles each time
- UserDomainRole.objects.all().delete()
-
- TransitionDomain.objects.get_or_create(username="mayor@igorville.gov", domain_name=self.domain_name)
+ self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
+ self.user, _ = User.objects.get_or_create(email=self.email)
def tearDown(self):
super().tearDown()
@@ -626,6 +622,8 @@ class TestUser(TestCase):
DomainInformation.objects.all().delete()
TransitionDomain.objects.all().delete()
User.objects.all().delete()
+ UserDomainRole.objects.all().delete()
+ TransitionDomain.objects.get_or_create(username="mayor@igorville.gov", domain_name=self.domain_name)
def test_check_transition_domains_without_domains_on_login(self):
"""A user's on_each_login callback does not check transition domains.
@@ -634,3 +632,26 @@ class TestUser(TestCase):
are created."""
self.user.on_each_login()
self.assertFalse(Domain.objects.filter(name=self.domain_name).exists())
+
+ def test_identity_verification_with_domain_manager(self):
+ """A domain manager should return False when tested with class
+ method needs_identity_verification"""
+ UserDomainRole.objects.get_or_create(user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
+ self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username))
+
+ def test_identity_verification_with_transition_user(self):
+ """A user from the Verisign transition should return False
+ when tested with class method needs_identity_verification"""
+ TransitionDomain.objects.get_or_create(username=self.user.email, domain_name=self.domain_name)
+ self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username))
+
+ def test_identity_verification_with_invited_user(self):
+ """An invited user should return False when tested with class
+ method needs_identity_verification"""
+ DomainInvitation.objects.get_or_create(email=self.user.email, domain=self.domain)
+ self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username))
+
+ def test_identity_verification_with_new_user(self):
+ """A new user who's neither transitioned nor invited should
+ return True when tested with class method needs_identity_verification"""
+ self.assertTrue(User.needs_identity_verification(self.user.email, self.user.username))
From ecb30fd73e2559cdd45a64e9ce16f4a7d38ceb7e Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Thu, 7 Dec 2023 01:27:27 -0500
Subject: [PATCH 34/54] Unit tests for the requires_step_up_auth logic in
login_callback
---
src/djangooidc/tests/test_views.py | 41 ++++++++++++++++++++++++++++--
1 file changed, 39 insertions(+), 2 deletions(-)
diff --git a/src/djangooidc/tests/test_views.py b/src/djangooidc/tests/test_views.py
index 5ff36a77c..fef567396 100644
--- a/src/djangooidc/tests/test_views.py
+++ b/src/djangooidc/tests/test_views.py
@@ -1,8 +1,9 @@
-from unittest.mock import patch
+from unittest.mock import MagicMock, patch
from django.http import HttpResponse
-from django.test import Client, TestCase
+from django.test import Client, TestCase, RequestFactory
from django.urls import reverse
+from ..views import login_callback
from .common import less_console_noise
@@ -11,6 +12,7 @@ from .common import less_console_noise
class ViewsTest(TestCase):
def setUp(self):
self.client = Client()
+ self.factory = RequestFactory()
def say_hi(*args):
return HttpResponse("Hi")
@@ -64,6 +66,41 @@ class ViewsTest(TestCase):
# assert
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("logout"))
+
+ def test_requires_step_up_auth(self, mock_client):
+ # Configure the mock to return an expected value for get_step_up_acr_value
+ mock_client.return_value.get_step_up_acr_value.return_value = "step_up_acr_value"
+
+ # Create a mock request
+ request = self.factory.get("/some-url")
+ request.session = {"acr_value": ""}
+
+ # Ensure that the CLIENT instance used in login_callback is the mock
+ # patch requires_step_up_auth to return True
+ with patch("djangooidc.views.requires_step_up_auth", return_value=True), \
+ patch("djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock()) as mock_create_authn_request:
+ login_callback(request)
+
+ # Assert that get_step_up_acr_value was called and session was updated
+ self.assertNotEqual(request.session["acr_value"], "")
+ # And create_authn_request was called again
+ mock_create_authn_request.assert_called_once()
+
+ def test_does_not_requires_step_up_auth(self, mock_client):
+ # Create a mock request
+ request = self.factory.get("/some-url")
+ request.session = {"acr_value": ""}
+
+ # Ensure that the CLIENT instance used in login_callback is the mock
+ # patch requires_step_up_auth to return False
+ with patch("djangooidc.views.requires_step_up_auth", return_value=False), \
+ patch("djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock()) as mock_create_authn_request:
+ login_callback(request)
+
+ # Assert that get_step_up_acr_value was NOT called and session was NOT updated
+ self.assertEqual(request.session["acr_value"], "")
+ # create_authn_request was not called
+ mock_create_authn_request.assert_not_called()
@patch("djangooidc.views.authenticate")
def test_login_callback_raises(self, mock_auth, mock_client):
From 6e2bba5dc97cb2ec0a8d680c0c61ebc84f7c253f Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Thu, 7 Dec 2023 07:56:18 -0700
Subject: [PATCH 35/54] Fix migrations
---
...ter_domain_state_alter_domainapplication_status_and_more.py} | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
rename src/registrar/migrations/{0053_alter_domain_state_alter_domainapplication_status_and_more.py => 0055_alter_domain_state_alter_domainapplication_status_and_more.py} (96%)
diff --git a/src/registrar/migrations/0053_alter_domain_state_alter_domainapplication_status_and_more.py b/src/registrar/migrations/0055_alter_domain_state_alter_domainapplication_status_and_more.py
similarity index 96%
rename from src/registrar/migrations/0053_alter_domain_state_alter_domainapplication_status_and_more.py
rename to src/registrar/migrations/0055_alter_domain_state_alter_domainapplication_status_and_more.py
index 2abf31364..9b6bac48c 100644
--- a/src/registrar/migrations/0053_alter_domain_state_alter_domainapplication_status_and_more.py
+++ b/src/registrar/migrations/0055_alter_domain_state_alter_domainapplication_status_and_more.py
@@ -6,7 +6,7 @@ import django_fsm
class Migration(migrations.Migration):
dependencies = [
- ("registrar", "0052_alter_domainapplication_anything_else_and_more"),
+ ("registrar", "0054_alter_domainapplication_federal_agency_and_more"),
]
operations = [
From 9efa5e2cdb3f6f388a636d03a42ff8c9bffd0cc6 Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Thu, 7 Dec 2023 11:06:38 -0800
Subject: [PATCH 36/54] Lowercase domain name on available API
---
src/registrar/models/domain.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 94430fb36..2db17afa5 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -174,7 +174,8 @@ class Domain(TimeStampedModel, DomainHelper):
"""Check if a domain is available."""
if not cls.string_could_be_domain(domain):
raise ValueError("Not a valid domain: %s" % str(domain))
- req = commands.CheckDomain([domain])
+ domain_name = domain.lower()
+ req = commands.CheckDomain([domain_name])
return registry.send(req, cleaned=True).res_data[0].avail
@classmethod
From 1a2b16a3da67ed340ca3246a61a6a66fc7523f64 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Thu, 7 Dec 2023 14:46:21 -0500
Subject: [PATCH 37/54] WIP on more oidc testing
---
src/djangooidc/tests/test_views.py | 42 +++++++++++++++++++++++++++---
src/djangooidc/views.py | 5 +++-
2 files changed, 42 insertions(+), 5 deletions(-)
diff --git a/src/djangooidc/tests/test_views.py b/src/djangooidc/tests/test_views.py
index fef567396..79266430d 100644
--- a/src/djangooidc/tests/test_views.py
+++ b/src/djangooidc/tests/test_views.py
@@ -1,9 +1,9 @@
from unittest.mock import MagicMock, patch
-from django.http import HttpResponse
+from django.http import HttpResponse, HttpResponseRedirect
from django.test import Client, TestCase, RequestFactory
from django.urls import reverse
-from ..views import login_callback
+from ..views import login_callback, requires_step_up_auth
from .common import less_console_noise
@@ -61,11 +61,44 @@ class ViewsTest(TestCase):
# mock
mock_client.callback.side_effect = self.user_info
# test
- with less_console_noise():
+ with patch("djangooidc.views.requires_step_up_auth", return_value=False), \
+ less_console_noise():
response = self.client.get(reverse("openid_login_callback"))
# assert
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("logout"))
+
+ def test_login_callback_no_step_up_auth(self, mock_client):
+ # setup
+ session = self.client.session
+ session.save()
+ # mock
+ mock_client.callback.side_effect = self.user_info
+ # test
+ with patch("djangooidc.views.requires_step_up_auth", return_value=False), \
+ less_console_noise():
+ response = self.client.get(reverse("openid_login_callback"))
+ # assert
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.url, "/")
+
+ @patch.object(requires_step_up_auth, return_value=True)
+ def test_login_callback_requires_step_up_auth(self, mock_client):
+ # setup
+ callback_url = reverse("openid_login_callback")
+ # session = self.client.session
+ # session.save()
+ # mock
+ # mock_client.callback.side_effect = self.user_info
+ # mock_client.create_authn_request.side_effect = self.say_hi
+ # test
+ # with patch("djangooidc.views.requires_step_up_auth", return_value=True):
+
+ response = self.client.get(reverse("openid_login_callback"))
+
+ # assert
+ # self.assertEqual(response.status_code, 200)
+ # self.assertContains(response, "Hi")
def test_requires_step_up_auth(self, mock_client):
# Configure the mock to return an expected value for get_step_up_acr_value
@@ -108,7 +141,8 @@ class ViewsTest(TestCase):
mock_client.callback.side_effect = self.user_info
mock_auth.return_value = None
# test
- with less_console_noise():
+ with patch("djangooidc.views.requires_step_up_auth", return_value=False), \
+ less_console_noise():
response = self.client.get(reverse("openid_login_callback"))
# assert
self.assertEqual(response.status_code, 401)
diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py
index 31f84d41f..2c8e75bb3 100644
--- a/src/djangooidc/views.py
+++ b/src/djangooidc/views.py
@@ -70,10 +70,13 @@ def login_callback(request):
userinfo = CLIENT.callback(query, request.session)
# test for need for identity verification and if it is satisfied
# if not satisfied, redirect user to login with stepped up acr_value
+ logger.info('login_callback start')
if requires_step_up_auth(userinfo):
# add acr_value to request.session
+ logger.info('login_callback inside requires_step_up_auth')
request.session["acr_value"] = CLIENT.get_step_up_acr_value()
- return CLIENT.create_authn_request(request.session)
+ logger.info('login_callback after get_step_up_acr_value')
+ # return CLIENT.create_authn_request(request.session)
user = authenticate(request=request, **userinfo)
if user:
login(request, user)
From f2a3aa5a45e88c0c0430eb989a25b422b34eddae Mon Sep 17 00:00:00 2001
From: Erin <121973038+erinysong@users.noreply.github.com>
Date: Thu, 7 Dec 2023 11:47:23 -0800
Subject: [PATCH 38/54] Update availability tests to reflect case sensitivity
---
src/api/tests/test_available.py | 4 ++--
src/registrar/tests/common.py | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/api/tests/test_available.py b/src/api/tests/test_available.py
index 9de152b06..319ab4eb3 100644
--- a/src/api/tests/test_available.py
+++ b/src/api/tests/test_available.py
@@ -69,8 +69,8 @@ class AvailableViewTest(MockEppLib):
self.assertTrue(check_domain_available("igorville.gov"))
# input is lowercased so GSA.GOV should also not be available
self.assertFalse(check_domain_available("GSA.gov"))
- # input is lowercased so IGORVILLE.GOV should also not be available
- self.assertFalse(check_domain_available("IGORVILLE.gov"))
+ # input is lowercased so IGORVILLE.GOV should also be available
+ self.assertTrue(check_domain_available("IGORVILLE.gov"))
def test_domain_available_dotgov(self):
"""Domain searches work without trailing .gov"""
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index f17e0f9fa..f07175902 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -832,7 +832,7 @@ class MockEppLib(TestCase):
elif "GSA.gov" in getattr(_request, "names", None):
return self._mockDomainName("GSA.gov", False)
elif "igorville.gov" in getattr(_request, "names", None):
- return self._mockDomainName("igorvilleremixed.gov", True)
+ return self._mockDomainName("igorville.gov", True)
elif "top-level-agency.gov" in getattr(_request, "names", None):
return self._mockDomainName("top-level-agency.gov", True)
elif "city.gov" in getattr(_request, "names", None):
From 1001454a856a2edd002f295aeafd023953082d91 Mon Sep 17 00:00:00 2001
From: Rachid Mrad
Date: Thu, 7 Dec 2023 16:33:35 -0500
Subject: [PATCH 39/54] More oidc tests
test_login_callback_requires_step_up_auth and
test_login_callback_no_step_up_auth, lint
---
src/djangooidc/oidc.py | 4 +-
src/djangooidc/tests/test_views.py | 90 +++++++++++++++++++-----------
src/djangooidc/views.py | 12 ++--
src/registrar/models/user.py | 23 ++++----
src/registrar/tests/test_models.py | 10 ++--
5 files changed, 82 insertions(+), 57 deletions(-)
diff --git a/src/djangooidc/oidc.py b/src/djangooidc/oidc.py
index 798771da2..91bfddc66 100644
--- a/src/djangooidc/oidc.py
+++ b/src/djangooidc/oidc.py
@@ -147,7 +147,7 @@ class Client(oic.Client):
raise o_e.InternalError(locator=state)
return response
-
+
def callback(self, unparsed_response, session):
"""Step 3: Receive OP's response, request an access token, and user info."""
logger.debug("Processing the OpenID Connect callback response...")
@@ -276,7 +276,7 @@ class Client(oic.Client):
"""returns the step_up_acr_value from settings
this helper function is called from djangooidc views"""
return self.behaviour.get("step_up_acr_value")
-
+
def __repr__(self):
return "Client {} {} {}".format(
self.client_id,
diff --git a/src/djangooidc/tests/test_views.py b/src/djangooidc/tests/test_views.py
index 79266430d..3ae3bef82 100644
--- a/src/djangooidc/tests/test_views.py
+++ b/src/djangooidc/tests/test_views.py
@@ -1,9 +1,9 @@
from unittest.mock import MagicMock, patch
-from django.http import HttpResponse, HttpResponseRedirect
+from django.http import HttpResponse
from django.test import Client, TestCase, RequestFactory
from django.urls import reverse
-from ..views import login_callback, requires_step_up_auth
+from ..views import login_callback
from .common import less_console_noise
@@ -61,46 +61,32 @@ class ViewsTest(TestCase):
# mock
mock_client.callback.side_effect = self.user_info
# test
- with patch("djangooidc.views.requires_step_up_auth", return_value=False), \
- less_console_noise():
+ with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise():
response = self.client.get(reverse("openid_login_callback"))
# assert
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("logout"))
-
+
def test_login_callback_no_step_up_auth(self, mock_client):
+ """Walk through login_callback when requires_step_up_auth returns False
+ and assert that we have a redirect to /"""
# setup
session = self.client.session
session.save()
# mock
mock_client.callback.side_effect = self.user_info
# test
- with patch("djangooidc.views.requires_step_up_auth", return_value=False), \
- less_console_noise():
+ with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise():
response = self.client.get(reverse("openid_login_callback"))
# assert
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/")
-
- @patch.object(requires_step_up_auth, return_value=True)
- def test_login_callback_requires_step_up_auth(self, mock_client):
- # setup
- callback_url = reverse("openid_login_callback")
- # session = self.client.session
- # session.save()
- # mock
- # mock_client.callback.side_effect = self.user_info
- # mock_client.create_authn_request.side_effect = self.say_hi
- # test
- # with patch("djangooidc.views.requires_step_up_auth", return_value=True):
-
- response = self.client.get(reverse("openid_login_callback"))
-
- # assert
- # self.assertEqual(response.status_code, 200)
- # self.assertContains(response, "Hi")
-
+
def test_requires_step_up_auth(self, mock_client):
+ """Invoke login_callback passing it a request when requires_step_up_auth returns True
+ and assert that session is updated and create_authn_request (mock) is called.
+
+ Possibly redundant with test_login_callback_no_step_up_auth"""
# Configure the mock to return an expected value for get_step_up_acr_value
mock_client.return_value.get_step_up_acr_value.return_value = "step_up_acr_value"
@@ -110,29 +96,35 @@ class ViewsTest(TestCase):
# Ensure that the CLIENT instance used in login_callback is the mock
# patch requires_step_up_auth to return True
- with patch("djangooidc.views.requires_step_up_auth", return_value=True), \
- patch("djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock()) as mock_create_authn_request:
+ with patch("djangooidc.views.requires_step_up_auth", return_value=True), patch(
+ "djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock()
+ ) as mock_create_authn_request:
login_callback(request)
# Assert that get_step_up_acr_value was called and session was updated
self.assertNotEqual(request.session["acr_value"], "")
# And create_authn_request was called again
mock_create_authn_request.assert_called_once()
-
+
def test_does_not_requires_step_up_auth(self, mock_client):
+ """Invoke login_callback passing it a request when requires_step_up_auth returns False
+ and assert that session is not updated and create_authn_request (mock) is not called.
+
+ Possibly redundant with test_login_callback_requires_step_up_auth"""
# Create a mock request
request = self.factory.get("/some-url")
request.session = {"acr_value": ""}
# Ensure that the CLIENT instance used in login_callback is the mock
# patch requires_step_up_auth to return False
- with patch("djangooidc.views.requires_step_up_auth", return_value=False), \
- patch("djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock()) as mock_create_authn_request:
+ with patch("djangooidc.views.requires_step_up_auth", return_value=False), patch(
+ "djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock()
+ ) as mock_create_authn_request:
login_callback(request)
# Assert that get_step_up_acr_value was NOT called and session was NOT updated
self.assertEqual(request.session["acr_value"], "")
- # create_authn_request was not called
+ # create_authn_request was not called
mock_create_authn_request.assert_not_called()
@patch("djangooidc.views.authenticate")
@@ -141,8 +133,7 @@ class ViewsTest(TestCase):
mock_client.callback.side_effect = self.user_info
mock_auth.return_value = None
# test
- with patch("djangooidc.views.requires_step_up_auth", return_value=False), \
- less_console_noise():
+ with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise():
response = self.client.get(reverse("openid_login_callback"))
# assert
self.assertEqual(response.status_code, 401)
@@ -189,3 +180,34 @@ class ViewsTest(TestCase):
# assert
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("logout"))
+
+
+class ViewsTestUnpatched(TestCase):
+ def setUp(self):
+ self.client = Client()
+ self.factory = RequestFactory()
+
+ def say_hi(*args):
+ return HttpResponse("Hi")
+
+ def user_info(*args):
+ return {
+ "sub": "TEST",
+ "email": "test@example.com",
+ "first_name": "Testy",
+ "last_name": "Tester",
+ "phone": "814564000",
+ }
+
+ def test_login_callback_requires_step_up_auth(self):
+ """Walk through login_callback when requires_step_up_auth returns True
+ and assert that create_authn_request is returned."""
+
+ with patch("djangooidc.views.requires_step_up_auth", return_value=True), patch(
+ "djangooidc.views.Client.callback", return_value=self.user_info
+ ), patch("djangooidc.views.Client.create_authn_request", side_effect=self.say_hi):
+ response = self.client.get(reverse("openid_login_callback"))
+
+ # assert
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "Hi")
diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py
index 2c8e75bb3..f354a43b4 100644
--- a/src/djangooidc/views.py
+++ b/src/djangooidc/views.py
@@ -70,13 +70,10 @@ def login_callback(request):
userinfo = CLIENT.callback(query, request.session)
# test for need for identity verification and if it is satisfied
# if not satisfied, redirect user to login with stepped up acr_value
- logger.info('login_callback start')
if requires_step_up_auth(userinfo):
# add acr_value to request.session
- logger.info('login_callback inside requires_step_up_auth')
request.session["acr_value"] = CLIENT.get_step_up_acr_value()
- logger.info('login_callback after get_step_up_acr_value')
- # return CLIENT.create_authn_request(request.session)
+ return CLIENT.create_authn_request(request.session)
user = authenticate(request=request, **userinfo)
if user:
login(request, user)
@@ -87,15 +84,17 @@ def login_callback(request):
except Exception as err:
return error_page(request, err)
+
def requires_step_up_auth(userinfo):
- """ if User.needs_identity_verification and step_up_acr_value not in
- ial returned from callback, return True """
+ """if User.needs_identity_verification and step_up_acr_value not in
+ ial returned from callback, return True"""
step_up_acr_value = CLIENT.get_step_up_acr_value()
acr_value = userinfo.get("ial", "")
uuid = userinfo.get("sub", "")
email = userinfo.get("email", "")
return User.needs_identity_verification(email, uuid) and acr_value != step_up_acr_value
+
def logout(request, next_page=None):
"""Redirect the user to the authentication provider (OP) logout page."""
try:
@@ -125,6 +124,7 @@ def logout(request, next_page=None):
if next_page:
request.session["next"] = next_page
+
def logout_callback(request):
"""Simple redirection view: after logout, redirect to `next`."""
next = request.session.get("next", "/")
diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py
index b4f2c08ab..ae278ef1b 100644
--- a/src/registrar/models/user.py
+++ b/src/registrar/models/user.py
@@ -70,29 +70,32 @@ class User(AbstractUser):
def needs_identity_verification(cls, email, uuid):
"""A method used by our oidc classes to test whether a user needs email/uuid verification
or the full identity PII verification"""
-
- # An existing user who is a domain manager of a domain (that is,
- # they have an entry in UserDomainRole for their User)
- try:
+
+ # An existing user who is a domain manager of a domain (that is,
+ # they have an entry in UserDomainRole for their User)
+ try:
existing_user = cls.objects.get(username=uuid)
if existing_user and UserDomainRole.objects.filter(user=existing_user).exists():
return False
- except:
+ except cls.DoesNotExist:
+ # Do nothing when the user is not found, as we're checking for existence.
pass
-
+ except Exception as err:
+ raise err
+
# A new incoming user who is a domain manager for one of the domains
# that we inputted from Verisign (that is, their email address appears
- # in the username field of a TransitionDomain)
+ # in the username field of a TransitionDomain)
if TransitionDomain.objects.filter(username=email).exists():
return False
-
+
# A new incoming user who is being invited to be a domain manager (that is,
# their email address is in DomainInvitation for an invitation that is not yet "retrieved").
if DomainInvitation.objects.filter(email=email, status=DomainInvitation.INVITED):
return False
-
+
return True
-
+
def check_domain_invitations_on_login(self):
"""When a user first arrives on the site, we need to retrieve any domain
invitations that match their email address."""
diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py
index 0add94ce6..ef65aa24d 100644
--- a/src/registrar/tests/test_models.py
+++ b/src/registrar/tests/test_models.py
@@ -613,7 +613,7 @@ class TestUser(TestCase):
self.email = "mayor@igorville.gov"
self.domain_name = "igorvilleInTransition.gov"
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
- self.user, _ = User.objects.get_or_create(email=self.email)
+ self.user, _ = User.objects.get_or_create(email=self.email)
def tearDown(self):
super().tearDown()
@@ -632,25 +632,25 @@ class TestUser(TestCase):
are created."""
self.user.on_each_login()
self.assertFalse(Domain.objects.filter(name=self.domain_name).exists())
-
+
def test_identity_verification_with_domain_manager(self):
"""A domain manager should return False when tested with class
method needs_identity_verification"""
UserDomainRole.objects.get_or_create(user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username))
-
+
def test_identity_verification_with_transition_user(self):
"""A user from the Verisign transition should return False
when tested with class method needs_identity_verification"""
TransitionDomain.objects.get_or_create(username=self.user.email, domain_name=self.domain_name)
self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username))
-
+
def test_identity_verification_with_invited_user(self):
"""An invited user should return False when tested with class
method needs_identity_verification"""
DomainInvitation.objects.get_or_create(email=self.user.email, domain=self.domain)
self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username))
-
+
def test_identity_verification_with_new_user(self):
"""A new user who's neither transitioned nor invited should
return True when tested with class method needs_identity_verification"""
From 527639e7c7e4d6504a2d61b94859cc0d8111eb51 Mon Sep 17 00:00:00 2001
From: David Kennedy
Date: Fri, 8 Dec 2023 05:50:47 -0500
Subject: [PATCH 40/54] changed DS Data to DS data in all places, including
comments
---
src/registrar/forms/common.py | 4 ++--
src/registrar/forms/domain.py | 2 +-
src/registrar/templates/domain_dsdata.html | 12 +++++------
src/registrar/templates/domain_sidebar.html | 2 +-
src/registrar/tests/test_views.py | 22 ++++++++++-----------
src/registrar/views/domain.py | 6 +++---
6 files changed, 24 insertions(+), 24 deletions(-)
diff --git a/src/registrar/forms/common.py b/src/registrar/forms/common.py
index 3ab36cf6f..e2a234e71 100644
--- a/src/registrar/forms/common.py
+++ b/src/registrar/forms/common.py
@@ -1,6 +1,6 @@
# common.py
#
-# ALGORITHM_CHOICES are options for alg attribute in DS Data
+# ALGORITHM_CHOICES are options for alg attribute in DS data
# reference:
# https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml
ALGORITHM_CHOICES = [
@@ -18,7 +18,7 @@ ALGORITHM_CHOICES = [
(15, "(15) Ed25519"),
(16, "(16) Ed448"),
]
-# DIGEST_TYPE_CHOICES are options for digestType attribute in DS Data
+# DIGEST_TYPE_CHOICES are options for digestType attribute in DS data
# reference: https://datatracker.ietf.org/doc/html/rfc4034#appendix-A.2
DIGEST_TYPE_CHOICES = [
(1, "(1) SHA-1"),
diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py
index ff41b9268..112552e5f 100644
--- a/src/registrar/forms/domain.py
+++ b/src/registrar/forms/domain.py
@@ -302,7 +302,7 @@ class DomainDnssecForm(forms.Form):
class DomainDsdataForm(forms.Form):
- """Form for adding or editing DNSSEC DS Data to a domain."""
+ """Form for adding or editing DNSSEC DS data to a domain."""
def validate_hexadecimal(value):
"""
diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html
index d049675fc..15343413b 100644
--- a/src/registrar/templates/domain_dsdata.html
+++ b/src/registrar/templates/domain_dsdata.html
@@ -1,13 +1,13 @@
{% extends "domain_base.html" %}
{% load static field_helpers url_helpers %}
-{% block title %}DS Data | {{ domain.name }} | {% endblock %}
+{% block title %}DS data | {{ domain.name }} | {% endblock %}
{% block domain_content %}
{% if domain.dnssecdata is None %}
- You have no DS Data added. Enable DNSSEC by adding DS Data.
+ You have no DS data added. Enable DNSSEC by adding DS data.
{% endif %}
@@ -16,11 +16,11 @@
{% include "includes/form_errors.html" with form=form %}
{% endfor %}
- DS Data
+ DS data
In order to enable DNSSEC, you must first configure it with your DNS hosting service.
- Enter the values given by your DNS provider for DS Data.
+ Enter the values given by your DNS provider for DS data.
{% include "includes/required_fields.html" %}
@@ -31,9 +31,9 @@
{% for form in formset %}
|